---
title: "Actions"
description: "Configure Android action workflows with editor callbacks and events."
platform: android
url: "https://img.ly/docs/cesdk/android/actions-6ch24x/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Actions](https://img.ly/docs/cesdk/android/actions-6ch24x/)
---
```kotlin file=@cesdk_android_examples/editor-guides-actions/ActionsEditorSolution.kt reference-only
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.editor.Editor
import ly.img.editor.core.component.EditorComponent
import ly.img.editor.core.component.remember
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.event.EditorEvent
import ly.img.editor.core.library.data.UploadAssetSourceType
import ly.img.engine.AssetDefinition
import ly.img.engine.Engine
import ly.img.engine.MimeType
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException
data class ActionsExportState(
val isExporting: Boolean = false,
val exportedFileName: String? = null,
val errorMessage: String? = null,
)
object ActionsExportStarted : EditorEvent
data class ActionsExportCompleted(
val fileName: String,
) : EditorEvent
data class ActionsExportFailed(
val message: String,
) : EditorEvent
@Composable
fun ActionsEditorSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration.remember {
var exportState by editorContext.mutableStateOf(
key = "actions.export.state",
initial = ActionsExportState(),
)
onExport = {
editorContext.eventHandler.send(ActionsExportStarted)
try {
val file = exportCurrentSceneToCache(
engine = editorContext.engine,
directory = editorContext.activity.cacheDir,
)
editorContext.eventHandler.send(ActionsExportCompleted(file.name))
} catch (throwable: CancellationException) {
throw throwable
} catch (throwable: Throwable) {
editorContext.eventHandler.send(
ActionsExportFailed(throwable.message ?: throwable.toString()),
)
}
}
onEvent = { event ->
exportState = when (event) {
is ActionsExportStarted -> {
ActionsExportState(isExporting = true)
}
is ActionsExportCompleted -> {
ActionsExportState(exportedFileName = event.fileName)
}
is ActionsExportFailed -> {
ActionsExportState(errorMessage = event.message)
}
is EditorEvent.Export.Cancel -> {
ActionsExportState()
}
else -> exportState
}
}
onUpload = { assetDefinition, uploadSource ->
uploadTransientResource(
assetDefinition = assetDefinition,
uploadSource = uploadSource,
)
}
overlay = {
EditorComponent.remember {
decoration = {
Box(
modifier = Modifier.fillMaxSize(),
) {
Button(
modifier =
Modifier
.align(Alignment.BottomCenter)
.padding(24.dp),
enabled = !exportState.isExporting,
onClick = {
editorContext.eventHandler.send(EditorEvent.Export.Start())
},
) {
Text("Export")
}
}
if (exportState.isExporting) {
AlertDialog(
onDismissRequest = {},
title = {
Text("Exporting")
},
text = {
CircularProgressIndicator()
},
confirmButton = {},
dismissButton = {
TextButton(
onClick = {
editorContext.eventHandler.send(EditorEvent.Export.Cancel())
},
) {
Text("Cancel")
}
},
properties =
DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
)
}
exportState.exportedFileName?.let { fileName ->
AlertDialog(
onDismissRequest = {
exportState = ActionsExportState()
},
title = {
Text("Export complete")
},
text = {
Text("Created $fileName in the app cache directory.")
},
confirmButton = {
TextButton(
onClick = {
exportState = ActionsExportState()
},
) {
Text("OK")
}
},
)
}
exportState.errorMessage?.let { errorMessage ->
AlertDialog(
onDismissRequest = {
exportState = ActionsExportState()
},
title = {
Text("Export failed")
},
text = {
Text(errorMessage)
},
confirmButton = {
TextButton(
onClick = {
exportState = ActionsExportState()
},
) {
Text("OK")
}
},
)
}
}
}
}
}
},
onClose = onClose,
)
}
private suspend fun exportCurrentSceneToCache(
engine: Engine,
directory: File,
): File {
val scene = requireNotNull(engine.scene.get()) { "No scene loaded for export." }
val buffer = engine.block.export(
block = scene,
mimeType = MimeType.PDF,
)
return writeToCacheFile(
byteBuffer = buffer,
directory = directory,
mimeType = MimeType.PDF,
)
}
private suspend fun writeToCacheFile(
byteBuffer: ByteBuffer,
directory: File,
mimeType: MimeType,
): File = withContext(Dispatchers.IO) {
val extension = when (mimeType) {
MimeType.PNG -> "png"
MimeType.JPEG -> "jpg"
MimeType.TGA -> "tga"
MimeType.SVG -> "svg"
MimeType.MP4 -> "mp4"
MimeType.BINARY -> "bin"
MimeType.PDF -> "pdf"
}
val outputFile = File.createTempFile(UUID.randomUUID().toString(), ".$extension", directory)
val sourceBuffer = byteBuffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (sourceBuffer.hasRemaining()) {
channel.write(sourceBuffer)
}
}
check(outputFile.length() > 0L) { "Exported file is empty." }
outputFile
}
private suspend fun uploadTransientResource(
assetDefinition: AssetDefinition,
uploadSource: UploadAssetSourceType,
): AssetDefinition {
val meta = assetDefinition.meta ?: return assetDefinition
val localUri = meta["uri"] ?: return assetDefinition
val permanentUri = uploadToPermanentStorage(
uri = localUri,
sourceId = uploadSource.sourceId,
)
val permanentMeta = meta.toMutableMap()
permanentMeta["uri"] = permanentUri
meta["thumbUri"]?.let { thumbnailUri ->
permanentMeta["thumbUri"] = if (thumbnailUri == localUri) {
permanentUri
} else {
uploadToPermanentStorage(
uri = thumbnailUri,
sourceId = uploadSource.sourceId,
)
}
}
return assetDefinition.copy(
meta = permanentMeta,
)
}
private suspend fun uploadToPermanentStorage(
uri: String,
sourceId: String,
): String {
check(sourceId.isNotBlank()) { "Upload source id is required." }
// Replace this with your app's storage client and return its permanent URI.
return uri
}
```
Configure action-style workflows in Android editors by dispatching
`EditorEvent`s and handling them with `EditorConfiguration` callbacks.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-actions)
Android does not expose the Web `cesdk.actions` registry or `cesdk.utils` API.
Use the Android editor configuration instead: UI components send events, and
callbacks own export, upload, analytics, and app-specific workflow steps.
The snippets below use the editor-provided `editorContext.engine`; the editor
owns engine initialization and lifecycle.
> **Note:** If you need a complete product surface around these hooks, start from a
> [Starter Kit](https://img.ly/docs/cesdk/android/starterkits-kxg120/) that matches your editor type and apply
> the same `EditorConfiguration` callbacks to your editor configuration.
## Action Hooks on Android
Action-style work on Android is split between callback hooks and event
dispatch. Configure the hooks in `EditorConfiguration.remember`.
| Hook | When it runs | Typical use |
| --- | --- | --- |
| `onCreate` | When the editor and engine are created | Load or build a scene and prepare asset sources. |
| `onLoaded` | After `onCreate` finishes on first launch or after process recreation | Collect editor flows or run setup that depends on a loaded editor. |
| `onExport` | After `EditorEvent.Export.Start()` is sent | Export, validate, upload, or hand the result to your app. |
| `onUpload` | After the user selects a file for an upload asset source | Replace temporary local URIs with permanent app storage URIs. |
| `onEvent` | Whenever an editor or custom event is sent | Reduce events into UI state, analytics, or follow-up work. |
| `onClose` | After `EditorEvent.OnClose` is sent or the system back action is unhandled | Confirm close behavior or route the close request to your app UI. |
| `onError` | When the editor captures a `Throwable` | Inspect the `Throwable` and route failures to your app UI. |
The default export callback only logs a warning. Add `onExport` whenever an
export action should produce, save, share, or upload a file.
## Trigger an Action from UI
Send `EditorEvent.Export.Start()` from your own Compose UI to start the export
workflow. The editor's built-in export controls use the same event path.
```kotlin highlight-android-trigger-export
Button(
modifier =
Modifier
.align(Alignment.BottomCenter)
.padding(24.dp),
enabled = !exportState.isExporting,
onClick = {
editorContext.eventHandler.send(EditorEvent.Export.Start())
},
) {
Text("Export")
}
```
The event is routed through the editor event handler and invokes `onExport`.
When your UI shows progress, send `EditorEvent.Export.Cancel()` to cancel the
running export job.
```kotlin highlight-android-cancel-export
TextButton(
onClick = {
editorContext.eventHandler.send(EditorEvent.Export.Cancel())
},
) {
Text("Cancel")
}
```
## Handle Exports
Keep custom events small and specific to the workflow state you want to display
or track.
```kotlin highlight-android-custom-events
object ActionsExportStarted : EditorEvent
data class ActionsExportCompleted(
val fileName: String,
) : EditorEvent
data class ActionsExportFailed(
val message: String,
) : EditorEvent
```
Implement `onExport` inside `EditorConfiguration.remember`. This sample exports
the current scene to PDF, writes it to app cache, and sends a success or failure
event back to the editor.
```kotlin highlight-android-handle-export
onExport = {
editorContext.eventHandler.send(ActionsExportStarted)
try {
val file = exportCurrentSceneToCache(
engine = editorContext.engine,
directory = editorContext.activity.cacheDir,
)
editorContext.eventHandler.send(ActionsExportCompleted(file.name))
} catch (throwable: CancellationException) {
throw throwable
} catch (throwable: Throwable) {
editorContext.eventHandler.send(
ActionsExportFailed(throwable.message ?: throwable.toString()),
)
}
}
```
Those custom events are reduced in `onEvent` below. The reducer turns
`ActionsExportStarted`, `ActionsExportCompleted`, and `ActionsExportFailed` into
state that the overlay uses to show progress, completion, or error dialogs.
The export helper reads the active scene from the editor engine and exports it
with `engine.block.export(...)`.
```kotlin highlight-android-export-helper
private suspend fun exportCurrentSceneToCache(
engine: Engine,
directory: File,
): File {
val scene = requireNotNull(engine.scene.get()) { "No scene loaded for export." }
val buffer = engine.block.export(
block = scene,
mimeType = MimeType.PDF,
)
return writeToCacheFile(
byteBuffer = buffer,
directory = directory,
mimeType = MimeType.PDF,
)
}
```
Write the returned `ByteBuffer` to app-controlled storage before sharing,
uploading, or handing it to another Android component.
```kotlin highlight-android-write-cache-file
private suspend fun writeToCacheFile(
byteBuffer: ByteBuffer,
directory: File,
mimeType: MimeType,
): File = withContext(Dispatchers.IO) {
val extension = when (mimeType) {
MimeType.PNG -> "png"
MimeType.JPEG -> "jpg"
MimeType.TGA -> "tga"
MimeType.SVG -> "svg"
MimeType.MP4 -> "mp4"
MimeType.BINARY -> "bin"
MimeType.PDF -> "pdf"
}
val outputFile = File.createTempFile(UUID.randomUUID().toString(), ".$extension", directory)
val sourceBuffer = byteBuffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (sourceBuffer.hasRemaining()) {
channel.write(sourceBuffer)
}
}
check(outputFile.length() > 0L) { "Exported file is empty." }
outputFile
}
```
## Reflect Results in the UI
Represent action state with a small data class. Store it with
`editorContext.mutableStateOf(...)` so it survives configuration changes while
the editor is open; use `editorContext.stateOf(...)` when another custom
component needs read-only access to the same keyed state. Then reduce editor
events into that state in `onEvent`.
```kotlin highlight-android-action-state
data class ActionsExportState(
val isExporting: Boolean = false,
val exportedFileName: String? = null,
val errorMessage: String? = null,
)
```
```kotlin highlight-android-remember-state
var exportState by editorContext.mutableStateOf(
key = "actions.export.state",
initial = ActionsExportState(),
)
```
```kotlin highlight-android-handle-events
onEvent = { event ->
exportState = when (event) {
is ActionsExportStarted -> {
ActionsExportState(isExporting = true)
}
is ActionsExportCompleted -> {
ActionsExportState(exportedFileName = event.fileName)
}
is ActionsExportFailed -> {
ActionsExportState(errorMessage = event.message)
}
is EditorEvent.Export.Cancel -> {
ActionsExportState()
}
else -> exportState
}
}
```
`onEvent` receives both built-in editor events and your custom event classes.
Use it for lightweight state updates, analytics, or follow-up workflow routing.
## Customize Uploads
Use `onUpload` to replace the Web `uploadFile` action pattern on Android. The
callback receives the generated `AssetDefinition` and the upload source that
triggered it.
```kotlin highlight-android-handle-upload
onUpload = { assetDefinition, uploadSource ->
uploadTransientResource(
assetDefinition = assetDefinition,
uploadSource = uploadSource,
)
}
```
Upload the local URI to your own storage client, then return an updated
`AssetDefinition` with permanent metadata. If the asset has a separate
`thumbUri`, upload that thumbnail separately instead of pointing it at the
media file.
```kotlin highlight-android-upload-helper
private suspend fun uploadTransientResource(
assetDefinition: AssetDefinition,
uploadSource: UploadAssetSourceType,
): AssetDefinition {
val meta = assetDefinition.meta ?: return assetDefinition
val localUri = meta["uri"] ?: return assetDefinition
val permanentUri = uploadToPermanentStorage(
uri = localUri,
sourceId = uploadSource.sourceId,
)
val permanentMeta = meta.toMutableMap()
permanentMeta["uri"] = permanentUri
meta["thumbUri"]?.let { thumbnailUri ->
permanentMeta["thumbUri"] = if (thumbnailUri == localUri) {
permanentUri
} else {
uploadToPermanentStorage(
uri = thumbnailUri,
sourceId = uploadSource.sourceId,
)
}
}
return assetDefinition.copy(
meta = permanentMeta,
)
}
private suspend fun uploadToPermanentStorage(
uri: String,
sourceId: String,
): String {
check(sourceId.isNotBlank()) { "Upload source id is required." }
// Replace this with your app's storage client and return its permanent URI.
return uri
}
```
## API Reference
| API | Purpose |
| --- | --- |
| `EditorConfiguration.remember { onExport = { ... } }` | Handles export actions triggered by `EditorEvent.Export.Start()`. |
| `EditorConfiguration.remember { onEvent = { event -> ... } }` | Observes built-in and custom editor events. |
| `EditorConfiguration.remember { onUpload = { assetDefinition, uploadSource -> ... } }` | Customizes assets selected through upload sources. |
| `editorContext.mutableStateOf(key=_, initial=_)` | Stores editor-scoped Compose state that survives configuration changes. |
| `editorContext.stateOf(key=_)` | Reads state previously declared with `editorContext.mutableStateOf(...)`. |
| `editorContext.eventHandler.send(event=_)` | Dispatches an `EditorEvent` from callbacks or editor UI components. |
| `editorContext.engine` | Provides the editor-owned engine for export work inside callbacks. |
| `editorContext.activity` | Provides the Android `Activity`, used here for `cacheDir`. |
| `EditorEvent.Export.Start()` | Starts the configured export workflow. |
| `EditorEvent.Export.Cancel()` | Cancels the running export job, if one exists. |
| `engine.scene.get()` | Reads the currently loaded scene block. |
| `engine.block.export(block=_, mimeType=_)` | Exports a scene, page, or block to the requested MIME type. |
| `assetDefinition.meta` | Reads the uploaded asset metadata generated by the editor. |
| `uploadSource.sourceId` | Identifies the upload asset source that triggered `onUpload`. |
| `AssetDefinition.copy(meta=_)` | Returns an updated asset definition from `onUpload`. |
Related types:
| Type | Role |
| --- | --- |
| `EditorEvent` | Base interface for built-in and custom editor events. |
| `UploadAssetSourceType` | Describes the upload asset source that invoked `onUpload`. |
| `MimeType` | Provides Android constants such as `MimeType.PDF`. |
## Next Steps
- [To PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - customize PDF export behavior from the
editor.
- [UI Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) - observe editor events and callback
lifecycle details.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Animation"
description: "Add motion to designs with support for keyframes, timeline editing, and programmatic animation control."
platform: android
url: "https://img.ly/docs/cesdk/android/animation-ce900c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/)
---
---
## Related Pages
- [Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) - Add motion to video scenes with preset animation controls and programmatic animation APIs.
- [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) - Apply different animation types to design blocks in CE.SDK and configure their properties.
- [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) - Build entrance, exit, loop, and text animations with CE.SDK's Android CreativeEngine APIs.
- [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) - Modify Android CE.SDK animations by reading properties, changing duration and easing, adjusting direction, and replacing or removing animation blocks.
- [Programmatic Animations](https://img.ly/docs/cesdk/android/animation/programmatic-eb359c/) - Control Android video-scene animations with typed CreativeEngine animation APIs.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Animations"
description: "Build entrance, exit, loop, and text animations with CE.SDK's Android CreativeEngine APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/create-15cf50/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-animations/CreateAnimations.kt reference-only
import ly.img.engine.AnimationEasingType
import ly.img.engine.AnimationType
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
data class CreateAnimations(
val supportsGraphicAnimation: Boolean,
val slideDirection: Float,
val slideEasing: String,
val entranceDuration: Double,
val exitDuration: Double,
val loopAnimationType: String,
val textWritingStyle: String,
val textOverlap: Float,
val replacedLoopAnimationIsValid: Boolean,
)
fun createAnimations(engine: Engine): CreateAnimations {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1080F)
engine.block.setDuration(block = page, duration = 5.0)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = 180F)
engine.block.setPositionY(block, value = 180F)
engine.block.setWidth(block, value = 360F)
engine.block.setHeight(block, value = 360F)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = block, fill = fill)
engine.block.setFillSolidColor(block = block, color = Color.fromRGBA(r = 0.12F, g = 0.44F, b = 0.95F, a = 1F))
engine.block.appendChild(parent = page, child = block)
val supportsGraphicAnimation = engine.block.supportsAnimation(block)
if (!supportsGraphicAnimation) {
error("Graphic block does not support animations.")
}
val slideIn = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = block, animation = slideIn)
engine.block.setDuration(block = slideIn, duration = 1.0)
val fadeOut = engine.block.createAnimation(AnimationType.Fade)
engine.block.setOutAnimation(block = block, animation = fadeOut)
engine.block.setDuration(block = fadeOut, duration = 0.75)
engine.block.setEnum(
block = fadeOut,
property = "animationEasing",
value = AnimationEasingType.EASE_IN.key,
)
val breathingLoop = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setLoopAnimation(block = block, animation = breathingLoop)
engine.block.setDuration(block = breathingLoop, duration = 2.0)
check(engine.block.getDuration(breathingLoop) == 2.0)
val slideProperties = engine.block.findAllProperties(slideIn)
if ("animation/slide/direction" in slideProperties) {
engine.block.setFloat(
block = slideIn,
property = "animation/slide/direction",
value = 1.5F * Math.PI.toFloat(),
)
}
val easingOptions = engine.block.getEnumValues(enumProperty = "animationEasing")
check(AnimationEasingType.EASE_OUT.key in easingOptions)
engine.block.setEnum(
block = slideIn,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.setPositionX(textBlock, value = 160F)
engine.block.setPositionY(textBlock, value = 620F)
engine.block.setWidth(textBlock, value = 720F)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
engine.block.replaceText(
textBlock,
"Create animations\nline by line,\nword by word,\nor character by character.",
)
check(engine.block.supportsAnimation(textBlock))
val groupedTextBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = groupedTextBlock)
engine.block.setPositionX(groupedTextBlock, value = 160F)
engine.block.setPositionY(groupedTextBlock, value = 820F)
engine.block.setWidth(groupedTextBlock, value = 720F)
engine.block.setHeightMode(groupedTextBlock, mode = SizeMode.AUTO)
engine.block.replaceText(groupedTextBlock, "Reveal this line word by word.")
check(engine.block.supportsAnimation(groupedTextBlock))
val typewriterText = engine.block.createAnimation(AnimationType.TypewriterText)
engine.block.setInAnimation(block = textBlock, animation = typewriterText)
engine.block.setDuration(block = typewriterText, duration = 2.0)
val wordReveal = engine.block.createAnimation(AnimationType.Baseline)
engine.block.setInAnimation(block = groupedTextBlock, animation = wordReveal)
engine.block.setEnum(
block = wordReveal,
property = "textAnimationWritingStyle",
value = "Word",
)
engine.block.setFloat(
block = wordReveal,
property = "textAnimationOverlap",
value = 0.4F,
)
val previousLoop = engine.block.getLoopAnimation(block)
val spinLoop = engine.block.createAnimation(AnimationType.SpinLoop)
engine.block.setLoopAnimation(block = block, animation = spinLoop)
if (engine.block.isValid(previousLoop)) {
engine.block.destroy(previousLoop)
}
val attachedIn = engine.block.getInAnimation(block)
val attachedOut = engine.block.getOutAnimation(block)
val attachedLoop = engine.block.getLoopAnimation(block)
check(engine.block.isValid(attachedIn))
check(engine.block.isValid(attachedOut))
check(engine.block.isValid(attachedLoop))
check(engine.block.getType(attachedIn) == AnimationType.Slide.key)
check(engine.block.getType(attachedOut) == AnimationType.Fade.key)
check(engine.block.getType(attachedLoop) == AnimationType.SpinLoop.key)
check(engine.block.getEnum(slideIn, "animationEasing") == AnimationEasingType.EASE_OUT.key)
check(engine.block.getEnum(fadeOut, "animationEasing") == AnimationEasingType.EASE_IN.key)
check(engine.block.getType(typewriterText) == AnimationType.TypewriterText.key)
check(engine.block.getEnum(wordReveal, "textAnimationWritingStyle") == "Word")
check(engine.block.getFloat(wordReveal, "textAnimationOverlap") == 0.4F)
check(engine.block.getDuration(attachedIn) == 1.0)
check(engine.block.getDuration(attachedOut) == 0.75)
return CreateAnimations(
supportsGraphicAnimation = supportsGraphicAnimation,
slideDirection = engine.block.getFloat(slideIn, "animation/slide/direction"),
slideEasing = engine.block.getEnum(slideIn, "animationEasing"),
entranceDuration = engine.block.getDuration(attachedIn),
exitDuration = engine.block.getDuration(attachedOut),
loopAnimationType = engine.block.getType(attachedLoop),
textWritingStyle = engine.block.getEnum(wordReveal, "textAnimationWritingStyle"),
textOverlap = engine.block.getFloat(wordReveal, "textAnimationOverlap"),
replacedLoopAnimationIsValid = engine.block.isValid(previousLoop),
)
}
```
Add motion to design elements by creating entrance, exit, and loop animations
using CE.SDK's animation system.

> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-animations)
CE.SDK creates animations as separate block instances and attaches them to target blocks. You can apply entrance animations that play when a block appears, exit animations that play before it leaves, and loop animations that run while it stays visible. Text blocks also support writing-style controls for whole-block, line, word, or character reveals.
This guide covers how to create and configure animations programmatically on Android, including timing, easing, type-specific properties, text animation settings, and lifecycle cleanup.
## Animation Fundamentals
Verify that a block supports animations before creating animation blocks for it. Once support is confirmed, create an animation with `createAnimation`, attach it to the target block, and configure the animation duration.
```kotlin highlight-android-check-support
val supportsGraphicAnimation = engine.block.supportsAnimation(block)
if (!supportsGraphicAnimation) {
error("Graphic block does not support animations.")
}
```
Animation support is available for common visible design blocks:
- **Graphic blocks** with image, video, color, or other fills
- **Text blocks** with additional writing-style options
- **Shape-backed graphics** whose graphic block uses a vector shape
CE.SDK exposes animation presets through `AnimationType`:
- **Entrance and exit animations**: `Slide`, `Pan`, `Fade`, `Blur`, `Grow`, `Zoom`, `Pop`, `Wipe`, `Baseline`, `CropZoom`, `Spin`, `KenBurns`
- **Text-only animations**: `TypewriterText`, `BlockSwipeText`, `SpreadText`, `MergeText`
- **Loop animations**: `SpinLoop`, `FadeLoop`, `BlurLoop`, `PulsatingLoop`, `BreathingLoop`, `JumpLoop`, `SqueezeLoop`, `SwayLoop`, `ScaleLoop`
## Entrance Animations
Entrance animations define how blocks appear on screen. Create the animation, attach it with `setInAnimation`, and set its duration in seconds.
```kotlin highlight-android-entrance-animation
val slideIn = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = block, animation = slideIn)
engine.block.setDuration(block = slideIn, duration = 1.0)
```
The animation duration controls how long the entrance effect runs after the target block becomes visible.
## Exit Animations
Exit animations define how blocks leave the scene. Attach them with `setOutAnimation`; CE.SDK coordinates entrance and exit timing against the block's visible duration.
```kotlin highlight-android-exit-animation
val fadeOut = engine.block.createAnimation(AnimationType.Fade)
engine.block.setOutAnimation(block = block, animation = fadeOut)
engine.block.setDuration(block = fadeOut, duration = 0.75)
engine.block.setEnum(
block = fadeOut,
property = "animationEasing",
value = AnimationEasingType.EASE_IN.key,
)
```
When a block has both entrance and exit animations, CE.SDK adjusts their durations against the block's visible time range to prevent overlap.
## Loop Animations
Loop animations run while the block remains visible. Use a loop animation type, then attach it with `setLoopAnimation`.
```kotlin highlight-android-loop-animation
val breathingLoop = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setLoopAnimation(block = block, animation = breathingLoop)
engine.block.setDuration(block = breathingLoop, duration = 2.0)
```
The loop animation duration controls one cycle of the repeated motion. Loop animations can run at the same time as entrance and exit animations, which makes them useful for subtle continuous motion.
## Animation Properties
Animation blocks expose type-specific properties. Use `findAllProperties` before writing a property such as slide direction, and use `getEnumValues` to inspect enum-backed options like easing curves.
```kotlin highlight-android-animation-properties
val slideProperties = engine.block.findAllProperties(slideIn)
if ("animation/slide/direction" in slideProperties) {
engine.block.setFloat(
block = slideIn,
property = "animation/slide/direction",
value = 1.5F * Math.PI.toFloat(),
)
}
val easingOptions = engine.block.getEnumValues(enumProperty = "animationEasing")
check(AnimationEasingType.EASE_OUT.key in easingOptions)
engine.block.setEnum(
block = slideIn,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
```
Common configurable properties include:
- **Direction**: Slide animations use radians as the motion direction (`0` = slides right and enters from the left, `0.5 * PI` = slides down and enters from the top, `PI` = slides left and enters from the right, `1.5 * PI` = slides up and enters from the bottom).
- **Easing**: `AnimationEasingType` includes `Linear`, the base `EaseIn`, `EaseOut`, and `EaseInOut` curves, and higher-order `Quart`, `Quint`, `Back`, and `Spring` families such as `EaseOutQuint`, `EaseOutBack`, and `EaseInOutSpring`. Call `engine.block.getEnumValues(enumProperty="animationEasing")` to enumerate the full list at runtime.
## Text Animations
Text blocks support text-only animation presets such as `TypewriterText`, `BlockSwipeText`, `SpreadText`, and `MergeText`. For text-capable entrance and exit animations, set `textAnimationWritingStyle` to `Block`, `Line`, `Word`, or `Character` to control how the animation is grouped, and set `textAnimationOverlap` to control how much consecutive segments overlap.
```kotlin highlight-android-text-animation
val typewriterText = engine.block.createAnimation(AnimationType.TypewriterText)
engine.block.setInAnimation(block = textBlock, animation = typewriterText)
engine.block.setDuration(block = typewriterText, duration = 2.0)
val wordReveal = engine.block.createAnimation(AnimationType.Baseline)
engine.block.setInAnimation(block = groupedTextBlock, animation = wordReveal)
engine.block.setEnum(
block = wordReveal,
property = "textAnimationWritingStyle",
value = "Word",
)
engine.block.setFloat(
block = wordReveal,
property = "textAnimationOverlap",
value = 0.4F,
)
```
An overlap value of `0` keeps segments sequential. Values closer to `1` make more of the segment animations run at the same time.
## Managing Animation Lifecycle
Read current animations with `getInAnimation`, `getOutAnimation`, or `getLoopAnimation` before replacing them. After assigning a replacement, destroy the previously attached animation block so it does not remain in the scene graph unused.
```kotlin highlight-android-manage-lifecycle
val previousLoop = engine.block.getLoopAnimation(block)
val spinLoop = engine.block.createAnimation(AnimationType.SpinLoop)
engine.block.setLoopAnimation(block = block, animation = spinLoop)
if (engine.block.isValid(previousLoop)) {
engine.block.destroy(previousLoop)
}
```
Invalid handles mean that no animation is attached for that slot.
## Troubleshooting
### Animation Not Playing
Verify the target block with `supportsAnimation`. Also check that the block is visible during playback and that the animation duration fits inside the block's visible duration.
### Duration Issues
Attach the animation to a block before setting its duration. Duration is set on the animation block, not on the target block.
### Memory Leaks
Destroy a replaced animation block after assigning its replacement. Replacing a block's animation detaches the old animation instance but does not automatically destroy it.
### Timing Conflicts
Entrance and exit animations share the target block's visible time range. If they seem to overlap incorrectly, CE.SDK automatically adjusts durations to prevent conflicts. Reduce individual animation durations if needed.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.block.supportsAnimation(block=_)` | Check whether a block can use animations. |
| `engine.block.createAnimation(type=_)` | Create an animation block for a supported `AnimationType`. |
| `engine.block.setInAnimation(block=_, animation=_)` | Attach an entrance animation. |
| `engine.block.setOutAnimation(block=_, animation=_)` | Attach an exit animation. |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Attach a loop animation. |
| `engine.block.getInAnimation(block=_)` | Read the current entrance animation handle. |
| `engine.block.getOutAnimation(block=_)` | Read the current exit animation handle. |
| `engine.block.getLoopAnimation(block=_)` | Read the current loop animation handle. |
| `engine.block.setDuration(block=_, duration=_)` | Set an animation duration in seconds. |
| `engine.block.getDuration(block=_)` | Read an animation duration in seconds. |
| `engine.block.findAllProperties(block=_)` | Discover properties supported by a specific animation block. |
| `engine.block.getEnumValues(enumProperty="animationEasing")` | List allowed enum values for a property. |
| `engine.block.setEnum(block=_, property="animationEasing", value=_)` | Configure an enum animation property such as easing. |
| `engine.block.setEnum(block=_, property="textAnimationWritingStyle", value=_)` | Configure text animation grouping as `Block`, `Line`, `Word`, or `Character`. |
| `engine.block.setFloat(block=_, property="animation/slide/direction", value=_)` | Configure slide direction in radians. |
| `engine.block.setFloat(block=_, property="textAnimationOverlap", value=_)` | Configure overlap between text animation segments. |
| `engine.block.isValid(block=_)` | Check whether an animation handle still points to a valid block. |
| `engine.block.destroy(block=_)` | Destroy a detached or replaced animation block. |
## Next Steps
- [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) — Detailed non-text block animations
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) — Text-specific animation control
- [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) — Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks.
- [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) — Animation concepts
---
## Related Pages
- [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) - Apply movement, scaling, rotation, or opacity changes to elements using time-based keyframes.
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) - Animate text elements with effects like fade, typewriter, and bounce for dynamic visual presentation.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Base Animations"
description: "Apply movement, scaling, rotation, or opacity changes to elements using time-based keyframes."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) > [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/)
---
```kotlin file=@cesdk_android_examples/engine-guides-using-animations/UsingAnimations.kt reference-only
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.AnimationEasingType
import ly.img.engine.AnimationType
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
fun usingAnimations(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
engine.scene.zoomToBlock(
page,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = 100F)
engine.block.setPositionY(block, value = 50F)
engine.block.setWidth(block, value = 300F)
engine.block.setHeight(block, value = 300F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block, fill = fill)
val supportsAnimations = engine.block.supportsAnimation(block)
if (supportsAnimations) {
val slideAnimation = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = block, animation = slideAnimation)
engine.block.setDuration(block = slideAnimation, duration = 1.0)
}
if (supportsAnimations) {
val initialIn = engine.block.getInAnimation(block)
if (engine.block.isValid(initialIn)) {
engine.block.destroy(initialIn)
}
val fadeInAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setInAnimation(block = block, animation = fadeInAnimation)
engine.block.setDuration(block = fadeInAnimation, duration = 1.0)
engine.block.setEnum(
block = fadeInAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
val entranceIn = engine.block.getInAnimation(block)
if (engine.block.isValid(entranceIn)) {
engine.block.destroy(entranceIn)
}
val entranceForTiming = engine.block.createAnimation(AnimationType.Zoom)
engine.block.setInAnimation(block = block, animation = entranceForTiming)
engine.block.setDuration(block = entranceForTiming, duration = 1.0)
val fadeOutAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setOutAnimation(block = block, animation = fadeOutAnimation)
engine.block.setDuration(block = fadeOutAnimation, duration = 1.0)
engine.block.setEnum(
block = fadeOutAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_IN.key,
)
val timingIn = engine.block.getInAnimation(block)
if (engine.block.isValid(timingIn)) {
engine.block.destroy(timingIn)
}
val breathingLoop = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setLoopAnimation(block = block, animation = breathingLoop)
engine.block.setDuration(block = breathingLoop, duration = 2.0)
val slideFromTop = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = block, animation = slideFromTop)
engine.block.setDuration(block = slideFromTop, duration = 1.0)
val slideProperties = engine.block.findAllProperties(slideFromTop)
val slideDirectionProperty = "animation/slide/direction"
check(slideDirectionProperty in slideProperties)
engine.block.setFloat(
block = slideFromTop,
property = slideDirectionProperty,
value = 0.5F * Math.PI.toFloat(),
)
engine.block.setEnum(
block = slideFromTop,
property = "animationEasing",
value = AnimationEasingType.EASE_IN_OUT.key,
)
val currentLoop = engine.block.getLoopAnimation(block)
val currentOut = engine.block.getOutAnimation(block)
val currentIn = engine.block.getInAnimation(block)
if (engine.block.isValid(currentIn)) {
engine.block.destroy(currentIn)
}
val replacementIn = engine.block.createAnimation(AnimationType.Wipe)
engine.block.setInAnimation(block = block, animation = replacementIn)
engine.block.setDuration(block = replacementIn, duration = 1.0)
val easingOptions = engine.block.getEnumValues("animationEasing")
check(engine.block.isValid(currentLoop))
check(engine.block.isValid(currentOut))
check(easingOptions.contains(AnimationEasingType.EASE_OUT.key))
check(engine.block.getDuration(replacementIn) == 1.0)
}
val text = engine.block.create(DesignBlockType.Text)
val textAnimation = engine.block.createAnimation(AnimationType.Baseline)
engine.block.setInAnimation(text, textAnimation)
engine.block.appendChild(page, text)
engine.block.setPositionX(text, 100F)
engine.block.setPositionY(text, 100F)
engine.block.setWidthMode(text, SizeMode.AUTO)
engine.block.setHeightMode(text, SizeMode.AUTO)
engine.block.replaceText(text, "You can animate text\nline by line,\nword by word,\nor character by character\nwith CE.SDK")
engine.block.setEnum(textAnimation, "textAnimationWritingStyle", "Word")
engine.block.setDuration(textAnimation, 2.0)
engine.block.setEnum(textAnimation, "animationEasing", "EaseOut")
val text2 = engine.block.create(DesignBlockType.Text)
val textAnimation2 = engine.block.createAnimation(AnimationType.Pan)
engine.block.setInAnimation(text2, textAnimation2)
engine.block.appendChild(page, text2)
engine.block.setPositionX(text2, 100F)
engine.block.setPositionY(text2, 500F)
engine.block.setWidth(text2, 500F)
engine.block.setHeightMode(text2, SizeMode.AUTO)
engine.block.replaceText(text2, "You can use the textAnimationOverlap property to control the overlap between text animation segments.")
engine.block.setFloat(textAnimation2, "textAnimationOverlap", 0.4F)
engine.block.setDuration(textAnimation2, 1.0)
engine.block.setEnum(textAnimation2, "animationEasing", "EaseOut")
engine.stop()
}
```
Add motion to design blocks with entrance, exit, and loop animations using
CE.SDK's Android Engine API.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-using-animations)
Base animations add motion to non-text design blocks through entrance (In), exit (Out), and loop animations. CE.SDK creates animations as separate blocks, attaches them to design blocks, and lets you configure duration, easing, and type-specific properties.
This guide covers base animation objects. For text-specific controls such as line, word, and character animation, see [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/).
## Animation Fundamentals
Before applying animations, check whether the target block supports them. Then create an animation with `createAnimation`, attach it with `setInAnimation`, and set the animation duration in seconds.
```kotlin highlight-android-supports-animation
val supportsAnimations = engine.block.supportsAnimation(block)
if (supportsAnimations) {
val slideAnimation = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = block, animation = slideAnimation)
engine.block.setDuration(block = slideAnimation, duration = 1.0)
}
```
Use Android `AnimationType` values that match the animation category you want to attach:
| Category | Android types | Use |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Entrance / Exit | `AnimationType.Slide`, `AnimationType.Fade`, `AnimationType.Blur`, `AnimationType.Grow`, `AnimationType.Zoom`, `AnimationType.Pop`, `AnimationType.Wipe`, `AnimationType.Pan`, `AnimationType.Baseline`, `AnimationType.CropZoom`, `AnimationType.Spin`, `AnimationType.KenBurns` | Animate a block as it appears or leaves the timeline |
| Text-only entrance | `AnimationType.TypewriterText`, `AnimationType.BlockSwipeText`, `AnimationType.SpreadText`, `AnimationType.MergeText` | Animate text-specific reveals; use the [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) guide for text controls |
| Loop | `AnimationType.SpinLoop`, `AnimationType.FadeLoop`, `AnimationType.BlurLoop`, `AnimationType.PulsatingLoop`, `AnimationType.BreathingLoop`, `AnimationType.JumpLoop`, `AnimationType.SqueezeLoop`, `AnimationType.SwayLoop`, `AnimationType.ScaleLoop` | Repeat while the block remains visible |
For the exhaustive list and type-specific properties, see [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/).
## Entrance Animations
Entrance animations define how a block appears on the timeline. Create the animation, attach it to the block with `setInAnimation`, set its duration, and configure optional properties such as easing.
```kotlin highlight-android-entrance-animation
val fadeInAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setInAnimation(block = block, animation = fadeInAnimation)
engine.block.setDuration(block = fadeInAnimation, duration = 1.0)
engine.block.setEnum(
block = fadeInAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
```
When replacing an entrance animation, destroy the current `getInAnimation(block)` handle if it is valid before calling `setInAnimation` again. See [Managing Animation Lifecycle](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/#managing-animation-lifecycle) for the full cleanup pattern.
`AnimationEasingType.EASE_OUT.key` starts fast and slows down toward the end, which makes the fade feel less abrupt.
## Exit Animations
Exit animations define how a block leaves the timeline. Attach them with `setOutAnimation` and configure their duration just like entrance animations.
```kotlin highlight-android-exit-animation
val fadeOutAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setOutAnimation(block = block, animation = fadeOutAnimation)
engine.block.setDuration(block = fadeOutAnimation, duration = 1.0)
engine.block.setEnum(
block = fadeOutAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_IN.key,
)
```
When replacing an exit animation, destroy the current `getOutAnimation(block)` handle if it is valid before calling `setOutAnimation` again.
When a block has both entrance and exit animations, CE.SDK keeps their timing valid for the block duration and adjusts conflicting durations to avoid overlap.
## Loop Animations
Loop animations run continuously while the block is visible. Attach them with `setLoopAnimation` and use the animation duration as the length of one loop cycle.
```kotlin highlight-android-loop-animation
val breathingLoop = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setLoopAnimation(block = block, animation = breathingLoop)
engine.block.setDuration(block = breathingLoop, duration = 2.0)
```
When replacing a loop animation, destroy the current `getLoopAnimation(block)` handle if it is valid before calling `setLoopAnimation` again.
A 2-second breathing loop completes one full pulse every 2 seconds while the block stays visible.
## Animation Properties
Each animation type exposes its own properties. Use `findAllProperties` to inspect type-specific keys, then update documented numeric values with `setFloat` and enum values with `setEnum`.
```kotlin highlight-android-animation-properties
val slideFromTop = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = block, animation = slideFromTop)
engine.block.setDuration(block = slideFromTop, duration = 1.0)
val slideProperties = engine.block.findAllProperties(slideFromTop)
val slideDirectionProperty = "animation/slide/direction"
check(slideDirectionProperty in slideProperties)
engine.block.setFloat(
block = slideFromTop,
property = slideDirectionProperty,
value = 0.5F * Math.PI.toFloat(),
)
engine.block.setEnum(
block = slideFromTop,
property = "animationEasing",
value = AnimationEasingType.EASE_IN_OUT.key,
)
```
For slide animations, `animation/slide/direction` is the motion direction in radians. The block enters from the opposite side of that direction:
- `0` - Slides right, entering from the left
- `0.5 * Math.PI` - Slides down, entering from the top
- `Math.PI` - Slides left, entering from the right
- `1.5 * Math.PI` - Slides up, entering from the bottom
## Managing Animation Lifecycle
Animation blocks need the same lifecycle attention as other blocks. When replacing an attached animation, read the current handle, destroy it if it is valid, and then attach the replacement.
```kotlin highlight-android-manage-animations
val currentIn = engine.block.getInAnimation(block)
if (engine.block.isValid(currentIn)) {
engine.block.destroy(currentIn)
}
val replacementIn = engine.block.createAnimation(AnimationType.Wipe)
engine.block.setInAnimation(block = block, animation = replacementIn)
engine.block.setDuration(block = replacementIn, duration = 1.0)
```
`getInAnimation`, `getOutAnimation`, and `getLoopAnimation` return invalid handles when no animation is attached. Destroying a design block also destroys its attached animations, but detached animation blocks must be destroyed manually.
## Easing Functions
Query available easing options with `getEnumValues` when you populate controls or validate a stored animation setting.
```kotlin highlight-android-easing-options
val easingOptions = engine.block.getEnumValues("animationEasing")
```
Common easing values include:
| Easing | Description |
| --- | --- |
| `Linear` | Constant speed throughout |
| `EaseIn` | Starts slow and accelerates toward the end |
| `EaseOut` | Starts fast and decelerates toward the end |
| `EaseInOut` | Starts slow, speeds up, then slows down again |
Use `AnimationEasingType` for typed access to the full Android easing surface, including the Quart, Quint, Back, and Spring `EaseIn`, `EaseOut`, and `EaseInOut` families.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.block.supportsAnimation(block=_)` | Check whether a design block can use animations. |
| `engine.block.createAnimation(type=_)` | Create an animation block for an `AnimationType`. |
| `engine.block.setInAnimation(block=_, animation=_)` | Attach an entrance animation to a block. |
| `engine.block.setOutAnimation(block=_, animation=_)` | Attach an exit animation to a block. |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Attach a loop animation to a block. |
| `engine.block.getInAnimation(block=_)` | Read the current entrance animation handle. |
| `engine.block.getOutAnimation(block=_)` | Read the current exit animation handle. |
| `engine.block.getLoopAnimation(block=_)` | Read the current loop animation handle. |
| `engine.block.isValid(block=_)` | Check whether an animation handle points to a live block. |
| `engine.block.setDuration(block=_, duration=_)` | Set an animation duration in seconds. |
| `engine.block.getDuration(block=_)` | Read an animation duration in seconds. |
| `engine.block.setEnum(block=_, property="animationEasing", value=_)` | Set an enum property such as easing. |
| `engine.block.getEnumValues(enumProperty="animationEasing")` | List supported values for an enum property. |
| `engine.block.setFloat(block=_, property="animation/slide/direction", value=_)` | Set numeric animation properties such as slide direction. |
| `engine.block.findAllProperties(block=_)` | List configurable properties for an animation block. |
| `engine.block.destroy(block=_)` | Destroy a detached or replaced animation block. |
## Next Steps
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) - Animate text with writing styles and character-level control
- [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) - Understand animation concepts and capabilities
- [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) — Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Text Animations"
description: "Animate text elements with effects like fade, typewriter, and bounce for dynamic visual presentation."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) > [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/)
---
```kotlin file=@cesdk_android_examples/engine-guides-text-animations/TextAnimations.kt reference-only
import ly.img.engine.AnimationEasingType
import ly.img.engine.AnimationType
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
suspend fun textAnimations(engine: Engine): TextAnimationSummary {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = page, value = 1920F)
engine.block.setHeight(block = page, value = 1080F)
engine.block.setDuration(block = page, duration = 10.0)
engine.block.appendChild(parent = scene, child = page)
val introText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = introText, value = 100F)
engine.block.setPositionY(block = introText, value = 100F)
engine.block.setWidth(block = introText, value = 600F)
engine.block.setHeight(block = introText, value = 200F)
engine.block.replaceText(block = introText, text = "Creating\nText\nAnimations")
engine.block.appendChild(parent = page, child = introText)
check(engine.block.supportsAnimation(block = introText))
val baselineAnimation = engine.block.createAnimation(type = AnimationType.Baseline)
engine.block.setInAnimation(block = introText, animation = baselineAnimation)
engine.block.setDuration(block = baselineAnimation, duration = 2.0)
val blockText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = blockText, value = 1300F)
engine.block.setPositionY(block = blockText, value = 700F)
engine.block.setWidth(block = blockText, value = 500F)
engine.block.setHeight(block = blockText, value = 200F)
engine.block.replaceText(block = blockText, text = "Animate the complete text block")
engine.block.appendChild(parent = page, child = blockText)
val blockAnimation = engine.block.createAnimation(type = AnimationType.Baseline)
engine.block.setInAnimation(block = blockText, animation = blockAnimation)
engine.block.setDuration(block = blockAnimation, duration = 2.0)
engine.block.setEnum(block = blockAnimation, property = "textAnimationWritingStyle", value = "Block")
engine.block.setEnum(block = blockAnimation, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key)
val lineText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = lineText, value = 700F)
engine.block.setPositionY(block = lineText, value = 100F)
engine.block.setWidth(block = lineText, value = 600F)
engine.block.setHeight(block = lineText, value = 200F)
engine.block.replaceText(block = lineText, text = "Line by line\nanimation\nfor text")
engine.block.appendChild(parent = page, child = lineText)
val lineAnimation = engine.block.createAnimation(type = AnimationType.Baseline)
engine.block.setInAnimation(block = lineText, animation = lineAnimation)
engine.block.setDuration(block = lineAnimation, duration = 2.0)
engine.block.setEnum(block = lineAnimation, property = "textAnimationWritingStyle", value = "Line")
engine.block.setEnum(block = lineAnimation, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key)
val wordText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = wordText, value = 1300F)
engine.block.setPositionY(block = wordText, value = 100F)
engine.block.setWidth(block = wordText, value = 600F)
engine.block.setHeight(block = wordText, value = 200F)
engine.block.replaceText(block = wordText, text = "Animate word by word for emphasis")
engine.block.appendChild(parent = page, child = wordText)
val wordAnimation = engine.block.createAnimation(type = AnimationType.Baseline)
engine.block.setInAnimation(block = wordText, animation = wordAnimation)
engine.block.setDuration(block = wordAnimation, duration = 2.5)
engine.block.setEnum(block = wordAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setEnum(block = wordAnimation, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key)
val characterText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = characterText, value = 100F)
engine.block.setPositionY(block = characterText, value = 400F)
engine.block.setWidth(block = characterText, value = 600F)
engine.block.setHeight(block = characterText, value = 200F)
engine.block.replaceText(block = characterText, text = "Character by character for typewriter effect")
engine.block.appendChild(parent = page, child = characterText)
val characterAnimation = engine.block.createAnimation(type = AnimationType.Baseline)
engine.block.setInAnimation(block = characterText, animation = characterAnimation)
engine.block.setDuration(block = characterAnimation, duration = 3.0)
engine.block.setEnum(block = characterAnimation, property = "textAnimationWritingStyle", value = "Character")
engine.block.setEnum(block = characterAnimation, property = "animationEasing", value = AnimationEasingType.LINEAR.key)
val sequentialText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = sequentialText, value = 700F)
engine.block.setPositionY(block = sequentialText, value = 400F)
engine.block.setWidth(block = sequentialText, value = 600F)
engine.block.setHeight(block = sequentialText, value = 200F)
engine.block.replaceText(block = sequentialText, text = "Sequential animation with zero overlap")
engine.block.appendChild(parent = page, child = sequentialText)
val sequentialAnimation = engine.block.createAnimation(type = AnimationType.Pan)
engine.block.setInAnimation(block = sequentialText, animation = sequentialAnimation)
engine.block.setDuration(block = sequentialAnimation, duration = 2.0)
engine.block.setEnum(block = sequentialAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setFloat(block = sequentialAnimation, property = "textAnimationOverlap", value = 0.0F)
engine.block.setEnum(block = sequentialAnimation, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key)
val cascadingText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = cascadingText, value = 1300F)
engine.block.setPositionY(block = cascadingText, value = 400F)
engine.block.setWidth(block = cascadingText, value = 600F)
engine.block.setHeight(block = cascadingText, value = 200F)
engine.block.replaceText(block = cascadingText, text = "Cascading animation with partial overlap")
engine.block.appendChild(parent = page, child = cascadingText)
val cascadingAnimation = engine.block.createAnimation(type = AnimationType.Pan)
engine.block.setInAnimation(block = cascadingText, animation = cascadingAnimation)
engine.block.setDuration(block = cascadingAnimation, duration = 1.5)
engine.block.setEnum(block = cascadingAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setFloat(block = cascadingAnimation, property = "textAnimationOverlap", value = 0.4F)
engine.block.setEnum(block = cascadingAnimation, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key)
val combinedText = engine.block.create(DesignBlockType.Text)
engine.block.setPositionX(block = combinedText, value = 100F)
engine.block.setPositionY(block = combinedText, value = 700F)
engine.block.setWidth(block = combinedText, value = 1200F)
engine.block.setHeight(block = combinedText, value = 200F)
engine.block.replaceText(block = combinedText, text = "Combine writing style, overlap, duration, and easing")
engine.block.appendChild(parent = page, child = combinedText)
val combinedAnimation = engine.block.createAnimation(type = AnimationType.Fade)
engine.block.setInAnimation(block = combinedText, animation = combinedAnimation)
engine.block.setEnum(block = combinedAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setFloat(block = combinedAnimation, property = "textAnimationOverlap", value = 0.3F)
engine.block.setDuration(block = combinedAnimation, duration = 1.5)
engine.block.setEnum(block = combinedAnimation, property = "animationEasing", value = AnimationEasingType.EASE_IN_OUT.key)
val writingStyleOptions = engine.block.getEnumValues(enumProperty = "textAnimationWritingStyle")
val easingOptions = engine.block.getEnumValues(enumProperty = "animationEasing")
return TextAnimationSummary(
writingStyleOptions = writingStyleOptions,
easingOptions = easingOptions,
blockWritingStyle = engine.block.getEnum(block = blockAnimation, property = "textAnimationWritingStyle"),
lineWritingStyle = engine.block.getEnum(block = lineAnimation, property = "textAnimationWritingStyle"),
wordWritingStyle = engine.block.getEnum(block = wordAnimation, property = "textAnimationWritingStyle"),
characterWritingStyle = engine.block.getEnum(block = characterAnimation, property = "textAnimationWritingStyle"),
sequentialOverlap = engine.block.getFloat(block = sequentialAnimation, property = "textAnimationOverlap"),
cascadingOverlap = engine.block.getFloat(block = cascadingAnimation, property = "textAnimationOverlap"),
combinedDuration = engine.block.getDuration(block = combinedAnimation),
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-text-animations/TextAnimationSummary.kt reference-only
data class TextAnimationSummary(
val writingStyleOptions: List,
val easingOptions: List,
val blockWritingStyle: String,
val lineWritingStyle: String,
val wordWritingStyle: String,
val characterWritingStyle: String,
val sequentialOverlap: Float,
val cascadingOverlap: Float,
val combinedDuration: Double,
)
```
Create text animations that reveal content as one block, line by line, word
by word, or character by character with control over timing and overlap.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-animations)
Text animations in CE.SDK animate text blocks with granular control over how the
text appears. Unlike standard block animations, text animations support writing
styles that determine whether animation applies to the entire text block, line
by line, word by word, or character by character.
This guide covers text-specific animation properties like writing styles and
segment overlap, enabling dynamic text presentations in your designs.
## Text Animation Fundamentals
Create animations by first creating an animation instance, then attaching it to
a text block. The animation defines how the text animates, while the text block
contains the content and styling.
```kotlin highlight-android-create-animation
val baselineAnimation = engine.block.createAnimation(type = AnimationType.Baseline)
engine.block.setInAnimation(block = introText, animation = baselineAnimation)
engine.block.setDuration(block = baselineAnimation, duration = 2.0)
```
Animations are created with `engine.block.createAnimation()` using a type like
`AnimationType.Baseline`, `AnimationType.Fade`, or `AnimationType.Pan`. Attach
the animation to the text block's entrance with `engine.block.setInAnimation()`
and set the timing with `engine.block.setDuration()`. For text-focused effects,
also consider text-only presets such as `AnimationType.TypewriterText`,
`AnimationType.BlockSwipeText`, `AnimationType.SpreadText`, and
`AnimationType.MergeText`; see [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/)
for the full preset list.
## Writing Style Control
Text animations support different granularity levels through the
`textAnimationWritingStyle` property. This controls whether the animation
applies to the entire text block at once or breaks it into segments such as
lines, words, or characters. Query the available options with
`engine.block.getEnumValues(enumProperty = "textAnimationWritingStyle")`.
### Whole-Block Animation
The `Block` writing style animates the complete text block as a single segment.
Use it when the text should appear as one unit instead of revealing individual
lines, words, or characters.
```kotlin highlight-android-writing-style-block
engine.block.setEnum(block = blockAnimation, property = "textAnimationWritingStyle", value = "Block")
```
Set the writing style to `Block` with `engine.block.setEnum()` to keep the
entire text block synchronized.
### Line-by-Line Animation
The `Line` writing style animates text one line at a time from top to bottom.
Each line appears sequentially, creating a structured reveal effect.
```kotlin highlight-android-writing-style-line
engine.block.setEnum(block = lineAnimation, property = "textAnimationWritingStyle", value = "Line")
```
Set the writing style to `Line` with `engine.block.setEnum()`. This is useful
for revealing multi-line text in a clear, organized manner.
### Word-by-Word Animation
The `Word` writing style animates text one word at a time in reading order. This
creates emphasis and draws attention to individual words.
```kotlin highlight-android-writing-style-word
engine.block.setEnum(block = wordAnimation, property = "textAnimationWritingStyle", value = "Word")
```
Setting the writing style to `Word` creates dynamic text reveals that emphasize
key phrases.
### Character-by-Character Animation
The `Character` writing style animates text one character at a time, creating a
typewriter effect. This is the most granular animation option.
```kotlin highlight-android-writing-style-character
engine.block.setEnum(block = characterAnimation, property = "textAnimationWritingStyle", value = "Character")
```
Use `Character` when you want maximum control over the animation timing.
## Segment Overlap Configuration
The `textAnimationOverlap` property controls timing between animation segments.
A value of `0` means segments animate sequentially; values between `0` and `1`
create cascading effects where segments overlap partially. Use
`engine.block.setFloat()` to set the overlap value.
### Sequential Animation (Overlap = 0)
When overlap is set to `0`, each segment completes before the next begins,
creating a clear reveal effect.
```kotlin highlight-android-overlap-sequential
engine.block.setFloat(block = sequentialAnimation, property = "textAnimationOverlap", value = 0.0F)
```
Sequential animation ensures each text segment fully appears before the next one
starts, making it useful for emphasis and readability.
### Cascading Animation (Overlap = 0.4)
When overlap is set to a value between `0` and `1`, segments animate in a
cascading pattern, creating a smooth effect as they blend together.
```kotlin highlight-android-overlap-cascading
engine.block.setFloat(block = cascadingAnimation, property = "textAnimationOverlap", value = 0.4F)
```
Cascading animation with partial overlap creates fluid text reveals that feel
natural.
## Combining with Animation Properties
Text animations can be enhanced with standard animation properties like duration
and easing. Duration controls the overall timing of the animation, while easing
controls the acceleration curve. The snippet below applies all four knobs -
writing style, overlap, duration, and easing - to a single animation, then
queries the engine for available enum options.
```kotlin highlight-android-duration-easing
engine.block.setEnum(block = combinedAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setFloat(block = combinedAnimation, property = "textAnimationOverlap", value = 0.3F)
engine.block.setDuration(block = combinedAnimation, duration = 1.5)
engine.block.setEnum(block = combinedAnimation, property = "animationEasing", value = AnimationEasingType.EASE_IN_OUT.key)
val writingStyleOptions = engine.block.getEnumValues(enumProperty = "textAnimationWritingStyle")
val easingOptions = engine.block.getEnumValues(enumProperty = "animationEasing")
```
Set the easing function with `engine.block.setEnum()` and
an `AnimationEasingType` constant such as `AnimationEasingType.LINEAR.key`,
`AnimationEasingType.EASE_OUT.key`, or
`AnimationEasingType.EASE_IN_OUT.key`. Call
`engine.block.getEnumValues(enumProperty = "animationEasing")` to discover the
current engine's easing values, including quart, quint, back, and spring
variants. Combining writing style, overlap, duration, and easing gives complete
control over how text animates.
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.create(blockType=_)` | Create a text or page block |
| `engine.block.appendChild(parent=_, child=_)` | Add a block to the scene hierarchy |
| `engine.block.setPositionX(block=_, value=_)` | Set the block's horizontal position |
| `engine.block.setPositionY(block=_, value=_)` | Set the block's vertical position |
| `engine.block.setWidth(block=_, value=_)` | Set the block width |
| `engine.block.setHeight(block=_, value=_)` | Set the block height |
| `engine.block.replaceText(block=_, text=_)` | Set text content |
| `engine.block.supportsAnimation(block=_)` | Check whether a block supports animations |
| `engine.block.createAnimation(type=_)` | Create a new animation instance |
| `engine.block.setInAnimation(block=_, animation=_)` | Apply animation to a block entrance |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Apply a looping animation to a block |
| `engine.block.setOutAnimation(block=_, animation=_)` | Apply animation to a block exit |
| `engine.block.getInAnimation(block=_)` | Get a block's entrance animation |
| `engine.block.getLoopAnimation(block=_)` | Get a block's looping animation |
| `engine.block.getOutAnimation(block=_)` | Get a block's exit animation |
| `engine.block.setDuration(block=_, duration=_)` | Set animation duration in seconds |
| `engine.block.getDuration(block=_)` | Get animation duration in seconds |
| `engine.block.setEnum(block=_, property=_, value=_)` | Set enum properties such as writing style and easing |
| `engine.block.getEnum(block=_, property=_)` | Get enum properties such as writing style and easing |
| `engine.block.setFloat(block=_, property=_, value=_)` | Set float properties such as segment overlap |
| `engine.block.getFloat(block=_, property=_)` | Get float properties such as segment overlap |
| `engine.block.getEnumValues(enumProperty=_)` | Get available enum options for a property |
## Troubleshooting
- **Animation is not visible**: For video scenes, make sure the page that
contains the text has a duration set before playback or export. The sample
sets the page duration before attaching animations to the text blocks.
- **Writing style does not apply**: Attach the animation to a text block with
`engine.block.setInAnimation()`, `engine.block.setLoopAnimation()`, or
`engine.block.setOutAnimation()`. `textAnimationWritingStyle` only affects
animations that run on text blocks.
- **Overlap has no visible effect**: Use a segmented writing style such as
`Line`, `Word`, or `Character`. With `Block`, the text animates as one
segment, so `textAnimationOverlap` has no segment timing to offset.
## Next Steps
- [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) - Create entrance, exit, and loop
animations
- [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) — Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks
- [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) - Understand animation concepts
and capabilities
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Edit Animations"
description: "Modify Android CE.SDK animations by reading properties, changing duration and easing, adjusting direction, and replacing or removing animation blocks."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/edit-32c12a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/)
---
```kotlin file=@cesdk_android_examples/engine-guides-edit-animations/EditAnimations.kt reference-only
import ly.img.engine.AnimationEasingType
import ly.img.engine.AnimationType
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
suspend fun editAnimations(engine: Engine): EditAnimationsSummary {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 800F)
engine.block.setHeight(block = page, value = 600F)
engine.block.setDuration(block = page, duration = 5.0)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.appendChild(parent = page, child = block)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block = block, value = 100F)
engine.block.setPositionY(block = block, value = 80F)
engine.block.setWidth(block = block, value = 320F)
engine.block.setHeight(block = block, value = 240F)
val fill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = fill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.12F, g = 0.38F, b = 0.95F, a = 1F),
)
engine.block.setFill(block = block, fill = fill)
check(engine.block.supportsAnimation(block = block))
val slideInAnimation = engine.block.createAnimation(AnimationType.Slide)
val fadeOutAnimation = engine.block.createAnimation(AnimationType.Fade)
val breathingLoopAnimation = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setInAnimation(block = block, animation = slideInAnimation)
engine.block.setOutAnimation(block = block, animation = fadeOutAnimation)
engine.block.setLoopAnimation(block = block, animation = breathingLoopAnimation)
engine.block.setDuration(block = slideInAnimation, duration = 1.0)
engine.block.setDuration(block = fadeOutAnimation, duration = 0.6)
engine.block.setDuration(block = breathingLoopAnimation, duration = 1.5)
engine.block.setEnum(
block = slideInAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
val inAnimation = engine.block.getInAnimation(block = block)
val outAnimation = engine.block.getOutAnimation(block = block)
val loopAnimation = engine.block.getLoopAnimation(block = block)
check(engine.block.isValid(block = inAnimation)) { "Expected an In animation." }
check(engine.block.isValid(block = outAnimation)) { "Expected an Out animation." }
check(engine.block.isValid(block = loopAnimation)) { "Expected a Loop animation." }
val inAnimationType = engine.block.getType(block = inAnimation)
val outAnimationType = engine.block.getType(block = outAnimation)
val loopAnimationType = engine.block.getType(block = loopAnimation)
val initialDuration = engine.block.getDuration(block = inAnimation)
val initialEasing = engine.block.getEnum(
block = inAnimation,
property = "animationEasing",
)
val slideProperties = engine.block.findAllProperties(block = inAnimation)
engine.block.setDuration(block = inAnimation, duration = 0.8)
engine.block.setDuration(block = loopAnimation, duration = 2.0)
val updatedDuration = engine.block.getDuration(block = inAnimation)
// animationEasing is the Engine property key for animation acceleration curves.
engine.block.setEnum(
block = inAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_IN_OUT.key,
)
val updatedEasing = engine.block.getEnum(
block = inAnimation,
property = "animationEasing",
)
val easingOptions = engine.block.getEnumValues(enumProperty = "animationEasing")
// Slide direction is exposed as radians.
engine.block.setFloat(
block = inAnimation,
property = "animation/slide/direction",
value = Math.PI.toFloat(),
)
val slideDirection = engine.block.getFloat(
block = inAnimation,
property = "animation/slide/direction",
)
// Slide fade is exposed as a boolean property.
engine.block.setBoolean(
block = inAnimation,
property = "animation/slide/fade",
value = true,
)
val slideFade = engine.block.getBoolean(
block = inAnimation,
property = "animation/slide/fade",
)
val currentInAnimation = engine.block.getInAnimation(block = block)
if (engine.block.isValid(block = currentInAnimation)) {
engine.block.destroy(block = currentInAnimation)
}
val zoomAnimation = engine.block.createAnimation(type = AnimationType.Zoom)
engine.block.setInAnimation(block = block, animation = zoomAnimation)
engine.block.setDuration(block = zoomAnimation, duration = 0.6)
engine.block.setEnum(
block = zoomAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_IN_OUT.key,
)
val replacementType = engine.block.getType(
block = engine.block.getInAnimation(block = block),
)
val currentLoopAnimation = engine.block.getLoopAnimation(block = block)
if (engine.block.isValid(block = currentLoopAnimation)) {
engine.block.destroy(block = currentLoopAnimation)
}
val loopAnimationRemoved = !engine.block.isValid(
block = engine.block.getLoopAnimation(block = block),
)
return EditAnimationsSummary(
inAnimationType = inAnimationType,
outAnimationType = outAnimationType,
loopAnimationType = loopAnimationType,
initialDuration = initialDuration,
updatedDuration = updatedDuration,
initialEasing = initialEasing,
updatedEasing = updatedEasing,
easingOptions = easingOptions,
slideProperties = slideProperties,
slideDirection = slideDirection,
slideFade = slideFade,
replacementType = replacementType,
loopAnimationRemoved = loopAnimationRemoved,
)
}
```
Modify existing animations by reading properties, changing duration and easing,
and replacing or removing animations from Android design blocks.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-edit-animations)
Editing animations in Android uses the same block API model as creating animations: retrieve the attached animation block, inspect its properties, then update or replace that animation block. This guide assumes you already attached In, Out, or Loop animations as covered in [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/).
The example works with a video scene that already contains a graphic block with slide, fade, and loop animations. It focuses on editing those animation handles rather than editor UI setup.
## Retrieving Animations
Before modifying an animation, retrieve it from the block with `getInAnimation`, `getOutAnimation`, or `getLoopAnimation`. Android returns an invalid handle when the slot is empty, so check `isValid` before passing the animation handle to other APIs.
```kotlin highlight-android-retrieve-animations
val inAnimation = engine.block.getInAnimation(block = block)
val outAnimation = engine.block.getOutAnimation(block = block)
val loopAnimation = engine.block.getLoopAnimation(block = block)
check(engine.block.isValid(block = inAnimation)) { "Expected an In animation." }
check(engine.block.isValid(block = outAnimation)) { "Expected an Out animation." }
check(engine.block.isValid(block = loopAnimation)) { "Expected a Loop animation." }
val inAnimationType = engine.block.getType(block = inAnimation)
val outAnimationType = engine.block.getType(block = outAnimation)
val loopAnimationType = engine.block.getType(block = loopAnimation)
```
Use `getType` after the validity check to branch on animation types such as slide, fade, zoom, or breathing loop.
## Reading Animation Properties
Inspect current settings with property getters. `getDuration` returns the animation length in seconds, `getEnum` reads enum properties such as easing, and `findAllProperties` lists the properties available for the specific animation type.
```kotlin highlight-android-read-properties
val initialDuration = engine.block.getDuration(block = inAnimation)
val initialEasing = engine.block.getEnum(
block = inAnimation,
property = "animationEasing",
)
val slideProperties = engine.block.findAllProperties(block = inAnimation)
```
Different animation types expose different properties. For example, slide animations expose `animation/slide/direction`, while text animations expose writing style and overlap properties.
## Modifying Animation Duration
Change timing with `setDuration`. In and Out animation durations are measured in seconds, while Loop animation duration defines one cycle of the repeated motion.
```kotlin highlight-android-modify-duration
engine.block.setDuration(block = inAnimation, duration = 0.8)
engine.block.setDuration(block = loopAnimation, duration = 2.0)
val updatedDuration = engine.block.getDuration(block = inAnimation)
```
When you change an In or Out animation duration, CE.SDK keeps paired entrance and exit animations from overlapping on the same block.
## Changing Easing Functions
Easing controls the acceleration curve during the animation. Use `setEnum` with the `animationEasing` property and one of the values exposed by `AnimationEasingType`.
```kotlin highlight-android-change-easing
// animationEasing is the Engine property key for animation acceleration curves.
engine.block.setEnum(
block = inAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_IN_OUT.key,
)
val updatedEasing = engine.block.getEnum(
block = inAnimation,
property = "animationEasing",
)
val easingOptions = engine.block.getEnumValues(enumProperty = "animationEasing")
```
Use `getEnumValues` to discover the active easing set at runtime. The public Android enum exposes these values:
| Easing | Description |
| --- | --- |
| `Linear` | Constant speed throughout |
| `EaseIn` | Starts slow, then accelerates |
| `EaseOut` | Starts fast, then decelerates |
| `EaseInOut` | Starts slow, speeds up, then slows down again |
| `EaseInQuart` | Quartic curve that accelerates toward the end |
| `EaseOutQuart` | Quartic curve that decelerates toward the end |
| `EaseInOutQuart` | Quartic curve that eases at both ends |
| `EaseInQuint` | Quintic curve that accelerates toward the end |
| `EaseOutQuint` | Quintic curve that decelerates toward the end |
| `EaseInOutQuint` | Quintic curve that eases at both ends |
| `EaseInBack` | Overshooting curve that accelerates into the motion |
| `EaseOutBack` | Overshooting curve that decelerates out of the motion |
| `EaseInOutBack` | Overshooting curve that eases at both ends |
| `EaseInSpring` | Spring-style curve that accelerates into the motion |
| `EaseOutSpring` | Spring-style curve that decelerates out of the motion |
| `EaseInOutSpring` | Spring-style curve that eases at both ends |
## Adjusting Animation-Specific Properties
Each animation type has its own configurable properties. For slide animations, set `animation/slide/direction` with a radian value to change the travel direction, and set `animation/slide/fade` to blend opacity during the slide.
```kotlin highlight-android-adjust-properties
// Slide direction is exposed as radians.
engine.block.setFloat(
block = inAnimation,
property = "animation/slide/direction",
value = Math.PI.toFloat(),
)
val slideDirection = engine.block.getFloat(
block = inAnimation,
property = "animation/slide/direction",
)
// Slide fade is exposed as a boolean property.
engine.block.setBoolean(
block = inAnimation,
property = "animation/slide/fade",
value = true,
)
val slideFade = engine.block.getBoolean(
block = inAnimation,
property = "animation/slide/fade",
)
```
Slide direction describes how the block moves:
- `0` — Slides right and enters from the left
- `Math.PI / 2` — Slides down and enters from the top
- `Math.PI` — Slides left and enters from the right
- `3 * Math.PI / 2` — Slides up and enters from the bottom
For text animations, use the same property APIs with `textAnimationWritingStyle` (Block, Line, Word, Character) and `textAnimationOverlap` (0 for sequential, 1 for simultaneous). See [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) for the text-specific flow.
## Replacing Animations
To change an animation type, destroy the existing animation block before attaching a new one. Check the slot first because empty animation slots return an invalid handle.
```kotlin highlight-android-replace-animation
val currentInAnimation = engine.block.getInAnimation(block = block)
if (engine.block.isValid(block = currentInAnimation)) {
engine.block.destroy(block = currentInAnimation)
}
val zoomAnimation = engine.block.createAnimation(type = AnimationType.Zoom)
engine.block.setInAnimation(block = block, animation = zoomAnimation)
engine.block.setDuration(block = zoomAnimation, duration = 0.6)
engine.block.setEnum(
block = zoomAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_IN_OUT.key,
)
val replacementType = engine.block.getType(
block = engine.block.getInAnimation(block = block),
)
```
The replacement animation can then receive its own duration, easing, and type-specific properties. Detached animation blocks are not cleaned up automatically.
## Removing Animations
Remove an animation by destroying the attached animation block. After destruction, the slot getter returns an invalid handle.
```kotlin highlight-android-remove-animation
val currentLoopAnimation = engine.block.getLoopAnimation(block = block)
if (engine.block.isValid(block = currentLoopAnimation)) {
engine.block.destroy(block = currentLoopAnimation)
}
val loopAnimationRemoved = !engine.block.isValid(
block = engine.block.getLoopAnimation(block = block),
)
```
Destroying a design block also destroys its attached animations. You only need to destroy animations manually when you replace or detach them while keeping the design block.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.block.createAnimation(type=_)` | Create an animation block for a supported `AnimationType`. |
| `engine.block.setInAnimation(block=_, animation=_)` | Attach an entrance animation to a design block. |
| `engine.block.setOutAnimation(block=_, animation=_)` | Attach an exit animation to a design block. |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Attach a repeated loop animation to a design block. |
| `engine.block.getInAnimation(block=_)` | Read the current entrance animation handle. |
| `engine.block.getOutAnimation(block=_)` | Read the current exit animation handle. |
| `engine.block.getLoopAnimation(block=_)` | Read the current loop animation handle. |
| `engine.block.isValid(block=_)` | Check whether a returned animation handle is usable. |
| `engine.block.getType(block=_)` | Read the animation block type. |
| `engine.block.getDuration(block=_)` | Read animation duration in seconds. |
| `engine.block.setDuration(block=_, duration=_)` | Set animation duration in seconds. |
| `engine.block.getEnum(block=_, property=_)` | Read an enum property such as `animationEasing`. |
| `engine.block.setEnum(block=_, property="animationEasing", value=_)` | Set an enum animation property. |
| `engine.block.getEnumValues(enumProperty="animationEasing")` | List supported easing values. |
| `engine.block.findAllProperties(block=_)` | Discover properties supported by a specific animation block. |
| `engine.block.getFloat(block=_, property="animation/slide/direction")` | Read a numeric animation property. |
| `engine.block.setFloat(block=_, property="animation/slide/direction", value=_)` | Set a numeric animation property. |
| `engine.block.getBoolean(block=_, property="animation/slide/fade")` | Read a boolean animation property. |
| `engine.block.setBoolean(block=_, property="animation/slide/fade", value=_)` | Set a boolean animation property. |
| `engine.block.destroy(block=_)` | Destroy a replaced or removed animation block. |
## Next Steps
- [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) — Create entrance, exit, and loop animations
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) — Animate text with writing styles and character control
- [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) — Understand animation concepts and capabilities
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Overview"
description: "Add motion to video scenes with preset animation controls and programmatic animation APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/)
---
Animations in CreativeEditor SDK (CE.SDK) bring your designs to life by adding motion to images, text, and design elements in video scenes. Whether you're creating a dynamic social media post, a video ad, or an engaging product demo, animations help capture attention and communicate ideas more effectively.
The editor UI can expose preset in, out, and loop animations for selected video, image, sticker, shape, and text blocks. You can adjust the properties exposed by each preset in the UI where available, or control animations programmatically with the CreativeEngine API.
Android integrations can use the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) Animations inspector for supported block types.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
## Timeline and Preset Timing
Animations in CE.SDK are time-based presets attached to blocks in video scenes. In, out, and loop animations start relative to block visibility on the page timeline.
In animations play when a block appears, out animations play when it leaves, and loop animations repeat while the block is visible. CE.SDK animations are preset effects, so they do not expose custom per-property keyframe editing.
Use the editor UI for supported preset selection and property adjustments, or use CreativeEngine for programmatic setup and rendering. Use MP4 export when you need to preserve motion in the final output.
## Next Steps
- [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) - Explore the types of animations
supported by CE.SDK, including object, text, and transition effects.
- [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) - Build animations manually or with
presets to animate objects, text, and scenes within your design.
- [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) — Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks.
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) - Animate text elements with effects like
fade, typewriter, and bounce for dynamic visual presentation.
- [Programmatic](https://img.ly/docs/cesdk/android/animation/programmatic-eb359c/) — Documentation for Programmatic
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and
edit video clips, audio, and animations frame by frame.
- [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) - Learn how to create and customize
videos in CE.SDK using scenes, assets, and time-based editing.
- [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/) - Export video compositions as MP4 files with configurable encoding options, progress tracking, and resolution control.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Programmatic Animations"
description: "Control Android video-scene animations with typed CreativeEngine animation APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/programmatic-eb359c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Programmatic](https://img.ly/docs/cesdk/android/animation/programmatic-eb359c/)
---
```kotlin file=@cesdk_android_examples/engine-guides-programmatic-animations/ProgrammaticAnimations.kt reference-only
import ly.img.engine.AnimationEasingType
import ly.img.engine.AnimationType
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
data class ProgrammaticAnimations(
val slideInType: String,
val firstLoopType: String,
val replacementLoopType: String,
val outType: String,
val slideDirection: Float,
val slideDuration: Double,
val slideEasing: String,
val slideProperties: List,
val easingValues: List,
val textWritingStyle: String,
val textOverlap: Float,
)
suspend fun programmaticAnimations(engine: Engine): ProgrammaticAnimations {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 1080F)
engine.block.setHeight(block = page, value = 1080F)
engine.block.setDuration(block = page, duration = 5.0)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block = block, value = 290F)
engine.block.setPositionY(block = block, value = 260F)
engine.block.setWidth(block = block, value = 500F)
engine.block.setHeight(block = block, value = 320F)
engine.block.setDuration(block = block, duration = 4.0)
engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color))
engine.block.appendChild(parent = page, child = block)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.setPositionX(block = textBlock, value = 180F)
engine.block.setPositionY(block = textBlock, value = 700F)
engine.block.setWidth(block = textBlock, value = 720F)
engine.block.setHeightMode(block = textBlock, mode = SizeMode.AUTO)
engine.block.replaceText(textBlock, "Animate text one word at a time")
check(engine.block.supportsAnimation(block)) {
"This block does not support animations."
}
val slideInAnimation = engine.block.createAnimation(AnimationType.Slide)
val breathingLoopAnimation = engine.block.createAnimation(AnimationType.BreathingLoop)
val fadeOutAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setInAnimation(block = block, animation = slideInAnimation)
engine.block.setLoopAnimation(block = block, animation = breathingLoopAnimation)
engine.block.setOutAnimation(block = block, animation = fadeOutAnimation)
val slideProperties = engine.block.findAllProperties(slideInAnimation)
val easingValues = engine.block.getEnumValues("animationEasing")
check(slideProperties.contains("animation/slide/direction")) {
"Slide animations do not expose animation/slide/direction."
}
check(easingValues.contains(AnimationEasingType.EASE_OUT.key)) {
"The animationEasing enum does not expose ${AnimationEasingType.EASE_OUT.key}."
}
engine.block.setDuration(block = slideInAnimation, duration = 0.6)
engine.block.setEnum(
block = slideInAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
// No type-safe Android helper exists for this animation-specific property yet.
engine.block.setFloat(
block = slideInAnimation,
property = "animation/slide/direction",
value = 0.5F * Math.PI.toFloat(),
)
val currentInAnimation = engine.block.getInAnimation(block)
val currentLoopAnimation = engine.block.getLoopAnimation(block)
val currentOutAnimation = engine.block.getOutAnimation(block)
check(engine.block.isValid(currentInAnimation))
val currentLoopType = engine.block.getType(currentLoopAnimation)
val previousLoopAnimation = engine.block.getLoopAnimation(block)
val squeezeLoopAnimation = engine.block.createAnimation(AnimationType.SqueezeLoop)
engine.block.destroy(previousLoopAnimation)
engine.block.setLoopAnimation(block = block, animation = squeezeLoopAnimation)
val textAnimation = engine.block.createAnimation(AnimationType.Baseline)
engine.block.setInAnimation(block = textBlock, animation = textAnimation)
engine.block.setEnum(block = textAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setFloat(block = textAnimation, property = "textAnimationOverlap", value = 0.4F)
val replacementLoopAnimation = engine.block.getLoopAnimation(block)
check(engine.block.getType(currentInAnimation) == AnimationType.Slide.key)
check(currentLoopType == AnimationType.BreathingLoop.key)
check(engine.block.getType(replacementLoopAnimation) == AnimationType.SqueezeLoop.key)
check(engine.block.getType(currentOutAnimation) == AnimationType.Fade.key)
check(slideProperties.contains("animation/slide/direction"))
check(easingValues.contains(AnimationEasingType.EASE_OUT.key))
check(engine.block.getFloat(slideInAnimation, "animation/slide/direction") == 0.5F * Math.PI.toFloat())
check(engine.block.getDuration(slideInAnimation) == 0.6)
check(engine.block.getEnum(slideInAnimation, "animationEasing") == AnimationEasingType.EASE_OUT.key)
check(engine.block.getEnum(textAnimation, "textAnimationWritingStyle") == "Word")
check(engine.block.getFloat(textAnimation, "textAnimationOverlap") == 0.4F)
return ProgrammaticAnimations(
slideInType = engine.block.getType(currentInAnimation),
firstLoopType = currentLoopType,
replacementLoopType = engine.block.getType(replacementLoopAnimation),
outType = engine.block.getType(currentOutAnimation),
slideDirection = engine.block.getFloat(slideInAnimation, "animation/slide/direction"),
slideDuration = engine.block.getDuration(slideInAnimation),
slideEasing = engine.block.getEnum(slideInAnimation, "animationEasing"),
slideProperties = slideProperties,
easingValues = easingValues,
textWritingStyle = engine.block.getEnum(textAnimation, "textAnimationWritingStyle"),
textOverlap = engine.block.getFloat(textAnimation, "textAnimationOverlap"),
)
}
```
Create, assign, inspect, and replace CE.SDK animations from Android code by
using CreativeEngine block APIs.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-programmatic-animations)
Programmatic animation control is useful for automation, templates, and custom mobile controls. Animations belong to video scenes and attach to design blocks through separate In, Loop, and Out slots.
## Check Animation Support
Check a target block before you create and attach animation objects. Not every block type supports animation, so the sample fails early when the selected block is not animatable.
```kotlin highlight-android-check-support
check(engine.block.supportsAnimation(block)) {
"This block does not support animations."
}
```
## Create Typed Animations
Create animation objects with Android's typed `AnimationType` values instead of raw animation type strings. In and Out slots can use block animation types such as `Slide`, `Pan`, `Fade`, `Blur`, `Grow`, `Zoom`, `Pop`, `Wipe`, `Baseline`, `CropZoom`, `Spin`, and `KenBurns`; Loop slots use loop types such as `SpinLoop`, `FadeLoop`, `BlurLoop`, `PulsatingLoop`, `BreathingLoop`, `JumpLoop`, `SqueezeLoop`, `SwayLoop`, and `ScaleLoop`. See the [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) catalog for the full list of supported block animation types.
```kotlin highlight-android-create-animations
val slideInAnimation = engine.block.createAnimation(AnimationType.Slide)
val breathingLoopAnimation = engine.block.createAnimation(AnimationType.BreathingLoop)
val fadeOutAnimation = engine.block.createAnimation(AnimationType.Fade)
```
## Attach Animations to Slots
Each design block has one In animation slot, one Loop animation slot, and one Out animation slot. Assign the animation object to the matching slot with `setInAnimation`, `setLoopAnimation`, or `setOutAnimation`.
```kotlin highlight-android-attach-animations
engine.block.setInAnimation(block = block, animation = slideInAnimation)
engine.block.setLoopAnimation(block = block, animation = breathingLoopAnimation)
engine.block.setOutAnimation(block = block, animation = fadeOutAnimation)
```
Setting a different animation for the same slot replaces that slot's association. Keep the previous animation handle if you plan to destroy or reuse it later.
## Configure Timing and Properties
Use `setDuration` for animation timing, `setEnum` for easing values, and type-specific property keys for properties such as slide direction. The sample also asks the engine which properties and easing values are available before relying on them.
```kotlin highlight-android-configure-properties
val slideProperties = engine.block.findAllProperties(slideInAnimation)
val easingValues = engine.block.getEnumValues("animationEasing")
check(slideProperties.contains("animation/slide/direction")) {
"Slide animations do not expose animation/slide/direction."
}
check(easingValues.contains(AnimationEasingType.EASE_OUT.key)) {
"The animationEasing enum does not expose ${AnimationEasingType.EASE_OUT.key}."
}
engine.block.setDuration(block = slideInAnimation, duration = 0.6)
engine.block.setEnum(
block = slideInAnimation,
property = "animationEasing",
value = AnimationEasingType.EASE_OUT.key,
)
// No type-safe Android helper exists for this animation-specific property yet.
engine.block.setFloat(
block = slideInAnimation,
property = "animation/slide/direction",
value = 0.5F * Math.PI.toFloat(),
)
```
The slide direction is stored in radians. In this example, `0.5F * Math.PI.toFloat()` makes the block slide along the vertical direction.
## Read and Replace Animations
Read the current slot handles when you need to inspect or replace animations. The returned handle can be passed to other block APIs, including `getType` and `isValid`.
```kotlin highlight-android-read-animations
val currentInAnimation = engine.block.getInAnimation(block)
val currentLoopAnimation = engine.block.getLoopAnimation(block)
val currentOutAnimation = engine.block.getOutAnimation(block)
check(engine.block.isValid(currentInAnimation))
val currentLoopType = engine.block.getType(currentLoopAnimation)
```
Destroy detached animation objects when you replace them and no longer need them. Destroying a design block also destroys its attached animations, but standalone animation objects should be cleaned up explicitly.
```kotlin highlight-android-replace-animation
val previousLoopAnimation = engine.block.getLoopAnimation(block)
val squeezeLoopAnimation = engine.block.createAnimation(AnimationType.SqueezeLoop)
engine.block.destroy(previousLoopAnimation)
engine.block.setLoopAnimation(block = block, animation = squeezeLoopAnimation)
```
## Configure Text Animation Properties
Text animations use the same property APIs. The sample attaches `AnimationType.Baseline` to a text block; dedicated text animation presets also include `TypewriterText`, `BlockSwipeText`, `SpreadText`, and `MergeText`. `textAnimationWritingStyle` controls whether text animates as a block, by line, by word, or by character, and `textAnimationOverlap` controls the timing overlap between those segments.
```kotlin highlight-android-text-animation-properties
val textAnimation = engine.block.createAnimation(AnimationType.Baseline)
engine.block.setInAnimation(block = textBlock, animation = textAnimation)
engine.block.setEnum(block = textAnimation, property = "textAnimationWritingStyle", value = "Word")
engine.block.setFloat(block = textAnimation, property = "textAnimationOverlap", value = 0.4F)
```
For detailed text animation behavior and type coverage, continue with [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/).
## API Reference
| API | Purpose |
| --- | --- |
| `engine.block.supportsAnimation(block=_)` | Check whether a block supports animation slots. |
| `engine.block.createAnimation(type=_)` | Create an animation object from an `AnimationType`. |
| `engine.block.setInAnimation(block=_, animation=_)` | Attach an entrance animation. |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Attach a looping animation. |
| `engine.block.setOutAnimation(block=_, animation=_)` | Attach an exit animation. |
| `engine.block.getInAnimation(block=_)` | Read the entrance animation handle. |
| `engine.block.getLoopAnimation(block=_)` | Read the looping animation handle. |
| `engine.block.getOutAnimation(block=_)` | Read the exit animation handle. |
| `engine.block.isValid(block=_)` | Check that a returned animation handle is still valid. |
| `engine.block.getType(block=_)` | Inspect an animation object's concrete type. |
| `engine.block.setDuration(block=_, duration=_)` | Set animation duration in seconds. |
| `engine.block.setEnum(block=_, property=_, value=_)` | Set enum properties such as easing or text writing style. |
| `engine.block.setFloat(block=_, property=_, value=_)` | Set numeric animation properties. |
| `engine.block.findAllProperties(block=_)` | Discover properties supported by an animation object. |
| `engine.block.getEnumValues(enumProperty=_)` | Discover enum values for a property. |
| `engine.block.destroy(block=_)` | Destroy an animation object when replacing or removing it. |
## Next Steps
- [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) - Review the full catalog of block animation setup and type details.
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) - Configure text-specific animation behavior.
- [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) — Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks.
- [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) - Review the animation model and supported platforms.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Supported Animation Types"
description: "Apply different animation types to design blocks in CE.SDK and configure their properties."
platform: android
url: "https://img.ly/docs/cesdk/android/animation/types-4e5f41/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/)
---
```kotlin file=@cesdk_android_examples/engine-guides-animation-types/AnimationTypes.kt reference-only
import ly.img.engine.AnimationType
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import kotlin.math.PI
data class AnimationTypes(
val slideDirection: Float,
val fadeEasing: String,
val zoomUsesFade: Boolean,
val wipeDirection: String,
val breathingIntensity: Float,
val spinDirection: String,
val spinIntensity: Float,
val slideProperties: List,
val easingOptions: List,
)
fun animationTypes(engine: Engine): AnimationTypes {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1920F)
engine.block.setHeight(page, value = 1080F)
engine.block.setDuration(page, duration = 6.0)
val pageFill = engine.block.createFill(FillType.Color)
engine.block.setColor(pageFill, property = "fill/color/value", value = Color.fromRGBA(250, 250, 252))
engine.block.setFill(page, fill = pageFill)
val demoColors = listOf(
Color.fromRGBA(67, 97, 238),
Color.fromRGBA(239, 71, 111),
Color.fromRGBA(255, 209, 102),
Color.fromRGBA(6, 214, 160),
Color.fromRGBA(17, 138, 178),
Color.fromRGBA(131, 56, 236),
)
val columns = 2
val blockWidth = 900F
val blockHeight = 300F
val blocks = demoColors.mapIndexed { index, color ->
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = 30F + (index % columns) * (blockWidth + 60F))
engine.block.setPositionY(block, value = 30F + (index / columns) * (blockHeight + 60F))
engine.block.setWidth(block, value = blockWidth)
engine.block.setHeight(block, value = blockHeight)
engine.block.setDuration(block, duration = 5.0)
val fill = engine.block.createFill(FillType.Color)
engine.block.setColor(fill, property = "fill/color/value", value = color)
engine.block.setFill(block, fill = fill)
engine.block.appendChild(parent = page, child = block)
block
}
blocks.forEach { block ->
check(engine.block.supportsAnimation(block))
}
val slideBlock = blocks[0]
val slideAnimation = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = slideBlock, animation = slideAnimation)
engine.block.setDuration(block = slideAnimation, duration = 1.0)
// Animation-specific fields are exposed through the generic property API.
engine.block.setFloat(
block = slideAnimation,
property = "animation/slide/direction",
value = PI.toFloat(),
)
engine.block.setEnum(block = slideAnimation, property = "animationEasing", value = "EaseOut")
val fadeBlock = blocks[1]
val fadeAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setInAnimation(block = fadeBlock, animation = fadeAnimation)
engine.block.setDuration(block = fadeAnimation, duration = 1.0)
engine.block.setEnum(block = fadeAnimation, property = "animationEasing", value = "EaseInOut")
val zoomBlock = blocks[2]
val zoomAnimation = engine.block.createAnimation(AnimationType.Zoom)
engine.block.setInAnimation(block = zoomBlock, animation = zoomAnimation)
engine.block.setDuration(block = zoomAnimation, duration = 1.0)
engine.block.setBoolean(block = zoomAnimation, property = "animation/zoom/fade", value = true)
val exitBlock = blocks[3]
val wipeIn = engine.block.createAnimation(AnimationType.Wipe)
engine.block.setInAnimation(block = exitBlock, animation = wipeIn)
engine.block.setDuration(block = wipeIn, duration = 1.0)
engine.block.setEnum(block = wipeIn, property = "animation/wipe/direction", value = "Right")
val fadeOut = engine.block.createAnimation(AnimationType.Fade)
engine.block.setOutAnimation(block = exitBlock, animation = fadeOut)
engine.block.setDuration(block = fadeOut, duration = 1.0)
engine.block.setEnum(block = fadeOut, property = "animationEasing", value = "EaseIn")
val loopBlock = blocks[4]
val breathingLoop = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setLoopAnimation(block = loopBlock, animation = breathingLoop)
engine.block.setDuration(block = breathingLoop, duration = 2.0)
// Intensity 0 scales to 1.25, while intensity 1 scales to 2.5.
engine.block.setFloat(
block = breathingLoop,
property = "animation/breathing_loop/intensity",
value = 0.3F,
)
val combinedBlock = blocks[5]
val spinIn = engine.block.createAnimation(AnimationType.Spin)
engine.block.setInAnimation(block = combinedBlock, animation = spinIn)
engine.block.setDuration(block = spinIn, duration = 1.0)
engine.block.setEnum(block = spinIn, property = "animation/spin/direction", value = "Clockwise")
engine.block.setFloat(block = spinIn, property = "animation/spin/intensity", value = 0.5F)
val blurOut = engine.block.createAnimation(AnimationType.Blur)
engine.block.setOutAnimation(block = combinedBlock, animation = blurOut)
engine.block.setDuration(block = blurOut, duration = 1.0)
val swayLoop = engine.block.createAnimation(AnimationType.SwayLoop)
engine.block.setLoopAnimation(block = combinedBlock, animation = swayLoop)
engine.block.setDuration(block = swayLoop, duration = 1.5)
val slideProperties = engine.block.findAllProperties(slideAnimation)
val easingOptions = engine.block.getEnumValues("animationEasing")
engine.block.setPlaybackTime(page, time = 1.9)
return AnimationTypes(
slideDirection = engine.block.getFloat(slideAnimation, property = "animation/slide/direction"),
fadeEasing = engine.block.getEnum(fadeAnimation, property = "animationEasing"),
zoomUsesFade = engine.block.getBoolean(zoomAnimation, property = "animation/zoom/fade"),
wipeDirection = engine.block.getEnum(wipeIn, property = "animation/wipe/direction"),
breathingIntensity = engine.block.getFloat(breathingLoop, property = "animation/breathing_loop/intensity"),
spinDirection = engine.block.getEnum(spinIn, property = "animation/spin/direction"),
spinIntensity = engine.block.getFloat(spinIn, property = "animation/spin/intensity"),
slideProperties = slideProperties,
easingOptions = easingOptions,
)
}
```
Apply entrance, exit, and loop animations to design blocks using the available animation types in CE.SDK.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-animation-types)
CE.SDK organizes animations into three categories: entrance (In), exit (Out), and loop. Each category determines when the animation plays during the block's lifecycle. This guide demonstrates different animation types and their configurable properties.
The snippets use existing graphic blocks in a video scene and focus on the animation APIs. Use the [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) guide when you need the lower-level flow for creating, attaching, replacing, and reading animations.
## Entrance Animations
Entrance animations define how a block appears. Use `createAnimation()` with an `AnimationType` and attach it with `setInAnimation()`.
### Slide Animation
The slide animation moves a block in from a specified direction. The `animation/slide/direction` property uses radians where `0` is right, `PI / 2` is bottom, `PI` is left, and `3 * PI / 2` is top. Use `animation/slide/fade` when the slide should also fade opacity during the movement.
```kotlin highlight-android-entrance-slide
val slideAnimation = engine.block.createAnimation(AnimationType.Slide)
engine.block.setInAnimation(block = slideBlock, animation = slideAnimation)
engine.block.setDuration(block = slideAnimation, duration = 1.0)
// Animation-specific fields are exposed through the generic property API.
engine.block.setFloat(
block = slideAnimation,
property = "animation/slide/direction",
value = PI.toFloat(),
)
engine.block.setEnum(block = slideAnimation, property = "animationEasing", value = "EaseOut")
```
### Fade Animation
The fade animation transitions opacity from invisible to fully visible. Easing controls the animation curve.
```kotlin highlight-android-entrance-fade
val fadeAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setInAnimation(block = fadeBlock, animation = fadeAnimation)
engine.block.setDuration(block = fadeAnimation, duration = 1.0)
engine.block.setEnum(block = fadeAnimation, property = "animationEasing", value = "EaseInOut")
```
### Zoom Animation
The zoom animation scales the block from a smaller size to its final dimensions. The `animation/zoom/fade` property adds an opacity transition during scaling.
```kotlin highlight-android-entrance-zoom
val zoomAnimation = engine.block.createAnimation(AnimationType.Zoom)
engine.block.setInAnimation(block = zoomBlock, animation = zoomAnimation)
engine.block.setDuration(block = zoomAnimation, duration = 1.0)
engine.block.setBoolean(block = zoomAnimation, property = "animation/zoom/fade", value = true)
```
Additional entrance animation types include:
- `AnimationType.Pan` - Moves content across the block
- `AnimationType.Blur` - Transitions from blurred to clear
- `AnimationType.Wipe` - Reveals with a directional wipe
- `AnimationType.Baseline` - Slides text in along its baseline
- `AnimationType.Pop` - Uses a bouncy scale effect
- `AnimationType.Spin` - Rotates the block into view
- `AnimationType.Grow` - Scales up from a point
- `AnimationType.CropZoom` - Zooms content inside the block frame
- `AnimationType.KenBurns` - Pans and zooms image or video content
Text-only entrance animation types include:
- `AnimationType.TypewriterText` - Text-only character reveal
- `AnimationType.BlockSwipeText` - Text-only block sweep reveal
- `AnimationType.SpreadText` - Text-only letter spacing effect
- `AnimationType.MergeText` - Text-only line merge effect
## Exit Animations
Exit animations define how a block leaves the screen. Use `setOutAnimation()` to attach them. CE.SDK prevents overlap between entrance and exit durations automatically.
```kotlin highlight-android-exit-animation
val wipeIn = engine.block.createAnimation(AnimationType.Wipe)
engine.block.setInAnimation(block = exitBlock, animation = wipeIn)
engine.block.setDuration(block = wipeIn, duration = 1.0)
engine.block.setEnum(block = wipeIn, property = "animation/wipe/direction", value = "Right")
val fadeOut = engine.block.createAnimation(AnimationType.Fade)
engine.block.setOutAnimation(block = exitBlock, animation = fadeOut)
engine.block.setDuration(block = fadeOut, duration = 1.0)
engine.block.setEnum(block = fadeOut, property = "animationEasing", value = "EaseIn")
```
In this example, a wipe entrance transitions to a fade exit. Mirror entrance effects for visual consistency, or use contrasting effects for emphasis.
## Loop Animations
Loop animations run continuously while the block is visible. They can combine with entrance and exit animations. Use `setLoopAnimation()` to attach them.
```kotlin highlight-android-loop-animation
val breathingLoop = engine.block.createAnimation(AnimationType.BreathingLoop)
engine.block.setLoopAnimation(block = loopBlock, animation = breathingLoop)
engine.block.setDuration(block = breathingLoop, duration = 2.0)
// Intensity 0 scales to 1.25, while intensity 1 scales to 2.5.
engine.block.setFloat(
block = breathingLoop,
property = "animation/breathing_loop/intensity",
value = 0.3F,
)
```
The duration controls each cycle length. Loop animation types include:
- `AnimationType.BreathingLoop` - Slow scale pulse
- `AnimationType.PulsatingLoop` - Rhythmic scale
- `AnimationType.SpinLoop` - Continuous rotation
- `AnimationType.FadeLoop` - Opacity cycling
- `AnimationType.SwayLoop` - Rotational oscillation
- `AnimationType.JumpLoop` - Jumping motion
- `AnimationType.BlurLoop` - Blur cycling
- `AnimationType.SqueezeLoop` - Squeezing effect
- `AnimationType.ScaleLoop` - Continuous scale animation
## Combined Animations
A single block can have entrance, exit, and loop animations running together. The loop animation runs throughout the block's visibility while entrance and exit animations play at the appropriate times. For spin animations, `animation/spin/fade` controls whether the rotation also fades opacity.
```kotlin highlight-android-combined-animations
val spinIn = engine.block.createAnimation(AnimationType.Spin)
engine.block.setInAnimation(block = combinedBlock, animation = spinIn)
engine.block.setDuration(block = spinIn, duration = 1.0)
engine.block.setEnum(block = spinIn, property = "animation/spin/direction", value = "Clockwise")
engine.block.setFloat(block = spinIn, property = "animation/spin/intensity", value = 0.5F)
val blurOut = engine.block.createAnimation(AnimationType.Blur)
engine.block.setOutAnimation(block = combinedBlock, animation = blurOut)
engine.block.setDuration(block = blurOut, duration = 1.0)
val swayLoop = engine.block.createAnimation(AnimationType.SwayLoop)
engine.block.setLoopAnimation(block = combinedBlock, animation = swayLoop)
engine.block.setDuration(block = swayLoop, duration = 1.5)
```
## Configuring Animation Properties
Each animation type has specific configurable properties. Use `findAllProperties()` to discover available properties and `getEnumValues()` to query options for enum properties.
```kotlin highlight-android-discover-properties
val slideProperties = engine.block.findAllProperties(slideAnimation)
val easingOptions = engine.block.getEnumValues("animationEasing")
```
Common configurable properties include:
- **Direction**: Controls entry or exit direction in radians or enum values
- **Easing**: Sets the animation curve, such as `Linear`, `EaseIn`, `EaseOut`, or `EaseInOut`
- **Intensity**: Controls the strength of the effect, with exact behavior depending on the animation type
- **Fade**: Adds or removes an opacity transition
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.supportsAnimation(block=_)` | Returns whether the block can have animations. |
| `engine.block.createAnimation(type=_)` | Creates an animation of the given `AnimationType`. |
| `engine.block.setInAnimation(block=_, animation=_)` | Attaches an entrance animation to a block. |
| `engine.block.setOutAnimation(block=_, animation=_)` | Attaches an exit animation to a block. |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Attaches a loop animation to a block. |
| `engine.block.setDuration(block=_, duration=_)` | Sets the animation duration in seconds. |
| `engine.block.setFloat(block=_, property="animation/slide/direction", value=_)` | Sets the slide direction in radians. |
| `engine.block.setBoolean(block=_, property="animation/slide/fade", value=_)` | Enables or disables opacity fading during a slide animation. |
| `engine.block.setFloat(block=_, property="animation/breathing_loop/intensity", value=_)` | Sets the breathing-loop scale intensity. |
| `engine.block.setFloat(block=_, property="animation/spin/intensity", value=_)` | Sets how far the spin animation rotates. |
| `engine.block.setEnum(block=_, property="animationEasing", value=_)` | Sets the animation easing curve: `Linear`, `EaseIn`, `EaseOut`, `EaseInOut`, `EaseInQuart`, `EaseOutQuart`, `EaseInOutQuart`, `EaseInQuint`, `EaseOutQuint`, `EaseInOutQuint`, `EaseInBack`, `EaseOutBack`, `EaseInOutBack`, `EaseInSpring`, `EaseOutSpring`, or `EaseInOutSpring`. |
| `engine.block.setEnum(block=_, property="animation/wipe/direction", value=_)` | Sets the wipe direction: `Up`, `Right`, `Down`, or `Left`. |
| `engine.block.setEnum(block=_, property="animation/spin/direction", value=_)` | Sets the spin direction: `Clockwise` or `CounterClockwise`. |
| `engine.block.setBoolean(block=_, property="animation/spin/fade", value=_)` | Enables or disables opacity fading during a spin animation. |
| `engine.block.setBoolean(block=_, property="animation/zoom/fade", value=_)` | Enables or disables the zoom fade. |
| `engine.block.findAllProperties(block=_)` | Lists the properties available on an animation block. |
| `engine.block.getEnumValues(enumProperty=_)` | Lists supported values for an enum property. |
## Next Steps
- [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) - Create and attach animations to blocks
- [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) - Animate text with writing styles
- [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) - Review animation concepts and capabilities
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "API Reference"
description: "Find out how to use the API of the CESDK."
platform: android
url: "https://img.ly/docs/cesdk/android/api-reference/overview-8f24e1/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [API Reference](https://img.ly/docs/cesdk/android/api-reference/overview-8f24e1/)
---
For Android, the following packages are available:
- [ly.img:engine](`$\{props.platform.slug}/api-reference/engine/ly.img[58]engine/ly.img.engine`)
- [ly.img:engine-camera](`$\{props.platform.slug}/api-reference/engine-camera/ly.img[58]engine-camera/ly.img.engine.camera`)
- [ly.img:editor](`$\{props.platform.slug}/api-reference/editor/ly.img[58]editor/ly.img.editor`)
- [ly.img:editor-core](`$\{props.platform.slug}/api-reference/editor-core`)
- [ly.img:camera](`$\{props.platform.slug}/api-reference/camera/ly.img[58]camera/ly.img.camera`)
- [ly.img:camera-core](`$\{props.platform.slug}/api-reference/camera-core/ly.img[58]camera-core/ly.img.camera.core`)
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Automate Workflows"
description: "Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale."
platform: android
url: "https://img.ly/docs/cesdk/android/automation-715209/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/)
---
```kotlin file=@cesdk_android_examples/engine-guides-automate-workflows/AutomateWorkflows.kt reference-only
package ly.img.editor.examples
import android.app.Application
import android.content.Context
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import ly.img.editor.defaultBaseUri
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.io.File
import ly.img.engine.Color as EngineColor
private data class AutomationJob(
val fileStem: String,
val headline: String,
val subline: String,
val cta: String,
val heroImageUri: String,
)
private val automationVariableKeys = listOf("headline", "subline", "cta")
private val automationAssetSourceIds = listOf("ly.img.color.palette", "ly.img.typeface")
private val automationEngineMutex = Mutex()
data class AutomationResult(
val variableKeys: List,
val tokenizedBlockNames: List,
val exportedFiles: List,
)
private sealed interface AutomationUiState {
object Loading : AutomationUiState
data class Success(
val result: AutomationResult,
) : AutomationUiState
data class Error(
val message: String,
) : AutomationUiState
}
@Composable
fun AutomateWorkflowsScreen(license: String) {
val context = LocalContext.current.applicationContext
var uiState by remember { mutableStateOf(AutomationUiState.Loading) }
LaunchedEffect(context, license) {
uiState = runCatching { runStandaloneAutomationWorkflow(context, license) }
.fold(
onSuccess = { AutomationUiState.Success(it) },
onFailure = { AutomationUiState.Error(it.message ?: "Unknown automation error.") },
)
}
Surface(
modifier = Modifier.fillMaxSize(),
) {
when (val state = uiState) {
AutomationUiState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
is AutomationUiState.Error -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Automation failed",
style = MaterialTheme.typography.headlineSmall,
)
Text(
text = state.message,
style = MaterialTheme.typography.bodyLarge,
)
}
}
is AutomationUiState.Success -> {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "Automate Workflows",
style = MaterialTheme.typography.headlineSmall,
)
Text(
text = "Variable store: ${state.result.variableKeys.joinToString()}",
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = "Tokenized blocks: ${state.result.tokenizedBlockNames.joinToString()}",
style = MaterialTheme.typography.bodyMedium,
)
state.result.exportedFiles.forEach { file ->
val bitmap = remember(file.absolutePath) {
BitmapFactory.decodeFile(file.absolutePath)?.asImageBitmap()
}
Text(
text = file.name,
style = MaterialTheme.typography.titleMedium,
)
if (bitmap != null) {
Image(
bitmap = bitmap,
contentDescription = file.name,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4f / 5f),
)
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
}
private suspend fun runStandaloneAutomationWorkflow(
context: Context,
license: String,
): AutomationResult = withContext(Dispatchers.Main) {
automationEngineMutex.withLock {
val application = context.applicationContext as Application
Engine.init(application)
val engine = Engine.getInstance(id = "ly.img.engine.automateWorkflows")
var engineStarted = false
try {
engineStarted = engine.start(
license = license,
userId = "automation-guide",
)
check(engineStarted) { "Unable to start the automation guide Engine." }
engine.bindOffscreen(width = 1080, height = 1350)
runAutomationWorkflow(engine = engine, context = context)
} finally {
if (engineStarted) {
withContext(NonCancellable) {
engine.stop()
}
}
}
}
}
suspend fun runAutomationWorkflow(
engine: Engine,
context: Context,
): AutomationResult = withContext(engine.dispatcher) {
val currentVariableKeys = engine.variable.findAll().toSet()
val previousVariables = automationVariableKeys
.filter(currentVariableKeys::contains)
.associateWith(engine.variable::get)
val originalAssetSources = engine.asset.findAllSources().toSet()
try {
val existingAssetSources = engine.asset.findAllSources().toSet()
val addedAssetSources = automationAssetSourceIds.filterNot(existingAssetSources::contains)
addedAssetSources.forEach { assetSource ->
engine.asset.addLocalSourceFromJSON(
contentUri = defaultBaseUri.buildUpon()
.appendPath(assetSource)
.appendPath("content.json")
.build(),
)
}
runAutomationWorkflowWithTemporaryState(engine = engine, context = context)
} finally {
val variablesToRemove = engine.variable.findAll().toSet()
automationVariableKeys.filter(variablesToRemove::contains).forEach(engine.variable::remove)
previousVariables.forEach { (key, value) -> engine.variable.set(key = key, value = value) }
automationAssetSourceIds.filterNot(originalAssetSources::contains).asReversed().forEach { sourceId ->
if (sourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId)
}
}
}
}
private suspend fun runAutomationWorkflowWithTemporaryState(
engine: Engine,
context: Context,
): AutomationResult {
val outputDirectory = withContext(Dispatchers.IO) {
File(context.cacheDir, "automate-workflows").apply {
mkdirs()
listFiles()?.forEach(File::delete)
}
}
val templateScene = createTemplateScene(engine)
val tokenizedBlockNames = discoverTokenizedBlocks(engine)
val jobs = listOf(
AutomationJob(
fileStem = "summer-sale",
headline = "Summer Sale",
subline = "Save 25% on the launch collection.",
cta = "Shop Now",
heroImageUri = "https://img.ly/static/ubq_samples/sample_1.jpg",
),
AutomationJob(
fileStem = "autumn-launch",
headline = "Autumn Launch",
subline = "New arrivals for cozy desk setups.",
cta = "Explore",
heroImageUri = "https://img.ly/static/ubq_samples/sample_4.jpg",
),
)
val exportedFiles = jobs.map { job ->
exportAutomationJob(
engine = engine,
templateScene = templateScene,
job = job,
outputDirectory = outputDirectory,
)
}
return AutomationResult(
variableKeys = automationVariableKeys.sorted(),
tokenizedBlockNames = tokenizedBlockNames,
exportedFiles = exportedFiles,
)
}
private suspend fun createTemplateScene(engine: Engine): String {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1350F)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = backgroundFill,
property = "fill/color/value",
value = EngineColor.fromRGBA(r = 0.96F, g = 0.94F, b = 0.90F, a = 1F),
)
engine.block.setFill(background, fill = backgroundFill)
engine.block.setWidth(background, value = 1080F)
engine.block.setHeight(background, value = 1350F)
engine.block.appendChild(parent = page, child = background)
val heroImage = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(heroImage, name = "hero-image")
engine.block.setShape(heroImage, shape = engine.block.createShape(ShapeType.Rect))
val heroFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = heroFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_2.jpg",
)
engine.block.setFill(heroImage, fill = heroFill)
engine.block.setWidth(heroImage, value = 860F)
engine.block.setHeight(heroImage, value = 720F)
engine.block.setPositionX(heroImage, value = 110F)
engine.block.setPositionY(heroImage, value = 100F)
engine.block.appendChild(parent = page, child = heroImage)
val copyPanel = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(copyPanel, shape = engine.block.createShape(ShapeType.Rect))
val copyPanelFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = copyPanelFill,
property = "fill/color/value",
value = EngineColor.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.92F),
)
engine.block.setFill(copyPanel, fill = copyPanelFill)
engine.block.setWidth(copyPanel, value = 860F)
engine.block.setHeight(copyPanel, value = 360F)
engine.block.setPositionX(copyPanel, value = 110F)
engine.block.setPositionY(copyPanel, value = 860F)
engine.block.appendChild(parent = page, child = copyPanel)
val headline = engine.block.create(DesignBlockType.Text)
engine.block.setName(headline, name = "headline-copy")
engine.block.setString(headline, property = "text/text", value = "{{headline}}")
engine.block.setTextFontSize(headline, fontSize = 14F)
engine.block.setTextColor(
headline,
color = EngineColor.fromRGBA(r = 0.12F, g = 0.10F, b = 0.15F, a = 1F),
)
engine.block.setWidth(headline, value = 700F)
engine.block.setWidthMode(headline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(headline, mode = SizeMode.AUTO)
engine.block.setBoolean(headline, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(headline, value = 160F)
engine.block.setPositionY(headline, value = 915F)
engine.block.appendChild(parent = page, child = headline)
val subline = engine.block.create(DesignBlockType.Text)
engine.block.setName(subline, name = "subline-copy")
engine.block.setString(subline, property = "text/text", value = "{{subline}}")
engine.block.setTextFontSize(subline, fontSize = 8F)
engine.block.setTextColor(
subline,
color = EngineColor.fromRGBA(r = 0.28F, g = 0.24F, b = 0.32F, a = 1F),
)
engine.block.setWidth(subline, value = 700F)
engine.block.setWidthMode(subline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(subline, mode = SizeMode.AUTO)
engine.block.setBoolean(subline, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(subline, value = 160F)
engine.block.setPositionY(subline, value = 1000F)
engine.block.appendChild(parent = page, child = subline)
val cta = engine.block.create(DesignBlockType.Text)
engine.block.setName(cta, name = "cta-copy")
engine.block.setString(cta, property = "text/text", value = "{{cta}}")
engine.block.setTextFontSize(cta, fontSize = 9F)
engine.block.setTextColor(
cta,
color = EngineColor.fromRGBA(r = 0.16F, g = 0.29F, b = 0.82F, a = 1F),
)
engine.block.setWidth(cta, value = 700F)
engine.block.setWidthMode(cta, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(cta, mode = SizeMode.AUTO)
engine.block.setBoolean(cta, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(cta, value = 160F)
engine.block.setPositionY(cta, value = 1090F)
engine.block.appendChild(parent = page, child = cta)
val serializedTemplate = engine.scene.saveToString(scene = scene)
engine.block.forceLoadResources(listOf(heroImage, headline, subline, cta))
return serializedTemplate
}
private fun discoverTokenizedBlocks(engine: Engine): List {
return engine.block.findAll()
.filter { block -> engine.block.referencesAnyVariables(block) }
.map { block -> engine.block.getName(block) }
.filter(String::isNotBlank)
.sorted()
}
private suspend fun exportAutomationJob(
engine: Engine,
templateScene: String,
job: AutomationJob,
outputDirectory: File,
): File {
engine.scene.load(
scene = templateScene,
waitForResources = true,
)
engine.variable.set(key = "headline", value = job.headline)
engine.variable.set(key = "subline", value = job.subline)
engine.variable.set(key = "cta", value = job.cta)
val heroImage = engine.block.findByName(name = "hero-image").first()
val heroFill = engine.block.getFill(heroImage)
engine.block.setString(
block = heroFill,
property = "fill/image/imageFileURI",
value = job.heroImageUri,
)
engine.block.resetCrop(heroImage)
val page = requireNotNull(engine.scene.getCurrentPage()) { "Expected a page in the automation template." }
engine.block.forceLoadResources(listOf(page))
val exportData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
val outputFile = File(outputDirectory, "${job.fileStem}.png")
withContext(Dispatchers.IO) {
outputFile.outputStream().channel.use { channel ->
while (exportData.hasRemaining()) {
channel.write(exportData)
}
}
}
return outputFile
}
```
Automate repetitive exports by keeping the editor UI out of the loop. On Android, you start the Engine headlessly, apply data to a reusable scene contract, and export each result sequentially on the main thread.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-automate-workflows)
## What You'll Learn
- Decide whether a workflow should stay on-device, pause for approval, or hand off to a backend runtime.
- Build a reusable template contract with tokenized text and named media slots.
- Populate that contract with record data and export variants sequentially.
- Keep Android-specific constraints in mind when you scale up a workflow.
## Choose a Workflow Pattern
| Pattern | Android does | Use it when |
| --- | --- | --- |
| Client-only | Load a scene, set variables, export, and save the file locally. | The batch is short, assets already live on-device or on your CDN, and the user expects an immediate result. |
| Hybrid approval | Generate a populated scene first, then hand that scene to an editor flow for review or touch-ups. | Automation prepares most of the design, but a person still approves the final output. |
| Backend handoff | Assemble the job payload, template identifier, and record data, then let another runtime render the assets. | The batch is large, long-running, or better handled outside the device lifecycle. |
Android is the client runtime in these flows. If a job needs background orchestration, queueing, or server-triggered rendering, keep the same scene contract and move the rendering step to your backend runtime.
## Define the Batch Input
Keep each export job small and explicit. The example uses one record per output file, carrying the file name, text variables, and replacement media URI.
```kotlin highlight-android-record
private data class AutomationJob(
val fileStem: String,
val headline: String,
val subline: String,
val cta: String,
val heroImageUri: String,
)
```
## Load Required Asset Sources
Load only the asset sources the workflow needs. The example follows the Android Starter Kit pattern and uses `engine.asset.addLocalSourceFromJSON(...)` instead of the deprecated `addDefaultAssetSources(...)` helper.
```kotlin highlight-android-asset-sources
val existingAssetSources = engine.asset.findAllSources().toSet()
val addedAssetSources = automationAssetSourceIds.filterNot(existingAssetSources::contains)
addedAssetSources.forEach { assetSource ->
engine.asset.addLocalSourceFromJSON(
contentUri = defaultBaseUri.buildUpon()
.appendPath(assetSource)
.appendPath("content.json")
.build(),
)
}
```
- Engine operations stay on the main thread. Use `withContext(Dispatchers.IO)` only for file I/O after export.
- The sample checks which CE.SDK default asset sources are already registered and only adds missing sources, so revisiting the screen does not add the same palette and font assets twice.
## Build a Reusable Scene Contract
The example creates its template scene in code so the workflow stays self-contained. In production, you would usually load the same structure from a saved `.scene` or archive instead.
```kotlin highlight-android-template
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1350F)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = backgroundFill,
property = "fill/color/value",
value = EngineColor.fromRGBA(r = 0.96F, g = 0.94F, b = 0.90F, a = 1F),
)
engine.block.setFill(background, fill = backgroundFill)
engine.block.setWidth(background, value = 1080F)
engine.block.setHeight(background, value = 1350F)
engine.block.appendChild(parent = page, child = background)
val heroImage = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(heroImage, name = "hero-image")
engine.block.setShape(heroImage, shape = engine.block.createShape(ShapeType.Rect))
val heroFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = heroFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_2.jpg",
)
engine.block.setFill(heroImage, fill = heroFill)
engine.block.setWidth(heroImage, value = 860F)
engine.block.setHeight(heroImage, value = 720F)
engine.block.setPositionX(heroImage, value = 110F)
engine.block.setPositionY(heroImage, value = 100F)
engine.block.appendChild(parent = page, child = heroImage)
val copyPanel = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(copyPanel, shape = engine.block.createShape(ShapeType.Rect))
val copyPanelFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = copyPanelFill,
property = "fill/color/value",
value = EngineColor.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.92F),
)
engine.block.setFill(copyPanel, fill = copyPanelFill)
engine.block.setWidth(copyPanel, value = 860F)
engine.block.setHeight(copyPanel, value = 360F)
engine.block.setPositionX(copyPanel, value = 110F)
engine.block.setPositionY(copyPanel, value = 860F)
engine.block.appendChild(parent = page, child = copyPanel)
val headline = engine.block.create(DesignBlockType.Text)
engine.block.setName(headline, name = "headline-copy")
engine.block.setString(headline, property = "text/text", value = "{{headline}}")
engine.block.setTextFontSize(headline, fontSize = 14F)
engine.block.setTextColor(
headline,
color = EngineColor.fromRGBA(r = 0.12F, g = 0.10F, b = 0.15F, a = 1F),
)
engine.block.setWidth(headline, value = 700F)
engine.block.setWidthMode(headline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(headline, mode = SizeMode.AUTO)
engine.block.setBoolean(headline, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(headline, value = 160F)
engine.block.setPositionY(headline, value = 915F)
engine.block.appendChild(parent = page, child = headline)
val subline = engine.block.create(DesignBlockType.Text)
engine.block.setName(subline, name = "subline-copy")
engine.block.setString(subline, property = "text/text", value = "{{subline}}")
engine.block.setTextFontSize(subline, fontSize = 8F)
engine.block.setTextColor(
subline,
color = EngineColor.fromRGBA(r = 0.28F, g = 0.24F, b = 0.32F, a = 1F),
)
engine.block.setWidth(subline, value = 700F)
engine.block.setWidthMode(subline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(subline, mode = SizeMode.AUTO)
engine.block.setBoolean(subline, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(subline, value = 160F)
engine.block.setPositionY(subline, value = 1000F)
engine.block.appendChild(parent = page, child = subline)
val cta = engine.block.create(DesignBlockType.Text)
engine.block.setName(cta, name = "cta-copy")
engine.block.setString(cta, property = "text/text", value = "{{cta}}")
engine.block.setTextFontSize(cta, fontSize = 9F)
engine.block.setTextColor(
cta,
color = EngineColor.fromRGBA(r = 0.16F, g = 0.29F, b = 0.82F, a = 1F),
)
engine.block.setWidth(cta, value = 700F)
engine.block.setWidthMode(cta, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(cta, mode = SizeMode.AUTO)
engine.block.setBoolean(cta, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(cta, value = 160F)
engine.block.setPositionY(cta, value = 1090F)
engine.block.appendChild(parent = page, child = cta)
val serializedTemplate = engine.scene.saveToString(scene = scene)
```
This template contract does two important things:
- Text blocks contain `{{headline}}`, `{{subline}}`, and `{{cta}}` tokens. Those tokens resolve against the Engine’s variable store at render time.
- The hero image block is named `hero-image`, giving the automation step a stable handle for media replacement.
- A dedicated footer panel reserves readable copy space below the image, so the exported variants stay legible on-device even in evaluation mode.
## Validate What the Template Exposes
On Android, `engine.variable.findAll()` only lists keys that are already present in the variable store. It does not discover `{{token}}` references directly from the scene. Before you run a batch, keep the expected variable keys in your own app contract and use block inspection to verify which named blocks still reference variables.
```kotlin highlight-android-discover-slots
return engine.block.findAll()
.filter { block -> engine.block.referencesAnyVariables(block) }
.map { block -> engine.block.getName(block) }
.filter(String::isNotBlank)
.sorted()
```
This gives you a lightweight structure check without mutating the scene. It is especially useful when designers iterate on a template and you want a fast sanity check before exporting a larger batch.
## Apply Record Data and Replace Media
For each record, reload the reusable template, set the variable values, then update the named media slot.
```kotlin highlight-android-apply-data
engine.scene.load(
scene = templateScene,
waitForResources = true,
)
engine.variable.set(key = "headline", value = job.headline)
engine.variable.set(key = "subline", value = job.subline)
engine.variable.set(key = "cta", value = job.cta)
val heroImage = engine.block.findByName(name = "hero-image").first()
val heroFill = engine.block.getFill(heroImage)
engine.block.setString(
block = heroFill,
property = "fill/image/imageFileURI",
value = job.heroImageUri,
)
engine.block.resetCrop(heroImage)
```
- Reloading the serialized template keeps each export isolated from the previous record.
- Variable keys are case-sensitive. Treat them like part of your API contract between the template and your app.
- `resetCrop()` reapplies the placeholder framing after a new image URI is assigned.
## Export Sequentially on Android
Export the current page, write the buffer to disk, and move on to the next record. Keeping the pipeline sequential avoids unnecessary memory pressure on the device.
```kotlin highlight-android-export
val exportData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
val outputFile = File(outputDirectory, "${job.fileStem}.png")
withContext(Dispatchers.IO) {
outputFile.outputStream().channel.use { channel ->
while (exportData.hasRemaining()) {
channel.write(exportData)
}
}
}
```
The sample exports PNG previews because they are easy to inspect in-app. The same pattern works with `MimeType.JPEG` or `MimeType.PDF` when your downstream workflow expects a different output format.
To process more than one record, keep the Engine alive and run the same steps in order:
```kotlin highlight-android-batch
val exportedFiles = jobs.map { job ->
exportAutomationJob(
engine = engine,
templateScene = templateScene,
job = job,
outputDirectory = outputDirectory,
)
}
```
## Add a Human Approval Step
If a design still needs review, stop after populating the scene instead of exporting immediately. Serialize that populated scene with `engine.scene.saveToString(...)`, then open the saved scene in an editor flow. This keeps one template contract for both automated generation and manual approval.
## Next Steps
- [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) – use the Engine directly when no prebuilt UI is needed.
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) – repeat the same automation flow across many records.
- [Create Templates](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) – design the reusable scenes your workflow populates.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) – manage the variable store and tokenized text safely.
---
## Related Pages
- [Overview](https://img.ly/docs/cesdk/android/automation/overview-34d971/) - Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale.
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) - Documentation for Batch Processing
- [Auto-Resize Blocks in Android (Kotlin)](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/) - Configure absolute, percent, and auto sizing modes to build responsive, content-driven layouts with the CE.SDK block API on Android.
- [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) - Generate personalized designs from a single template by merging external data into CE.SDK scenes with variables and named placeholder blocks.
- [Product Variations](https://img.ly/docs/cesdk/android/automation/product-variations-f3349f/) - Generate multiple product variants from a single template by swapping text, images and styles programmatically.
- [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) - Create many image variants from structured data by interpolating content into reusable design templates.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Auto-Resize Blocks in Android (Kotlin)"
description: "Configure absolute, percent, and auto sizing modes to build responsive, content-driven layouts with the CE.SDK block API on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Auto-Resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/)
---
```kotlin file=@cesdk_android_examples/engine-guides-auto-resize/AutoResize.kt reference-only
@file:Suppress("ktlint:standard:filename")
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
data class AutoResizeMetrics(
val titleWidth: Float,
val titleHeight: Float,
val subtitleWidth: Float,
val titleWidthMode: SizeMode,
val titleHeightMode: SizeMode,
val backgroundWidthMode: SizeMode,
val backgroundHeightMode: SizeMode,
)
suspend fun autoResize(engine: Engine): AutoResizeMetrics = withContext(engine.dispatcher) {
runAutoResizeGuide(engine)
}
private suspend fun runAutoResizeGuide(engine: Engine): AutoResizeMetrics {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val titleBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(titleBlock, text = "Auto-Resize Demo")
engine.block.setFloat(titleBlock, property = "text/fontSize", value = 64F)
engine.block.setWidthMode(titleBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(titleBlock, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = titleBlock)
val coverBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(coverBlock, shape = engine.block.createShape(ShapeType.Rect))
val coverFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
coverFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.08F),
)
engine.block.setFill(coverBlock, fill = coverFill)
engine.block.appendChild(parent = page, child = coverBlock)
engine.block.fillParent(coverBlock)
engine.block.destroy(coverBlock)
yield()
val titleWidth = engine.block.getFrameWidth(titleBlock)
val titleHeight = engine.block.getFrameHeight(titleBlock)
println("Title dimensions: ${titleWidth.toInt()}x${titleHeight.toInt()} pixels")
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val centerX = (pageWidth - titleWidth) / 2F
val centerY = (pageHeight - titleHeight) / 2F - 100F
engine.block.setPositionX(titleBlock, value = centerX)
engine.block.setPositionY(titleBlock, value = centerY)
val backgroundBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(backgroundBlock, shape = engine.block.createShape(ShapeType.Rect))
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
backgroundFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 0.3F),
)
engine.block.setFill(backgroundBlock, fill = backgroundFill)
engine.block.setWidthMode(backgroundBlock, mode = SizeMode.PERCENT)
engine.block.setHeightMode(backgroundBlock, mode = SizeMode.PERCENT)
engine.block.setWidth(backgroundBlock, value = 0.8F)
engine.block.setHeight(backgroundBlock, value = 0.3F)
engine.block.setPositionX(backgroundBlock, value = pageWidth * 0.1F)
engine.block.setPositionY(backgroundBlock, value = pageHeight * 0.6F)
engine.block.appendChild(parent = page, child = backgroundBlock)
engine.block.sendToBack(backgroundBlock)
val subtitleBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(subtitleBlock, text = "Text automatically sizes to fit content")
engine.block.setFloat(subtitleBlock, property = "text/fontSize", value = 32F)
engine.block.setWidthMode(subtitleBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(subtitleBlock, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = subtitleBlock)
yield()
val subtitleWidth = engine.block.getFrameWidth(subtitleBlock)
val subtitleCenterX = (pageWidth - subtitleWidth) / 2F
engine.block.setPositionX(subtitleBlock, value = subtitleCenterX)
engine.block.setPositionY(subtitleBlock, value = pageHeight * 0.7F)
val titleWidthMode = engine.block.getWidthMode(titleBlock)
val titleHeightMode = engine.block.getHeightMode(titleBlock)
val backgroundWidthMode = engine.block.getWidthMode(backgroundBlock)
val backgroundHeightMode = engine.block.getHeightMode(backgroundBlock)
println("Title modes: width=$titleWidthMode, height=$titleHeightMode")
println("Background modes: width=$backgroundWidthMode, height=$backgroundHeightMode")
engine.block.forceLoadResources(listOf(titleBlock, subtitleBlock))
return AutoResizeMetrics(
titleWidth = titleWidth,
titleHeight = titleHeight,
subtitleWidth = subtitleWidth,
titleWidthMode = titleWidthMode,
titleHeightMode = titleHeightMode,
backgroundWidthMode = backgroundWidthMode,
backgroundHeightMode = backgroundHeightMode,
)
}
```
Configure blocks to size themselves from fixed values, their parent, or their content. On Android, use `SizeMode.ABSOLUTE`, `SizeMode.PERCENT`, and `SizeMode.AUTO` on each axis, then read `getFrameWidth()` and `getFrameHeight()` after layout when you need the computed result.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-auto-resize)
This example uses a title with Auto mode, a background panel with Percent mode, and computed frame sizes to center text. Android also exposes `fillParent()` as a shortcut when an attached block should cover its parent in one call.
## Create a Reference Page
Create a design scene with an 800 by 600 page so the percent-mode values have a predictable parent size.
```kotlin highlight-android-setup
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
## Size modes
- `SizeMode.ABSOLUTE` is the default. Width and height are design units that you control directly with `setWidth()` and `setHeight()`.
- `SizeMode.PERCENT` interprets width and height as normalized parent-relative values. `1.0F` means 100 percent of the parent on that axis.
- `SizeMode.AUTO` lets the engine compute the block size from its content. This is most useful for text and other intrinsic-content blocks.
## Auto mode for text
Use Auto mode when content should decide the final frame. Here the title expands to fit its text instead of using a hard-coded width.
```kotlin highlight-android-auto-mode
val titleBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(titleBlock, text = "Auto-Resize Demo")
engine.block.setFloat(titleBlock, property = "text/fontSize", value = 64F)
engine.block.setWidthMode(titleBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(titleBlock, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = titleBlock)
```
## Fill the parent in one call
When a block is already attached to a parent and should cover it completely, Android offers a convenience API:
```kotlin highlight-android-fill-parent
engine.block.fillParent(coverBlock)
```
`fillParent()` also resets crop values when needed and can switch a crop-based fill to cover so the block stays in a valid state.
## Read computed frame dimensions
Layout values are not the same as the raw width and height properties in Auto mode. Read frame dimensions after the engine has performed a layout update.
```kotlin highlight-android-read-frame-dimensions
val titleWidth = engine.block.getFrameWidth(titleBlock)
val titleHeight = engine.block.getFrameHeight(titleBlock)
println("Title dimensions: ${titleWidth.toInt()}x${titleHeight.toInt()} pixels")
```
If you query frame size immediately after changing content, yield to the next coroutine turn or another engine update before reading.
## Center the block with frame dimensions
Once you have the computed title size, use the page dimensions to place it precisely.
```kotlin highlight-android-center-block
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val centerX = (pageWidth - titleWidth) / 2F
val centerY = (pageHeight - titleHeight) / 2F - 100F
engine.block.setPositionX(titleBlock, value = centerX)
engine.block.setPositionY(titleBlock, value = centerY)
```
This pattern is useful whenever content length changes between generated outputs.
## Percent mode for responsive layouts
Percent mode makes a block track its parent. The example uses 80 percent width, 30 percent height, with a 10 percent left margin and a 60 percent top offset.
```kotlin highlight-android-percent-mode
val backgroundBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(backgroundBlock, shape = engine.block.createShape(ShapeType.Rect))
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
backgroundFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 0.3F),
)
engine.block.setFill(backgroundBlock, fill = backgroundFill)
engine.block.setWidthMode(backgroundBlock, mode = SizeMode.PERCENT)
engine.block.setHeightMode(backgroundBlock, mode = SizeMode.PERCENT)
engine.block.setWidth(backgroundBlock, value = 0.8F)
engine.block.setHeight(backgroundBlock, value = 0.3F)
engine.block.setPositionX(backgroundBlock, value = pageWidth * 0.1F)
engine.block.setPositionY(backgroundBlock, value = pageHeight * 0.6F)
engine.block.appendChild(parent = page, child = backgroundBlock)
engine.block.sendToBack(backgroundBlock)
```
Because values are normalized, the same layout logic adapts to different page sizes without recalculating pixel dimensions.
## Additional auto-sized content
You can repeat the same pattern for other text blocks. This subtitle uses Auto mode and recenters itself from its computed width.
```kotlin highlight-android-subtitle-auto
val subtitleBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(subtitleBlock, text = "Text automatically sizes to fit content")
engine.block.setFloat(subtitleBlock, property = "text/fontSize", value = 32F)
engine.block.setWidthMode(subtitleBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(subtitleBlock, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = subtitleBlock)
yield()
val subtitleWidth = engine.block.getFrameWidth(subtitleBlock)
val subtitleCenterX = (pageWidth - subtitleWidth) / 2F
engine.block.setPositionX(subtitleBlock, value = subtitleCenterX)
engine.block.setPositionY(subtitleBlock, value = pageHeight * 0.7F)
```
## Verify the active modes
Query the current modes when you need to branch behavior or assert that template setup is correct.
```kotlin highlight-android-check-modes
val titleWidthMode = engine.block.getWidthMode(titleBlock)
val titleHeightMode = engine.block.getHeightMode(titleBlock)
val backgroundWidthMode = engine.block.getWidthMode(backgroundBlock)
val backgroundHeightMode = engine.block.getHeightMode(backgroundBlock)
println("Title modes: width=$titleWidthMode, height=$titleHeightMode")
println("Background modes: width=$backgroundWidthMode, height=$backgroundHeightMode")
```
## Troubleshooting
**Frame dimensions are `0` or stale**: wait for a layout pass before calling `getFrameWidth()` or `getFrameHeight()`.
**Percent sizing has no effect**: the block must be attached to a parent, and the parent needs a resolved size.
**Auto sizing does not change the block**: Auto mode is primarily useful for blocks with intrinsic content, such as text.
**A fill looks different after `fillParent()`**: the helper may reset crop values or force cover mode to keep the block valid.
## API reference
| Method | Purpose |
| ----------------------------------------- | ---------------------------------------------------- |
| `engine.block.getWidth(block)` | Read the configured width value in the current mode |
| `engine.block.setWidth(block, value)` | Set width in the current mode |
| `engine.block.getWidthMode(block)` | Read the width sizing mode |
| `engine.block.setWidthMode(block, mode)` | Set the width sizing mode |
| `engine.block.getHeight(block)` | Read the configured height value in the current mode |
| `engine.block.setHeight(block, value)` | Set height in the current mode |
| `engine.block.getHeightMode(block)` | Read the height sizing mode |
| `engine.block.setHeightMode(block, mode)` | Set the height sizing mode |
| `engine.block.getFrameWidth(block)` | Read the computed width after layout |
| `engine.block.getFrameHeight(block)` | Read the computed height after layout |
| `engine.block.fillParent(block)` | Resize and reposition a block to cover its parent |
## Next Steps
- [Resize blocks (manual)](https://img.ly/docs/cesdk/android/edit-image/transform/resize-407242/) — change a block frame explicitly
with width and height values.
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) — apply the same sizing logic across
many records.
- [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) — combine template data replacement
with responsive layout rules.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Batch Processing"
description: "Documentation for Batch Processing"
platform: android
url: "https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/)
---
Batch processing lets your app automatically generate scores of assets from a single design template. For example, you might create 100 personalized posters or social posts from a JSON file of names and photos, without opening the editor for each one. CE.SDK's headless engine makes this possible entirely in Kotlin.
This guide shows you how to do that in Kotlin for Android. You'll learn how to load a saved design, substitute text and images, and export each variation as an asset file. The same techniques apply to more complex outputs like PDFs or videos.
## What You'll Learn
- How to start CE.SDK's **headless engine** without a UI editor.
- How to **load a template** from an archive or URL and attach it to a new scene.
- How to **replace variables and images** for each record in your data.
- How to **export** each generated design as a common format like PNG, JPEG or PDF.
## When You'll Use This
Headless batch generation is ideal for tasks that need automation, not user interaction. Use it to mass-produce:
- Branded materials
- Social media graphics
- Dynamic thumbnails
- Personalized certificates
- Product cards at scale
Because you're not displaying the editor UI, it works well for background processing and server-side workflows.
## Headless Engine
At the center of CE.SDK is the `Engine`, a lightweight rendering system you can use without the prebuilt editors. It can run in the background, respond to coroutines, and render scenes directly to image data.
```kotlin
import ly.img.engine.Engine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun startHeadlessEngine(license: String, userId: String) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.batch")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
}
```
For automation, you'll typically create one `Engine` instance for the full batch run.
- **On mobile**, a single-engine, sequential approach is safest.
- **On more powerful hardware or servers**, you can explore modest parallelism, as each instance of `Engine` is independent.
## Loading Templates
The template defines the design you'll use for all generated images. You can:
1. Create a template in the CE.SDK editor.
2. Save it as an archive or scene file.
3. Bundle that file with your app in the assets folder or host it at a URL for the batch to use.
```kotlin
import android.net.Uri
val templateUri = Uri.parse("file:///android_asset/templates/badge_template.scene")
```
**Archives** are self-contained ZIP files that include:
- Your layout
- Text
- All linked assets
They're ideal for predictable batch exports. You can also save templates as scene JSON files, but in those cases, the URI of every asset must resolve correctly at runtime.
Once loaded, always validate the structure before using it.
```kotlin
import android.net.Uri
import ly.img.engine.DesignBlock
import ly.img.engine.Engine
suspend fun loadTemplate(engine: Engine, uri: Uri): DesignBlock {
val scene = engine.scene.load(sceneUri = uri)
return scene
}
```
This ensures that missing or corrupt templates don't interrupt your batch.
`engine.scene.load()` loads the template and returns the scene root block, which you can then render, modify, and export.
## Supplying Data from JSON
Every batch needs a list of records. Each record holds the values to apply to the template. A common pattern is:
1. Store them as a JSON array.
2. Decode them during the batch.
A record might have these properties:
```kotlin
import kotlinx.serialization.Serializable
@Serializable
data class Record(
val id: String,
val variables: Map,
val outputFileName: String,
val images: Map? = null // optional blockName → image URI
)
```
Then decode any JSON using kotlinx.serialization or Gson:
```kotlin
import android.content.Context
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
import java.io.IOException
fun loadRecords(context: Context): List {
return try {
val jsonString = context.assets.open("records.json")
.bufferedReader()
.use { it.readText() }
Json.decodeFromString>(jsonString)
} catch (e: IOException) {
emptyList()
}
}
```
Example `records.json`:
```json
[
{
"id": "001",
"variables": { "name": "Ruth", "tagline": "Ship great apps" },
"outputFileName": "badge-ruth"
},
{
"id": "002",
"variables": { "name": "Chris", "tagline": "Move fast, polish later" },
"outputFileName": "badge-chris"
}
]
```
In a production environment, you'll load data from an API or database instead of bundled assets. If your dataset is large, consider streaming it in chunks instead of loading everything at once.
## Templates and Variables
Templates often include placeholders, or variables, that you can update with real data at runtime. In CE.SDK (Android), template variables follow a key/value pattern and are **always stored as strings**. Your app can convert them into types like numbers or colors when needed. For text blocks, CE.SDK automatically matches placeholders in the template with variable names. Displaying `\{\{username\}\}` as the text in a text box becomes the variable `username` you can replace with a person's name before exporting.
```kotlin
import ly.img.engine.Engine
// All variables are set via (key:String, value:String)
engine.variable.set(key = "name", value = "Chris") // text
engine.variable.set(key = "price", value = "9.99") // number encoded as string
engine.variable.set(key = "brandColor", value = "#FFD60A") // color as hex string
engine.variable.set(key = "isFeatured", value = "true") // boolean as "true" / "false"
engine.variable.set(key = "imageURL", value = "https://example.com/image.jpg") // URL as string
```
Discover the available variable keys at runtime to validate a template using:
```kotlin
val keys = engine.variable.findAll()
// assert or log missing keys before a long batch run
```
## Applying Data to the Template
Once the engine loads the template, you can fill in variables. These correspond to the placeholders you set in your CE.SDK scene, like `\{\{name\}\}` or `\{\{tagline\}\}`.
```kotlin
import ly.img.engine.Engine
fun applyVariables(engine: Engine, values: Map) {
for ((key, value) in values) {
engine.variable.set(key = key, value = value)
}
}
```
You can also swap out placeholder images at runtime. The simplest method is to find the block by its name and update its image fill.
```kotlin
import ly.img.engine.Engine
fun replaceNamedImage(engine: Engine, blockName: String, imageUri: String) {
val matches = engine.block.findByName(blockName)
if (matches.isNotEmpty()) {
val imageBlock = matches.first()
val fill = engine.block.getFill(imageBlock)
engine.block.setString(fill, property = "fill/image/imageFileURI", value = imageUri)
engine.block.setFill(imageBlock, fill = fill)
engine.block.setKind(imageBlock, kind = "image")
}
}
```
This snippet looks up a block named `productImage` and replaces its image fill with the URI of the new image.
> **Note:** Using block names keeps your automation readable and less fragile than referencing IDs.
## Create Thumbnails
You can generate previews by exporting a scaled version of each result:
```kotlin
import android.content.Context
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
import java.io.File
suspend fun exportThumbnail(
engine: Engine,
context: Context,
fileName: String,
scale: Float = 0.25f
): File {
val scene = requireNotNull(engine.scene.get()) { "No scene loaded" }
val width = engine.block.getFrameWidth(scene) * scale
val height = engine.block.getFrameHeight(scene) * scale
val options = ExportOptions(
jpegQuality = 0.7f,
targetWidth = width,
targetHeight = height
)
val exportData = engine.block.export(scene, mimeType = MimeType.JPEG, options = options)
val outputDir = context.filesDir
val thumbFile = File(outputDir, "thumb_$fileName.jpg")
withContext(Dispatchers.IO) {
thumbFile.outputStream().channel.use { channel ->
channel.write(exportData)
}
}
return thumbFile
}
```
## Exporting to Multiple Formats
Exports can target different output types. Just switch the mime type you pass:
```kotlin
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
val pngData = engine.block.export(scene, mimeType = MimeType.PNG, options = ExportOptions(targetHeight = 1080f))
val pdfData = engine.block.export(scene, mimeType = MimeType.PDF)
```
|Format|MimeType|Typical Use|
|---|---|---|
|PNG|`MimeType.PNG`|Lossless images with transparency|
|JPEG|`MimeType.JPEG`|Photos and smaller files|
|PDF|`MimeType.PDF`|Printable designs|
|MP4|`MimeType.MP4`|Animated or timed templates|
Use an `ExportOptions` instance to tune output quality, size and other properties of the export. You can get the details in the [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) guides.
If you need multiple formats at once, run several export calls back-to-back using the same engine and scene.
## Managing Memory and Resources
Each export involves GPU textures, image buffers, and temporary files. To keep your app responsive:
- Reuse a single engine for sequential jobs.
- Clean up temporary directories between batches.
- Call `engine.stop()` when completely done to free resources.
## Performance Tuning Checklist
- Use JPEG quality 0.8–0.9 to balance file size and speed.
- Keep templates simple. Avoid unnecessary effects or large images.
- Chunk data into smaller groups for large datasets.
- Limit concurrency to 2–3 parallel tasks if attempting parallel processing.
- Profile on the lowest device you support.
## Error Handling and Retries
Batch jobs can fail for network hiccups or invalid data. Use Kotlin's try/catch blocks to retry a few times before giving up.
```kotlin
import kotlinx.coroutines.delay
suspend fun processRecordWithRetry(record: Record, maxAttempts: Int = 3) {
var attempts = 0
while (attempts < maxAttempts) {
try {
exportRecord(record)
break
} catch (e: Exception) {
attempts++
if (attempts >= maxAttempts) {
throw e
}
delay((attempts * 500L)) // exponential backoff
}
}
}
```
You can also log each attempt for easier debugging.
## Logging and Monitoring Progress
Adding logging helps track how long each export takes:
```kotlin
import android.util.Log
const val TAG = "BatchProcessing"
Log.i(TAG, "Starting batch processing for ${records.size} records")
records.forEachIndexed { index, record ->
val startTime = System.currentTimeMillis()
try {
processRecord(record)
val duration = System.currentTimeMillis() - startTime
Log.i(TAG, "Exported ${record.outputFileName} in ${duration}ms [${index + 1}/${records.size}]")
} catch (e: Exception) {
Log.e(TAG, "Failed to export ${record.outputFileName}", e)
}
}
```
Wrap your entire run in timestamps to measure throughput and display progress in your UI.
## Batch Workflow
Batch processing isn't limited to mobile apps. The same logic can run on backends or web services using CE.SDK for Web or Node. If your workload scales beyond device limits, consider:
1. Migrating automation to a server workflow.
2. Sending results back to the app.
An example batch process, below, calls `processRecord()` for each record in the dataset. The record is processed by:
1. Loading the template
2. Setting variables
3. Replacing images
4. Exporting the result
```kotlin
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlock
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
import java.io.File
suspend fun processRecord(
engine: Engine,
context: Context,
record: Record,
templateUri: Uri
): File {
// Load the template
val scene = engine.scene.load(sceneUri = templateUri)
// Apply variables
applyVariables(engine, record.variables)
// Replace images if specified
record.images?.forEach { (blockName, imageUri) ->
replaceNamedImage(engine, blockName, imageUri)
}
// Export the result
val exportData = engine.block.export(
scene,
mimeType = MimeType.JPEG,
options = ExportOptions(jpegQuality = 0.9f)
)
// Save to file
val outputDir = context.filesDir
val outputFile = File(outputDir, "${record.outputFileName}.jpg")
withContext(Dispatchers.IO) {
outputFile.outputStream().channel.use { channel ->
channel.write(exportData)
}
}
return outputFile
}
suspend fun runBatch(
context: Context,
license: String,
userId: String,
records: List
) {
val engine = Engine.getInstance(id = "ly.img.engine.batch")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
val templateUri = Uri.parse("file:///android_asset/templates/badge_template.scene")
for (record in records) {
try {
processRecord(engine, context, record, templateUri)
} catch (e: Exception) {
Log.e("Batch", "Failed to process ${record.id}", e)
}
}
engine.stop()
}
```
Use modest parallelism for faster processing on capable devices:
```kotlin
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
suspend fun runBatchParallel(
context: Context,
license: String,
userId: String,
records: List,
maxConcurrent: Int = 3
) = coroutineScope {
val templateUri = Uri.parse("file:///android_asset/templates/badge_template.scene")
records.chunked(maxConcurrent).forEach { chunk ->
chunk.map { record ->
async(Dispatchers.Main) {
// Create a separate engine instance for each parallel task
val engine = Engine.getInstance(id = "ly.img.engine.batch.${record.id}")
try {
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
processRecord(engine, context, record, templateUri)
} finally {
engine.stop()
}
}
}.awaitAll()
}
}
```
## Troubleshooting
**❌ Your exports appear blank**:
- Verify that the scene loaded successfully with `engine.scene.get()`.
- Check that all asset URIs are reachable (network or local).
- Ensure the page has content before exporting.
**❌ Text variables don't update**:
- Confirm variable names match the template's tokens exactly (case-sensitive).
- Use `engine.variable.findAll()` to see what variables exist in the template.
- Verify that `engine.variable.set()` is called with the correct key.
**❌ Your image placeholder doesn't update**:
- Ensure you're setting the image URI on an image fill.
- Verify that the fill is applied to the target block with `engine.block.setFill()`.
- Check that the URI is valid and reachable (add INTERNET permission for remote URLs).
- Confirm the block's kind is set to `"image"` after applying the new fill.
**❌ The batch job becomes sluggish**:
- Performance issues are rare in sequential runs, but if you attempt parallel exports:
- Limit concurrency to a few simultaneous tasks (2-3 on mobile).
- Ensure each engine instance is properly stopped after use.
- Monitor memory usage and reduce batch size if needed.
**❌ Network errors when loading remote templates or images**:
- Add `` to AndroidManifest.xml.
- Verify URLs are using HTTPS.
- Test URLs in a browser to confirm they're accessible.
## Next Steps
Continue learning about automation and export workflows with these related guides:
- Use Templates to [generate content](https://img.ly/docs/cesdk/android/use-templates/generate-334e15/).
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) & [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) for dynamic content.
- [Export assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) in different formats.
- Generate [multiple assets](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) from a single record.
- Create [Preview Thumbnails](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/).
These guides expand on how to prepare templates, manage variable data, and optimize export pipelines for larger-scale automation.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Data Merge"
description: "Generate personalized designs from a single template by merging external data into CE.SDK scenes with variables and named placeholder blocks."
platform: android
url: "https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/)
---
```kotlin file=@cesdk_android_examples/engine-guides-data-merge/DataMergeGuide.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.nio.ByteBuffer
data class MergeRecord(
val fullName: String,
val jobTitle: String,
val email: String,
val photoUri: String,
)
data class MergedCard(
val fileName: String,
val pngData: ByteBuffer,
)
private val dataMergeVariableKeys = listOf("full_name", "job_title", "email")
suspend fun mergeBusinessCards(engine: Engine): List = withContext(engine.dispatcher) {
val currentVariableKeys = engine.variable.findAll().toSet()
val previousVariables = dataMergeVariableKeys
.filter(currentVariableKeys::contains)
.associateWith(engine.variable::get)
try {
mergeBusinessCardsWithTemporaryVariables(engine)
} finally {
val variablesToRemove = engine.variable.findAll().toSet()
dataMergeVariableKeys.filter(variablesToRemove::contains).forEach(engine.variable::remove)
previousVariables.forEach { (key, value) -> engine.variable.set(key = key, value = value) }
}
}
private suspend fun mergeBusinessCardsWithTemporaryVariables(engine: Engine): List {
val records = listOf(
MergeRecord(
fullName = "Alex Rivera",
jobTitle = "Senior Product Designer",
email = "alex.rivera@example.com",
photoUri = "https://img.ly/static/ubq_samples/sample_1.jpg",
),
MergeRecord(
fullName = "Jordan Lee",
jobTitle = "Lifecycle Marketing Lead",
email = "jordan.lee@example.com",
photoUri = "https://img.ly/static/ubq_samples/sample_2.jpg",
),
)
val templateScene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1050F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = templateScene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(background, fill = backgroundFill)
engine.block.appendChild(parent = page, child = background)
engine.block.fillParent(background)
engine.block.setColor(
block = backgroundFill,
property = "fill/color/value",
value = Color.fromHex("#FFF7F0"),
)
val photoBlock = engine.block.create(DesignBlockType.Graphic)
val placeholderFill = engine.block.createFill(FillType.Image)
engine.block.setName(photoBlock, name = "profile-photo")
engine.block.setShape(photoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(photoBlock, value = 48F)
engine.block.setPositionY(photoBlock, value = 48F)
engine.block.setWidth(photoBlock, value = 280F)
engine.block.setHeight(photoBlock, value = 504F)
engine.block.setEnum(photoBlock, property = "contentFill/mode", value = "Cover")
engine.block.setString(
block = placeholderFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(photoBlock, fill = placeholderFill)
engine.block.appendChild(parent = page, child = photoBlock)
val nameText = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(nameText, text = "{{full_name}}")
engine.block.setPositionX(nameText, value = 368F)
engine.block.setPositionY(nameText, value = 110F)
engine.block.setWidth(nameText, value = 620F)
engine.block.setHeightMode(nameText, mode = SizeMode.AUTO)
engine.block.setTextFontSize(nameText, fontSize = 56F)
engine.block.setTextColor(nameText, color = Color.fromHex("#211B17"))
engine.block.appendChild(parent = page, child = nameText)
val detailsText = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(detailsText, text = "{{job_title}}\n{{email}}")
engine.block.setPositionX(detailsText, value = 368F)
engine.block.setPositionY(detailsText, value = 214F)
engine.block.setWidth(detailsText, value = 580F)
engine.block.setHeightMode(detailsText, mode = SizeMode.AUTO)
engine.block.setTextFontSize(detailsText, fontSize = 28F)
engine.block.setTextColor(detailsText, color = Color.fromHex("#5D5248"))
engine.block.appendChild(parent = page, child = detailsText)
val templateSceneString = engine.scene.saveToString(scene = templateScene)
engine.block.forceLoadResources(listOf(photoBlock, nameText, detailsText))
val mergedCards = mutableListOf()
for (record in records) {
val currentVariableKeys = engine.variable.findAll().toSet()
dataMergeVariableKeys.filter(currentVariableKeys::contains).forEach(engine.variable::remove)
engine.scene.load(scene = templateSceneString)
val exportPage = engine.scene.getPages().first()
engine.variable.set(key = "full_name", value = record.fullName)
engine.variable.set(key = "job_title", value = record.jobTitle)
engine.variable.set(key = "email", value = record.email)
val variableNames = engine.variable.findAll()
check(variableNames.containsAll(listOf("full_name", "job_title", "email")))
val variableBlocks = engine.block.findByType(DesignBlockType.Text).filter { block ->
engine.block.referencesAnyVariables(block)
}
check(variableBlocks.isNotEmpty())
val profilePhoto = engine.block.findByName("profile-photo").first()
val profileFill = engine.block.getFill(profilePhoto)
engine.block.setString(
block = profileFill,
property = "fill/image/imageFileURI",
value = record.photoUri,
)
engine.block.resetCrop(profilePhoto)
engine.block.forceLoadResources(listOf(exportPage))
val pngData = engine.block.export(exportPage, mimeType = MimeType.PNG).asReadOnlyBuffer()
mergedCards += MergedCard(
fileName = record.fullName.lowercase().replace(" ", "-") + ".png",
pngData = pngData,
)
}
return mergedCards
}
```
Generate personalized designs at scale using CE.SDK's headless Android engine to batch process templates with external data.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-data-merge)
Data merge generates multiple personalized designs from a single template by replacing variable content with external data. On Android, this works best as an engine-only workflow: build a reusable scene once, load it for each record, set variable values, update named placeholder blocks, and export the result.
This guide covers how to prepare data, build templates with variables, and process multiple records in a batch workflow.
## Prepare Data Records
Data typically comes from a CSV file, database query, or API response. Here we define sample records with the fields we want to merge into the template.
```kotlin highlight-android-sample-data
val records = listOf(
MergeRecord(
fullName = "Alex Rivera",
jobTitle = "Senior Product Designer",
email = "alex.rivera@example.com",
photoUri = "https://img.ly/static/ubq_samples/sample_1.jpg",
),
MergeRecord(
fullName = "Jordan Lee",
jobTitle = "Lifecycle Marketing Lead",
email = "jordan.lee@example.com",
photoUri = "https://img.ly/static/ubq_samples/sample_2.jpg",
),
)
```
Each record contains field names that map to template variables and the named placeholder block that holds the profile image.
## Build the Template
We build a reusable business-card layout with one named image placeholder and two text blocks that contain variable placeholders. The scene is then serialized once so the loop can reload it for every record.
```kotlin highlight-android-create-template
val templateScene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1050F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = templateScene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(background, fill = backgroundFill)
engine.block.appendChild(parent = page, child = background)
engine.block.fillParent(background)
engine.block.setColor(
block = backgroundFill,
property = "fill/color/value",
value = Color.fromHex("#FFF7F0"),
)
val photoBlock = engine.block.create(DesignBlockType.Graphic)
val placeholderFill = engine.block.createFill(FillType.Image)
engine.block.setName(photoBlock, name = "profile-photo")
engine.block.setShape(photoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(photoBlock, value = 48F)
engine.block.setPositionY(photoBlock, value = 48F)
engine.block.setWidth(photoBlock, value = 280F)
engine.block.setHeight(photoBlock, value = 504F)
engine.block.setEnum(photoBlock, property = "contentFill/mode", value = "Cover")
engine.block.setString(
block = placeholderFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(photoBlock, fill = placeholderFill)
engine.block.appendChild(parent = page, child = photoBlock)
val nameText = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(nameText, text = "{{full_name}}")
engine.block.setPositionX(nameText, value = 368F)
engine.block.setPositionY(nameText, value = 110F)
engine.block.setWidth(nameText, value = 620F)
engine.block.setHeightMode(nameText, mode = SizeMode.AUTO)
engine.block.setTextFontSize(nameText, fontSize = 56F)
engine.block.setTextColor(nameText, color = Color.fromHex("#211B17"))
engine.block.appendChild(parent = page, child = nameText)
val detailsText = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(detailsText, text = "{{job_title}}\n{{email}}")
engine.block.setPositionX(detailsText, value = 368F)
engine.block.setPositionY(detailsText, value = 214F)
engine.block.setWidth(detailsText, value = 580F)
engine.block.setHeightMode(detailsText, mode = SizeMode.AUTO)
engine.block.setTextFontSize(detailsText, fontSize = 28F)
engine.block.setTextColor(detailsText, color = Color.fromHex("#5D5248"))
engine.block.appendChild(parent = page, child = detailsText)
val templateSceneString = engine.scene.saveToString(scene = templateScene)
```
Using `setName()` for the image placeholder keeps later updates predictable and avoids depending on transient block handles.
## Batch Processing Loop
We iterate through each data record, clear previously assigned variables, and load a fresh copy of the template scene before applying the next merge payload.
```kotlin highlight-android-batch-loop
for (record in records) {
val currentVariableKeys = engine.variable.findAll().toSet()
dataMergeVariableKeys.filter(currentVariableKeys::contains).forEach(engine.variable::remove)
engine.scene.load(scene = templateSceneString)
val exportPage = engine.scene.getPages().first()
```
Loading the serialized template for each record keeps the block hierarchy stable while isolating changes between exports.
## Set Variable Values
Android uses `engine.variable.set(key =, value =)` to assign text values. Once the keys are set, text blocks that reference `{{full_name}}`, `{{job_title}}`, or `{{email}}` update automatically during export.
```kotlin highlight-android-set-variables
engine.variable.set(key = "full_name", value = record.fullName)
engine.variable.set(key = "job_title", value = record.jobTitle)
engine.variable.set(key = "email", value = record.email)
```
Variable values persist on the engine until you overwrite or remove them, which is why the batch loop clears them before loading the next scene copy.
## Verify Variables
On Android, `engine.variable.findAll()` reports the variable keys that currently have values stored in the engine. Pair it with `engine.block.referencesAnyVariables()` to confirm that your text blocks still reference variable placeholders in the loaded template.
```kotlin highlight-android-check-variables
val variableNames = engine.variable.findAll()
check(variableNames.containsAll(listOf("full_name", "job_title", "email")))
val variableBlocks = engine.block.findByType(DesignBlockType.Text).filter { block ->
engine.block.referencesAnyVariables(block)
}
check(variableBlocks.isNotEmpty())
```
This is useful for validating that your data model and template stay aligned before exporting a larger batch.
## Find and Update Placeholder Blocks
Use `engine.block.findByName()` to locate the named placeholder block, then update its image fill URI before the export.
```kotlin highlight-android-find-by-name
val profilePhoto = engine.block.findByName("profile-photo").first()
val profileFill = engine.block.getFill(profilePhoto)
engine.block.setString(
block = profileFill,
property = "fill/image/imageFileURI",
value = record.photoUri,
)
engine.block.resetCrop(profilePhoto)
```
Resetting the crop after swapping the image keeps the placeholder framing consistent when source images have different aspect ratios.
## Export Each Design
After merging one record into the loaded scene, export the personalized card as a PNG and store the bytes with a record-specific filename.
```kotlin highlight-android-export
val pngData = engine.block.export(exportPage, mimeType = MimeType.PNG).asReadOnlyBuffer()
mergedCards += MergedCard(
fileName = record.fullName.lowercase().replace(" ", "-") + ".png",
pngData = pngData,
)
```
You can switch the `MimeType` to JPEG, PNG, or PDF if your batch job targets different delivery channels.
## Troubleshooting
### Variables Not Rendering
If placeholder text appears in the export instead of merged data:
- Verify the variable keys match the placeholders exactly, including case.
- Confirm `engine.variable.findAll()` contains the keys you expected to set for the current record.
- Check that the text blocks still return `true` from `engine.block.referencesAnyVariables()`.
### Placeholder Block Not Found
If `findByName("profile-photo")` returns an empty list:
- Make sure the template uses `engine.block.setName()` before it is serialized.
- Keep the placeholder name stable across template revisions so the batch loop does not need special cases.
- Reload a fresh template scene instead of mutating one scene indefinitely between records.
### Export Failures
If one record fails to export:
- Validate that the current scene still has a page block before calling `engine.block.export()`.
- Check that the image URI assigned to the placeholder is reachable on the device.
- Keep the loop sequential on Android and write the exported bytes out before moving to the next record.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.variable.set(key, value)` | Set a text variable value for the current engine session |
| `engine.variable.get(key)` | Read back a previously assigned variable value |
| `engine.variable.findAll()` | List the variable keys that currently have values stored in the engine |
| `engine.variable.remove(key)` | Remove a previously assigned variable value |
| `engine.block.setName(block, name)` | Assign a stable semantic name to a block |
| `engine.block.findByName(name)` | Find blocks by their semantic name |
| `engine.block.findByType(type)` | Find blocks by design-block type |
| `engine.block.referencesAnyVariables(block)` | Check whether a block still contains variable placeholders |
| `engine.block.getFill(block)` | Get the fill block attached to a design block |
| `engine.block.setString(block, property, value)` | Update string-backed properties such as image file URIs |
| `engine.block.export(block, mimeType)` | Export a block to an image format |
| `engine.scene.create()` | Create a new scene for the template |
| `engine.scene.getPages()` | Get the page blocks from the currently loaded scene |
| `engine.scene.saveToString(scene)` | Serialize the template scene so it can be reloaded for each record |
| `engine.scene.load(scene)` | Load a serialized scene into the active engine |
## Next Steps
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) — Automate generation of multiple designs from a template in a loop.
- [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) — Templates enable dynamic, reusable designs with text variables and placeholder media. Learn to create, load, and personalize templates programmatically.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Multiple Image Generation"
description: "Create many image variants from structured data by interpolating content into reusable design templates."
platform: android
url: "https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/)
---
Generate image variants, such as square, portrait, or landscape layouts, from a single data record using the CreativeEditor SDK's Engine API. This pattern lets you populate templates programmatically with text, images, and colors to create consistent, on-brand designs across all formats.
## What You'll Learn
- Load multiple templates into CE.SDK and populate them with structured data.
- Replace text and image placeholders dynamically using variables and named blocks.
- Apply consistent brand color themes across scenes.
- Export each variant as PNG, JPEG, or PDF.
- Build efficient workflows for generating multiple format variations.
## When to Use It
Use multi-image generation when a single record (like a restaurant listing or product) needs to produce multiple layout variants. For larger datasets with many records generating many images, refer to the [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) guide.
## Core Concepts
**Templates and Instances**:
Templates define reusable layout and placeholders. An instance is a populated version with specific data. Use `engine.scene.saveToString()` to serialize a template and `engine.scene.load(scene =)` to load it for processing.
**Variables for Dynamic Text**:
Define variables in your templates for fields like `RestaurantName` or `Rating`. Set them at runtime with `engine.variable.set(key =, value =)`. Use `engine.variable.findAll()` to verify available variable names.
**Named Blocks for Image Replacement**:
Name your image placeholders (for example, `RestaurantImage`, `Logo`). Retrieve them with `engine.block.findByName()`, access the fill with `getFill()`, then update its source URI using `setString(..., property = "fill/image/imageFileURI")`. Always reset the crop after replacing an image fill for proper framing.
**Brand and Conditional Styling**:
Use predictable block naming for elements such as star ratings. Apply color changes programmatically with `setColor` to visualize rating or brand status.
**Sequential Template Processing**:
Process each variant one at a time to reduce memory pressure and simplify export tracking.
## Prerequisites
- CE.SDK for Android integrated through Gradle.
- A valid license key.
- Templates saved as `.scene` files in assets or available via URLs.
- Template variables and named blocks prepared for population.
## Initialize the Engine
```kotlin
import ly.img.engine.Engine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun makeEngine(license: String, userId: String) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.multiimage")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
val baseUri = Uri.parse("https://cdn.img.ly/packages/imgly/cesdk-android/$UBQ_VERSION$/assets")
listOf(
"ly.img.sticker", "ly.img.vector.shape", "ly.img.filter", "ly.img.color.palette",
"ly.img.effect", "ly.img.blur", "ly.img.typeface", "ly.img.crop.presets",
"ly.img.page.presets", "ly.img.text", "ly.img.text.components",
).forEach { id ->
engine.asset.addLocalSourceFromJSON(contentUri = "$baseUri/$id/content.json".toUri())
}
}
```
## Define Your Data Model
Your data model can use proper typing for variables. When you insert values into the templates, you will often need to convert them to strings.
```kotlin
import java.util.UUID
data class Restaurant(
val id: UUID = UUID.randomUUID(),
val name: String,
val rating: Double,
val reviewCount: Int,
val imageURL: String,
val logoURL: String,
val brandPrimary: String,
val brandSecondary: String
)
```
This model provides a data record for the example code below.
## Populate Templates and Export Variants
Use one template per format such as:
- square
- portrait
- landscape
Populate the templates sequentially.
```kotlin
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.MimeType
import java.io.File
suspend fun generateVariants(
engine: Engine,
context: Context,
restaurant: Restaurant
): List {
val templates = listOf(
"restaurant_square.scene",
"restaurant_portrait.scene",
"restaurant_landscape.scene"
)
val results = mutableListOf()
for (template in templates) {
val templateUri = Uri.parse("file:///android_asset/templates/$template")
val scene = engine.scene.load(sceneUri = templateUri)
// Set text variables
engine.variable.set(key = "RestaurantName", value = restaurant.name)
engine.variable.set(key = "Rating", value = String.format("%.1f ★", restaurant.rating))
engine.variable.set(key = "ReviewCount", value = "${restaurant.reviewCount}")
// Replace images
replaceImage(engine, name = "RestaurantImage", uri = restaurant.imageURL)
replaceImage(engine, name = "Logo", uri = restaurant.logoURL)
// Apply brand theme
applyBrandTheme(
engine = engine,
primary = parseColor(restaurant.brandPrimary),
secondary = parseColor(restaurant.brandSecondary)
)
// Export variant
val output = exportJPEG(engine, context, outputName(restaurant, template))
results.add(output)
}
return results
}
fun outputName(restaurant: Restaurant, template: String): String {
val format = template.substringAfter("restaurant_").substringBefore(".scene")
return "${restaurant.name.replace(" ", "_")}_$format"
}
```
**Helper Functions**:
The preceding code example uses some helper functions. These aren't part of the CE.SDK. Possible implementations of the functions follow.
```kotlin
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.MimeType
import java.io.File
fun replaceImage(engine: Engine, name: String, uri: String) {
val matches = engine.block.findByName(name)
if (matches.isNotEmpty()) {
val block = matches.first()
val fill = engine.block.getFill(block)
engine.block.setString(fill, property = "fill/image/imageFileURI", value = uri)
engine.block.resetCrop(block)
}
}
fun applyBrandTheme(engine: Engine, primary: Color, secondary: Color) {
val allBlocks = engine.block.findAll()
for (block in allBlocks) {
when (engine.block.getType(block)) {
"//ly.img.ubq/text" -> {
engine.block.setTextColor(block, color = primary)
}
"//ly.img.ubq/graphic" -> {
runCatching {
val fill = engine.block.getFill(block)
engine.block.setColor(fill, property = "fill/color/value", color = secondary)
}
}
}
}
}
suspend fun exportJPEG(engine: Engine, context: Context, name: String): File {
val page = engine.block.findByType(DesignBlockType.Page).firstOrNull()
?: throw IllegalStateException("No page found")
val data = engine.block.export(page, mimeType = MimeType.JPEG)
val dir = context.filesDir
val file = File(dir, "$name.jpg")
withContext(Dispatchers.IO) {
file.outputStream().channel.use { channel ->
channel.write(data)
}
}
return file
}
```
## Color Utility
Add this helper to convert hex strings into CE.SDK `Color` values.
```kotlin
import ly.img.engine.Color
fun parseColor(hex: String): Color {
var hexString = hex.trim().removePrefix("#")
// Add alpha if missing
if (hexString.length == 6) {
hexString += "FF"
}
val hexValue = hexString.toLongOrNull(16) ?: 0L
val r = ((hexValue and 0xFF000000) shr 24).toFloat() / 255f
val g = ((hexValue and 0x00FF0000) shr 16).toFloat() / 255f
val b = ((hexValue and 0x0000FF00) shr 8).toFloat() / 255f
val a = (hexValue and 0x000000FF).toFloat() / 255f
return Color(r = r, g = g, b = b, a = a)
}
```
## Preview the Generated Variants
Use Jetpack Compose to display and share generated images.
```kotlin
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import java.io.File
@Composable
fun VariantsGrid(files: List) {
var selectedFile by remember { mutableStateOf(null) }
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(files) { file ->
Card(
modifier = Modifier
.aspectRatio(1f)
.shadow(elevation = 2.dp, shape = RoundedCornerShape(10.dp))
.clickable { selectedFile = file },
shape = RoundedCornerShape(10.dp)
) {
Image(
painter = rememberAsyncImagePainter(file),
contentDescription = file.name,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
}
}
// Handle share dialog for selectedFile if needed
}
```
## Advanced Use Cases
**Conditional Content**:
Show or hide elements based on data values—for example, color stars according to the rating.
```kotlin
import ly.img.engine.Color
import ly.img.engine.Engine
fun colorStars(engine: Engine, rating: Int, baseName: String = "Rating") {
for (index in 1..5) {
val starBlocks = engine.block.findByName("$baseName$index")
if (starBlocks.isEmpty()) continue
val star = starBlocks.first()
runCatching {
val fill = engine.block.getFill(star)
val color = if (index <= rating) {
parseColor("#FFD60A")
} else {
parseColor("#CCCCCC")
}
engine.block.setColor(fill, property = "fill/color/value", color = color)
}
}
}
```
**Custom Assets**:
Add your own logos or fonts by registering a custom asset source. See the [Unsplash](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) guide for a complete custom asset source example covering search, pagination, and asset mapping.
**Adopter Mode Editing**:
Allow users to open the generated design in the editor UI for minor edits. Serialize the populated scene with `engine.scene.saveToString()` and load it into the Design Editor configured for [restricted content](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) editing.
## Troubleshooting
**❌ Variables not updating**:
- Verify variable names in both template and code using `engine.variable.findAll()`.
- Variable names are case-sensitive.
- Ensure `engine.variable.set()` is called with the correct key.
**❌ Images missing**:
- Confirm local path or remote URL points to a valid image.
- For remote images, add `` to AndroidManifest.xml.
- Verify CORS settings for remote images.
**❌ Colors incorrect**:
- Check block type before applying color with `engine.block.getType()`.
- Ensure color values are in range 0-1 (not 0-255).
- Use `runCatching` to handle blocks that don't support fills.
**❌ Memory spikes**:
- Process templates sequentially, not in parallel.
- Call `engine.stop()` when completely done.
- Clean up temporary files after export.
**❌ Export size unexpected**:
- Confirm consistent page dimensions across templates.
- Verify `engine.block.getFrameWidth()` and `engine.block.getFrameHeight()` values.
- Check template design settings.
**Debugging Tips**:
- Print variable names using `engine.variable.findAll()`
- Log block names with `engine.block.getName(id)`
- Test with one minimal template before expanding
- Use Android Logcat to track processing flow
## Next Steps
Multi-image generation is one way to automate your workflow. Some other ways the CE.SDK can automate are in these guides:
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) lets you process many data records at once.
- Adapt layouts across aspect ratios using [auto resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/).
- Explore [export formats](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and settings.
- Add branded fonts, logos, and graphics by creating custom asset sources, following the [Unsplash](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) example.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Overview"
description: "Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale."
platform: android
url: "https://img.ly/docs/cesdk/android/automation/overview-34d971/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Overview](https://img.ly/docs/cesdk/android/automation/overview-34d971/)
---
Workflow automation with CreativeEditor SDK (CE.SDK) enables you to programmatically generate, manipulate, and export creative assets—at scale. Whether you're creating thousands of localized ads, preparing platform-specific variants of a campaign, or populating print-ready templates with dynamic data, CE.SDK provides a flexible foundation for automation.
You can run automation entirely on the client, integrate it with your backend, or build hybrid “human-in-the-loop” workflows where users interact with partially automated scenes before export. The automation engine supports static pipelines, making it suitable for a wide range of publishing, e-commerce, and marketing applications. Video support will follow soon.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
### Output Formats
## Mapping Your Use Case
Use the table below to find the right guide for your automation scenario:
| Use Case | Relevant Guide |
| --- | --- |
| Create many images from one template | [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) |
| Merge CSV or JSON data with a template | [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) |
| Generate platform-specific size variants | [Product Variations](https://img.ly/docs/cesdk/android/automation/product-variations-f3349f/) |
| Resize designs for different aspect ratios | [Auto-Resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/) |
| Build scenes programmatically | Automate Design Generation |
| Generate multiple images per design | [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Product Variations"
description: "Generate multiple product variants from a single template by swapping text, images and styles programmatically."
platform: android
url: "https://img.ly/docs/cesdk/android/automation/product-variations-f3349f/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Product Variations](https://img.ly/docs/cesdk/android/automation/product-variations-f3349f/)
---
```kotlin file=@cesdk_android_examples/engine-guides-product-variations/ProductVariations.kt reference-only
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
private data class ProductVariant(
val name: String,
val color: String,
val size: String,
val price: String,
val imageURL: String,
)
private val productVariableKeys = listOf("ProductName", "ProductColor", "ProductPrice")
suspend fun productVariations(
engine: Engine,
context: Context,
): List = withContext(engine.dispatcher) {
val currentVariableKeys = engine.variable.findAll().toSet()
val previousVariables = productVariableKeys
.filter(currentVariableKeys::contains)
.associateWith(engine.variable::get)
try {
createProductVariations(engine = engine, context = context)
} finally {
val variablesToRemove = engine.variable.findAll().toSet()
productVariableKeys.filter(variablesToRemove::contains).forEach(engine.variable::remove)
previousVariables.forEach { (key, value) -> engine.variable.set(key = key, value = value) }
}
}
private suspend fun createProductVariations(
engine: Engine,
context: Context,
): List {
val variants = listOf(
ProductVariant(
name = "Classic Tee",
color = "Midnight Black",
size = "M",
price = "$29.99",
imageURL = "https://img.ly/static/ubq_samples/sample_1.jpg",
),
ProductVariant(
name = "Classic Tee",
color = "Ocean Blue",
size = "L",
price = "$34.99",
imageURL = "https://img.ly/static/ubq_samples/sample_2.jpg",
),
)
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 500F)
engine.block.setHeight(page, value = 500F)
val text = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = text)
engine.block.setWidth(text, value = 400F)
engine.block.setHeight(text, value = 50F)
engine.block.setPositionX(text, value = 50F)
engine.block.setPositionY(text, value = 50F)
engine.block.setName(text, name = "ProductTitle")
engine.block.replaceText(text, text = "{{ProductName}} – {{ProductColor}}")
engine.block.setTextColor(text, color = Color.fromHex("#FF000000"))
val priceText = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = priceText)
engine.block.setWidth(priceText, value = 200F)
engine.block.setHeight(priceText, value = 40F)
engine.block.setPositionX(priceText, value = 50F)
engine.block.setPositionY(priceText, value = 120F)
engine.block.setName(priceText, name = "ProductPriceLabel")
engine.block.replaceText(priceText, text = "{{ProductPrice}}")
engine.block.setTextColor(priceText, color = Color.fromHex("#FF000000"))
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.appendChild(parent = page, child = imageBlock)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 300F)
engine.block.setPositionX(imageBlock, value = 100F)
engine.block.setPositionY(imageBlock, value = 180F)
engine.block.setName(imageBlock, name = "ProductImage")
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setFill(imageBlock, fill = imageFill)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setContentFillMode(imageBlock, ContentFillMode.CONTAIN)
// Seed the variable store that this sample persists with the reusable template string.
engine.variable.set(key = "ProductName", value = variants.first().name)
engine.variable.set(key = "ProductColor", value = variants.first().color)
engine.variable.set(key = "ProductPrice", value = variants.first().price)
val templateString = engine.scene.saveToString(scene = scene)
engine.block.forceLoadResources(listOf(imageBlock, text, priceText))
val tokenRegex = Regex("""\{\{\s*([^{}]+?)\s*\}\}""")
val templateTokens = engine.block.findByType(DesignBlockType.Text)
.flatMap { textBlock ->
tokenRegex.findAll(engine.block.getString(textBlock, property = "text/text"))
.map { match -> match.groupValues[1].trim() }
.toList()
}
.distinct()
println("Template text tokens: $templateTokens")
// Expected: [ProductName, ProductColor, ProductPrice]
val exportedFiles = mutableListOf()
for (variant in variants) {
engine.scene.load(scene = templateString)
engine.variable.set(key = "ProductName", value = variant.name)
engine.variable.set(key = "ProductColor", value = variant.color)
engine.variable.set(key = "ProductPrice", value = variant.price)
// Keep the rendered text blocks in sync for Android's offscreen export path.
engine.block.findByName("ProductTitle").firstOrNull()?.let { title ->
engine.block.replaceText(
title,
text = "${variant.name} – ${variant.color} (${variant.size})",
)
}
engine.block.findByName("ProductPriceLabel").firstOrNull()?.let { priceLabel ->
engine.block.replaceText(priceLabel, text = "${variant.price} · Size ${variant.size}")
}
engine.block.findByName("ProductImage").firstOrNull()?.let { block ->
val fill = engine.block.getFill(block)
engine.block.setString(
block = fill,
property = "fill/image/imageFileURI",
value = variant.imageURL,
)
engine.block.resetCrop(block)
}
val exportPage = engine.block.findByType(DesignBlockType.Page).firstOrNull()
?: continue
// Android needs explicit resource preloading so text glyphs and image fills are
// resolved before the offscreen export runs.
engine.block.forceLoadResources(
engine.block.findByType(DesignBlockType.Text) +
engine.block.findByName("ProductImage"),
)
val blob = engine.block.export(exportPage, mimeType = MimeType.JPEG)
val fileName =
"product-${variant.color.lowercase().replace(" ", "-")}-${variant.size}.jpg"
val file = File(context.cacheDir, fileName)
withContext(Dispatchers.IO) {
blob.rewind()
file.outputStream().channel.use { channel ->
channel.write(blob)
}
}
exportedFiles += file
}
return exportedFiles
}
```
Generate multiple product variants — different colors, sizes or copy — from a single design template using the CE.SDK Engine API in Kotlin.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-product-variations)
## What You'll Learn
- Define a data model to describe product attribute combinations.
- Load a design template and scan its text blocks for `{{...}}` tokens.
- Replace text placeholders and swap product images for each variant.
- Export each variation as JPEG.
## When to Use It
Use product variations when a single product needs multiple visual representations — for example, a t-shirt in five colors or a sneaker in three sizes. Each variant shares the same layout but differs in text, images or styling.
For generating many images from **different data records**, see [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/). For producing **multiple layout formats** from one record, see [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/).
## Define the Data Model
Start by modeling your product variants. Each entry represents one combination of attributes to apply to the template.
```kotlin highlight-android-product-variations-data-model
val variants = listOf(
ProductVariant(
name = "Classic Tee",
color = "Midnight Black",
size = "M",
price = "$29.99",
imageURL = "https://img.ly/static/ubq_samples/sample_1.jpg",
),
ProductVariant(
name = "Classic Tee",
color = "Ocean Blue",
size = "L",
price = "$34.99",
imageURL = "https://img.ly/static/ubq_samples/sample_2.jpg",
),
)
```
The `imageURL` points to the product photo for that color variant. In production, you'd load these from an API or database.
## Create a Template
Product variation templates use **text variables** (wrapped in `{{double braces}}`) and **named image blocks** as placeholders. You can create templates in the Web CE.SDK editor and save them as archives, or build them on any platform programmatically (see example below).
```kotlin highlight-android-product-variations-create-template
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 500F)
engine.block.setHeight(page, value = 500F)
val text = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = text)
engine.block.setWidth(text, value = 400F)
engine.block.setHeight(text, value = 50F)
engine.block.setPositionX(text, value = 50F)
engine.block.setPositionY(text, value = 50F)
engine.block.setName(text, name = "ProductTitle")
engine.block.replaceText(text, text = "{{ProductName}} – {{ProductColor}}")
engine.block.setTextColor(text, color = Color.fromHex("#FF000000"))
val priceText = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = priceText)
engine.block.setWidth(priceText, value = 200F)
engine.block.setHeight(priceText, value = 40F)
engine.block.setPositionX(priceText, value = 50F)
engine.block.setPositionY(priceText, value = 120F)
engine.block.setName(priceText, name = "ProductPriceLabel")
engine.block.replaceText(priceText, text = "{{ProductPrice}}")
engine.block.setTextColor(priceText, color = Color.fromHex("#FF000000"))
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.appendChild(parent = page, child = imageBlock)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 300F)
engine.block.setPositionX(imageBlock, value = 100F)
engine.block.setPositionY(imageBlock, value = 180F)
engine.block.setName(imageBlock, name = "ProductImage")
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setFill(imageBlock, fill = imageFill)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setContentFillMode(imageBlock, ContentFillMode.CONTAIN)
// Seed the variable store that this sample persists with the reusable template string.
engine.variable.set(key = "ProductName", value = variants.first().name)
engine.variable.set(key = "ProductColor", value = variants.first().color)
engine.variable.set(key = "ProductPrice", value = variants.first().price)
val templateString = engine.scene.saveToString(scene = scene)
```
The template text uses `{{ProductName}}`, `{{ProductColor}}` and `{{ProductPrice}}` tokens, and the sample seeds those keys into `engine.variable` before saving the reusable template string. Named image blocks like `"ProductImage"` let you swap fills by name.
## Discover Template Variables
Before processing, scan the template text blocks for `{{...}}` tokens so you can validate the placeholders the design expects at runtime.
```kotlin highlight-android-product-variations-discover-variables
val tokenRegex = Regex("""\{\{\s*([^{}]+?)\s*\}\}""")
val templateTokens = engine.block.findByType(DesignBlockType.Text)
.flatMap { textBlock ->
tokenRegex.findAll(engine.block.getString(textBlock, property = "text/text"))
.map { match -> match.groupValues[1].trim() }
.toList()
}
.distinct()
println("Template text tokens: $templateTokens")
// Expected: [ProductName, ProductColor, ProductPrice]
```
On Android, `engine.variable.findAll()` inspects the current variable store, not unresolved placeholder tokens in the template text. The sample therefore scans each text block's `text/text` content with a regex and extracts the referenced token names directly from the template.
## Generate Variations
Loop through each variant, reload the template, populate variables, then export.
```kotlin highlight-android-product-variations-generate-loop
for (variant in variants) {
engine.scene.load(scene = templateString)
engine.variable.set(key = "ProductName", value = variant.name)
engine.variable.set(key = "ProductColor", value = variant.color)
engine.variable.set(key = "ProductPrice", value = variant.price)
// Keep the rendered text blocks in sync for Android's offscreen export path.
engine.block.findByName("ProductTitle").firstOrNull()?.let { title ->
engine.block.replaceText(
title,
text = "${variant.name} – ${variant.color} (${variant.size})",
)
}
engine.block.findByName("ProductPriceLabel").firstOrNull()?.let { priceLabel ->
engine.block.replaceText(priceLabel, text = "${variant.price} · Size ${variant.size}")
}
engine.block.findByName("ProductImage").firstOrNull()?.let { block ->
val fill = engine.block.getFill(block)
engine.block.setString(
block = fill,
property = "fill/image/imageFileURI",
value = variant.imageURL,
)
engine.block.resetCrop(block)
}
val exportPage = engine.block.findByType(DesignBlockType.Page).firstOrNull()
?: continue
// Android needs explicit resource preloading so text glyphs and image fills are
// resolved before the offscreen export runs.
engine.block.forceLoadResources(
engine.block.findByType(DesignBlockType.Text) +
engine.block.findByName("ProductImage"),
)
val blob = engine.block.export(exportPage, mimeType = MimeType.JPEG)
val fileName =
"product-${variant.color.lowercase().replace(" ", "-")}-${variant.size}.jpg"
val file = File(context.cacheDir, fileName)
withContext(Dispatchers.IO) {
blob.rewind()
file.outputStream().channel.use { channel ->
channel.write(blob)
}
}
exportedFiles += file
}
```
### Set Text Variables
Use `engine.variable.set(key =, value =)` to replace each placeholder with the variant's data:
```kotlin highlight-android-product-variations-set-variables
engine.variable.set(key = "ProductName", value = variant.name)
engine.variable.set(key = "ProductColor", value = variant.color)
engine.variable.set(key = "ProductPrice", value = variant.price)
// Keep the rendered text blocks in sync for Android's offscreen export path.
engine.block.findByName("ProductTitle").firstOrNull()?.let { title ->
engine.block.replaceText(
title,
text = "${variant.name} – ${variant.color} (${variant.size})",
)
}
engine.block.findByName("ProductPriceLabel").firstOrNull()?.let { priceLabel ->
engine.block.replaceText(priceLabel, text = "${variant.price} · Size ${variant.size}")
}
```
All variable values are strings. Convert numbers or prices to their display format before setting them.
### Replace the Product Image
Find the image block by its name and update its fill URI:
```kotlin highlight-android-product-variations-replace-image
engine.block.findByName("ProductImage").firstOrNull()?.let { block ->
val fill = engine.block.getFill(block)
engine.block.setString(
block = fill,
property = "fill/image/imageFileURI",
value = variant.imageURL,
)
engine.block.resetCrop(block)
}
```
The block name `"ProductImage"` was assigned when the template was created. Using names keeps automation readable compared to referencing block IDs directly.
### Export the Variant
Export the populated page as JPEG and write it to disk:
```kotlin highlight-android-product-variations-export
val exportPage = engine.block.findByType(DesignBlockType.Page).firstOrNull()
?: continue
// Android needs explicit resource preloading so text glyphs and image fills are
// resolved before the offscreen export runs.
engine.block.forceLoadResources(
engine.block.findByType(DesignBlockType.Text) +
engine.block.findByName("ProductImage"),
)
val blob = engine.block.export(exportPage, mimeType = MimeType.JPEG)
val fileName =
"product-${variant.color.lowercase().replace(" ", "-")}-${variant.size}.jpg"
val file = File(context.cacheDir, fileName)
withContext(Dispatchers.IO) {
blob.rewind()
file.outputStream().channel.use { channel ->
channel.write(blob)
}
}
exportedFiles += file
```
On Android, preload the text and image blocks before exporting so glyphs and remote image fills are resolved before the offscreen JPEG render runs.
You can export as PNG, PDF or other formats by changing the `mimeType` parameter. See the [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) guide for all available options.
## Next Steps
Product variations are one pattern for automating design output. Explore related guides:
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) — process many data records at once.
- [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) — create multiple layout formats from one record.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) — deep dive into the variable system.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) — work with placeholder blocks.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Bundle Size"
description: "Understand CE.SDK’s engine and editor bundle sizes and how they affect your mobile app’s download footprint."
platform: android
url: "https://img.ly/docs/cesdk/android/bundle-size-df9210/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/) > [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/)
---
## Engine Download Size
When included in your app, the download size of the engine is different depending on the architecture:
- arm64-v8a ~ 14.9MB
- armeabi-v7a ~ 13.7MB
- x86\_64 ~ 14.9MB
- x86 ~ 14.9MB
This means that the download size from Play Store will be increased by the amount mentioned above.
## Mobile Editor Download Size
In order to use the mobile editor, you either have to use the gradle dependency, or directly copy the solutions from our [repository](https://github.com/imgly/cesdk-android-examples). No matter which approach you choose, you can expect the download size of the mobile editor to be around the size of the engine plus a few additional megabytes. The precise size may depend on the bundled assets (scene files, images, stickers), however, with the default resources you can expect it to be around **3 MB** plus the size of the engine. Note that this does not include the size of the Jetpack Compose library. Also note that this is measured without R8 optimizations. Enabling it in your project will shrink it further. For more information on R8, follow this [link](https://developer.android.com/build/shrink-code).
## Including as a Dynamic Feature
If you want to include the engine or the mobile editor via dependency as a dynamic feature, create an android module, add the dependency of the engine/mobile editor to that module and make that module dynamic. Here is the [link](https://developer.android.com/guide/playcore/feature-delivery) on how to create a dynamic module and load it. If you want to include the mobile editor by copying our repository, you do not need to create an extra module. Simply declare the `:editor` module as dynamic when you copy that module to your project.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Capabilities"
description: "Explore the full list of CE.SDK capabilities available for your platform, including design, video, image, text, and more."
platform: android
url: "https://img.ly/docs/cesdk/android/capabilities-e1906f/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) > [Capabilities](https://img.ly/docs/cesdk/android/capabilities-e1906f/)
---
A comprehensive overview of all CE.SDK capabilities available for .
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Colors"
description: "Manage color usage in your designs, from applying brand palettes to handling print and screen formats."
platform: android
url: "https://img.ly/docs/cesdk/android/colors-a9b79c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/)
---
---
## Related Pages
- [Overview](https://img.ly/docs/cesdk/android/colors/overview-16a177/) - Manage color usage in your designs, from applying brand palettes to handling print and screen formats.
- [Color Basics](https://img.ly/docs/cesdk/android/colors/basics-307115/) - Learn how color works in CE.SDK, including the three supported color spaces (sRGB, CMYK, and Spot) and when to use each for screen display or print workflows.
- [For Print](https://img.ly/docs/cesdk/android/colors/for-print-59bc05/) - Use print-ready color models and settings for professional-quality, production-ready exports.
- [For Screen](https://img.ly/docs/cesdk/android/colors/for-screen-1911f8/) - Documentation for For Screen
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply solid colors to shapes, backgrounds, and other design elements.
- [Create a Color Palette](https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/) - Build reusable color palettes to maintain consistency and streamline user choices.
- [Replace Individual Colors](https://img.ly/docs/cesdk/android/colors/replace-48cd71/) - Selectively replace specific colors in images using CE.SDK's Recolor and Green Screen effects on Android.
- [Adjust Colors](https://img.ly/docs/cesdk/android/colors/adjust-590d1e/) - Fine-tune image-backed graphic blocks by adjusting brightness, contrast, saturation, exposure, and other color properties.
- [Extract Dominant Colors](https://img.ly/docs/cesdk/android/colors/extract-colors-d4c0a1/) - Read the most prominent colors from the rendered appearance of a block with the CE.SDK engine on Android.
- [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/) - Learn how to convert colors between color spaces in CE.SDK. Convert sRGB, CMYK, and spot colors programmatically for screen display or print workflows.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Adjust Colors"
description: "Fine-tune image-backed graphic blocks by adjusting brightness, contrast, saturation, exposure, and other color properties."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/adjust-590d1e/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Adjust Colors](https://img.ly/docs/cesdk/android/colors/adjust-590d1e/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-adjust/ColorsAdjust.kt reference-only
import android.net.Uri
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import kotlin.math.abs
data class ColorsAdjust(
val brightness: Float,
val contrast: Float,
val saturation: Float,
val propertyCount: Int,
val disabledState: Boolean,
val enabledState: Boolean,
val moodySaturation: Float,
val orderedStackMatches: Boolean,
val sharpness: Float,
val resetSucceeded: Boolean,
val removed: Boolean,
)
suspend fun colorsAdjust(engine: Engine): ColorsAdjust {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageGraphicBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageGraphicBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageGraphicBlock, value = 100F)
engine.block.setPositionY(imageGraphicBlock, value = 50F)
engine.block.setWidth(imageGraphicBlock, value = 300F)
engine.block.setHeight(imageGraphicBlock, value = 300F)
engine.block.appendChild(parent = page, child = imageGraphicBlock)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(imageGraphicBlock, fill = fill)
val sceneSupportsEffects = engine.block.supportsEffects(scene)
val pageSupportsEffects = engine.block.supportsEffects(page)
val imageGraphicSupportsEffects = engine.block.supportsEffects(imageGraphicBlock)
require(!sceneSupportsEffects) { "Scenes do not support effect stacks." }
require(pageSupportsEffects) { "Pages can expose effect stacks." }
require(imageGraphicSupportsEffects) { "Image-backed graphic blocks can render adjustments." }
val adjustmentsEffect = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.appendEffect(block = imageGraphicBlock, effectBlock = adjustmentsEffect)
check(engine.block.getEffects(imageGraphicBlock).contains(adjustmentsEffect))
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/brightness", value = 0.2F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/contrast", value = 0.15F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/saturation", value = 0.3F)
val brightness = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/brightness")
val contrast = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/contrast")
val saturation = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/saturation")
val availableProperties = engine.block.findAllProperties(adjustmentsEffect)
check(abs(brightness - 0.2F) < 0.0001F)
check(abs(contrast - 0.15F) < 0.0001F)
check(abs(saturation - 0.3F) < 0.0001F)
check(availableProperties.any { it.startsWith("effect/adjustments/") })
engine.block.setEffectEnabled(effectBlock = adjustmentsEffect, enabled = false)
val disabledState = engine.block.isEffectEnabled(adjustmentsEffect)
engine.block.setEffectEnabled(effectBlock = adjustmentsEffect, enabled = true)
val enabledState = engine.block.isEffectEnabled(adjustmentsEffect)
check(!disabledState)
check(enabledState)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/brightness", value = -0.1F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/contrast", value = 0.35F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/saturation", value = -0.25F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/temperature", value = -0.2F)
val moodySaturation = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/saturation")
check(abs(moodySaturation - -0.25F) < 0.0001F)
val pixelizeEffect = engine.block.createEffect(type = EffectType.Pixelize)
engine.block.insertEffect(block = imageGraphicBlock, effectBlock = pixelizeEffect, index = 1)
val orderedEffects = engine.block.getEffects(imageGraphicBlock)
val orderedStackMatches = orderedEffects == listOf(adjustmentsEffect, pixelizeEffect)
check(orderedStackMatches)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/sharpness", value = 0.3F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/clarity", value = 0.25F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/highlights", value = -0.15F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/shadows", value = 0.2F)
val sharpness = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/sharpness")
check(abs(sharpness - 0.3F) < 0.0001F)
val adjustmentProperties = engine.block
.findAllProperties(adjustmentsEffect)
.filter { it.startsWith("effect/adjustments/") }
check(adjustmentProperties.isNotEmpty())
adjustmentProperties.forEach { property ->
engine.block.setFloat(adjustmentsEffect, property = property, value = 0F)
}
val resetSucceeded = adjustmentProperties.all { property ->
abs(engine.block.getFloat(adjustmentsEffect, property = property)) < 0.0001F
}
check(resetSucceeded)
val effects = engine.block.getEffects(imageGraphicBlock)
val adjustmentIndex = effects.indexOf(adjustmentsEffect)
require(adjustmentIndex >= 0) { "The adjustments effect must be attached before it can be removed." }
engine.block.removeEffect(block = imageGraphicBlock, index = adjustmentIndex)
engine.block.destroy(adjustmentsEffect)
val removed = engine.block.getEffects(imageGraphicBlock).none { it == adjustmentsEffect }
check(removed)
val pixelizeIndex = engine.block.getEffects(imageGraphicBlock).indexOf(pixelizeEffect)
if (pixelizeIndex >= 0) {
engine.block.removeEffect(block = imageGraphicBlock, index = pixelizeIndex)
}
engine.block.destroy(pixelizeEffect)
return ColorsAdjust(
brightness = brightness,
contrast = contrast,
saturation = saturation,
propertyCount = adjustmentProperties.size,
disabledState = disabledState,
enabledState = enabledState,
moodySaturation = moodySaturation,
orderedStackMatches = orderedStackMatches,
sharpness = sharpness,
resetSucceeded = resetSucceeded,
removed = removed,
)
}
```
Fine-tune image-backed graphic blocks on Android by applying CE.SDK adjustment effects for brightness, contrast, saturation, and tonal refinement.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-adjust)
Color adjustments modify the visual appearance of image-backed graphic blocks by changing properties like brightness, contrast, saturation, and color temperature. CE.SDK represents these changes as an `EffectType.Adjustments` block that you attach to a compatible design block.
This guide covers the default Android adjustments UI and the engine APIs you can use when your app needs to apply the same changes programmatically.
## Using the Built-in Adjustments UI
The default Android editor exposes adjustments through its built-in dock and inspector controls when the current selection allows appearance adjustments. Users can open the adjustments sheet, move sliders, and preview the result immediately.
The built-in sheet uses the same adjustments effect shown below: it creates the effect when needed, attaches it to the selected block, and writes float properties on that effect block.
## Check Block Compatibility
Before applying adjustments, verify that the target block supports effects. Scene blocks do not expose effect stacks. Pages and image-backed graphic blocks both support effects; choose the graphic block when the adjustment should affect image content rather than the entire page background.
```kotlin highlight-android-check-support
val sceneSupportsEffects = engine.block.supportsEffects(scene)
val pageSupportsEffects = engine.block.supportsEffects(page)
val imageGraphicSupportsEffects = engine.block.supportsEffects(imageGraphicBlock)
require(!sceneSupportsEffects) { "Scenes do not support effect stacks." }
require(pageSupportsEffects) { "Pages can expose effect stacks." }
require(imageGraphicSupportsEffects) { "Image-backed graphic blocks can render adjustments." }
```
## Create and Apply Adjustments Effect
Create an `EffectType.Adjustments` block and append it to the image-backed graphic block. A block should only have one adjustments effect in its effect stack. That effect stores all color adjustment properties for the block.
```kotlin highlight-android-create-adjustments
val adjustmentsEffect = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.appendEffect(block = imageGraphicBlock, effectBlock = adjustmentsEffect)
```
## Modify Adjustment Properties
Set individual adjustment values with `setFloat()` on the adjustments effect block. Each adjustment property uses the `effect/adjustments/` prefix followed by the property name.
```kotlin highlight-android-set-properties
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/brightness", value = 0.2F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/contrast", value = 0.15F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/saturation", value = 0.3F)
```
CE.SDK provides these adjustment properties:
| Property | Description |
| --- | --- |
| `brightness` | Overall lightness; positive values lighten and negative values darken |
| `contrast` | Tonal range; positive values increase separation between light and dark |
| `saturation` | Color intensity; positive values increase vibrancy and negative values desaturate |
| `exposure` | Exposure compensation |
| `gamma` | Midtone brightness through the gamma curve |
| `highlights` | Bright area intensity |
| `shadows` | Dark area intensity |
| `whites` | White point adjustment |
| `blacks` | Black point adjustment |
| `temperature` | Warm/cool color cast; positive for warmer, negative for cooler tones |
| `sharpness` | Edge sharpness; positive values sharpen and negative values soften edges |
| `clarity` | Midtone contrast |
The built-in editor sliders use `-1F` to `1F` for these adjustment properties, while `setFloat()` writes the float values you provide. Validate custom controls and presets before writing them.
## Read Adjustment Values
Read current adjustment values with `getFloat()` and the same property paths. Use `findAllProperties()` when you need to inspect which properties are available on the effect block.
```kotlin highlight-android-read-values
val brightness = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/brightness")
val contrast = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/contrast")
val saturation = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/saturation")
val availableProperties = engine.block.findAllProperties(adjustmentsEffect)
```
This is useful for custom controls, synchronization, or persisting adjustment settings in your app.
## Enable and Disable Adjustments
Toggle the adjustments effect when you need a before/after preview without losing the configured values.
```kotlin highlight-android-enable-disable
engine.block.setEffectEnabled(effectBlock = adjustmentsEffect, enabled = false)
val disabledState = engine.block.isEffectEnabled(adjustmentsEffect)
engine.block.setEffectEnabled(effectBlock = adjustmentsEffect, enabled = true)
val enabledState = engine.block.isEffectEnabled(adjustmentsEffect)
```
Disabling the effect keeps it attached to the block. Re-enable it to render the same adjustment values again.
## Applying Different Adjustment Styles
Combine several adjustment properties to create a specific look. This example creates a cooler, moodier result with lower brightness, higher contrast, reduced saturation, and lower temperature.
```kotlin highlight-android-combine-effects
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/brightness", value = -0.1F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/contrast", value = 0.35F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/saturation", value = -0.25F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/temperature", value = -0.2F)
```
Use the same pattern for warm, vibrant, or high-contrast styles.
## Combine Adjustments with Other Effects
Effect stacks render in order, so add effects in the order you want them to render, or use `insertEffect()` when a new effect needs a specific index. This example inserts a pixelize effect after the adjustments effect and reads the stack back to confirm the order.
```kotlin highlight-android-stack-order
val pixelizeEffect = engine.block.createEffect(type = EffectType.Pixelize)
engine.block.insertEffect(block = imageGraphicBlock, effectBlock = pixelizeEffect, index = 1)
val orderedEffects = engine.block.getEffects(imageGraphicBlock)
```
Use `appendEffect()` when a new effect can render after the existing stack, `insertEffect()` when it must occupy a specific index, and `getEffects()` when you need to inspect the current stack.
## Refinement Adjustments
Refinement properties help tune detail and tonal balance after the basic color correction is in place.
```kotlin highlight-android-refinement-adjustments
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/sharpness", value = 0.3F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/clarity", value = 0.25F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/highlights", value = -0.15F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/shadows", value = 0.2F)
```
The refinement properties are useful for photo enhancement:
- **Sharpness**: Enhances edge definition.
- **Clarity**: Increases local midtone contrast.
- **Highlights**: Controls bright image areas.
- **Shadows**: Controls dark image areas.
## Reset Adjustments
Reset adjustments by writing `0F` to each property. This keeps the effect attached while returning it to its neutral state.
```kotlin highlight-android-reset-adjustments
val adjustmentProperties = engine.block
.findAllProperties(adjustmentsEffect)
.filter { it.startsWith("effect/adjustments/") }
check(adjustmentProperties.isNotEmpty())
adjustmentProperties.forEach { property ->
engine.block.setFloat(adjustmentsEffect, property = property, value = 0F)
}
```
## Remove Adjustments
When you no longer need the adjustments effect, remove it from the block's effect stack and destroy the effect block when it is no longer used.
```kotlin highlight-android-remove-adjustments
val effects = engine.block.getEffects(imageGraphicBlock)
val adjustmentIndex = effects.indexOf(adjustmentsEffect)
require(adjustmentIndex >= 0) { "The adjustments effect must be attached before it can be removed." }
engine.block.removeEffect(block = imageGraphicBlock, index = adjustmentIndex)
engine.block.destroy(adjustmentsEffect)
```
`removeEffect()` takes the effect index within the block's effect stack, so read the stack before removing the effect.
## Troubleshooting
| Issue | Fix |
| --- | --- |
| Adjustments are not visible | Check `supportsEffects()` on the target block, verify the effect is enabled, and make sure it was appended to the block. |
| Values have no visible effect | Confirm the values are non-zero, the block contains image content, and the effect stack order is correct when multiple effects are attached. |
| Property lookup fails | Use `findAllProperties()` on the adjustments effect and verify the `effect/adjustments/` prefix. |
## API Reference
| API | Description |
| --- | --- |
| `engine.block.supportsEffects(block=_)` | Checks whether a design block can render effects |
| `engine.block.createEffect(type=EffectType.Adjustments)` | Creates an adjustments effect block |
| `engine.block.createEffect(type=EffectType.Pixelize)` | Creates a second effect used to demonstrate effect stack order |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Adds the effect to the end of a block's effect stack |
| `engine.block.insertEffect(block=_, effectBlock=_, index=_)` | Inserts an effect at a specific stack index |
| `engine.block.getEffects(block=_)` | Returns the effects attached to a block |
| `engine.block.removeEffect(block=_, index=_)` | Removes the effect at the specified stack index |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Returns whether an effect block is enabled |
| `engine.block.setFloat(block=_, property="effect/adjustments/brightness", value=_)` | Writes a float adjustment value |
| `engine.block.getFloat(block=_, property="effect/adjustments/brightness")` | Reads a float adjustment value |
| `engine.block.findAllProperties(block=_)` | Lists the properties available on a block |
| `engine.block.destroy(block=_)` | Destroys an unused effect block |
## Next Steps
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply colors to fills, strokes, and shadows.
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Apply, configure, stack, and manage filters and effects.
- [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/) - Convert between color spaces.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Apply Colors"
description: "Apply solid colors to shapes, backgrounds, and other design elements."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/apply-2211e3/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors/Colors.kt reference-only
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.CMYKColor
import ly.img.engine.Color
import ly.img.engine.ColorSpace
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
fun colors(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
try {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = 350F)
engine.block.setPositionY(block, value = 400F)
engine.block.setWidth(block, value = 100F)
engine.block.setHeight(block, value = 100F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(block, fill = fill)
val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F)
val cmykRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 1F)
val cmykPartialRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 0.5F)
val spotPinkFlamingo = Color.fromSpotColor(
name = "Pink-Flamingo",
tint = 1F,
externalReference = "Brand-Colors",
)
val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F)
engine.editor.setSpotColor(
name = "Pink-Flamingo",
Color.fromRGBA(r = 0.988F, g = 0.455F, b = 0.992F),
)
engine.editor.setSpotColor(name = "Yellow", Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F))
val colorFill = engine.block.getFill(block)
// Fill colors use the generic color property path for RGB, CMYK, and spot colors.
engine.block.setColor(
colorFill,
property = "fill/color/value",
value = rgbaBlue,
)
engine.block.setColor(
colorFill,
property = "fill/color/value",
value = cmykRed,
)
engine.block.setColor(
colorFill,
property = "fill/color/value",
value = spotPinkFlamingo,
)
val currentFillColor = engine.block.getColor(
colorFill,
property = "fill/color/value",
)
check(currentFillColor == spotPinkFlamingo)
engine.block.setStrokeEnabled(block, enabled = true)
engine.block.setStrokeWidth(block, width = 8F)
engine.block.setStrokeColor(block, color = cmykPartialRed)
val currentStrokeColor = engine.block.getStrokeColor(block)
check(currentStrokeColor == cmykPartialRed)
engine.block.setDropShadowEnabled(block, enabled = true)
engine.block.setDropShadowOffsetX(block, offsetX = 12F)
engine.block.setDropShadowOffsetY(block, offsetY = 12F)
engine.block.setDropShadowColor(block, color = spotPartialYellow)
val currentShadowColor = engine.block.getDropShadowColor(block)
check(currentShadowColor == spotPartialYellow)
val cmykBlueConverted = engine.editor.convertColorToColorSpace(
color = rgbaBlue,
colorSpace = ColorSpace.CMYK,
)
val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace(
color = spotPinkFlamingo,
colorSpace = ColorSpace.SRGB,
)
check(cmykBlueConverted is CMYKColor)
check(rgbaPinkFlamingoConverted is RGBAColor)
val definedSpotColors = engine.editor.findAllSpotColors()
check("Pink-Flamingo" in definedSpotColors)
check("Yellow" in definedSpotColors)
engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F))
val updatedYellow = engine.editor.getSpotColorCMYK("Yellow")
check(updatedYellow.c == 0.2F)
engine.editor.removeSpotColor("Yellow")
check("Yellow" !in engine.editor.findAllSpotColors())
} finally {
engine.stop()
}
}
```
Apply solid colors to design elements like shapes, text, and backgrounds using
CE.SDK's Android Engine API.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors)
Colors in CE.SDK are applied to block properties like fill, stroke, and shadow. The Android Engine API supports sRGB for screen display, CMYK for print production, and spot colors for specialized printing workflows.
This guide covers how to create Android color objects, apply them to fill, stroke, and shadow properties, manage spot color definitions, and convert colors to sRGB or CMYK.
## Create Color Objects
CE.SDK represents each color space with a dedicated Android color type. Create RGB, CMYK, and spot color values that match the target output.
```kotlin highlight-android-create-colors
val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F)
val cmykRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 1F)
val cmykPartialRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 0.5F)
val spotPinkFlamingo = Color.fromSpotColor(
name = "Pink-Flamingo",
tint = 1F,
externalReference = "Brand-Colors",
)
val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F)
```
RGB colors use red, green, blue, and alpha values from `0.0` to `1.0`. CMYK colors use cyan, magenta, yellow, black, and tint values from `0.0` to `1.0`. Spot colors reference a named spot color definition and can include an external reference for the color source.
## Define Spot Colors
Before applying a spot color, define its screen preview approximation. The engine needs this approximation because spot colors represent inks that cannot be rendered directly on screen.
```kotlin highlight-android-define-spot
engine.editor.setSpotColor(
name = "Pink-Flamingo",
Color.fromRGBA(r = 0.988F, g = 0.455F, b = 0.992F),
)
engine.editor.setSpotColor(name = "Yellow", Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F))
```
Use `engine.editor.setSpotColor()` with either `Color.fromRGBA()` or `Color.fromCMYK()` to define the approximation. Reuse the same name to update an existing spot color definition.
## Apply Fill Colors
To set a block's fill color, get the fill block with `engine.block.getFill()`, then set `"fill/color/value"` on that fill block.
```kotlin highlight-android-apply-fill
val colorFill = engine.block.getFill(block)
// Fill colors use the generic color property path for RGB, CMYK, and spot colors.
engine.block.setColor(
colorFill,
property = "fill/color/value",
value = rgbaBlue,
)
engine.block.setColor(
colorFill,
property = "fill/color/value",
value = cmykRed,
)
engine.block.setColor(
colorFill,
property = "fill/color/value",
value = spotPinkFlamingo,
)
val currentFillColor = engine.block.getColor(
colorFill,
property = "fill/color/value",
)
```
The fill is a separate block from the graphic block. Use `engine.block.getColor()` with the same property path when you need to read the current fill color.
## Apply Stroke Colors
Stroke colors are applied directly to the design block. Enable the stroke first, set its width, and then set the stroke color.
```kotlin highlight-android-apply-stroke
engine.block.setStrokeEnabled(block, enabled = true)
engine.block.setStrokeWidth(block, width = 8F)
engine.block.setStrokeColor(block, color = cmykPartialRed)
val currentStrokeColor = engine.block.getStrokeColor(block)
```
The Android API exposes typed helpers for stroke color, so this path does not need a raw property string.
## Apply Shadow Colors
Drop shadow colors are also applied directly to the design block. Enable the shadow and configure its offset before setting the color.
```kotlin highlight-android-apply-shadow
engine.block.setDropShadowEnabled(block, enabled = true)
engine.block.setDropShadowOffsetX(block, offsetX = 12F)
engine.block.setDropShadowOffsetY(block, offsetY = 12F)
engine.block.setDropShadowColor(block, color = spotPartialYellow)
val currentShadowColor = engine.block.getDropShadowColor(block)
```
Spot colors work with shadows the same way they work with fills and strokes.
## Convert Between Color Spaces
Use `engine.editor.convertColorToColorSpace()` when you need a color value in sRGB or CMYK.
```kotlin highlight-android-convert-color
val cmykBlueConverted = engine.editor.convertColorToColorSpace(
color = rgbaBlue,
colorSpace = ColorSpace.CMYK,
)
val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace(
color = spotPinkFlamingo,
colorSpace = ColorSpace.SRGB,
)
```
Pass the source color and either `ColorSpace.SRGB` or `ColorSpace.CMYK` as the target. `ColorSpace.SPOT_COLOR` is not a valid conversion target. Spot colors can still be source colors; the engine converts them from their registered approximation to sRGB or CMYK. Color conversions are approximations because RGB, CMYK, and spot colors use different color models and may not represent the same colors exactly. Some vibrant sRGB colors may appear muted when converted to CMYK.
## List Defined Spot Colors
Query all spot colors currently registered in the engine with `engine.editor.findAllSpotColors()`.
```kotlin highlight-android-list-spot
val definedSpotColors = engine.editor.findAllSpotColors()
```
The returned list contains the spot color names defined through `engine.editor.setSpotColor()`.
## Update Spot Color Definitions
Redefine a spot color by calling `engine.editor.setSpotColor()` with the same name and a new approximation.
```kotlin highlight-android-update-spot
engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F))
val updatedYellow = engine.editor.getSpotColorCMYK("Yellow")
```
Blocks that reference that spot color use the updated approximation the next time they render.
## Remove Spot Color Definitions
Remove a spot color definition with `engine.editor.removeSpotColor()`.
```kotlin highlight-android-remove-spot
engine.editor.removeSpotColor("Yellow")
```
Blocks that still reference the removed spot color fall back to the default magenta approximation.
## Troubleshooting
### Spot Color Appears Magenta
The spot color was not defined before use. Call `engine.editor.setSpotColor()` with the exact spot color name before applying it to blocks.
### Stroke or Shadow Color Is Not Visible
The effect is not enabled. Call `engine.block.setStrokeEnabled(block, true)` or `engine.block.setDropShadowEnabled(block, true)` before setting the color.
### Color Looks Different After Conversion
Color space conversions are approximations. CMYK has a smaller gamut than sRGB, so vibrant colors can appear muted after conversion.
### Fill Color Does Not Change
Apply colors to the fill block returned by `engine.block.getFill()`, not to the parent design block.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set a fill color that can preserve RGB, CMYK, or spot color values |
| `engine.block.getColor(block=_, property="fill/color/value")` | Read a color property from a fill block |
| `engine.block.getFill(block=_)` | Get the fill block of a design block |
| `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable stroke on a design block |
| `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke width |
| `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color |
| `engine.block.getStrokeColor(block=_)` | Read the stroke color |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable the drop shadow |
| `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set the drop shadow x offset |
| `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set the drop shadow y offset |
| `engine.block.setDropShadowColor(block=_, color=_)` | Set the drop shadow color |
| `engine.block.getDropShadowColor(block=_)` | Read the drop shadow color |
| `engine.editor.setSpotColor(name=_, color=_)` | Define or update a spot color approximation |
| `engine.editor.findAllSpotColors()` | List all defined spot color names |
| `engine.editor.getSpotColorCMYK(name=_)` | Read a spot color's CMYK approximation |
| `engine.editor.removeSpotColor(name=_)` | Remove a spot color definition |
| `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Convert a color to `ColorSpace.SRGB` or `ColorSpace.CMYK`; `ColorSpace.SPOT_COLOR` is not supported as a target |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Color Basics"
description: "Learn how color works in CE.SDK, including the three supported color spaces (sRGB, CMYK, and Spot) and when to use each for screen display or print workflows."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/basics-307115/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Color Basics](https://img.ly/docs/cesdk/android/colors/basics-307115/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-basics/ColorsBasics.kt reference-only
import android.util.Log
import ly.img.engine.CMYKColor
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
import ly.img.engine.SpotColor
private const val TAG = "ColorsBasics"
suspend fun colorsBasics(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val srgbBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(srgbBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(srgbBlock, value = 80F)
engine.block.setPositionY(srgbBlock, value = 120F)
engine.block.setWidth(srgbBlock, value = 160F)
engine.block.setHeight(srgbBlock, value = 160F)
val srgbFill = engine.block.createFill(FillType.Color)
engine.block.setFill(srgbBlock, fill = srgbFill)
engine.block.appendChild(parent = page, child = srgbBlock)
val srgbColor = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
engine.block.setColor(srgbFill, property = "fill/color/value", value = srgbColor)
check(engine.block.getColor(srgbFill, property = "fill/color/value") == srgbColor)
val cmykBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(cmykBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(cmykBlock, value = 320F)
engine.block.setPositionY(cmykBlock, value = 120F)
engine.block.setWidth(cmykBlock, value = 160F)
engine.block.setHeight(cmykBlock, value = 160F)
val cmykFill = engine.block.createFill(FillType.Color)
engine.block.setFill(cmykBlock, fill = cmykFill)
engine.block.appendChild(parent = page, child = cmykBlock)
val cmykColor = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 1F)
engine.block.setColor(cmykFill, property = "fill/color/value", value = cmykColor)
check(engine.block.getColor(cmykFill, property = "fill/color/value") == cmykColor)
engine.editor.setSpotColor(
name = "MyBrand Red",
// RGB spot-color approximations are stored as opaque colors; apply tint on the
// SpotColor reference.
color = Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F),
)
engine.editor.setSpotColor(
name = "MyBrand Blue",
color = Color.fromCMYK(c = 1F, m = 0.7F, y = 0F, k = 0.1F),
)
val storedBrandRed = engine.editor.getSpotColorRGB("MyBrand Red")
val storedBrandBlue = engine.editor.getSpotColorCMYK("MyBrand Blue")
check(storedBrandRed == Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F))
check(storedBrandBlue == Color.fromCMYK(c = 1F, m = 0.7F, y = 0F, k = 0.1F))
val spotBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(spotBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(spotBlock, value = 560F)
engine.block.setPositionY(spotBlock, value = 120F)
engine.block.setWidth(spotBlock, value = 160F)
engine.block.setHeight(spotBlock, value = 160F)
val spotFill = engine.block.createFill(FillType.Color)
engine.block.setFill(spotBlock, fill = spotFill)
engine.block.appendChild(parent = page, child = spotBlock)
val spotColor = Color.fromSpotColor(
name = "MyBrand Red",
tint = 1F,
externalReference = "brand-palette-v1",
)
engine.block.setColor(spotFill, property = "fill/color/value", value = spotColor)
val appliedSpotColor = engine.block.getColor(spotFill, property = "fill/color/value")
check(appliedSpotColor is SpotColor)
check(appliedSpotColor.name == spotColor.name)
check(appliedSpotColor.tint == spotColor.tint)
if (engine.block.supportsStroke(srgbBlock)) {
engine.block.setStrokeEnabled(srgbBlock, enabled = true)
engine.block.setStrokeWidth(srgbBlock, width = 4F)
engine.block.setStrokeColor(
srgbBlock,
color = Color.fromRGBA(r = 0.1F, g = 0.2F, b = 0.5F, a = 1F),
)
}
val cmykStroke = Color.fromCMYK(c = 0F, m = 0.5F, y = 0.6F, k = 0.2F, tint = 1F)
if (engine.block.supportsStroke(cmykBlock)) {
engine.block.setStrokeEnabled(cmykBlock, enabled = true)
engine.block.setStrokeWidth(cmykBlock, width = 4F)
engine.block.setStrokeColor(cmykBlock, color = cmykStroke)
}
if (engine.block.supportsStroke(spotBlock)) {
engine.block.setStrokeEnabled(spotBlock, enabled = true)
engine.block.setStrokeWidth(spotBlock, width = 4F)
engine.block.setStrokeColor(
spotBlock,
color = Color.fromSpotColor(name = "MyBrand Red", tint = 0.7F),
)
}
val readSrgb = engine.block.getColor(srgbFill, property = "fill/color/value")
val readCmyk = engine.block.getColor(cmykFill, property = "fill/color/value")
val readSpot = engine.block.getColor(spotFill, property = "fill/color/value")
val readStroke = engine.block.getStrokeColor(cmykBlock)
for (color in listOf(readSrgb, readCmyk, readSpot, readStroke)) {
when (color) {
is RGBAColor -> Log.i(TAG, "sRGB: r=${color.r}, g=${color.g}, b=${color.b}, a=${color.a}")
is CMYKColor -> Log.i(
TAG,
"CMYK: c=${color.c}, m=${color.m}, y=${color.y}, k=${color.k}, tint=${color.tint}",
)
is SpotColor -> Log.i(
TAG,
"Spot: name=${color.name}, tint=${color.tint}, ref=${color.externalReference}",
)
}
}
check(readSrgb is RGBAColor)
check(readCmyk is CMYKColor)
check(readSpot is SpotColor)
check(readStroke == cmykStroke)
}
```
Understand the three color spaces in CE.SDK and when to use each for screen or print workflows.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-basics)
CE.SDK supports three color spaces: **sRGB** for screen display, **CMYK** for print workflows, and **Spot Color** for specialized printing. On Android, create color values with the `Color` factory functions and apply them with typed block APIs where available.
This guide covers how to choose the correct color space, apply colors to supported properties, and define spot colors with screen preview approximations.
## Color Spaces Overview
CE.SDK represents Android colors as `Color` values:
- `Color.fromRGBA(r=_, g=_, b=_, a=_)` - sRGB color for screen display
- `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` - CMYK color for print workflows
- `Color.fromSpotColor(name=_, tint=_, externalReference=_)` - named spot color for specialized printing
Use `engine.block.setColor()` for generic color properties such as fill colors, and use typed helpers such as `engine.block.setStrokeColor()` or `engine.block.setDropShadowColor()` when Android exposes one for the property.
**Supported color properties:**
- `'fill/color/value'` - Fill color of a color fill
- `'stroke/color'` - Stroke or outline color
- `'dropShadow/color'` - Drop shadow color
- `'backgroundColor/color'` - Background color
Canvas clear color is a setting, not a regular block color property. On Android, color settings accept `RGBAColor` values only, so use `engine.editor.setSettingColor(keypath = "clearColor", value = Color.fromRGBA(...))` instead of the deprecated `'camera/clearColor'` property.
## sRGB Colors
sRGB is the default color space for screen display. Create an `RGBAColor` with `Color.fromRGBA()`, using components in the range 0.0 to 1.0. The `a` value controls alpha transparency.
```kotlin highlight-android-srgb-color
val srgbColor = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
engine.block.setColor(srgbFill, property = "fill/color/value", value = srgbColor)
```
sRGB colors are best for digital outputs such as PNG, JPEG, WebP, and screen-only editor previews.
## CMYK Colors
CMYK is the color space for print workflows. Create a `CMYKColor` with `Color.fromCMYK()`, using `c`, `m`, `y`, `k`, and `tint` values in the range 0.0 to 1.0. The `tint` value blends the converted screen preview toward white; it does not change alpha transparency.
```kotlin highlight-android-cmyk-color
val cmykColor = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 1F)
engine.block.setColor(cmykFill, property = "fill/color/value", value = cmykColor)
```
When CE.SDK renders CMYK colors on screen, it converts them to RGB using standard conversion formulas and applies tint to that converted preview.
> **Note:** In standard PDF export, direct CMYK colors are converted to RGB using the standard conversion. Tint is applied to the opaque RGB preview; it is not exported as alpha.
## Spot Colors
Spot colors are named colors used for specialized printing. Before using a spot color on a block, register the name with an RGB or CMYK approximation so CE.SDK can preview it on screen.
### Defining Spot Colors
Use `engine.editor.setSpotColor()` with either an `RGBAColor` or a `CMYKColor` approximation. RGB approximations are stored as opaque colors, so `getSpotColorRGB()` always returns alpha `1.0`; apply print tint with `Color.fromSpotColor(..., tint=_)`.
```kotlin highlight-android-define-spot-color
engine.editor.setSpotColor(
name = "MyBrand Red",
// RGB spot-color approximations are stored as opaque colors; apply tint on the
// SpotColor reference.
color = Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F),
)
engine.editor.setSpotColor(
name = "MyBrand Blue",
color = Color.fromCMYK(c = 1F, m = 0.7F, y = 0F, k = 0.1F),
)
val storedBrandRed = engine.editor.getSpotColorRGB("MyBrand Red")
val storedBrandBlue = engine.editor.getSpotColorCMYK("MyBrand Blue")
check(storedBrandRed == Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F))
check(storedBrandBlue == Color.fromCMYK(c = 1F, m = 0.7F, y = 0F, k = 0.1F))
```
### Applying Spot Colors
Reference a registered spot color by name with `Color.fromSpotColor()`. The `tint` blends the preview approximation toward white, and `externalReference` can store the source of the spot color.
```kotlin highlight-android-spot-color
val spotColor = Color.fromSpotColor(
name = "MyBrand Red",
tint = 1F,
externalReference = "brand-palette-v1",
)
engine.block.setColor(spotFill, property = "fill/color/value", value = spotColor)
```
When rendered on screen, the spot color uses its RGB or CMYK approximation. During PDF export, CE.SDK saves spot colors as [Separation Color Space](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.6.pdf#G9.1850648) output that preserves print information.
> **Note:** If a block references an undefined spot color, CE.SDK displays magenta (RGB: 1, 0, 1) as a fallback.
## Applying Stroke Colors
Strokes support all three color spaces. Check `engine.block.supportsStroke()` before changing arbitrary blocks, then enable the stroke, set its width, and apply a color with `engine.block.setStrokeColor()`.
```kotlin highlight-android-stroke-color
if (engine.block.supportsStroke(srgbBlock)) {
engine.block.setStrokeEnabled(srgbBlock, enabled = true)
engine.block.setStrokeWidth(srgbBlock, width = 4F)
engine.block.setStrokeColor(
srgbBlock,
color = Color.fromRGBA(r = 0.1F, g = 0.2F, b = 0.5F, a = 1F),
)
}
val cmykStroke = Color.fromCMYK(c = 0F, m = 0.5F, y = 0.6F, k = 0.2F, tint = 1F)
if (engine.block.supportsStroke(cmykBlock)) {
engine.block.setStrokeEnabled(cmykBlock, enabled = true)
engine.block.setStrokeWidth(cmykBlock, width = 4F)
engine.block.setStrokeColor(cmykBlock, color = cmykStroke)
}
if (engine.block.supportsStroke(spotBlock)) {
engine.block.setStrokeEnabled(spotBlock, enabled = true)
engine.block.setStrokeWidth(spotBlock, width = 4F)
engine.block.setStrokeColor(
spotBlock,
color = Color.fromSpotColor(name = "MyBrand Red", tint = 0.7F),
)
}
```
## Reading Color Values
Use `engine.block.getColor()` to retrieve generic color properties, and use `engine.block.getStrokeColor()` for stroke colors. The returned `Color` subtype tells you whether the value is an `RGBAColor`, `CMYKColor`, or `SpotColor`.
```kotlin highlight-android-get-color
val readSrgb = engine.block.getColor(srgbFill, property = "fill/color/value")
val readCmyk = engine.block.getColor(cmykFill, property = "fill/color/value")
val readSpot = engine.block.getColor(spotFill, property = "fill/color/value")
val readStroke = engine.block.getStrokeColor(cmykBlock)
for (color in listOf(readSrgb, readCmyk, readSpot, readStroke)) {
when (color) {
is RGBAColor -> Log.i(TAG, "sRGB: r=${color.r}, g=${color.g}, b=${color.b}, a=${color.a}")
is CMYKColor -> Log.i(
TAG,
"CMYK: c=${color.c}, m=${color.m}, y=${color.y}, k=${color.k}, tint=${color.tint}",
)
is SpotColor -> Log.i(
TAG,
"Spot: name=${color.name}, tint=${color.tint}, ref=${color.externalReference}",
)
}
}
```
For drop shadows, prefer the type-safe Android helpers `engine.block.setDropShadowColor()` and `engine.block.getDropShadowColor()` after confirming support with `engine.block.supportsDropShadow()`.
## Choosing the Right Color Space
| Color Space | Use Case | Output |
| --- | --- | --- |
| **sRGB** | Web, digital, screen display | PNG, JPEG, WebP |
| **CMYK** | Print workflows and print color values | Standard PDF export converts to RGB |
| **Spot Color** | Specialized printing and brand colors | PDF Separation Color Space |
## API Reference
| Method | Description |
| --- | --- |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an `RGBAColor`. Components range from 0.0 to 1.0. |
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` | Create a `CMYKColor`. Components and tint range from 0.0 to 1.0. |
| `Color.fromSpotColor(name=_, tint=_, externalReference=_)` | Create a `SpotColor` reference to a registered spot color name. |
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set a color property on a block or fill. |
| `engine.block.getColor(block=_, property="fill/color/value")` | Get the current color value from a property. |
| `engine.editor.setSettingColor(keypath="clearColor", value=_)` | Set the canvas clear color through the settings API. Pass an `RGBAColor`, for example `Color.fromRGBA(...)`. |
| `engine.editor.getSettingColor(keypath="clearColor")` | Get the current canvas clear color as an `RGBAColor`. |
| `engine.block.supportsStroke(block=_)` | Check whether a block supports stroke properties. |
| `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable a block stroke. |
| `engine.block.isStrokeEnabled(block=_)` | Check whether a block stroke is enabled. |
| `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke width before applying a stroke color. |
| `engine.block.getStrokeWidth(block=_)` | Get the current stroke width. |
| `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color with the type-safe Android stroke API. |
| `engine.block.getStrokeColor(block=_)` | Get the stroke color with the type-safe Android stroke API. |
| `engine.block.supportsDropShadow(block=_)` | Check whether a block supports drop shadow properties. |
| `engine.block.setDropShadowColor(block=_, color=_)` | Set the drop shadow color with the type-safe Android drop-shadow API. |
| `engine.block.getDropShadowColor(block=_)` | Get the drop shadow color with the type-safe Android drop-shadow API. |
| `engine.editor.setSpotColor(name=_, color=_)` | Define or update a spot color with an RGB or CMYK screen preview approximation. |
| `engine.editor.findAllSpotColors()` | List registered spot color names. |
| `engine.editor.getSpotColorRGB(name=_)` | Get the RGB approximation for a spot color. The returned alpha is always `1.0`. |
| `engine.editor.getSpotColorCMYK(name=_)` | Get the CMYK approximation for a spot color. |
| `engine.editor.removeSpotColor(name=_)` | Remove a spot color from the registry. |
| Type | Properties | Description |
| --- | --- | --- |
| `RGBAColor` | `r`, `g`, `b`, `a` (0.0-1.0) | sRGB color for screen display. Alpha controls transparency. |
| `CMYKColor` | `c`, `m`, `y`, `k`, `tint` (0.0-1.0) | CMYK color for print workflows. Tint blends the preview toward white. |
| `SpotColor` | `name`, `tint`, `externalReference` | Named color for specialized printing. Tint blends the preview toward white. |
## Next Steps
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply colors to design elements programmatically
- [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/) — Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control.
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Define and manage spot colors for specialized printing
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Color Conversion"
description: "Learn how to convert colors between color spaces in CE.SDK. Convert sRGB, CMYK, and spot colors programmatically for screen display or print workflows."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-conversion/ColorConversion.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.CMYKColor
import ly.img.engine.Color
import ly.img.engine.ColorSpace
import ly.img.engine.Engine
import ly.img.engine.RGBAColor
import ly.img.engine.SpotColor
suspend fun colorConversion(engine: Engine): ColorConversionResult = withContext(Dispatchers.Main) {
// Define a spot color with an RGB approximation for screen preview.
engine.editor.setSpotColor(
name = "Brand Red",
color = Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F),
)
val srgbColor = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
val cmykColor = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 1F)
val spotColor = Color.fromSpotColor(
name = "Brand Red",
tint = 1F,
externalReference = "",
)
val cmykToSrgb = engine.editor.convertColorToColorSpace(
color = cmykColor,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
val spotToSrgb = engine.editor.convertColorToColorSpace(
color = spotColor,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
println("CMYK converted to sRGB: $cmykToSrgb")
println("Spot color converted to sRGB: $spotToSrgb")
val srgbToCmyk = engine.editor.convertColorToColorSpace(
color = srgbColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
// Add a CMYK approximation before converting the spot color for print output.
engine.editor.setSpotColor(
name = "Brand Red",
color = Color.fromCMYK(c = 0F, m = 0.85F, y = 0.9F, k = 0.05F, tint = 1F),
)
val spotToCmyk = engine.editor.convertColorToColorSpace(
color = spotColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
println("sRGB converted to CMYK: $srgbToCmyk")
println("Spot color converted to CMYK: $spotToCmyk")
val detectedTypes = listOf(srgbColor, cmykColor, spotColor).map { color ->
when (color) {
is RGBAColor -> "sRGB"
is CMYKColor -> "CMYK"
is SpotColor -> "SpotColor"
}
}
val transparentSrgb = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 0.5F)
val transparentSrgbToCmyk = engine.editor.convertColorToColorSpace(
color = transparentSrgb,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
val tintedCmyk = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 0.5F)
val tintedCmykPreview = engine.editor.convertColorToColorSpace(
color = tintedCmyk,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
val tintedSpotColor = Color.fromSpotColor(
name = "Brand Red",
tint = 0.4F,
externalReference = "",
)
val tintedSpotPreview = engine.editor.convertColorToColorSpace(
color = tintedSpotColor,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
val tintedSpotToCmyk = engine.editor.convertColorToColorSpace(
color = tintedSpotColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
val colorFromDesign: Color = spotColor
val pickerPreviewColor = engine.editor.convertColorToColorSpace(
color = colorFromDesign,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
// Display pickerPreviewColor.r, pickerPreviewColor.g, pickerPreviewColor.b, and pickerPreviewColor.a.
val colorForExport: Color = srgbColor
val printColor = if (colorForExport is CMYKColor) {
colorForExport
} else {
engine.editor.convertColorToColorSpace(
color = colorForExport,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
}
ColorConversionResult(
cmykToSrgb = cmykToSrgb,
spotToSrgb = spotToSrgb,
srgbToCmyk = srgbToCmyk,
spotToCmyk = spotToCmyk,
transparentSrgbToCmyk = transparentSrgbToCmyk,
tintedCmykPreview = tintedCmykPreview,
tintedSpotPreview = tintedSpotPreview,
tintedSpotToCmyk = tintedSpotToCmyk,
detectedTypes = detectedTypes,
pickerPreviewColor = pickerPreviewColor,
printColor = printColor,
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-colors-conversion/ColorConversionResult.kt reference-only
import ly.img.engine.CMYKColor
import ly.img.engine.RGBAColor
data class ColorConversionResult(
val cmykToSrgb: RGBAColor,
val spotToSrgb: RGBAColor,
val srgbToCmyk: CMYKColor,
val spotToCmyk: CMYKColor,
val transparentSrgbToCmyk: CMYKColor,
val tintedCmykPreview: RGBAColor,
val tintedSpotPreview: RGBAColor,
val tintedSpotToCmyk: CMYKColor,
val detectedTypes: List,
val pickerPreviewColor: RGBAColor,
val printColor: CMYKColor,
)
```
Convert colors between sRGB, CMYK, and spot color spaces programmatically in CE.SDK.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-conversion)
CE.SDK supports sRGB, CMYK, and SpotColor values. Color conversion is a programmatic Engine API on Android, so use it when you build custom color interfaces, show print values, or prepare colors before export.
This guide covers converting colors to sRGB and CMYK, converting spot colors through their approximations, identifying Android color types, and checking how alpha and tint values are preserved.
## Supported Color Spaces
CE.SDK works with these Android color value types:
| Color Space | Android Type | Use Case |
| --- | --- | --- |
| **sRGB** | `RGBAColor` with `r`, `g`, `b`, `a` components from `0.0` to `1.0` | Screen display and previews |
| **CMYK** | `CMYKColor` with `c`, `m`, `y`, `k`, `tint` components from `0.0` to `1.0` | Print workflows |
| **SpotColor** | `SpotColor` with `name`, `tint`, and `externalReference` | Specialized printing with named inks |
Use `ColorSpace.SRGB` or `ColorSpace.CMYK` as conversion targets. A `SpotColor` can be converted by using the RGB or CMYK approximation registered for its name.
## Setting Up Colors
Before converting a spot color, register an approximation for the spot color name. Android uses the overloaded `setSpotColor(...)` API for both RGB and CMYK approximations.
```kotlin highlight-android-define-spot-color
// Define a spot color with an RGB approximation for screen preview.
engine.editor.setSpotColor(
name = "Brand Red",
color = Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F),
)
```
Create color values with the Android `Color` factory methods. The sample uses the same sRGB, CMYK, and spot color values throughout the conversion steps.
```kotlin highlight-android-create-colors
val srgbColor = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
val cmykColor = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 1F)
val spotColor = Color.fromSpotColor(
name = "Brand Red",
tint = 1F,
externalReference = "",
)
```
## Converting to sRGB
Use `engine.editor.convertColorToColorSpace(color, ColorSpace.SRGB)` when you need screen-display values. The returned value is a `Color`, so cast it to `RGBAColor` after converting to sRGB.
```kotlin highlight-android-convert-to-srgb
val cmykToSrgb = engine.editor.convertColorToColorSpace(
color = cmykColor,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
val spotToSrgb = engine.editor.convertColorToColorSpace(
color = spotColor,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
println("CMYK converted to sRGB: $cmykToSrgb")
println("Spot color converted to sRGB: $spotToSrgb")
```
CMYK colors convert to `RGBAColor` components. Spot colors use the registered RGB approximation for their spot color name. When the source color has a tint below `1.0`, the returned sRGB color keeps `a = 1.0` and blends its RGB components toward white.
## Converting to CMYK
Use `engine.editor.convertColorToColorSpace(color, ColorSpace.CMYK)` when a print workflow needs CMYK components. For spot colors, register a CMYK approximation before converting to CMYK.
```kotlin highlight-android-convert-to-cmyk
val srgbToCmyk = engine.editor.convertColorToColorSpace(
color = srgbColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
// Add a CMYK approximation before converting the spot color for print output.
engine.editor.setSpotColor(
name = "Brand Red",
color = Color.fromCMYK(c = 0F, m = 0.85F, y = 0.9F, k = 0.05F, tint = 1F),
)
val spotToCmyk = engine.editor.convertColorToColorSpace(
color = spotColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
println("sRGB converted to CMYK: $srgbToCmyk")
println("Spot color converted to CMYK: $spotToCmyk")
```
> **Note:** Color space conversions may not be perfectly reversible. Some sRGB colors cannot be represented exactly in CMYK because the color gamuts differ.
## Identifying Color Types
Android exposes color values as a sealed `Color` hierarchy. Use Kotlin type checks before reading type-specific properties.
```kotlin highlight-android-identify-types
val detectedTypes = listOf(srgbColor, cmykColor, spotColor).map { color ->
when (color) {
is RGBAColor -> "sRGB"
is CMYKColor -> "CMYK"
is SpotColor -> "SpotColor"
}
}
```
This lets you branch on `RGBAColor`, `CMYKColor`, and `SpotColor` before reading their component properties.
## Handling Tint and Alpha
On Android, alpha and tint are not simply copied between target fields. Non-black sRGB colors convert to CMYK with `tint = 1.0` even if the source has alpha, while pure black sRGB uses the source alpha as the CMYK tint. Tinted CMYK and spot colors convert to full-opacity sRGB previews, but their RGB components are blended toward white based on the tint. For example, pure CMYK red with `tint = 0.5` converts to a pink preview with `r = 1.0`, `g = 0.5`, `b = 0.5`, and `a = 1.0`.
| Source | Target | Transformation |
| --- | --- | --- |
| sRGB alpha (non-black) | CMYK | The converted CMYK color uses `tint = 1.0` |
| sRGB alpha (pure black) | CMYK | The converted CMYK color uses `tint = source alpha` |
| CMYK tint | sRGB | RGB components blend toward white and `a = 1.0` |
| SpotColor tint | sRGB | RGB components blend toward white and `a = 1.0` |
| SpotColor tint | CMYK | The converted CMYK color uses `tint = source tint` |
The sample below uses a non-black transparent sRGB color, so its converted CMYK color keeps `tint = 1.0`.
```kotlin highlight-android-handle-tint-alpha
val transparentSrgb = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 0.5F)
val transparentSrgbToCmyk = engine.editor.convertColorToColorSpace(
color = transparentSrgb,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
val tintedCmyk = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 0.5F)
val tintedCmykPreview = engine.editor.convertColorToColorSpace(
color = tintedCmyk,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
val tintedSpotColor = Color.fromSpotColor(
name = "Brand Red",
tint = 0.4F,
externalReference = "",
)
val tintedSpotPreview = engine.editor.convertColorToColorSpace(
color = tintedSpotColor,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
val tintedSpotToCmyk = engine.editor.convertColorToColorSpace(
color = tintedSpotColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
```
## Practical Use Cases
### Building a Color Picker
When a custom color picker needs screen preview values, convert the design color to sRGB and display the returned `RGBAColor` components.
```kotlin highlight-android-color-picker
val colorFromDesign: Color = spotColor
val pickerPreviewColor = engine.editor.convertColorToColorSpace(
color = colorFromDesign,
colorSpace = ColorSpace.SRGB,
) as RGBAColor
// Display pickerPreviewColor.r, pickerPreviewColor.g, pickerPreviewColor.b, and pickerPreviewColor.a.
```
### Export Preparation
Before a print-oriented export, check whether the color is already CMYK. Convert non-CMYK colors to `ColorSpace.CMYK` before showing or storing print values.
```kotlin highlight-android-print-preparation
val colorForExport: Color = srgbColor
val printColor = if (colorForExport is CMYKColor) {
colorForExport
} else {
engine.editor.convertColorToColorSpace(
color = colorForExport,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
}
```
## Troubleshooting
| Issue | Cause | Solution |
| --- | --- | --- |
| Spot color converts to an unexpected value | The spot color has no approximation for the target color space | Call `setSpotColor(...)` with an `RGBAColor` or `CMYKColor` before conversion |
| Colors differ after round-tripping | Color conversion is not always lossless | Avoid assuming that converting sRGB to CMYK and back returns the exact original value |
| Type-specific properties are unavailable | `convertColorToColorSpace(...)` returns the base `Color` type | Cast after converting, or check the type with Kotlin `is` checks |
## API Reference
| API | Purpose |
| --- | --- |
| `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Converts a color to `ColorSpace.SRGB` or `ColorSpace.CMYK` |
| `engine.editor.setSpotColor(name=_, color=Color.fromRGBA(r=_, g=_, b=_, a=_))` | Defines or updates an RGB approximation for a spot color |
| `engine.editor.setSpotColor(name=_, color=Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_))` | Defines or updates a CMYK approximation for a spot color |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Creates an sRGB color value |
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` | Creates a CMYK color value |
| `Color.fromSpotColor(name=_, tint=_, externalReference=_)` | Creates a spot color reference that uses the registered approximation for its name |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create a Color Palette"
description: "Build reusable color palettes to maintain consistency and streamline user choices."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Create a Color Palette](https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/)
---
```kotlin file=@cesdk_android_examples/editor-guides-configuration-color-palette/ColorPaletteEditorSolution.kt reference-only
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import ly.img.editor.Editor
import ly.img.editor.core.component.InspectorBar
import ly.img.editor.core.component.remember
import ly.img.editor.core.component.rememberFillStroke
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.engine.AssetColor
import ly.img.engine.AssetDefinition
import ly.img.engine.AssetPayload
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FindAssetsQuery
import androidx.compose.ui.graphics.Color as ComposeColor
private const val BRAND_COLOR_SOURCE_ID = "my-brand-colors"
private const val BRAND_CORAL_ID = "brand-coral"
private data class BrandColorAsset(
val definition: AssetDefinition,
val paletteColor: ComposeColor?,
)
private fun brandColorAssets() = listOf(
BrandColorAsset(
definition = AssetDefinition(
id = "brand-blue",
label = mapOf("en" to "Brand Blue"),
tags = mapOf("en" to listOf("brand", "blue", "primary")),
payload = AssetPayload(
color = AssetColor.RGB(r = 0.2F, g = 0.4F, b = 0.8F),
),
),
paletteColor = ComposeColor(red = 0.2F, green = 0.4F, blue = 0.8F),
),
BrandColorAsset(
definition = AssetDefinition(
id = BRAND_CORAL_ID,
label = mapOf("en" to "Brand Coral"),
tags = mapOf("en" to listOf("brand", "coral", "secondary")),
payload = AssetPayload(
color = AssetColor.RGB(r = 0.95F, g = 0.45F, b = 0.4F),
),
),
paletteColor = ComposeColor(red = 0.95F, green = 0.45F, blue = 0.4F),
),
BrandColorAsset(
definition = AssetDefinition(
id = "print-magenta",
label = mapOf("en" to "Print Magenta"),
tags = mapOf("en" to listOf("print", "magenta", "cmyk")),
payload = AssetPayload(
color = AssetColor.CMYK(c = 0F, m = 0.9F, y = 0.2F, k = 0F),
),
),
paletteColor = ComposeColor(red = 1F, green = 0.1F, blue = 0.8F),
),
BrandColorAsset(
definition = AssetDefinition(
id = "metallic-gold",
label = mapOf("en" to "Metallic Gold"),
tags = mapOf("en" to listOf("spot", "metallic", "gold")),
payload = AssetPayload(
color = AssetColor.SpotColor(
name = "Metallic Gold Ink",
externalReference = "Custom Inks",
representation = AssetColor.RGB(r = 0.85F, g = 0.65F, b = 0.13F),
),
),
),
paletteColor = ComposeColor(red = 0.85F, green = 0.65F, blue = 0.13F),
),
)
private fun brandPaletteColors() = brandColorAssets().mapNotNull { it.paletteColor }
private fun createBrandColorLibrary(engine: Engine) {
// Keep repeated guide launches idempotent inside the same editor process.
if (BRAND_COLOR_SOURCE_ID in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = BRAND_COLOR_SOURCE_ID)
}
engine.asset.addLocalSource(
sourceId = BRAND_COLOR_SOURCE_ID,
supportedMimeTypes = emptyList(),
)
brandColorAssets().forEach { color ->
engine.asset.addAsset(sourceId = BRAND_COLOR_SOURCE_ID, asset = color.definition)
}
engine.asset.assetSourceContentsChanged(sourceId = BRAND_COLOR_SOURCE_ID)
}
private fun removeBrandColor(
engine: Engine,
assetId: String = BRAND_CORAL_ID,
) {
engine.asset.removeAsset(sourceId = BRAND_COLOR_SOURCE_ID, assetId = assetId)
engine.asset.assetSourceContentsChanged(sourceId = BRAND_COLOR_SOURCE_ID)
}
data class ColorPaletteSmokeResult(
val paletteColorCount: Int,
val initialAssetIds: List,
val remainingAssetIds: List,
)
suspend fun colorPalette(engine: Engine): ColorPaletteSmokeResult {
createBrandColorLibrary(engine)
val initialAssetIds = engine.asset.findAssets(
sourceId = BRAND_COLOR_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10),
).assets.map { asset -> asset.id }
removeBrandColor(engine)
val remainingAssetIds = engine.asset.findAssets(
sourceId = BRAND_COLOR_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10),
).assets.map { asset -> asset.id }
return ColorPaletteSmokeResult(
paletteColorCount = brandPaletteColors().size,
initialAssetIds = initialAssetIds,
remainingAssetIds = remainingAssetIds,
)
}
// Add this composable to your NavHost
@Composable
fun ColorPaletteEditorSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember {
onCreate = {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
createBrandColorLibrary(editorContext.engine)
}
onLoaded = {
// Select the page so the Fill/Stroke inspector button is visible immediately.
editorContext.engine.block.findByType(DesignBlockType.Page)
.firstOrNull()
?.let { editorContext.engine.block.setSelected(it, selected = true) }
}
colorPalette = {
remember {
brandPaletteColors()
}
}
inspectorBar = {
InspectorBar.remember {
listBuilder = {
InspectorBar.ListBuilder.remember {
add { InspectorBar.Button.rememberFillStroke() }
}
}
}
}
}
},
onClose = onClose,
)
}
```
Create a brand color palette for the Android editor and keep the underlying
colors reusable as engine assets.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-color-palette)
Color libraries in Android are regular local asset sources that contain color assets. Use the local source when your app needs reusable color definitions for custom UI, asset queries, or workflows outside the built-in editor controls. Use `EditorConfiguration.colorPalette` to choose which screen-preview colors appear as swatches in the built-in Android editor color controls.
This guide keeps both parts connected with a small `BrandColorAsset` bridge: the asset definition stores the reusable engine color, and `paletteColor` stores the Compose `Color` preview required by Android swatches. Applying colors to blocks programmatically is covered in [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/).
## Defining Color Assets
Colors are added to an asset source as `AssetDefinition` objects. Define stable source and asset IDs, then give each asset an `id`, localized `label` and `tags`, and an `AssetPayload.color` value. The same local data also provides the Compose preview swatches that `EditorConfiguration.colorPalette` needs later.
```kotlin highlight-android-defining-color-assets
private const val BRAND_COLOR_SOURCE_ID = "my-brand-colors"
private const val BRAND_CORAL_ID = "brand-coral"
private data class BrandColorAsset(
val definition: AssetDefinition,
val paletteColor: ComposeColor?,
)
private fun brandColorAssets() = listOf(
BrandColorAsset(
definition = AssetDefinition(
id = "brand-blue",
label = mapOf("en" to "Brand Blue"),
tags = mapOf("en" to listOf("brand", "blue", "primary")),
payload = AssetPayload(
color = AssetColor.RGB(r = 0.2F, g = 0.4F, b = 0.8F),
),
),
paletteColor = ComposeColor(red = 0.2F, green = 0.4F, blue = 0.8F),
),
BrandColorAsset(
definition = AssetDefinition(
id = BRAND_CORAL_ID,
label = mapOf("en" to "Brand Coral"),
tags = mapOf("en" to listOf("brand", "coral", "secondary")),
payload = AssetPayload(
color = AssetColor.RGB(r = 0.95F, g = 0.45F, b = 0.4F),
),
),
paletteColor = ComposeColor(red = 0.95F, green = 0.45F, blue = 0.4F),
),
BrandColorAsset(
definition = AssetDefinition(
id = "print-magenta",
label = mapOf("en" to "Print Magenta"),
tags = mapOf("en" to listOf("print", "magenta", "cmyk")),
payload = AssetPayload(
color = AssetColor.CMYK(c = 0F, m = 0.9F, y = 0.2F, k = 0F),
),
),
paletteColor = ComposeColor(red = 1F, green = 0.1F, blue = 0.8F),
),
BrandColorAsset(
definition = AssetDefinition(
id = "metallic-gold",
label = mapOf("en" to "Metallic Gold"),
tags = mapOf("en" to listOf("spot", "metallic", "gold")),
payload = AssetPayload(
color = AssetColor.SpotColor(
name = "Metallic Gold Ink",
externalReference = "Custom Inks",
representation = AssetColor.RGB(r = 0.85F, g = 0.65F, b = 0.13F),
),
),
),
paletteColor = ComposeColor(red = 0.85F, green = 0.65F, blue = 0.13F),
),
)
private fun brandPaletteColors() = brandColorAssets().mapNotNull { it.paletteColor }
```
### sRGB Colors
sRGB colors use `AssetColor.RGB` with `r`, `g`, and `b` components from `0F` to `1F`. Use them for screen-based brand colors such as "Brand Blue" and "Brand Coral".
### CMYK Colors
CMYK colors use `AssetColor.CMYK` with `c`, `m`, `y`, and `k` components from `0F` to `1F`. The example keeps "Print Magenta" in the reusable asset source and supplies an sRGB preview color for the Android editor swatch.
### Spot Colors
Spot colors use `AssetColor.SpotColor` with a `name`, optional `externalReference`, and an RGB or CMYK `representation`. The representation gives the editor and custom UI a predictable preview color for named inks such as "Metallic Gold Ink".
## Creating a Color Library
Create a local asset source with `engine.asset.addLocalSource()`, add each color with `engine.asset.addAsset()`, then notify listeners that the source changed.
```kotlin highlight-android-add-library
private fun createBrandColorLibrary(engine: Engine) {
// Keep repeated guide launches idempotent inside the same editor process.
if (BRAND_COLOR_SOURCE_ID in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = BRAND_COLOR_SOURCE_ID)
}
engine.asset.addLocalSource(
sourceId = BRAND_COLOR_SOURCE_ID,
supportedMimeTypes = emptyList(),
)
brandColorAssets().forEach { color ->
engine.asset.addAsset(sourceId = BRAND_COLOR_SOURCE_ID, asset = color.definition)
}
engine.asset.assetSourceContentsChanged(sourceId = BRAND_COLOR_SOURCE_ID)
}
```
The source ID `my-brand-colors` identifies this library when you query, update, or remove its assets. You can create multiple local sources when your app needs separate brand, print, or campaign palettes.
## Registering the Library
Call the helper from `EditorConfiguration.onCreate` so the local source is registered during the editor and engine initialization block. When you provide a custom `onCreate`, create or load the scene there as well.
```kotlin highlight-android-register-library
onCreate = {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
createBrandColorLibrary(editorContext.engine)
}
```
## Configuring Palette Labels
Android color assets carry their display metadata on each `AssetDefinition`. Put user-facing names in `label` and searchable terms in `tags` so custom palette UIs and asset queries can present the colors consistently.
The built-in Android editor swatch row does not render asset-source section labels from translation keys. Use `EditorConfiguration.colorPalette` for the mobile editor controls, and use the asset labels when you build a grouped or searchable color picker in your own UI.
## Configuring the Editor Palette
Pass a list of Compose `Color` values through `EditorConfiguration.colorPalette`. The example derives those swatches from the same `BrandColorAsset` entries used for the asset source, and Android shows the swatches in the same order as the list.
```kotlin highlight-android-config-palette
colorPalette = {
remember {
brandPaletteColors()
}
}
```
The palette appears in Android editor controls that expose predefined color options, such as fill and stroke color controls. When you provide a custom list, it replaces the built-in default swatches; include any default colors you still want to offer. CMYK and Spot entries need an sRGB preview color because Android UI swatches are screen colors.
## Removing Colors
Remove an individual color from the local source with `engine.asset.removeAsset()`, then mark the source as changed.
```kotlin highlight-android-remove-color
private fun removeBrandColor(
engine: Engine,
assetId: String = BRAND_CORAL_ID,
) {
engine.asset.removeAsset(sourceId = BRAND_COLOR_SOURCE_ID, assetId = assetId)
engine.asset.assetSourceContentsChanged(sourceId = BRAND_COLOR_SOURCE_ID)
}
```
This removes the asset from future queries against the local color library. Update the `colorPalette` list as well if the color is also visible in the built-in editor swatches.
## Troubleshooting
### Colors Not Available from the Asset Source
- Verify the source was created with `engine.asset.addLocalSource()` before adding assets.
- Check that every color uses a unique asset ID.
- Call `engine.asset.assetSourceContentsChanged()` after mutating a local source.
### Palette Swatches Not Changing
- Confirm `EditorConfiguration.colorPalette` returns the colors you want the mobile editor to show.
- Keep the desired swatch order in the list, and include default colors manually if you still want them available.
- Reopen the color controls after changing the configuration so the editor reads the updated swatch list.
### Spot Color Preview Looks Wrong
- Provide a valid RGB or CMYK `representation` for every `AssetColor.SpotColor`.
- Keep the spot color `name` stable so exported designs can preserve the intended named ink.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.scene.create(sceneLayout=_)` | Create the scene that hosts the sample page |
| `engine.block.create(blockType=_)` | Create the page block used by the sample scene |
| `engine.block.setWidth(block=_, value=_, maintainCrop=_)` | Set the sample page width |
| `engine.block.setHeight(block=_, value=_, maintainCrop=_)` | Set the sample page height |
| `engine.block.appendChild(parent=_, child=_)` | Add the page block to the created scene |
| `engine.asset.findAllSources()` | List registered asset source IDs before replacing the sample source |
| `engine.asset.removeSource(sourceId=_)` | Remove an existing local source with the same ID |
| `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create a local asset source for color assets |
| `engine.asset.addAsset(sourceId=_, asset=_)` | Add a color asset to a local source |
| `engine.asset.removeAsset(sourceId=_, assetId=_)` | Remove a color asset from a local source |
| `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify listeners that a local source changed |
| `EditorConfiguration.colorPalette` | Provide the ordered swatch list for Android editor color controls, replacing the default palette when set |
| Type | Description |
|------|-------------|
| `AssetDefinition` | Stores a color asset ID, labels, tags, and payload |
| `AssetPayload(color=_)` | Carries the color data inside an asset definition |
| `AssetColor.RGB` | Defines an sRGB color with normalized RGB components |
| `AssetColor.CMYK` | Defines a CMYK color with normalized CMYK components |
| `AssetColor.SpotColor` | Defines a named spot color with a preview representation |
## Next Steps
- [Color Basics](https://img.ly/docs/cesdk/android/colors/basics-307115/) — Review the three color spaces CE.SDK supports and when to use each
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) — Apply colors to design elements programmatically
- [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/) — Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) — Define and manage spot colors for specialized printing
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Extract Dominant Colors"
description: "Read the most prominent colors from the rendered appearance of a block with the CE.SDK engine on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/extract-colors-d4c0a1/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Extract Dominant Colors](https://img.ly/docs/cesdk/android/colors/extract-colors-d4c0a1/)
---
Read the most prominent colors out of a block directly from the engine. The result reflects what is actually rendered on the canvas, so crops, color adjustments, and effects are all taken into account.
`engine.block.getDominantColors()` analyzes the rendered appearance of a block and returns its most prominent colors, sorted from most to least dominant. It works on any visible block — an image fill, a solid graphic, or a whole page — and runs entirely inside the engine.
## Extracting Dominant Colors
`getDominantColors()` is a `suspend` function, so call it from a coroutine. By default you get up to five colors, ordered by how much of the rendered block they cover.
```kotlin
val block = engine.block.findByType(DesignBlockType.Graphic).first()
val colors = engine.block.getDominantColors(block)
colors.forEach { color ->
val percent = (color.weight * 100).toInt()
println("rgb(${color.r}, ${color.g}, ${color.b}) covers $percent%")
}
```
The function suspends because the engine first resolves the block's final layout. If the block still has assets loading — for example an image fill whose source has not finished downloading — the call waits for them to settle instead of returning an empty result.
## Understanding the Result
Each entry is a `DominantColor` describing one extracted color:
| Property | Type | Description |
| -------- | ------- | ---------------------------------------------------------------- |
| `r` | `Float` | Red component in sRGB, normalized to `0`–`1`. |
| `g` | `Float` | Green component in sRGB, normalized to `0`–`1`. |
| `b` | `Float` | Blue component in sRGB, normalized to `0`–`1`. |
| `weight` | `Float` | Share of analyzed pixels this color represents, from `0` to `1`. |
Colors are sorted by `weight` in descending order, so the first entry is always the most dominant. The weights of a single call sum to `1`, which lets you treat them as percentages of the block's visible surface.
Because the components use the same `0`–`1` range that the rest of the CE.SDK color APIs expect, you can map a result into a Compose `Color`:
```kotlin
val swatches = colors.map { Color(it.r, it.g, it.b) }
```
## Configuring the Analysis
The optional `options` argument controls how many colors you get back and whether near-white pixels are considered.
```kotlin
val colors = engine.block.getDominantColors(
block,
options = DominantColorsOptions(count = 3, ignoreWhite = true),
)
```
| Option | Type | Default | Description |
| ------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `count` | `Int` | `5` | Number of colors to extract. The palette may contain fewer entries for images with little variation, and is empty when `count` is `0`. |
| `ignoreWhite` | `Boolean` | `false` | When `true`, near-white pixels are skipped. Useful for product shots on white backgrounds so the background does not dominate. |
## What the Colors Reflect
The analysis runs on the block's rendered output, not its source file. That means:
- **Crops, adjustments, and effects are included.** If you crop an image, lower its saturation, or apply a filter, the returned palette reflects the result, not the original asset.
- **Transparent pixels are skipped.** Fully or mostly transparent areas are excluded, so a logo on a transparent background returns the logo's colors rather than blending in the empty space.
- **The block must be visible.** It has to be attached to a scene and render visible content. Analyzing a detached or fully transparent block returns no colors.
## Troubleshooting
### The call never resolves
`getDominantColors()` waits for pending assets to finish loading. Make sure the block's resources are reachable — a broken image URI keeps the asset in a pending state.
### The result is empty
- Check that `count` is greater than `0`.
- Confirm the block is attached to a scene and renders visible content.
- A fully transparent block, or one whose only color is filtered out by `ignoreWhite`, returns no colors.
### The colors look different from the source image
This is expected. The palette is taken from the rendered block, so any crop, color adjustment, filter, or opacity applied to the block changes the result.
## API Reference
| Method | Description |
| -------------------------------------------- | ---------------------------------------------------------------------------- |
| `engine.block.getDominantColors(block, options)` | Returns the block's dominant colors, sorted by weight. A `suspend` function. |
| Type | Properties | Description |
| ----------------------- | ----------------------- | -------------------------------------------- |
| `DominantColor` | `r`, `g`, `b`, `weight` | A single extracted color and its prominence. |
| `DominantColorsOptions` | `count`, `ignoreWhite` | Options controlling the analysis. |
## Next Steps
- [Color Basics](https://img.ly/docs/cesdk/android/colors/basics-307115/) — Review the three color spaces CE.SDK supports and when to use each
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) — Apply colors to design elements programmatically
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "For Print"
description: "Use print-ready color models and settings for professional-quality, production-ready exports."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/for-print-59bc05/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [For Print](https://img.ly/docs/cesdk/android/colors/for-print-59bc05/)
---
---
## Related Pages
- [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/) - Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control.
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Define, apply, and manage spot colors in CE.SDK for professional print workflows with exact color matching through premixed inks.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "CMYK Colors"
description: "Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [For Print](https://img.ly/docs/cesdk/android/colors/for-print-59bc05/) > [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-for-print-cmyk/CMYKColors.kt reference-only
import ly.img.engine.CMYKColor
import ly.img.engine.Color
import ly.img.engine.ColorSpace
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GradientColorStop
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
data class CMYKColors(
val fillColor: CMYKColor,
val tintedColor: CMYKColor,
val strokeColor: CMYKColor,
val shadowColor: CMYKColor,
val readColor: CMYKColor,
val convertedCmyk: CMYKColor,
val convertedSrgb: RGBAColor,
val gradientStops: List,
)
fun cmykColors(engine: Engine): CMYKColors {
// CMYK components (c, m, y, k) and tint all range from 0F to 1F.
val cmykCyan = Color.fromCMYK(c = 1F, m = 0F, y = 0F, k = 0F, tint = 1F)
val cmykMagenta = Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F, tint = 1F)
val cmykYellow = Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F, tint = 1F)
val cmykBlack = Color.fromCMYK(c = 0F, m = 0F, y = 0F, k = 1F, tint = 1F)
val fillBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(fillBlock, shape = engine.block.createShape(ShapeType.Rect))
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(fillBlock, fill = fill)
// Color fill values currently use the generic color property key.
engine.block.setColor(fill, property = "fill/color/value", value = cmykCyan)
// Tint scales the color intensity without changing the CMYK components.
val cmykHalfMagenta = Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F, tint = 0.5F)
val tintedBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(tintedBlock, shape = engine.block.createShape(ShapeType.Rect))
val tintedFill = engine.block.createFill(FillType.Color)
engine.block.setFill(tintedBlock, fill = tintedFill)
engine.block.setColor(tintedFill, property = "fill/color/value", value = cmykHalfMagenta)
val strokeBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(strokeBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setStrokeEnabled(strokeBlock, enabled = true)
engine.block.setStrokeWidth(strokeBlock, width = 8F)
val cmykStrokeColor = Color.fromCMYK(c = 0.8F, m = 0.2F, y = 0F, k = 0.1F, tint = 1F)
engine.block.setStrokeColor(strokeBlock, color = cmykStrokeColor)
val shadowBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(shadowBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setDropShadowEnabled(shadowBlock, enabled = true)
engine.block.setDropShadowOffsetX(shadowBlock, offsetX = 8F)
engine.block.setDropShadowOffsetY(shadowBlock, offsetY = 8F)
engine.block.setDropShadowBlurRadiusX(shadowBlock, blurRadiusX = 12F)
engine.block.setDropShadowBlurRadiusY(shadowBlock, blurRadiusY = 12F)
val cmykShadowColor = cmykBlack
engine.block.setDropShadowColor(shadowBlock, color = cmykShadowColor)
val readBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(readBlock, shape = engine.block.createShape(ShapeType.Rect))
val readFill = engine.block.createFill(FillType.Color)
engine.block.setFill(readBlock, fill = readFill)
val cmykOrange = Color.fromCMYK(c = 0F, m = 0.5F, y = 1F, k = 0F, tint = 1F)
engine.block.setColor(readFill, property = "fill/color/value", value = cmykOrange)
val retrievedColor = engine.block.getColor(readFill, property = "fill/color/value")
val retrievedCmyk = when (retrievedColor) {
is CMYKColor -> retrievedColor
else -> error("Expected a CMYK color, got $retrievedColor")
}
println(
"CMYK Color - C: ${retrievedCmyk.c}, M: ${retrievedCmyk.m}, " +
"Y: ${retrievedCmyk.y}, K: ${retrievedCmyk.k}, Tint: ${retrievedCmyk.tint}",
)
val rgbBlue = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
val convertedCmyk = engine.editor.convertColorToColorSpace(
color = rgbBlue,
colorSpace = ColorSpace.CMYK,
)
val cmykGreen = Color.fromCMYK(c = 0.7F, m = 0F, y = 1F, k = 0.2F, tint = 1F)
val convertedSrgb = engine.editor.convertColorToColorSpace(
color = cmykGreen,
colorSpace = ColorSpace.SRGB,
)
val gradientBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(gradientBlock, shape = engine.block.createShape(ShapeType.Rect))
val gradientFill = engine.block.createFill(FillType.LinearGradient)
engine.block.setFill(gradientBlock, fill = gradientFill)
val gradientStops = listOf(
GradientColorStop(stop = 0F, color = cmykCyan),
GradientColorStop(stop = 0.5F, color = cmykMagenta),
GradientColorStop(stop = 1F, color = cmykYellow),
)
// Gradient fills currently expose their stops through the generic property key.
engine.block.setGradientColorStops(
gradientFill,
property = "fill/gradient/colors",
colorStops = gradientStops,
)
return CMYKColors(
fillColor = engine.block.getColor(fill, property = "fill/color/value") as CMYKColor,
tintedColor = engine.block.getColor(tintedFill, property = "fill/color/value") as CMYKColor,
strokeColor = engine.block.getStrokeColor(strokeBlock) as CMYKColor,
shadowColor = engine.block.getDropShadowColor(shadowBlock) as CMYKColor,
readColor = retrievedCmyk,
convertedCmyk = convertedCmyk as CMYKColor,
convertedSrgb = convertedSrgb as RGBAColor,
gradientStops = engine.block.getGradientColorStops(
gradientFill,
property = "fill/gradient/colors",
),
)
}
```
Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-for-print-cmyk)
CMYK (Cyan, Magenta, Yellow, Key/Black) is the standard color model for print production. Unlike sRGB, which is additive and designed for screens, CMYK uses subtractive color mixing to represent how inks combine on paper. Android represents CMYK values with `CMYKColor`, so the same color APIs can work with sRGB, CMYK, and spot colors.
This guide covers how to create CMYK colors, apply them to fills, strokes, and drop shadows, use the `tint` value, read colors back, convert between color spaces, and use CMYK in gradients.
## Understanding CMYK Colors
### When to Use CMYK
Use CMYK colors when preparing designs for print workflows or when a print service provider gives you CMYK values. Screen previews convert CMYK to sRGB for display, so use proofing from your production print workflow when exact output appearance matters.
A CMYK color in CE.SDK has five properties:
- `c` (Cyan): `0F` to `1F`
- `m` (Magenta): `0F` to `1F`
- `y` (Yellow): `0F` to `1F`
- `k` (Key/Black): `0F` to `1F`
- `tint`: `0F` to `1F` (controls overall color intensity)
## Creating CMYK Colors
Create a CMYK color with `Color.fromCMYK()`. Every component ranges from `0F` to `1F`.
```kotlin highlight-android-create-cmyk
// CMYK components (c, m, y, k) and tint all range from 0F to 1F.
val cmykCyan = Color.fromCMYK(c = 1F, m = 0F, y = 0F, k = 0F, tint = 1F)
val cmykMagenta = Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F, tint = 1F)
val cmykYellow = Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F, tint = 1F)
val cmykBlack = Color.fromCMYK(c = 0F, m = 0F, y = 0F, k = 1F, tint = 1F)
```
## Applying CMYK Colors to Fills
Apply a CMYK color to a color fill with `engine.block.setColor()` on the fill's `"fill/color/value"` property. Create the fill with `FillType.Color`, assign it to a block, then set the CMYK value.
```kotlin highlight-android-apply-fill
val fillBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(fillBlock, shape = engine.block.createShape(ShapeType.Rect))
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(fillBlock, fill = fill)
// Color fill values currently use the generic color property key.
engine.block.setColor(fill, property = "fill/color/value", value = cmykCyan)
```
The same `setColor()` method accepts `RGBAColor`, `CMYKColor`, and `SpotColor` values, so you do not need a separate code path for each color space.
## Using the Tint Property
The `tint` value scales the color's intensity without changing its CMYK components. A tint of `1F` applies the full color; `0.5F` scales it down.
```kotlin highlight-android-tint
// Tint scales the color intensity without changing the CMYK components.
val cmykHalfMagenta = Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F, tint = 0.5F)
val tintedBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(tintedBlock, shape = engine.block.createShape(ShapeType.Rect))
val tintedFill = engine.block.createFill(FillType.Color)
engine.block.setFill(tintedBlock, fill = tintedFill)
engine.block.setColor(tintedFill, property = "fill/color/value", value = cmykHalfMagenta)
```
> **Note:** On Android, screen conversion applies tint by blending the CMYK color toward
> white and returns an opaque sRGB preview. Android PDF export currently writes
> DeviceRGB output because `ExportOptions` does not expose
> `exportPdfWithDeviceCMYK`.
## Applying CMYK to Strokes
Enable the stroke and set its width, then assign a CMYK color with `engine.block.setStrokeColor()`.
```kotlin highlight-android-stroke
val strokeBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(strokeBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setStrokeEnabled(strokeBlock, enabled = true)
engine.block.setStrokeWidth(strokeBlock, width = 8F)
val cmykStrokeColor = Color.fromCMYK(c = 0.8F, m = 0.2F, y = 0F, k = 0.1F, tint = 1F)
engine.block.setStrokeColor(strokeBlock, color = cmykStrokeColor)
```
## Applying CMYK to Drop Shadows
Enable the drop shadow, configure its offset and blur radius, then assign a CMYK color with `engine.block.setDropShadowColor()`.
```kotlin highlight-android-shadow
val shadowBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(shadowBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setDropShadowEnabled(shadowBlock, enabled = true)
engine.block.setDropShadowOffsetX(shadowBlock, offsetX = 8F)
engine.block.setDropShadowOffsetY(shadowBlock, offsetY = 8F)
engine.block.setDropShadowBlurRadiusX(shadowBlock, blurRadiusX = 12F)
engine.block.setDropShadowBlurRadiusY(shadowBlock, blurRadiusY = 12F)
val cmykShadowColor = cmykBlack
engine.block.setDropShadowColor(shadowBlock, color = cmykShadowColor)
```
## Reading CMYK Colors
`engine.block.getColor()` returns the shared `Color` type. Use Kotlin type checking to handle `CMYKColor` values and read their components.
```kotlin highlight-android-read
val readBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(readBlock, shape = engine.block.createShape(ShapeType.Rect))
val readFill = engine.block.createFill(FillType.Color)
engine.block.setFill(readBlock, fill = readFill)
val cmykOrange = Color.fromCMYK(c = 0F, m = 0.5F, y = 1F, k = 0F, tint = 1F)
engine.block.setColor(readFill, property = "fill/color/value", value = cmykOrange)
val retrievedColor = engine.block.getColor(readFill, property = "fill/color/value")
val retrievedCmyk = when (retrievedColor) {
is CMYKColor -> retrievedColor
else -> error("Expected a CMYK color, got $retrievedColor")
}
println(
"CMYK Color - C: ${retrievedCmyk.c}, M: ${retrievedCmyk.m}, " +
"Y: ${retrievedCmyk.y}, K: ${retrievedCmyk.k}, Tint: ${retrievedCmyk.tint}",
)
```
## Converting Between Color Spaces
Use `engine.editor.convertColorToColorSpace()` with `ColorSpace.CMYK` or `ColorSpace.SRGB` to convert between color spaces.
```kotlin highlight-android-convert
val rgbBlue = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
val convertedCmyk = engine.editor.convertColorToColorSpace(
color = rgbBlue,
colorSpace = ColorSpace.CMYK,
)
val cmykGreen = Color.fromCMYK(c = 0.7F, m = 0F, y = 1F, k = 0.2F, tint = 1F)
val convertedSrgb = engine.editor.convertColorToColorSpace(
color = cmykGreen,
colorSpace = ColorSpace.SRGB,
)
```
Conversions may not be perfectly reversible because sRGB and CMYK have different gamuts. Some sRGB colors cannot be represented exactly in CMYK and vice versa.
## Using CMYK in Gradients
CMYK colors work in gradient color stops. Create a linear gradient fill and pass `GradientColorStop` values whose `color` is a CMYK color.
```kotlin highlight-android-gradient
val gradientBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(gradientBlock, shape = engine.block.createShape(ShapeType.Rect))
val gradientFill = engine.block.createFill(FillType.LinearGradient)
engine.block.setFill(gradientBlock, fill = gradientFill)
val gradientStops = listOf(
GradientColorStop(stop = 0F, color = cmykCyan),
GradientColorStop(stop = 0.5F, color = cmykMagenta),
GradientColorStop(stop = 1F, color = cmykYellow),
)
// Gradient fills currently expose their stops through the generic property key.
engine.block.setGradientColorStops(
gradientFill,
property = "fill/gradient/colors",
colorStops = gradientStops,
)
```
## Troubleshooting
### Colors Look Different on Screen vs. Print
Screen previews convert CMYK to sRGB using a standard conversion. Android PDF export currently writes DeviceRGB output, so use calibrated proofing from your print workflow when exact print appearance matters.
### Tint Not Having the Expected Effect
The `tint` value must be between `0F` and `1F`. On Android, values below `1F` do not lower alpha; the sRGB preview blends the color toward white and stays opaque.
## API Reference
| Method | Description |
|--------|-------------|
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` | Create a CMYK color with normalized components and tint. |
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set a color property on a fill. Accepts any `Color` type. |
| `engine.block.getColor(block=_, property="fill/color/value")` | Get the current color value from a property. Returns `Color`. |
| `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Convert a color between `ColorSpace.SRGB` and `ColorSpace.CMYK`. |
| `engine.block.createFill(fillType=_)` | Create a fill. Use `FillType.Color` for solid fills or `FillType.LinearGradient`, `FillType.RadialGradient`, or `FillType.ConicalGradient` for gradients. |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill to a block. |
| `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color on a block. |
| `engine.block.setDropShadowColor(block=_, color=_)` | Set the drop shadow color on a block. |
| `engine.block.setGradientColorStops(block=_, property="fill/gradient/colors", colorStops=_)` | Set color stops on a gradient fill. |
| Type | Description |
|------|-------------|
| `CMYKColor` | CMYK color for print workflows. Components and tint range from `0F` to `1F`. |
| `ColorSpace.CMYK` / `ColorSpace.SRGB` | Target color space for `convertColorToColorSpace()`. |
| `GradientColorStop` | Gradient stop with a `color: Color` and a `stop: Float` position. |
## Next Steps
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Work with named spot colors for brand consistency and specialized printing
- [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/) - Convert colors between sRGB, CMYK, and spot color spaces
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply colors to design elements programmatically
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Spot Colors"
description: "Define, apply, and manage spot colors in CE.SDK for professional print workflows with exact color matching through premixed inks."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [For Print](https://img.ly/docs/cesdk/android/colors/for-print-59bc05/) > [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/)
---
```kotlin file=@cesdk_android_examples/engine-guides-spot-colors/SpotColors.kt reference-only
import ly.img.engine.Color
import ly.img.engine.CutoutType
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SpotColor
fun spotColors(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val primaryBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(primaryBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(primaryBlock, value = 50F)
engine.block.setPositionY(primaryBlock, value = 50F)
engine.block.setWidth(primaryBlock, value = 150F)
engine.block.setHeight(primaryBlock, value = 150F)
engine.block.appendChild(parent = page, child = primaryBlock)
val primaryFill = engine.block.createFill(FillType.Color)
engine.block.setFill(primaryBlock, fill = primaryFill)
val tintedBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(tintedBlock, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setPositionX(tintedBlock, value = 240F)
engine.block.setPositionY(tintedBlock, value = 50F)
engine.block.setWidth(tintedBlock, value = 150F)
engine.block.setHeight(tintedBlock, value = 150F)
engine.block.appendChild(parent = page, child = tintedBlock)
val tintedFill = engine.block.createFill(FillType.Color)
engine.block.setFill(tintedBlock, fill = tintedFill)
val strokeBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(strokeBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(strokeBlock, value = 430F)
engine.block.setPositionY(strokeBlock, value = 50F)
engine.block.setWidth(strokeBlock, value = 150F)
engine.block.setHeight(strokeBlock, value = 150F)
engine.block.appendChild(parent = page, child = strokeBlock)
val strokeFill = engine.block.createFill(FillType.Color)
engine.block.setFill(strokeBlock, fill = strokeFill)
engine.block.setColor(
strokeFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 1F, g = 1F, b = 1F),
)
val shadowBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(shadowBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(shadowBlock, value = 50F)
engine.block.setPositionY(shadowBlock, value = 250F)
engine.block.setWidth(shadowBlock, value = 150F)
engine.block.setHeight(shadowBlock, value = 150F)
engine.block.appendChild(parent = page, child = shadowBlock)
val shadowFill = engine.block.createFill(FillType.Color)
engine.block.setFill(shadowBlock, fill = shadowFill)
engine.block.setColor(
shadowFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.95F),
)
val temporaryBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(temporaryBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(temporaryBlock, value = 240F)
engine.block.setPositionY(temporaryBlock, value = 250F)
engine.block.setWidth(temporaryBlock, value = 150F)
engine.block.setHeight(temporaryBlock, value = 150F)
engine.block.appendChild(parent = page, child = temporaryBlock)
val temporaryFill = engine.block.createFill(FillType.Color)
engine.block.setFill(temporaryBlock, fill = temporaryFill)
engine.editor.setSpotColor(
name = "Brand-Primary",
Color.fromRGBA(r = 0.8F, g = 0.1F, b = 0.2F, a = 1F),
)
engine.editor.setSpotColor(
name = "Brand-Accent",
Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F),
)
engine.editor.setSpotColor(
name = "Brand-Primary",
Color.fromCMYK(c = 0.05F, m = 0.95F, y = 0.85F, k = 0F),
)
engine.editor.setSpotColor(
name = "Brand-Accent",
Color.fromCMYK(c = 0.75F, m = 0.5F, y = 0F, k = 0F),
)
val brandPrimary = Color.fromSpotColor(
name = "Brand-Primary",
tint = 1F,
externalReference = "BrandBook",
)
engine.block.setColor(
primaryFill,
property = "fill/color/value",
value = brandPrimary,
)
val brandPrimaryHalfTint = Color.fromSpotColor(
name = "Brand-Primary",
tint = 0.5F,
externalReference = "BrandBook",
)
engine.block.setColor(
tintedFill,
property = "fill/color/value",
value = brandPrimaryHalfTint,
)
engine.block.setStrokeEnabled(strokeBlock, enabled = true)
engine.block.setStrokeWidth(strokeBlock, width = 8F)
engine.block.setStrokeColor(
strokeBlock,
color = Color.fromSpotColor(name = "Brand-Primary"),
)
engine.block.setDropShadowEnabled(shadowBlock, enabled = true)
engine.block.setDropShadowOffsetX(shadowBlock, offsetX = 10F)
engine.block.setDropShadowOffsetY(shadowBlock, offsetY = 10F)
engine.block.setDropShadowBlurRadiusX(shadowBlock, blurRadiusX = 15F)
engine.block.setDropShadowBlurRadiusY(shadowBlock, blurRadiusY = 15F)
engine.block.setDropShadowColor(
shadowBlock,
color = Color.fromSpotColor(name = "Brand-Accent", tint = 0.8F),
)
val spotColorNames = engine.editor.findAllSpotColors()
val brandPrimaryRgb = engine.editor.getSpotColorRGB(name = "Brand-Primary")
val brandPrimaryCmyk = engine.editor.getSpotColorCMYK(name = "Brand-Primary")
check(spotColorNames.containsAll(listOf("Brand-Primary", "Brand-Accent")))
check(brandPrimaryRgb == Color.fromRGBA(r = 0.8F, g = 0.1F, b = 0.2F, a = 1F))
check(brandPrimaryCmyk == Color.fromCMYK(c = 0.05F, m = 0.95F, y = 0.85F, k = 0F))
val retrievedColor = engine.block.getColor(
primaryFill,
property = "fill/color/value",
)
val retrievedSpotColor = when (retrievedColor) {
is SpotColor -> retrievedColor
else -> error("Expected a spot color, got $retrievedColor")
}
check(retrievedSpotColor.name == "Brand-Primary")
check(retrievedSpotColor.tint == 1F)
engine.editor.setSpotColor(
name = "Brand-Accent",
Color.fromRGBA(r = 0.3F, g = 0.5F, b = 0.9F, a = 1F),
)
engine.editor.setSpotColor(
name = "Temporary-Color",
Color.fromRGBA(r = 0.5F, g = 0.8F, b = 0.3F, a = 1F),
)
engine.block.setColor(
temporaryFill,
property = "fill/color/value",
value = Color.fromSpotColor(name = "Temporary-Color"),
)
engine.editor.removeSpotColor(name = "Temporary-Color")
check("Temporary-Color" !in engine.editor.findAllSpotColors())
engine.editor.setSpotColor(
name = "DieLine",
Color.fromRGBA(r = 1F, g = 0F, b = 1F, a = 1F),
)
engine.editor.setSpotColor(
name = "DieLine",
Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F),
)
engine.block.setSpotColorForCutoutType(type = CutoutType.SOLID, name = "DieLine")
val cutoutSpotColor = engine.block.getSpotColorForCutoutType(type = CutoutType.SOLID)
check(cutoutSpotColor == "DieLine")
}
```
Define, apply, and manage spot colors in CE.SDK for professional print workflows with exact color matching through premixed inks.

> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-spot-colors)
Spot colors are named colors reproduced using premixed inks in print production, providing exact color matching that CMYK process colors cannot guarantee. CE.SDK maintains a registry of spot color definitions on the editor instance, where each entry has a name and screen approximations (RGB and/or CMYK) for display. The premixed ink is selected at print time based on the spot color name.
This guide covers how to define spot colors with RGB and CMYK approximations, apply them to fills, strokes, and shadows, control intensity with tints, query and update definitions, and assign spot colors to cutout types for die-cutting operations.
## Understanding Spot Colors
Spot colors differ from CMYK process colors in several important ways:
- **Exact color matching** - Premixed inks guarantee consistent color reproduction across print runs.
- **Brand consistency** - Essential for logos and corporate brand colors.
- **Specialty effects** - Enable metallic, fluorescent, and other specialty inks.
- **Color gamut** - Some colors cannot be reproduced with CMYK process inks.
A spot color in CE.SDK has four components:
- `name` - The identifier used in the print output, for example `"Brand-Red-485"`.
- Approximations - RGB and/or CMYK values used for on-screen display.
- `tint` - A value from `0F` to `1F` that controls color intensity.
- `externalReference` - Optional metadata recording the originating color system. Android stores this as a nullable string and does not use it for on-screen rendering.
Use `Color.fromSpotColor(name, tint, externalReference)` to create a spot color reference for block color properties. `tint` defaults to `1F`, and `externalReference` defaults to `null`.
## Define Spot Colors
### RGB Approximation
Register spot colors with `engine.editor.setSpotColor(name, Color.fromRGBA(...))`. This creates a new spot color if the name doesn't exist, or updates the RGB approximation if it does. RGB components range from `0F` to `1F`; the alpha value is ignored for spot color definitions.
```kotlin highlight-android-define-rgb
engine.editor.setSpotColor(
name = "Brand-Primary",
Color.fromRGBA(r = 0.8F, g = 0.1F, b = 0.2F, a = 1F),
)
engine.editor.setSpotColor(
name = "Brand-Accent",
Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F),
)
```
RGB approximations control how the spot color is rendered on screen during editing.
### CMYK Approximation
Add CMYK approximations with the `Color.fromCMYK(...)` overload to provide print-accurate previews alongside the RGB display. Calling either overload with an existing name updates that approximation without affecting the other.
```kotlin highlight-android-define-cmyk
engine.editor.setSpotColor(
name = "Brand-Primary",
Color.fromCMYK(c = 0.05F, m = 0.95F, y = 0.85F, k = 0F),
)
engine.editor.setSpotColor(
name = "Brand-Accent",
Color.fromCMYK(c = 0.75F, m = 0.5F, y = 0F, k = 0F),
)
```
Provide both approximations whenever possible: RGB drives the on-screen display, while CMYK enables accurate print preview.
## Apply Spot Colors to Design Elements
Apply a spot color to any color property with `engine.block.setColor()` and a `Color.fromSpotColor(...)` value. The spot color must be defined first; undefined names fall back to magenta on screen.
```kotlin highlight-android-apply-spot-fill
val brandPrimary = Color.fromSpotColor(
name = "Brand-Primary",
tint = 1F,
externalReference = "BrandBook",
)
engine.block.setColor(
primaryFill,
property = "fill/color/value",
value = brandPrimary,
)
```
The `externalReference` argument is optional metadata describing where the spot color comes from, such as an in-house color book or an internal style identifier. It is preserved by the engine alongside the name but doesn't affect on-screen rendering.
`Color` is a single interface across all color spaces, so the same `setColor()` method works for RGBA, CMYK, and spot color values. Color fill blocks expose their color through the `"fill/color/value"` property.
### Using Tints
`tint` scales the spot color's intensity without changing the underlying name in the print output. A tint of `0.5F` produces a 50 percent strength variation; the name in the exported PDF stays the same.
```kotlin highlight-android-tint
val brandPrimaryHalfTint = Color.fromSpotColor(
name = "Brand-Primary",
tint = 0.5F,
externalReference = "BrandBook",
)
engine.block.setColor(
tintedFill,
property = "fill/color/value",
value = brandPrimaryHalfTint,
)
```
Use tints for lighter variations in a design system rather than defining a separate spot color per shade.
### Strokes and Shadows
Spot colors work with any color property, including strokes and drop shadows. Enable the feature, configure widths and offsets as usual, then assign the spot color.
```kotlin highlight-android-stroke-shadow
engine.block.setStrokeEnabled(strokeBlock, enabled = true)
engine.block.setStrokeWidth(strokeBlock, width = 8F)
engine.block.setStrokeColor(
strokeBlock,
color = Color.fromSpotColor(name = "Brand-Primary"),
)
engine.block.setDropShadowEnabled(shadowBlock, enabled = true)
engine.block.setDropShadowOffsetX(shadowBlock, offsetX = 10F)
engine.block.setDropShadowOffsetY(shadowBlock, offsetY = 10F)
engine.block.setDropShadowBlurRadiusX(shadowBlock, blurRadiusX = 15F)
engine.block.setDropShadowBlurRadiusY(shadowBlock, blurRadiusY = 15F)
engine.block.setDropShadowColor(
shadowBlock,
color = Color.fromSpotColor(name = "Brand-Accent", tint = 0.8F),
)
```
For PDF output, use `setStrokeOverprint()` or `setFillOverprint()` when stacked spot-color strokes or fills should print over underlying artwork. The PDF writer only honors these flags when the stroke or fill uses a spot color. For process colors the flag is a silent no-op and on-screen rendering ignores it.
## Query Spot Color Definitions
### List and Inspect Approximations
Retrieve every defined spot color with `engine.editor.findAllSpotColors()`. Query individual approximations with `engine.editor.getSpotColorRGB()` and `engine.editor.getSpotColorCMYK()`.
```kotlin highlight-android-query-spot
val spotColorNames = engine.editor.findAllSpotColors()
val brandPrimaryRgb = engine.editor.getSpotColorRGB(name = "Brand-Primary")
val brandPrimaryCmyk = engine.editor.getSpotColorCMYK(name = "Brand-Primary")
check(spotColorNames.containsAll(listOf("Brand-Primary", "Brand-Accent")))
check(brandPrimaryRgb == Color.fromRGBA(r = 0.8F, g = 0.1F, b = 0.2F, a = 1F))
check(brandPrimaryCmyk == Color.fromCMYK(c = 0.05F, m = 0.95F, y = 0.85F, k = 0F))
```
Querying an undefined spot color returns the magenta default for the requested representation. The same magenta value is also returned for a name that is defined but only has the other representation set, so check `findAllSpotColors()` to reliably determine whether a name is defined.
### Read Colors from Blocks
`engine.block.getColor()` returns a `Color` value. Reusing the `primaryFill` from the Apply Spot Colors to Design Elements step, check whether the value is a `SpotColor` before reading its name and tint.
```kotlin highlight-android-read-color
val retrievedColor = engine.block.getColor(
primaryFill,
property = "fill/color/value",
)
val retrievedSpotColor = when (retrievedColor) {
is SpotColor -> retrievedColor
else -> error("Expected a spot color, got $retrievedColor")
}
check(retrievedSpotColor.name == "Brand-Primary")
check(retrievedSpotColor.tint == 1F)
```
## Update and Remove Spot Colors
### Update Approximations
Update an existing spot color by calling `setSpotColor()` again with the same name. This changes how the color appears on screen without changing the name written to the print output, and existing blocks automatically reflect the new approximation.
```kotlin highlight-android-update-spot
engine.editor.setSpotColor(
name = "Brand-Accent",
Color.fromRGBA(r = 0.3F, g = 0.5F, b = 0.9F, a = 1F),
)
```
### Remove Spot Colors
Remove a spot color with `engine.editor.removeSpotColor()`. Removing an undefined name is a no-op.
```kotlin highlight-android-remove-spot
engine.editor.setSpotColor(
name = "Temporary-Color",
Color.fromRGBA(r = 0.5F, g = 0.8F, b = 0.3F, a = 1F),
)
engine.block.setColor(
temporaryFill,
property = "fill/color/value",
value = Color.fromSpotColor(name = "Temporary-Color"),
)
engine.editor.removeSpotColor(name = "Temporary-Color")
check("Temporary-Color" !in engine.editor.findAllSpotColors())
```
Removing a spot color doesn't update blocks already using it; they display magenta until the color is redefined or replaced with a different value.
## Spot Colors for Cutouts
CE.SDK can assign a spot color to a cutout type for die-cutting and other print finishing operations. On Android, use `engine.block.setSpotColorForCutoutType()` to associate a defined spot color with `CutoutType.SOLID` or `CutoutType.DASHED`. Query the current assignment with `getSpotColorForCutoutType()`.
```kotlin highlight-android-cutout
engine.editor.setSpotColor(
name = "DieLine",
Color.fromRGBA(r = 1F, g = 0F, b = 1F, a = 1F),
)
engine.editor.setSpotColor(
name = "DieLine",
Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F),
)
engine.block.setSpotColorForCutoutType(type = CutoutType.SOLID, name = "DieLine")
val cutoutSpotColor = engine.block.getSpotColorForCutoutType(type = CutoutType.SOLID)
check(cutoutSpotColor == "DieLine")
```
When no assignment is made, `CutoutType.SOLID` defaults to `"CutContour"` and `CutoutType.DASHED` defaults to `"PerfCutContour"`. All cutout blocks of the given type render with the assigned spot color immediately after the call.
## Best Practices
**Define early** - Register spot colors before applying them to blocks. Undefined colors display as magenta, which can confuse users.
**Use descriptive names** - Match your print vendor's reference, such as `"Brand-Red-485"`, to ensure correct ink matching in production.
**Provide both approximations** - RGB controls screen display, while CMYK supports print preview.
**Use tints sparingly** - Prefer tints for lighter variations instead of defining a separate spot color for every shade.
**Validate before export** - Query `findAllSpotColors()` to verify all expected spot colors are defined before exporting for print.
## Troubleshooting
### Spot Color Displays as Magenta
The spot color hasn't been defined. Call `setSpotColor()` with that name and either an RGB or CMYK approximation before applying it to a block.
### Color Approximation Looks Wrong
Call the matching `setSpotColor()` overload again with new component values. RGB values drive the on-screen display while CMYK values drive the print preview, so updating one does not change the other.
### Spot Color Not in Output
Verify that the spot color name matches exactly, including capitalization. Also check that the block color is a `SpotColor` value created with `Color.fromSpotColor(...)`, not an RGB or CMYK process color.
### Can't Remove Spot Color
Pass the exact name string to `removeSpotColor()`. Removing a spot color does not update blocks already using it; they display magenta until the color is redefined or replaced with another color.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.editor.setSpotColor(name=_, color=Color.fromRGBA(r=_, g=_, b=_, a=_))` | Define or update the RGB approximation of a spot color. |
| `engine.editor.setSpotColor(name=_, color=Color.fromCMYK(c=_, m=_, y=_, k=_))` | Define or update the CMYK approximation of a spot color. |
| `engine.editor.findAllSpotColors()` | Return the names of every defined spot color. |
| `engine.editor.getSpotColorRGB(name=_)` | Read the RGB approximation. Returns magenta if undefined. |
| `engine.editor.getSpotColorCMYK(name=_)` | Read the CMYK approximation. Returns magenta if undefined. |
| `engine.editor.removeSpotColor(name=_)` | Remove a spot color from the registry. |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an RGB approximation or process color. |
| `Color.fromCMYK(c=_, m=_, y=_, k=_)` | Create a CMYK approximation or process color. |
| `Color.fromSpotColor(name=_, tint=_, externalReference=_)` | Create a spot color reference. `tint` defaults to `1F`; `externalReference` defaults to `null`. |
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Apply a color, including a spot color, to a fill property. |
| `engine.block.getColor(block=_, property="fill/color/value")` | Read a color from a fill property. Returns a `Color` value. |
| `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable a block stroke. |
| `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke width before assigning a stroke color. |
| `engine.block.setStrokeColor(block=_, color=_)` | Apply a color, including a spot color, to a block stroke. |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable a block drop shadow. |
| `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set the horizontal drop-shadow offset. |
| `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set the vertical drop-shadow offset. |
| `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Set the horizontal drop-shadow blur radius. |
| `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Set the vertical drop-shadow blur radius. |
| `engine.block.setDropShadowColor(block=_, color=_)` | Apply a color, including a spot color, to a block drop shadow. |
| `engine.block.setStrokeOverprint(block=_, overprint=_)` | Mark a spot-color stroke for overprinting in PDF export. |
| `engine.block.getStrokeOverprint(block=_)` | Read whether the stroke is marked for PDF overprinting. |
| `engine.block.setFillOverprint(block=_, overprint=_)` | Mark a spot-color fill for overprinting in PDF export. |
| `engine.block.getFillOverprint(block=_)` | Read whether the fill is marked for PDF overprinting. |
| `engine.block.setSpotColorForCutoutType(type=_, name=_)` | Assign a spot color to `CutoutType.SOLID` or `CutoutType.DASHED`. |
| `engine.block.getSpotColorForCutoutType(type=_)` | Read the spot color assigned to a cutout type. |
| Related type | Description |
|--------------|-------------|
| `SpotColor` | Color value returned by `engine.block.getColor()` when a property uses a spot color. |
| `CutoutType.SOLID` / `CutoutType.DASHED` | Cutout type values accepted by the cutout APIs. |
## Next Steps
- [Export for Printing](https://img.ly/docs/cesdk/android/export-save-publish/for-printing-bca896/) - Export designs from CE.SDK as print-ready PDFs with professional output options including high compatibility mode, underlayers for special media, and scene DPI configuration.
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply colors to fills, strokes, and shadows.
- [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/) - Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "For Screen"
description: "Documentation for For Screen"
platform: android
url: "https://img.ly/docs/cesdk/android/colors/for-screen-1911f8/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [For Screen](https://img.ly/docs/cesdk/android/colors/for-screen-1911f8/)
---
---
## Related Pages
- [sRGB Colors](https://img.ly/docs/cesdk/android/colors/for-screen/srgb-e6f59b/) - Work with sRGB colors in CE.SDK for screen-based Android designs using RGBA values for fills, text, strokes, backgrounds, shadows, and transparency.
- [P3 Colors](https://img.ly/docs/cesdk/android/colors/for-screen/p3-706127/) - Documentation for P3 Colors
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "P3 Colors"
description: "Documentation for P3 Colors"
platform: android
url: "https://img.ly/docs/cesdk/android/colors/for-screen/p3-706127/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [For Screen](https://img.ly/docs/cesdk/android/colors/for-screen-1911f8/) > [P3 Colors](https://img.ly/docs/cesdk/android/colors/for-screen/p3-706127/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-for-screen-p3/P3Colors.kt reference-only
import ly.img.engine.Engine
// Demonstrates P3 support checks, enabling, and graceful fallback.
fun p3Colors(engine: Engine) {
val p3IsSupported = engine.editor.supportsP3()
try {
engine.editor.checkP3Support()
} catch (exception: Exception) {
println("P3 unavailable: ${exception.message}")
}
if (p3IsSupported) {
engine.editor.setSettingBoolean(
keypath = "features/p3WorkingColorSpace",
value = true,
)
}
try {
engine.editor.checkP3Support()
engine.editor.setSettingBoolean(
keypath = "features/p3WorkingColorSpace",
value = true,
)
} catch (exception: Exception) {
println("Staying on sRGB: ${exception.message}")
}
}
```
Detect support for the Display P3 wide color gamut and switch the engine into
a 16-bit P3 working color space on capable Android devices.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-for-screen-p3)
P3 is a wide color gamut that covers roughly 25% more visible colors than sRGB, especially in the reds, oranges, and green-cyan regions. CE.SDK can render and export in a 16-bit Display P3 working space on Android devices that support it, preserving colors that would otherwise be clipped to sRGB.
## What is P3?
The DCI-P3 color space was developed for digital cinema and has been adopted in modern consumer displays. CE.SDK uses the Display P3 variant, which keeps sRGB's gamma curve and replaces only the color primaries. Compared to sRGB:
- **Gamut size**: P3 covers about 25% more visible colors
- **Primary colors**: P3 red and green are more saturated
- **Precision**: The P3 working space uses 16 bits per channel instead of 8, which reduces banding in gradients and color adjustments
- **Backwards compatibility**: P3 content displayed on sRGB hardware is automatically converted
On sRGB displays, colors are converted for display but the underlying P3 data is preserved in exports.
## Check P3 Support
`supportsP3()` returns `true` when the engine can run in the P3 working color space on the current Android device. Use the result to decide whether to enable P3 — the setting is silently ignored on unsupported devices.
```kotlin highlight-android-check-support
val p3IsSupported = engine.editor.supportsP3()
```
For a richer diagnostic, use `checkP3Support()` instead. It throws an exception whose message explains why P3 is unavailable, such as missing platform support, missing 16-bit float GPU support, or no P3-capable display.
```kotlin highlight-android-check-support-throwing
try {
engine.editor.checkP3Support()
} catch (exception: Exception) {
println("P3 unavailable: ${exception.message}")
}
```
## Enable the P3 Working Color Space
Switch the engine into a 16-bit Display P3 pipeline by setting the `features/p3WorkingColorSpace` flag. The setting affects everything the editor renders, including the color picker and live preview, and image exports preserve the wider gamut by writing 16-bit Display P3 PNGs with an embedded ICC profile.
```kotlin highlight-android-enable
if (p3IsSupported) {
engine.editor.setSettingBoolean(
keypath = "features/p3WorkingColorSpace",
value = true,
)
}
```
Guard the setting with `supportsP3()` so your app only enables P3 when the current device can use it.
## Graceful Fallback
Combine `checkP3Support()` with the setting update when you want a single pattern that enables P3 where available and continues in sRGB otherwise.
```kotlin highlight-android-graceful-fallback
try {
engine.editor.checkP3Support()
engine.editor.setSettingBoolean(
keypath = "features/p3WorkingColorSpace",
value = true,
)
} catch (exception: Exception) {
println("Staying on sRGB: ${exception.message}")
}
```
When the device does not support P3, the engine continues using its default 8-bit sRGB working space. Existing colors and exports remain valid; no further code changes are required.
## Platform Support
Android P3 availability depends on the device display and graphics stack:
| Android Runtime | P3 Working Color Space |
| --- | --- |
| P3-capable display with required 16-bit float support | Supported |
| Missing P3 display or required GPU support | Uses sRGB fallback |
`supportsP3()` returns the runtime result, so the same code compiles and runs across Android devices without model-specific checks.
## P3 vs sRGB: When to Use Each
| Use Case | Recommended |
| --- | --- |
| Android apps targeting wide-gamut devices | P3 |
| Photo or video editing where color accuracy matters | P3 |
| Importing P3 photos from modern mobile cameras | P3 |
| Cross-platform consistency across broad device ranges | sRGB |
| Smaller export files | sRGB |
The P3 working space exports 16-bit PNGs with an embedded Display P3 ICC profile. The doubled bit depth means files are typically about 1.67x the size of their 8-bit sRGB equivalents.
## API Reference
| Method | Description |
| --- | --- |
| `engine.editor.supportsP3()` | Returns `true` if the device supports the P3 working color space |
| `engine.editor.checkP3Support()` | Throws an exception describing why P3 is unavailable; returns normally when supported |
| `engine.editor.setSettingBoolean(keypath="features/p3WorkingColorSpace", value=true)` | Enables the 16-bit Display P3 working color space when the device supports it |
## Next Steps
- [sRGB Colors](https://img.ly/docs/cesdk/android/colors/for-screen/srgb-e6f59b/) — Work with sRGB colors for screen-based designs including creating RGBA colors, applying them to design elements, and converting from other color spaces.
- [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/) — Convert colors between sRGB and CMYK
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "sRGB Colors"
description: "Work with sRGB colors in CE.SDK for screen-based Android designs using RGBA values for fills, text, strokes, backgrounds, shadows, and transparency."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/for-screen/srgb-e6f59b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [For Screen](https://img.ly/docs/cesdk/android/colors/for-screen-1911f8/) > [sRGB Colors](https://img.ly/docs/cesdk/android/colors/for-screen/srgb-e6f59b/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-for-screen-srgb/SrgbColors.kt reference-only
import ly.img.engine.CMYKColor
import ly.img.engine.Color
import ly.img.engine.ColorSpace
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
import ly.img.engine.SpotColor
fun srgbColors(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = page, value = 800F)
engine.block.setHeight(block = page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color))
engine.block.setWidth(block = block, value = 400F)
engine.block.setHeight(block = block, value = 300F)
engine.block.setPositionX(block = block, value = 200F)
engine.block.setPositionY(block = block, value = 150F)
engine.block.appendChild(parent = page, child = block)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(textBlock, text = "sRGB Background")
engine.block.setWidth(block = textBlock, value = 320F)
engine.block.setHeight(block = textBlock, value = 80F)
engine.block.setPositionX(block = textBlock, value = 240F)
engine.block.setPositionY(block = textBlock, value = 480F)
engine.block.appendChild(parent = page, child = textBlock)
val rgbaBlue = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
val rgbaBlueFromInts = Color.fromRGBA(r = 51, g = 102, b = 230, a = 255)
val rgbaRed = Color.fromHex("#FFD91A1A")
val rgbaNavy = Color.fromColor(android.graphics.Color.rgb(13, 20, 46))
val rgbaOrange = Color.fromResource(android.R.color.holo_orange_light)
check(rgbaBlueFromInts.a == 1F)
val semiTransparentBlack = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.5F)
engine.block.setFillSolidColor(block = block, color = rgbaBlue)
engine.block.setTextColor(block = textBlock, color = rgbaNavy)
engine.block.setStrokeEnabled(block = block, enabled = true)
engine.block.setStrokeWidth(block = block, width = 8F)
engine.block.setStrokeColor(block = block, color = rgbaRed)
engine.block.setBackgroundColorEnabled(block = textBlock, enabled = true)
engine.block.setBackgroundColor(block = textBlock, color = rgbaOrange)
check(engine.block.isBackgroundColorEnabled(textBlock))
engine.block.setDropShadowEnabled(block = block, enabled = true)
engine.block.setDropShadowOffsetX(block = block, offsetX = 15F)
engine.block.setDropShadowOffsetY(block = block, offsetY = 15F)
engine.block.setDropShadowColor(block = block, color = semiTransparentBlack)
val currentFillColor = engine.block.getFillSolidColor(block)
val currentTextColors = engine.block.getTextColors(block = textBlock)
val currentBackgroundColor = engine.block.getBackgroundColor(textBlock)
val currentStrokeColor = engine.block.getStrokeColor(block)
val currentShadowColor = engine.block.getDropShadowColor(block)
check(currentFillColor == rgbaBlue)
check(currentTextColors.single() == rgbaNavy)
check(currentBackgroundColor == rgbaOrange)
check(currentShadowColor == semiTransparentBlack)
when (currentStrokeColor) {
is RGBAColor -> {
check(currentStrokeColor.r == rgbaRed.r)
check(currentStrokeColor.g == rgbaRed.g)
check(currentStrokeColor.b == rgbaRed.b)
check(currentStrokeColor.a == rgbaRed.a)
}
is CMYKColor -> error("Expected sRGB color, got CMYK: $currentStrokeColor")
is SpotColor -> error("Expected sRGB color, got spot color: $currentStrokeColor")
}
val cmykOrange = Color.fromCMYK(c = 0F, m = 0.5F, y = 1F, k = 0F, tint = 1F)
val convertedToSrgb = engine.editor.convertColorToColorSpace(
color = cmykOrange,
colorSpace = ColorSpace.SRGB,
)
check(convertedToSrgb is RGBAColor)
}
```
Apply sRGB colors to design elements for screen-based output using RGBA values
with red, green, blue, and alpha components.

> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-for-screen-srgb)
sRGB is the standard color space for screen displays. In the Android engine API, sRGB values are represented by `RGBAColor` objects. Each red, green, blue, and alpha component uses a floating-point value from `0F` to `1F`, not the `0` to `255` integer range used by many Android and design-tool color utilities.
The built-in Android editor color controls also work with sRGB preview colors. Users can choose from a saturation/value picker, hue slider, preset swatches, and an opacity slider where the edited property supports transparency. Configure reusable brand choices through [color palettes](https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/); use the engine API below when your app needs to set exact values programmatically.
## Creating sRGB Colors Programmatically
Create sRGB colors with `Color.fromRGBA`. The float overload returns an `RGBAColor`, and the alpha parameter defaults to `1F` for fully opaque colors. Android also provides an integer overload for `0` to `255` RGBA components where alpha defaults to `255`, plus factory helpers for hex strings, `@ColorInt` values, and color resources.
```kotlin highlight-android-create-rgba
val rgbaBlue = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F)
val rgbaBlueFromInts = Color.fromRGBA(r = 51, g = 102, b = 230, a = 255)
val rgbaRed = Color.fromHex("#FFD91A1A")
val rgbaNavy = Color.fromColor(android.graphics.Color.rgb(13, 20, 46))
val rgbaOrange = Color.fromResource(android.R.color.holo_orange_light)
```
## Working with Transparency
The alpha component controls transparency: `1F` is fully opaque and `0F` is fully transparent. Use values in between for overlays, shadows, and layered effects.
```kotlin highlight-android-create-transparent
val semiTransparentBlack = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.5F)
```
## Applying sRGB Colors to Fills
Use `setFillSolidColor` when a graphic block already has a color fill. The method writes the block's fill color without exposing the fill property path.
```kotlin highlight-android-apply-fill
engine.block.setFillSolidColor(block = block, color = rgbaBlue)
```
## Applying sRGB Colors to Text
Use `setTextColor` to color all text in a text block, or pass `from` and `to` to color a specific UTF-16 text range. Read back text colors with `getTextColors`, which returns the ordered unique colors in the range.
```kotlin highlight-android-apply-text
engine.block.setTextColor(block = textBlock, color = rgbaNavy)
```
## Applying sRGB Colors to Strokes
Stroke colors are stored on the design block. Enable the stroke first, set a visible width, then assign the sRGB color with `setStrokeColor`.
```kotlin highlight-android-apply-stroke
engine.block.setStrokeEnabled(block = block, enabled = true)
engine.block.setStrokeWidth(block = block, width = 8F)
engine.block.setStrokeColor(block = block, color = rgbaRed)
```
## Applying sRGB Colors to Backgrounds
Background colors use typed Android helpers as well. For a text block, enable the background, then assign an `RGBAColor` with `setBackgroundColor`.
```kotlin highlight-android-apply-background
engine.block.setBackgroundColorEnabled(block = textBlock, enabled = true)
engine.block.setBackgroundColor(block = textBlock, color = rgbaOrange)
```
## Applying sRGB Colors to Shadows
Drop shadow colors also accept `RGBAColor`. Enable the shadow before setting offsets and color so the configured value is visible when the block renders.
```kotlin highlight-android-apply-shadow
engine.block.setDropShadowEnabled(block = block, enabled = true)
engine.block.setDropShadowOffsetX(block = block, offsetX = 15F)
engine.block.setDropShadowOffsetY(block = block, offsetY = 15F)
engine.block.setDropShadowColor(block = block, color = semiTransparentBlack)
```
## Retrieving Colors from Elements
Read colors back with the matching getter for each property you changed. Fill and background helpers return `RGBAColor`, text returns an ordered `Color` list, and stroke or drop shadow helpers return the shared `Color` interface because those properties can hold sRGB, CMYK, or spot colors.
```kotlin highlight-android-get-color
val currentFillColor = engine.block.getFillSolidColor(block)
val currentTextColors = engine.block.getTextColors(block = textBlock)
val currentBackgroundColor = engine.block.getBackgroundColor(textBlock)
val currentStrokeColor = engine.block.getStrokeColor(block)
val currentShadowColor = engine.block.getDropShadowColor(block)
```
## Identifying sRGB Colors
Android represents color spaces as implementations of the `Color` interface. Use a Kotlin type check for `RGBAColor` before reading the `r`, `g`, `b`, and `a` components.
```kotlin highlight-android-identify-rgba
when (currentStrokeColor) {
is RGBAColor -> {
check(currentStrokeColor.r == rgbaRed.r)
check(currentStrokeColor.g == rgbaRed.g)
check(currentStrokeColor.b == rgbaRed.b)
check(currentStrokeColor.a == rgbaRed.a)
}
is CMYKColor -> error("Expected sRGB color, got CMYK: $currentStrokeColor")
is SpotColor -> error("Expected sRGB color, got spot color: $currentStrokeColor")
}
```
## Converting Colors to sRGB
Use `engine.editor.convertColorToColorSpace` with `ColorSpace.SRGB` when you need an sRGB representation of another color space, such as CMYK.
```kotlin highlight-android-convert-to-srgb
val cmykOrange = Color.fromCMYK(c = 0F, m = 0.5F, y = 1F, k = 0F, tint = 1F)
val convertedToSrgb = engine.editor.convertColorToColorSpace(
color = cmykOrange,
colorSpace = ColorSpace.SRGB,
)
check(convertedToSrgb is RGBAColor)
```
Color conversions are approximations because CMYK has a smaller gamut than sRGB, so vibrant colors may appear muted after conversion.
## Troubleshooting
**Colors appear incorrect:** Check which overload of `Color.fromRGBA` you passed. Float components use `0F` to `1F`; integer components use `0` to `255`, with `255` representing fully opaque alpha.
**Color not visible:** Make sure the target property is enabled. For example, call `setStrokeEnabled` before expecting stroke color to render, `setBackgroundColorEnabled` before expecting a background color to render, and `setDropShadowEnabled` before expecting a shadow to render.
**Type checks fail:** A value returned as `Color` may be `RGBAColor`, `CMYKColor`, or `SpotColor`. Convert to `ColorSpace.SRGB` or handle each type explicitly before reading RGBA components.
## API Reference
| Method | Description |
|--------|-------------|
| `Color.fromRGBA(r=_, g=_, b=_, a=1F)` | Create an sRGB color from `0F` to `1F` float components |
| `Color.fromRGBA(r=_, g=_, b=_, a=255)` | Create an sRGB color from `0` to `255` integer components |
| `Color.fromColor(color=_)` | Create an sRGB color from an Android `@ColorInt` value |
| `Color.fromHex(colorString=_)` | Create an sRGB color from an Android hex color string |
| `Color.fromResource(colorResource=_)` | Create an sRGB color from an Android color resource |
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` | Create a CMYK source color before converting it to sRGB |
| `engine.block.supportsFill(block=_)` | Check whether a block supports fill properties before setting fill colors |
| `engine.block.setFillSolidColor(block=_, color=_)` | Apply an sRGB color to a block's color fill |
| `engine.block.getFillSolidColor(block=_)` | Read a block's color fill as `RGBAColor` |
| `engine.block.setTextColor(block=_, color=_, from=_, to=_)` | Apply a color to all text or a UTF-16 text range |
| `engine.block.getTextColors(block=_, from=_, to=_)` | Read the ordered unique colors from all text or a UTF-16 text range |
| `engine.block.supportsBackgroundColor(block=_)` | Check whether a block supports background color properties |
| `engine.block.setBackgroundColorEnabled(block=_, enabled=_)` | Enable or disable background color rendering |
| `engine.block.isBackgroundColorEnabled(block=_)` | Read whether background color rendering is enabled |
| `engine.block.setBackgroundColor(block=_, color=_)` | Apply an sRGB color to a block background |
| `engine.block.getBackgroundColor(block=_)` | Read a block's background color as `RGBAColor` |
| `engine.block.supportsStroke(block=_)` | Check whether a block supports stroke properties |
| `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable stroke rendering |
| `engine.block.isStrokeEnabled(block=_)` | Read whether stroke rendering is enabled |
| `engine.block.setStrokeWidth(block=_, width=_)` | Set stroke width |
| `engine.block.getStrokeWidth(block=_)` | Read stroke width |
| `engine.block.setStrokeColor(block=_, color=_)` | Apply a color to a block stroke |
| `engine.block.getStrokeColor(block=_)` | Read a block's stroke color as `Color` |
| `engine.block.supportsDropShadow(block=_)` | Check whether a block supports drop shadow properties |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable drop shadow rendering |
| `engine.block.isDropShadowEnabled(block=_)` | Read whether drop shadow rendering is enabled |
| `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set a block's drop shadow x offset |
| `engine.block.getDropShadowOffsetX(block=_)` | Read a block's drop shadow x offset |
| `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set a block's drop shadow y offset |
| `engine.block.getDropShadowOffsetY(block=_)` | Read a block's drop shadow y offset |
| `engine.block.setDropShadowColor(block=_, color=_)` | Apply a color to a block drop shadow |
| `engine.block.getDropShadowColor(block=_)` | Read a block's drop shadow color as `Color` |
| `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Convert a color to another color space |
## Next Steps
- [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/) — Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control.
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Use named spot colors for brand consistency
- [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/) - Convert colors between sRGB, CMYK, and spot color spaces
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Comprehensive color application guide
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Overview"
description: "Manage color usage in your designs, from applying brand palettes to handling print and screen formats."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/overview-16a177/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Overview](https://img.ly/docs/cesdk/android/colors/overview-16a177/)
---
Colors are a fundamental part of design in the CreativeEditor SDK (CE.SDK). In Android apps, consistent color management helps designs look the way you intend across editor previews, digital output, and print-oriented workflows. CE.SDK offers flexible tools for working with colors through both the user interface and programmatically, making it easy to manage color workflows at any scale.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Replace Individual Colors"
description: "Selectively replace specific colors in images using CE.SDK's Recolor and Green Screen effects on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/colors/replace-48cd71/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Replace Individual Colors](https://img.ly/docs/cesdk/android/colors/replace-48cd71/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-replace/ColorsReplace.kt reference-only
import android.net.Uri
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
fun colorsReplace(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
fun addImageBlock(
x: Float,
y: Float,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 150F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block, fill = fill)
return block
}
val recolorBlock = addImageBlock(x = 50F, y = 50F)
val recolorEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0.5F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = recolorBlock, effectBlock = recolorEffect)
check(engine.block.getEffects(recolorBlock) == listOf(recolorEffect))
val tolerancesBlock = addImageBlock(x = 300F, y = 50F)
val tolerancesEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.6F, b = 0.4F, a = 1F),
)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.3F, g = 0.7F, b = 0.3F, a = 1F),
)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/colorMatch", value = 0.3F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/brightnessMatch", value = 0.2F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/smoothness", value = 0.1F)
engine.block.appendEffect(block = tolerancesBlock, effectBlock = tolerancesEffect)
check(engine.block.getFloat(tolerancesEffect, property = "effect/recolor/colorMatch") == 0.3F)
check(engine.block.getFloat(tolerancesEffect, property = "effect/recolor/brightnessMatch") == 0.2F)
check(engine.block.getFloat(tolerancesEffect, property = "effect/recolor/smoothness") == 0.1F)
val greenScreenBlock = addImageBlock(x = 550F, y = 50F)
val greenScreenEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = greenScreenEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = greenScreenBlock, effectBlock = greenScreenEffect)
check(engine.block.getEffects(greenScreenBlock) == listOf(greenScreenEffect))
val spillBlock = addImageBlock(x = 50F, y = 250F)
val spillEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = spillEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0.2F, g = 0.8F, b = 0.3F, a = 1F),
)
engine.block.setFloat(spillEffect, property = "effect/green_screen/colorMatch", value = 0.4F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/smoothness", value = 0.2F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/spill", value = 0.5F)
engine.block.appendEffect(block = spillBlock, effectBlock = spillEffect)
check(engine.block.getFloat(spillEffect, property = "effect/green_screen/colorMatch") == 0.4F)
check(engine.block.getFloat(spillEffect, property = "effect/green_screen/smoothness") == 0.2F)
check(engine.block.getFloat(spillEffect, property = "effect/green_screen/spill") == 0.5F)
val stackedBlock = addImageBlock(x = 300F, y = 250F)
val redToBlue = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = redToBlue)
val stackedGreenScreen = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = stackedGreenScreen,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = stackedGreenScreen)
val stackedEffects = engine.block.getEffects(stackedBlock)
check(stackedEffects == listOf(redToBlue, stackedGreenScreen))
engine.block.setEffectEnabled(effectBlock = stackedEffects[0], enabled = false)
val isFirstEffectEnabled = engine.block.isEffectEnabled(stackedEffects[0])
engine.block.removeEffect(block = stackedBlock, index = 1)
engine.block.destroy(stackedGreenScreen)
check(!isFirstEffectEnabled)
check(engine.block.getEffects(stackedBlock) == listOf(redToBlue))
val batchBlock = addImageBlock(x = 550F, y = 250F)
val allGraphicBlocks = engine.block.findByType(type = DesignBlockType.Graphic)
for (block in allGraphicBlocks) {
if (engine.block.getEffects(block).isNotEmpty()) {
continue
}
val batchRecolor = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.7F, b = 0.6F, a = 1F),
)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.6F, g = 0.7F, b = 0.9F, a = 1F),
)
engine.block.setFloat(batchRecolor, property = "effect/recolor/colorMatch", value = 0.25F)
engine.block.appendEffect(block = block, effectBlock = batchRecolor)
}
check(engine.block.getEffects(batchBlock).size == 1)
}
```
Selectively swap image colors or remove colored backgrounds using CE.SDK's
Recolor and Green Screen effects.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-replace)
CE.SDK provides two effects for selective color modification. **Recolor** replaces pixels that match a source color with a target color, while **Green Screen** makes pixels that match a source color transparent. Both effects expose tolerance parameters so you can control how closely pixels must match before they are changed.
This guide covers the built-in Android editor controls and the programmatic block API for applying, tuning, stacking, and batch-processing color replacement effects.
## Using the Built-In Effects UI
The standard Android editor exposes Recolor and Green Screen through the effects controls for supported image or graphic blocks. Users select a block, add the effect, choose the source color, and adjust the available color-match sliders while previewing the result on the canvas.
The same effect properties are available in code. Use the UI when users should make creative choices interactively, and use the engine API when your app needs repeatable color changes, automation, or batch processing.
## Prepare the Scene
The sample creates a scene and page, then uses a small `addImageBlock` helper to create one image-filled graphic block per section so the effects can be configured independently.
```kotlin highlight-android-prepare-scene
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
```kotlin highlight-android-create-image-blocks
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
fun addImageBlock(
x: Float,
y: Float,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 150F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block, fill = fill)
return block
}
```
## Programmatic Color Replacement
Effects are design blocks. Create an effect with `createEffect`, configure its properties, then attach it to a target block with `appendEffect`.
### Creating a Recolor Effect
The Recolor effect replaces pixels matching a source color with a target color. Use `EffectType.Recolor`, then set `effect/recolor/fromColor` and `effect/recolor/toColor`.
```kotlin highlight-android-create-recolor
val recolorBlock = addImageBlock(x = 50F, y = 50F)
val recolorEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0.5F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = recolorBlock, effectBlock = recolorEffect)
```
`fromColor` selects the color to match in the image. `toColor` defines the replacement color. `Color.fromRGBA` accepts either Float components in the `0F` to `1F` range or Int components in the 0 to 255 range.
### Configuring Color Matching Precision
Adjust the tolerance parameters with `setFloat` to fine-tune which pixels are affected.
```kotlin highlight-android-configure-recolor
val tolerancesBlock = addImageBlock(x = 300F, y = 50F)
val tolerancesEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.6F, b = 0.4F, a = 1F),
)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.3F, g = 0.7F, b = 0.3F, a = 1F),
)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/colorMatch", value = 0.3F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/brightnessMatch", value = 0.2F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/smoothness", value = 0.1F)
engine.block.appendEffect(block = tolerancesBlock, effectBlock = tolerancesEffect)
```
| Property | Range | Description |
| --- | --- | --- |
| `effect/recolor/colorMatch` | 0-1 | Hue tolerance. Higher values include more color variations around the source color. |
| `effect/recolor/brightnessMatch` | 0-1 | Brightness weighting. Lower values let brighter or darker pixels match; higher values require brightness closer to the source color. |
| `effect/recolor/smoothness` | 0-1 | Edge blending. Higher values create softer transitions at the boundaries of affected areas. |
### Creating a Green Screen Effect
The Green Screen effect removes pixels matching a specified color, making those pixels transparent. Use `EffectType.GreenScreen` and set `effect/green_screen/fromColor`.
```kotlin highlight-android-create-green-screen
val greenScreenBlock = addImageBlock(x = 550F, y = 50F)
val greenScreenEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = greenScreenEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = greenScreenBlock, effectBlock = greenScreenEffect)
```
Use this effect for workflows such as removing a solid-color background from a product or subject image.
### Configuring Green Screen Parameters
Green Screen exposes precision parameters for the color match, cutout edge, and color-spill reduction.
```kotlin highlight-android-configure-green-screen
val spillBlock = addImageBlock(x = 50F, y = 250F)
val spillEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = spillEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0.2F, g = 0.8F, b = 0.3F, a = 1F),
)
engine.block.setFloat(spillEffect, property = "effect/green_screen/colorMatch", value = 0.4F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/smoothness", value = 0.2F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/spill", value = 0.5F)
engine.block.appendEffect(block = spillBlock, effectBlock = spillEffect)
```
| Property | Description |
| --- | --- |
| `effect/green_screen/colorMatch` | Tolerance for matching the background color. |
| `effect/green_screen/smoothness` | Edge softness for cleaner cutouts around subjects. |
| `effect/green_screen/spill` | Reduces color bleed from the removed background onto subject edges. |
## Managing Multiple Effects
A block can hold multiple effects. Use `getEffects` to read the stack, `appendEffect` or `insertEffect` to place effects, `setEffectEnabled` to toggle an effect without removing it, and `removeEffect` to detach an effect by index.
```kotlin highlight-android-manage-effects
val stackedBlock = addImageBlock(x = 300F, y = 250F)
val redToBlue = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = redToBlue)
val stackedGreenScreen = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = stackedGreenScreen,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = stackedGreenScreen)
val stackedEffects = engine.block.getEffects(stackedBlock)
check(stackedEffects == listOf(redToBlue, stackedGreenScreen))
engine.block.setEffectEnabled(effectBlock = stackedEffects[0], enabled = false)
val isFirstEffectEnabled = engine.block.isEffectEnabled(stackedEffects[0])
engine.block.removeEffect(block = stackedBlock, index = 1)
engine.block.destroy(stackedGreenScreen)
```
The snippet adds Recolor and Green Screen effects to show stack order, disables the first effect without removing it, reads its enabled state, and removes the second effect by index. Destroy detached effect blocks when your app no longer needs them.
## Batch Processing
Apply the same color replacement configuration to every graphic block in a scene. Use `findByType(DesignBlockType.Graphic)` to locate targets, and skip blocks that already have effects when you need to preserve existing edits.
```kotlin highlight-android-batch-processing
val allGraphicBlocks = engine.block.findByType(type = DesignBlockType.Graphic)
for (block in allGraphicBlocks) {
if (engine.block.getEffects(block).isNotEmpty()) {
continue
}
val batchRecolor = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.7F, b = 0.6F, a = 1F),
)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.6F, g = 0.7F, b = 0.9F, a = 1F),
)
engine.block.setFloat(batchRecolor, property = "effect/recolor/colorMatch", value = 0.25F)
engine.block.appendEffect(block = block, effectBlock = batchRecolor)
}
```
This pattern is useful for product variations, template personalization, and other automated image-processing workflows.
## Troubleshooting
**Colors not matching as expected**: Increase `colorMatch` for a broader selection, or decrease it when the source color should match more precisely. Check that your source color is close to the actual color in the image.
**Harsh edges around replaced areas**: Increase `smoothness` to soften transitions at the boundaries of affected pixels.
**Color spill on Green Screen subjects**: Increase `spill` to reduce tint from the removed background on subject edges.
**Effect not visible**: Verify that the effect is enabled with `isEffectEnabled` and that the effect was attached to the target block with `appendEffect`.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.create()` | Create the scene used by the sample. |
| `engine.block.create(blockType=_)` | Create page and graphic blocks. |
| `engine.block.setWidth(block=_, value=_)` | Set page or block width. |
| `engine.block.setHeight(block=_, value=_)` | Set page or block height. |
| `engine.block.appendChild(parent=_, child=_)` | Add a page or graphic block to its parent. |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular shape for a graphic block. |
| `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block. |
| `engine.block.setPositionX(block=_, value=_)` | Set a block's horizontal position. |
| `engine.block.setPositionY(block=_, value=_)` | Set a block's vertical position. |
| `engine.block.createFill(fillType=FillType.Image)` | Create an image fill. |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on an image fill. |
| `engine.block.setFill(block=_, fill=_)` | Assign the image fill to a graphic block. |
| `engine.block.createEffect(type=EffectType.Recolor)` | Create a Recolor effect block. |
| `engine.block.createEffect(type=EffectType.GreenScreen)` | Create a Green Screen effect block. |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Add an effect to a block's effect stack. |
| `engine.block.insertEffect(block=_, effectBlock=_, index=_)` | Add an effect at a specific stack index. |
| `engine.block.getEffects(block=_)` | Read the effects applied to a block. |
| `engine.block.removeEffect(block=_, index=_)` | Detach an effect by stack index. |
| `engine.block.destroy(block=_)` | Destroy a detached effect block that is no longer needed. |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enable or disable an effect without removing it. |
| `engine.block.isEffectEnabled(effectBlock=_)` | Check whether an effect is enabled. |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create source and target RGBA colors for effect properties. |
| `engine.block.setColor(block=_, property="effect/recolor/fromColor", value=_)` | Set the source color matched by Recolor. |
| `engine.block.setColor(block=_, property="effect/recolor/toColor", value=_)` | Set the replacement color used by Recolor. |
| `engine.block.setColor(block=_, property="effect/green_screen/fromColor", value=_)` | Set the color removed by Green Screen. |
| `engine.block.setFloat(block=_, property="effect/recolor/colorMatch", value=_)` | Set Recolor hue tolerance. |
| `engine.block.setFloat(block=_, property="effect/recolor/brightnessMatch", value=_)` | Set Recolor brightness tolerance. |
| `engine.block.setFloat(block=_, property="effect/recolor/smoothness", value=_)` | Set Recolor edge blending. |
| `engine.block.setFloat(block=_, property="effect/green_screen/colorMatch", value=_)` | Set Green Screen color-match tolerance. |
| `engine.block.setFloat(block=_, property="effect/green_screen/smoothness", value=_)` | Set Green Screen edge softness. |
| `engine.block.setFloat(block=_, property="effect/green_screen/spill", value=_)` | Set Green Screen spill reduction. |
| `engine.block.findByType(type=DesignBlockType.Graphic)` | Find graphic blocks for batch processing. |
## Next Steps
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply solid colors to shapes, backgrounds, and other design elements.
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Apply, configure, stack, and manage filters and effects.
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Export processed images in various formats.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "System Compatibility"
description: "Learn how device performance and hardware limits affect CE.SDK editing, rendering, and export capabilities."
platform: android
url: "https://img.ly/docs/cesdk/android/compatibility-139ef9/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/) > [System Compatibility](https://img.ly/docs/cesdk/android/compatibility-139ef9/)
---
## Targets
On Android, CE.SDK makes use of system-frameworks to benefit from hardware acceleration and platform native performance. The following targets are supported:
- Android 7 or later (`minSdk 24`)
## Recommended Hardware
Android phones released in the last 5 years, e.g. Asus Zenfone 3, Samsung M31s, or Google Pixel 5. Video capabilities directly depend on the video capabilities of the individual phone.
## Video
Playback and exporting is **supported for all codecs** mentioned in the general section.
However, mobile devices have stricter limits around the number of parallel encoders and decoders compared to fully fledged desktop machines. This means, that very large scenes with more than 10 videos shown in parallel may fail to play all videos at the same time.
## Export Limitations
The export size is limited by the hardware capabilities of the device, e.g., due to the maximum texture size that can be allocated. The maximum possible export size can be queried via API, see [export guide](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/).
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Compatibility & Security"
description: "Learn about CE.SDK's compatibility and security features."
platform: android
url: "https://img.ly/docs/cesdk/android/compatibility-fef719/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/)
---
CE.SDK provides robust compatibility and security features across platforms.
Learn about supported browsers, frameworks, file formats, language support,
and how CE.SDK ensures secure operation in your applications.
---
## Related Pages
- [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) - Understand CE.SDK’s engine and editor bundle sizes and how they affect your mobile app’s download footprint.
- [System Compatibility](https://img.ly/docs/cesdk/android/compatibility-139ef9/) - Learn how device performance and hardware limits affect CE.SDK editing, rendering, and export capabilities.
- [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) - See which image, video, audio, font, and template formats CE.SDK supports for import and export.
- [Security](https://img.ly/docs/cesdk/android/security-777bfd/) - Learn how CE.SDK keeps your data private with client-side processing, secure licensing, and GDPR-compliant practices.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Concepts"
description: "Key concepts and principles of CE.SDK"
platform: android
url: "https://img.ly/docs/cesdk/android/concepts-c9ff51/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/)
---
Key Concepts and principles of CE.SDK.
---
## Related Pages
- [Key Concepts](https://img.ly/docs/cesdk/android/key-concepts-21a270/) - Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control.
- [Key Capabilities](https://img.ly/docs/cesdk/android/key-capabilities-dbb5b1/) - Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control.
- [Architecture](https://img.ly/docs/cesdk/android/concepts/architecture-6ea9b2/) - Understand how CE.SDK is structured around the CreativeEngine and its six interconnected APIs.
- [Terminology](https://img.ly/docs/cesdk/android/concepts/terminology-99e82d/) - Definitions for the core terms and concepts used throughout CE.SDK documentation, including Engine, Scene, Block, Fill, Shape, Effect, and more.
- [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) - Control editing access with Creator, Adopter, Viewer, and Presenter roles using global and block-level scopes for tailored permissions.
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Learn how blocks define elements in a scene and how to structure them for rendering in CE.SDK.
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) - Create, configure, save, and load scenes—the root container for all design elements in CE.SDK.
- [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) - Structure Android scenes with consistent pages, shared dimensions, and page-level properties in CE.SDK.
- [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) - Understand the Android asset system in CE.SDK, including asset definitions, custom asset sources, queries, and apply flows.
- [Editor State](https://img.ly/docs/cesdk/android/concepts/edit-modes-1f5b6c/) - Control how users interact with content by switching between edit modes like transform, crop, and text.
- [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) - Understand how templates work in CE.SDK—reusable designs with variables for dynamic text and placeholders for swappable media.
- [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) - Subscribe to block creation, update, and deletion events to track changes in your CE.SDK scene.
- [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) - Use buffers to store temporary, non-serializable data in CE.SDK via the CreativeEngine API.
- [Working With Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/) - Preload resources, find transient data, detect MIME types, and relocate URLs in CE.SDK for Android.
- [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) - Manage undo and redo stacks in CE.SDK using multiple histories, callbacks, and API-based controls.
- [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/) - Configure design units (pixels, millimeters, inches) and DPI settings for print-ready output in CE.SDK.
- [Font Size Unit](https://img.ly/docs/cesdk/android/concepts/font-size-unit-3b2d60/) - Configure how font sizes are interpreted (Pixel vs Point) per scene in the CE.SDK Android engine.
- [Headless](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Use the engine directly, without any prebuilt UI.
- [Error Catalog](https://img.ly/docs/cesdk/android/concepts/error-catalog-z3djzn/) - Reference of every structured CE.SDK engine error code, its message, hint, and related documentation page.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Architecture"
description: "Understand how CE.SDK is structured around the CreativeEngine and its six interconnected APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/architecture-6ea9b2/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Architecture](https://img.ly/docs/cesdk/android/concepts/architecture-6ea9b2/)
---
```kotlin file=@cesdk_android_examples/engine-guides-concepts-architecture/Architecture.kt reference-only
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
@Suppress("UNUSED_VARIABLE")
suspend fun architecture(engine: Engine) = withContext(engine.dispatcher) {
var subscription: Job? = null
val variableKey = "username"
val hadPreviousVariable = variableKey in engine.variable.findAll()
val previousVariable = if (hadPreviousVariable) engine.variable.get(variableKey) else null
try {
// The engine exposes six API namespaces:
engine.scene // Scene API — content hierarchy
engine.block // Block API — create and modify blocks
engine.asset // Asset API — manage asset sources
engine.editor // Editor API — edit modes, undo/redo, roles
engine.event // Event API — subscribe to changes
engine.variable // Variable API — template variables
// Create a scene with a page and a graphic block.
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block, fill = engine.block.createFill(FillType.Color))
engine.block.appendChild(parent = page, child = block)
// Traverse the hierarchy.
val pages = engine.scene.getPages()
val children = engine.block.getChildren(block = pages.first())
// Scenes use the same hierarchy for static and time-based experiences.
val contentScene = engine.scene.create()
val contentPage = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = contentScene, child = contentPage)
// Subscribe to block changes using Flow.
subscription =
engine.event.subscribe(blocks = listOf(scene))
.onEach { events ->
events.forEach { event ->
println("Block ${event.block} had event: ${event.type}")
}
}
// `this` is the surrounding coroutine scope.
.launchIn(this)
// Set and retrieve template variables.
engine.variable.set(key = "username", value = "Jane")
val username = engine.variable.get(key = "username")
} finally {
subscription?.cancel()
if (hadPreviousVariable) {
engine.variable.set(key = variableKey, value = checkNotNull(previousVariable))
} else if (variableKey in engine.variable.findAll()) {
engine.variable.remove(variableKey)
}
}
}
```
Understand how CE.SDK is structured around the CreativeEngine and its six interconnected APIs.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-concepts-architecture)
CE.SDK is built around the **CreativeEngine** runtime, exposed on Android through the `Engine` class. It manages state, rendering, and coordination between six specialized APIs. Understanding how these pieces connect makes it much easier to navigate the SDK and decide where a change belongs.
## The CreativeEngine
The `Engine` is the central coordinator. Creating content, manipulating blocks, rendering, and exporting all flow through it. Start it once, keep engine work on the main thread, and access the rest of CE.SDK through its API namespaces.
The `Engine` manages:
- **One active scene** containing all design content
- **Six API namespaces** for different domains of functionality
- **Event dispatching** for reactive state management
- **Resource loading** and caching
- **Rendering** to a `SurfaceView`, `TextureView`, or offscreen context
On Android, lifecycle methods such as `start`, `bindSurfaceView`, `bindTextureView`, and `bindOffscreen` are annotated `@MainThread`. Engine-only integrations therefore typically run inside `CoroutineScope(Dispatchers.Main).launch { ... }`.
## Core APIs
The engine exposes six API namespaces, each handling a specific domain of functionality:
```kotlin highlight-android-architecture-apis
// The engine exposes six API namespaces:
engine.scene // Scene API — content hierarchy
engine.block // Block API — create and modify blocks
engine.asset // Asset API — manage asset sources
engine.editor // Editor API — edit modes, undo/redo, roles
engine.event // Event API — subscribe to changes
engine.variable // Variable API — template variables
```
| API | Namespace | Purpose |
| --- | --- | --- |
| Scene API | `engine.scene` | Content hierarchy: create, load, and save scenes |
| Block API | `engine.block` | Create, modify, and query design blocks |
| Asset API | `engine.asset` | Register and query asset sources |
| Editor API | `engine.editor` | Edit modes, undo/redo, and user roles |
| Event API | `engine.event` | Subscribe to engine state changes |
| Variable API | `engine.variable` | Template variables for data-driven designs |
## Content Hierarchy
CE.SDK organizes content in a tree: **Scene** → **Pages** → **Blocks**.
- **Scene**: The root container. One scene per engine instance. Supports both static designs and time-based video editing.
- **Pages**: Containers within a scene. Artboards in design scenes and timeline compositions in video scenes.
- **Blocks**: The atomic units: graphics, text, audio, video, and more. Everything visible is a block.
Create a scene, add a page, and populate it with blocks:
```kotlin highlight-android-architecture-hierarchy
// Create a scene with a page and a graphic block.
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block, fill = engine.block.createFill(FillType.Color))
engine.block.appendChild(parent = page, child = block)
// Traverse the hierarchy.
val pages = engine.scene.getPages()
val children = engine.block.getChildren(block = pages.first())
```
The **Scene API** manages this hierarchy. The **Block API** manipulates individual blocks within it. See [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) and [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for details.
## Scene Contexts
CE.SDK uses the same scene hierarchy for static designs and time-based content. Starter kits and editor configurations decide which editing tools are available for a given experience; the scene itself still contains pages and blocks.
```kotlin highlight-android-architecture-scene-modes
// Scenes use the same hierarchy for static and time-based experiences.
val contentScene = engine.scene.create()
val contentPage = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = contentScene, child = contentPage)
```
- **Design experiences**: Static outputs such as social posts, print materials, and graphics. Blocks are positioned spatially on pages.
- **Video experiences**: Time-based outputs with playback, timeline, and audio support. Blocks can use temporal properties such as duration and trim.
Create the scene with `engine.scene.create()`, then configure the editor experience or automation pipeline around the content you want to produce. See [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) for details.
## Event System
Subscribe to engine events to build reactive UIs that update when state changes. On Android, the Event API exposes a Kotlin `Flow` of `DesignBlockEvent` batches:
```kotlin highlight-android-architecture-events
// Subscribe to block changes using Flow.
subscription =
engine.event.subscribe(blocks = listOf(scene))
.onEach { events ->
events.forEach { event ->
println("Block ${event.block} had event: ${event.type}")
}
}
// `this` is the surrounding coroutine scope.
.launchIn(this)
```
Store the `Job` returned by `launchIn` and cancel it when you no longer need updates.
See [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) for details on subscribing to engine state changes.
## Template Variables
The Variable API enables data-driven designs. Define variables at the scene level and reference them in text blocks with `{{variableName}}` syntax:
```kotlin highlight-android-architecture-variables
// Set and retrieve template variables.
engine.variable.set(key = "username", value = "Jane")
val username = engine.variable.get(key = "username")
```
When variable values change, affected blocks update automatically.
## How They Connect
A typical flow shows the interconnection:
1. **Scene API** creates the content structure.
2. **Asset API** provides images, templates, or other content.
3. **Block API** creates blocks and applies assets to them.
4. **Variable API** injects dynamic data into text blocks.
5. **Editor API** controls what users can modify.
6. **Event API** notifies your UI of every change.
Each API focuses on one domain, but they operate through the same Engine instance. The runtime coordinates these interactions for you.
## Integration Patterns
CE.SDK runs in two main Android contexts:
- **Interactive UI**: Use the `Editor` composable directly or start from one of the Android starter kits. This gives you a ready-made editing surface while still exposing the same Engine APIs underneath. The legacy solution composables such as `DesignEditor` are deprecated in favor of this architecture.
- **Headless**: Create the engine yourself with `Engine.getInstance(...)`, call `start(...)`, and render through `bindOffscreen(...)`. Use this for exports, automation, and batch processing. See [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/).
Both patterns use the same six APIs. The difference is how you host the engine and whether you attach a UI render target.
## Next Steps
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Scene creation and management
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Working with design blocks
- [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) — Page management and configuration
- [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) — Running without UI
- [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) — Creating data-driven designs
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Assets"
description: "Understand the Android asset system in CE.SDK, including asset definitions, custom asset sources, queries, and apply flows."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/)
---
```kotlin file=@cesdk_android_examples/engine-guides-concepts-assets/ConceptsAssets.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import ly.img.engine.Asset
import ly.img.engine.AssetContext
import ly.img.engine.AssetCredits
import ly.img.engine.AssetDefinition
import ly.img.engine.AssetLicense
import ly.img.engine.AssetSource
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FetchAssetOptions
import ly.img.engine.FillType
import ly.img.engine.FindAssetsQuery
import ly.img.engine.FindAssetsResult
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.util.UUID
suspend fun conceptsAssets(engine: Engine) = withContext(engine.dispatcher) {
val sourceEventJobs = mutableListOf()
val invocationId = UUID.randomUUID()
val source = BrandedAssetSource(sourceId = "ly.img.asset.source.branded.$invocationId")
val localSourceId = "my-local-images.$invocationId"
val registeredGuideSourceIds = mutableListOf()
try {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
sourceEventJobs += engine.asset.onAssetSourceAdded()
.onEach { println("Asset source added: $it") }
.launchIn(this)
sourceEventJobs += engine.asset.onAssetSourceRemoved()
.onEach { println("Asset source removed: $it") }
.launchIn(this)
sourceEventJobs += engine.asset.onAssetSourceUpdated()
.onEach { println("Asset source updated: $it") }
.launchIn(this)
engine.asset.addSource(source)
registeredGuideSourceIds += source.sourceId
val queriedAssets = engine.asset.findAssets(
sourceId = source.sourceId,
query = FindAssetsQuery(
perPage = 10,
page = 0,
query = "logo",
groups = listOf("logos"),
),
)
val queriedAsset = queriedAssets.assets.first()
val groups = engine.asset.getGroups(sourceId = source.sourceId)
println("Found ${queriedAssets.total} assets in groups $groups")
val appliedBlock = engine.asset.applyAssetSourceAsset(
sourceId = source.sourceId,
asset = queriedAsset,
)
if (appliedBlock != null) {
engine.block.setPositionX(appliedBlock, 64F)
engine.block.setPositionY(appliedBlock, 64F)
}
if (appliedBlock != null) {
engine.block.forceLoadResources(listOf(appliedBlock))
}
engine.asset.addLocalSource(
sourceId = localSourceId,
supportedMimeTypes = listOf(MimeType.JPEG.key),
)
registeredGuideSourceIds += localSourceId
val localAsset = AssetDefinition(
id = "sunrise-poster",
label = mapOf("en" to "Sunrise Poster"),
tags = mapOf("en" to listOf("poster", "sunrise", "brand")),
groups = listOf("posters"),
meta = mapOf(
"uri" to "https://img.ly/static/ubq_samples/sample_1.jpg",
"thumbUri" to "https://img.ly/static/ubq_samples/sample_1.jpg",
"mimeType" to MimeType.JPEG.key,
"kind" to "image",
"blockType" to DesignBlockType.Graphic.key,
"fillType" to FillType.Image.key,
"shapeType" to ShapeType.Rect.key,
"width" to "1080",
"height" to "1080",
),
)
engine.asset.addAsset(sourceId = localSourceId, asset = localAsset)
engine.asset.assetSourceContentsChanged(sourceId = localSourceId)
} finally {
withContext(NonCancellable) {
try {
removeRegisteredGuideSources(engine = engine, sourceIds = registeredGuideSourceIds)
} finally {
sourceEventJobs.forEach { it.cancel() }
sourceEventJobs.forEach { it.join() }
}
}
}
}
private fun removeRegisteredGuideSources(
engine: Engine,
sourceIds: List,
) {
var cleanupFailure: Throwable? = null
sourceIds.asReversed().forEach { sourceId ->
try {
engine.asset.removeSource(sourceId)
} catch (throwable: Throwable) {
val previousFailure = cleanupFailure
if (previousFailure == null) {
cleanupFailure = throwable
} else {
previousFailure.addSuppressed(throwable)
}
}
}
cleanupFailure?.let { throw it }
}
private class BrandedAssetSource(
sourceId: String,
) : AssetSource(sourceId = sourceId) {
override val supportedMimeTypes = listOf(MimeType.JPEG.key)
override val credits = AssetCredits(
name = "IMG.LY",
uri = Uri.parse("https://img.ly/"),
)
override val license = AssetLicense(
name = "Sample content",
uri = Uri.parse("https://img.ly/legal/"),
)
override suspend fun getGroups(): List? = brandedAssets.flatMap { it.groups.orEmpty() }.distinct()
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult {
val searchQuery = query.query
val queryGroups = query.groups.orEmpty()
val filteredAssets = brandedAssets.filter { asset ->
val matchesQuery =
searchQuery.isNullOrBlank() ||
buildList {
asset.label?.let(::add)
addAll(asset.tags.orEmpty())
}.any { value ->
value.contains(searchQuery, ignoreCase = true)
}
val matchesGroups =
queryGroups.isEmpty() ||
asset.groups.orEmpty().any(queryGroups::contains)
matchesQuery && matchesGroups
}
val startIndex = query.page * query.perPage
val pageAssets = filteredAssets.drop(startIndex).take(query.perPage)
val nextPage =
if (startIndex + pageAssets.size < filteredAssets.size) {
query.page + 1
} else {
-1
}
return FindAssetsResult(
assets = pageAssets,
currentPage = query.page,
nextPage = nextPage,
total = filteredAssets.size,
)
}
override suspend fun fetchAsset(
id: String,
options: FetchAssetOptions,
): Asset? = brandedAssets.firstOrNull { it.id == id }
private val brandedAssets = listOf(
Asset(
id = "imgly-logo",
context = AssetContext(sourceId = sourceId),
label = "IMG.LY Logo",
locale = "en",
tags = listOf("logo", "brand", "header"),
groups = listOf("logos"),
meta = mapOf(
"uri" to "https://img.ly/static/ubq_samples/imgly_logo.jpg",
"thumbUri" to "https://img.ly/static/ubq_samples/imgly_logo.jpg",
"mimeType" to MimeType.JPEG.key,
"kind" to "image",
"blockType" to DesignBlockType.Graphic.key,
"fillType" to FillType.Image.key,
"shapeType" to ShapeType.Rect.key,
"width" to "640",
"height" to "320",
),
),
Asset(
id = "brand-background",
context = AssetContext(sourceId = sourceId),
label = "Brand Background",
locale = "en",
tags = listOf("background", "brand", "hero"),
groups = listOf("backgrounds"),
meta = mapOf(
"uri" to "https://img.ly/static/ubq_samples/sample_4.jpg",
"thumbUri" to "https://img.ly/static/ubq_samples/sample_4.jpg",
"mimeType" to MimeType.JPEG.key,
"kind" to "image",
"blockType" to DesignBlockType.Graphic.key,
"fillType" to FillType.Image.key,
"shapeType" to ShapeType.Rect.key,
"width" to "1080",
"height" to "720",
),
),
)
}
```
Understand the asset system on Android, including how CE.SDK models asset data, exposes assets through sources, and turns
those assets into blocks in a scene.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-concepts-assets)
Images, videos, audio, fonts, stickers, and templates are all *assets* in CE.SDK. The Android engine gets access to them
through *asset sources*. When you apply an asset, CE.SDK creates or updates a block so that the asset becomes visible in the
scene.
This guide covers the core concepts of the asset system. For a concrete media workflow, see the [Images](https://img.ly/docs/cesdk/android/insert-media/images-63848a/)
guide. For related concepts, see [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) and [Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/).
## Assets vs Blocks
**Assets** are content definitions with metadata such as URIs, dimensions, tags, and grouping information. They exist outside
the scene tree. **Blocks** are the visual elements in the scene that render or reference that content.
When you apply an asset, CE.SDK creates a block configured from the asset metadata or updates an existing block with new asset
data. Multiple blocks can reuse the same asset definition, and an asset can exist in a source without being used in the scene yet.
## The Asset Data Model
On Android, `findAssets()` returns `Asset` objects and local sources accept `AssetDefinition` objects. They share the same core
ideas: IDs, localized labels, tags, groups, structured payload data, and `meta` entries that describe how the asset should be
handled.
```kotlin highlight-android-concepts-assets-asset-definition
Asset(
id = "imgly-logo",
context = AssetContext(sourceId = sourceId),
label = "IMG.LY Logo",
locale = "en",
tags = listOf("logo", "brand", "header"),
groups = listOf("logos"),
meta = mapOf(
"uri" to "https://img.ly/static/ubq_samples/imgly_logo.jpg",
"thumbUri" to "https://img.ly/static/ubq_samples/imgly_logo.jpg",
"mimeType" to MimeType.JPEG.key,
"kind" to "image",
"blockType" to DesignBlockType.Graphic.key,
"fillType" to FillType.Image.key,
"shapeType" to ShapeType.Rect.key,
"width" to "640",
"height" to "320",
),
),
```
Key properties include:
- `id` for the stable asset identifier.
- `context` for the `sourceId` that produced the asset.
- `label` and `locale` for localized display text.
- `tags` and `groups` for search and filtering.
- `meta` for content-specific fields such as `uri`, `thumbUri`, `mimeType`, `blockType`, `fillType`, `shapeType`, `width`, and `height`.
- `payload` for structured values such as colors, typefaces, source sets, or transform presets when plain string metadata is not enough.
> **Note:** When you load a JSON-backed local source or add assets programmatically, the same metadata keys show up in your asset catalog
> definitions.
## Asset Sources
Asset sources provide assets to the editor and the engine APIs. On Android, a custom source subclasses `AssetSource` and
implements at least `findAssets(query)` and `getGroups()`.
```kotlin highlight-android-concepts-assets-asset-source
override suspend fun getGroups(): List? = brandedAssets.flatMap { it.groups.orEmpty() }.distinct()
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult {
val searchQuery = query.query
val queryGroups = query.groups.orEmpty()
val filteredAssets = brandedAssets.filter { asset ->
val matchesQuery =
searchQuery.isNullOrBlank() ||
buildList {
asset.label?.let(::add)
addAll(asset.tags.orEmpty())
}.any { value ->
value.contains(searchQuery, ignoreCase = true)
}
val matchesGroups =
queryGroups.isEmpty() ||
asset.groups.orEmpty().any(queryGroups::contains)
matchesQuery && matchesGroups
}
val startIndex = query.page * query.perPage
val pageAssets = filteredAssets.drop(startIndex).take(query.perPage)
val nextPage =
if (startIndex + pageAssets.size < filteredAssets.size) {
query.page + 1
} else {
-1
}
return FindAssetsResult(
assets = pageAssets,
currentPage = query.page,
nextPage = nextPage,
total = filteredAssets.size,
)
}
override suspend fun fetchAsset(
id: String,
options: FetchAssetOptions,
): Asset? = brandedAssets.firstOrNull { it.id == id }
```
The `FindAssetsQuery` object contains paging, text search, sorting, tag, and group filters, plus the structured `filter`
predicates and the requested `facets` paths. Your source responds with a `FindAssetsResult` that contains the assets for the
requested page, the total match count, `nextPage`, which is `-1` when there are no more results, and a `facets` map when the
query requests distributions.
Sources can also expose `supportedMimeTypes`, `credits`, `license`, `fetchAsset()`, and custom `applyAsset()` behavior when you
need more than the default block creation logic.
## Querying Assets
Use `engine.asset.findAssets()` to search a source. Android pages are zero-based, so the first request uses `page = 0`.
```kotlin highlight-android-concepts-assets-query-assets
val queriedAssets = engine.asset.findAssets(
sourceId = source.sourceId,
query = FindAssetsQuery(
perPage = 10,
page = 0,
query = "logo",
groups = listOf("logos"),
),
)
val queriedAsset = queriedAssets.assets.first()
val groups = engine.asset.getGroups(sourceId = source.sourceId)
println("Found ${queriedAssets.total} assets in groups $groups")
```
This is the point where you typically combine free-text search with `groups`, `tags`, or sorting. You can also call
`engine.asset.getGroups()` to inspect the filters that a source exposes before you build your own asset browser UI.
The optional `filter` parameter narrows a query with structured `AssetFilter` predicates (`Equals` and `Contains` on a property
path, combined with `And`, `Or`, and `Not`). The optional `facets` parameter requests value distributions for `tags`, `groups`,
or `meta.` paths over the matched set—for example to populate a filter dropdown. Each distribution is ordered by count
descending and returned in `FindAssetsResult.facets`; combine `facets` with `perPage = 0` to enumerate available values without
fetching assets.
## Applying Assets
Use `engine.asset.applyAssetSourceAsset()` when you want the source's custom apply behavior. If the source does not override
`applyAsset()`, CE.SDK falls back to `defaultApplyAsset()` and creates a block from the asset's `meta` fields.
```kotlin highlight-android-concepts-assets-apply-asset
val appliedBlock = engine.asset.applyAssetSourceAsset(
sourceId = source.sourceId,
asset = queriedAsset,
)
if (appliedBlock != null) {
engine.block.setPositionX(appliedBlock, 64F)
engine.block.setPositionY(appliedBlock, 64F)
}
```
That block can then be positioned, resized, or otherwise modified through the regular block APIs.
## Local Asset Sources
Local asset sources keep their assets in memory and are ideal for uploads, generated media, or app-specific catalogs that you
construct at runtime.
```kotlin highlight-android-concepts-assets-local-source
engine.asset.addLocalSource(
sourceId = localSourceId,
supportedMimeTypes = listOf(MimeType.JPEG.key),
)
registeredGuideSourceIds += localSourceId
val localAsset = AssetDefinition(
id = "sunrise-poster",
label = mapOf("en" to "Sunrise Poster"),
tags = mapOf("en" to listOf("poster", "sunrise", "brand")),
groups = listOf("posters"),
meta = mapOf(
"uri" to "https://img.ly/static/ubq_samples/sample_1.jpg",
"thumbUri" to "https://img.ly/static/ubq_samples/sample_1.jpg",
"mimeType" to MimeType.JPEG.key,
"kind" to "image",
"blockType" to DesignBlockType.Graphic.key,
"fillType" to FillType.Image.key,
"shapeType" to ShapeType.Rect.key,
"width" to "1080",
"height" to "1080",
),
)
engine.asset.addAsset(sourceId = localSourceId, asset = localAsset)
engine.asset.assetSourceContentsChanged(sourceId = localSourceId)
```
`AssetDefinition` uses localized `label` and `tags` maps, while `meta` carries the URI, MIME type, and block creation hints that
`defaultApplyAsset()` needs later on.
## Source Events
The asset API exposes `Flow` streams for source lifecycle changes. These are useful when your UI needs to refresh its
filters or grid contents after sources are added, removed, or updated.
```kotlin highlight-android-concepts-assets-source-events
sourceEventJobs += engine.asset.onAssetSourceAdded()
.onEach { println("Asset source added: $it") }
.launchIn(this)
sourceEventJobs += engine.asset.onAssetSourceRemoved()
.onEach { println("Asset source removed: $it") }
.launchIn(this)
sourceEventJobs += engine.asset.onAssetSourceUpdated()
.onEach { println("Asset source updated: $it") }
.launchIn(this)
```
After mutating a source, call `engine.asset.assetSourceContentsChanged(sourceId)` so subscribers know they should re-query the
source.
## Next Steps
- [Basics](https://img.ly/docs/cesdk/android/import-media/asset-panel/basics-f29078/) - Explore how the asset library connects sources, categories, and dock buttons
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Learn about design blocks that display assets
- [Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/) - Understand how CE.SDK loads external files
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Blocks"
description: "Learn how blocks define elements in a scene and how to structure them for rendering in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/blocks-90241e/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/)
---
```kotlin file=@cesdk_android_examples/engine-guides-concepts-blocks/ConceptsBlocks.kt reference-only
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.HorizontalAlignment
import ly.img.engine.ShapeType
suspend fun conceptsBlocks(engine: Engine) = withContext(engine.dispatcher) {
var selectionObserver: Job? = null
var stateObserver: Job? = null
try {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
engine.scene.zoomToBlock(
page,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
val pages = engine.block.findByType(DesignBlockType.Page)
val firstPage = pages.first()
val pageType = engine.block.getType(firstPage)
println("Page block type: $pageType")
engine.block.setKind(firstPage, kind = "main-canvas")
val pageKind = engine.block.getKind(firstPage)
println("Page kind: $pageKind")
val mainCanvasBlocks = engine.block.findByKind("main-canvas")
println("Blocks with kind 'main-canvas': ${mainCanvasBlocks.size}")
val graphic = engine.block.create(DesignBlockType.Graphic)
val graphicCopy = engine.block.duplicate(graphic)
engine.block.destroy(graphicCopy)
val isOriginalValid = engine.block.isValid(graphic)
val isCopyValid = engine.block.isValid(graphicCopy)
println("Original valid: $isOriginalValid")
println("Copy valid after destroy: $isCopyValid")
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(graphic, shape = rectShape)
engine.block.setPositionX(graphic, value = 200F)
engine.block.setPositionY(graphic, value = 100F)
engine.block.setWidth(graphic, value = 400F)
engine.block.setHeight(graphic, value = 300F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(graphic, fill = imageFill)
engine.block.setContentFillMode(graphic, ContentFillMode.COVER)
engine.block.appendChild(parent = page, child = graphic)
val graphicParent = engine.block.getParent(graphic)
println("Graphic parent is page: ${graphicParent == page}")
val pageChildren = engine.block.getChildren(page)
println("Page has children: ${pageChildren.size}")
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.setPositionX(textBlock, value = 200F)
engine.block.setPositionY(textBlock, value = 450F)
engine.block.setWidth(textBlock, value = 400F)
engine.block.setHeight(textBlock, value = 80F)
engine.block.setString(
block = textBlock,
property = "text/text",
value = "Blocks are the building units of CE.SDK designs",
)
engine.block.setTextFontSize(textBlock, fontSize = 24F)
engine.block.setTextHorizontalAlignment(textBlock, alignment = HorizontalAlignment.Center)
val textType = engine.block.getType(textBlock)
println("Text block type: $textType")
val graphicProperties = engine.block.findAllProperties(graphic)
println("Graphic block has ${graphicProperties.size} properties")
val opacityType = engine.block.getPropertyType("opacity")
println("Opacity property type: $opacityType")
val isOpacityReadable = engine.block.isPropertyReadable("opacity")
val isOpacityWritable = engine.block.isPropertyWritable("opacity")
println("Opacity readable: $isOpacityReadable writable: $isOpacityWritable")
engine.block.setFloat(block = graphic, property = "opacity", value = 0.9F)
val opacity = engine.block.getFloat(block = graphic, property = "opacity")
println("Graphic opacity: $opacity")
engine.block.setBoolean(block = page, property = "page/marginEnabled", value = false)
val marginEnabled = engine.block.getBoolean(block = page, property = "page/marginEnabled")
println("Page margin enabled: $marginEnabled")
val blendModes = engine.block.getEnumValues("blend/mode")
println("Available blend modes: ${blendModes.take(3).joinToString()} ...")
engine.block.setEnum(block = graphic, property = "blend/mode", value = "Multiply")
val blendMode = engine.block.getEnum(block = graphic, property = "blend/mode")
println("Graphic blend mode: $blendMode")
val graphicUUID = engine.block.getUUID(graphic)
println("Graphic UUID: $graphicUUID")
engine.block.setName(graphic, name = "Hero Image")
engine.block.setName(textBlock, name = "Caption")
val graphicName = engine.block.getName(graphic)
println("Graphic name: $graphicName")
val namedBlocks = engine.block.findByName("Hero Image")
println("Blocks named Hero Image: ${namedBlocks.size}")
selectionObserver = launch {
engine.block.onSelectionChanged().collect {
val selected = engine.block.findAllSelected()
println("Selection changed, now selected: ${selected.size} blocks")
}
}
engine.block.select(graphic)
val isGraphicSelected = engine.block.isSelected(graphic)
println("Graphic is selected: $isGraphicSelected")
engine.block.setSelected(textBlock, selected = true)
val selectedBlocks = engine.block.findAllSelected()
println("Selected blocks count: ${selectedBlocks.size}")
engine.block.setVisible(graphic, visible = true)
val isVisible = engine.block.isVisible(graphic)
println("Graphic is visible: $isVisible")
engine.block.setIncludedInExport(graphic, enabled = true)
val inExport = engine.block.isIncludedInExport(graphic)
println("Graphic included in export: $inExport")
engine.block.setClipped(graphic, clipped = false)
val isClipped = engine.block.isClipped(graphic)
println("Graphic is clipped: $isClipped")
val graphicState = engine.block.getState(graphic)
println("Graphic state: $graphicState")
stateObserver = launch {
engine.block.onStateChanged(listOf(graphic)).collect { changedBlocks ->
changedBlocks.forEach { changedBlock ->
val state = engine.block.getState(changedBlock)
println("Block $changedBlock state changed to: $state")
}
}
}
val savedString = engine.block.saveToString(blocks = listOf(graphic, textBlock))
println("Blocks saved to string, length: ${savedString.length}")
// Alternatively, blocks can be saved with their assets in an archive:
// val savedArchive = engine.block.saveToArchive(blocks = listOf(graphic, textBlock))
val loadedBlocks = engine.block.loadFromString(savedString)
println("Loaded blocks from string: ${loadedBlocks.size}")
// Alternatively, blocks can be loaded from an archive or an extracted archive directory:
// val loadedArchiveBlocks = engine.block.loadFromArchive(Uri.parse("file:///path/to/blocks.zip"))
// val loadedUrlBlocks = engine.block.loadFromURL(Uri.parse("file:///path/to/blocks.blocks"))
loadedBlocks.forEach { loadedBlock ->
engine.block.destroy(loadedBlock)
}
engine.block.forceLoadResources(listOf(graphic, textBlock))
println("Blocks guide initialized successfully.")
println("Created graphic and text blocks, then exercised hierarchy and state APIs.")
} finally {
selectionObserver?.cancel()
stateObserver?.cancel()
}
}
```
Work with blocks, the fundamental building units for all visual elements in
CE.SDK designs.
> **Reading time:** 15 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-concepts-blocks)
Every visual element in CE.SDK, including images, text, shapes, and audio, is represented as a block. Blocks are organized in a tree structure within scenes and pages, where parent-child relationships determine rendering order and visibility. Each block has properties you can read and modify, a `Type` that defines its core behavior, and an optional `Kind` for custom categorization.
This guide focuses on engine APIs. The standalone sample creates a small offscreen scene so the snippets can run in isolation; the highlighted code is the block logic you would apply to your own scene or editor workflow.
## Block Types
CE.SDK provides several block types, each designed for specific content:
- **`DesignBlockType.Graphic`** (`//ly.img.ubq/graphic`): Visual blocks for images, shapes, and graphics
- **`DesignBlockType.Text`** (`//ly.img.ubq/text`): Text content with typography controls
- **`DesignBlockType.Audio`** (`//ly.img.ubq/audio`): Audio content for video scenes
- **`DesignBlockType.Page`** (`//ly.img.ubq/page`): Container blocks representing canvases or artboards
- **`DesignBlockType.Cutout`** (`//ly.img.ubq/cutout`): Blocks for masking operations
Query a block's type using `getType()` and find blocks of a specific type with `findByType()`:
```kotlin highlight-android-block-types
val pages = engine.block.findByType(DesignBlockType.Page)
val firstPage = pages.first()
val pageType = engine.block.getType(firstPage)
println("Page block type: $pageType")
```
Block types are immutable. Once created, a block's type cannot change. This is what distinguishes `Type` from `Kind`.
## Type vs Kind
Type and kind serve different purposes. The **type** is determined at creation and defines core behavior. The **kind** is a custom string label you assign for application-specific categorization.
```kotlin highlight-android-type-vs-kind
engine.block.setKind(firstPage, kind = "main-canvas")
val pageKind = engine.block.getKind(firstPage)
println("Page kind: $pageKind")
val mainCanvasBlocks = engine.block.findByKind("main-canvas")
println("Blocks with kind 'main-canvas': ${mainCanvasBlocks.size}")
```
Use kind to tag blocks for your application's logic. Set it with `setKind()`, query it with `getKind()`, and find blocks by kind with `findByKind()`.
## Block Hierarchy
Blocks form a tree structure where scenes contain pages, and pages contain design elements.
```kotlin highlight-android-block-hierarchy
engine.block.appendChild(parent = page, child = graphic)
val graphicParent = engine.block.getParent(graphic)
println("Graphic parent is page: ${graphicParent == page}")
val pageChildren = engine.block.getChildren(page)
println("Page has children: ${pageChildren.size}")
```
Only blocks that are direct or indirect children of a page block are rendered. A scene without any page children will not show content in the editor or in offscreen exports. Use `appendChild()` to attach blocks, `getParent()` to inspect the hierarchy, and `getChildren()` to read a block's render-order children.
## Block Lifecycle
Create new blocks with `create()`, duplicate existing blocks with `duplicate()`, and remove blocks with `destroy()`. After destroying a block, `isValid()` returns `false` for that block handle.
```kotlin highlight-android-block-lifecycle
val graphic = engine.block.create(DesignBlockType.Graphic)
val graphicCopy = engine.block.duplicate(graphic)
engine.block.destroy(graphicCopy)
val isOriginalValid = engine.block.isValid(graphic)
val isCopyValid = engine.block.isValid(graphicCopy)
println("Original valid: $isOriginalValid")
println("Copy valid after destroy: $isCopyValid")
```
When duplicating a block, all children are included, and the duplicate receives a new UUID.
## Working with Shapes
Graphic blocks need a shape before they can render. Create a shape, attach it to the graphic block, and then size or position the graphic block in the scene.
```kotlin highlight-android-shape
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(graphic, shape = rectShape)
engine.block.setPositionX(graphic, value = 200F)
engine.block.setPositionY(graphic, value = 100F)
engine.block.setWidth(graphic, value = 400F)
engine.block.setHeight(graphic, value = 300F)
```
## Working with Fills
Graphic blocks display content through fills. After a graphic block has a shape, create a fill, attach it to the block, and configure its source.
```kotlin highlight-android-fill
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(graphic, fill = imageFill)
engine.block.setContentFillMode(graphic, ContentFillMode.COVER)
```
CE.SDK supports several fill types including image, video, color, and gradient fills. See the [Fills guide](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) for details on available fill types.
## Creating Text Blocks
Text blocks display formatted text content. Create a text block, position it, and set its content and styling.
```kotlin highlight-android-text-block
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.setPositionX(textBlock, value = 200F)
engine.block.setPositionY(textBlock, value = 450F)
engine.block.setWidth(textBlock, value = 400F)
engine.block.setHeight(textBlock, value = 80F)
engine.block.setString(
block = textBlock,
property = "text/text",
value = "Blocks are the building units of CE.SDK designs",
)
engine.block.setTextFontSize(textBlock, fontSize = 24F)
engine.block.setTextHorizontalAlignment(textBlock, alignment = HorizontalAlignment.Center)
val textType = engine.block.getType(textBlock)
println("Text block type: $textType")
```
Text blocks support extensive typography controls covered in the [Text guides](https://img.ly/docs/cesdk/android/text-8a993a/).
## Block Properties
The reflection system lets you discover and manipulate any block property dynamically. Use `findAllProperties()` to get all available properties for a block. Property names are prefixed by category, for example `shape/star/points` or `text/fontSize`.
```kotlin highlight-android-block-properties
val graphicProperties = engine.block.findAllProperties(graphic)
println("Graphic block has ${graphicProperties.size} properties")
val opacityType = engine.block.getPropertyType("opacity")
println("Opacity property type: $opacityType")
val isOpacityReadable = engine.block.isPropertyReadable("opacity")
val isOpacityWritable = engine.block.isPropertyWritable("opacity")
println("Opacity readable: $isOpacityReadable writable: $isOpacityWritable")
```
Query property types with `getPropertyType()`. Android returns `PropertyType` enum values such as `BOOL`, `INT`, `FLOAT`, `DOUBLE`, `STRING`, `COLOR`, `ENUM`, `STRUCT`, and `SOURCESET`. For enum properties, use `getEnumValues()` to get the allowed string values.
### Property Accessors
Use type-specific getters and setters that match the property's `PropertyType`:
```kotlin highlight-android-property-accessors
engine.block.setFloat(block = graphic, property = "opacity", value = 0.9F)
val opacity = engine.block.getFloat(block = graphic, property = "opacity")
println("Graphic opacity: $opacity")
engine.block.setBoolean(block = page, property = "page/marginEnabled", value = false)
val marginEnabled = engine.block.getBoolean(block = page, property = "page/marginEnabled")
println("Page margin enabled: $marginEnabled")
val blendModes = engine.block.getEnumValues("blend/mode")
println("Available blend modes: ${blendModes.take(3).joinToString()} ...")
engine.block.setEnum(block = graphic, property = "blend/mode", value = "Multiply")
val blendMode = engine.block.getEnum(block = graphic, property = "blend/mode")
println("Graphic blend mode: $blendMode")
```
Using the wrong accessor type causes an error. Check `getPropertyType()` first if you are not sure which accessor to use, and use `isPropertyReadable()` or `isPropertyWritable()` before building generic editors.
## UUID, Names, and Identity
Each block has a UUID and an optional mutable name for organization.
```kotlin highlight-android-uuid-identity
val graphicUUID = engine.block.getUUID(graphic)
println("Graphic UUID: $graphicUUID")
engine.block.setName(graphic, name = "Hero Image")
engine.block.setName(textBlock, name = "Caption")
val graphicName = engine.block.getName(graphic)
println("Graphic name: $graphicName")
val namedBlocks = engine.block.findByName("Hero Image")
println("Blocks named Hero Image: ${namedBlocks.size}")
```
Use `getUUID()` when you need a stable identifier for a block while it exists in the current scene. Android's `loadFromString()`, `loadFromArchive()`, and `loadFromURL()` APIs create new block instances with new UUIDs, so keep your own mapping if you need to reconcile originals with loaded copies.
Use `findByName()` when you need to look up blocks by an application-assigned name, for example after naming imported template elements.
## Selection
Control which blocks are selected programmatically. Use `select()` to select a single block and deselect others, or `setSelected()` to change one block's selection state without clearing the rest.
```kotlin highlight-android-selection
selectionObserver = launch {
engine.block.onSelectionChanged().collect {
val selected = engine.block.findAllSelected()
println("Selection changed, now selected: ${selected.size} blocks")
}
}
engine.block.select(graphic)
val isGraphicSelected = engine.block.isSelected(graphic)
println("Graphic is selected: $isGraphicSelected")
engine.block.setSelected(textBlock, selected = true)
val selectedBlocks = engine.block.findAllSelected()
println("Selected blocks count: ${selectedBlocks.size}")
```
Subscribe to selection changes with `onSelectionChanged()`, which returns a `Flow` you collect from a coroutine that stays alive while you need selection updates.
## Visibility
Control whether blocks appear on the canvas and whether they are included in exports.
```kotlin highlight-android-visibility
engine.block.setVisible(graphic, visible = true)
val isVisible = engine.block.isVisible(graphic)
println("Graphic is visible: $isVisible")
engine.block.setIncludedInExport(graphic, enabled = true)
val inExport = engine.block.isIncludedInExport(graphic)
println("Graphic included in export: $inExport")
```
A block with `isVisible()` returning `true` may still not appear if it has not been attached to a parent, its parent is hidden, or another block obscures it.
### Clipping
Clipping determines whether a block's content is constrained to its own bounds.
```kotlin highlight-android-clipping
engine.block.setClipped(graphic, clipped = false)
val isClipped = engine.block.isClipped(graphic)
println("Graphic is clipped: $isClipped")
```
When clipping is enabled, content outside the block's frame is hidden. When clipping is disabled, the content can render beyond the block's bounds.
## Block State
Blocks track loading progress and error conditions through a state system with three possible states:
- `BlockState.Ready`: Normal state, no pending operations
- `BlockState.Pending(progress)`: Operation in progress with a progress value in the range `0..1`
- `BlockState.Error(type)`: Operation failed with `AUDIO_DECODING`, `IMAGE_DECODING`, `FILE_FETCH`, `VIDEO_DECODING`, or `UNKNOWN`
```kotlin highlight-android-block-state
val graphicState = engine.block.getState(graphic)
println("Graphic state: $graphicState")
stateObserver = launch {
engine.block.onStateChanged(listOf(graphic)).collect { changedBlocks ->
changedBlocks.forEach { changedBlock ->
val state = engine.block.getState(changedBlock)
println("Block $changedBlock state changed to: $state")
}
}
}
```
Subscribe to state changes with `onStateChanged(listOf(block))` when you want to drive loading indicators or error UI from asynchronous resource loading.
## Serialization
Save blocks to strings for persistence and restore them later.
```kotlin highlight-android-serialization
val savedString = engine.block.saveToString(blocks = listOf(graphic, textBlock))
println("Blocks saved to string, length: ${savedString.length}")
// Alternatively, blocks can be saved with their assets in an archive:
// val savedArchive = engine.block.saveToArchive(blocks = listOf(graphic, textBlock))
val loadedBlocks = engine.block.loadFromString(savedString)
println("Loaded blocks from string: ${loadedBlocks.size}")
// Alternatively, blocks can be loaded from an archive or an extracted archive directory:
// val loadedArchiveBlocks = engine.block.loadFromArchive(Uri.parse("file:///path/to/blocks.zip"))
// val loadedUrlBlocks = engine.block.loadFromURL(Uri.parse("file:///path/to/blocks.blocks"))
loadedBlocks.forEach { loadedBlock ->
engine.block.destroy(loadedBlock)
}
```
Use `saveToString()` for lightweight serialization or `saveToArchive()` to include referenced assets. Load archived data back with `loadFromArchive()` or point `loadFromURL()` at a `blocks.blocks` file inside an extracted archive directory.
Loaded blocks are not attached to the scene automatically. Parent them with `appendChild()` if you want them to render.
## Troubleshooting
- Block is not visible: ensure it is appended to a page, and that the page is appended to the scene.
- Property writes fail: verify the property name with `findAllProperties()` and the accessor with `getPropertyType()`.
- Selection or state callbacks never fire: collect the returned `Flow` from a coroutine that stays active while the observer is needed.
- Loaded blocks do not appear after deserialization: append them back into the scene hierarchy after `loadFromString()`, `loadFromArchive()`, or `loadFromURL()`.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Buffers"
description: "Use buffers to store temporary, non-serializable data in CE.SDK via the CreativeEngine API."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/)
---
```kotlin file=@cesdk_android_examples/engine-guides-buffers/Buffers.kt reference-only
import android.net.Uri
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.PI
import kotlin.math.sin
suspend fun buffers(engine: Engine) = withContext(engine.dispatcher) {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1920F)
engine.block.setDuration(page, duration = 2.0)
val bufferUri = engine.editor.createBuffer()
try {
val sampleRate = 44_100
val durationSeconds = 2
val frequencyHz = 440.0
val numChannels = 2
val samplesPerChannel = sampleRate * durationSeconds
val sampleCount = samplesPerChannel * numChannels
val samples = FloatArray(sampleCount)
for (sampleIndex in 0 until samplesPerChannel) {
val time = sampleIndex / sampleRate.toDouble()
val sampleValue = (sin(2 * PI * frequencyHz * time) * 0.5).toFloat()
val bufferIndex = sampleIndex * numChannels
samples[bufferIndex] = sampleValue
samples[bufferIndex + 1] = sampleValue
}
val bytesPerSample = 2
val wavDataSize = sampleCount * bytesPerSample
val wavFileSize = 44 + wavDataSize
val wavData = ByteBuffer.allocateDirect(wavFileSize).order(ByteOrder.LITTLE_ENDIAN)
"RIFF".forEach { wavData.put(it.code.toByte()) }
wavData.putInt(wavFileSize - 8)
"WAVE".forEach { wavData.put(it.code.toByte()) }
"fmt ".forEach { wavData.put(it.code.toByte()) }
wavData.putInt(16)
wavData.putShort(1.toShort())
wavData.putShort(numChannels.toShort())
wavData.putInt(sampleRate)
wavData.putInt(sampleRate * numChannels * bytesPerSample)
wavData.putShort((numChannels * bytesPerSample).toShort())
wavData.putShort((bytesPerSample * 8).toShort())
"data".forEach { wavData.put(it.code.toByte()) }
wavData.putInt(wavDataSize)
for (sample in samples) {
val clampedSample = sample.coerceIn(-1F, 1F)
val pcmScale = if (clampedSample < 0F) 32768F else Short.MAX_VALUE.toFloat()
val pcmSample = clampedSample * pcmScale
wavData.putShort(pcmSample.toInt().toShort())
}
wavData.flip()
engine.editor.setBufferData(uri = bufferUri, offset = 0, data = wavData)
val header = engine.editor.getBufferData(uri = bufferUri, offset = 0, length = 44)
val riff = buildString {
repeat(4) { append(header.get().toInt().toChar()) }
}
check(riff == "RIFF")
val bufferLength = engine.editor.getBufferLength(uri = bufferUri)
check(bufferLength == wavFileSize)
val demoBuffer = engine.editor.createBuffer()
try {
val demoData =
ByteBuffer.allocateDirect(8).apply {
for (value in 1..8) put(value.toByte())
flip()
}
engine.editor.setBufferData(uri = demoBuffer, offset = 0, data = demoData)
engine.editor.setBufferLength(uri = demoBuffer, length = 4)
check(engine.editor.getBufferLength(uri = demoBuffer) == 4)
} finally {
engine.editor.destroyBuffer(uri = demoBuffer)
}
val audioBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(block = audioBlock, property = "audio/fileURI", value = bufferUri)
engine.block.setDuration(audioBlock, duration = durationSeconds.toDouble())
engine.block.appendChild(parent = page, child = audioBlock)
engine.block.forceLoadAVResource(audioBlock)
val transientResources = engine.editor.findAllTransientResources()
check(transientResources.any { (uri, size) -> uri == bufferUri && size == bufferLength })
val relocatedUri = Uri.parse("https://cdn.example.com/audio/generated-tone.wav")
val persistedData = engine.editor.getBufferData(uri = bufferUri, offset = 0, length = bufferLength)
check(persistedData.remaining() == bufferLength)
engine.editor.relocateResource(currentUri = bufferUri, relocatedUri = relocatedUri)
check(engine.block.getUri(block = audioBlock, property = "audio/fileURI") == relocatedUri)
} finally {
engine.block.findByType(DesignBlockType.Audio)
.filter(engine.block::isValid)
.filter { audioBlock ->
engine.block.getUri(audioBlock, property = "audio/fileURI") == bufferUri
}
.forEach(engine.block::destroy)
engine.editor.destroyBuffer(uri = bufferUri)
}
}
```
Store and manage temporary binary data directly in memory using CE.SDK's buffer API for dynamically generated content like procedural audio or streaming media.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-buffers)
Buffers are in-memory containers referenced via `buffer://` Uris. Unlike external files that require network or file I/O, buffers live only inside the current engine session. This makes them useful for generated audio, real-time image data, or any content you want to pass to blocks without writing it to disk first.
This guide covers how to create and destroy buffers, write and read bytes with Android's direct `ByteBuffer` API, assign a buffer to an audio block, and relocate transient resources before saving or exporting a scene.
## Setting Up a Video Scene
Audio blocks need a video scene and a page with a duration. The example creates a two-second 1080 x 1920 page so the generated audio has a timeline context.
```kotlin highlight-android-setup-video-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1920F)
engine.block.setDuration(page, duration = 2.0)
```
## Creating and Managing Buffers
Use `engine.editor.createBuffer()` to allocate a buffer and get back its `buffer://` Uri. Buffers stay in memory until you explicitly destroy them with `engine.editor.destroyBuffer()` or stop the engine.
```kotlin highlight-android-create-buffer
val bufferUri = engine.editor.createBuffer()
```
## Writing Data to Buffers
On Android, `engine.editor.setBufferData()` requires a direct `ByteBuffer`, so the example builds the payload in `ByteBuffer.allocateDirect(...)`. It generates a 440 Hz stereo tone at 44.1 kHz for two seconds and wraps the samples in a WAV header so the audio block can load the buffer as a normal audio resource.
```kotlin highlight-android-generate-samples
val sampleRate = 44_100
val durationSeconds = 2
val frequencyHz = 440.0
val numChannels = 2
val samplesPerChannel = sampleRate * durationSeconds
val sampleCount = samplesPerChannel * numChannels
val samples = FloatArray(sampleCount)
for (sampleIndex in 0 until samplesPerChannel) {
val time = sampleIndex / sampleRate.toDouble()
val sampleValue = (sin(2 * PI * frequencyHz * time) * 0.5).toFloat()
val bufferIndex = sampleIndex * numChannels
samples[bufferIndex] = sampleValue
samples[bufferIndex + 1] = sampleValue
}
```
```kotlin highlight-android-write-buffer
val bytesPerSample = 2
val wavDataSize = sampleCount * bytesPerSample
val wavFileSize = 44 + wavDataSize
val wavData = ByteBuffer.allocateDirect(wavFileSize).order(ByteOrder.LITTLE_ENDIAN)
"RIFF".forEach { wavData.put(it.code.toByte()) }
wavData.putInt(wavFileSize - 8)
"WAVE".forEach { wavData.put(it.code.toByte()) }
"fmt ".forEach { wavData.put(it.code.toByte()) }
wavData.putInt(16)
wavData.putShort(1.toShort())
wavData.putShort(numChannels.toShort())
wavData.putInt(sampleRate)
wavData.putInt(sampleRate * numChannels * bytesPerSample)
wavData.putShort((numChannels * bytesPerSample).toShort())
wavData.putShort((bytesPerSample * 8).toShort())
"data".forEach { wavData.put(it.code.toByte()) }
wavData.putInt(wavDataSize)
for (sample in samples) {
val clampedSample = sample.coerceIn(-1F, 1F)
val pcmScale = if (clampedSample < 0F) 32768F else Short.MAX_VALUE.toFloat()
val pcmSample = clampedSample * pcmScale
wavData.putShort(pcmSample.toInt().toShort())
}
wavData.flip()
engine.editor.setBufferData(uri = bufferUri, offset = 0, data = wavData)
```
The `offset` parameter is measured in bytes, which lets you append or overwrite specific regions of the buffer when you stream or update data incrementally.
## Reading Data from Buffers
Use `engine.editor.getBufferData()` to read any byte range back into another `ByteBuffer`. Here we read the first 44 bytes and verify the WAV `RIFF` header.
```kotlin highlight-android-read-buffer
val header = engine.editor.getBufferData(uri = bufferUri, offset = 0, length = 44)
val riff = buildString {
repeat(4) { append(header.get().toInt().toChar()) }
}
check(riff == "RIFF")
```
## Querying Buffer Length
Use `engine.editor.getBufferLength()` to check how many bytes are currently stored in the buffer. This is useful before full reads or before relocating the data elsewhere.
```kotlin highlight-android-get-buffer-length
val bufferLength = engine.editor.getBufferLength(uri = bufferUri)
check(bufferLength == wavFileSize)
```
## Resizing Buffers
You can grow or shrink a buffer with `engine.editor.setBufferLength()`. The example uses a separate demo buffer so the audio payload stays intact while we demonstrate truncation.
```kotlin highlight-android-resize-buffer
val demoBuffer = engine.editor.createBuffer()
try {
val demoData =
ByteBuffer.allocateDirect(8).apply {
for (value in 1..8) put(value.toByte())
flip()
}
engine.editor.setBufferData(uri = demoBuffer, offset = 0, data = demoData)
engine.editor.setBufferLength(uri = demoBuffer, length = 4)
check(engine.editor.getBufferLength(uri = demoBuffer) == 4)
} finally {
engine.editor.destroyBuffer(uri = demoBuffer)
}
```
Truncating a buffer permanently discards bytes beyond the new length, so read or copy the data first if you still need it.
## Assigning Buffers to Blocks
Buffer Uris work like any other resource Uri in CE.SDK. On Android, `engine.block.setUri()` is the most convenient way to assign them to Uri-valued properties such as `audio/fileURI`.
```kotlin highlight-android-assign-buffer-to-audio-block
val audioBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(block = audioBlock, property = "audio/fileURI", value = bufferUri)
engine.block.setDuration(audioBlock, duration = durationSeconds.toDouble())
engine.block.appendChild(parent = page, child = audioBlock)
engine.block.forceLoadAVResource(audioBlock)
```
After assigning the buffer Uri, `engine.block.forceLoadAVResource()` loads the audio resource metadata so the engine can resolve duration and playback data from the generated WAV bytes.
The same pattern works for other Uri properties:
- **Audio blocks**: `audio/fileURI`
- **Image fills**: `fill/image/imageFileURI`
- **Video fills**: `fill/video/fileURI`
## Transient Resources and Scene Serialization
Buffers are transient resources. The Uri may be serialized, but the bytes themselves are not persisted with the scene. Use `engine.editor.findAllTransientResources()` before export or save so you know which resources still need to be relocated.
```kotlin highlight-android-find-transient-resources
val transientResources = engine.editor.findAllTransientResources()
check(transientResources.any { (uri, size) -> uri == bufferUri && size == bufferLength })
```
> **Note:** **Limitations**Buffers are intended for temporary data only.* Buffer data is not part of [scene serialization](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/).
> * Changes to buffers cannot be undone with the [history system](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/).
## Persisting Buffer Data
To keep buffer content beyond the current session, read the bytes back out, upload them to persistent storage, then call `engine.editor.relocateResource()` so every block reference points at the new Uri.
```kotlin highlight-android-persist-buffer
val relocatedUri = Uri.parse("https://cdn.example.com/audio/generated-tone.wav")
val persistedData = engine.editor.getBufferData(uri = bufferUri, offset = 0, length = bufferLength)
check(persistedData.remaining() == bufferLength)
engine.editor.relocateResource(currentUri = bufferUri, relocatedUri = relocatedUri)
check(engine.block.getUri(block = audioBlock, property = "audio/fileURI") == relocatedUri)
```
The example uses a placeholder CDN URL to show the relocation step. In production, replace that with the URL returned by your own storage or upload pipeline.
## Troubleshooting
**Audio block does not load the buffer**
Make sure the buffer contains a valid audio file format such as WAV. Raw PCM bytes alone are not enough for `audio/fileURI`.
**`setBufferData()` throws on Android**
The `data` argument must be a direct `ByteBuffer`. Use `ByteBuffer.allocateDirect(...)` instead of `ByteArray` or a heap-backed buffer.
**Buffer data is missing after saving or exporting**
Buffers are transient. Find them with `findAllTransientResources()`, upload them to persistent storage, then relocate the scene references before serializing.
**Memory usage keeps growing**
Destroy buffers when they are no longer needed. They stay resident until you call `destroyBuffer()` or stop the engine.
## Next Steps
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Understand how scenes are structured and what gets serialized.
- [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) — Learn which editor changes participate in undo and redo.
- [Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/) — Explore how CE.SDK resolves, loads, and relocates resource Uris.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Design Units"
description: "Configure design units (pixels, millimeters, inches) and DPI settings for print-ready output in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/)
---
```kotlin file=@cesdk_android_examples/engine-guides-design-units/DesignUnits.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import kotlin.math.roundToInt
suspend fun designUnits(engine: Engine): DesignBlock = withContext(engine.dispatcher) {
val page = configureDesignUnits(engine)
engine.scene.zoomToBlock(page)
engine.block.forceLoadResources(listOf(page))
page
}
internal fun configureDesignUnits(engine: Engine): DesignBlock {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
// Get the current design unit. New scenes default to PIXEL.
val currentUnit = engine.scene.getDesignUnit()
println("Current design unit: $currentUnit") // PIXEL
// Switch to millimeters for a print workflow.
engine.scene.setDesignUnit(DesignUnit.MILLIMETER)
// Verify the change.
val newUnit = engine.scene.getDesignUnit()
println("Design unit changed to: $newUnit") // MILLIMETER
// Set DPI to 300 for print-quality exports.
engine.block.setFloat(scene, property = "scene/dpi", value = 300F)
// Read back the DPI value.
val dpi = engine.block.getFloat(scene, property = "scene/dpi")
println("DPI set to: $dpi") // 300.0
// Set the page to A4 dimensions (210 x 297 mm).
engine.block.setWidth(page, value = 210F)
engine.block.setHeight(page, value = 297F)
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
println("Page dimensions: ${pageWidth}mm x ${pageHeight}mm")
// Create a text block positioned and sized in millimeters.
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
// Position at 20 mm from left, 30 mm from top.
engine.block.setPositionX(textBlock, value = 20F)
engine.block.setPositionY(textBlock, value = 30F)
// Size: 170 mm wide, 50 mm tall.
engine.block.setWidth(textBlock, value = 170F)
engine.block.setHeight(textBlock, value = 50F)
engine.block.setString(
textBlock,
property = "text/text",
value = "This A4 document uses millimeter units with 300 DPI for print-ready output.",
)
// Font sizes stay in points even when the scene uses millimeters.
engine.block.setTextFontSize(textBlock, fontSize = 24F)
// At 300 DPI: 1 inch = 300 pixels, 1 mm ~= 11.81 pixels.
val a4WidthPixels = 210.0 * (300.0 / 25.4)
val a4HeightPixels = 297.0 * (300.0 / 25.4)
println("A4 at 300 DPI exports as ${a4WidthPixels.roundToInt()} x ${a4HeightPixels.roundToInt()} pixels")
return page
}
```
Control measurement systems for precise physical dimensions - create print-ready
documents with millimeter or inch units and configurable DPI for export quality.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-design-units)
Design units determine the coordinate system for all layout values in CE.SDK - positions, sizes, and margins. The engine supports three unit types: **Pixel** for screen-based designs, **Millimeter** for metric print dimensions, and **Inch** for imperial print formats.
This guide covers how to get and set design units, configure DPI for export quality, and set up scenes for specific physical dimensions like A4 paper.
## Understanding Design Units
### Supported Unit Types
CE.SDK supports three design unit types, each suited for different output scenarios:
- **Pixel** (`DesignUnit.PIXEL`) - Default unit, ideal for screen-based designs, web graphics, and video content. One unit equals one pixel in the design coordinate space.
- **Millimeter** (`DesignUnit.MILLIMETER`) - For print designs targeting metric dimensions (A4, A5, business cards). One unit equals one millimeter at the scene's DPI setting.
- **Inch** (`DesignUnit.INCH`) - For print designs targeting imperial dimensions (letter, legal, US business cards). One unit equals one inch at the scene's DPI setting.
### Design Unit and DPI Relationship
DPI (dots per inch) determines how physical units convert to pixels during export. At 300 DPI, a 1-inch block exports as 300 pixels wide. Higher DPI values produce higher-resolution exports suitable for professional printing.
For pixel-based scenes, DPI primarily affects font size conversions since font sizes are always specified in points.
## Getting the Current Design Unit
Use `engine.scene.getDesignUnit()` to retrieve the current scene's design unit. This returns a `DesignUnit` enum value: `DesignUnit.PIXEL`, `DesignUnit.MILLIMETER`, or `DesignUnit.INCH`.
```kotlin highlight-android-design-units-get-design-unit
// Get the current design unit. New scenes default to PIXEL.
val currentUnit = engine.scene.getDesignUnit()
println("Current design unit: $currentUnit") // PIXEL
```
## Setting the Design Unit
Use `engine.scene.setDesignUnit()` to change the measurement system. When you change the design unit, CE.SDK automatically converts existing layout values to maintain visual appearance.
```kotlin highlight-android-design-units-set-design-unit
// Switch to millimeters for a print workflow.
engine.scene.setDesignUnit(DesignUnit.MILLIMETER)
// Verify the change.
val newUnit = engine.scene.getDesignUnit()
println("Design unit changed to: $newUnit") // MILLIMETER
```
## Configuring DPI
Access DPI through the scene's `scene/dpi` property. For print workflows, 300 DPI is the standard for high-quality output.
```kotlin highlight-android-design-units-configure-dpi
// Set DPI to 300 for print-quality exports.
engine.block.setFloat(scene, property = "scene/dpi", value = 300F)
// Read back the DPI value.
val dpi = engine.block.getFloat(scene, property = "scene/dpi")
println("DPI set to: $dpi") // 300.0
```
DPI affects different aspects depending on the design unit:
- **Physical units (mm, in)**: DPI determines the pixel resolution of exported files.
- **Pixel units**: DPI only affects the conversion of font sizes from points to pixels.
## Setting Up Print-Ready Designs
For print workflows, combine `engine.scene.setDesignUnit(DesignUnit.MILLIMETER)` with appropriate DPI and page dimensions. Here's how to set up an A4 document ready for print export:
```kotlin highlight-android-design-units-set-page-dimensions
// Set the page to A4 dimensions (210 x 297 mm).
engine.block.setWidth(page, value = 210F)
engine.block.setHeight(page, value = 297F)
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
println("Page dimensions: ${pageWidth}mm x ${pageHeight}mm")
```
## Font Sizes and Design Units
Font sizes are always specified in points (`pt`), regardless of the scene's design unit. On Android, call `engine.block.setTextFontSize()` with point values even when the scene uses `DesignUnit.MILLIMETER` or `DesignUnit.INCH`.
```kotlin highlight-android-design-units-create-text-block
// Create a text block positioned and sized in millimeters.
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
// Position at 20 mm from left, 30 mm from top.
engine.block.setPositionX(textBlock, value = 20F)
engine.block.setPositionY(textBlock, value = 30F)
// Size: 170 mm wide, 50 mm tall.
engine.block.setWidth(textBlock, value = 170F)
engine.block.setHeight(textBlock, value = 50F)
engine.block.setString(
textBlock,
property = "text/text",
value = "This A4 document uses millimeter units with 300 DPI for print-ready output.",
)
// Font sizes stay in points even when the scene uses millimeters.
engine.block.setTextFontSize(textBlock, fontSize = 24F)
```
When DPI changes, text blocks automatically adjust their rendered size to maintain visual consistency.
## Understanding Export Resolution
The relationship between design units and export resolution is important for print workflows:
```kotlin highlight-android-design-units-compare-units
// At 300 DPI: 1 inch = 300 pixels, 1 mm ~= 11.81 pixels.
val a4WidthPixels = 210.0 * (300.0 / 25.4)
val a4HeightPixels = 297.0 * (300.0 / 25.4)
println("A4 at 300 DPI exports as ${a4WidthPixels.roundToInt()} x ${a4HeightPixels.roundToInt()} pixels")
```
At 300 DPI:
- An A4 page (210 x 297 mm) exports as 2480 x 3508 pixels.
- A letter page (8.5 x 11 in) exports as 2550 x 3300 pixels.
## Troubleshooting
### Exported Dimensions Don't Match Expected Size
Verify that DPI is set correctly for physical units. At 300 DPI, 1 inch becomes 300 pixels. Check that your design unit matches your target output format.
### Text Appears Wrong Size After Unit Change
Font sizes in points auto-adjust based on DPI. If text looks incorrect, verify the DPI setting matches your workflow requirements.
### Blocks Shift Position After Unit Change
CE.SDK preserves visual appearance during unit conversion. If positions seem unexpected, check the original coordinate values - the numeric values change but visual positions should remain stable.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.getDesignUnit()` | Get the current design unit of the scene. |
| `engine.scene.setDesignUnit(designUnit)` | Set the design unit for the scene. |
| `engine.block.getFloat(scene, property = "scene/dpi")` | Get the DPI value of a scene. |
| `engine.block.setFloat(scene, property = "scene/dpi", value = 300F)` | Set the DPI value of a scene. |
| `engine.block.setWidth(block, value = 210F)` | Set block width in the current design unit. |
| `engine.block.setHeight(block, value = 297F)` | Set block height in the current design unit. |
## Next Steps
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) - Learn about scene structure and management.
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Understand block types and properties.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Editor State"
description: "Control how users interact with content by switching between edit modes like transform, crop, and text."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/edit-modes-1f5b6c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Editor State](https://img.ly/docs/cesdk/android/concepts/edit-modes-1f5b6c/)
---
```kotlin file=@cesdk_android_examples/editor-guides-editor-state/EditorStateEditorSolution.kt reference-only
import androidx.compose.runtime.Composable
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import ly.img.editor.Editor
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.engine.DesignBlockType
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import ly.img.engine.UnstableEngineApi
@OptIn(UnstableEngineApi::class)
@Composable
fun EditorStateEditorSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration.remember {
onCreate = {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(page, value = 800F)
editorContext.engine.block.setHeight(page, value = 600F)
editorContext.engine.block.appendChild(parent = scene, child = page)
// Add an image block to demonstrate Crop mode
val imageBlock = editorContext.engine.block.create(DesignBlockType.Graphic)
editorContext.engine.block.setName(imageBlock, name = "editor-state-image")
editorContext.engine.block.setShape(
imageBlock,
shape = editorContext.engine.block.createShape(ShapeType.Rect),
)
editorContext.engine.block.setWidth(imageBlock, value = 350F)
editorContext.engine.block.setHeight(imageBlock, value = 250F)
editorContext.engine.block.setPositionX(imageBlock, value = 50F)
editorContext.engine.block.setPositionY(imageBlock, value = 175F)
val imageFill = editorContext.engine.block.createFill(FillType.Image)
editorContext.engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
editorContext.engine.block.setFill(imageBlock, fill = imageFill)
editorContext.engine.block.appendChild(parent = page, child = imageBlock)
// Add a text block to demonstrate Text mode
val textBlock = editorContext.engine.block.create(DesignBlockType.Text)
editorContext.engine.block.setName(textBlock, name = "editor-state-text")
editorContext.engine.block.appendChild(parent = page, child = textBlock)
editorContext.engine.block.replaceText(textBlock, text = "Edit this text")
editorContext.engine.block.setTextFontSize(textBlock, fontSize = 48F)
editorContext.engine.block.setWidthMode(textBlock, mode = SizeMode.AUTO)
editorContext.engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
editorContext.engine.block.setPositionX(textBlock, value = 450F)
editorContext.engine.block.setPositionY(textBlock, value = 275F)
}
onLoaded = {
val engine = editorContext.engine
val imageBlock = engine.block.findByName(name = "editor-state-image").first()
val textBlock = engine.block.findByName(name = "editor-state-text").first()
val requiredSources = setOf("ly.img.crop.presets", "ly.img.page.presets")
coroutineScope {
val existingSources = engine.asset.findAllSources().toSet()
requiredSources
.filterNot { it in existingSources }
.forEach { sourceId ->
engine.asset.addLocalSourceFromJSON(
contentUri = editorContext.baseUri.buildUpon()
.appendPath(sourceId)
.appendPath("content.json")
.build(),
)
}
launch {
engine.editor.onStateChanged()
.map { engine.editor.getEditMode() }
.distinctUntilChanged()
.collect { currentMode ->
println("Edit mode changed to: $currentMode")
}
}
val initialMode = engine.editor.getEditMode()
println("Initial edit mode: $initialMode")
engine.block.select(imageBlock)
engine.editor.setEditMode("Crop")
println(
"Edit mode changed to: ${engine.editor.getEditMode()} " +
"(requested before entering the crop-based demo state)",
)
engine.editor.setEditMode(
editMode = "MyCustomCropMode",
baseMode = "Crop",
)
println(
"Edit mode changed to: ${engine.editor.getEditMode()} " +
"(steady state after launch)",
)
engine.block.select(textBlock)
engine.editor.setEditMode("Text")
val textCursorX = engine.editor.getTextCursorPositionInScreenSpaceX()
val textCursorY = engine.editor.getTextCursorPositionInScreenSpaceY()
println(
"Text cursor position before placing a live caret: " +
"($textCursorX, $textCursorY)",
)
engine.block.select(imageBlock)
engine.editor.setEditMode(
editMode = "MyCustomCropMode",
baseMode = "Crop",
)
println(
"Edit mode changed to: ${engine.editor.getEditMode()} " +
"(restored after the text-cursor check)",
)
val isInteracting = engine.editor.isInteractionHappening()
println("Is interaction happening: $isInteracting")
}
}
}
},
onClose = onClose,
)
}
```
Control how users interact with content on the canvas by switching between edit modes, subscribing to state changes, and reading text cursor and interaction state in the Android bindings.

> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-editor-state)
This guide covers:
- The five built-in edit modes (Transform, Crop, Text, Trim, Playback)
- Switching edit modes programmatically
- Creating custom edit modes that inherit from built-in modes
- Subscribing to state changes for UI synchronization
- Reading text cursor coordinates for custom overlays
- Detecting active user interactions
## Setup
Set up a design editor scene with an image block and a text block. The example also registers the default crop and page preset asset sources before entering Crop mode so the Android crop sheet can open successfully. The demo briefly checks the text-cursor APIs on the text block during startup, then restores a custom mode that inherits from `Crop`, so the steady visual state remains the dimmed crop grid shown below.
```kotlin highlight-android-editor-state-setup
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(page, value = 800F)
editorContext.engine.block.setHeight(page, value = 600F)
editorContext.engine.block.appendChild(parent = scene, child = page)
// Add an image block to demonstrate Crop mode
val imageBlock = editorContext.engine.block.create(DesignBlockType.Graphic)
editorContext.engine.block.setName(imageBlock, name = "editor-state-image")
editorContext.engine.block.setShape(
imageBlock,
shape = editorContext.engine.block.createShape(ShapeType.Rect),
)
editorContext.engine.block.setWidth(imageBlock, value = 350F)
editorContext.engine.block.setHeight(imageBlock, value = 250F)
editorContext.engine.block.setPositionX(imageBlock, value = 50F)
editorContext.engine.block.setPositionY(imageBlock, value = 175F)
val imageFill = editorContext.engine.block.createFill(FillType.Image)
editorContext.engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
editorContext.engine.block.setFill(imageBlock, fill = imageFill)
editorContext.engine.block.appendChild(parent = page, child = imageBlock)
// Add a text block to demonstrate Text mode
val textBlock = editorContext.engine.block.create(DesignBlockType.Text)
editorContext.engine.block.setName(textBlock, name = "editor-state-text")
editorContext.engine.block.appendChild(parent = page, child = textBlock)
editorContext.engine.block.replaceText(textBlock, text = "Edit this text")
editorContext.engine.block.setTextFontSize(textBlock, fontSize = 48F)
editorContext.engine.block.setWidthMode(textBlock, mode = SizeMode.AUTO)
editorContext.engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
editorContext.engine.block.setPositionX(textBlock, value = 450F)
editorContext.engine.block.setPositionY(textBlock, value = 275F)
```
## Edit Modes
CE.SDK on Android exposes five built-in edit mode strings.
| Mode | Purpose |
|------|---------|
| `Transform` | Move, resize, and rotate blocks (default) |
| `Crop` | Adjust media content inside an image or video frame |
| `Text` | Edit text content inline |
| `Trim` | Adjust clip start and end points in video scenes |
| `Playback` | Play video or audio content with limited editing interactions |
The browser guide also demonstrates `Vector` mode. That mode is not currently surfaced by Android's `EditorApi`, so Android integrations typically work with the five modes above plus any custom mode strings you define.
### Getting the Current Mode
Query the current mode with `engine.editor.getEditMode()`. The initial mode is always `Transform`.
```kotlin highlight-android-editor-state-get-edit-mode
val initialMode = engine.editor.getEditMode()
println("Initial edit mode: $initialMode")
```
### Switching Edit Modes
Use `engine.editor.setEditMode()` to change the current editing mode. The selected block still needs to support the target mode for the UI to react visibly.
```kotlin highlight-android-editor-state-set-edit-mode
engine.editor.setEditMode("Crop")
println(
"Edit mode changed to: ${engine.editor.getEditMode()} " +
"(requested before entering the crop-based demo state)",
)
```
> **Tip:** Crop mode only has a visible effect when an image or video block is selected. Text mode requires a selected text block.
### Custom Edit Modes
You can create custom edit modes that inherit their behavior from one of the built-in modes. This is useful when your app needs to track an app-specific tool state without losing the underlying editor behavior. The demo uses this pattern for its steady state, leaving the editor in a custom mode backed by `Crop`.
```kotlin highlight-android-editor-state-custom-edit-mode
engine.editor.setEditMode(
editMode = "MyCustomCropMode",
baseMode = "Crop",
)
println(
"Edit mode changed to: ${engine.editor.getEditMode()} " +
"(steady state after launch)",
)
```
## Subscribing to State Changes
The engine notifies subscribers whenever the editor state changes, including edit mode switches triggered by your code or by the built-in UI.
```kotlin highlight-android-editor-state-on-state-changed
launch {
engine.editor.onStateChanged()
.map { engine.editor.getEditMode() }
.distinctUntilChanged()
.collect { currentMode ->
println("Edit mode changed to: $currentMode")
}
}
```
The example subscribes before it seeds the initial Crop state for the demo and then promotes that into its custom crop-based mode. The collector maps state-change events to the current edit mode and filters duplicate mode values, which keeps toolbar state or analytics logs focused on actual mode transitions.
Common use cases include updating toolbar state, toggling mode-specific controls, or logging edit-mode transitions for analytics.
## Cursor State
The web and Apple bindings expose cursor type and cursor rotation APIs for pointer-based interfaces. The Android bindings currently do not expose equivalent `getCursorType()` or `getCursorRotation()` methods on `EditorApi`.
### Reading Cursor Type
If your Android app supports a mouse or trackpad, handle the pointer icon at the View or Compose layer. Use `onStateChanged()` and `getEditMode()` to decide when your surrounding UI should switch between text-editing and transform-oriented pointer affordances.
### Reading Cursor Rotation
Directional cursor rotation is also not exposed on Android. If you render custom resize affordances in your own UI, derive their orientation from your own gesture or layout state instead of the engine.
## Text Cursor Position
When Text mode is active, you can read the text cursor position in screen coordinates to anchor your own overlays near the caret.
### Screen Space Coordinates
The runnable demo briefly selects the text block, switches it into `Text` mode, reads `getTextCursorPositionInScreenSpaceX()` and `getTextCursorPositionInScreenSpaceY()`, and then restores the crop-based steady state shown above. The coordinates remain `0,0` until a live caret is present, which makes this a useful way to detect that inline text editing has not started yet.
```kotlin highlight-android-editor-state-text-cursor-position
val textCursorX = engine.editor.getTextCursorPositionInScreenSpaceX()
val textCursorY = engine.editor.getTextCursorPositionInScreenSpaceY()
println(
"Text cursor position before placing a live caret: " +
"($textCursorX, $textCursorY)",
)
```
After the user places a live caret inside the inline text editor, these values become useful for positioning floating formatting controls, autocomplete popovers, or other app-specific text UI near the insertion point.
## Detecting Active Interactions
Determine whether the user is currently dragging, resizing, or performing another in-progress editor interaction before you trigger heavier UI updates.
### Using isInteractionHappening
Call `engine.editor.isInteractionHappening()` to check whether an interaction is currently in progress.
```kotlin highlight-android-editor-state-interaction-happening
val isInteracting = engine.editor.isInteractionHappening()
println("Is interaction happening: $isInteracting")
```
> **Warning:** `isInteractionHappening()` is marked with `@UnstableEngineApi` and may change in future releases.
## Troubleshooting
### Crop or Text Mode Doesn't Change Visually
Make sure the selected block supports the mode you are switching to. Image and video blocks can enter `Crop`; text blocks can enter `Text`.
### State Change Logs Never Appear
Verify the subscription is active before the operation that changes state. If you subscribe after the state change occurs, you won't receive that earlier notification.
### Text Cursor Position Stays at `0,0`
Make sure the selected block is a text block and that `Text` mode is active before you read the screen-space coordinates. The example returns `0,0` until a live caret is present, so a persistent `0,0` result usually indicates the editor never entered inline text editing for that selection.
## Next Steps
- [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) — Implement undo/redo functionality and manage history stacks
- [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) — Subscribe to block creation, update, and deletion events
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Understand block types and the design hierarchy
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Learn about scene structure and page management
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Editing Workflow"
description: "Control editing access with Creator, Adopter, Viewer, and Presenter roles using global and block-level scopes for tailored permissions."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/)
---
```kotlin file=@cesdk_android_examples/engine-guides-editing-workflow/EditingWorkflow.kt reference-only
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
private const val BRAND_BANNER_NAME = "Brand banner"
private const val COMPANY_NAME = "Company name"
private const val ATTENDEE_NAME = "Attendee name"
suspend fun editingWorkflow(
engine: Engine,
restoreEngineState: Boolean = false,
) = withContext(engine.dispatcher) {
val previousRole = if (restoreEngineState) engine.editor.getRole() else null
val previousSelectionMode = if (restoreEngineState) {
engine.editor.getSettingEnum("doubleClickSelectionMode")
} else {
null
}
val previousGlobalScopes = if (restoreEngineState) {
engine.editor.findAllScopes().associateWith { scope ->
engine.editor.getGlobalScope(key = scope)
}
} else {
emptyMap()
}
val roleCustomization = customizeEditingWorkflowRoles(engine, this)
try {
val template = createEditingWorkflowTemplate(engine)
engine.block.forceLoadResources(listOf(template.companyName, template.attendeeName))
} finally {
withContext(NonCancellable) {
roleCustomization.cancelAndJoin()
previousRole?.let(engine.editor::setRole)
previousSelectionMode?.let {
engine.editor.setSettingEnum("doubleClickSelectionMode", value = it)
}
previousGlobalScopes.forEach { (scope, globalScope) ->
engine.editor.setGlobalScope(key = scope, globalScope = globalScope)
}
}
}
}
internal fun createEditingWorkflowTemplate(engine: Engine): EditingWorkflowTemplate {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 720F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, "Card background")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 720F)
engine.block.setHeight(background, value = 1080F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(background, color = Color.fromRGBA(247, 249, 252, 255))
engine.block.appendChild(parent = page, child = background)
val brandBanner = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(brandBanner, BRAND_BANNER_NAME)
engine.block.setShape(brandBanner, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(brandBanner, value = 640F)
engine.block.setHeight(brandBanner, value = 220F)
engine.block.setPositionX(brandBanner, value = 40F)
engine.block.setPositionY(brandBanner, value = 48F)
engine.block.setFill(brandBanner, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(brandBanner, color = Color.fromHex("#FF0B1220"))
engine.block.appendChild(parent = page, child = brandBanner)
val companyName = engine.block.create(DesignBlockType.Text)
engine.block.setName(companyName, COMPANY_NAME)
engine.block.setWidthMode(companyName, mode = SizeMode.AUTO)
engine.block.setHeightMode(companyName, mode = SizeMode.AUTO)
engine.block.setPositionX(companyName, value = 88F)
engine.block.setPositionY(companyName, value = 122F)
engine.block.replaceText(companyName, text = "IMGLY Labs")
engine.block.setTextColor(companyName, color = Color.fromHex("#FFFFFFFF"))
engine.block.appendChild(parent = page, child = companyName)
val attendeeName = engine.block.create(DesignBlockType.Text)
engine.block.setName(attendeeName, ATTENDEE_NAME)
engine.block.setWidthMode(attendeeName, mode = SizeMode.AUTO)
engine.block.setHeightMode(attendeeName, mode = SizeMode.AUTO)
engine.block.setPositionX(attendeeName, value = 88F)
engine.block.setPositionY(attendeeName, value = 404F)
engine.block.replaceText(attendeeName, text = "Alex Morgan")
engine.block.setTextColor(attendeeName, color = Color.fromHex("#FF0B1220"))
engine.block.setBackgroundColor(attendeeName, color = Color.fromRGBA(231, 240, 255, 255))
engine.block.setBackgroundColorEnabled(attendeeName, enabled = true)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingLeft", value = 24F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingTop", value = 20F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingRight", value = 24F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingBottom", value = 20F)
engine.block.setFloat(attendeeName, property = "backgroundColor/cornerRadius", value = 18F)
engine.block.appendChild(parent = page, child = attendeeName)
// Roles define user types: "Creator", "Adopter", "Viewer", "Presenter".
val role = engine.editor.getRole()
println("Current role: $role") // "Creator"
engine.editor.setRole("Adopter")
val adopterRole = engine.editor.getRole()
println("Preview role: $adopterRole") // "Adopter"
engine.editor.setRole("Creator")
// Defer to the block-level settings so the template controls the Adopter experience.
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.DEFER)
val moveScope = engine.editor.getGlobalScope(key = "layer/move")
val allScopes = engine.editor.findAllScopes()
println("Global 'layer/move' scope: $moveScope")
println("Available scopes: ${allScopes.count()}")
engine.block.setScopeEnabled(page, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(page, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(page, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(background, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(background, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(background, key = "lifecycle/destroy", enabled = false)
// Locked brand elements stay fixed for Adopters.
engine.block.setScopeEnabled(brandBanner, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(brandBanner, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(brandBanner, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(companyName, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(companyName, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(companyName, key = "text/edit", enabled = false)
engine.block.setScopeEnabled(companyName, key = "text/character", enabled = false)
engine.block.setScopeEnabled(companyName, key = "lifecycle/destroy", enabled = false)
// Keep the attendee name editable but fixed in place.
engine.block.setScopeEnabled(attendeeName, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(attendeeName, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(attendeeName, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(attendeeName, key = "text/character", enabled = false)
engine.block.setScopeEnabled(attendeeName, key = "lifecycle/destroy", enabled = false)
engine.editor.setRole("Creator")
val creatorCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val creatorCanEditAttendeeName = engine.block.isAllowedByScope(attendeeName, key = "text/edit")
println("Creator can select the banner: $creatorCanSelectBrandBanner") // true
println("Creator can edit the attendee name: $creatorCanEditAttendeeName") // true
engine.editor.setRole("Adopter")
val adopterCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val adopterCanEditAttendeeName = engine.block.isAllowedByScope(attendeeName, key = "text/edit")
println("Adopter can select the banner: $adopterCanSelectBrandBanner") // false
println("Adopter can edit the attendee name: $adopterCanEditAttendeeName") // true
engine.editor.setRole("Creator")
return EditingWorkflowTemplate(
brandBanner = brandBanner,
companyName = companyName,
attendeeName = attendeeName,
)
}
fun customizeEditingWorkflowRoles(
engine: Engine,
scope: CoroutineScope,
): Job = scope.launch(start = CoroutineStart.UNDISPATCHED) {
engine.editor.onRoleChanged().collect { role ->
if (role == "Adopter") {
engine.editor.setGlobalScope(key = "appearance/filter", globalScope = GlobalScope.ALLOW)
engine.editor.setGlobalScope(key = "appearance/effect", globalScope = GlobalScope.ALLOW)
}
}
}
fun findEditingWorkflowTemplate(engine: Engine): EditingWorkflowTemplate = EditingWorkflowTemplate(
brandBanner = requireNotNull(engine.block.findByName(BRAND_BANNER_NAME).firstOrNull()),
companyName = requireNotNull(engine.block.findByName(COMPANY_NAME).firstOrNull()),
attendeeName = requireNotNull(engine.block.findByName(ATTENDEE_NAME).firstOrNull()),
)
```
```kotlin file=@cesdk_android_examples/engine-guides-editing-workflow/EditingWorkflowTemplate.kt reference-only
import ly.img.engine.DesignBlock
data class EditingWorkflowTemplate(
val brandBanner: DesignBlock,
val companyName: DesignBlock,
val attendeeName: DesignBlock,
)
```
CE.SDK controls editing access through roles and scopes, enabling template workflows where designers create locked layouts and end-users customize only the permitted parts.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-editing-workflow)
The Kotlin snippets below assume you already have an `Engine` instance on the main thread.
CE.SDK uses a two-tier permission system: **roles** define user types with preset permissions, while **scopes** control specific capabilities. This enables workflows where templates can be prepared by designers and safely customized by end-users.
This guide covers:
- The four user roles and their purposes
- How scopes control editing capabilities
- The permission resolution hierarchy
- Common template workflow patterns
## Roles
Roles define user types with different default permissions:
| Role | Purpose | Default Access |
|------|---------|----------------|
| **Creator** | Designers building templates | Full access to all operations |
| **Adopter** | End-users customizing templates | Limited by block-level scopes |
| **Viewer** | Static preview without interaction | Read-only, no playback controls |
| **Presenter** | Presenting slideshows or playing videos | Read-only with playback and navigation |
Creators set the block-level scopes that constrain what Adopters can do. This separation enables brand consistency while allowing personalization.
```kotlin highlight-android-roles
// Roles define user types: "Creator", "Adopter", "Viewer", "Presenter".
val role = engine.editor.getRole()
println("Current role: $role") // "Creator"
engine.editor.setRole("Adopter")
val adopterRole = engine.editor.getRole()
println("Preview role: $adopterRole") // "Adopter"
engine.editor.setRole("Creator")
```
## Scopes
Scopes define specific capabilities organized into categories:
- **Text**: Editing content and character formatting
- **Fill/Stroke**: Changing colors and shapes
- **Layer**: Moving, resizing, rotating, cropping
- **Appearance**: Filters, effects, shadows, animations
- **Lifecycle**: Deleting and duplicating elements
- **Editor**: Adding new elements and selecting
## Global vs Block-Level Scopes
**Global scopes** apply editor-wide and determine whether block-level settings are checked:
- `GlobalScope.ALLOW` — Always permit the operation
- `GlobalScope.DENY` — Always block the operation
- `GlobalScope.DEFER` — Check block-level scope settings
**Block-level scopes** control permissions on individual blocks. These settings only take effect when the corresponding global scope is set to `GlobalScope.DEFER`.
```kotlin highlight-android-globalScopes
// Defer to the block-level settings so the template controls the Adopter experience.
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.DEFER)
val moveScope = engine.editor.getGlobalScope(key = "layer/move")
val allScopes = engine.editor.findAllScopes()
println("Global 'layer/move' scope: $moveScope")
println("Available scopes: ${allScopes.count()}")
```
To lock a specific block, disable its scopes:
```kotlin highlight-android-blockScopes
engine.block.setScopeEnabled(page, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(page, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(page, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(background, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(background, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(background, key = "lifecycle/destroy", enabled = false)
// Locked brand elements stay fixed for Adopters.
engine.block.setScopeEnabled(brandBanner, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(brandBanner, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(brandBanner, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(companyName, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(companyName, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(companyName, key = "text/edit", enabled = false)
engine.block.setScopeEnabled(companyName, key = "text/character", enabled = false)
engine.block.setScopeEnabled(companyName, key = "lifecycle/destroy", enabled = false)
// Keep the attendee name editable but fixed in place.
engine.block.setScopeEnabled(attendeeName, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(attendeeName, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(attendeeName, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(attendeeName, key = "text/character", enabled = false)
engine.block.setScopeEnabled(attendeeName, key = "lifecycle/destroy", enabled = false)
```
In the example template, the brand banner and company name are locked while the attendee name keeps `editor/select` and `text/edit` enabled so adopters can personalize the template without moving or deleting anything.
## Permission Resolution
Permissions resolve in this order:
1. **Role defaults** — Each role has preset global scope values
2. **Global scope** — If `GlobalScope.ALLOW` or `GlobalScope.DENY`, this is the final answer
3. **Block-level scope** — If global is `GlobalScope.DEFER`, check the block's settings
Use `isAllowedByScope()` to check the final computed permission for any block and scope combination:
```kotlin highlight-android-checkPermissions
engine.editor.setRole("Creator")
val creatorCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val creatorCanEditAttendeeName = engine.block.isAllowedByScope(attendeeName, key = "text/edit")
println("Creator can select the banner: $creatorCanSelectBrandBanner") // true
println("Creator can edit the attendee name: $creatorCanEditAttendeeName") // true
```
## Switching Roles
Change roles at runtime with `setRole()`. When switching to Adopter, block-level restrictions take effect. Switching back to Creator restores full access.
```kotlin highlight-android-switchRole
engine.editor.setRole("Adopter")
val adopterCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val adopterCanEditAttendeeName = engine.block.isAllowedByScope(attendeeName, key = "text/edit")
println("Adopter can select the banner: $adopterCanSelectBrandBanner") // false
println("Adopter can edit the attendee name: $adopterCanEditAttendeeName") // true
engine.editor.setRole("Creator")
```
## Customizing Role Behavior
`onRoleChanged()` returns a `Flow` that emits after role defaults are applied. Collect it before switching roles when you need to override selected scopes for a role:
```kotlin highlight-android-customizeRoleBehavior
fun customizeEditingWorkflowRoles(
engine: Engine,
scope: CoroutineScope,
): Job = scope.launch(start = CoroutineStart.UNDISPATCHED) {
engine.editor.onRoleChanged().collect { role ->
if (role == "Adopter") {
engine.editor.setGlobalScope(key = "appearance/filter", globalScope = GlobalScope.ALLOW)
engine.editor.setGlobalScope(key = "appearance/effect", globalScope = GlobalScope.ALLOW)
}
}
}
```
> **Warning:** Scope changes made in the role-change collector override the role defaults for the active engine session.
## Template Workflow Pattern
A typical template workflow:
1. **Designer (Creator)** creates the template layout
2. **Designer** locks brand elements using block scopes
3. **Designer** keeps personalization fields editable
4. **End-user (Adopter)** opens the template
5. **End-user** edits only permitted elements
6. **End-user** exports the personalized result
This pattern ensures brand consistency while enabling personalization. The Android example creates a small template with locked brand elements and one editable personalization field:
```kotlin highlight-android-templateScene
val brandBanner = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(brandBanner, BRAND_BANNER_NAME)
engine.block.setShape(brandBanner, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(brandBanner, value = 640F)
engine.block.setHeight(brandBanner, value = 220F)
engine.block.setPositionX(brandBanner, value = 40F)
engine.block.setPositionY(brandBanner, value = 48F)
engine.block.setFill(brandBanner, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(brandBanner, color = Color.fromHex("#FF0B1220"))
engine.block.appendChild(parent = page, child = brandBanner)
val companyName = engine.block.create(DesignBlockType.Text)
engine.block.setName(companyName, COMPANY_NAME)
engine.block.setWidthMode(companyName, mode = SizeMode.AUTO)
engine.block.setHeightMode(companyName, mode = SizeMode.AUTO)
engine.block.setPositionX(companyName, value = 88F)
engine.block.setPositionY(companyName, value = 122F)
engine.block.replaceText(companyName, text = "IMGLY Labs")
engine.block.setTextColor(companyName, color = Color.fromHex("#FFFFFFFF"))
engine.block.appendChild(parent = page, child = companyName)
val attendeeName = engine.block.create(DesignBlockType.Text)
engine.block.setName(attendeeName, ATTENDEE_NAME)
engine.block.setWidthMode(attendeeName, mode = SizeMode.AUTO)
engine.block.setHeightMode(attendeeName, mode = SizeMode.AUTO)
engine.block.setPositionX(attendeeName, value = 88F)
engine.block.setPositionY(attendeeName, value = 404F)
engine.block.replaceText(attendeeName, text = "Alex Morgan")
engine.block.setTextColor(attendeeName, color = Color.fromHex("#FF0B1220"))
engine.block.setBackgroundColor(attendeeName, color = Color.fromRGBA(231, 240, 255, 255))
engine.block.setBackgroundColorEnabled(attendeeName, enabled = true)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingLeft", value = 24F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingTop", value = 20F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingRight", value = 24F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingBottom", value = 20F)
engine.block.setFloat(attendeeName, property = "backgroundColor/cornerRadius", value = 18F)
engine.block.appendChild(parent = page, child = attendeeName)
```
## Troubleshooting
- **Block-level restrictions do not apply** — Set the matching global scope to `GlobalScope.DEFER`; `GlobalScope.ALLOW` and `GlobalScope.DENY` bypass block settings.
- **Role-specific overrides disappear after switching roles** — Apply custom scope changes from `onRoleChanged()` because role defaults are applied during the role switch.
- **A block cannot be selected at all** — Check `editor/select`; disabling that scope prevents interaction before other scope checks can matter.
## API Reference
| API | Purpose |
|-----|---------|
| `engine.editor.setRole(role=_)` | Set the active user role. |
| `engine.editor.getRole()` | Read the active user role. |
| `engine.editor.onRoleChanged()` | Collect role changes after role defaults are applied. |
| `engine.editor.setGlobalScope(key="layer/move",globalScope=_)` | Allow, deny, or defer an operation globally. |
| `engine.editor.getGlobalScope(key="layer/move")` | Read the global state for one scope. |
| `engine.editor.findAllScopes()` | List all scope keys supported by the engine. |
| `engine.scene.create()` | Create the template scene. |
| `engine.block.create(type=_)` | Create page, graphic, and text blocks. |
| `engine.block.setName(block=_,name=_)` | Assign readable block names for later lookup. |
| `engine.block.createShape(type=_)` | Create the banner shape. |
| `engine.block.setShape(block=_,shape=_)` | Attach a shape to a graphic block. |
| `engine.block.setWidth(block=_,value=_)` / `setHeight(block=_,value=_)` | Set fixed block dimensions. |
| `engine.block.setWidthMode(block=_,mode=_)` / `setHeightMode(block=_,mode=_)` | Let text blocks size to their content. |
| `engine.block.setPositionX(block=_,value=_)` / `setPositionY(block=_,value=_)` | Position template blocks on the page. |
| `engine.block.createFill(type=_)` | Create a fill block for a graphic. |
| `engine.block.setFill(block=_,fill=_)` | Assign the fill to a graphic block. |
| `engine.block.setFillSolidColor(block=_,color=_)` | Set the color of a solid fill without raw property paths. |
| `engine.block.replaceText(block=_,text=_)` | Set the editable template text. |
| `engine.block.setTextColor(block=_,color=_)` | Set text color. |
| `engine.block.setBackgroundColor(block=_,color=_)` | Set the text background color. |
| `engine.block.setBackgroundColorEnabled(block=_,enabled=_)` | Enable the text background. |
| `engine.block.setFloat(block=_,property=_,value=_)` | Configure text background padding and corner radius. |
| `engine.block.appendChild(parent=_,child=_)` | Add blocks to the scene hierarchy. |
| `engine.block.setScopeEnabled(block=_,key="text/edit",enabled=_)` | Enable or disable one scope on a block. |
| `engine.block.isScopeEnabled(block=_,key="text/edit")` | Read the block-level scope flag. |
| `engine.block.isAllowedByScope(block=_,key="text/edit")` | Check the final resolved permission for a block. |
## Next Steps
- [Lock Design Elements](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) — Step-by-step instructions for locking specific elements in templates
- [Set Editing Constraints](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) — Learn how to control editing capabilities in CE.SDK templates using the Scope system to lock positions, prevent transformations, and create guided editing experiences
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Error Catalog"
description: "Reference of every structured CE.SDK engine error code, its message, hint, and related documentation page."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/error-catalog-z3djzn/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Error Catalog](https://img.ly/docs/cesdk/android/concepts/error-catalog-z3djzn/)
---
Every recoverable engine failure is a structured error with a stable `code` (for example `SCENE.NOT_VALID`), an English developer-facing message and hint, typed arguments, and an optional documentation link. Match on the `code` rather than the message string — the code is stable across releases. For how to read these on each binding, see the [structured-errors migration guide](#broken-link-e7f3a1).
This page lists all catalog errors grouped by category including links to documentation pages. The `code` is what you branch on. The `message` and `hint` are the English strings the engine renders (developer-facing — surface localized copy in your UI layer).
## ASSET
Asset sources, asset library, asset references.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `ASSET.CANNOT_APPLY_COLOR_NO_TARGET` | Could not apply color asset to block \{block}. Block has nothing to apply color to. | The block has no fill, stroke, or text color to receive the asset. Set the relevant property type first. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_CMYK_MISSING_FIELDS` | \`payload.color\` with \`CMYK\` color space must have fields \`c\`, \`m\`, \`y\`, \`k\`. | CMYK colors require numeric \`c\`, \`m\`, \`y\`, \`k\` components. Add them to the payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_MISSING` | Asset does not contain a color. | Color asset operations require a \`color\` payload. Verify the asset is a color asset. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_MISSING_COLOR_SPACE` | \`payload.color\` must have a \`colorSpace\` field. | Add \`colorSpace\` (sRGB, CMYK, or SpotColor) to the asset payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_SPACE_UNKNOWN` | Unknown color space: \{colorSpace} | Color space '\{colorSpace}' is not supported. Valid values are sRGB, CMYK, and SpotColor. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_SPOT_MISSING_FIELDS` | \`payload.color\` with \`SpotColor\` color space must have fields \`name\`, \`externalReference\`, \`representation\`. | Spot colors require all three fields; \`representation\` must itself be a sub-color in sRGB or CMYK. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_SPOT_REPRESENTATION_INVALID` | A \`payload.color\` with colorSpace \`SpotColor\` must use a \`RGB\` or \`CMYK\` color for its \`representation\` field. | Spot color representation must be a primitive sRGB or CMYK value. Nested SpotColor is not allowed. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.COLOR_SRGB_MISSING_FIELDS` | \`payload.color\` with \`sRGB\` color space must have fields \`r\`, \`g\`, and \`b\`. | sRGB colors require numeric \`r\`, \`g\`, \`b\` components. Add them to the payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.DOCUMENT_SOURCE_NO_ADD` | A document asset source does not allow adding assets. | Document asset sources are read-only views into the active scene. Add assets to a local asset source instead. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.DOCUMENT_SOURCE_NO_REMOVE` | A document asset source does not allow removing assets. | Document asset sources are read-only. Remove the underlying scene blocks instead. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FACET_PATH_NOT_FACETABLE` | Asset property '\{property}' is not facetable. Use 'tags', 'groups', or 'meta.\'. | Facets enumerate bounded value sets; 'label' and 'id' are unbounded. Request 'tags', 'groups', or 'meta.\' instead. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_COMBINATOR_EMPTY` | Asset filter '\{combinator}' must have at least one child. | Add at least one filter expression to the '\{combinator}' array. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_COMBINATOR_NOT_ARRAY` | Asset filter '\{combinator}' must be an array. | Provide an array of filter expressions for '\{combinator}'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_META_KEY_MISSING` | Asset property filter 'meta' path must include a key (e.g. 'meta.languages'). | Append the meta key after 'meta.' — for example 'meta.languages'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_MULTIPLE_DISCRIMINATORS` | Asset filter has multiple discriminators; specify exactly one of 'property', 'and', 'or', 'not'. | Keep only one of 'property', 'and', 'or', 'not' on the expression. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_NOT_CHILD_NOT_OBJECT` | Asset filter '\{combinator}' must be an object. | Provide a single filter-expression object as the value of '\{combinator}'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_NOT_OBJECT` | Asset filter must be an object. | Wrap each filter expression in an object with one of 'property', 'and', 'or', 'not'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_NO_DISCRIMINATOR` | Asset filter must have exactly one of 'property', 'and', 'or', 'not'. | Set one discriminator key on the filter expression. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_OPERAND_MISSING` | Asset property filter must have exactly one of 'contains' or 'equals'. | Set either 'contains' or 'equals' on the property filter, not both and not neither. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_OPERAND_NOT_STRING` | Asset property filter '\{operand}' must be a string. | Provide a string value for '\{operand}' on the property filter. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_PROPERTY_EMPTY` | Asset property filter 'property' must not be empty. | Set 'property' to 'label', 'id', 'tags', 'groups', or 'meta.\'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_PROPERTY_NOT_STRING` | Asset property filter must have a string 'property'. | Provide 'property' as a string identifying the asset field to match. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_PROPERTY_UNKNOWN` | Unknown asset property '\{property}'. Use 'label', 'id', 'tags', 'groups', or 'meta.\'. | Replace '\{property}' with one of the supported property roots. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_ROOT_NOT_ARRAY` | Asset filter must be an array of filter expressions. | Pass the top-level filter as a JSON array of filter expressions (or null/empty for no filter). | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FILTER_UNRECOGNIZED_DISCRIMINATOR` | Asset filter has no recognized discriminator. | Use one of 'property', 'and', 'or', 'not' on the filter expression. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FIND_FN_REQUIRED` | findAssetsFn is required. | Pass a non-null findAssetsFn callback when registering a custom asset source. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FONT_MISSING_NAME` | Missing required field 'name' in font. | Each font entry needs a \`name\`. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.FONT_MISSING_URL` | Missing required field 'url' in font. | Each font entry needs a \`url\` pointing at the font file. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.ID_ALREADY_EXISTS` | Asset with id \{id} already exists in asset source. | Each asset id must be unique within a source. Remove the existing asset, or use a different id. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.JSON_INVALID_IN_URI` | Invalid JSON content in URI: \{uri} | The JSON at '\{uri}' could not be parsed. Validate the payload or check the URL response body. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.JSON_MALFORMED_LOCAL` | Invalid JSON content for local asset source. Expected 'id' and 'assets' fields. | The JSON payload must include top-level 'id' and 'assets' fields. See the local-asset-source schema for the full shape. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.META_NON_STRING_ENTRY` | Unexpectedly found non-string entry in asset's meta. | Asset meta is a flat string map. Numeric or object values are not supported — stringify them before inserting. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.NO_SELECTION` | No elements selected. | This asset application requires a selected block. Verify a selection exists with api.block.findAllSelected() before applying. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.RESOURCE_DATA_NOT_AVAILABLE` | Resource data not available: \{uri} | The resource at '\{uri}' is registered but has no data buffer. Confirm the source provides bytes (not just metadata). | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.RESOURCE_DATA_UNAVAILABLE` | Resource data is not available. | The resource has no attached data provider. Ensure the resource has been fetched and is ready before requesting its data. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.RESOURCE_NOT_FOUND_AT_URI` | Resource not found: \{uri} | No resource registered at '\{uri}'. Confirm the asset source serves the URI and that loading completed. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.RESOURCE_NOT_READY` | Resource is not ready. | The resource is still loading or in an error state. Wait for the Ready state, or handle the error via the resource's loading state. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.RESOURCE_NOT_READY_AT_URI` | Resource not ready: \{uri} | The resource at '\{uri}' is still loading. Subscribe to resource events or retry after the resource is Ready. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SORT_KEY_MISSING` | Sort key not found in asset source. | The asset source does not provide the requested sort key. Use a sort key the source declares, or omit the sort parameter. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_ADD_DENIED` | Assets cannot be added to this asset source: \{sourceId}. | The source '\{sourceId}' rejected addAssets(). It may be backed by an immutable provider. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_ALREADY_EXISTS` | AssetSource with id \{sourceId} already exists. | Asset source ids must be unique. Remove the existing source via removeAssetSource() or pick a different id. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_CANNOT_ADD_ASSETS` | AssetSource with id \{sourceId} cannot add assets. | The source '\{sourceId}' is read-only. Modify the underlying data store or pick a writable source. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_CANNOT_REMOVE_ASSETS` | AssetSource with id \{sourceId} cannot remove assets. | The source '\{sourceId}' is read-only. Modify the underlying data store or pick a writable source. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_DOES_NOT_SUPPORT_APPLY_PROPERTIES` | Asset source \{source} does not support applying properties. | The source '\{source}' does not implement applyProperties(). Use a source that supports it, or apply properties through a different surface. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_NOT_EXISTS` | AssetSource with id \{sourceId} does not exist. | No asset source registered under '\{sourceId}'. Use api.asset.addLocalAssetSource() or addAssetSource() to register one first. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_NOT_REMOVABLE` | AssetSource with id \{sourceId} may not be removed. | Built-in asset sources cannot be unregistered. Override their behavior by adding a higher-priority custom source instead. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_NO_PROPERTY_MOD` | This asset source does not support property modification. | The active asset source does not support property modification. Register a writable source via api.asset.addLocalAssetSource(), or modify the underlying assets directly. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_REMOVE_DENIED` | Assets cannot be removed from this asset source: \{sourceId}. | The source '\{sourceId}' rejected removeAssets(). It may be backed by an immutable provider. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.SOURCE_UNKNOWN` | The asset source \{sourceId} is unknown. | No asset source registered under '\{sourceId}'. Add it before requesting its content. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.STYLE_PRESET_INVALID_PAYLOAD` | Style preset payload is not a valid JSON object. | The asset's payload.stylePreset must be a JSON object. Check the asset definition's stylePreset field. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.STYLE_PRESET_MISSING_BLOCK_TYPE` | A style preset requires a blockType to create a new block. | When applying a style preset without a target block, set meta.blockType so the engine knows which block type to create. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.STYLE_PRESET_NOT_APPLICABLE` | Style preset is not applicable to a block of type '\{blockType}'. | The style preset declares the block types it supports. Apply it to a supported block type, or widen the preset's blockType. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TARGET_BLOCK_NOT_VALID` | Block is not valid. | The selected block is no longer valid. Re-resolve it via api.block.findAllSelected() and confirm with api.block.isValid(block) before applying an asset. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TRANSFORM_PRESET_FIXED_ASPECT_MISSING_FIELDS` | \`payload.transformPreset\` of type \`fixedAspectRatio\` must have fields \`width\`, and \`height\`. | FixedAspectRatio presets need \`width\` and \`height\` to determine the ratio. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TRANSFORM_PRESET_FIXED_SIZE_MISSING_FIELDS` | \`payload.transformPreset\` of type \`fixedAspectRatio\` must have fields \`width\`, \`height\`, and \`designUnit\`. | FixedSize presets need numeric \`width\`, \`height\`, and a \`designUnit\` (e.g. 'Pixel', 'Inch'). | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TRANSFORM_PRESET_MISSING_TYPE` | \`payload.transformPreset\` must have a \`type\` field. | Add a \`type\` to the transformPreset payload (e.g. 'FixedSize', 'FixedAspectRatio', 'ContentAspectRatio'). | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TRANSFORM_PRESET_TYPE_UNKNOWN` | Unknown transformPreset type: \{type} | TransformPreset type '\{type}' is not recognized. Valid values: 'FixedSize', 'FixedAspectRatio', 'ContentAspectRatio'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TYPEFACE_MISSING_FAMILY` | Missing required field 'family' in typeface. | Typefaces declare a \`family\` grouping their fonts. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.TYPEFACE_MISSING_WEIGHT` | Missing required field 'weight' in typeface. | Typefaces declare a \`weight\`. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.UNSUPPORTED_MIME_TYPE` | Unsupported mime type for loadWithoutService: \{mimeType} | The bypass loader cannot handle '\{mimeType}'. Route through the regular resource service or extend the loader. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.UNSUPPORTED_MIME_TYPE_FOR_BLOCK` | Cannot create a block from an asset with the MIME type \{mimeType}. | MIME type '\{mimeType}' has no block factory. Convert the asset to a supported type (image/*, video/*, audio/\*) or register a custom factory. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.URI_INVALID_BARE` | Invalid URI \{uri}. | The asset URI is malformed. Use a valid absolute URL or a registered scheme like 'buffer://'. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
| `ASSET.URI_META_MISSING` | 'uri' not found in asset 'meta'. Can't replace. | The asset's meta block must include a 'uri' field for replace() to work. Add it before invoking. | [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) |
## AUDIO
Audio playback and routing.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `AUDIO.DATA_SOURCE_INIT_FAILED` | Mini Audio data source initialization failed. | miniaudio could not allocate the data source. Out-of-memory or invalid decoder state is the likely cause. | |
| `AUDIO.DATA_SOURCE_NODE_INIT_FAILED` | Data source node initialization failed (\{resultCode}). | miniaudio could not attach the data source as a node with error \{resultCode}. The graph configuration may be invalid. | |
| `AUDIO.DATA_SOURCE_NO_DURATION` | Audio data source has no known duration. | miniaudio could not determine the source length. Streaming or seekless sources may not report a duration; decode the source fully or supply one with a known length. | |
| `AUDIO.DECODER_FORMAT_FAILED` | Failed to get decoder data format. | The decoder reported an indeterminate sample format. The file header may be truncated or malformed. | |
| `AUDIO.DECODER_INIT_FAILED` | Mini Audio decoder initialization failed (\{resultCode}). | miniaudio rejected the audio source with error \{resultCode}. The file may be corrupted or use an unsupported codec. | |
| `AUDIO.DEVICE_INIT_FAILED` | Initializing audio device failed. | The platform's audio device could not be opened. Verify the device exists and is not in use by another process. | |
| `AUDIO.DEVICE_RESUME_FAILED` | Failed to resume the audio device. | The audio device was suspended and could not be resumed. On web, trigger playback from within a user-gesture handler (e.g. a click) so the browser unlocks the audio context before calling play. | |
| `AUDIO.DEVICE_START_FAILED` | Audio device start failed. | The audio device opened but did not start playback. Check device state and platform permissions. | |
| `AUDIO.DEVICE_STOP_FAILED` | Audio device stop failed. | The audio device could not be stopped cleanly. Subsequent restart may be required. | |
| `AUDIO.INVALID_SOUND_HANDLE` | Invalid sound handle. | The sound handle does not refer to a live audio source. It may have been stopped or never created. | |
| `AUDIO.NODE_ATTACH_OUTPUT_BUS_FAILED` | Attaching output bus failed (\{resultCode}). | miniaudio could not attach the node's output bus with error \{resultCode}. The graph layout may be invalid. | |
| `AUDIO.NODE_GRAPH_INIT_FAILED` | Audio node graph init failed. | The miniaudio node graph could not be created. Check available system resources and the audio backend installation. | |
| `AUDIO.NODE_SET_STATE_FAILED` | Setting node state failed (\{resultCode}). | miniaudio rejected the node state transition with error \{resultCode}. | |
| `AUDIO.NODE_STATE_CHANGE_FAILED` | Failed to change sound node state to \{targetState}. | miniaudio did not converge on the requested node state '\{targetState}'. The node may have been destroyed concurrently. | |
| `AUDIO.PCM_READ_FAILED` | Failed to read PCM frames from node graph (\{resultCode}). | miniaudio returned error \{resultCode} while reading PCM. The graph may be in an invalid state; re-create it. | |
| `AUDIO.RESAMPLER_INIT_FAILED` | Mini Audio resampler initialization failed. | miniaudio could not construct a resampler for the source rate. The source sample rate may be unsupported. | |
| `AUDIO.UNSUPPORTED_CODEC` | Unsupported audio codec. | miniaudio cannot decode this audio format. Re-encode the source as WAV, FLAC, or one of the documented supported codecs. | [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) |
## BINDING
Errors raised at the binding bridge layer (WASM/JNI/ObjC++/N-API parameter validation, host-callback invariants, asset-source platform plumbing).
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `BINDING.ASSET_PLATFORM_SOURCE_UNAVAILABLE` | Platform source object is unavailable. | The platform-side asset source backing this engine handle was garbage collected. Keep a reference while the engine may call into it. | |
| `BINDING.ASSET_SOURCE_ADD_UNSUPPORTED` | Assets cannot be added to this asset source. | Implement \`addAsset\` on the asset source if you need this operation to succeed. | |
| `BINDING.ASSET_SOURCE_APPLY_PROPERTIES_UNSUPPORTED` | Asset properties cannot be applied by this asset source. | Implement \`applyAssetProperties\` on the asset source if you need this operation to succeed. | |
| `BINDING.ASSET_SOURCE_FETCH_UNSUPPORTED` | Assets cannot be fetched from this asset source. | Implement \`fetchAsset\` on the asset source if you need this operation to succeed. | |
| `BINDING.ASSET_SOURCE_REMOVE_UNSUPPORTED` | Assets cannot be removed from this asset source. | Implement \`removeAsset\` on the asset source if you need this operation to succeed. | |
| `BINDING.FACETS_QUERY_ENTRY_NOT_STRING` | Each entry in 'facets' must be a property path string | Every \`facets\` entry must be a string property path such as \`'groups'\`, \`'tags'\`, or \`'meta.\'\`. | |
| `BINDING.FACETS_QUERY_NOT_ARRAY` | Field 'facets' must be an array of property path strings | Pass \`facets\` as a JavaScript array of property path strings such as \`'groups'\`, \`'tags'\`, or \`'meta.\'\`. | |
| `BINDING.FACETS_RESULT_NOT_OBJECT` | Field 'facets' in find assets result is not an object keyed by facet path | Return \`facets\` from a custom source's \`findAssets\` as an object keyed by the requested property paths. | |
| `BINDING.FACET_COUNT_NOT_NUMBER` | Field 'count' in facet '\{facet}' of find assets result is not a number | Omit \`count\` or set it to a number; it is optional per bucket. | |
| `BINDING.FACET_ENTRY_NOT_OBJECT` | An entry of facet '\{facet}' in find assets result is not a value/count object | Each facet bucket must be an object with a string \`value\` and an optional numeric \`count\`. | |
| `BINDING.FACET_NOT_ARRAY` | Facet '\{facet}' in find assets result is not an array of value/count entries | Each \`facets\[path]\` must be an array of value/count buckets. | |
| `BINDING.FACET_VALUE_MISSING` | Missing required string field 'value' in facet '\{facet}' of find assets result | Every facet bucket must carry a string \`value\`. | |
| `BINDING.HOST_CALLBACK_THREW` | \{operation} callback threw: \{message} | The platform-side \`\{operation}\` callback raised an error. Inspect \`args.message\` for the original host-language exception text and ensure the callback handles its input correctly. | |
| `BINDING.JSON_NOT_REPRESENTABLE` | Value cannot be represented as JSON. | Pass only JSON-serializable values (no functions, no cycles, no \`BigInt\`). | |
| `BINDING.JSON_PARSE_FAILED` | Failed to parse JSON from JS value. | Pass a value the bridge can stringify with \`JSON.stringify\` without throwing. | |
| `BINDING.NODE_NOT_INITIALIZED` | Not initialized | Call \`CreativeEngine.init(...)\` before invoking other engine methods. | |
| `BINDING.NO_TEXT_BLOCK_BEING_EDITED` | No text block is currently being edited. | Enter text editing mode via \`block.setTextEditMode(...)\` before invoking text-editing APIs. | |
| `BINDING.URI_RESOLVER_INVALID_RESULT` | URI resolver returned an invalid result. | Return a non-null string URL (or throw) from the URI resolver callback. | |
| `BINDING.URI_RESOLVER_PROMISE_REJECTED` | URI resolver promise rejected. | Resolve the returned promise with a string URL, or reject with a descriptive error message. | |
| `BINDING.URI_RESOLVER_UNAVAILABLE` | Async URI resolver is unavailable. | Configure an async URI resolver on the editor before triggering this call. | |
| `BINDING.WASM_ASSET_FILTER_JSON_PARSE_FAILED` | Failed to parse asset filter JSON from JS. | Pass an object the JS engine can serialize to JSON (no functions, no cycles, no \`BigInt\`). | |
| `BINDING.WASM_ASSET_GROUPS_NOT_ARRAY` | Field 'groups' is not an array | Pass \`groups\` as a JavaScript array of group id strings. | |
| `BINDING.WASM_ASSET_MISSING_ID` | Missing required field 'id' in asset | Each asset definition must carry an \`id\` string. | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_DEFAULT_VALUE` | Missing required field 'defaultValue' in asset property | Set the property's \`defaultValue\` field to a value matching its declared \`type\` (same shape as \`value\`). | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_MAX` | Missing required field 'max' in asset property | Provide a numeric \`max\` on a numeric asset property. | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_MIN` | Missing required field 'min' in asset property | Provide a numeric \`min\` on a numeric asset property. | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_OPTIONS` | Missing required field 'options' in asset property | Provide an \`options\` array on an \`enum\`-type asset property. | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_STEP` | Missing required field 'step' in asset property | Provide a numeric \`step\` on a numeric asset property. | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_TYPE` | Missing required field 'type' in asset property | Set the property's \`type\` to a supported value: \`String\`, \`Boolean\`, \`Color\`, \`Enum\`, \`Int\`, \`Float\`, or \`Double\`. | |
| `BINDING.WASM_ASSET_PROPERTY_MISSING_VALUE` | Missing required field 'value' in asset property | Set the property's \`value\` field to a value matching its declared \`type\` (e.g. a number for \`Int\`/\`Float\`/\`Double\`, a string for \`String\`/\`Enum\`). | |
| `BINDING.WASM_ASSET_PROPERTY_UNKNOWN_TYPE` | Unknown property type: \{type} | '\{type}' is not a valid asset property type. Use one of \`String\`, \`Boolean\`, \`Color\`, \`Enum\`, \`Int\`, \`Float\`, or \`Double\`. | |
| `BINDING.WASM_ASSET_RESULT_MISSING_ID` | Missing required field 'id' in asset result | Each asset result must carry an \`id\` string. | |
| `BINDING.WASM_ASSET_RESULT_MISSING_SOURCE_ID` | Missing required field 'context.sourceId' in asset result | Set \`context.sourceId\` on each asset result to the asset source id that produced it. | |
| `BINDING.WASM_BLOCK_STATE_MISSING_ERROR` | Missing required field 'error' in block state | Pass an \`error\` field on a block state with \`type: 'Error'\`. | |
| `BINDING.WASM_BLOCK_STATE_MISSING_PROGRESS` | Missing required field 'progress' in block state | Pass a numeric \`progress\` field on a block state with \`type: 'Pending'\`. | |
| `BINDING.WASM_BLOCK_STATE_MISSING_TYPE` | Missing required field 'type' in block state | Pass a \`type\` field of \`'Ready'\`, \`'Pending'\`, or \`'Error'\` on the block state object. | |
| `BINDING.WASM_BLOCK_STATE_UNHANDLED` | Unhandled state error. | Internal bridge error — file a bug if you encounter this with a reproducible block state. | |
| `BINDING.WASM_BLOCK_STATE_UNKNOWN_ERROR` | Unknown block state error: \{error} | Use one of the documented \`BlockStateError\` enum values. | |
| `BINDING.WASM_BLOCK_STATE_UNKNOWN_TYPE` | Unknown block state type: \{type} | Use one of \`'Ready'\`, \`'Pending'\`, \`'Error'\`. | |
| `BINDING.WASM_COLOR_MISSING_COLOR_SPACE` | Missing colorSpace field in the color object. | Provide a \`colorSpace\` value of \`'sRGB'\`, \`'CMYK'\`, or \`'SpotColor'\` when constructing a color from JavaScript. | |
| `BINDING.WASM_COLOR_MISSING_COMPONENTS` | Missing components field in the color object. | Provide a \`components\` array of channel values matching the color space (3 for sRGB, 4 for CMYK). | |
| `BINDING.WASM_COLOR_MISSING_EXTERNAL_REFERENCE` | Missing externalReference field in the color object. | Set \`externalReference\` (empty string is allowed) when constructing a color from JavaScript. | |
| `BINDING.WASM_COLOR_MISSING_SPOT_NAME` | Missing spotColorName field in the color object. | Set \`spotColorName\` to a non-empty string when constructing a SpotColor. | |
| `BINDING.WASM_COLOR_MISSING_TINT` | Missing tint field in the color object. | Set \`tint\` to a number between 0 and 1 when constructing a color from JavaScript. | |
| `BINDING.WASM_COLOR_PARSE_FAILED` | Could not parse color. | Pass a color object with \`colorSpace\` and the matching channel fields, not a raw value. | |
| `BINDING.WASM_COMMAND_ARG_MAP_FAILED` | Couldn't map value for argument $\{index} while executing command \`\{command}\` > \{message} | Argument types must match the command's declared signature; see the inner message for the failing field. | |
| `BINDING.WASM_COMMAND_SINGLE_ARG_MISMATCH` | Received single argument for command \{command}. Expected \{expected}. | Pass exactly the number of arguments the command expects, as an array. | |
| `BINDING.WASM_FIND_RESULT_MISSING_ASSETS` | Missing required field 'assets' in find assets result | Return an \`assets\` array (may be empty) from the asset source \`findAssets\` callback. | |
| `BINDING.WASM_FIND_RESULT_MISSING_CURRENT_PAGE` | Missing required field 'currentPage' in find assets result | Return a \`currentPage\` integer from the asset source \`findAssets\` callback. | |
| `BINDING.WASM_FIND_RESULT_MISSING_TOTAL` | Missing required field 'total' in find assets result | Return a \`total\` integer from the asset source \`findAssets\` callback. | |
| `BINDING.WASM_FONT_MISSING_SUBFAMILY` | Missing required field 'subFamily' in font | Provide a \`subFamily\` string on each font entry. | |
| `BINDING.WASM_FONT_MISSING_URI` | Missing required field 'uri' in font | Provide a \`uri\` string on each font entry. | |
| `BINDING.WASM_GRADIENT_STOP_MISSING_STOP_VALUE` | Gradient color stop must have a stop value | Each gradient color stop needs a \`stop\` number between 0 and 1. | |
| `BINDING.WASM_MEM_ALLOC_FAILED_BUFFER` | Failed to allocate memory for buffer \{bufferUri} | Reduce the buffer size or grow the WASM memory cap before allocating. | |
| `BINDING.WASM_MEM_ALLOC_FAILED_HANDLE` | Failed to allocate memory for handle \{handle} | Reduce concurrent in-flight engine handles or grow the WASM memory cap. | |
| `BINDING.WASM_PAYLOAD_COLOR_CMYK_MISSING_FIELDS` | \`payload.color\` with \`CMYK\` color space must have fields \`c\`, \`m\`, \`y\`, \`k\`. | Provide \`c\`, \`m\`, \`y\`, \`k\` numeric fields on \`payload.color\` when \`colorSpace\` is \`'CMYK'\`. | |
| `BINDING.WASM_PAYLOAD_COLOR_MISSING_COLOR_SPACE` | \`payload.color\` must have \`colorSpace\` field. | Set \`payload.color.colorSpace\` to \`'sRGB'\`, \`'CMYK'\`, or \`'SpotColor'\`. | |
| `BINDING.WASM_PAYLOAD_COLOR_SPOT_MISSING_FIELDS` | \`payload.color\` with \`SpotColor\` color space must have fields \`name\`, \`externalReference\`, \`representation\`. | Provide \`name\`, \`externalReference\`, and \`representation\` on a \`SpotColor\` payload color. | |
| `BINDING.WASM_PAYLOAD_COLOR_SPOT_REPRESENTATION_INVALID` | A \`payload.color\` with colorSpace \`SpotColor\` must use a \`RGB\` or \`CMYK\` color for its \`representation\`. | Set \`representation.colorSpace\` to \`'sRGB'\` or \`'CMYK'\` on the SpotColor payload. | |
| `BINDING.WASM_PAYLOAD_COLOR_SRGB_MISSING_FIELDS` | \`payload.color\` with \`sRGB\` color space must have fields \`r\`, \`g\`, and \`b\`. | Provide \`r\`, \`g\`, \`b\` numeric fields on \`payload.color\` when \`colorSpace\` is \`'sRGB'\`. | |
| `BINDING.WASM_PROPERTIES_NOT_ARRAY` | 'properties' is not an array. | Pass \`properties\` as a JavaScript array of \`AssetProperty\` objects. | |
| `BINDING.WASM_SOURCE_SET_NOT_ARRAY` | 'sourceSet' is not an array. | Pass \`sourceSet\` as a JavaScript array of source objects. | |
| `BINDING.WASM_TRANSFORM_PRESET_FIXED_ASPECT_MISSING_FIELDS` | 'payload.transformPreset' of type 'FixedAspectRatio' must have fields 'width', and 'height'. | Provide numeric \`width\` and \`height\` fields on a \`'FixedAspectRatio'\` transformPreset. | |
| `BINDING.WASM_TRANSFORM_PRESET_FIXED_SIZE_MISSING_FIELDS` | 'payload.transformPreset' of type 'FixedSize' must have fields 'width', 'height', and 'designUnit'. | Provide numeric \`width\`, \`height\`, and a \`designUnit\` string on a \`'FixedSize'\` transformPreset. | |
| `BINDING.WASM_TRANSFORM_PRESET_MISSING_TYPE` | Missing required field 'type' in asset transformPreset | Set \`transformPreset.type\` to one of \`'FreeAspectRatio'\`, \`'FixedAspectRatio'\`, \`'ContentAspectRatio'\`, \`'FixedSize'\`. | |
| `BINDING.WASM_TRANSFORM_PRESET_UNKNOWN_TYPE` | Unknown transformPreset type: \{type} | Use one of \`'FreeAspectRatio'\`, \`'FixedAspectRatio'\`, \`'ContentAspectRatio'\`, \`'FixedSize'\`. | |
| `BINDING.WASM_TYPEFACE_MISSING_FONTS` | Missing required field 'fonts' in typeface | Provide a non-empty \`fonts\` array on the typeface object. | |
| `BINDING.WASM_TYPEFACE_MISSING_NAME` | Missing required field 'name' in typeface | Provide a \`name\` string on the typeface object. | |
| `BINDING.WASM_UNKNOWN_AUDIO_OUTPUT_TYPE` | Unknown audio output type: \{audioOutput} | Pass one of the documented audio-output type strings. | |
| `BINDING.WASM_UNKNOWN_COLOR_SPACE` | Unknown color space: \{colorSpace} | Pass one of \`'sRGB'\`, \`'CMYK'\`, \`'SpotColor'\`. | |
| `BINDING.WASM_UNKNOWN_CUTOUT_OPERATION` | Unknown CutoutOperation \{op} | Pass one of the documented \`CutoutOperation\` enum values. | |
| `BINDING.WASM_UNKNOWN_CUTOUT_TYPE` | Unknown CutoutType \{type} | Pass one of the documented \`CutoutType\` enum values. | |
| `BINDING.WASM_UNKNOWN_HORIZONTAL_ALIGNMENT` | Unknown horizontal alignment \{alignment}, use Left, Right, or Center | Pass \`'Left'\`, \`'Right'\`, or \`'Center'\`. | |
| `BINDING.WASM_UNKNOWN_SCOPE_STATE` | Unknown GlobalScopeState \{value} | Pass one of the documented \`GlobalScopeState\` enum values. | |
| `BINDING.WASM_UNKNOWN_VERTICAL_ALIGNMENT` | Unknown vertical alignment \{alignment}, use Top, Bottom, or Center | Pass \`'Top'\`, \`'Bottom'\`, or \`'Center'\`. | |
## BLOCK
Design-block lifecycle, properties, hierarchy, animation.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `BLOCK.ALWAYS_ON_BOTTOM_UNSUPPORTED` | The block does not have an always-on-bottom property. | Only specific block types (e.g. scene, page) carry always-on-bottom. Check api.block.getType(block) is a supported container before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ALWAYS_ON_TOP_UNSUPPORTED` | The block does not have an always-on-top property. | Only specific block types (e.g. scene, page) carry always-on-top. Check api.block.getType(block) is a supported container before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_ASSET_MALFORMED_MODE_KEY` | Malformed asset. The mode should be stored in meta under the "mode" key. | Move the animation mode into asset.meta.mode. The current asset shape is rejected by the loader. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_ASSET_MALFORMED_TYPE_KEY` | Malformed asset. The animation type or "none" should be stored in meta under the "type" key. | Move the animation type into asset.meta.type. The value must be the type identifier or the literal string 'none'. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_ASSET_MISSING_MODE` | The asset does not contain a mode property. | Animation assets must declare a 'mode' field in their meta. Add the field to the asset definition. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_ASSET_MISSING_TYPE` | The asset does not contain a type property. | Animation assets must declare a 'type' field in their meta. Add the field to the asset definition. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NOT_A_PAN_ANIMATION` | The selected animation is not a pan animation. | This API only applies to pan-type animations. Switch to a pan animation first or use the generic property setter. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NOT_A_SLIDE_ANIMATION` | The selected animation is not a slide animation. | This API only applies to slide-type animations. Switch to a slide animation first or use the generic property setter. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NOT_IN_TYPE` | The animation \{blockId} cannot be used as an "in" animation. | The animation block is not tagged as an in/out animation. Create an in/out animation via createAnimation with a compatible type. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NOT_LOOP_TYPE` | The animation \{blockId} cannot be used as a "loop" animation. | The animation block is not tagged as a loop animation. Create a loop animation via createAnimation with a compatible type. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NOT_OUT_TYPE` | The animation \{blockId} cannot be used as an "out" animation. | The animation block is not tagged as an in/out animation. Create an in/out animation via createAnimation with a compatible type. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_ANIMATIONS_ON_BLOCK` | The block does not have any animations. | No animation has been applied to this block. Add an animation asset before querying or modifying it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_ANIMATION_TO_EDIT` | The selected block does not have an animation to be edited. | Apply an animation asset to the block before invoking edit APIs. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_EASING_PROPERTY` | The selected animation has no easing property. | The current animation type does not expose an easing setting. Verify the animation type supports easing before setting it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_EASING_PROPERTY_ON_BLOCK` | The target block does not have an animationEasing property. | The current animation type doesn't expose easing. Pick a type that does, or skip the property. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_OVERLAP_PROPERTY` | The target block does not have an textAnimationOverlap property. | textAnimationOverlap only exists on text-animation types. Confirm the animation type first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_TEXT_WRITING_STYLE` | The selected animation has no text writing style property. | Text writing style is only available on text-animation types. Confirm the active animation supports it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_NO_WRITING_STYLE_PROPERTY` | The target block does not have an textAnimationWritingStyle property. | textAnimationWritingStyle only exists on text-animation types. Confirm the animation type first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_TEXT_ONLY` | The animation \{blockId} can only be added to text blocks. | Text animations only apply to text blocks. Either pick a non-text animation or attach this animation to a text block. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_TYPE_NOT_REGISTERED` | Unknown animation type: \{type} | Animation type '\{type}' is not registered. Use createAnimation with a built-in type or register a custom animation first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNKNOWN_EASING` | Unknown easing value: \{value} | Easing '\{value}' is not recognized. Use one of the predefined easing names. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNKNOWN_ENUM` | Unknown enum value: \{value} | The enum value '\{value}' is not a member of the targeted animation property. Inspect the property's valid set via the schema. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNKNOWN_MODE` | Unknown animation mode: \{mode} | Animation mode '\{mode}' is not one of 'In', 'Loop', or 'Out'. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNKNOWN_MODE_IN_ASSET` | The asset contains an unknown mode. | The asset's 'mode' value is not recognized. Valid modes are 'In', 'Loop', and 'Out'. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNKNOWN_PROPERTY_TYPE` | Unknown property type. | The animation property type is not handled by the editor. This likely indicates a stale or unsupported asset definition. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNKNOWN_TYPE` | Unknown animation type. | The animation type identifier is not registered. Confirm the asset 'type' matches a built-in or registered custom animation. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ANIMATION_UNSUPPORTED` | The block does not support being animated. | Only blocks with animation capability accept animation assets. Call api.block.supportsAnimation(block) before applying an animation. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ASSET_SOURCE_SORT_KEY_MISSING` | Sort key not found in asset source. | The asset source does not provide the requested sort key. Use a sort key the source declares. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.AUDIO_TRACK_INDEX_OUT_OF_BOUNDS` | Audio track index \{index} is out of bounds. Valid range: 0-\{max}. | Pass an audio-track index in \[0, \{max}]. Use getAudioTrackCountFromVideo() to inspect the count. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.AUTO_TO_FREE_LAYOUT_UNSUPPORTED` | Switching from an automatic to free layout isn't supported yet. | Once a page or stack is in automatic layout, free-layout switching requires reconstructing the children. Re-create the layout instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BACKGROUND_COLOR_UNSUPPORTED` | The block doesn't have a background color. | Only pages and a handful of container blocks expose a background color. Call api.block.supportsBackgroundColor(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BLEND_MODE_UNSUPPORTED` | The block doesn't have a blend mode. | Only renderable design blocks expose blendMode. Call api.block.supportsBlendMode(block) before setting it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BLOCKS_NOT_COMBINABLE` | Blocks cannot be combined. | Boolean combine requires two or more shape-bearing blocks. Verify the selection contains compatible shapes. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BLUR_UNKNOWN_TYPE` | Unknown blur type: \{type} | Blur type '\{type}' is not registered. Use one of the built-in blur types (uniform, linear, radial, mirrored). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BLUR_UNSUPPORTED` | Target block doesn't support blur. | Only renderable design blocks carry blur. Verify with supportsBlur(block) before applying. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BUFFER_LENGTH_OUT_OF_RANGE` | Length \{length} at offset \{offset} is out of range \[0, \{max}]. | Ensure offset + length \<= \{max}. Either reduce the length or grow the buffer first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BUFFER_NOT_FOUND` | Buffer not found: \{uri} | No buffer is registered at URI '\{uri}'. Call createBuffer or write to the URI before reading. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BUFFER_OFFSET_OUT_OF_RANGE` | Offset \{offset} is out of range \[0, \{max}]. | Pass an offset in \[0, \{max}]. Use the buffer's current size to validate before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.BUFFER_URI_INVALID` | Invalid buffer URI: \{uri} | Buffer URIs must use the 'buffer://' scheme. Re-create the buffer to obtain a valid URI. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CAMERA_DESTRUCTION_NOT_ALLOWED` | Destruction of camera is not allowed. | Cameras are managed by the scene. Destroy the scene, not the camera, to remove camera state. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CAMERA_TRANSFORM_LOCK` | Camera's transform cannot be locked. | Cameras are not lockable. Use scene-level interaction settings to restrict camera movement. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CAPTIONS_DISABLED` | Creation of captions is not allowed. Please enable the video captions feature. | Enable settings.features.videoCaptionsEnabled before creating a caption block. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CAPTION_TRACKS_DISABLED` | Creation of caption tracks is not allowed. Please enable the video captions feature. | Enable settings.features.videoCaptionsEnabled before creating a caption track block. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CHILDREN_INDEX_OUT_OF_BOUNDS` | Index \{index} is out of bounds for \{count} children. | Pass an index in \[0, \{count}]. Use getChildren(block).size() to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COLOR_COMPONENT_OUT_OF_RANGE` | \{component} color component out of range \[0, 1]: \{value}. | Clamp '\{component}' to the \[0, 1] range. NaN is also rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COLOR_NOT_IN_COLOR_SPACE` | The color is not in the '\{colorSpace}' color space. | Read the color via the accessor for its actual color space, or convert it first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COLOR_SPACE_CONVERSION_NOT_SUPPORTED` | Cannot convert to that color space. | Color-space conversion to SpotColor is not defined — convert to sRGB or CMYK instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COLOR_TINT_OUT_OF_RANGE` | Tint out of range \[0, 1]: \{value}. | Clamp the tint to the \[0, 1] range. NaN is also rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COMBINE_TEXT_FONT_LOADING` | Block \{id} is a text block whose fonts have not finished loading. Call forceLoadResources on it before combine. | Wait for the text block's fonts to load (api.block.forceLoadResources) before invoking boolean combine. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COMPONENT_NOT_REGISTERED` | \{component} is not a registered type. | Reflection has no registration for component '\{component}'. Confirm the property string is correct. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.COMPONENT_NO_PROPERTY` | \{component} doesn't have property \{property}. | Component '\{component}' has no field named '\{property}'. Inspect findAllProperties(block) for valid keys. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CONTENT_ASPECT_IMAGE_NO_DIMENSIONS` | ContentAspectRatio: image fill has no usable intrinsic dimensions yet; the image is not decoded and the sourceSet is empty. | Set 'fill/image/sourceSet' with explicit width and height, or wait for the image to decode before applying the preset. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CONTENT_ASPECT_NOT_IMAGE_OR_VIDEO` | ContentAspectRatio: fill is neither an image nor a video; only those carry intrinsic dimensions. | Replace the fill with an image or video fill. Color and gradient fills have no intrinsic dimensions. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CONTENT_ASPECT_NO_FILL` | ContentAspectRatio: block has no fill; only image and video fills carry intrinsic dimensions. | Add an image or video fill to the block before querying its content aspect ratio. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CONTENT_ASPECT_VIDEO_NO_DIMENSIONS` | ContentAspectRatio: video fill has no usable intrinsic dimensions yet; wait for the first frame to decode or set the sourceSet with explicit width and height. | Wait for the first frame to decode, or set 'fill/video/sourceSet' with explicit width and height before applying the preset. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CONTENT_FILL_ALIGNMENT_UNSUPPORTED` | The block doesn't support content fill alignment. | Only blocks with a sized image/video fill support contentFillAlignment. Resolve the fill with api.block.getFill(block) and confirm api.block.getType(fill) is an image or video fill first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CONTENT_FILL_MODE_UNSUPPORTED` | The block doesn't have a content fill mode. | Only blocks with image/video fills expose a content fill mode. Call api.block.supportsContentFillMode(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CREATION_NOT_ALLOWED` | Creation of \{type} is not allowed. | Type '\{type}' is not user-creatable. It is created implicitly by other engine operations. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CUTOUT_NO_BLOCK_SELECTED` | No block selected can be given a cutout. | Cutouts apply to selected graphic blocks. Select at least one cutout-capable block first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.CUTOUT_PATH_REQUIRED` | 'vectorPath' is required for Cutout. | Pass a non-empty 'vectorPath' string when creating a Cutout block. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.DIFFERENCE_NO_EFFECT` | Difference has no visible effect. | The subtracted shape does not intersect the base. Reposition or resize so they overlap. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.DROP_SHADOWS_UNSUPPORTED` | The block doesn't support drop shadows. | Only renderable design blocks carry shadow properties. Call api.block.supportsDropShadow(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.EFFECTS_UNSUPPORTED` | Target block doesn't support effects. | Only renderable design blocks carry an effects stack. Verify with supportsEffects(block) before mutating it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.EFFECT_INDEX_OUT_OF_BOUNDS` | Index \{index} out of bounds (\{count}). | Pass an index in \[0, \{count}]. Use getEffects(block).size() to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.EFFECT_NOT_AN_EFFECT` | Only effects can be enabled and disabled. Use setVisible instead if you are trying to change the visibility of a design block. | Pass an effect block, or use setVisible(block, ...) to toggle a design block's visibility. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.EFFECT_UNKNOWN_TYPE` | Unknown effect type: \{type} | Effect type '\{type}' is not registered. Use a built-in effect type or register a custom one before creating. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ELEMENTS_NOT_ALIGNABLE` | Elements cannot be aligned. | Alignment requires two or more selectable design blocks under a common parent that supports free layout. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ELEMENTS_NOT_DISTRIBUTABLE` | Elements cannot be distributed. | Distribution requires three or more selectable design blocks under a common parent that supports free layout. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ELEMENTS_NOT_GROUPABLE` | Elements cannot be grouped. | Grouping requires two or more design blocks under the same parent. Verify the selection before calling group(). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ENTITY_NOT_LAID_OUT` | Entity \{block} has not been laid out yet. | Call api.scene.update() (or wait for the next frame) so layout has run, then retry. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.ENUM_VALUE_INVALID` | Invalid enum value \{value} for property \{property}. | Use one of the values returned by api.block.getEnumValues('\{property}'). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.EXPORTABLE_UNSUPPORTED` | The block doesn't have exportable option. | Only specific block types track the 'exportable' flag. Check api.block.getType(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_GET_SOLID_COLOR_WRONG_TYPE` | Tried to get solid color fill value on a block that has no such fill color. | The block's fill is not a solid color. Inspect the fill type via getType(getFill(block)) first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_MISSING` | Target block has no Fill. | Attach a fill via setFill(block, fill) or use supportsFill(block) to confirm support. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_NOT_VALID` | Specified fill is not a valid fill. | Create the fill via api.block.createFill(...) and pass that id. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_NO_SOLID_COLOR` | Fill has no solid color. | The fill block is not a SolidColor. Inspect getType(getFill(block)) and use the matching accessor. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_NO_SOLID_COLOR_FILL` | Block has no solid color fill. | Attach a solid-color fill via setFill(block, createFill('color')) before reading the color. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_SET_SOLID_COLOR_WRONG_TYPE` | Tried to set Fill Color on a block with different fill type. | Switch the block's fill to a solid-color fill, or use the matching API for the current fill type. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_TEXT_SOLID_COLOR_ONLY` | Text blocks only support solid color fills. | Use createFill('color') for text blocks. Image, video, and gradient fills are not supported on text. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.FILL_UNSUPPORTED` | The block doesn't have a fill. | Only renderable design blocks expose a fill. Confirm the block type before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.GRADIENT_COLOR_STOPS_DUPLICATE` | Gradient color stops need to have unique values between 0 and 1. Found duplicate value \{value}. | Remove the duplicate stop at \{value} so each stop's position is unique. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.GRADIENT_COLOR_STOPS_NOT_SORTED` | Gradient color stops must be sorted in ascending order. | Sort the stops by position ascending before passing the array. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.GRADIENT_COLOR_STOPS_OUT_OF_RANGE` | Gradient color stops must be between 0 and 1. | Clamp each stop's position into \[0, 1] before passing the array to api.block.setGradientColorStops; positions outside the range are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.GRADIENT_COLOR_UNSUPPORTED` | The block does not have a gradient color property. | Only gradient-fill blocks expose gradient color stops. Resolve the fill with api.block.getFill(block) and confirm api.block.getType(fill) is a gradient fill first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.GROUP_ABSOLUTE_ONLY` | Block is a group and can only have Absolute position mode. | Groups always use absolute layout. Don't set a relative position mode on a group block. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.HEIGHT_INVALID_FOR_SCALING` | Current height is invalid for scaling. | The block's current height is zero or non-finite. Set a valid height before scaling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.HISTORY_HANDLE_INVALID_AT` | \{handle} is not a valid history handle. | The history handle is unknown or has been released. Re-obtain it via createHistory() before calling. | [Undo And History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) |
| `BLOCK.ID_INVALID` | The block with ID \{block} is not valid. It may have been deleted, e.g., when a new scene was loaded. | Re-resolve the block id after scene changes. Use isValid(block) to guard before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.IMAGE_FILL_UNSUPPORTED` | The block does not have an image fill property. | Only blocks with an image fill expose this property. Resolve the fill with api.block.getFill(block) and confirm api.block.getType(fill) is an image fill before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.IMAGE_LOAD_FAILED` | Failed to load image. | The image source could not be decoded or fetched. Verify the URI is reachable, the MIME type is one the engine supports, and the bytes are a valid image. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.KEY_NOT_FOUND` | Key \{key} on block \{block} not found. | Property '\{key}' is not defined on block \{block}. Use findAllProperties(block) to enumerate valid keys. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.MISSING_REQUESTED_COMPONENT` | The block doesn't have the requested properties. | The requested component is not attached to this block. Use the matching api.block.supports\*(block) query (or check api.block.getType(block)) to confirm the feature before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NAME_UNSUPPORTED` | The block does not have a name. | Only renderable design blocks carry a name. Check api.block.getType(block) is a design block before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_ATTACHED_TO_SCENE` | Block \{block} is not attached to a scene. | Append the block under the active scene (or one of its descendants) before calling this API. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_A_CUTOUT_BLOCK` | The block \{id} is not a Cutout block. | This API requires a cutout block. Use createCutout or filter by '//ly.img.ubq/cutout' first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_A_DESIGN_BLOCK` | Block is not a design block. | This API requires a design block (page, graphic, text, audio, video, etc.). Fills, shapes, and animations are not design blocks. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_A_TEXT_BLOCK` | Entity \{block} is not a text block. | Pass a text block id. Use getType(block) to confirm the block type before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_A_TEXT_BLOCK_SIMPLE` | block is not a text block. | Pass a text block. Use getType(block) to confirm the block type before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_A_VIDEO_FILL_BLOCK` | The provided block must be a video fill block or a block with a video fill. | Pass a video fill block directly, or a design block whose fill is a video. Use getFill(block) to resolve. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_KNOWN_BLOCK_TYPE` | \{type} is not a known block type. | Block type '\{type}' is not registered. Use findAllTypes() to enumerate supported types. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT` | Block hasn't been laid out yet. | Call api.scene.update() (or wait for the next frame) so layout has run, then retry. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_AABB` | Could not get AABB. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before querying the AABB. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_BEFORE_ADJUST_CROP` | Block has not been laid out yet. Call update() before adjustCropToFillFrame(). | adjustCropToFillFrame() requires a laid-out frame. Run api.scene.update() first, then retry. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_FLIP_H` | Could not flip horizontally. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before flipping. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_FLIP_V` | Could not flip vertically. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before flipping. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_FOR_SCENE` | The block \{id} has not been layouted and may therefore not be part of the scene. | Confirm block \{id} is attached and the scene has been updated. Trigger an explicit update if the block was just added. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_ROTATION` | Could not set rotation. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before setting rotation. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_LAID_OUT_SCALE` | Could not scale. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before scaling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NOT_VALID` | Block is not valid. | The block id no longer references a live block. It may have been destroyed; re-resolve the id before calling this API. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_AUDIO_TRACKS_FOUND` | No audio tracks found in the video. | The video container reports zero audio tracks. Use a source that includes audio or skip this operation. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_AUDIO_TRACKS_IN_VIDEO` | The video does not contain any audio tracks. | The decoded video has no audio streams. Confirm the source file actually contains audio. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_BLOCK_SELECTED` | No block is selected. | This API requires a selected block. Use api.block.findAllSelected() to confirm a selection before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_KIND` | Block does not have a kind. | Only design blocks expose a 'kind' property. Check api.block.getType(block) to confirm it is a design block (not a fill/shape/animation) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_PARENT` | Block doesn't have a parent. | The block is detached (e.g. the scene root). Attach it to a parent or guard with hasParent(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_POSITION` | Block has no defined position. | Position requires a Position component. Auto-layout containers or unattached blocks may not expose one. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_SHAPE_PROPERTY` | Target block has no shape property. | Only blocks with a shape component (graphic, vector) expose a shape property. Call api.block.supportsShape(block) before accessing the shape. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.NO_SIZE` | Block has no defined size. | Size requires a Size component. Confirm the block is a sized type (page, graphic, text) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.OPACITY_UNSUPPORTED` | The block doesn't have an opacity. | Only renderable design blocks expose opacity. Call api.block.supportsOpacity(block) before setting it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.OPERATION_PRECONDITION_FAILED` | \{reason} | The structural precondition check returned a free-text reason — see \`reason\` for the specific blocker (e.g. mixed parents, incompatible block types, missing layout). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.OP_NEEDS_TWO_BLOCKS` | Not enough blocks to perform operation. Must be at least two. | Select at least two blocks before invoking this operation. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PARENT_NOT_LAID_OUT` | Parent block hasn't been laid out yet. | Layout has not run for the parent. Trigger an update or wait one frame before reading relative geometry. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PENDING_PROGRESS_INVALID` | Pending progress must be between 0 and 1. | Clamp the progress value to \[0, 1] before passing it to setPendingProgress. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.POSITION_LOCKED` | Block's position is locked and can't be modified. | Call setPositionLocked(block, false) before changing the position. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.POSITION_PARENT_CONTROLLED` | Block's position is controlled by the parent block and can't be modified. | The block's parent uses an automatic layout. Detach the block, switch the parent to free layout, or modify the parent instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_ENUM_CAST_FAILED` | \{property} cannot be cast to an enum value | The property value does not convert to an unsigned 32-bit integer. The property may not actually be an enum type, or its reflected representation is misconfigured. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_ENUM_MEMBER_CAST_FAILED` | \{member} cannot be cast to an enum value | An enum member's value does not convert to an unsigned 32-bit integer. The reflected enum is misconfigured. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_FONT_FILE_URI_DIRECT_UNSUPPORTED` | Setting the "text/fontFileUri" property directly is unsupported. Use the setFont API instead. | Call setFont(block, fontFileUri, typefaceName) so the typeface, weight and style stay consistent. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_GETTER_MISMATCH` | Incorrect function used, expected get\{kind}. | The property's type is '\{kind}'. Call the matching get\{kind}(...) accessor instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_INVALID_ENUM_VALUE` | \{property} holds an invalid enum value. | The property's integer value does not match any registered enum member. The block's stored state may be corrupt. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_INVALID_ENUM_VALUE_CHOICES` | Invalid enum value, expected one of: \[\{choices}] | Pass one of the listed enum identifiers. The catalog \`choices\` arg carries the comma-separated quoted list the setter would have accepted. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_NOT_AN_ENUMERATION` | Property's type is not an enumeration. | Querying the allowed enum choices is only valid for enum-typed properties. Check the property's \`PropertyType\` first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_NOT_ENUM` | \{property} is not an enum property. | \`getPropertyAllowedValues(...)\` / enum accessors only apply to enum-typed properties. Check the property type first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_NOT_FOUND` | Property not found: "\{property}". | Inspect api.block.findAllProperties(block) for the set of properties this block exposes. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_NOT_FOUND_WITH_HINT` | \{prefix}Valid properties, as listed by \`findAllProperties(id)\`, are: \{properties}. | Pick one of the listed properties or call findAllProperties(block) to inspect the live set. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_NOT_READABLE` | Property is not readable. | This property is write-only on the requested block type. Use the matching setter instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_NOT_WRITEABLE` | Property is not writeable. | This property is read-only on the requested block type. Inspect the value with the matching getter instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.PROPERTY_SETTER_MISMATCH` | Incorrect function used, expected set\{kind}. | The property's type is '\{kind}'. Call the matching set\{kind}(...) accessor instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.RESULT_EMPTY_SHAPE` | Result is an empty shape. | The boolean operation produced no visible geometry. Adjust the inputs so they overlap appropriately. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SCENE_CREATE_DIFFERENT` | To create a scene, use \`createScene\` instead. | Scenes have a dedicated lifecycle. Call api.scene.create() rather than block.create('//ly.img.ubq/scene'). | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `BLOCK.SCOPES_UNSUPPORTED` | Block \{block} does not support scopes. | Scope APIs require a block type that carries access control. Most renderable design blocks do; pages and tracks typically do not. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SCOPE_INVALID` | Invalid scope: \{scope} | The scope name '\{scope}' is not recognized. Use one of the documented scope keys (e.g. 'design/style', 'editor/select'). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SCOPE_MIXED_VALUES` | Not all underlying scopes of \{scope} have the same value. | The compound scope '\{scope}' aggregates multiple keys that currently disagree. Set them individually or accept the indeterminate state. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SCOPE_PERMISSION_DENIED` | \{message} | Unlock the scope '\{scope}' via setScopeEnabled(block, '\{scope}', true) before invoking this API. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SELECTION_DISABLED` | Selection disabled for block. | The block's selectability has been disabled via setSelectable(block, false). Re-enable to select it programmatically. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SHADOW_X_BLUR_INVALID` | The x-blur radius must be a finite number greater than or equal to 0. | Pass a non-negative finite x-blur. NaN and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SHADOW_X_OFFSET_INVALID` | The x-offset must be a finite number. | Pass a finite x-offset. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SHADOW_Y_BLUR_INVALID` | The y-blur radius must be a finite number greater than or equal to 0. | Pass a non-negative finite y-blur. NaN and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SHADOW_Y_OFFSET_INVALID` | The y-offset must be a finite number. | Pass a finite y-offset. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SHAPE_NOT_VALID` | Specified shape is not a valid shape. | The provided block id does not reference a shape sub-block. Use createShape or pick a registered shape type. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SOME_ELEMENTS_NOT_LOADED` | Some elements are not completely loaded. | Wait for the scene's resources to finish loading. Subscribe to resource events or poll api.scene.loadingState. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SOURCE_SET_EMPTY` | The source set is empty. | Populate the source set via addSource(...) before invoking source-set APIs. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.STROKES_UNSUPPORTED` | The block doesn't support strokes. | Only renderable design blocks carry stroke properties. Verify with supportsStroke(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.STROKE_DASH_ARRAY_INVALID` | Stroke dash array values must be finite numbers. | Replace any NaN/infinity entries in the dash array with finite numbers. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.STROKE_DASH_OFFSET_INVALID` | Stroke dash offset must be a finite number. | Pass a finite numeric dash offset. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.STROKE_MISSING` | Target block has no Stroke. | Enable the stroke via setStrokeEnabled(block, true) before mutating stroke properties. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.STROKE_WIDTH_INVALID` | The stroke width must be a finite number greater than or equal to 0. | Pass a non-negative finite width. NaN and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.SVG_PATH_PARSE_FAILED` | The SVG path could not be parsed. | Pass a valid SVG path string (e.g. "M10 10 L20 20 Z"). Validate with an SVG path parser if unsure. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TARGET_NOT_AN_ANIMATION` | The target block is not an animation. | Pass an animation block id. Use getType(block) to confirm before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TARGET_NOT_A_VIDEO_FILL` | The target block is not a video fill. | Pass a video fill block. Use getFill(block) on a design block to resolve it. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_CANNOT_TOGGLE_BOLD` | The block cannot be toggled between bold and normal font weights. | The current typeface does not expose both regular and bold weights. Pick a typeface that does. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_CANNOT_TOGGLE_ITALIC` | The block cannot be toggled between italic and normal font styles. | The current typeface does not expose both normal and italic styles. Pick a typeface that does. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_INVALID_FONT_SIZE` | The font size must be a positive finite number. | Pass a positive finite font size. NaN, zero, and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_INVALID_KERNING` | Kerning must be a finite number. | Pass a finite kerning value. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_INVALID_LINE_INDEX` | Invalid line index: \{lineIndex}. | Pass a line index in \[0, lineCount(block) - 1]. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_INVALID_RANGE_FOR_LINE` | Invalid text range for line index: \{lineIndex}. | Wait for layout to settle (api.scene.update()) before requesting text ranges for a line. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_LINE_BOUNDS_FAILED` | Failed to calculate line bounds for line index: \{lineIndex}. | Ensure the block has been laid out (api.scene.update()) before requesting line bounds. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_LINE_HEIGHT_INVALID` | lineHeight must be greater than zero. | Pass a positive lineHeight. Zero and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_LIST_LEVEL_NEGATIVE` | The list level must be non-negative. | Pass a list level in \[0, 4]. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_LIST_LEVEL_TOO_LARGE` | The list level must be less than 5. | Pass a list level in \[0, 4]. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_NO_BLOCK_BEING_EDITED` | No text block is currently being edited. | Enter text editing mode (e.g. via setEditMode('Text')) before calling cursor/composition APIs. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_NO_TYPEFACE` | block has no typeface. | Assign a typeface to the block via setFont() before invoking this API. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_NO_TYPEFACE_AND_DEFAULT_NOT_REGISTERED` | block has no typeface set and the default font is not registered as a typeface. | Either set a typeface explicitly via setFont() or register the default font as a typeface. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_ON_PATH_INVALID_SVG_PATH` | svgPath is not a valid SVG path string. | Pass a non-empty, parseable SVG path (e.g. 'M 0,0 L 100,0'). Validate the string before calling setTextOnPath. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_ON_PATH_MULTIPLE_SUBPATHS` | svgPath must contain exactly one subpath (no multiple 'M' commands). | Text on path follows a single contour. Split the path or pass only one subpath (a single leading 'M'). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_ON_PATH_NO_MEASURABLE_CONTOUR` | svgPath contains no measurable contour. | The path has zero length (e.g. a single 'M' with no draw commands). Provide a path with a real, measurable contour. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_PARAGRAPH_INDEX_NEGATIVE` | paragraphIndex must be non-negative. | Use a paragraph index >= 0. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_PARAGRAPH_INDEX_OUT_OF_RANGE` | paragraphIndex is out of range. | Pass a paragraph index in \[0, paragraphCount(block) - 1]. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_RANGE_FROM_OUT_OF_RANGE` | The \`from\` index is out of range. | Clamp 'from' to a non-negative index within the block's character count. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_RANGE_INVALID_ORDER` | Invalid range: from (\{from}) cannot be greater than to (\{to}). | Swap or adjust the bounds so 'from' \<= 'to'. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_RANGE_NEGATIVE` | Invalid range: from (\{from}) and to (\{to}) must be -1 or non-negative. | Use -1 to signal 'end of text' or pass a non-negative index for either bound. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_RANGE_TO_OUT_OF_RANGE` | The \`to\` index is out of range. | Clamp 'to' to an index within the block's character count. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_TYPEFACE_UPDATE_FAILED` | Failed to update text typeface: \{reason} | Inspect \`reason\` for the underlying text-shaping failure (typically font fallback, glyph coverage, or asset registry issues). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_UNKNOWN_TYPEFACE` | block has an unknown typeface: '\{typeface}'. | Register the typeface '\{typeface}' with the engine, or pick one that has already been registered. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_UNSUPPORTED_FONT_STYLE` | The block does not support the given style. Failed to find a font with the specified style. | Pick a style that the current typeface supports. Inspect getTextFontStyles(block) for the available set. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TEXT_UNSUPPORTED_FONT_WEIGHT` | The block does not support the given weight. Failed to find a font with the specified weight. | Pick a weight that the current typeface supports. Inspect getTextFontWeights(block) for the available set. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.THRESHOLD_NOT_FINITE` | The threshold values must be a finite number. | NaN and infinity are not accepted as threshold inputs. Pass finite floating-point values. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSFORM_LOCKED_FILL_PARENT` | Block's transform is locked and can't fill its parent. | Call setTransformLocked(block, false) before invoking fillParent. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSFORM_LOCKED_FLIP` | Block's transform is locked and can't be flipped. | Call setTransformLocked(block, false) before flipping. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSFORM_LOCKED_RESIZE` | Block's transform is locked and can't be resized. | Call setTransformLocked(block, false) before resizing, or pick a different block. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSFORM_LOCKED_ROTATE` | Block's transform is locked and can't be rotated. | Call setTransformLocked(block, false) before rotating. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSFORM_LOCKED_SCALE` | Block's transform is locked and can't be scaled. | Call setTransformLocked(block, false) before scaling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSITION_BLOCK_INVALID` | Block \{blockId} is not a valid transition. | The block passed to setTransition does not exist or has no block type. Create the transition via createTransition first. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSITION_BLOCK_NOT_A_TRANSITION` | Block \{blockId} is not a transition. | The block passed to setTransition is not a transition block. Create one via createTransition and pass that instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TRANSITION_TYPE_NOT_REGISTERED` | Unknown transition type: \{type} | Transition type '\{type}' is not registered. Use createTransition with one of the built-in transition types. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_BE_ARRANGED` | Object of type \{type} cannot be arranged. | Arrange operations (bringToFront, etc.) apply only to design blocks within a container. Type \{type} doesn't qualify. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_BE_CLIPPED` | Object of type \{type} cannot be clipped. | Type \{type} has no clipping support. Wrap it in a group or page and apply clipping there. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_BE_FLIPPED` | Object of type \{type} cannot be flipped. | Type \{type} has no flip capability. Apply the flip to its parent or wrap in a group. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_BE_LOCKED` | Object of type \{type} cannot be locked. | Type \{type} has no lock capability. Locking only applies to design blocks. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_BE_PLACEHOLDER` | Object of type \{type} cannot be a placeholder. | Placeholders only apply to design blocks with content (graphic, text). Type \{type} cannot host placeholder behavior. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_BE_SELECTED` | Object of type \{type} cannot be selected. | Type \{type} is not user-selectable. Use a different selection target. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_CANNOT_HAVE_ROTATION` | Object of type \{type} cannot have a rotation. | Type \{type} does not support rotation. Wrap it in a group or rotate its parent instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NOT_A_CHILD` | Cannot add block of type \{type} as a child of another block. | Blocks of type \{type} are render blocks and must be attached via their dedicated property (e.g. fill, shape) rather than as a generic child. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_ADDING_CHILDREN` | Cannot add children to a block of type \{type}. | Blocks of type \{type} are not containers. Use a page, group, or track block to compose children. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_CHILDREN` | Object of type \{type} cannot have children. | Blocks of type \{type} are leaves in the scene tree. Use a container type (page, group, track) to hold children. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_FRAME` | Object of type \{type} does not have a frame. | Only design blocks with computed layout expose a frame. Cameras, scenes, fills, and shapes do not. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_GROUPS` | Object of type \{type} does not support groups. | Type \{type} cannot be grouped. Only design blocks under a common parent can be grouped. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_HIGHLIGHTING` | Object of type \{type} does not support highlighting. | Highlighting only applies to design blocks. Type \{type} cannot be highlighted. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_PARENT` | Object of type \{type} cannot have a parent. | Blocks of type \{type} are render blocks (fills, shapes, blurs, effects). They live alongside design blocks rather than under them. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_PLACEHOLDER_BEHAVIOR` | Object of type \{type} does not support placeholder behavior. | Type \{type} cannot be configured with placeholder hints or controls. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_PLACEHOLDER_CONTROLS` | Object of type \{type} does not have placeholder controls. | Type \{type} has no placeholder-related properties. Choose a design block that supports placeholders (graphic, text). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_POSITION` | Object of type \{type} cannot have a position. | Blocks of type \{type} have no position property. Render blocks and scenes are positioned implicitly. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_ROTATION` | Object of type \{type} does not have a rotation. | Rotation requires a Rotation component, which is not present on type \{type}. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_SIZE` | Object of type \{type} cannot have a size. | Blocks of type \{type} have no size property. Cameras, scenes, fills, and shapes are sized implicitly via other blocks. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_NO_VISIBILITY_STATE` | Object of type \{type} does not have any visibility state. | Type \{type} is always visible (cameras, scenes). Use show/hide on a design block instead. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.TYPE_PERMANENTLY_NON_SELECTABLE` | Object of type \{type} is permanently non-selectable. | Type \{type} is engine-managed and cannot be made selectable. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.UNION_NO_EFFECT` | Union has no visible effect. | The shapes already overlap or are identical. Pick distinct shapes to see a union result. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.UNKNOWN` | Block \{block} is unknown. | The block id no longer references a live design block. Re-resolve the id, or guard with isValid(block) before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.UNKNOWN_BLOCK_TYPE` | Unknown block type: \{type} | Block type '\{type}' is not registered. Use a built-in type or register a custom type before calling create(). | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.UNKNOWN_FILL_TYPE` | Unknown fill type: \{type} | Fill type '\{type}' is not registered. Supported types: color, gradient/linear, gradient/radial, gradient/conical, image, video, pixelStream. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.UNKNOWN_SHAPE_TYPE` | Unknown shape type: \{type} | Shape type '\{type}' is not registered. Use a built-in shape type or register a custom one before creation. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.UUID_UNSUPPORTED` | The block does not have a UUID. | Only renderable design blocks carry a UUID. Check api.block.getType(block) is a design block before calling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VALUES_NOT_FINITE_THREE` | The values \{first}, \{second}, and \{third} must be finite numbers. | All three values must be finite. Replace any NaN or infinity with a concrete number. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VALUES_NOT_FINITE_TWO` | The values \{first} and \{second} must be a finite number. | Both values must be finite. Replace any NaN or infinity with a concrete number. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VALUE_NOT_FINITE` | The value \{value} must be a finite number. | Pass a finite numeric value. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VALUE_NOT_FINITE_IN_UNIT_RANGE` | The value \{value} must be a finite number in the range \[0, 1]. | Clamp the value to \[0, 1] before passing it in. NaN and infinities are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VALUE_NOT_NUMBER` | The value \{value} must be a number. | Pass a numeric value. Strings, NaN, and infinities are rejected. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VARIABLE_NOT_FOUND` | Variable with name "\{name}" not found. | Variable '\{name}' has not been declared. Call setVariable(name, value) once before reading. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VIDEO_FILL_NO_URI` | The video fill block does not have a valid file URI. | Set a non-empty fileURI on the video fill via setURI() before invoking this API. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VIDEO_LOAD_FAILED` | Failed to load video. | The video source could not be decoded or fetched. Verify the URI is reachable, the container/codec is supported, and the bytes are a valid video. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VIDEO_RESOURCE_NOT_LOADED` | The video resource has not been loaded yet. | Wait for the video resource to reach Ready, or call forceLoadAVResource(block, callback) before retrying. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.VIDEO_RESOURCE_NOT_LOADED_FOR_OPERATION` | The video resource has not been loaded yet. Please ensure the video is loaded before \{operation}. | Await the resource-loaded signal (e.g. the \`'audio'\`/\`'video'\` block-state transition to \`Ready\`) before invoking \`\{operation}\`. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
| `BLOCK.WIDTH_INVALID_FOR_SCALING` | Current width is invalid for scaling. | The block's current width is zero or non-finite. Set a valid width before scaling. | [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) |
## CODEC
Audio/video codec capability and decoding.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `CODEC.ANDROID_AUDIO_ENCODER_CREATE_FAILED` | Cannot create audio encoder: media\_status = \{mediaStatus} | Android MediaCodec returned media\_status=\{mediaStatus}. Consult the AMediaCodec status codes. | |
| `CODEC.ANDROID_JNI_ERROR` | \{reason} | The Android MediaCodec/JNI layer reported: \{reason} | |
| `CODEC.APPLE_OSSTATUS_FAILURE` | \{operation} failed (\{status}) | The Apple media-framework call '\{operation}' returned a non-success status: \{status}. The codec, container, or hardware session could not be configured. | |
| `CODEC.AUDIO_DECODER_CREATE_FAILED` | Could not create audio decoder: \{reason} | The platform's audio decoder rejected the configuration. Reason: \{reason} | |
| `CODEC.AUDIO_DECODER_FATAL` | Encountered fatal audio decoder error: \{reason} | The audio decoder produced an unrecoverable error during playback. Reason: \{reason} | |
| `CODEC.AUDIO_DECODER_METADATA_INVALID` | Invalid audio metadata. | The audio metadata required to construct the decoder is missing or invalid. | |
| `CODEC.AUDIO_DECODER_NO_CHUNKS` | Audio has no chunks. | The audio resource has no decoded chunks available. Ensure the source has audible data. | |
| `CODEC.AUDIO_DECODER_NO_FRAMES` | Audio has no frames. | The audio resource has no decoded frames. The source may be empty or unparseable. | |
| `CODEC.AUDIO_DECODER_ZERO_FRAMES_PER_CHUNK` | Frames per chunk is zero. | Audio chunking metadata is invalid; cannot decode. Re-encode the source. | |
| `CODEC.AUDIO_DECODER_ZERO_FRAMES_PER_PACKET` | Frames per packet is zero. | Audio packetization metadata is invalid; cannot construct the decoder. Re-encode the source. | |
| `CODEC.AUDIO_DECODE_UNSUPPORTED` | Audio decoding is not supported on this platform. | This build does not include an audio decoder. Rebuild with the relevant codec backend, or run on a supported platform. | |
| `CODEC.AUDIO_ENCODER_CONFIG_INVALID` | Invalid audio encoder configuration: \{channels} channels at \{sampleRate} Hz | Audio channels and sample rate must be positive. Got channels=\{channels}, sampleRate=\{sampleRate}. | |
| `CODEC.AUDIO_ENCODER_CREATE_FAILED` | Could not create audio encoder: \{reason} | The platform's audio encoder rejected the configuration. Reason: \{reason} | |
| `CODEC.AUDIO_ENCODE_UNSUPPORTED` | Audio encoding is not supported on this platform. | This build does not include an audio encoder. Rebuild with the relevant codec backend, or run on a supported platform. | |
| `CODEC.AUDIO_TRACK_NOT_FOUND` | Couldn't find audio track in AVContainer. | The container has no audio track. Use a media file with an audio stream. | |
| `CODEC.BACKEND_TEXTURE_INCOMPLETE` | Backend texture is incomplete. | The Skia backend texture is missing state required for codec read-back. | |
| `CODEC.EMPTY_AUDIO_CODEC_STRING` | Empty audio codec string. | The container did not declare an audio codec. Re-encode the file with explicit codec metadata. | |
| `CODEC.EMPTY_VIDEO_CODEC_STRING` | Empty video codec string. | The container did not declare a video codec. Re-encode the file with explicit codec metadata. | |
| `CODEC.ENCODER_STATE_NOT_FOUND` | Could not find encoder state. | The encoder state was released. Construct a fresh encoder before calling this API. | |
| `CODEC.GSTREAMER_CREATE_ELEMENT_FAILED` | Could not create GStreamer element \{name} of type \{factory} | GStreamer factory '\{factory}' is not installed. Install the corresponding gst plugin or pick a different element. | |
| `CODEC.GSTREAMER_CREATE_SINK_CAPS_FAILED` | Could not create sink caps. | GStreamer rejected the sink caps. The target codec or format may not be supported. | |
| `CODEC.GSTREAMER_CREATE_SOURCE_CAPS_FAILED` | Could not create source caps. | GStreamer rejected the source caps. The codec or media format may not be supported. | |
| `CODEC.GSTREAMER_LINK_AUDIO_ENCODER_FAILED` | Could not link audio encoder pipeline elements. | GStreamer could not connect the audio encoder branch. Verify supported caps. | |
| `CODEC.GSTREAMER_LINK_AUDIO_FAILED` | Could not link audio pipeline elements. | GStreamer could not connect audio elements. Caps negotiation failed. | |
| `CODEC.GSTREAMER_LINK_VIDEO_ENCODER_FAILED` | Could not link video encoder pipeline elements. | GStreamer could not connect the video encoder branch. Verify supported caps. | |
| `CODEC.GSTREAMER_LINK_VIDEO_FAILED` | Could not link video pipeline elements. | GStreamer could not connect video elements. Caps negotiation failed. | |
| `CODEC.GSTREAMER_PIPELINE_CREATE_FAILED` | Could not create a new GStreamer pipeline. | gst\_pipeline\_new() failed. The GStreamer runtime may be in a bad state. | |
| `CODEC.GSTREAMER_PIPELINE_ERROR` | \{reason} | GStreamer pipeline reported: \{reason} | |
| `CODEC.GSTREAMER_UNEXPECTED_MESSAGE` | Unexpected message error type. | The GStreamer bus reported an unexpected message kind. This may indicate a pipeline state mismatch. | |
| `CODEC.METAL_TEXTURE_FROM_IOSURFACE_FAILED` | Failed to create Metal texture from IOSurface. | Metal rejected the IOSurface-backed texture descriptor. The pixel format or surface dimensions may be unsupported. | |
| `CODEC.NO_CONTEXT` | No context. | The compute context is not initialized. | |
| `CODEC.NO_GPU_CONTEXT` | No GPU context. | The compute context lacks GPU support required for this codec. | |
| `CODEC.OFFSCREEN_CANVAS_CREATE_FAILED` | Could not create offscreen canvas. | Offscreen canvas allocation failed. Out-of-memory or unsupported size is the likely cause. | |
| `CODEC.OFFSCREEN_CONTEXT_UNAVAILABLE` | Cannot get offscreen context for encoding. | The compute context is not an offscreen (Metal) context. Video encoding requires an offscreen GPU context. | |
| `CODEC.PIXEL_BUFFER_NO_IOSURFACE` | CVPixelBuffer has no IOSurface backing. | The pooled pixel buffer is not IOSurface-backed, so it cannot be wrapped as a Metal texture. The pool was created without the IOSurface property. | |
| `CODEC.PIXEL_BUFFER_POOL_CREATE_FAILED` | Failed to create CVPixelBufferPool. | CoreVideo could not allocate a pixel buffer pool for the encoder. Out-of-memory or an unsupported pixel format is the likely cause. | |
| `CODEC.PRESENTATION_TIMESTAMPS_NOT_UNIQUE` | Presentation timestamps are not unique. | The video track has duplicate presentation timestamps, which prevents building a frame index. Re-mux the source with monotonically increasing PTS. | |
| `CODEC.RECORDING_CONTEXT_UNAVAILABLE` | Could not obtain a recording context. | The Skia display canvas has no GPU recording context. The compute context may not be initialized for hardware video decoding. | |
| `CODEC.UNKNOWN_AUDIO_DECODER_HANDLE` | Unknown audio decoder handle. | The audio decoder handle is not registered. It may have been destroyed already. | |
| `CODEC.UNKNOWN_CODEC_STRING` | Unknown codec string \{codec} | The codec identifier '\{codec}' is not recognized by this build. Pass a supported codec string (e.g. an H.264 video or AAC audio identifier) when configuring the export. See the supported codecs list for valid identifiers. | [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) |
| `CODEC.UNKNOWN_VIDEO_DECODER_HANDLE` | Unknown video decoder handle \{handle} | The decoder handle is not registered. It may have been destroyed already. | |
| `CODEC.UNKNOWN_VIDEO_DECODER_HANDLE_NO_ARG` | Unknown video decoder handle. | The decoder handle is not registered. It may have been destroyed already. | |
| `CODEC.UNREACHABLE_FOR_CODEC` | Unreachable code reached for codec \{codec} | Internal switch did not handle codec '\{codec}'. This is an engine bug — please file a ticket. | |
| `CODEC.UNSUPPORTED_CODEC_STRING` | Unsupported codec string \{codec} | Codec '\{codec}' is recognized but not supported in the current configuration. Choose a widely supported codec such as H.264 for video or AAC for audio. See the supported codecs list for the full set. | [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) |
| `CODEC.UNSUPPORTED_H265_CODEC_STRING` | Unsupported H265 codec string \{codec} | The H.265 profile encoded in '\{codec}' is not supported by GStreamer in this build. See the supported codecs list for alternatives. | [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) |
| `CODEC.VIDEO_BITRATE_INVALID` | Video bitrate must be equal or greater than 0. | Pass a non-negative \`bitrate\` value to the video encoder configuration. | |
| `CODEC.VIDEO_DECODER_CREATE_FAILED` | Could not create video decoder: \{reason} | The platform's video decoder rejected the configuration. Reason: \{reason} | |
| `CODEC.VIDEO_DECODER_FATAL` | Encountered fatal video decoder error: \{reason} | The decoder produced an unrecoverable error during playback. Reason: \{reason} | |
| `CODEC.VIDEO_DECODER_UNRESPONSIVE` | Video decoder has been unresponsive for more than 10 seconds. | Possible decoder deadlock or hardware stall. Reset the decoder or fall back to a software path. | |
| `CODEC.VIDEO_DECODE_UNSUPPORTED` | Video decoding is not supported on this platform. | This build does not include a video decoder. Rebuild with the relevant codec backend, or run on a supported platform. | |
| `CODEC.VIDEO_ENCODER_BUSY` | Already encoding another video. | Wait for the current video encoding session to finish before starting another. | |
| `CODEC.VIDEO_ENCODER_CREATE_FAILED` | Could not create video encoder: \{reason} | The platform's video encoder rejected the configuration. Reason: \{reason} | |
| `CODEC.VIDEO_ENCODER_INVALID_RESOLUTION` | Invalid resolution for video encoder: \{width} x \{height} | Resolution must be > 0 in both dimensions. Got \{width}x\{height}. | |
| `CODEC.VIDEO_ENCODE_UNSUPPORTED` | Video encoding is not supported on this platform. | This build does not include a video encoder. Rebuild with the relevant codec backend, or run on a supported platform. | |
| `CODEC.VIDEO_SESSION_CREATE_FAILED` | VTCompressionSessionCreate failed. | VideoToolbox could not create a compression session for the requested codec and resolution. The codec may be unsupported on this hardware. | |
| `CODEC.VIDEO_TRACK_NOT_FOUND` | Couldn't find video track in AVContainer. | The container has no video track. Use a media file with a video stream. | |
| `CODEC.WEBCODECS_INVALID_FORMAT` | Invalid codec string format for WebCodecs: \{codec} | WebCodecs requires a fully-qualified codec id. '\{codec}' is not in the expected format. | |
| `CODEC.WEBCODECS_NOT_AVAILABLE_NODE` | WebCodecs API is not available in Node.js. | Run in a browser environment, or use a different codec backend on Node. | |
| `CODEC.WEBCODECS_NOT_SUPPORTED` | WebCodecs API is not supported. | The current browser does not expose the WebCodecs API. Upgrade to a supported version. | |
## COMPUTE
Compute contexts (Metal, GL, CPU) and capability negotiation.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `COMPUTE.COLOR_SPACE_BIT_DEPTH_UNSUPPORTED` | The current device does not support the required pixel bit depth for the requested color space. | Wide-gamut color spaces require 10+ bpp surfaces. Use sRGB on devices that don't expose deep-color buffers. | |
| `COMPUTE.COLOR_SPACE_DISPLAY_UNSUPPORTED` | The current device does not support displaying the requested color space. | The device's display pipeline cannot render this color space. Fall back to sRGB or query supported spaces first. | |
| `COMPUTE.COLOR_SPACE_UNSUPPORTED_BY_ENGINE` | The engine does not support the requested color space on this platform. | Pick a supported color space (sRGB / Display P3) for this build, or rebuild the engine with the appropriate Skia configuration. | |
| `COMPUTE.CONTEXT_CREATE_FN_NOT_FOUND` | Context creation function not found for type \{type}. | \`createContext()\` was called with a context type the build was not compiled with. Rebuild with the appropriate \`HAS\_UBQ\_\*\_CONTEXT\` macro defined, or pass a supported type. | |
| `COMPUTE.DATA_PROVIDER_EMPTY` | \{kind} data provider is empty. | No bytes have been registered for this resource yet. Ensure the data was provided before reading. | |
| `COMPUTE.DATA_PROVIDER_NOT_CONTIGUOUS` | Data provider does not expose contiguous data. | The active data provider serves data in chunks. Use the streaming API instead of asking for a contiguous buffer. | |
| `COMPUTE.DATA_PROVIDER_NOT_FULLY_AVAILABLE` | \{kind} data provider is not fully available. | The provider is still streaming. Wait for the resource state to transition to Ready before requesting the full buffer. | |
| `COMPUTE.DATA_PROVIDER_TOO_LARGE` | \{kind} data provider too large for contiguous data. | The resource exceeds the contiguous-buffer threshold. Read in chunks via the streaming API instead. | |
| `COMPUTE.EGL_CREATE_CONTEXT_FAILED` | EGL create context error. | \`eglCreateContext\` returned \`EGL\_NO\_CONTEXT\`. The requested GL version / attributes are not supported by the driver. | |
| `COMPUTE.EGL_CREATE_SURFACE_FAILED` | EGL create surface error. | \`eglCreateWindowSurface\` / \`eglCreatePbufferSurface\` returned \`EGL\_NO\_SURFACE\`. The native window or pbuffer attributes are incompatible with the chosen EGL config. | |
| `COMPUTE.EGL_INVALID_CONTEXT_TYPE` | Invalid context type passed to the creation function. | The EGL context factory received an unexpected type tag. Confirm the caller selects one of the supported context types. | |
| `COMPUTE.EGL_NO_CONFIGS_MATCH` | No EGL configs match the requested attribute list. | The requested EGL config attributes (color depth, surface type, GL profile) are not satisfiable on this device. Relax the attribute list or fall back to a CPU context. | |
| `COMPUTE.EGL_NO_DISPLAY` | EGL display unavailable. | \`eglGetDisplay\` returned \`EGL\_NO\_DISPLAY\`. The platform does not expose a usable EGL display — fall back to a CPU context. | |
| `COMPUTE.EGL_OPERATION_FAILED` | EGL \{operation} error: \{reason} | The EGL driver reported a failure during \`\{operation}\`. Inspect \`reason\` for the underlying EGL error string and ensure the platform's EGL stack is initialized correctly. | |
| `COMPUTE.EMSCRIPTEN_MAKE_CONTEXT_CURRENT_FAILED` | Could not make the WebGL context current. | \`emscripten\_webgl\_make\_context\_current\` failed. The browser may have lost the context or the canvas was detached from the DOM. | |
| `COMPUTE.GR_DIRECT_CONTEXT_CREATE_FAILED` | Creating a Skia GrDirectContext failed. | Skia could not initialize a direct GPU context from the current EGL/GL surface. Inspect Skia logs for the underlying cause; falling back to a CPU context is the safe option. | |
| `COMPUTE.HTTP_DATA_NO_BUFFER` | HTTP data provider has no buffer. | The HTTP data provider has not yet allocated its receive buffer. Wait for the first chunk before reading. | |
| `COMPUTE.MP3_PARSE_TRACK_DATA_FAILED` | Failed to parse MP3 file: could not load track data. | The MP3 parser could not extract track frames. The file may be truncated or have an unsupported container variant. | |
| `COMPUTE.MP3_PARSE_TRACK_METADATA_INVALID` | Failed to parse MP3 file: invalid track metadata (channels, sample rate, or frames). | The MP3 header lists impossible values for channels / sample rate / frames. Re-encode the file. | |
| `COMPUTE.MP4_DURATION_ZERO` | MP4 duration is zero. | The container reports zero duration. The file may be truncated or be a fragmented MP4 missing the duration box. | |
| `COMPUTE.MP4_TIMESCALE_ZERO` | MP4 timescale is zero. | The container reports zero timescale. The header is malformed; regenerate the file. | |
| `COMPUTE.OPFS_READ_FAILED` | Failed to read OPFS data. | The Origin Private File System rejected the read. The file may have been removed or the browser revoked access. | |
| `COMPUTE.SKIA_GL_INTERFACE_INVALID` | Skia OpenGL interface failed validation. | The GL function pointers Skia loaded for this context do not satisfy its requirements. Verify the GL driver advertises the extensions Skia depends on. | |
| `COMPUTE.VIDEO_UNSUPPORTED_AUDIO_TRACKS` | Video has \{count} unsupported audio track\{plural}. | Tracks with unsupported codecs were removed. Re-encode with AAC or another supported codec to retain audio. | |
## EDITOR
Editor state, history, selection, commands.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `EDITOR.AUDIO_BUFFER_INVALID_SIZE` | Invalid audio buffer: size \{bufferSize} is not a multiple of frame size \{frameSize}. | Audio buffer length must be aligned to the frame size. Trim the buffer to a multiple of \{frameSize} bytes before submitting. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_BUFFER_INVALID_URI` | Invalid buffer URI: \{uri} | The audio buffer URI '\{uri}' does not resolve to a registered buffer. Confirm the buffer was created and the URI matches. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_BUFFER_NO_DATA` | No buffer data available. | The audio buffer block has no data registered for its file URI. Ensure data is written to the buffer before requesting chunks. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_DECODER_CREATE_FAILED` | Could not create audio decoder: \{reason} | The platform's audio decoder rejected the input. Confirm the codec is supported on this platform. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_DECODE_FAILED` | Fatal audio decoding error: \{reason} | The audio decoder produced an unrecoverable error during playback. The source may be corrupted or use an unsupported format. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_FETCH_FAILED` | Failed to fetch audio. | The audio resource could not be retrieved. Check the URI, asset-source registrations, and network connectivity. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_INVALID_RANGE` | Invalid range requested: start = \{start}, end = \{end}. | Audio chunk ranges must satisfy 0 \<= start \< end. Adjust the request bounds and retry. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_NOT_LOADED` | The audio has not been loaded yet. | Call forceLoadAVResource(block, callback) or wait for the resource state to reach Ready before querying audio metadata. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AUDIO_UNSUPPORTED_FORMAT` | Unsupported audio format with channels: \{channels}, bits: \{bits}, sample format: \{sampleFormat}. | The engine cannot resample this combination on the current platform. Re-encode the audio to a supported profile. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AV_BLOCK_INVALID_WITH_HINT` | The block with ID \{id} is not valid. It may have been deleted, e.g., when a new scene was loaded. | Re-resolve the block id (\{id}) before calling AV APIs. Async callbacks frequently outlive their target block. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AV_DURATION_UNDEFINED` | The AV container duration is undefined. | The decoder could not determine the resource's total duration. The container may be malformed or use an unsupported codec. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AV_OPERATION_AUDIO_OR_VIDEO_FILL` | This operation is only supported for audio blocks and video fills. | Pass an audio block, or resolve the video fill of the block via getFill(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AV_OPERATION_BLOCK_TYPE_UNSUPPORTED` | This operation is only supported for video fills and audio blocks. | Resolve the video fill of the block via getFill(block), or pass an audio block directly. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.AV_OPERATION_VIDEO_ONLY` | This operation is only supported for video fills. | Only blocks whose fill is a video fill expose video-specific metadata. Audio blocks don't apply here. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.BLOCK_NOT_ATTACHED_TO_PAGE` | The target block is not attached to the page. | Add the block as a child of the page before setting it as the page's duration source. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.BLOCK_NOT_A_PAGE` | The given page block is not a page. | The first argument must be a page block. Use api.block.findByType('//ly.img.ubq/page') to obtain one. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.BLOCK_NO_DURATION` | The target block has no duration. | The block has no Duration component. Confirm the block type supports durations, or check via supportsDuration(block) first. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.BLOCK_NO_DURATION_AS_PAGE_SOURCE` | The target block doesn't support duration and therefore cannot be duration source. | Pick a block that has a Duration component (e.g. video fill, audio, or track) to drive the page's duration. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.BLOCK_NO_DURATION_SUPPORT` | The target block doesn't support durations. | Duration applies only to time-aware blocks (pages, tracks, video/audio fills). Use supportsDuration(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.BLOCK_NO_PLAYBACK_CONTROL_SUPPORT` | The target block doesn't support playback control. | Playback control (looping, muted, speed, volume) applies only to AV-source blocks. Use supportsPlaybackControl(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.BLOCK_NO_PLAYBACK_SUPPORT` | The target block doesn't support playback. | Playback time applies only to blocks with a PlaybackTime component (scenes, pages, video/audio). Use supportsPlaybackTime(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.BLOCK_NO_TIME_OFFSET_SUPPORT` | The target block doesn't support time offsets. | Time offsets apply only to time-aware children of a track. Use supportsTimeOffset(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.BLOCK_NO_TRIM_SUPPORT` | The target block doesn't support trimming. | Trimming applies only to video fills, audio blocks, and similar AV-source blocks. Use supportsTrim(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_CLAMP_ACROSS_SCENES` | Cannot clamp camera to elements of different scenes. | All blocks passed to camera clamping must belong to the same scene. Group by scene before calling. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_CLAMP_BLOCKS_WITHOUT_SCENE` | Cannot clamp camera to blocks without a scene. | Attach the blocks to a scene before enabling camera clamping. Floating blocks are not eligible. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_CLAMP_EMPTY_BLOCKS` | Cannot clamp camera to an empty block list. | Provide at least one block (or the scene itself) to clamp against. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_CLAMP_NO_PAGES` | No pages found in the scene. | Camera clamping with page-carousel features requires at least one page block. Add a page before enabling clamping. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.CAMERA_CLAMP_PAGE_NOT_LAYOUTED` | The page has not been layouted. | Layout has not run yet for this page. Trigger an engine update or wait one frame before enabling camera clamping. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.CAMERA_ENTITY_INVALID` | Camera entity is invalid. | Could not resolve a valid camera for the provided scene or camera id. Confirm the scene has a main camera and the id references it. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_NOT_VALID` | Camera is not valid. | The active scene has no camera entity. Create the scene through the normal \`scene.create\*()\` path so the camera is set up automatically. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `EDITOR.CAMERA_POSITION_CLAMP_NOT_ENABLED` | This block does not have camera position clamping enabled. | Enable camera position clamping for this block first, or check whether it is enabled before disabling it. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_ZOOM_CLAMP_LIMITS_INVALID` | To enable camera zoom clamping, at least one of minZoomLimit or maxZoomLimit must be positive. | Set at least one of minZoomLimit or maxZoomLimit to a positive value. Use a negative value on the other side to leave it unbounded. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_ZOOM_CLAMP_LIMITS_ORDER` | The minZoomLimit has to be smaller or equal to maxZoomLimit. | Swap or adjust the values so minZoomLimit \<= maxZoomLimit. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_ZOOM_CLAMP_NOT_ENABLED` | This block does not have camera zoom clamping enabled. | Enable camera zoom clamping for this block first, or check whether it is enabled before disabling it. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CAMERA_ZOOM_LIMITS_NOT_FINITE` | The zoom limits must be finite numbers. | Both minZoomLimit and maxZoomLimit must be finite. Use a negative value to indicate no limit on that side. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.COMMAND_ARG_TYPE_MISMATCH` | Argument at index \{index} for command '\{command}' doesn't match expected type '\{expectedType}'. Got '\{actualType}' instead. | Convert the argument to '\{expectedType}' or pass a different value matching the command signature. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.COMMAND_NOT_REGISTERED` | \{command} is not a registered command. | Command '\{command}' was not found in the registry. Confirm the name is correct and that the responsible module is loaded. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.COMMAND_RETURN_TYPE_MISMATCH` | Returned value for command '\{command}' did not match expected type (got '\{actualType}', expected '\{expectedType}'). Did the responsible system never execute? | The system handling '\{command}' returned a value of the wrong type. Verify the handler is registered and producing '\{expectedType}'. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.COMMAND_UNIMPLEMENTED` | Unimplemented. | The command type has no implementation of this method. Subclasses must override determineType() and any other defaults they rely on. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.COMMAND_WRONG_ARG_COUNT` | Received \{received} arguments for command \{command}, but expected \{expected}. | Command '\{command}' takes \{expected} arguments. Adjust the call to match the documented signature. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CROP_ELEMENT_NOT_CROPPABLE` | This element can't be cropped. | Crop operations require a block with a cropped fill (image or video). Verify api.block.supportsCrop(id) before calling \{operation}. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.CROP_NO_SELECTED_ELEMENT` | Select an element to crop. | Select a block before invoking \{operation}. Use api.block.findAllSelected() to verify a selection exists. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.FONT_DATA_LOAD_FAILED` | Font resource ready but failed to load font data: \{uri}. | The font resource finished loading but the data could not be parsed. The payload may be empty or use an unsupported font format. | [Custom Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) |
| `EDITOR.FONT_LOAD_FAILED` | Failed to load font '\{uri}': \{reason}. | The font resource at '\{uri}' could not be loaded (\{reason}). Verify the URI is reachable and the asset is registered. | [Custom Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) |
| `EDITOR.FONT_METRICS_EXTRACT_FAILED` | Failed to extract font metrics from: \{uri}. | The font file at '\{uri}' could not be parsed for metrics. Confirm the file is a valid TTF/OTF and not corrupted. | [Custom Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) |
| `EDITOR.FONT_URI_EMPTY` | Font file URI cannot be empty. | Pass a non-empty URI pointing to a font file registered with the engine. See addLocalAssetSourceFromJSON or registerFont docs for setup. | [Custom Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) |
| `EDITOR.HISTORY_HANDLE_INVALID` | Could not obtain a valid history handle. | The editor has no active history buffer. Confirm a scene is loaded and the engine has fully initialized before invoking history APIs. | [Undo And History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) |
| `EDITOR.MEMORY_QUERY_UNAVAILABLE` | Could not obtain \{kind} memory for the current platform. | The platform does not expose \{kind}-memory statistics, or the query failed. Treat memory metrics as best-effort on this platform. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.MOVEMENT_CONSTRAINT_NEGATIVE` | setMovementConstraint value must be non-negative; call removeMovementConstraint to clear a constraint. | Pass a non-negative distance, or call removeMovementConstraint(targets) instead of using a negative sentinel. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.NEGATIVE_DURATION` | Negative durations are not supported. | Pass a non-negative duration. Use 0 for an empty interval or std::numeric\_limits\::infinity() for unbounded. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.NO_SCENE_AVAILABLE` | No scene available. | The editor has no active scene. Load or create one before invoking this API. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.NO_UNDO_STEP_AVAILABLE` | No undo step available to remove. | The undo stack is empty. Verify the editor has performed at least one undoable action before calling removeUndoStep(). | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.PADDING_NOT_FINITE` | The padding values must be finite numbers. | All four padding components must be finite. Replace any NaN or infinity with a concrete number (zero is fine). | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.PAGES_NOT_RESIZED` | Pages could not be resized. | Resize failed for one or more pages. Confirm each page is unlocked (api.block.isTransformLocked / isAllowedByScope) and that the requested width and height are finite and positive. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.PAGE_CONTENT_ASPECT_RATIO_INVALID` | ContentAspectRatio preset cannot be used to create a new page; it only applies to blocks with intrinsic content dimensions. | Pages have no intrinsic content size. Use a FreeAspectRatio, FixedAspectRatio, or FixedSize preset instead. | |
| `EDITOR.PAGE_RESIZE_DISABLED` | Page resizing interaction is disabled in UBQ settings. | Enable page resizing via settings (e.g. 'features/page/resize') before invoking the resize interaction. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.PAGE_RESIZE_FIXED_ASPECT_ONLY` | Page resizing interaction is restricted to fixed aspect ratio in UBQ settings. | Allow free-aspect resizing in settings, or constrain the interaction to the configured fixed aspect ratio. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `EDITOR.PLAYBACK_SPEED_OUT_OF_RANGE` | Invalid playback speed \{speed}. Must be between 0.25 and 3.0. | For non-video blocks playback speed must be in \[0.25, 3.0]. Video fills support a wider range. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.PLAYBACK_SPEED_TOO_LOW` | Invalid playback speed \{speed}. Must be at least 0.25. | Pass a playback speed of at least 0.25. Speeds below this are not supported. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.RESOURCE_LOAD_FAILED` | Failed to load resource \{uri}. | The resource at '\{uri}' could not be loaded. Check that the URI is reachable, CORS allows access, and the format is supported. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.RESOURCE_URI_EMPTY` | The resource has an empty URI. | Set a non-empty file URI on the video fill or audio block before requesting a load. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.ROLE_NOT_FOUND` | Role \{role} not found. | '\{role}' is not a known editor role. Valid roles are defined by the EditorRole enum (e.g. Adopter, Creator). | [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) |
| `EDITOR.SAFE_AREA_INSETS_NEGATIVE` | Safe area inset values must be non-negative. | Each inset describes a non-negative distance from the viewport edge. Use 0 to disable an inset rather than a negative value. | |
| `EDITOR.SAFE_AREA_INSETS_NOT_FINITE` | Safe area inset values must be finite numbers. | All four inset components must be finite. Replace any NaN or infinity with a concrete number (zero is fine). | |
| `EDITOR.SCENE_CONTENT_EMPTY` | Received empty scene content. | The scene-serialization command was called with an empty payload. Pass the serialized scene string returned by \`scene.saveToString(...)\`. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `EDITOR.SCENE_DECOMPRESS_FAILED` | Failed to decompress scene data. | The compressed scene payload could not be decoded. The file may be truncated or produced by an incompatible compressor. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.SCENE_ENTITY_INVALID` | Scene entity is invalid. | Could not resolve a valid scene for the provided id. Confirm the id is a scene or a camera attached to a scene. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.SCENE_INPUT_INVALID` | Received scene input is not a valid serialization. | The payload does not parse as a CE.SDK scene document. Confirm it was produced by api.scene.saveToString() or saveToArchive(). | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.SCENE_MISSING_REQUIRED_KEY` | Invalid scene file: "\{key}" key not found. | The serialized scene is missing the required top-level "\{key}" field. The file may be truncated or produced by an incompatible writer. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.SCENE_REQUIRES_URL_LOAD` | Scene is part of an archive and must be loaded via URL. | Use api.scene.loadFromURL() or loadFromArchive() rather than loadFromString() for archive-bound scenes. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.SCENE_SIZE_UNAVAILABLE` | Could not get scene size. Scene may not have a layout yet. | Wait for the first layout pass to complete (e.g. after the next \`api.update()\`) before reading scene dimensions. | |
| `EDITOR.SETTING_ENUM_VALUE_NOT_FOUND` | Enum value \{value} not found. | '\{value}' is not a member of the target enum. Call getSettingEnumOptions() to list valid values. | [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) |
| `EDITOR.SETTING_NOT_ENUM` | Setting \{keypath} is not an enum. | Use the type-specific getter/setter (Bool/Int/Float/String/Color) instead of the Enum variant for non-enum settings. | [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) |
| `EDITOR.SETTING_NOT_FOUND` | Setting \{keypath} not found. | No setting registered at '\{keypath}'. Use findAllSettings() to discover valid key paths. | [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) |
| `EDITOR.SETTING_TYPE_UNSUPPORTED` | Unsupported setting type for \{keypath}. | The setting at '\{keypath}' has a type the public API does not expose. Use getSettingType() to discover the supported categories. | [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) |
| `EDITOR.SPLIT_BLOCK_FAILED` | Could not split block of type "\{blockType}": \{reason} | Splitting a block failed at an intermediate step (\{reason}). Confirm the block via api.block.isValid(block) and api.block.supportsDuration(block), and that the split time lies inside the block's time range (api.block.getTimeOffset(block) to getTimeOffset + getDuration). | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.THUMBNAIL_SAMPLES_INVALID` | Can't generate thumbnail sequence with samplesPerChunk \<= 0. | Pass a positive samplesPerChunk so each chunk contains a non-empty waveform sample window. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.TOUCH_ROTATE_TOO_MANY_POINTS` | Touch rotate event has more than two touch points. | The rotate gesture is only defined for two-finger input. Cancel the gesture when a third pointer joins. | |
| `EDITOR.TRIM_OFFSET_UNDEFINED` | The trim offset is undefined. | The AV resource duration or playback speed yields a non-finite trim offset. Confirm the resource is loaded and playback speed is positive. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.UNIT_CONVERSION_FAILED` | Failed to convert to given design unit. | The requested unit conversion is not supported in this context. Confirm the scene has a configured DPI and design unit. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.UNSUPPORTED_SERIALIZATION_FORMAT` | Unsupported serialization format. | The scene declares a serialization format this engine version cannot read. Re-save the scene with the current engine, or upgrade the engine. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.VALUE_NOT_FINITE` | The value \{value} must be a finite number. | NaN and infinity are not accepted. Pass a finite floating-point value. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.VECTOR_INVALID_MIRROR_MODE` | Invalid mirror mode \{mode}. Expected 0 (None), 1 (AngleAndLength), or 2 (AngleOnly). | Pass 0, 1, or 2 — values map to the VectorHandleMirrorMode enum (None, AngleAndLength, AngleOnly). | [Vector Edit](#broken-link-f3a7b2) |
| `EDITOR.VECTOR_NODE_NOT_DELETABLE` | Cannot delete node: contour must have at least 3 nodes. | A vector contour cannot be reduced below 3 nodes. Delete the entire contour or merge it with another path instead. | [Vector Edit](#broken-link-f3a7b2) |
| `EDITOR.VECTOR_NO_CONTROL_POINT_SELECTED` | No vector control point is selected. | Vector control-point operations require an active selection. Select a control point first before invoking this API. | [Vector Edit](#broken-link-f3a7b2) |
| `EDITOR.VECTOR_NO_NODE_SELECTED` | No vector node is selected. | Vector node operations require an active selection. Select a node first via the vector editor UI or programmatically before invoking this API. | [Vector Edit](#broken-link-f3a7b2) |
| `EDITOR.VECTOR_NO_PATH_EDITING` | No vector path is being edited. | Enter vector edit mode on a vector block before invoking this API. Vector path mutations require an active edited path. | [Vector Edit](#broken-link-f3a7b2) |
| `EDITOR.VIDEO_DECODER_CREATE_FAILED` | Could not create video decoder: \{reason} | The platform's video decoder rejected the input. Confirm the codec is supported on this platform — see CE.SDK capability docs. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.VIDEO_DECODE_FAILED` | Fatal video decoding error: \{reason} | The decoder produced an unrecoverable error during playback. The source may be corrupted or use an unsupported codec profile. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.VIDEO_FETCH_FAILED` | Failed to fetch video. | The video resource could not be retrieved. Check the URI, asset-source registrations, and network connectivity. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.VIDEO_NOT_LOADED` | The video has not been loaded yet. | Call forceLoadAVResource(block, callback) or wait for the resource state to reach Ready before querying video metadata. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.ZOOM_AUTO_FIT_NOT_ENABLED` | This block does not have zoom auto-fit enabled. | Call enableZoomAutoFit(block) before disabling, or check isZoomAutoFitEnabled(block) first. | [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) |
| `EDITOR.ZOOM_BLOCK_NOT_LAYOUTED` | Could not zoom to block. Block hasn't been layouted yet. | Wait for the first layout pass to complete (e.g. after the next \`api.update()\`) before zooming to the block. | |
| `EDITOR.ZOOM_NO_CAMERA` | No valid camera exists, can't zoom to block. | Ensure the scene has been initialized with a camera before invoking zoom-to-block. | |
## ENCODE
Image and video encoding/export.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `ENCODE.AUDIO_BLOCK_DIRECT_EXTRACTION_UNSUPPORTED` | Unable to extract MP4 audio directly from audio block. The audio file may not be in a compatible format or may not be loaded. | Re-encode the audio block as MP4-compatible AAC before attempting direct extraction, or load the block first. | |
| `ENCODE.AUDIO_BLOCK_INVALID` | Invalid audio block entity. | The id does not reference a live audio block. Re-resolve it before exporting and verify with api.block.isValid(block). | |
| `ENCODE.AUDIO_BUFFER_EMPTY` | Audio buffer is empty. | The audio block has zero bytes of audio data. Provide a valid, non-empty audio source for the block before exporting. | |
| `ENCODE.AUDIO_BUFFER_NOT_FOUND` | Audio buffer not found. | The audio block's buffer is not registered. Ensure audio data was provided before export. | |
| `ENCODE.AUDIO_CHANNEL_COUNT_INVALID` | Number of audio channels must be \{expected}. | Audio export requires \{expected} channels. Convert mono/multichannel sources first. | |
| `ENCODE.AUDIO_CHUNK_READ_FAILED` | Failed to read audio chunk data from data provider. | The data provider returned no bytes for an audio chunk. The source may be truncated. | |
| `ENCODE.AUDIO_CONTEXT_NOT_AVAILABLE_DURING_EXPORT` | Audio context not available during export. | The audio context was destroyed mid-export. Keep it alive until the export callback fires. | |
| `ENCODE.AUDIO_CONTEXT_PARAMS_INVALID` | Invalid audio context parameters. | Pass a supported sample rate and channel count in the audio export options before exporting. | |
| `ENCODE.AUDIO_CONTEXT_UNAVAILABLE` | No audio context available for export. | Audio context is not initialized. Create one with the active compute context before exporting. | |
| `ENCODE.AUDIO_DURATION_INVALID` | Duration must be a positive finite number. | Audio export duration must be > 0 and finite. Check the source has a well-defined duration. | |
| `ENCODE.AUDIO_EXPORT_OPTIONS_NOT_OBJECT` | Audio export options must be a valid JSON object. | Pass the audio export options as a JSON object string, e.g. \{"sampleRate": 48000}. | |
| `ENCODE.AUDIO_EXPORT_OPTIONS_PARSE_FAILED` | Failed to parse audio export options: \{reason} | The audio export options string is not valid JSON (\{reason}). Fix the JSON syntax and retry. | |
| `ENCODE.AUDIO_FRAME_CALC_INVALID` | Invalid audio frame calculations. | Computed frame counts are nonsensical (zero or negative). Verify duration, sample rate, and processing rate. | |
| `ENCODE.AUDIO_FRAME_SIZE_CALC_INVALID` | Invalid audio frame size calculations. | Frame-size math produced zero or negative values. Verify channel count and bit depth. | |
| `ENCODE.AUDIO_INVALID_SAMPLE_RATE_FOR_TIMESTAMP` | Invalid sample rate for timestamp conversion. | Source audio sample rate is zero or negative. Verify it before computing timestamps. | |
| `ENCODE.AUDIO_MIME_TYPE_INVALID` | Mime type must be "audio/wav" or "audio/mp4". | Pass audio/wav or audio/mp4 as the target mime type for audio export. | |
| `ENCODE.AUDIO_MP4_NO_DATA_MUXED` | No audio data was muxed into MP4 container. | The muxer received no PCM frames. Check the source produced audio packets before finalization. | |
| `ENCODE.AUDIO_MUXER_DESTROYED` | Muxer was destroyed before finalization. | The MP4 muxer was released early. Keep the encode service alive until the result callback fires. | |
| `ENCODE.AUDIO_NO_CHUNKS_IN_RANGE` | No audio chunks found in the specified time range. | The selected time range has no audio data. Adjust start/end times or verify the audio source covers the requested span. | |
| `ENCODE.AUDIO_NO_DATA_CAPTURED` | No audio data captured during export. | The export pipeline produced no audio samples. Verify the source has audible content within the requested range. | |
| `ENCODE.AUDIO_NO_PCM_COLLECTED` | No PCM data was collected. | The decoder returned zero PCM samples. The audio source may be empty or fully silent. | |
| `ENCODE.AUDIO_PROCESSING_RATE_INVALID` | Audio processing rate must be a positive finite number. | Pass a positive, finite audio sample rate in the audio export options before exporting. | |
| `ENCODE.AUDIO_SAMPLE_RATE_INVALID` | Sample rate must be \{expected}. | Audio export requires sample rate \{expected}. Resample the source before exporting. | |
| `ENCODE.AUDIO_SERVICE_UNAVAILABLE` | No audio service available for export. | The engine was built without audio support. Enable it in the build config and rebuild. | |
| `ENCODE.AUDIO_SINGLE_TRACK_INDEX_NONZERO` | Single-track audio container only supports trackIndex=0, got \{index} | For single-track containers, pass trackIndex=0. | |
| `ENCODE.AUDIO_START_TIME_OUT_OF_BOUNDS` | Start time \{start} is out of bounds. Valid range: 0-\{max} | Audio start time \{start} must be within \[0, \{max}]. | |
| `ENCODE.AUDIO_TIME_RANGE_INVALID` | Invalid time range: start=\{start}, end=\{end} | Audio time range must satisfy 0 \<= start \< end. Got start=\{start}, end=\{end}. | |
| `ENCODE.AUDIO_TIME_RANGE_INVALID_AFTER_TRIM` | Invalid time range after applying trim settings and options. | After applying trim/clip parameters the selected range is empty. Adjust start/end times. | |
| `ENCODE.AUDIO_TRACK_INDEX_OUT_OF_BOUNDS` | Audio track index \{index} is out of bounds. Valid range: 0-\{max} | Pass an audio track index within \[0, \{max}]. | |
| `ENCODE.AUDIO_UNSUPPORTED_EXPORT_FORMAT` | Unsupported audio export format. | Use audio/wav or audio/mp4 as the export target. | |
| `ENCODE.BLOCK_MUST_BE_PAGE` | The block to export must be a page. | Audio export targets a page block. Pass a page or call api.scene.getPages() to obtain one. | |
| `ENCODE.BLOCK_SIZE_ZERO` | Block to export has size 0. | Set a positive width and height on the block, or pick a different block, before exporting. | |
| `ENCODE.CANCELLED_BY_BLOCK_ERROR` | The export was cancelled due to block \{block} having an error: \{reason} | Block \{block} entered an error state during export (\{reason}). Resolve the block's underlying issue and retry. | |
| `ENCODE.COLOR_MASK_DATA_FAILED` | Color masking data could not be generated. | The renderer failed to produce the color-mask buffer. Retry, or simplify the scene if it has very high resolution. | |
| `ENCODE.COLOR_MASK_DISABLED` | Color masking is not enabled. | Enable color masking via the export options before requesting mask data. | |
| `ENCODE.CONTEXT_AUDIO_CREATE_FAILED` | Could not create audio context. | Audio context creation failed. Ensure the platform exposes an AudioContext and is not in a frozen tab. | |
| `ENCODE.CONTEXT_RENDER_CREATE_FAILED` | Could not create render context. | GPU context creation failed. Confirm the host supports the required backend (WebGL2/Metal) and retry. | |
| `ENCODE.DIRECT_EXTRACTION_UNSUPPORTED` | Direct extraction not supported for this block/mime type combination. | The requested block kind cannot be extracted directly to this mime type. Use a transcoding path instead. | |
| `ENCODE.ENTITY_INVALID` | Entity is invalid. | The block id no longer references a live block. Re-resolve before exporting. | |
| `ENCODE.ENTITY_NOT_PART_OF_PAGE` | Entity is not part of a valid page. | Audio export requires the block to be inside a page. Add the block to a page before exporting. | |
| `ENCODE.EXPORT_FAILED` | Export failed. | A non-specific export error was produced. Check earlier log lines for the root cause and retry. | |
| `ENCODE.EXPORT_OPTIONS_NOT_OBJECT` | Export options must be a valid JSON object. | Pass the export options as a JSON object string, e.g. \{"pngCompressionLevel": 6}. | |
| `ENCODE.EXPORT_OPTIONS_PARSE_FAILED` | Failed to parse export options: \{reason} | The export options string is not valid JSON (\{reason}). Fix the JSON syntax and retry. | |
| `ENCODE.GPU_LOST_GENERIC` | GPU context was lost during export rendering. Try exporting at a lower resolution. | Reduce the export resolution or split the scene, then retry. Restart the engine if the GPU context fails to recover. | |
| `ENCODE.GPU_LOST_PDF` | GPU context was lost during PDF export rendering. The scene may contain elements that exceed the device's capabilities. | Lower the export DPI or split the scene into multiple pages, then retry. | |
| `ENCODE.GPU_LOST_SVG` | GPU context was lost during SVG export rendering. Try reducing the size of large images. | Re-encode large source images to a lower resolution before exporting, or restart the engine to recover the GPU context. | |
| `ENCODE.GROUP_INVALID` | Invalid group entity. | The group id does not reference a live group. Re-resolve it before exporting and verify with api.block.isValid(group). | |
| `ENCODE.GROUP_NOT_IN_PAGE_HIERARCHY` | Group entity not found in page hierarchy. | The group is not a descendant of the page passed to the encode call. | |
| `ENCODE.GROUP_NOT_PART_OF_PAGE` | Group is not part of a valid page. | Groups must live under a page. Reparent the group or pick a page descendant. | |
| `ENCODE.GROUP_NO_CHILDREN` | Group has no child blocks. | Audio export requires at least one block in the group. | |
| `ENCODE.IMAGE_JPEG_FAILED` | Failed to encode JPEG. | The JPEG encoder rejected the input. | |
| `ENCODE.IMAGE_JPEG_OOM` | Failed to encode JPEG. Memory allocation failed. | Not enough memory for the JPEG buffer. | |
| `ENCODE.IMAGE_PNG_FAILED` | Failed to encode PNG. | The PNG encoder rejected the input. The image dimensions or pixel format may be unsupported. | |
| `ENCODE.IMAGE_PNG_OOM` | Failed to encode PNG. Memory allocation failed. | Not enough memory for the PNG buffer. Reduce dimensions or free memory. | |
| `ENCODE.IMAGE_READ_PIXELS_FAILED` | Failed to read pixels. | The source image could not be read into a CPU buffer. The compute context may be missing GPU read-back support. | |
| `ENCODE.IMAGE_TGA_OOM` | Failed to encode TGA. Memory allocation failed. | Not enough memory for the TGA buffer. Reduce dimensions or free memory. | |
| `ENCODE.IMAGE_TGA_REQUIRES_RGBA32` | TGA encoding only supports 32-bit RGBA pixel data. | Convert the image to 32-bit RGBA before encoding to TGA. | |
| `ENCODE.IMAGE_UNKNOWN_MIME` | Unknown MIME type for image encoding: \{mimeType} | Mime type '\{mimeType}' is not supported. Use one of image/png, image/jpeg, image/webp, image/tga. | |
| `ENCODE.IMAGE_WEBP_FAILED` | Failed to encode WebP. | The WebP encoder rejected the input. | |
| `ENCODE.IMAGE_WEBP_OOM` | Failed to encode WebP. Memory allocation failed. | Not enough memory for the WebP buffer. | |
| `ENCODE.INSUFFICIENT_RESOURCES` | Not enough resources available on device. Try exporting at a lower resolution. | Reduce the requested export resolution or simplify the scene to fit available memory. | |
| `ENCODE.MASK_BUFFER_FAILED` | Could not acquire mask buffer. Please try again. | The renderer transiently failed to allocate a mask buffer. Retry the export after a short delay. | |
| `ENCODE.MASK_COLOR_OUT_OF_RANGE` | Mask color values must be between 0 and 1. | Clamp each mask color component (r/g/b/a) to \[0, 1] before passing to the export options. | |
| `ENCODE.MIME_TYPE_INVALID` | Invalid mime-type '\{mimeType}', expected '\{a}', '\{b}' or '\{c}'. | Use one of '\{a}', '\{b}', or '\{c}'. Update the asset's mime type at registration time. | |
| `ENCODE.NOT_ALL_RESOURCES_LOADED` | Not all resources were loaded. | Some scene resources are still pending. Subscribe to resource events or poll until all assets reach Ready, then retry. | |
| `ENCODE.OFFSCREEN_CANVAS_CREATE_FAILED` | Could not create offscreen canvas. | Offscreen canvas allocation failed. Confirm the host environment supports the required GPU surface. | |
| `ENCODE.OUTPUT_SIZE_EXCEEDS_MAX` | The output size of \{width}x\{height} px for the export exceeds the maximum supported size \{maxSize} of the device. | Reduce the export resolution so its larger dimension is at most \{maxSize} px. | |
| `ENCODE.OUTPUT_SIZE_INSUFFICIENT_RESOURCES` | Could not export at \{width}x\{height}. Not enough resources available on device. Try exporting at a lower resolution. | Reduce the export resolution from \{width}x\{height} or simplify the scene, then retry. | |
| `ENCODE.PAGE_NO_CHILDREN` | Page has no children. | Audio export requires at least one child block on the page. | |
| `ENCODE.PDF_CREATE_FAILED` | Failed to create a PDF document. | The PDF writer rejected the document. Check available memory and retry with a smaller scene if needed. | |
| `ENCODE.PDF_CREATE_FAILED_RESOURCES` | Failed to create PDF document. Not enough resources available on device. Try exporting at a lower resolution. | Reduce the page size or DPI and retry. Very large scenes can exceed device memory limits. | |
| `ENCODE.PDF_RENDER_SIZE_EXCEEDS_MAX` | The effective rendering size of \{width}x\{height} px (at \{dpi} DPI) for the PDF export exceeds the maximum supported texture size \{maxSize} of the device. Try reducing the scene DPI or the size of the exported block. | Reduce the scene DPI or the export size so the rendered dimensions stay at or below \{maxSize} px. | |
| `ENCODE.PIXEL_BUFFER_UNEXPECTED_SIZE` | Unexpected pixel buffer size for color analysis. | The pixel buffer dimensions don't match the expected analysis layout. Re-read the buffer or verify the source surface. | |
| `ENCODE.PIXEL_STREAM_NO_DATA` | Failed to encode pixel stream: no data was produced. | Inspect the pixel stream pipeline. The exporter produced an empty result for the requested options. | |
| `ENCODE.RELATIVE_URLS_NOT_SUPPORTED` | Relative URLs are not supported. | Resolve all relative URLs to absolute ones before invoking the operation. | |
| `ENCODE.RESOURCE_DATA_EMPTY` | Empty resource data: \{uri} | The resource at '\{uri}' loaded but returned empty bytes. Re-source the file. | |
| `ENCODE.RESOURCE_LOAD_FAILED_WITH_REASON` | Failed to load resource '\{uri}': \{reason} | The resource at '\{uri}' failed to load (\{reason}). Confirm reachability and supported format. | |
| `ENCODE.RESULT_BUFFER_FAILED` | Could not acquire result buffer. Please try again. | The renderer transiently failed to allocate a result buffer. Retry the export after a short delay. | |
| `ENCODE.SVG_CANVAS_CREATE_FAILED` | Failed to create SVG canvas. | The SVG canvas could not be allocated. Check available memory and retry. | |
| `ENCODE.SVG_COLOR_MASK_UNSUPPORTED` | Color masking is not supported for SVG export. | SVG output cannot carry a color mask. Disable color masking or export as PNG/PDF instead. | |
| `ENCODE.SVG_MEMORY_ALLOC_FAILED` | Could not allocate memory for SVG export. | Reduce the scene complexity or available pixel count, then retry the SVG export. | |
| `ENCODE.SVG_NO_DATA` | SVG export produced no data. | The renderer produced an empty SVG. Confirm the page has visible content and try again. | |
| `ENCODE.TARGET_NOT_IN_PAGE_HIERARCHY` | Target entity not found in page hierarchy. The entity may not be part of this page. | Verify the target block is a descendant of the page passed to the encode call. | |
| `ENCODE.TRACK_INVALID` | Invalid track entity. | The track id does not reference a live track. Re-resolve it before exporting and verify with api.block.isValid(track). | |
| `ENCODE.TRACK_NOT_IN_PAGE_HIERARCHY` | Track entity not found in page hierarchy. | The track is not a descendant of the page passed to the encode call. | |
| `ENCODE.TRACK_NOT_PART_OF_PAGE` | Track is not part of a valid page. | Tracks must live under a page. Reparent the track or pick a page descendant. | |
| `ENCODE.TRACK_NO_CHILDREN` | Track has no child blocks. | Audio export requires at least one block on the track. | |
| `ENCODE.VIDEO_BLOCK_HAS_ERROR` | The export was cancelled due to block \{block} having an error: \{reason} | Block \{block} reported an error during export: \{reason}. Fix the block before retrying. | |
| `ENCODE.VIDEO_CONCURRENT_ENCODING` | The VideoEncodeService is currently encoding. Concurrent encoding is not supported. | Wait for the current video export to finish before starting another. | |
| `ENCODE.VIDEO_EXPORT_OPTIONS_NOT_OBJECT` | Video export options must be a valid JSON object. | Pass the video export options as a JSON object string, e.g. \{"videoBitrate": 8000000}. | |
| `ENCODE.VIDEO_EXPORT_OPTIONS_PARSE_FAILED` | Failed to parse video export options: \{reason} | The video export options string is not valid JSON (\{reason}). Fix the JSON syntax and retry. | |
| `ENCODE.VIDEO_FILL_AUDIO_REQUIRES_MP4` | Video fill audio extraction is only supported for audio/mp4 format. | Pass audio/mp4 as the target mime type when extracting audio from a video fill. | |
| `ENCODE.VIDEO_FILL_INVALID` | Invalid video fill entity. | The video fill id does not reference a live fill. Re-resolve it with api.block.getFill(block) and confirm api.block.isValid(fill) before exporting. | |
| `ENCODE.VIDEO_FILL_NO_URI` | The video fill block does not have a valid file URI. | Assign a valid fileURI to the video fill before requesting audio extraction. | |
| `ENCODE.VIDEO_FRAME_FAILED` | Failed to encode video frame: \{reason} | The video encoder rejected a frame (\{reason}). Confirm codec compatibility and available memory, then retry. | |
| `ENCODE.VIDEO_HAS_NO_AUDIO_TRACKS` | The video does not contain any audio tracks. | Use a video that has at least one audio track for audio extraction. | |
| `ENCODE.VIDEO_MIME_NOT_MP4` | Mime type is not "video/mp4". | Video export currently only supports video/mp4 as the target mime type. | |
| `ENCODE.VIDEO_NO_AUDIO_DURATION` | Video has no audio duration. | The video's audio track reports zero duration. The file may be malformed. | |
| `ENCODE.VIDEO_PCM_READ_FAILED` | Could not read PCM frames. | The audio pipeline returned no PCM frames during video export. Audio context may have died. | |
| `ENCODE.VIDEO_RESOURCE_LOAD_FAILED` | Failed to load video resource. Please ensure the video file is accessible and valid. | The video fill's URI could not be loaded. Check accessibility and format. | |
## EVENT
Engine event subscription and dispatch.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `EVENT.SUBSCRIPTION_NOT_FOUND` | Event subscription does not exist. | The subscription id does not refer to an active subscription. It may have been unsubscribed already or never created. | [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) |
## FETCH
HTTP fetch and remote resource loading.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `FETCH.JSON_FETCH_FAILED` | Could not fetch JSON: \{uri} | The JSON resource at '\{uri}' could not be fetched. Check reachability, CORS, and the response status. | |
| `FETCH.JSON_URI_EMPTY` | Could not fetch JSON: URI cannot be empty. | Pass a non-empty URI to the JSON fetch call. | |
| `FETCH.RESOURCE_DATA_EMPTY` | Empty resource data: \{uri} | The resource at '\{uri}' loaded but returned no bytes. Re-source the file or confirm the URL serves content. | |
| `FETCH.RESOURCE_FAILED` | Error fetching resource: \{url} | The HTTP fetch to '\{url}' failed. Inspect network status, response code, and CORS configuration. | |
| `FETCH.URI_INVALID` | Invalid URI: \{uri} | The URI '\{uri}' is malformed or uses an unsupported scheme. Provide a fully qualified URL or a registered scheme. | |
| `FETCH.URL_PARSE_FAILED` | Failed to parse URL: \{url} | The string '\{url}' is not a valid URL. Provide a fully qualified URL including the scheme. | |
## LICENSE
License unlock, API-key handling, entitlement checks.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `LICENSE.ALREADY_UNLOCKED` | License is already unlocked. | The license has already been unlocked successfully. Skip the redundant unlock call. | |
| `LICENSE.API_SERVICE_UNAVAILABLE` | API service is unavailable. Please contact support. | The license API endpoint did not respond. Check network connectivity; if the problem persists, contact support. | |
| `LICENSE.CANNOT_DEACTIVATE_OFFLINE` | Cannot deactivate offline license. | Offline licenses cannot be deactivated remotely. Switch to an online license if dynamic activation is required. | |
| `LICENSE.DEACTIVATION_TIMEOUT` | Deactivation timed out. | The license server did not acknowledge the deactivation in time. Retry, or contact support if the issue persists. | |
| `LICENSE.ENGINE_VERSION_INVALID` | The License Key (API Key) you are using requires a newer version of the IMG.LY SDK. Please update to the latest version. | Upgrade the CE.SDK engine to a version compatible with this license. | |
| `LICENSE.EXPIRED` | Thanks for using IMG.LY for creative editing. Please note that your license file or commercial use is expired. | Renew your subscription at https://img.ly/pricing to continue commercial use. | |
| `LICENSE.IDENTIFIER_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this app identifier. Current app identifier "\{current}" differs from license app identifiers: \[\{allowed}] | Your license is tied to specific app identifiers. Adjust the application bundle id to one of '\{allowed}' or update the license. | |
| `LICENSE.INVALID` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid. | Verify the License Key matches your IMG.LY subscription. If issues persist, contact support@img.ly. | |
| `LICENSE.INVALID_API_KEY` | Invalid API key. | The API key did not authenticate. Verify it matches the subscription registered in your IMG.LY dashboard. | |
| `LICENSE.INVALID_FORMAT` | Invalid license format. Please contact support. | The license string could not be parsed. Verify it was copied in full and contact support if the issue persists. | |
| `LICENSE.MISSING` | The license is missing. | Call api.unlockWithLicense() or unlockWithAPIKey() before invoking engine APIs that require entitlements. | |
| `LICENSE.MISSING_FLUENDO_FLAG` | License does not support Fluendo codecs. | Add the 'fluendo' capability flag to your license, or do not request a session that requires it. | |
| `LICENSE.MIXED_UNLOCK_METHODS` | unlockWithLicense shouldn't be called after unlockWithAPIKey. | Choose one unlock method per engine instance. Re-create the engine to switch between license-file and API-key authentication. | |
| `LICENSE.NO_ACTIVE_TO_DEACTIVATE` | No active license to deactivate. | The deactivation request targets a license that is not currently active. Unlock a license first. | |
| `LICENSE.NO_USER_ID` | License does not have a user ID. | User-specific entitlements require a license that carries a user id. Re-issue the license with a user id assigned. | |
| `LICENSE.PLATFORM_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this platform. Current platform: "\{current}" differs from license platforms: \[\{allowed}] | Your license restricts the platforms this engine may run on. Update the license to include '\{current}' or run on one of: \{allowed}. | |
| `LICENSE.PRODUCT_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this product. Current build product: "\{current}" differs from license product: "\{required}" | Your license is tied to a specific product. Use a build for '\{required}' or obtain a license for '\{current}'. | |
| `LICENSE.REQUEST_IN_PROGRESS` | License request already in progress. | A license fetch is in flight. Wait for it to finish before calling unlock again. | |
| `LICENSE.SERVER_ERROR` | License server reported an error: \{reason} | Server response: \{reason}. If the message is unexpected, contact IMG.LY support. | |
| `LICENSE.STILL_FETCHING` | License is still being fetched. | The license fetch has not completed. Await the unlock promise/callback before invoking entitlement-gated APIs. | |
| `LICENSE.TARGET_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this build target. Current build target triplet: "\{target}" | Your license restricts which build targets may run. Update the license to include '\{target}' or run on a permitted target. | |
| `LICENSE.UNSUPPORTED_SESSION_TYPE` | Unsupported session type. | The requested session kind is not recognized. Pass a documented session type identifier. | |
| `LICENSE.VERSION_INVALID` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this version. | The license does not cover the running engine version. Update the license tier or downgrade the engine. | |
## MEDIA
Media containers and demuxing.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `MEDIA.BLOCK_NOT_AUDIO_OR_VIDEO_FILL` | Entity is not an audio block nor a video fill. | Audio waveform/thumbnail APIs require an audio block or a block with a video fill. | |
| `MEDIA.BLOCK_NOT_A_PAGE` | Block is not a page. | Page-grid thumbnail APIs require a page block. Pass a block where api.block.getType(id) == '//ly.img.ubq/page'. | |
| `MEDIA.BLOCK_NOT_PAGE_OR_CHILD` | Block must be a page or a child of a page. | The block must live under a page in the scene tree. Reparent it or pick a page descendant. | |
| `MEDIA.BLOCK_NOT_VALID` | Block is not valid. | The block id no longer references a live block. Re-resolve the id before requesting media. | |
| `MEDIA.BLOCK_NOT_VIDEO_FILL` | Block is not a video fill. | This operation requires a block whose fill is a video. Verify api.block.getKind(id) returns 'video' before calling. | |
| `MEDIA.BLOCK_SIZE_ZERO` | Block size is zero. | Cannot generate a thumbnail for a zero-sized block. Set the block's width and height first. | |
| `MEDIA.CANVAS_SURFACE_GET_FAILED` | Could not get canvas surface. | The canvas does not expose its underlying surface. This indicates an internal renderer state issue. | |
| `MEDIA.CHANNEL_COUNT_INVALID` | The number of channels must be 1 or 2. | Mono (1) and stereo (2) are the only supported channel counts. | |
| `MEDIA.FRAME_COUNT_INVALID` | The number of frames must be greater than 0. | Pass a positive \`frameCount\`. | |
| `MEDIA.GRID_DIMENSIONS_INVALID` | Rows and columns must be greater than 0. | Pass positive \`rows\` and \`columns\` for grid-thumbnail generation. | |
| `MEDIA.IMAGE_ENCODE_FAILED` | Image encoding failed. | The encoder rejected the source image. This is usually an unsupported pixel format or zero-sized input. | |
| `MEDIA.NEGATIVE_TIME_RANGE` | A negative time range is not allowed. | Ensure end > start (both in seconds, non-negative). | |
| `MEDIA.OFFSCREEN_CANVAS_GET_FAILED` | Could not get offscreen canvas. | Skia surface has no canvas attached. The compute context may not have been initialized. | |
| `MEDIA.OFFSCREEN_SURFACE_CREATE_FAILED` | Could not create offscreen surface. | Skia could not allocate the offscreen rendering surface. Out-of-memory or unsupported pixel format is the likely cause. | |
| `MEDIA.OPERATION_UNSUPPORTED_FOR_BLOCK` | This operation is not supported for the given block. | The selected block type does not implement this media operation. Check api.block.getType(id) and call this API only on the block types it supports. | |
| `MEDIA.SAMPLES_PER_CHUNK_INVALID` | The number of samples per chunk must be greater than 0. | Pass a positive \`samplesPerChunk\`. | |
| `MEDIA.SAMPLE_COUNT_INVALID` | The number of samples must be greater than 0. | Pass a positive \`sampleCount\`. | |
| `MEDIA.SNAPSHOT_FAILED` | Could not snapshot rendered thumbnail. | Skia could not produce an image snapshot from the rendered surface. The surface may not be readable on this backend. | |
| `MEDIA.THUMBNAIL_ALLOC_FAILED` | Failed to allocate memory for thumbnail data. | The thumbnail size exceeds available memory. Reduce dimensions or free memory. | |
| `MEDIA.THUMBNAIL_HEIGHT_INVALID` | The height of the thumbnail must be greater than 0. | Pass a positive \`height\` (pixels). | |
| `MEDIA.THUMBNAIL_UPSCALE_FAILED` | Could not upscale thumbnail to requested size. | Upscaling failed mid-render. The destination size may be larger than the maximum surface size on this platform. | |
| `MEDIA.UPSCALE_SURFACE_CREATE_FAILED` | Could not create upscale surface. | Skia could not allocate the destination surface for upscaling. Out-of-memory or unsupported config is the likely cause. | |
| `MEDIA.VIDEO_FETCH_FAILED` | Failed to fetch video. | The video resource could not be retrieved. Check the URI, asset-source registrations, and network connectivity. | |
## SCENE
Scene-level operations (load, save, archive, structural validation).
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `SCENE.ARCHIVAL_REQUEST_FAILED` | Archival request failed: \{reason} | An async archival operation (save/load) was completed in error state. The underlying reason is: \{reason} | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_ADD_RESOURCE_FAILED` | Could not add the resource '\{resource}' to the archive. Adding data failed. | The engine could not fetch or write the bytes for \{resource}. Verify the resource is reachable and not larger than the archive can hold. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_CHUNK_READ_FAILED` | Failed to read chunk data from data provider. | The data provider returned no bytes for an available range. The underlying source may have disconnected or returned a partial response. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_CORRUPTED_EMPTY_RESOURCE` | Corrupted archive. Some elements in the scene are referencing empty data, e.g., '\{resource}'. | The archive is internally inconsistent. Regenerate it from the original scene and verify the source has no missing assets. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_CREATE_FAILED` | Could not create archive. | Final archive assembly failed for an unspecified reason. Inspect prior log lines for the underlying failure. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_DATA_PROVIDER_RANGE_UNAVAILABLE` | Data provider range is not available. | The requested byte range cannot be served by the underlying provider. Check the resource size and the requested offset/length. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_DATA_UNAVAILABLE_FOR_URL` | Archive data is not available for URL '\{url}'. | The archive references \{url} but the data could not be retrieved. Check the URL and the asset source it resolves through. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_FETCH_FAILED` | Failed to fetch archive from URL '\{url}': \{reason} | Network or asset-source failure. Verify the URL is reachable and the host serves the archive bytes. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_INVALID` | Not a valid archive. | The supplied bytes are not a recognizable CE.SDK archive. Was the file produced by saveToArchive()? | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_LOAD_AS_BLOCKS_NOT_SCENE` | Archive contains a blocks file. This archive has to be loaded as blocks and not as a scene. | Call loadFromArchiveAsBlocks() instead of loadFromArchive() for this file. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_LOAD_AS_SCENE_NOT_BLOCKS` | Archive contains a scene file. This archive has to be loaded as a scene and not as blocks. | Call loadFromArchive() instead of loadFromArchiveAsBlocks() for this file. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_MISSING_FILE` | Archive is missing a \{kind} file. | The archive is malformed: it does not contain the expected \{kind}.\* entry at its root. The file may be truncated or corrupted. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_NO_CURRENT_RESOURCE` | No current resource. | The data provider has not selected a resource. Call seekResource() (or the equivalent) before reading. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_NO_RESOURCE_BEGUN` | No resource begun or entry not open. | Call beginResource() before writing chunks or ending the resource. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_OFFSET_EXCEEDS_RESOURCE_SIZE` | Requested offset exceeds resource size. | Clamp the requested offset to be less than the resource's reported size before reading. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_RESOURCES_TOO_LARGE` | Resources too large. Not enough memory to create archive. | Allocation for the in-memory archive failed. Reduce scene size, free memory, or use a platform with more available RAM. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_RESOURCE_ALREADY_BEGUN` | Resource already begun. | A streaming resource is already in progress. Call endResource() before beginning another. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_RESOURCE_DATA_INVALID` | Resource data is invalid. | The bytes returned by the data provider failed validation. The archive may be corrupted. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_RESOURCE_DATA_UNAVAILABLE` | Resource data is not available. | The data provider has no bytes for the current resource. The underlying source may have been moved or deleted. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_STREAMED_WRITE_FAILED` | Failed to write streamed data to archive: \{reason} | Streaming a resource into the archive failed mid-flight. The underlying writer reported: \{reason} | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_WRITER_ALREADY_INITIALIZED` | Archive already initialized. | The ArchiveBufferWriter is already in an initialized state. Construct a new writer or release the current one before re-initializing. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ARCHIVE_WRITER_NOT_INITIALIZED` | Archive not initialized. | Call initialize() on the ArchiveBufferWriter before invoking entry or directory operations. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.BLOCKS_INPUT_INVALID` | Received blocks input is not a valid serialization. | The payload does not parse as a CE.SDK blocks document. Confirm it was produced by api.block.saveToString() or saveToArchive(). | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.BLOCK_AT_INDEX_INVALID` | Block at index \{index} is invalid. Can't save list. | One of the blocks in the list was destroyed. Filter or re-resolve the ids before saving. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.COMPRESSION_SERVICE_UNAVAILABLE` | Compression service not available for detected format. | The archive is compressed with a codec this build does not support. Ensure the engine was built with the matching compression backend. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.CONTENT_EMPTY` | Received empty \{kind} content. | The serialized \{kind} payload is empty. Verify the source produced non-empty content before passing it to the engine. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.DECOMPRESS_FAILED` | Failed to decompress scene data: \{reason} | The compressed payload could not be decoded. The archive may be truncated or produced by an incompatible writer. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.DISALLOWED_SCHEMES` | Scene contains disallowed schemes in resource URLs: \{schemes} | The scene references resources via blocked URL schemes (\{schemes}). Update the policy or rewrite the scene's resource URLs. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ENTITY_INVALID` | Invalid scene entity. | The provided design-block id no longer references a valid entity. It may have been destroyed by an earlier call; re-resolve the id before use. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ENTITY_NOT_A_SCENE` | Entity is not a scene. | Pass a scene block id. Use api.scene.get() or api.block.findByType('//ly.img.ubq/scene') to obtain one. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.LOAD_FROM_URI_FAILED` | Could not load \{kind} from \{uri}. | Check that the URI is reachable and serves a valid \{kind} payload. Common causes: 404, CORS, or wrong file type. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.MEDIA_URI_NOT_FOUND` | Could not load \{kind} from \{uri}. | The \{kind} resource at \{uri} could not be resolved. Check the URI, network connectivity, and asset-source registrations. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.MEDIA_URI_PARSE_FAILED` | Could not load \{kind}: \{reason} | The \{kind} URI failed to parse. Confirm it is a well-formed absolute URI (http(s)://, file://, or a data URL). | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.MUST_EXIST` | A scene must already exist. | Create or load a scene before invoking this API. Use api.scene.create() or loadFromString(). | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.MUST_EXIST_FOR_TEMPLATE` | A scene must already exist for a template to be applied to it. | Load a scene first, then apply the template. Templates only modify existing scenes. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.NOT_IMPLEMENTED` | Not implemented. | This data-provider method has no implementation on the active subclass. The caller must select a different provider or operation. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.NOT_SCENE_TYPE` | Not a scene. | The root deserialized entity is not a scene. The file may have been produced by saveAsBlocks() — load it with loadBlocks() instead. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.NOT_VALID` | No valid scene. | Create or load a scene before calling APIs that operate on the active scene. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.NO_MODE` | Scene has no mode set. | Use api.scene.setMode(scene, mode) before requesting mode-dependent behavior. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.NO_PAGES_FOR_AUDIO_EXPORT` | Scene has no pages to export audio from. | Add at least one page to the scene before exporting audio. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.NO_PAGE_FOUND` | No page found. | Add at least one page to the scene or check the current viewport's page resolution. | [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) |
| `SCENE.NO_SCENE_FOUND` | No scene found. | The serialized payload deserialized successfully but contains no scene root. Was the file saved as 'blocks' instead of 'scene'? | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.TEMP_FILE_CREATE_FAILED` | Failed to create temporary file. | The engine could not create a scratch file for archival. Check available disk space and the platform's temporary-directory permissions. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_CHUNK_WRITE_FAILED` | Failed to write chunk to ZIP entry: \{zipErrorCode} | libzip rejected writing a chunk to the current streaming entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_CREATE_FAILED` | Failed to create ZIP archive. | The ZIP container could not be opened for writing. This is usually a file-system or memory issue. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_DIRECTORY_ENTRY_CLOSE_FAILED` | Failed to close ZIP directory entry: \{zipErrorCode} | libzip rejected closing a directory entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_DIRECTORY_ENTRY_OPEN_FAILED` | Failed to open ZIP directory entry: \{zipErrorCode} | libzip rejected opening a directory entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_ENTRY_CLOSE_FAILED` | Failed to close ZIP entry: \{zipErrorCode} | libzip rejected closing the current entry with error code \{zipErrorCode}. The entry data may be truncated. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_ENTRY_OPEN_FAILED` | Failed to open ZIP entry: \{zipErrorCode} | libzip rejected opening a new entry with error code \{zipErrorCode}. The archive state may be inconsistent. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_ENTRY_WRITE_FAILED` | Failed to write ZIP entry: \{zipErrorCode} | libzip rejected writing bytes to the current entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_STREAMING_ENTRY_CLOSE_FAILED` | Failed to close ZIP entry for streaming resource: \{zipErrorCode} | libzip rejected closing a streaming entry with error code \{zipErrorCode}. The streamed bytes may be truncated. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_STREAMING_ENTRY_OPEN_FAILED` | Failed to open ZIP entry for streaming resource: \{zipErrorCode} | libzip rejected opening a streaming entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_WRITER_CLOSE_FAILED` | Failed to close ZIP writer: \{zipErrorCode} | libzip reported error \{zipErrorCode} on close. The output bytes may be truncated; treat the archive as invalid. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_WRITER_CREATE_FAILED` | Failed to create ZIP writer. | The libzip writer could not be allocated. Out-of-memory or platform-resource exhaustion is the likely cause. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
| `SCENE.ZIP_WRITER_OPEN_FAILED` | Failed to open ZIP writer: \{zipErrorCode} | libzip rejected the writer initialization with error code \{zipErrorCode}. Inspect libzip's documentation for the meaning. | [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) |
## UTILS
Generic utility failures not specific to another category.
| Code | Message | Hint | Docs |
| --- | --- | --- | --- |
| `UTILS.APNG_DATA_TOO_SMALL` | APNG data is empty or too small. | The APNG buffer is too short to contain a valid PNG signature. The source may be truncated. | |
| `UTILS.APNG_FCTL_BEFORE_ACTL` | APNG has fcTL before acTL. | fcTL chunks must follow acTL. Reorder or regenerate the APNG. | |
| `UTILS.APNG_FDAT_WITHOUT_FCTL` | APNG has fdAT with no preceding fcTL. | Each fdAT chunk must follow an fcTL declaring its frame. The animation stream is broken. | |
| `UTILS.APNG_FDAT_WITHOUT_SEQ` | APNG has an fdAT chunk without a sequence number. | fdAT chunks must include a 4-byte sequence number. The chunk is malformed. | |
| `UTILS.APNG_FRAME_INDEX_OUT_OF_RANGE` | APNG frame index out of range. | The requested frame index is past the last frame. Clamp it to \[0, frameCount). | |
| `UTILS.APNG_FRAME_NO_PIXEL_DATA` | APNG has an animation frame with no pixel data. | Each animation frame must have at least one IDAT/fdAT chunk. | |
| `UTILS.APNG_FRAME_OUT_OF_CANVAS` | APNG frame rectangle extends beyond the canvas. | fcTL coordinates must fit inside the canvas declared by IHDR. The file is malformed. | |
| `UTILS.APNG_FRAME_ZERO_DIMENSIONS` | APNG has an animation frame with zero dimensions. | Each fcTL must declare positive width and height. The frame is invalid. | |
| `UTILS.APNG_IDAT_BEFORE_IHDR` | APNG has IDAT before IHDR. | IDAT chunks must follow IHDR. The file violates PNG chunk ordering. | |
| `UTILS.APNG_INVALID_ACTL` | APNG has invalid or misplaced acTL chunk. | The acTL chunk must precede IDAT and appear exactly once. The animation chunk is malformed. | |
| `UTILS.APNG_INVALID_FCTL` | APNG has an invalid fcTL chunk. | An animation control chunk failed validation. The file may be partially corrupted. | |
| `UTILS.APNG_INVALID_IHDR` | APNG has invalid or duplicated IHDR chunk. | PNG must contain exactly one IHDR chunk at the start. The file is malformed. | |
| `UTILS.APNG_INVALID_SIGNATURE` | APNG data does not start with a valid PNG signature. | The first 8 bytes must be the PNG magic number. The source is not a PNG/APNG file. | |
| `UTILS.APNG_MISSING_ACTL` | Not an APNG: acTL chunk is missing. | This is a static PNG, not an APNG. Use it with a single-frame decode path or supply an animated file. | |
| `UTILS.APNG_MISSING_IHDR` | APNG source has no valid IHDR chunk. | Every PNG starts with IHDR. The file is empty or corrupted. | |
| `UTILS.APNG_NO_FRAMES` | APNG declares no animation frames. | The acTL reports zero frames. Regenerate the APNG with at least one fcTL/fdAT. | |
| `UTILS.APNG_ZERO_CANVAS` | APNG has zero canvas dimensions. | Canvas width and height must be > 0. The IHDR is corrupt. | |
| `UTILS.CAPTION_DATA_UNAVAILABLE` | Caption data not available. | The caption resource has no bytes registered. Ensure data is loaded before requesting captions. | |
| `UTILS.CAPTION_PARSE_EMPTY` | Failed to parse captions, no captions found. | The caption file parsed but contained no entries. Verify the source format. | |
| `UTILS.CAPTION_UNSUPPORTED_MIME` | Unsupported caption mime type: \{mimeType} | Caption format '\{mimeType}' is not recognized. Convert to a supported format (e.g. text/vtt, application/x-subrip). | |
| `UTILS.CAPTION_UTF16_INVALID_SIZE` | Failed to parse captions, unexpected file size for UTF-16BE encoded text. | UTF-16BE text must have an even byte count. The file is truncated. | |
| `UTILS.COMPRESSION_EMPTY_DATA` | Cannot decompress empty data. | The input buffer is empty. Pass at least one byte of compressed data. | |
| `UTILS.COMPRESSION_FORMAT_NONE_FOR_COMPRESS` | Cannot compress data with format None: no compression requested. | Pass a non-None compression format if compression is intended. | |
| `UTILS.COMPRESSION_FORMAT_NONE_FOR_DECOMPRESS` | Cannot decompress data with format None: data is not compressed. | The detected format is None — the data is not compressed and does not need decompression. | |
| `UTILS.COMPRESSION_FORMAT_UNSUPPORTED` | Unsupported compression format: \{format} | Compression format \{format} is not handled by this build. | |
| `UTILS.COMPRESSION_NO_MAGIC_BYTES` | Unable to detect compression format: data does not have valid compression magic bytes. | The first bytes do not match any supported compression format. The data may not be compressed. | |
| `UTILS.COMPRESSION_ZSTD_INVALID_FRAME` | Invalid zstd frame. | The zstd decoder rejected the frame header. The data may be truncated. | |
| `UTILS.ENGINE_UNKNOWN_COMPONENT_TYPE` | Component \{component} is not a known reflected type. | Component '\{component}' is not registered with the reflection system. | |
| `UTILS.ENUM_VALUE_INVALID` | Invalid enum value, expected one of: \{validValues} | The supplied string is not a member of the target enum. Pass one of: \{validValues}. | |
| `UTILS.FILE_ALLOC_FAILED` | Could not allocate an output buffer of size \{size} to load \{path} | Out-of-memory loading '\{path}' (\{size} bytes). Free memory or stream the file in chunks. | |
| `UTILS.FILE_MAP_FAILED` | Could not map file \{path} into memory: \{reason} | mmap() rejected '\{path}': \{reason}. Out-of-memory or virtual-memory limits may be the cause. | |
| `UTILS.FILE_OPEN_FAILED` | Could not open file \{path}: \{reason} | The file '\{path}' could not be opened. Reason: \{reason} | |
| `UTILS.FILE_READ_FAILED` | Could not read complete file \{path}: \{reason} | Reading '\{path}' returned an error or hit EOF prematurely. Reason: \{reason} | |
| `UTILS.FILE_SIZE_FAILED` | Could not determine file size for \{path}: \{reason} | stat() failed on '\{path}': \{reason}. The file may have been removed or is unreadable. | |
| `UTILS.FILE_TOO_LARGE` | File \{path} has size \{size} larger than the maximum supported \{max} | Loading is capped at \{max} bytes. Use streaming APIs for files larger than this. | |
| `UTILS.GIF_PARSE_FAILED` | Failed to parse GIF file: \{reason} | The GIF source could not be decoded. Underlying reason: \{reason} | |
| `UTILS.METAANY_EXPECTED_ARRAY` | Expected array, but got \{actual} | JS value must be an array. Got '\{actual}' instead. | |
| `UTILS.METAANY_ITEM_MAP_FAILED` | Couldn't map item at index \{index} > \{reason} | Array element \{index} failed conversion. Reason: \{reason} | |
| `UTILS.METAANY_MEMBER_MAP_FAILED` | Couldn't map value for member \`\{member}\` > \{reason} | Member '\{member}' failed conversion. Reason: \{reason} | |
| `UTILS.METAANY_MISSING_PROPERTY` | Expected object of type '\{type}' to have property '\{property}'. | The JS object is missing required property '\{property}'. Add it before passing across the boundary. | |
| `UTILS.METAANY_NON_STRING_TO_STRING` | Can't map non-string em::val to string. | The JavaScript value is not a string. Convert it to a string in JS before crossing the boundary. | |
| `UTILS.METAANY_RESULT_TYPE_NOT_REFLECTED` | Type of Result value for \{details} is not reflected. | The Result type returned from '\{details}' is not reflection-registered. | |
| `UTILS.METAANY_SET_FAILED` | Could not set value for \{member} on object of type \{type} | The reflected setter for '\{member}' on '\{type}' rejected the value. | |
| `UTILS.METAANY_TYPE_NEEDS_REFLECTION` | Type \{type} must have reflection info. | Register type '\{type}' with the reflection system before crossing the JS↔C++ boundary. | |
| `UTILS.METAANY_UNDEFINED` | Can't map \`undefined\` to \`\{type}\` | JavaScript \`undefined\` cannot be converted to '\{type}'. Pass a defined value of the expected type. | |
| `UTILS.METAANY_UNHANDLED_INTEGRAL` | Unhandled integral type: \{type} | The integral type '\{type}' has no MetaAny ↔ em::val mapping. | |
| `UTILS.METAANY_UNHANDLED_SEQUENCE` | Unhandled sequence container type: \{type} | Sequence container '\{type}' has no MetaAny ↔ em::val mapping. | |
| `UTILS.METAANY_UNHANDLED_TYPE_KIND` | Unhandled type kind. \{type} | Type kind for '\{type}' is not recognized by the conversion layer. | |
| `UTILS.META_TYPE_FUNCTION_UNKNOWN` | Function must be known. | The requested reflected function is not registered on this MetaType. | |
| `UTILS.META_TYPE_INVALID` | Not a valid type, no name available. | The MetaType handle does not refer to a registered reflection type. | |
| `UTILS.META_TYPE_INVOKE_FAILED` | Could not invoke \{name}. | The reflected function '\{name}' could not be invoked. Argument types or arity may not match. | |
| `UTILS.MKV_EBML_PARSE_FAILED` | Failed to parse EBML header. | The EBML header is malformed. The file may not be a Matroska container. | |
| `UTILS.MKV_INVALID_DATA` | Invalid MKV data. | The MKV demuxer rejected the source bytes. The file may be truncated or use an unsupported variant. | |
| `UTILS.MKV_NO_TRACKS` | No tracks found. | The MKV file declares no tracks. Verify it contains video/audio streams. | |
| `UTILS.MKV_SEGMENT_CREATE_FAILED` | Failed to create segment. | libwebm could not allocate a segment. Out-of-memory or unsupported MKV variant. | |
| `UTILS.MKV_SEGMENT_LOAD_FAILED` | Failed to load segment. | The MKV segment payload is unreadable. The file may be truncated mid-segment. | |
| `UTILS.MP4_AUDIO_CODEC_UNSUPPORTED` | Unsupported audio codec '\{fourcc}' (OTI=\{oti}). | MP4 audio must use the AAC (mp4a), MP3, or AMR (samr/sawb) codec. Re-encode the audio to AAC. | |
| `UTILS.MP4_AUDIO_TRACK_INDEX_OUT_OF_BOUNDS` | Audio track index \{index} is out of bounds. Valid range: 0-\{max} | Pass an audio track index within \[0, \{max}]. | |
| `UTILS.MP4_OPEN_FAILED` | Failed to open MP4 file. | The MP4 demuxer could not open the source. The file may be missing or unreadable. | |
| `UTILS.MP4_VIDEO_CODEC_UNSUPPORTED` | Unsupported video codec '\{fourcc}' (OTI=\{oti}). | MP4 video must use the H.264 (avc1) or H.265 (hvc1) codec. Re-encode the video to H.264 or H.265, or use a WebM file for AV1, VP8, or VP9. | |
| `UTILS.PIXEL_BUFFER_BACKEND_TEXTURE_INCOMPLETE` | Backend texture is incomplete. | The Skia backend texture handle does not carry enough state for read-back. | |
| `UTILS.PIXEL_BUFFER_GL_TEXTURE_INFO_FAILED` | Failed to retrieve OpenGL texture info. | The backend texture did not expose its GL info. The compute context may not be a GL context. | |
| `UTILS.PIXEL_BUFFER_INVALID_IMAGE` | Invalid RasterImage or SkImage. | The image handle is null or not a recognized Skia/raster image. | |
| `UTILS.PIXEL_BUFFER_NOT_STREAM_FILL` | This operation is only supported for pixel stream fills. | Use a block with a pixel-stream fill type. Standard fills are not supported by this conversion API. | |
| `UTILS.PIXEL_BUFFER_NO_BACKEND_TEXTURE` | No valid backend texture. | The Skia image is not backed by a GPU texture. The current compute context may not support direct read-back. | |
| `UTILS.PIXEL_BUFFER_NO_CANVAS` | No canvas. | The Skia surface has no canvas attached. The compute context may not be initialized. | |
| `UTILS.PIXEL_BUFFER_NO_GPU_CONTEXT` | No GPU context. | The compute context lacks a GPU backend. Pixel-buffer conversion requires GPU access. | |
| `UTILS.PIXEL_BUFFER_UNSUPPORTED_FORMAT` | Unsupported pixel format. | The native pixel buffer is not 32-bit BGRA. Convert the source to kCVPixelFormatType\_32BGRA before updating the pixel-stream fill. | |
| `UTILS.REFLECTION_BLOCK_NOT_VALID` | The block with ID \{block} is not valid. It may have been deleted, e.g., when a new scene was loaded. | Resolve the block id before invoking reflection APIs. Block ids become invalid after scene reloads or block destruction. | |
| `UTILS.REFLECTION_COMPONENT_NOT_REFLECTED` | '\{component}' is not a reflected type (tried to get '\{memberPath}' member). | Register the component with the reflection system or pass an already-registered type name. | |
| `UTILS.REFLECTION_COMPONENT_NOT_SET` | Component \{component} is not set on entity \{entity}. | Verify the entity has been assigned the component before reading. Use the entity's component-list API to inspect. | |
| `UTILS.REFLECTION_ENTITY_MISSING_COMPONENT` | Entity ID \{entity}\{optionalType} doesn't have component "\{component}". | The block does not carry this component. Use a block type that supports it or attach the component first. | |
| `UTILS.REFLECTION_KEY_NOT_FOUND` | Could not find member "\{key}" | The key '\{key}' does not exist on the current type. Inspect available members and adjust. | |
| `UTILS.REFLECTION_KEY_PATH_EMPTY` | Key path must be at least 1 element but is empty. | Provide a non-empty key path (e.g. "transform/x"). | |
| `UTILS.REFLECTION_KEY_PATH_MEMBER_MISSING` | Member "\{member}" for type "\{type}" not found for given key path "\{keyPath}". Valid members for key path "\{prefix}*" are: \{members}. | Adjust '\{keyPath}' to reference one of the listed members. | |
| `UTILS.REFLECTION_MEMBER_NOT_ACCESSIBLE_BY_REF` | Could not get member "\{member}". Must access by reference, if not modifying the most nested member. Reflected members for key path "\{prefix}*" are: \{members}. | Intermediate segments of a key path require by-reference access. Mark '\{member}' as a by-reference member or address its tail directly. | |
| `UTILS.REFLECTION_MEMBER_NOT_FOUND` | Member \{member} not found on current type. | The member '\{member}' is not a reflected field on this type. Check the type's reflected members. | |
| `UTILS.REFLECTION_MEMBER_TYPE_NOT_REFLECTED` | Type of member named "\{member}" on "\{type}" is not reflected. Reflected members for key path "\{prefix}*" are: \{members}. | Reflection cannot descend into '\{member}' because its type is not registered. Reflect the type or stop the key path at '\{member}'. | |
| `UTILS.REFLECTION_NO_MEMBERS_FOR_PREFIX` | "\{member}" has no reflected members. | The intermediate type at '\{member}' has no fields to descend into. Adjust the key path. | |
| `UTILS.REFLECTION_SET_MEMBER_FAILED` | Could not set member "\{member}" of type "\{type}". | The reflected setter for '\{member}' returned false. Verify the value type matches the member. | |
| `UTILS.REFLECTION_SET_WILDCARD_TYPE_MISMATCH` | Type mismatch between "\{lhs}" and "\{rhs}": "\{lhsType}" vs. "\{rhsType}". | Wildcard set requires all members to share a type. | |
| `UTILS.REFLECTION_TYPE_NOT_REFLECTED` | \{component} is not a reflected type. | Register the type with the reflection system before accessing it dynamically. | |
| `UTILS.REFLECTION_TYPE_NO_MEMBERS` | Type "\{type}" has no reflected members. | The wildcard key path requires a type with reflected fields. Use a concrete member name or pick a different type. | |
| `UTILS.REFLECTION_UNSUPPORTED_TYPE` | Unsupported type. | The reflected type is not handled by this operation. Pass a supported type. | |
| `UTILS.REFLECTION_WILDCARD_NOT_TAIL` | Wildcard only supported at tail end of path. | Move the wildcard '*' to the last segment of the key path. | |
| `UTILS.REFLECTION_WILDCARD_SET_PARTIAL` | Could not set all members of type "\{type}". | At least one reflected member of '\{type}' rejected the assignment. Member-level setters may have failed silently. | |
| `UTILS.REFLECTION_WILDCARD_TYPE_MISMATCH` | Type mismatch between "\{lhs}" and "\{rhs}". | Wildcard get requires all matched members to share a type. Members '\{lhs}' and '\{rhs}' differ. | |
| `UTILS.REFLECTION_WILDCARD_VALUE_MISMATCH` | Value mismatch between "\{lhs}" and "\{rhs}". | Wildcard get returns a single representative value; members '\{lhs}' and '\{rhs}' currently disagree. | |
| `UTILS.SETTING_NO_ARGS` | Received no arguments. Can't determine type. | The setting's type is inferred from the value you pass, so the call cannot accept zero arguments. Pass the setting value explicitly (bool, int, float, color, or string). | |
| `UTILS.STD_EXPECTED_STRING_ERROR` | \{message} | Adapter path between \`std::expected\\` and \`Result\\`. The source returned a free-text \`std::string\` error — surfacing it through the catalog so consumers still see the original wording. Convert the producer to return a catalog id when possible. | |
| `UTILS.STORAGE_KEY_NOT_FOUND` | Key not found. | The requested storage key has no value. Write it before reading, or guard the read by checking for existence first. | |
| `UTILS.TRACKING_NOT_ENABLED` | Tracking is not enabled. | Enable tracking on the engine before invoking tracking APIs. | |
| `UTILS.URI_NON_ASCII` | Failed to parse URI: \{uri}. Contains non-ASCII byte 0x\{byte} at position \{position}. URIs must only contain ASCII characters, please percent-encode special characters. | Percent-encode special characters in '\{uri}' before parsing. Non-ASCII byte 0x\{byte} found at position \{position}. | |
| `UTILS.URI_PARSE_FAILED` | Failed to parse URI: \{uri} | The URI '\{uri}' is malformed. Confirm scheme, host, and path. | |
| `UTILS.WAV_DATA_BEFORE_FMT` | Invalid WAV file: data chunk found before fmt chunk. | WAV chunk order requires fmt before data. Re-encode the file. | |
| `UTILS.WAV_FMT_CHUNK_TOO_SMALL` | WAV fmt chunk too small: expected at least \{expected} bytes, got \{actual}. | The fmt chunk does not carry the minimum WAV header fields. | |
| `UTILS.WAV_INVALID_BITS_PER_SAMPLE` | Invalid WAV bits\_per\_sample: \{bitsPerSample} | WAV bits\_per\_sample must be one of 8/16/24/32. Got \{bitsPerSample}. | |
| `UTILS.WAV_MISSING_FMT_OR_DATA` | Invalid WAV file: missing fmt or data chunk. | Both fmt and data chunks are required. The file is missing one or both. | |
| `UTILS.WAV_NOT_WAVE` | Not a WAV file: RIFF format is not WAVE. | The RIFF container is not a WAVE file. The source is a different RIFF type or not RIFF at all. | |
| `UTILS.WAV_TRUNCATED_FMT_CHUNK` | WAV file truncated: not enough data for fmt chunk. | The fmt chunk is cut off. The file is incomplete. | |
| `UTILS.WAV_TRUNCATED_RIFF_HEADER` | WAV file truncated: not enough data for RIFF header. | The file is shorter than the 12-byte RIFF header. The source is incomplete. | |
| `UTILS.WAV_TRUNCATED_UNKNOWN_CHUNK` | WAV file truncated: not enough data to skip unknown chunk. | Parsing tried to skip an unknown chunk but ran past the buffer end. The file is incomplete. | |
| `UTILS.WAV_UNSUPPORTED_AUDIO_FORMAT` | Unsupported WAV audio format: \{audioFormat} | WAV audio format code \{audioFormat} is not handled. Re-encode as PCM (1) or IEEE float (3). | |
| `UTILS.WAV_ZERO_CHANNELS` | Invalid WAV file: num\_channels is 0. | WAV must declare at least one channel. | |
| `UTILS.WAV_ZERO_SAMPLE_RATE` | Invalid WAV file: sample\_rate is 0. | WAV must declare a positive sample rate. | |
| `UTILS.WAV_ZERO_SIZE_CHUNK` | Invalid WAV file: zero-size chunk encountered. | A chunk reports zero size, which would cause infinite parsing. The file is malformed. | |
| `UTILS.ZSTD_COMPRESS_FAILED` | Zstd compression failed: \{reason} | The zstd library reported a compression failure. \`reason\` is the value returned by \`ZSTD\_getErrorName(...)\`. | |
| `UTILS.ZSTD_DECOMPRESS_FAILED` | Zstd decompression failed: \{reason} | The zstd library reported a decompression failure. \`reason\` is the value returned by \`ZSTD\_getErrorName(...)\`. The input may be truncated or not zstd-encoded. | |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Events"
description: "Subscribe to block creation, update, and deletion events to track changes in your CE.SDK scene."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/events-353f97/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/)
---
```kotlin file=@cesdk_android_examples/engine-guides-events/Events.kt reference-only
package ly.img.editor.examples.engine.guides.events
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockEvent
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
suspend fun events(engine: Engine) = withContext(engine.dispatcher) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val allBlocksSubscription = engine.event.subscribe(blocks = emptyList())
.onEach { events ->
events.forEach { event ->
println("[All Blocks] ${event.type} event for block ${event.block}")
}
}.launchIn(this)
val graphic = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = graphic, shape = rectShape)
engine.block.setPositionX(graphic, value = 200F)
engine.block.setPositionY(graphic, value = 150F)
engine.block.setWidth(graphic, value = 400F)
engine.block.setHeight(graphic, value = 300F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(block = graphic, fill = imageFill)
engine.block.setEnum(
block = graphic,
property = "contentFill/mode",
value = "Cover",
)
engine.block.appendChild(parent = page, child = graphic)
engine.block.forceLoadResources(listOf(graphic))
val specificBlocksSubscription = engine.event.subscribe(blocks = listOf(graphic))
.onEach { events ->
events.forEach { event ->
println("[Specific Block] ${event.type} event for block ${event.block}")
}
}.launchIn(this)
val processedEventsSubscription = engine.event.subscribe(blocks = emptyList())
.onEach { events ->
events.forEach { event ->
when (event.type) {
DesignBlockEvent.Type.CREATED -> {
val blockType = engine.block.getType(event.block)
println("Block created with type: $blockType")
}
DesignBlockEvent.Type.UPDATED -> {
println("Block ${event.block} was updated")
}
DesignBlockEvent.Type.DESTROYED -> {
println("Block ${event.block} was destroyed")
}
}
}
}.launchIn(this)
try {
val cachedBlocks = mutableSetOf(graphic)
cachedBlocks.removeAll { block -> !engine.block.isValid(block) }
engine.block.setRotation(graphic, radians = 0.1F)
engine.block.setFloat(
block = graphic,
property = "opacity",
value = 0.9F,
)
engine.block.destroy(graphic)
println("Destroyed graphic block $graphic")
// Let the guide test collect the final batched events before cleanup.
delay(1_000)
} finally {
withContext(NonCancellable) {
allBlocksSubscription.cancelAndJoin()
specificBlocksSubscription.cancelAndJoin()
processedEventsSubscription.cancelAndJoin()
}
}
}
```
Monitor and react to block changes in real time by subscribing to creation,
update, and destruction events in your CE.SDK scene.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-events)
Events let you monitor block changes as they happen. On Android,
`engine.event.subscribe()` returns a `Flow>`, so you
collect batched updates inside a coroutine on `Dispatchers.Main` and cancel the
collection job when you no longer need it.
This guide covers subscribing to block lifecycle events, processing the three
event types (`CREATED`, `UPDATED`, `DESTROYED`), filtering events to specific
blocks, understanding batching and deduplication behavior, and properly
cleaning up subscriptions.
## Setup
Create a scene and page before subscribing. Call the sample from your Engine's
main-thread coroutine; each event collector stays scoped to that coroutine and
is cancelled during cleanup.
```kotlin highlight-android-setup
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
## Event Types
CE.SDK provides three event types that capture the block lifecycle:
| Type | Description |
| --- | --- |
| `DesignBlockEvent.Type.CREATED` | Fires when a new block is created. |
| `DesignBlockEvent.Type.UPDATED` | Fires when any property of a block changes. |
| `DesignBlockEvent.Type.DESTROYED` | Fires when a block is destroyed. |
Each `DesignBlockEvent` contains a `block` property with the block ID and a
`type` property indicating which event occurred.
## Subscribing to All Blocks
Use `engine.event.subscribe()` to register a collector that receives batched
events. Pass `emptyList()` to receive events from every block:
```kotlin highlight-android-subscribe-all
val allBlocksSubscription = engine.event.subscribe(blocks = emptyList())
.onEach { events ->
events.forEach { event ->
println("[All Blocks] ${event.type} event for block ${event.block}")
}
}.launchIn(this)
```
The `launchIn(this)` call returns a `Job`. Keep that job so you can cancel the
subscription during cleanup.
## Subscribing to Specific Blocks
When you only care about certain blocks, pass their IDs to filter the flow:
```kotlin highlight-android-subscribe-specific
val specificBlocksSubscription = engine.event.subscribe(blocks = listOf(graphic))
.onEach { events ->
events.forEach { event ->
println("[Specific Block] ${event.type} event for block ${event.block}")
}
}.launchIn(this)
```
Filtering reduces overhead because the engine only prepares events for the
blocks you are tracking.
### API Reference
Signature: `fun subscribe(blocks: List = emptyList()): Flow>`
Subscribe to block life-cycle events
- `blocks`: a list of blocks to filter events by. If the list is empty, events for every block are sent.
## Creating Blocks and Handling `CREATED` Events
Creating a block triggers a `CREATED` event. This example also appends the block
to the page, adds a 400×300 graphic at `(200, 150)` with a remote image fill,
and sets `Cover` content fill mode.
```kotlin highlight-android-event-created
val graphic = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = graphic, shape = rectShape)
engine.block.setPositionX(graphic, value = 200F)
engine.block.setPositionY(graphic, value = 150F)
engine.block.setWidth(graphic, value = 400F)
engine.block.setHeight(graphic, value = 300F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(block = graphic, fill = imageFill)
engine.block.setEnum(
block = graphic,
property = "contentFill/mode",
value = "Cover",
)
engine.block.appendChild(parent = page, child = graphic)
```
Use `CREATED` events to start tracking a block, update local state, or trigger
follow-up work such as analytics or sync jobs.
## Updating Blocks and Handling `UPDATED` Events
Changing any property of a block triggers an `UPDATED` event. Here, the example
rotates the block by `0.1f` radians and sets its opacity to `0.9f`.
```kotlin highlight-android-event-updated
engine.block.setRotation(graphic, radians = 0.1F)
engine.block.setFloat(
block = graphic,
property = "opacity",
value = 0.9F,
)
```
The event itself does not tell you which property changed, only that the block
was updated.
## Processing Events by Type
Process each batch by switching on `event.type`. For `CREATED` and `UPDATED`
events, Block API calls are safe. For `DESTROYED` events, treat the block ID as
invalid and clean up local references without calling more Block API methods on
that ID.
```kotlin highlight-android-process-events
val processedEventsSubscription = engine.event.subscribe(blocks = emptyList())
.onEach { events ->
events.forEach { event ->
when (event.type) {
DesignBlockEvent.Type.CREATED -> {
val blockType = engine.block.getType(event.block)
println("Block created with type: $blockType")
}
DesignBlockEvent.Type.UPDATED -> {
println("Block ${event.block} was updated")
}
DesignBlockEvent.Type.DESTROYED -> {
println("Block ${event.block} was destroyed")
}
}
}
}.launchIn(this)
```
## Handling `DESTROYED` Events Safely
Once a block is destroyed, its ID becomes invalid. If your app keeps cached block
IDs, use `engine.block.isValid()` to prune stale references before passing them
to other Block API methods:
```kotlin highlight-android-destroyed-safety
val cachedBlocks = mutableSetOf(graphic)
cachedBlocks.removeAll { block -> !engine.block.isValid(block) }
```
Destroying the tracked graphic block in the example triggers a `DESTROYED` event:
```kotlin highlight-android-event-destroyed
engine.block.destroy(graphic)
println("Destroyed graphic block $graphic")
```
After a `DESTROYED` event, clean up matching cached references in your app state
instead of calling more Block API methods on that ID.
## Unsubscribing from Events
On Android, unsubscribing means cancelling the `Job`s that collect your event
flows:
```kotlin highlight-android-unsubscribe
allBlocksSubscription.cancelAndJoin()
specificBlocksSubscription.cancelAndJoin()
processedEventsSubscription.cancelAndJoin()
```
Cancel subscriptions when your screen leaves composition, the editor closes, or
you stop tracking a block. Leaving collectors active forces the engine to keep
preparing event lists on every update.
## Event Batching and Deduplication
Events are collected during an engine update and delivered together at the end.
The engine deduplicates `UPDATED` events, so you receive at most one
`UPDATED` event per block per update cycle.
This batching behavior means:
- Multiple rapid changes to a single block result in one `UPDATED` event.
- The array order does not reflect the chronological order of changes inside one update cycle.
- If you need to know which property changed, compare against cached values in your app.
## Use Cases
Events support several reactive patterns in Android apps:
- **Syncing external state**: Keep ViewModels, Redux stores, or persistence layers aligned with scene changes.
- **Building reactive UI**: Update Compose state when blocks change without polling the engine.
- **Tracking changes for undo/redo**: Monitor block changes for custom history or analytics pipelines.
- **Validating scene constraints**: React to new or updated blocks when you enforce design rules in code.
## Troubleshooting
**Events not firing**: Make sure you have not cancelled the collection job too early, and confirm the filtered block IDs are still valid.
**Exception on `DESTROYED` events**: Treat destroyed block IDs as invalid, and prune stale cached IDs with `engine.block.isValid()` before calling other Block API methods.
**Missing `UPDATED` events**: The engine deduplicates updates, so multiple rapid property changes become one `UPDATED` event per block.
**Leaking collectors**: Store the `Job` returned by `launchIn()` and cancel it during cleanup.
## Next Steps
[Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Learn about block types, properties, and lifecycle.
[Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) — Implement undo/redo functionality.
[Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Understand scene structure and management.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Font Size Unit"
description: "Configure how font sizes are interpreted (Pixel vs Point) per scene in the CE.SDK Android engine."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/font-size-unit-3b2d60/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Font Size Unit](https://img.ly/docs/cesdk/android/concepts/font-size-unit-3b2d60/)
---
```kotlin file=@cesdk_android_examples/engine-guides-concepts-font-size-unit/FontSizeUnit.kt reference-only
import ly.img.engine.DesignBlockType
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import ly.img.engine.FontUnit
import ly.img.engine.SceneLayout
import ly.img.engine.SizeMode
import kotlin.math.abs
private const val FONT_SIZE_EPSILON = 0.001F
fun fontSizeUnit(engine: Engine) {
// `scene.create(designUnit, fontSizeUnit)` lets you pair both units
// explicitly. When `fontSizeUnit` is null, CE.SDK pairs it with
// `designUnit`: `Pixel` design -> `Pixel` fonts, `Millimeter` and
// `Inch` -> `Point` fonts.
val scene = engine.scene.create(
designUnit = DesignUnit.PIXEL,
fontSizeUnit = FontUnit.POINT,
sceneLayout = SceneLayout.FREE,
)
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
val text = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = text)
engine.block.replaceText(text, text = "Font Size Unit")
engine.block.setWidthMode(text, mode = SizeMode.AUTO)
engine.block.setHeightMode(text, mode = SizeMode.AUTO)
// Read the scene's current font-size unit. This scene passes `Point`
// explicitly even though the design unit is `Pixel`.
val initialUnit = engine.scene.getFontSizeUnit()
println("Initial font-size unit: $initialUnit") // POINT
check(initialUnit == FontUnit.POINT)
// Switch the scene-wide default. Existing text keeps its visual size;
// only future `setTextFontSize` / `getTextFontSizes` calls use the new
// unit. `setDesignUnit` does not overwrite this setting, so the choice
// survives changes to the design coordinate system.
engine.scene.setFontSizeUnit(FontUnit.PIXEL)
val switchedUnit = engine.scene.getFontSizeUnit()
println("After switch: $switchedUnit") // PIXEL
check(switchedUnit == FontUnit.PIXEL)
// The value 24f is interpreted in the scene's `fontSizeUnit`, so the
// engine reads it as 24 px.
engine.block.setTextFontSize(text, fontSize = 24F)
// Font-size float properties use the same font-size unit.
engine.block.setFloat(block = text, property = "text/fontSize", value = 24F)
// `getTextFontSizes` returns values in the scene's `fontSizeUnit`,
// mirroring how `setTextFontSize` interpreted them.
val sizesInPixels = engine.block.getTextFontSizes(text)
val propertyFontSizeInPixels = engine.block.getFloat(block = text, property = "text/fontSize")
println("Sizes (px): $sizesInPixels")
println("Property size (px): $propertyFontSizeInPixels")
check(sizesInPixels.size == 1 && abs(sizesInPixels.first() - 24F) < FONT_SIZE_EPSILON)
check(abs(propertyFontSizeInPixels - 24F) < FONT_SIZE_EPSILON)
}
```
Pick the unit your scene uses for `setTextFontSize` and `getTextFontSizes`.
The engine continues to store font sizes in points internally; this setting
only changes how values are interpreted at the API boundary.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-concepts-font-size-unit)
A scene's `fontSizeUnit` is the unit `BlockApi.setTextFontSize` expects when setting a value and `BlockApi.getTextFontSizes` returns the value. CE.SDK supports two units: `FontUnit.POINT` (the typographic default) and `FontUnit.PIXEL` (to match a pixel-based design unit).
This guide covers reading and changing the scene's font-size unit, how that default flows through Android text APIs, why Android font-size calls use the scene-level font unit, and how to pair the font unit with the design unit at scene creation.
## Reading the Current Font-Size Unit
Use `engine.scene.getFontSizeUnit()` to retrieve the font unit the current scene uses for the font size APIs. In this sample, the scene passes `FontUnit.POINT` explicitly even though the design unit is `DesignUnit.PIXEL`.
When the unit-aware `engine.scene.create(designUnit, fontSizeUnit, sceneLayout)` overload receives `fontSizeUnit=null`, CE.SDK pairs the font-size unit with the design unit: `DesignUnit.PIXEL` uses `FontUnit.PIXEL`, while `DesignUnit.MILLIMETER` and `DesignUnit.INCH` use `FontUnit.POINT`. Loaded scenes saved before `fontSizeUnit` existed return `FontUnit.POINT` for compatibility.
```kotlin highlight-android-get-font-size-unit
// Read the scene's current font-size unit. This scene passes `Point`
// explicitly even though the design unit is `Pixel`.
val initialUnit = engine.scene.getFontSizeUnit()
println("Initial font-size unit: $initialUnit") // POINT
```
## Setting the Font-Size Unit
`engine.scene.setFontSizeUnit(FontUnit)` switches the scene-wide default. Existing text retains its visual size—the engine still stores values in points and converts on the way in and out. Only subsequent `setTextFontSize` and `getTextFontSizes` calls use the new unit.
```kotlin highlight-android-set-font-size-unit
// Switch the scene-wide default. Existing text keeps its visual size;
// only future `setTextFontSize` / `getTextFontSizes` calls use the new
// unit. `setDesignUnit` does not overwrite this setting, so the choice
// survives changes to the design coordinate system.
engine.scene.setFontSizeUnit(FontUnit.PIXEL)
val switchedUnit = engine.scene.getFontSizeUnit()
println("After switch: $switchedUnit") // PIXEL
```
`setDesignUnit` does not change `fontSizeUnit`, so a deliberate font-unit choice survives changes to the design coordinate system.
## Setting Font Sizes in the Scene Unit
When you call `BlockApi.setTextFontSize(block, fontSize, from, to)`, the value is interpreted in the scene's `fontSizeUnit`. The optional `from` and `to` parameters limit the write to a UTF-16 code unit range; keep the default `-1` values to target the whole text block, or the current editing selection when the block is being edited.
The same unit applies to `BlockApi.setFloat` and `BlockApi.getFloat` for `text/fontSize`, `text/minAutomaticFontSize`, `text/maxAutomaticFontSize`, `caption/fontSize`, `caption/minAutomaticFontSize`, and `caption/maxAutomaticFontSize`.
```kotlin highlight-android-implicit-set
// The value 24f is interpreted in the scene's `fontSizeUnit`, so the
// engine reads it as 24 px.
engine.block.setTextFontSize(text, fontSize = 24F)
// Font-size float properties use the same font-size unit.
engine.block.setFloat(block = text, property = "text/fontSize", value = 24F)
```
## No Per-Call Unit Override on Android
Android's `BlockApi.setTextFontSize` and `BlockApi.getTextFontSizes` APIs use the scene's `fontSizeUnit` directly. They do not expose a per-call unit option, so set the scene unit that matches your workflow before writing or reading font sizes.
If your app stores typography in another unit, convert those values in your application layer before passing them to CE.SDK, or switch the scene's `fontSizeUnit` for the workflow that owns those text edits.
## Reading Font Sizes
`BlockApi.getTextFontSizes(block, from, to)` returns values in the scene's `fontSizeUnit`. Use `from` and `to` UTF-16 code unit offsets to read a substring range; keep the default `-1` values to read the current cursor or selection while the block is being edited, or the whole text block otherwise. The same conversion applies to `BlockApi.getFloat(block, "text/fontSize")`, so reads match how Android interpreted the previous write.
```kotlin highlight-android-read-sizes
// `getTextFontSizes` returns values in the scene's `fontSizeUnit`,
// mirroring how `setTextFontSize` interpreted them.
val sizesInPixels = engine.block.getTextFontSizes(text)
val propertyFontSizeInPixels = engine.block.getFloat(block = text, property = "text/fontSize")
println("Sizes (px): $sizesInPixels")
println("Property size (px): $propertyFontSizeInPixels")
```
## Pairing Units at Scene Creation
The unit-aware `engine.scene.create(designUnit, fontSizeUnit, sceneLayout)` overload accepts both options. When `fontSizeUnit` is `null`, CE.SDK pairs it with `designUnit` (`DesignUnit.PIXEL` ⇒ `FontUnit.PIXEL`, `DesignUnit.MILLIMETER` and `DesignUnit.INCH` ⇒ `FontUnit.POINT`). Pass both explicitly when you want to mix them—for example, a Pixel design with Point-based typography.
```kotlin highlight-android-create-with-units
// `scene.create(designUnit, fontSizeUnit)` lets you pair both units
// explicitly. When `fontSizeUnit` is null, CE.SDK pairs it with
// `designUnit`: `Pixel` design -> `Pixel` fonts, `Millimeter` and
// `Inch` -> `Point` fonts.
val scene = engine.scene.create(
designUnit = DesignUnit.PIXEL,
fontSizeUnit = FontUnit.POINT,
sceneLayout = SceneLayout.FREE,
)
```
Auto-pairing only applies to the unit-aware overload above. The layout-only `engine.scene.create(sceneLayout)` overload creates a Pixel scene with `FontUnit.POINT`. `engine.scene.createForVideo()`, `engine.scene.createFromImage()`, and `engine.scene.createFromVideo()` also keep `FontUnit.POINT` for compatibility. Call `engine.scene.setFontSizeUnit(FontUnit.PIXEL)` after creation if you want font sizes to match Pixel-based coordinates.
## API Reference
| API | Purpose |
| --- | --- |
| `engine.scene.getFontSizeUnit()` | Get the current scene's font-size unit. |
| `engine.scene.setFontSizeUnit(fontSizeUnit=_)` | Set the current scene's font-size unit. |
| `engine.scene.create(designUnit=_, fontSizeUnit=null, sceneLayout=SceneLayout.FREE)` | Create a scene whose font-size unit is paired automatically with the design unit. |
| `engine.scene.create(sceneLayout=SceneLayout.FREE)` | Create a Pixel scene with the compatibility font-size unit `FontUnit.POINT`. |
| `engine.scene.createForVideo()` | Create a video scene with the compatibility font-size unit `FontUnit.POINT`. |
| `engine.scene.createFromImage(imageUri=_, dpi=300F, pixelScaleFactor=1F, sceneLayout=SceneLayout.FREE)` | Create an image scene with the compatibility font-size unit `FontUnit.POINT`. |
| `engine.scene.createFromVideo(videoUri=_)` | Create a video scene from a video URI with the compatibility font-size unit `FontUnit.POINT`. |
| `engine.block.setTextFontSize(block=_, fontSize=_, from=-1, to=-1)` | Set a text block's font size, or a UTF-16 text range, in the scene unit. |
| `engine.block.getTextFontSizes(block=_, from=-1, to=-1)` | Read a text block's font sizes, or a UTF-16 text range, in the scene unit. |
| `engine.block.setFloat(block=_, property="text/fontSize", value=_)` `engine.block.getFloat(block=_, property="text/fontSize")` | Set or read the main text font-size property in the scene unit. |
| `engine.block.setFloat(block=_, property="text/minAutomaticFontSize", value=_)` `engine.block.getFloat(block=_, property="text/minAutomaticFontSize")` | Set or read the text auto-resize minimum in the scene unit. |
| `engine.block.setFloat(block=_, property="text/maxAutomaticFontSize", value=_)` `engine.block.getFloat(block=_, property="text/maxAutomaticFontSize")` | Set or read the text auto-resize maximum in the scene unit. |
| `engine.block.setFloat(block=_, property="caption/fontSize", value=_)` `engine.block.getFloat(block=_, property="caption/fontSize")` | Set or read the caption font-size property in the scene unit. |
| `engine.block.setFloat(block=_, property="caption/minAutomaticFontSize", value=_)` `engine.block.getFloat(block=_, property="caption/minAutomaticFontSize")` | Set or read the caption auto-resize minimum in the scene unit. |
| `engine.block.setFloat(block=_, property="caption/maxAutomaticFontSize", value=_)` `engine.block.getFloat(block=_, property="caption/maxAutomaticFontSize")` | Set or read the caption auto-resize maximum in the scene unit. |
## Next Steps
- [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/) - Understand the broader unit system that determines layout coordinates and DPI.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Headless"
description: "Use the engine directly, without any prebuilt UI."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-scene-from-scratch/CreateSceneFromScratch.kt reference-only
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
fun createSceneFromScratch(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(
Dispatchers.Main,
).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Star))
engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color))
engine.block.appendChild(parent = page, child = block)
engine.stop()
}
```
Headless Mode lets you use the CreativeEditor SDK's Engine directly. No prebuilt editor UI required. You initialize the Engine, load or build scenes programmatically, and export to images/PDF/video entirely in code. This is ideal for custom UIs, automation, server-triggered rendering, or batch exports.
## What You'll Learn
- What "Headless / Engine-only" means and how it differs from the UI editor.
- When to choose Headless Mode (and when not to).
- How to initialize the Engine for headless use in Kotlin.
- How to load/build a scene and modify blocks programmatically.
- How to export (PNG/JPEG/PDF, and notes on video) without launching the UI.
## When to Use It
**Pick Headless Mode when you need:**
- A fully custom UI: You're building your own editing interface or integrating into an existing app layout.
- Programmatic rendering: Generate images/PDFs from templates or data—no user interaction.
- Automation & batch work: Merge data at scale, pre-render previews, or run background exports.
- Export-only flows: Quickly render a scene without ever opening the editor.
**Avoid Headless Mode when:**
- You want turnkey editing UX out of the box (use the standard Editor for that).
- You don't want to create selection, gestures, or tool panels yourself.
### Quick Comparison
|Scenario | Headless (Engine-only) | Standard UI Editor |
|---|---|---|
|Automate design generation from code|✅|❌|
|Export scenes without user interaction|✅|❌|
|Let users visually edit with ready-made panels|❌|✅|
|Build a custom editor interface|✅|⭘ (extend via config)|
### How Headless Mode Works
With the prebuilt editors, user actions call the **Engine** API through the UI. In Headless Mode, you start the Engine and work entirely in Kotlin to:
- Scene management: create/load scenes; add pages; read/write properties
- Blocks: create text/graphics/shapes; set fills, sizes, transforms; append to parents
- Assets: register sources, resolve URIs, add media programmatically
- Templates & data: load scene archives/JSON, update text variables, swap images
- Export: render blocks or pages to PNG/JPEG/PDF (and trigger video exports where appropriate)
### Initialize the Engine in Headless Mode (Kotlin)
```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.Engine
class HeadlessRenderer {
private lateinit var engine: Engine
fun startEngine(license: String, userId: String) = CoroutineScope(Dispatchers.Main).launch {
engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(
license = license,
userId = userId
)
// Bind offscreen for headless rendering (no UI needed)
engine.bindOffscreen(width = 1080, height = 1920)
}
}
```
This is "headless" because you never instantiate or present the editor UI. You only create an Engine and use its APIs. The `bindOffscreen` method creates an offscreen rendering surface—perfect for headless scenarios where no visible View is needed.
### Create and Export a Scene (Kotlin)
Below is a minimal, end-to-end example that works with the preceding class to:
- Create a scene with a single page.
- Add a rectangle filled with a remote image.
- Add a text block.
- Export the page as PNG data.
```kotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.nio.ByteBuffer
data class ExportResult(
val pngData: ByteBuffer,
val suggestedFilename: String
)
suspend fun HeadlessRenderer.buildAndExport(): ExportResult = withContext(Dispatchers.Main) {
// 1) Create an empty scene and a page
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
// Set page size
engine.block.setWidth(page, value = 800f)
engine.block.setHeight(page, value = 600f)
// Attach page to scene root
engine.block.appendChild(parent = scene, child = page)
// 2) Add an image rectangle
val rect = engine.block.create(DesignBlockType.Graphic)
val shape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(rect, shape = shape)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
// Use your own asset URL or registered source URI
value = "https://img.ly/static/ubq_samples/sample_1.jpg"
)
engine.block.setFill(rect, fill = imageFill)
// Position & size the rect
engine.block.setPositionX(rect, value = 100f)
engine.block.setPositionY(rect, value = 100f)
engine.block.setWidth(rect, value = 400f)
engine.block.setHeight(rect, value = 300f)
engine.block.appendChild(parent = page, child = rect)
// 3) Add text
val text = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(text, text = "Hello, From Headless Mode!")
engine.block.setPositionX(text, value = 100f)
engine.block.setPositionY(text, value = 450f)
engine.block.setWidthMode(text, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = text)
// 4) Export the page to PNG
val pngData = engine.block.export(page, mimeType = MimeType.PNG)
ExportResult(pngData = pngData, suggestedFilename = "headless-output.png")
}
```
### Saving the File (optional)
After creating the image, you may want to save it. Here is a minimal code example to save the file to the app's files directory and return the file path.
```kotlin
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
suspend fun savePNG(context: Context, result: ExportResult): File = withContext(Dispatchers.IO) {
val outputDir = context.filesDir
val destinationFile = File(outputDir, result.suggestedFilename)
destinationFile.outputStream().channel.use { channel ->
channel.write(result.pngData)
}
destinationFile
}
```

The example code creates this `.png` image.
Some variations to the preceding code depending on your workflow might be:
- Export as different file format such as `.jpeg` or `.pdf` by changing the `mimeType` argument.
- Export a sub-tree instead of the entire page. Passing a block's ID exports just that block and its children.
### Working with Assets (Headless)
- Default sources: registering the default asset sources with `engine.asset.addLocalSourceFromJSON(...)` wires up built-in sources so URIs like `"fill/image/imageFileURI"` can resolve to remote/local assets.
- Your own sources: In production, you'll typically register a custom source (from your server, local storage, or device gallery) and then set block properties to URIs your source resolves.
- Local files: Use `file://` URLs or Android content URIs that your asset source understands.
- Fonts: Bundle or register your fonts the same way. The Engine needs them available for text layout before export.
### Templates & Data-Driven Generation
Headless Mode pairs well with:
- Scene templates (ZIP/JSON): load and then swap images or set text (update `text/text` or variable placeholders).
- Text variables / placeholders: bind your app's data model to text fields and render many variants in a loop.
- Batching: loop through data rows → set properties → export → repeat.
> **Note:** Keep content keys ("text/text", "fill/image/imageFileURI", etc.) stable across templates or use helper methods, so your code doesn't change when designers iterate.
## Troubleshooting
**❌ I'm getting nothing on export (empty data or errors)**:
- Verify you export a renderable block (the page or scene root's child).
- Ensure remote image URIs are reachable and network permissions are set in AndroidManifest.xml.
- For debugging, try a known good URL, then swap.
**❌ Images don't load or are missing in output**:
- Confirm your asset source can resolve the URI you set.
- If using remote URLs, ensure INTERNET permission is declared in AndroidManifest.xml.
- For custom domains, verify the app's network security configuration and TLS compatibility, then check redirects and URL reachability from the device or emulator.
**❌ Text looks wrong or uses fallback fonts**:
- The Engine needs the exact font used by the text style. Register/bundle the font and ensure it's discoverable before export.
**❌ Memory spikes on big batches**:
- Reuse a single Engine instance when possible.
- Export, write to disk, and release large buffers before rendering the next item.
- Call `engine.stop()` when completely done to free resources.
## Next Steps
Now that you understand the basics of headless mode, below are some topics to help you expand your knowledge:
- [Templates & Variables](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) – Design tokenized templates and drive them from data.
- [Exporting](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) – PNG/JPEG/PDF exports, plus format options and best practices.
- [Standard Editor vs Headless](https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/) – If you need turnkey UI, start here and decide whether to drop to headless for specific flows.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Pages"
description: "Structure Android scenes with consistent pages, shared dimensions, and page-level properties in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/)
---
```kotlin file=@cesdk_android_examples/engine-guides-concepts-pages/Pages.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.SceneLayout
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
suspend fun pages(engine: Engine): PagesGuideSummary = withContext(engine.dispatcher) {
// Create a scene with VerticalStack layout for multi-page designs.
val scene = engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK)
val stack = engine.block.findByType(DesignBlockType.Stack).first()
engine.block.setFloat(block = stack, property = "stack/spacing", value = 20F)
engine.block.setBoolean(
block = stack,
property = "stack/spacingInScreenspace",
value = true,
)
// Set page dimensions at the scene level so new pages share the same size.
engine.block.setFloat(
block = scene,
property = "scene/pageDimensions/width",
value = 800F,
)
engine.block.setFloat(
block = scene,
property = "scene/pageDimensions/height",
value = 600F,
)
val firstPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = firstPage, value = 800F)
engine.block.setHeight(block = firstPage, value = 600F)
engine.block.appendChild(parent = stack, child = firstPage)
val secondPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = secondPage, value = 800F)
engine.block.setHeight(block = secondPage, value = 600F)
engine.block.appendChild(parent = stack, child = secondPage)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.appendChild(parent = firstPage, child = imageBlock)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = imageBlock, shape = rectShape)
engine.block.setWidth(block = imageBlock, value = 400F)
engine.block.setHeight(block = imageBlock, value = 300F)
engine.block.setPositionX(block = imageBlock, value = 200F)
engine.block.setPositionY(block = imageBlock, value = 150F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(block = imageBlock, fill = imageFill)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = secondPage, child = textBlock)
engine.block.replaceText(textBlock, text = "Page 2")
engine.block.setTextFontSize(block = textBlock, fontSize = 48F)
engine.block.setTextColor(
block = textBlock,
color = Color.fromRGBA(r = 0.2F, g = 0.2F, b = 0.2F, a = 1F),
)
engine.block.setWidthMode(block = textBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = textBlock, mode = SizeMode.AUTO)
val textWidth = engine.block.getFrameWidth(textBlock)
val textHeight = engine.block.getFrameHeight(textBlock)
engine.block.setPositionX(block = textBlock, value = (800F - textWidth) / 2F)
engine.block.setPositionY(block = textBlock, value = (600F - textHeight) / 2F)
engine.block.setBoolean(
block = firstPage,
property = "page/marginEnabled",
value = true,
)
engine.block.setFloat(block = firstPage, property = "page/margin/top", value = 10F)
engine.block.setFloat(block = firstPage, property = "page/margin/bottom", value = 10F)
engine.block.setFloat(block = firstPage, property = "page/margin/left", value = 10F)
engine.block.setFloat(block = firstPage, property = "page/margin/right", value = 10F)
engine.block.setString(
block = firstPage,
property = "page/titleTemplate",
value = "Cover",
)
engine.block.setString(
block = secondPage,
property = "page/titleTemplate",
value = "Content",
)
engine.block.setFillSolidColor(
block = secondPage,
color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 1F, a = 1F),
)
val allPages = engine.scene.getPages()
val currentPage = engine.scene.getCurrentPage()
val pagesByType = engine.block.findByType(DesignBlockType.Page)
val nearestPages = engine.scene.findNearestToViewPortCenterByType(DesignBlockType.Page)
engine.block.forceLoadResources(listOf(imageBlock, textBlock))
PagesGuideSummary(
pageCount = allPages.size,
currentPageTitle = currentPage?.let {
engine.block.getString(block = it, property = "page/titleTemplate")
},
pageTitles = pagesByType.map {
engine.block.getString(block = it, property = "page/titleTemplate")
},
nearestPageCount = nearestPages.size,
)
}
```
Pages define the format of your designs. Every graphic block, text element, and media asset lives inside a page. This guide shows how pages fit into the Android scene hierarchy, how stacked layouts keep page sizes aligned, and which page-level properties you can configure in Kotlin.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-concepts-pages)
Pages provide the canvas and frame for your designs. Whether you're building a multi-page document, a carousel, or a video composition, understanding how pages work helps you structure content correctly.
This guide covers:
- Understanding the scene hierarchy: Scene → Pages → Blocks
- Creating and managing multiple pages
- Setting shared page dimensions at the scene level
- Configuring margins, title templates, and page backgrounds
- Finding pages programmatically
## Pages in the Scene Hierarchy
In CE.SDK, content follows a strict hierarchy: a **scene** contains **pages**, and pages contain **content blocks**. Only blocks attached to a page are rendered on the canvas.
```kotlin highlight-android-pages-create-scene
// Create a scene with VerticalStack layout for multi-page designs.
val scene = engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK)
val stack = engine.block.findByType(DesignBlockType.Stack).first()
engine.block.setFloat(block = stack, property = "stack/spacing", value = 20F)
engine.block.setBoolean(
block = stack,
property = "stack/spacingInScreenspace",
value = true,
)
```
When you create a scene with `SceneLayout.VERTICAL_STACK`, CE.SDK inserts a stack container that arranges pages automatically. Configure the stack before adding pages if you need gaps between them.
```kotlin highlight-android-pages-create-pages
val firstPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = firstPage, value = 800F)
engine.block.setHeight(block = firstPage, value = 600F)
engine.block.appendChild(parent = stack, child = firstPage)
val secondPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = secondPage, value = 800F)
engine.block.setHeight(block = secondPage, value = 600F)
engine.block.appendChild(parent = stack, child = secondPage)
```
Create pages with `engine.block.create(DesignBlockType.Page)`. In stacked layouts, append pages to the stack container; for single-page or free layouts, you can append a page directly to the scene. Stacked pages should use the same dimensions to avoid normalization when the editor loads the scene.
```kotlin highlight-android-pages-add-content
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.appendChild(parent = firstPage, child = imageBlock)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = imageBlock, shape = rectShape)
engine.block.setWidth(block = imageBlock, value = 400F)
engine.block.setHeight(block = imageBlock, value = 300F)
engine.block.setPositionX(block = imageBlock, value = 200F)
engine.block.setPositionY(block = imageBlock, value = 150F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(block = imageBlock, fill = imageFill)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = secondPage, child = textBlock)
engine.block.replaceText(textBlock, text = "Page 2")
engine.block.setTextFontSize(block = textBlock, fontSize = 48F)
engine.block.setTextColor(
block = textBlock,
color = Color.fromRGBA(r = 0.2F, g = 0.2F, b = 0.2F, a = 1F),
)
engine.block.setWidthMode(block = textBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = textBlock, mode = SizeMode.AUTO)
val textWidth = engine.block.getFrameWidth(textBlock)
val textHeight = engine.block.getFrameHeight(textBlock)
engine.block.setPositionX(block = textBlock, value = (800F - textWidth) / 2F)
engine.block.setPositionY(block = textBlock, value = (600F - textHeight) / 2F)
```
Content blocks must be appended to a page before they render. In this example, the first page shows an image block with a rectangular image fill, and the second page centers a text block with auto-sized width and height.
## Page Dimensions and Consistency
The Creative Engine can store pages with different dimensions, but the editor UI is designed around consistent page sizes for stacked layouts such as `VERTICAL_STACK` and `HORIZONTAL_STACK`. If you load a stacked scene with mixed page sizes, the editor may normalize the scene to keep the layout predictable.
```kotlin highlight-android-pages-set-dimensions
// Set page dimensions at the scene level so new pages share the same size.
engine.block.setFloat(
block = scene,
property = "scene/pageDimensions/width",
value = 800F,
)
engine.block.setFloat(
block = scene,
property = "scene/pageDimensions/height",
value = 600F,
)
```
Use the scene-level properties `scene/pageDimensions/width` and `scene/pageDimensions/height` to define the shared default size for pages. The `scene/aspectRatioLock` property controls whether width and height stay linked when you change one dimension.
Individual pages can also be sized directly with `engine.block.setWidth()` and `engine.block.setHeight()`. Keep the scene-level dimensions as the shared default for stacked layouts, and use `SceneLayout.FREE` when different page sizes are intentional.
## Finding and Navigating Pages
CE.SDK provides several ways to inspect the pages in the current scene.
```kotlin highlight-android-pages-find-pages
val allPages = engine.scene.getPages()
val currentPage = engine.scene.getCurrentPage()
val pagesByType = engine.block.findByType(DesignBlockType.Page)
val nearestPages = engine.scene.findNearestToViewPortCenterByType(DesignBlockType.Page)
```
Use these APIs based on your workflow:
- `engine.scene.getPages()` returns all pages in scene order.
- `engine.scene.getCurrentPage()` returns the selected page or the page nearest to the viewport center.
- `engine.block.findByType(DesignBlockType.Page)` finds every page block regardless of selection state.
- `engine.scene.findNearestToViewPortCenterByType(DesignBlockType.Page)` sorts pages by distance to the viewport center.
## Page Properties
Page-specific properties live on the page block itself, not on the scene.
### Margins
Page margins are useful for print and bleed-safe layouts. Enable margins once, then set each side individually.
```kotlin highlight-android-pages-page-margins
engine.block.setBoolean(
block = firstPage,
property = "page/marginEnabled",
value = true,
)
engine.block.setFloat(block = firstPage, property = "page/margin/top", value = 10F)
engine.block.setFloat(block = firstPage, property = "page/margin/bottom", value = 10F)
engine.block.setFloat(block = firstPage, property = "page/margin/left", value = 10F)
engine.block.setFloat(block = firstPage, property = "page/margin/right", value = 10F)
```
Set `page/marginEnabled` to `true`, then adjust `page/margin/top`, `page/margin/bottom`, `page/margin/left`, and `page/margin/right` in design units.
### Title Template
The `page/titleTemplate` property controls the label shown for a page. It supports template tokens such as `{{ubq.page_index}}` for numbered labels.
```kotlin highlight-android-pages-title-template
engine.block.setString(
block = firstPage,
property = "page/titleTemplate",
value = "Cover",
)
engine.block.setString(
block = secondPage,
property = "page/titleTemplate",
value = "Content",
)
```
The default template is `"Page {{ubq.page_index}}"`. Override it when you want labels such as `"Cover"` or `"Content"`.
### Fill and Background
Pages support fills through the standard fill API. This example applies a solid background color to a page.
```kotlin highlight-android-pages-page-background
engine.block.setFillSolidColor(
block = secondPage,
color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 1F, a = 1F),
)
```
Use `engine.block.setFillSolidColor(page, color)` for a solid page background.
## Page Layout Modes
The scene layout controls how multiple pages are arranged. Use `engine.scene.create(sceneLayout = ...)` when you create the scene or `engine.scene.setLayout(...)` later.
| Layout | Description |
| ------ | ----------- |
| `SceneLayout.VERTICAL_STACK` | Pages stack vertically from top to bottom. |
| `SceneLayout.HORIZONTAL_STACK` | Pages arrange side by side from left to right. |
| `SceneLayout.DEPTH_STACK` | Pages overlap in depth order, which is common for video scenes. |
| `SceneLayout.FREE` | Pages can be positioned freely and may use different dimensions. |
## Pages for Static Designs vs. Video Editing
Pages behave differently depending on the scene mode you choose.
### Static Designs
For static design scenes, pages act like artboards. Each page is a separate canvas for documents, carousels, social posts, or print layouts, and pages stay spatially arranged according to the scene layout.
### Video Editing
For video scenes, pages represent time-based compositions that play one after another. Page-level playback properties control timeline behavior:
- `playback/duration` determines how long each page is shown.
- `playback/time` tracks the current playback position.
## Troubleshooting
### Content Not Visible
If a block is not visible, check these common causes:
- Verify the block is attached to a page with `engine.block.appendChild(parent = page, child = block)`.
- For graphic blocks, ensure both a shape and a fill are set.
- Append blocks to the page before setting their size and position.
### Dimension Inconsistencies
If stacked pages show unexpected sizes in the editor, set the shared size on the scene first and keep page dimensions aligned. Use `SceneLayout.FREE` only when different page sizes are intentional.
### Page Not Found
If `engine.scene.getPages()` returns an empty list, make sure a scene exists and that you appended at least one page. In headless workflows, you must create both the scene and the pages yourself.
## API Reference
| API | Purpose |
| --- | ------- |
| `engine.scene.create(sceneLayout=SceneLayout.VERTICAL_STACK)` | Create a multi-page scene with automatic page stacking. |
| `engine.scene.setLayout(layout=SceneLayout.HORIZONTAL_STACK)` | Change how existing pages are arranged. |
| `engine.scene.getPages()` | Return all pages in scene order. |
| `engine.scene.getCurrentPage()` | Return the selected or nearest visible page. |
| `engine.scene.findNearestToViewPortCenterByType(type=DesignBlockType.Page)` | Sort pages by distance to the viewport center. |
| `engine.block.findByType(type=DesignBlockType.Page)` | Find all page blocks in the scene. |
| `engine.block.setFloat(block=_, property="scene/pageDimensions/width", value=_)` | Set the shared page width. |
| `engine.block.setFloat(block=_, property="scene/pageDimensions/height", value=_)` | Set the shared page height. |
| `engine.block.setString(block=_, property="page/titleTemplate", value=_)` | Override the displayed page label. |
| `engine.block.setFillSolidColor(block=_, color=Color.fromRGBA(r=_, g=_, b=_, a=_))` | Apply a solid background color to a page. |
## Next Steps
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Learn about scene structure and management
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Understand the building blocks that live inside pages
- [Page Format](https://img.ly/docs/cesdk/android/user-interface/customization/page-format-496315/) — Configure default page sizes in the UI
- [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/) — Define layout in px, mm, or in—CE.SDK supports unit conversion and DPI scaling for consistent design.
- [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) — Templates enable dynamic, reusable designs with text variables and placeholder media. Learn to create, load, and personalize templates programmatically.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Working With Resources"
description: "Preload resources, find transient data, detect MIME types, and relocate URLs in CE.SDK for Android."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/resources-a58d71/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/)
---
```kotlin file=@cesdk_android_examples/engine-guides-resources/Resources.kt reference-only
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import kotlinx.coroutines.withContext
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import java.io.OutputStream
import java.nio.ByteBuffer
suspend fun resources(engine: Engine): List = withContext(engine.dispatcher) {
val logLines = mutableListOf()
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 680F)
engine.block.setHeight(page, value = 260F)
engine.block.setDuration(page, duration = 1.0)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 30F)
engine.block.setPositionY(imageBlock, value = 30F)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 200F)
val imageUri = Uri.parse(
"https://img.ly/static/ubq_samples/sample_4.jpg?cesdk-resources-guide=source",
)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = page, child = imageBlock)
val videoBlock = engine.block.create(DesignBlockType.Graphic)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(videoBlock, value = 350F)
engine.block.setPositionY(videoBlock, value = 30F)
engine.block.setWidth(videoBlock, value = 300F)
engine.block.setHeight(videoBlock, value = 200F)
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(block = videoBlock, fill = videoFill)
engine.block.setContentFillMode(block = videoBlock, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = page, child = videoBlock)
engine.block.forceLoadAVResource(block = videoFill)
val duration = engine.block.getAVResourceTotalDuration(block = videoFill)
val videoWidth = engine.block.getVideoWidth(videoFill = videoFill)
val videoHeight = engine.block.getVideoHeight(videoFill = videoFill)
val transientBitmap =
Bitmap
.createBitmap(32, 32, Bitmap.Config.ARGB_8888)
.apply {
eraseColor(Color.rgb(255, 196, 0))
}
val transientImageBuffer =
DirectByteBufferOutputStream().use { output ->
check(transientBitmap.compress(Bitmap.CompressFormat.PNG, 100, output))
output.toByteBuffer()
}
transientBitmap.recycle()
val transientImageSize = transientImageBuffer.remaining()
val transientImageBufferUri = engine.editor.createBuffer()
var transientImageBlockToDestroy: DesignBlock? = null
var transientImageFillToDestroy: DesignBlock? = null
var transientImageRelocated = false
try {
engine.editor.setBufferData(
uri = transientImageBufferUri,
offset = 0,
data = transientImageBuffer,
)
check(engine.editor.getBufferLength(uri = transientImageBufferUri) == transientImageSize)
val transientImageBlock = engine.block.create(DesignBlockType.Graphic)
transientImageBlockToDestroy = transientImageBlock
val transientImageFill = engine.block.createFill(FillType.Image)
transientImageFillToDestroy = transientImageFill
engine.block.setShape(transientImageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(transientImageBlock, value = 600F)
engine.block.setPositionY(transientImageBlock, value = 20F)
engine.block.setWidth(transientImageBlock, value = 48F)
engine.block.setHeight(transientImageBlock, value = 48F)
engine.block.setUri(
block = transientImageFill,
property = "fill/image/imageFileURI",
value = transientImageBufferUri,
)
engine.block.setFill(block = transientImageBlock, fill = transientImageFill)
engine.block.appendChild(parent = page, child = transientImageBlock)
// Preload every resource referenced by the scene and its children.
engine.block.forceLoadResources(blocks = listOf(scene))
// Or preload only guide-owned blocks whose resources are needed next.
val graphics = listOf(imageBlock, videoBlock, transientImageBlock)
engine.block.forceLoadResources(blocks = graphics)
// No preload call is required; this inspects resource references that cannot be serialized.
val transientResources = engine.editor.findAllTransientResources()
.filter { (uri, _) -> uri == transientImageBufferUri }
val mediaUris = engine.editor.findAllMediaURIs()
val persistentMediaUris = mediaUris.filter { it.scheme in listOf("http", "https", "file") }
val unusedBlock = engine.block.create(DesignBlockType.Graphic)
val unusedBlocks = engine.block.findAllUnused()
check(unusedBlock in unusedBlocks)
engine.block.destroy(unusedBlock)
val mimeType = engine.editor.getMimeType(uri = imageUri)
val relocatedImageUri = Uri.parse(
"https://img.ly/static/ubq_samples/sample_1.jpg?cesdk-resources-guide=relocated",
)
engine.editor.relocateResource(
currentUri = imageUri,
relocatedUri = relocatedImageUri,
)
val relocatedResources =
transientResources.map { (transientUri, resourceSize) ->
val resourceData = ByteBuffer.allocateDirect(resourceSize)
engine.editor.getResourceData(
uri = transientUri,
chunkSize = 64 * 1024,
) { chunk ->
resourceData.put(chunk.asReadOnlyBuffer())
true
}
resourceData.flip()
val permanentUri =
uploadTransientResourceToPermanentStorage(
sourceUri = transientUri,
data = resourceData.asReadOnlyBuffer(),
)
engine.editor.relocateResource(
currentUri = transientUri,
relocatedUri = permanentUri,
)
transientImageRelocated = true
transientUri to permanentUri
}
val remainingTransientResources = engine.editor.findAllTransientResources()
.filter { (uri, _) -> uri == transientImageBufferUri }
val sceneString =
engine.scene.saveToString(
scene = scene,
allowedResourceSchemes = listOf("http", "https"),
)
logLines +=
"Created an image block for $imageUri. The resource loads on-demand when rendered or exported."
logLines += "Preloaded resources for the scene and ${graphics.size} guide-owned graphic blocks."
logLines += "Video metadata: ${duration}s, ${videoWidth}x$videoHeight."
transientResources.forEach { (uri, size) ->
logLines += "Transient resource: $uri ($size bytes)."
}
mediaUris.forEach { uri ->
logLines += "Media URI: $uri"
}
logLines += "Persistent media URI count: ${persistentMediaUris.size}."
logLines += "Found ${unusedBlocks.size} unused blocks and destroyed the guide-owned block."
logLines += "MIME type for $imageUri: $mimeType"
relocatedResources.forEach { (transientUri, permanentUri) ->
logLines += "Relocated $transientUri to $permanentUri."
}
logLines += "Relocated image resource to $relocatedImageUri."
logLines += "Transient resources after relocation: ${remainingTransientResources.size}."
logLines += "Saved scene string (${sceneString.length} characters)."
logLines
} finally {
if (!transientImageRelocated) {
transientImageBlockToDestroy
?.takeIf(engine.block::isValid)
?.let(engine.block::destroy)
transientImageFillToDestroy
?.takeIf(engine.block::isValid)
?.let(engine.block::destroy)
}
engine.editor.destroyBuffer(uri = transientImageBufferUri)
}
}
private suspend fun uploadTransientResourceToPermanentStorage(
sourceUri: Uri,
data: ByteBuffer,
): Uri {
check(data.hasRemaining()) { "Cannot upload an empty resource." }
// Upload the bytes with your app's storage client here, then return its permanent URI.
// This sample only creates a placeholder URL so the guide can focus on the CE.SDK flow.
val fileName =
sourceUri
.lastPathSegment
?.takeIf { it.isNotBlank() }
?: "transient-resource-${data.remaining()}"
return Uri.parse("https://your-storage.example/uploads/$fileName")
}
private class DirectByteBufferOutputStream(
initialCapacity: Int = 4 * 1024,
) : OutputStream() {
private var buffer = ByteBuffer.allocateDirect(initialCapacity)
override fun write(value: Int) {
ensureCapacity(1)
buffer.put(value.toByte())
}
fun toByteBuffer(): ByteBuffer {
val data = buffer.asReadOnlyBuffer()
data.flip()
return data.slice().asReadOnlyBuffer()
}
private fun ensureCapacity(additionalBytes: Int) {
if (buffer.remaining() >= additionalBytes) return
val expanded = ByteBuffer.allocateDirect(
maxOf(buffer.capacity() * 2, buffer.position() + additionalBytes),
)
buffer.flip()
expanded.put(buffer)
buffer = expanded
}
}
```
Manage external media files—images, videos, audio, and fonts—that blocks
reference via URIs in CE.SDK.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-resources)
Resources are external media files that blocks reference through URI properties like `fill/image/imageFileURI` or `fill/video/fileURI`. CE.SDK loads resources automatically when needed, but you can preload them for better performance. When working with transient resources whose data would be lost during serialization, upload their data and relocate them to permanent URLs before saving. If resource URLs change, you can update the mappings without modifying scene data.
This guide covers on-demand and preloaded resource loading, identifying transient resources, relocating them to permanent URLs before serialization, and discovering all media URIs in a scene.
| Method | Category | Purpose |
| --- | --- | --- |
| `engine.block.forceLoadResources(blocks=_)` | Preloading | Load resources for blocks and their children |
| `engine.block.forceLoadAVResource(block=_)` | Preloading | Load audio or video resource data for a block |
| `engine.block.getAVResourceTotalDuration(block=_)` | Properties | Get the duration of an audio or video resource |
| `engine.block.getVideoWidth(videoFill=_)` | Properties | Get the width of a loaded video resource |
| `engine.block.getVideoHeight(videoFill=_)` | Properties | Get the height of a loaded video resource |
| `engine.editor.findAllTransientResources()` | Discovery | Find resources whose data would be lost during serialization |
| `engine.editor.getResourceData(uri=_, chunkSize=_, onData=_)` | Discovery | Read resource bytes in chunks before uploading |
| `engine.editor.findAllMediaURIs()` | Discovery | List serializable media URIs referenced in the scene |
| `engine.block.findAllUnused()` | Cleanup | Find detached blocks before relocating or destroying resources |
| `engine.editor.getMimeType(uri=_)` | Discovery | Detect the MIME type of a resource |
| `engine.editor.relocateResource(currentUri=_, relocatedUri=_)` | Management | Update URI mappings after assets move |
| `engine.scene.saveToString(scene=_, allowedResourceSchemes=_)` | Serialization | Save the scene after transient resources are relocated |
## On-Demand Loading
The engine fetches resources automatically when rendering blocks or preparing exports. This approach requires no extra code but may delay the first render while assets download.
```kotlin highlight-android-on-demand-loading
val imageBlock = engine.block.create(DesignBlockType.Graphic)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 30F)
engine.block.setPositionY(imageBlock, value = 30F)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 200F)
val imageUri = Uri.parse(
"https://img.ly/static/ubq_samples/sample_4.jpg?cesdk-resources-guide=source",
)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = page, child = imageBlock)
```
When you create a graphic block with an image fill, the engine downloads that image only when the block is needed for rendering or export.
## Preloading Resources
Load resources before they are needed with `forceLoadResources()`. Pass the scene to preload everything in that scene, or pass a smaller set of blocks to control the load order. An empty list loads every resource currently known to the engine, so reserve that form for workflows that intentionally own the engine's complete resource set.
```kotlin highlight-android-preload-resources
// Preload every resource referenced by the scene and its children.
engine.block.forceLoadResources(blocks = listOf(scene))
// Or preload only guide-owned blocks whose resources are needed next.
val graphics = listOf(imageBlock, videoBlock, transientImageBlock)
engine.block.forceLoadResources(blocks = graphics)
```
Use this when you want a scene fully ready before showing it to users or before starting an export workflow.
## Preloading Audio and Video
Audio and video resources require `forceLoadAVResource()` for full metadata access. The engine needs to download and parse the media file before you query properties like duration or dimensions.
```kotlin highlight-android-preload-av
val videoBlock = engine.block.create(DesignBlockType.Graphic)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(videoBlock, value = 350F)
engine.block.setPositionY(videoBlock, value = 30F)
engine.block.setWidth(videoBlock, value = 300F)
engine.block.setHeight(videoBlock, value = 200F)
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(block = videoBlock, fill = videoFill)
engine.block.setContentFillMode(block = videoBlock, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = page, child = videoBlock)
engine.block.forceLoadAVResource(block = videoFill)
val duration = engine.block.getAVResourceTotalDuration(block = videoFill)
val videoWidth = engine.block.getVideoWidth(videoFill = videoFill)
val videoHeight = engine.block.getVideoHeight(videoFill = videoFill)
```
Without preloading, methods like `getAVResourceTotalDuration()`, `getVideoWidth()`, and `getVideoHeight()` may return zero or incomplete values.
## Finding Transient Resources
Transient resources are scene resources whose data would be lost during scene serialization. Use `findAllTransientResources()` to discover them before saving.
```kotlin highlight-android-find-transient
// No preload call is required; this inspects resource references that cannot be serialized.
val transientResources = engine.editor.findAllTransientResources()
.filter { (uri, _) -> uri == transientImageBufferUri }
```
Each pair contains the resource `Uri` and its size in bytes. In this example, a generated image fill uses a buffer URI, so the scene reports a transient resource that must be uploaded or otherwise persisted.
## Finding Media URIs
Get all serializable media URIs referenced in the scene with `findAllMediaURIs()`. This returns a deduplicated list of valid `http://`, `https://`, and `file://` media URIs from image, video, audio, and other media sources.
```kotlin highlight-android-find-media-uris
val mediaUris = engine.editor.findAllMediaURIs()
val persistentMediaUris = mediaUris.filter { it.scheme in listOf("http", "https", "file") }
```
Transient buffer resources are intentionally excluded, which makes this API useful for building a manifest of assets that already exist in persistent storage.
## Finding Unused Blocks
Once a scene has gone through several edits, it can accumulate blocks that are no longer attached to any scene. These dangling blocks still hold references to images, videos, and audio resources. Use `findAllUnused` to enumerate them so you can free the memory or skip relocating their resources.
```kotlin highlight-android-find-unused-blocks
val unusedBlock = engine.block.create(DesignBlockType.Graphic)
val unusedBlocks = engine.block.findAllUnused()
check(unusedBlock in unusedBlocks)
engine.block.destroy(unusedBlock)
```
Pair this with `findAllMediaURIs()` to skip relocating resources for blocks that are no longer reachable. Review ownership before calling `destroy()`: the sample destroys only the detached block it created, rather than every unused block reported by a potentially shared engine.
## Detecting MIME Types
Determine a resource's content type with `getMimeType()`. The engine downloads the resource if it is not already cached.
```kotlin highlight-android-detect-mime-type
val mimeType = engine.editor.getMimeType(uri = imageUri)
```
Common return values include `image/jpeg`, `image/png`, `video/mp4`, and `audio/mpeg`.
## Relocating Resources
Update URL mappings when resources move with `relocateResource()`. This changes the URI associated with a resource so the scene can keep working after you upload data to a CDN or migrate assets between storage locations.
```kotlin highlight-android-relocate
val relocatedImageUri = Uri.parse(
"https://img.ly/static/ubq_samples/sample_1.jpg?cesdk-resources-guide=relocated",
)
engine.editor.relocateResource(
currentUri = imageUri,
relocatedUri = relocatedImageUri,
)
```
Relocation lets you keep working with the existing scene graph while switching resource access over to permanent URLs. It updates every reference to the current URI in the engine, so use a URI owned by the workflow when the engine is shared.
## Persisting Transient Resources
Android exposes `saveToString()` with `allowedResourceSchemes`. Persist transient resources by reading their bytes with `getResourceData()`, uploading those bytes, calling `relocateResource()` with the returned permanent URI, and then serializing the scene with only the schemes you want to allow.
```kotlin highlight-android-persist-transient
val relocatedResources =
transientResources.map { (transientUri, resourceSize) ->
val resourceData = ByteBuffer.allocateDirect(resourceSize)
engine.editor.getResourceData(
uri = transientUri,
chunkSize = 64 * 1024,
) { chunk ->
resourceData.put(chunk.asReadOnlyBuffer())
true
}
resourceData.flip()
val permanentUri =
uploadTransientResourceToPermanentStorage(
sourceUri = transientUri,
data = resourceData.asReadOnlyBuffer(),
)
engine.editor.relocateResource(
currentUri = transientUri,
relocatedUri = permanentUri,
)
transientImageRelocated = true
transientUri to permanentUri
}
val remainingTransientResources = engine.editor.findAllTransientResources()
.filter { (uri, _) -> uri == transientImageBufferUri }
val sceneString =
engine.scene.saveToString(
scene = scene,
allowedResourceSchemes = listOf("http", "https"),
)
```
The sample upload helper stands in for your app's storage client and must return the URI of the uploaded bytes.
```kotlin highlight-android-upload-helper
private suspend fun uploadTransientResourceToPermanentStorage(
sourceUri: Uri,
data: ByteBuffer,
): Uri {
check(data.hasRemaining()) { "Cannot upload an empty resource." }
// Upload the bytes with your app's storage client here, then return its permanent URI.
// This sample only creates a placeholder URL so the guide can focus on the CE.SDK flow.
val fileName =
sourceUri
.lastPathSegment
?.takeIf { it.isNotBlank() }
?: "transient-resource-${data.remaining()}"
return Uri.parse("https://your-storage.example/uploads/$fileName")
}
```
If any transient URI remains in the scene, `saveToString()` throws because the serialized scene would reference data that cannot be restored later.
## Troubleshooting
- **Slow initial render**: Preload resources with `forceLoadResources()` before showing the scene or starting an export.
- **Video metadata returns `0`**: Load the video resource with `forceLoadAVResource()` before querying duration or dimensions.
- **Unexpected transient resources**: Call `findAllTransientResources()` after paste, capture, or buffer workflows to see what still needs persistence.
- **`saveToString()` fails**: Relocate every transient URI to a supported scheme such as `https` before serializing the scene.
## Next Steps
- [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) — Work with in-memory data
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Understand scene serialization and persistence
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) — Explore export options, supported formats, and configuration features for sharing or rendering output.
- [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) — Learn how assets provide external content to CE.SDK designs and how asset sources make them available programmatically.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Scenes"
description: "Create, configure, save, and load scenes—the root container for all design elements in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/)
---
Scenes are the root container for all designs in CE.SDK. They hold pages,
blocks, and the camera that controls what you see in the canvas, and the
engine manages only one active scene at a time.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-modifying-scenes)
Every design you create starts with a scene. Scenes contain pages, and pages contain the visible design elements such as text, images, shapes, and other blocks. Understanding how scenes work is essential for building, saving, and restoring user designs.
```kotlin file=@cesdk_android_examples/engine-guides-modifying-scenes/ModifyingScenes.kt reference-only
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.SceneLayout
import ly.img.engine.ShapeType
import ly.img.engine.ZoomAutoFitAxis
suspend fun modifyingScenes(engine: Engine) = withContext(engine.dispatcher) {
val scene = engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK)
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
val shape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block, shape = shape)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(block, fill = fill)
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 200F)
engine.block.appendChild(parent = page, child = block)
val designUnit = engine.scene.getDesignUnit()
println("Design unit: $designUnit")
engine.scene.setDesignUnit(DesignUnit.MILLIMETER)
engine.scene.setLayout(SceneLayout.HORIZONTAL_STACK)
val layout = engine.scene.getLayout()
println("Layout: $layout")
val pages = engine.scene.getPages()
println("Number of pages: ${pages.size}")
val currentPage = engine.scene.getCurrentPage()
println("Current page: $currentPage")
engine.scene.zoomToBlock(
block = page,
paddingLeft = 20F,
paddingTop = 20F,
paddingRight = 20F,
paddingBottom = 20F,
)
val zoomLevel = engine.scene.getZoomLevel()
println("Zoom level: $zoomLevel")
engine.scene.setZoomLevel(1F)
engine.scene.enableZoomAutoFit(
block = page,
axis = ZoomAutoFitAxis.BOTH,
paddingLeft = 20F,
paddingTop = 20F,
paddingRight = 20F,
paddingBottom = 20F,
)
println("Auto-fit enabled: ${engine.scene.isZoomAutoFitEnabled(page)}")
engine.scene.disableZoomAutoFit(page)
val savedScene = engine.scene.saveToString(scene = scene)
println("Scene saved, length: ${savedScene.length}")
val loadedScene = engine.scene.load(scene = savedScene)
println("Scene loaded: $loadedScene")
val zoomEvents = engine.scene.onZoomLevelChanged()
.onEach {
println("Zoom changed: ${engine.scene.getZoomLevel()}")
}
.launchIn(this)
val activeSceneEvents = engine.scene.onActiveChanged()
.onEach {
println("Active scene changed")
}
.launchIn(this)
try {
engine.scene.setZoomLevel(2F)
engine.scene.load(scene = savedScene)
} finally {
zoomEvents.cancel()
activeSceneEvents.cancel()
}
}
```
This guide covers how to create scenes from scratch, manage pages within scenes, configure scene properties, save and load designs, and control the camera's zoom and position.
## Scene Hierarchy
Scenes form the root of CE.SDK's design structure. The hierarchy works as follows:
- **Scene** — The root container holding all design content
- **Pages** — Direct children of scenes, arranged according to the scene's layout
- **Blocks** — Design elements such as text, images, and shapes that belong to pages
Only blocks attached to pages within the active scene are rendered in the canvas. Use `engine.scene.get()` to retrieve the current scene and `engine.scene.getPages()` to access its pages.
## Creating Scenes
### Creating an Empty Scene
Use `engine.scene.create(sceneLayout = ...)` to create a new design scene with a configurable page layout. The `sceneLayout` parameter controls how pages are arranged in the canvas.
```kotlin highlight-android-create-scene
val scene = engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK)
```
Available layouts:
| Layout | Description |
|--------|-------------|
| `SceneLayout.VERTICAL_STACK` | Pages arranged vertically |
| `SceneLayout.HORIZONTAL_STACK` | Pages arranged horizontally |
| `SceneLayout.DEPTH_STACK` | Pages layered on top of each other |
| `SceneLayout.FREE` | Manual positioning (default) |
### Creating for Video Editing
For video projects, use `engine.scene.createForVideo()` to configure the scene for timeline-based editing. Unlike `create(sceneLayout = ...)`, this method takes no parameters, so you set the page size separately after creating the scene.
### Creating from Media Files
Create scenes directly from images or videos with `engine.scene.createFromImage(imageUri = ...)` and `engine.scene.createFromVideo(videoUri = ...)`. The resulting scene uses the source media dimensions for its initial page.
### Adding Pages
After creating a scene, add pages using `engine.block.create(DesignBlockType.Page)`. Configure the page dimensions and append it to the scene.
```kotlin highlight-android-create-page
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
### Adding Blocks
With pages in place, add design elements like shapes, text, or images. Create a graphic block, configure its shape and fill, then append it to a page.
```kotlin highlight-android-create-block
val block = engine.block.create(DesignBlockType.Graphic)
val shape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block, shape = shape)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(block, fill = fill)
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 200F)
engine.block.appendChild(parent = page, child = block)
```
## Scene Properties
### Design Units
Query or configure how measurements are interpreted using `engine.scene.getDesignUnit()` and `engine.scene.setDesignUnit()`. This is useful for print workflows where precise physical dimensions matter.
```kotlin highlight-android-design-unit
val designUnit = engine.scene.getDesignUnit()
println("Design unit: $designUnit")
engine.scene.setDesignUnit(DesignUnit.MILLIMETER)
```
Supported units are `DesignUnit.PIXEL`, `DesignUnit.MILLIMETER`, and `DesignUnit.INCH`.
### Scene Layout
Control how pages are arranged using `engine.scene.getLayout()` and `engine.scene.setLayout()`. The layout affects how users navigate between pages in multi-page designs.
```kotlin highlight-android-scene-layout
engine.scene.setLayout(SceneLayout.HORIZONTAL_STACK)
val layout = engine.scene.getLayout()
println("Layout: $layout")
```
## Page Navigation
Access pages within your scene using these methods:
```kotlin highlight-android-page-navigation
val pages = engine.scene.getPages()
println("Number of pages: ${pages.size}")
val currentPage = engine.scene.getCurrentPage()
println("Current page: $currentPage")
```
`getCurrentPage()` returns the page nearest to the viewport center, which is useful for determining which page the user is currently viewing. For more advanced block queries, use `engine.scene.findNearestToViewPortCenterByType(...)` and `engine.scene.findNearestToViewPortCenterByKind(...)`.
## Camera and Zoom
### Zoom to Block
Use `engine.scene.zoomToBlock()` to frame a specific block in the viewport with padding. Passing the page focuses the current page; passing the scene lets you frame the complete scene.
```kotlin highlight-android-zoom-to-block
engine.scene.zoomToBlock(
block = page,
paddingLeft = 20F,
paddingTop = 20F,
paddingRight = 20F,
paddingBottom = 20F,
)
```
### Zoom Level
Get and set the zoom level directly with `engine.scene.getZoomLevel()` and `engine.scene.setZoomLevel()`. The zoom level is a camera scale value; how that maps to physical output depends on the scene's design unit and DPI.
```kotlin highlight-android-zoom-level
val zoomLevel = engine.scene.getZoomLevel()
println("Zoom level: $zoomLevel")
engine.scene.setZoomLevel(1F)
```
### Auto-Fit Zoom
For continuous auto-framing, use `engine.scene.enableZoomAutoFit(block = page, axis = ZoomAutoFitAxis.BOTH, ...)` to keep a block centered as the viewport changes. Disable it with `engine.scene.disableZoomAutoFit(page)` and query the current state with `engine.scene.isZoomAutoFitEnabled(page)`.
```kotlin highlight-android-zoom-auto-fit
engine.scene.enableZoomAutoFit(
block = page,
axis = ZoomAutoFitAxis.BOTH,
paddingLeft = 20F,
paddingTop = 20F,
paddingRight = 20F,
paddingBottom = 20F,
)
println("Auto-fit enabled: ${engine.scene.isZoomAutoFitEnabled(page)}")
engine.scene.disableZoomAutoFit(page)
```
## Saving Scenes
### Saving to String
Use `engine.scene.saveToString(scene = scene)` to serialize the current scene. This captures the complete scene structure, including pages, blocks, and their properties, as a string you can store.
```kotlin highlight-android-save-scene
val savedScene = engine.scene.saveToString(scene = scene)
println("Scene saved, length: ${savedScene.length}")
```
The serialized string references external assets by URL instead of embedding them. For a self-contained bundle that includes referenced assets, use `engine.scene.saveToArchive(scene = scene)`.
## Loading Scenes
### Loading from String
Use `engine.scene.load(scene = savedScene)` to restore a scene from a saved string:
```kotlin highlight-android-load-scene
val loadedScene = engine.scene.load(scene = savedScene)
println("Scene loaded: $loadedScene")
```
Loading a new scene replaces any existing scene. The engine only keeps one active scene at a time.
### Loading from URL
Use `engine.scene.load(sceneUri = Uri.parse(...))` to load a scene from a local or remote location. For scene bundles that include referenced assets, use `engine.scene.loadArchive(archiveUri = Uri.parse(...))`.
### Applying Templates
Apply template content to the current scene using `engine.scene.applyTemplate(template = ...)` or `engine.scene.applyTemplate(templateUri = ...)`. Template content is scaled automatically to the current page dimensions while keeping the current scene's design unit and page size.
## Event Subscriptions
Subscribe to scene-related events with Kotlin `Flow` to react to changes in real time. The example below keeps both collectors active long enough to observe a zoom change and a scene reload before cleaning them up.
```kotlin highlight-android-event-subscriptions
val zoomEvents = engine.scene.onZoomLevelChanged()
.onEach {
println("Zoom changed: ${engine.scene.getZoomLevel()}")
}
.launchIn(this)
val activeSceneEvents = engine.scene.onActiveChanged()
.onEach {
println("Active scene changed")
}
.launchIn(this)
try {
engine.scene.setZoomLevel(2F)
engine.scene.load(scene = savedScene)
} finally {
zoomEvents.cancel()
activeSceneEvents.cancel()
}
```
| Event | Description |
|-------|-------------|
| `onZoomLevelChanged()` | Fires when the zoom level changes |
| `onActiveChanged()` | Fires when the active scene changes |
## Next Steps
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Create and manipulate design elements within pages
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Templating"
description: "Understand how templates work in CE.SDK—reusable designs with variables for dynamic text and placeholders for swappable media."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/templating-f94385/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/)
---
```kotlin file=@cesdk_android_examples/editor-guides-concepts-templating/TemplatingEditorSolution.kt reference-only
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import ly.img.editor.Editor
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
@Composable
fun TemplatingEditorSolution(
license: String? = null,
onClose: (Throwable?) -> Unit,
) {
val context = LocalContext.current
Editor(
license = license,
baseUri = "file:///android_asset/".toUri(),
configuration = {
EditorConfiguration.remember {
onCreate = {
val engine = editorContext.engine
loadPostcardTemplate(engine)
// Register the variables used by this tropical postcard template.
engine.variable.set(key = "first_name", value = "Alice")
engine.variable.set(key = "last_name", value = "Smith")
engine.variable.set(key = "city", value = "Paris")
engine.variable.set(key = "address", value = "10 Rue de Rivoli")
val variableNames = engine.variable.findAll()
Log.d("TemplatingGuide", "Registered scene variables: $variableNames")
Log.d(
"TemplatingGuide",
"Loaded tropical postcard template for ${engine.variable.get("first_name")} ${engine.variable.get("last_name")}",
)
val placeholderBlocks = engine.block.findAllPlaceholders()
Log.d("TemplatingGuide", "Template placeholders: ${placeholderBlocks.size}")
placeholderBlocks.forEach { placeholder ->
if (engine.block.supportsPlaceholderControls(placeholder)) {
engine.block.setPlaceholderControlsOverlayEnabled(placeholder, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(placeholder, enabled = true)
}
}
}
onError = { throwable ->
Toast.makeText(context, throwable.message, Toast.LENGTH_SHORT).show()
}
}
},
onClose = onClose,
)
}
private suspend fun loadPostcardTemplate(engine: Engine) {
engine.scene.load(
sceneUri = "https://cdn.img.ly/assets/demo/v3/ly.img.template/templates/cesdk_postcard_2.scene".toUri(),
waitForResources = true,
)
}
private suspend fun applyPostcardTemplate(engine: Engine) {
val scene = engine.scene.get() ?: engine.scene.create()
if (engine.scene.getPages().isEmpty()) {
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = page, value = 1080F)
engine.block.setHeight(block = page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
}
engine.scene.applyTemplate(
templateUri = "https://cdn.img.ly/assets/demo/v3/ly.img.template/templates/cesdk_postcard_2.scene".toUri(),
)
}
```
Templates transform static designs into dynamic, data-driven content. They combine reusable layouts with variable text and placeholder media, enabling personalization at scale.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-concepts-templating)
A template is a regular CE.SDK scene that contains **variable tokens** in text and **placeholder blocks** for media. When you load a template, you can populate the variables with data and swap placeholder content without rebuilding the underlying layout.
The runnable Android sample loads the hosted `cesdk_postcard_2.scene` tropical postcard template, preserves the asset's authored framing, registers the postcard's known recipient variables, and exposes the template's existing placeholder regions. In the verified editor run, the template opens centered on its hero image area while the personalization contract is demonstrated through the registered scene variables and placeholder controls. For implementation details, see the guides linked in each section.
## What Makes a Template
Any CE.SDK scene can become a template by adding dynamic elements:
| Element | Purpose | Example |
|---------|---------|---------|
| **Variables** | Dynamic text replacement | `Hello, {{first_name}}!` |
| **Placeholders** | Swappable media slots | Profile photo, product image |
| **Editing Constraints** | Protected design elements | Locked logo, fixed layout |
Templates separate **design** (created once by designers) from **content** (populated at runtime with data). This enables workflows like batch generation, form-based customization, and user personalization.
## Variables
Variables enable dynamic text without modifying the design structure. In the sample, the loaded tropical postcard template already contains tokens such as `{{first_name}}`, `{{last_name}}`, `{{city}}`, and `{{address}}`, and the Android app registers values for those keys after loading the template.
```kotlin highlight-android-set-variables
// Register the variables used by this tropical postcard template.
engine.variable.set(key = "first_name", value = "Alice")
engine.variable.set(key = "last_name", value = "Smith")
engine.variable.set(key = "city", value = "Paris")
engine.variable.set(key = "address", value = "10 Rue de Rivoli")
```
```kotlin highlight-android-discover-variables
val variableNames = engine.variable.findAll()
```
**How variables work:**
- Register known template variables with `engine.variable.set(key = "first_name", value = "Alice")`
- Reference them in text blocks with tokens such as `{{first_name}}`, `{{city}}`, and `{{address}}`
- Use `engine.variable.findAll()` to enumerate the variables currently stored on the active scene
- CE.SDK stores the variable values on the scene so matching template tokens can resolve during rendering and export
- Tokens are case-sensitive; unmatched tokens render as literal text
Variables are scene-scoped and persist when you save the template. On Android, `engine.variable.findAll()` does not inspect the loaded template file for token names. Treat the token names used by your template as part of your scene contract, then use `findAll()` to confirm which values are currently registered on the scene.
[Learn more about text variables →](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/)
## Placeholders
Placeholders mark blocks as content slots that users or automation can replace. When you mark an image block as a placeholder, it becomes a designated swap target inside the editor.
```kotlin highlight-android-discover-placeholders
val placeholderBlocks = engine.block.findAllPlaceholders()
```
**How placeholders work:**
- Mark swappable content with `engine.block.setPlaceholderEnabled(block, enabled = true)`
- Enable overlay or button affordances for supported blocks with `setPlaceholderControlsOverlayEnabled()` and `setPlaceholderControlsButtonEnabled()`
- Let adopters swap images or other media without changing the rest of the design
Use `engine.block.findAllPlaceholders()` to enumerate the blocks currently marked as placeholders. The sample loads a postcard template that already contains multiple placeholder-enabled regions, then enables the overlay controls for each supported block.
[Learn more about placeholders →](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/)
## Template Workflows
Templates support several common workflows:
### Form-Based Customization
Load a template, collect form input for variables, and let users personalize text while the design stays consistent. Placeholder blocks give them controlled media replacement instead of unrestricted editing.
### Batch Generation
Load a template programmatically, iterate through data records, set variables for each record, and export personalized designs. This powers certificates, badges, postcards, and personalized marketing.
### Design Systems
Create template libraries where designers maintain approved layouts and end users customize within defined boundaries using variables and placeholders.
## Loading and Applying Templates
**Load a template** with `engine.scene.load(sceneUri = ...)` to replace the current scene entirely:
```kotlin highlight-android-load-template
engine.scene.load(
sceneUri = "https://cdn.img.ly/assets/demo/v3/ly.img.template/templates/cesdk_postcard_2.scene".toUri(),
waitForResources = true,
)
```
`sceneUri` can point to a CDN resource, a local file, or another Android `Uri` that resolves to a scene file. The runnable sample uses this exact flow with the hosted `cesdk_postcard_2.scene` postcard template and keeps the template's authored framing so the guide opens on the same tropical postcard asset every time.
**Apply a template** with `engine.scene.applyTemplate(templateUri = ...)` to merge template content into an existing scene while preserving the current design unit and page dimensions:
```kotlin highlight-android-apply-template
engine.scene.applyTemplate(
templateUri = "https://cdn.img.ly/assets/demo/v3/ly.img.template/templates/cesdk_postcard_2.scene".toUri(),
)
```
Learn more about importing templates with [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/).
## Creating Templates
Build templates by adding variable tokens to text blocks and marking media blocks as placeholders. Save the finished scene with `engine.scene.saveToString(scene = scene)` or `engine.scene.saveToArchive(scene = scene)` so it can be loaded again later.
[Learn more about creating templates →](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/)
## Next Steps
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) — Define, inspect, and populate text variables in Android templates.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Mark swappable content slots and expose replacement controls.
- [Create Templates From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) — Build reusable template scenes programmatically and save them for reuse.
- [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/) — Load and import design templates into CE.SDK from URLs, archives, and serialized strings.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Terminology"
description: "Definitions for the core terms and concepts used throughout CE.SDK documentation, including Engine, Scene, Block, Fill, Shape, Effect, and more."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/terminology-99e82d/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Terminology](https://img.ly/docs/cesdk/android/concepts/terminology-99e82d/)
---
A reference guide to the core terms and concepts used throughout CE.SDK documentation.
CE.SDK uses consistent terminology across all platforms. Understanding what we call things helps you navigate the API, read documentation efficiently, and communicate effectively with other developers working on CE.SDK integration.
## Core Architecture
### Engine
All operations, like creating scenes, manipulating blocks, rendering, and exporting, go through the *Engine*. Initialize it once and use it throughout your application's lifecycle.
### Scene
The root container for all design content. A *Scene* contains *Pages*, which contain *Blocks*. Only one *Scene* can be active per *Engine* instance. You can create a *Scene* programmatically or load one from a file.
*Scenes* support both static designs (social posts, print materials, graphics) and time-based content (duration, playback time, animation).
See [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) for details.
### Page
*Pages* are containers within a *Scene* that hold content *Blocks* and define working area dimensions.
For static designs, pages are individual artboards. For video editing, pages are time-based compositions where *Blocks* are arranged across time.
### Block
The fundamental building unit in CE.SDK. Everything visible in a design is a *Block*: images, text, shapes, graphics, audio, video, and even *Pages* themselves. *Blocks* form a parent-child hierarchy.
Each *Block* has two identifiers:
- **DesignBlock**: A Kotlin `Int` handle that references a block in API calls
- **UUID**: A stable string identifier that persists across save and load operations
In this guide, *Block* refers to the CE.SDK design element concept, while `DesignBlock` refers to the Android handle type.
See [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for details.
## Block Anatomy
Modify a *Block's* appearance and behavior by attaching *Fills*, *Shapes*, and *Effects*. Most of these modifiers must be created separately and then attached to a *Block*.
### Fill
*Fills* cover the surface of a *Block's* shape:
- **Color Fill**: Solid color
- **Gradient Fill**: Linear, radial, or conical gradients
- **Image Fill**: Image content
- **Video Fill**: Video content
See [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/) for details.
### Shape
*Shapes* define a *Block's* outline and dimensions, determining the silhouette and how the *Fill* is clipped. *Shape* types include:
- **Rect**: Rectangles and squares
- **Ellipse**: Circles and ovals
- **Polygon**: Multi-sided shapes
- **Star**: Star shapes with configurable points
- **Line**: Straight lines
- **Vector Path**: Custom vector shapes
Like *Fills*, *Shapes* are created separately and attached to *Blocks*. See [Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) for details.
### Effect
*Effects* are non-destructive visual modifications applied to a *Block*. Multiple *Effects* can be stacked. *Effect* categories include:
- **Adjustments**: Brightness, contrast, saturation, and other image corrections
- **Filters**: LUT-based color grading, duotone
- **Stylization**: Pixelize, posterize, half-tone, dot pattern, linocut, outliner
- **Distortion**: Liquid, mirror, shifter, cross-cut, extrude blur
- **Focus**: Tilt-shift, vignette
- **Color**: Recolor, green screen (chroma key)
- **Other**: Glow, TV glitch
The order determines how multiple effects attached to a single block interact. See [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) for details.
### Blur
A modifier that reduces sharpness. *Blur* types include:
- **Uniform Blur**: Even blur across the entire block
- **Radial Blur**: Circular blur from a center point
- **Mirrored Blur**: Blur with reflection
> **Note:** **Blur has a dedicated API because it composites differently than other effects.** While most effects like brightness or saturation operate only on a block's own pixels, blur needs to sample pixels from the surrounding area to calculate the blurred result. This means blur interacts with the scene's layering and transparency in ways other effects do not. When you blur a partially transparent block, the engine must handle how that blur blends with whatever content sits behind it.
See [Blur](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) for details.
### Drop Shadow
A built-in block property, not an *Effect*, that renders a shadow beneath blocks. *Drop Shadow* has dedicated API methods for enabling, color, offset, and blur radius.
> **Warning:** Unlike effects, drop shadow is configured directly on the block rather than created and attached separately.
## Block Handling
These terms describe how *Blocks* are categorized and identified.
### Type
The built-in *Type* defines a *Block's* core behavior and available properties. *Type* is immutable: you choose it when creating the *Block*.
- `//ly.img.ubq/graphic` — Visual block for images, shapes, and graphics
- `//ly.img.ubq/text` — Text content
- `//ly.img.ubq/audio` — Audio content
- `//ly.img.ubq/page` — Page container
- `//ly.img.ubq/scene` — Root scene container
- `//ly.img.ubq/track` — Video timeline track
- `//ly.img.ubq/stack` — Stack container for layering
- `//ly.img.ubq/group` — Group container for organizing blocks
- `//ly.img.ubq/camera` — Camera for scene viewing
- `//ly.img.ubq/cutout` — Cutout or mask block
- `//ly.img.ubq/caption` — Caption or subtitle block
- `//ly.img.ubq/captionTrack` — Track for captions
The *Type* determines which properties and capabilities a *Block* has.
### Kind
A custom string label you assign to categorize *Blocks* for your application. Unlike *Type*, *Kind* is mutable and application-defined. Changing the *Kind* has no effect on appearance or behavior at the engine level. You can query and search for *Blocks* by *Kind*. Common uses:
- Categorizing template elements ("logo", "headline", "background")
- Filtering blocks for custom UI
- Automation workflows that process blocks by purpose
### Property
A configurable attribute of a *Block*. *Properties* have types (`Bool`, `Int`, `Float`, `String`, `Color`, `Enum`) and paths like `text/fontSize` or `fill/image/imageFileURI`.
Access *Properties* using type-specific getter and setter methods. Each *Block* type exposes different properties, which you can discover programmatically. See [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for details.
## Assets and Resources
### Asset
Think of *Assets* as media items that you can provide to your users: images, videos, audio files, fonts, stickers, or templates, meaning anything that can be added to a design. *Assets* have metadata including:
- **ID**: Unique identifier within an asset source
- **Label**: Display name
- **Meta**: Custom metadata (URI, dimensions, format)
- **Thumbnail URI**: Preview image URL
*Assets* are provided by *Asset Sources* and added through the UI or programmatically.
### Asset Source
A provider of *Assets*. *Asset Sources* can be built-in, like the default sticker library, or custom. *Asset Sources* implement a query interface returning paginated results with search and filtering.
- **Local Asset Source**: Assets defined in JSON, loaded at initialization
- **Remote Asset Source**: Custom implementation fetching from external APIs
Register *Asset Sources* with the *Engine* to make *Assets* available throughout your application.
### Resource
Loaded data from an *Asset* URI. When you reference an image or video URL in a *Block*, the *Engine* fetches and caches the *Resource*. *Resources* include binary data and metadata for rendering. See [Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/) for details.
### Buffer
A resizable container for arbitrary binary data. *Buffers* are useful for dynamically generated content that does not come from a URL, such as synthesized audio or programmatically created images.
Create a *Buffer*, write data to it, and reference it by URI in *Block* properties. *Buffer* data is not serialized with scenes and changes cannot be undone. See [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) for details.
## Templating and Automation
These terms describe dynamic content and reusable designs.
### Template
A reusable design with predefined structure and styling. *Templates* typically contain *Placeholders* and *Variables* that users customize while maintaining overall layout and branding.
*Templates* are scenes saved in a format that can be loaded and modified.
### Placeholder
A *Block* marked for content replacement. When a *Block's* placeholder property is enabled, it signals that the *Block* expects user-provided content, like an image drop zone or editable text field.
*Placeholders* indicate which parts of a design should be customized versus fixed. See [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) for details.
### Variable
A named value referenced in text blocks using `{{variableName}}` syntax. *Variables* enable data-driven design generation by populating templates with dynamic content.
Define *Variables* at the scene level and reference them in text blocks. When a *Variable* value changes, all referencing text blocks update automatically. See [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) for details.
## Permissions and Scopes
These terms relate to controlling what operations are allowed.
### Scope
A permission setting controlling whether specific operations are allowed on a *Block*. *Scopes* enable fine-grained control over what users can modify, which is essential for template workflows where some elements should be editable and others locked.
Common scopes:
- `layer/move` — Allow or prevent moving
- `layer/resize` — Allow or prevent resizing
- `layer/rotate` — Allow or prevent rotation
- `layer/visibility` — Allow or prevent hiding
- `lifecycle/destroy` — Allow or prevent deletion
- `editor/select` — Allow or prevent selection
Enable or disable *Scopes* per *Block* to create controlled editing experiences. See [Lock Design Elements](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) for details.
### Role
A preset collection of *Scope* settings. CE.SDK defines four built-in *Roles*. On Android, set the role with `editor.setRole(...)`:
- **Creator**: Full access to all operations, for template authors
- **Adopter**: Restricted access for end users customizing templates
- **Viewer**: Read-only access without editing capabilities
- **Presenter**: Presentation-focused, non-editing access
*Roles* provide a convenient way to apply consistent permission sets.
## Layout and Units
These terms relate to positioning and measurement.
### Design Unit
The measurement unit for dimensions in a *Scene*. The choice affects how positions, sizes, and exports are interpreted. Options:
- **Pixel**: Screen pixels, default for digital designs
- **Millimeter**: Metric measurement for print
- **Inch**: Imperial measurement for print
Set the design unit at the scene level. All dimension values are interpreted in that unit.
### DPI (Dots Per Inch)
Resolution setting affecting export quality and unit conversion. Higher DPI produces larger exports with more detail. The default is 300 DPI, suitable for print-quality output.
DPI matters when working with physical units, like millimeters or inches, as it determines how measurements translate to pixel dimensions during export.
## Operating Modes
These terms describe how CE.SDK runs.
### Scene Capabilities
Every *Scene* supports the full range of features:
- **Static designs**: Content arranged spatially on pages.
- **Video editing**: *Blocks* can have duration, time offset, playback time, and animation properties.
On Android, choose the starting configuration when creating the scene: `engine.scene.create()` for a static design layout, or `engine.scene.createForVideo()` for video editing. See [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) for details.
### Headless Mode
Running CE.SDK without the built-in UI. Used for:
- Offscreen rendering and export
- Automation pipelines
- Custom UI implementations
- Batch processing
In *Headless Mode*, you work directly with *Engine* APIs without the visual editor. See [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) for setup.
## Events and State
These terms relate to monitoring changes.
### Event / Subscription
A callback mechanism for reacting to changes in the *Engine*. Subscribe to events and receive notifications when state changes. Common events:
- Selection changes
- Block state changes
- History (undo and redo) changes
On Android, block lifecycle subscriptions are exposed as Kotlin `Flow`s. Cancel the coroutine collecting the `Flow` when you no longer need notifications. See [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) for details.
### Block State
The current status of a *Block* indicating readiness or issues:
- **Ready**: Normal state, no pending operations
- **Pending**: Operation in progress, with optional progress value from 0 to 1
- **Error**: Operation failed, with error type (`IMAGE_DECODING`, `VIDEO_DECODING`, `FILE_FETCH`, `AUDIO_DECODING`, or `UNKNOWN`)
*Block State* reflects the combined status of the *Block* and its attached *Fill*, *Shape*, and *Effects*.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Undo and History"
description: "Manage undo and redo stacks in CE.SDK using multiple histories, callbacks, and API-based controls."
platform: android
url: "https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/)
---
```kotlin file=@cesdk_android_examples/engine-guides-undo-and-history/UndoAndHistory.kt reference-only
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.HistoryUpdate
import ly.img.engine.ShapeType
suspend fun undoAndHistory(
engine: Engine,
page: DesignBlock? = null,
) = withContext(engine.dispatcher) {
val targetPage = page ?: createDemoPage(engine)
val activeHistory = engine.editor.getActiveHistory()
val guideHistory = engine.editor.createHistory()
var historyUpdates: Job? = null
try {
engine.editor.setActiveHistory(guideHistory)
historyUpdates = subscribeToHistoryUpdates(scope = this, engine = engine)
val primaryBlock = createPrimaryBlock(engine, targetPage)
undoLatestChange(engine)
redoLatestChange(engine)
applyManualUndoStep(engine, primaryBlock)
createSecondaryHistoryDemo(engine, targetPage)
} finally {
withContext(NonCancellable) {
try {
historyUpdates?.cancelAndJoin()
} finally {
try {
engine.editor.setActiveHistory(activeHistory)
} finally {
engine.editor.destroyHistory(guideHistory)
}
}
}
}
}
private fun subscribeToHistoryUpdates(
scope: CoroutineScope,
engine: Engine,
): Job = scope.launch {
// Subscribe to history updates.
engine.editor.onHistoryUpdatedWithKind().collect { kind ->
when (kind) {
HistoryUpdate.ACTIVATED -> println("Active history switched, scene unchanged.")
HistoryUpdate.UPDATED -> println(
"History updated: canUndo=${engine.editor.canUndo()}, " +
"canRedo=${engine.editor.canRedo()}",
)
}
}
}
private fun createPrimaryBlock(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setPositionX(block, 140F)
engine.block.setPositionY(block, 95F)
engine.block.setWidth(block, 265F)
engine.block.setHeight(block, 265F)
val triangleShape = engine.block.createShape(ShapeType.Polygon)
engine.block.setInt(
triangleShape,
property = "shape/polygon/sides",
value = 3,
)
engine.block.setShape(block, triangleShape)
val triangleFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
triangleFill,
property = "fill/color/value",
value = Color.fromRGBA(0.2F, 0.5F, 0.9F, 1F),
)
engine.block.setFill(block, triangleFill)
engine.block.appendChild(page, block)
engine.editor.addUndoStep()
return block
}
private fun undoLatestChange(engine: Engine) {
if (engine.editor.canUndo()) {
engine.editor.undo()
}
}
private fun redoLatestChange(engine: Engine) {
if (engine.editor.canRedo()) {
engine.editor.redo()
}
}
private fun applyManualUndoStep(
engine: Engine,
primaryBlock: DesignBlock,
) {
engine.block.setPositionX(primaryBlock, 190F)
engine.editor.addUndoStep()
if (engine.editor.canUndo()) {
engine.editor.removeUndoStep()
}
engine.block.setPositionX(primaryBlock, 140F)
}
private fun createSecondaryHistoryDemo(
engine: Engine,
page: DesignBlock,
) {
val primaryHistory = engine.editor.getActiveHistory()
val secondaryHistory = engine.editor.createHistory()
try {
engine.editor.setActiveHistory(secondaryHistory)
val secondaryBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setPositionX(secondaryBlock, 440F)
engine.block.setPositionY(secondaryBlock, 95F)
engine.block.setWidth(secondaryBlock, 220F)
engine.block.setHeight(secondaryBlock, 220F)
val circleShape = engine.block.createShape(ShapeType.Ellipse)
engine.block.setShape(secondaryBlock, circleShape)
val circleFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
circleFill,
property = "fill/color/value",
value = Color.fromRGBA(0.9F, 0.3F, 0.3F, 1F),
)
engine.block.setFill(secondaryBlock, circleFill)
engine.block.appendChild(page, secondaryBlock)
engine.editor.addUndoStep()
} finally {
try {
engine.editor.setActiveHistory(primaryHistory)
} finally {
engine.editor.destroyHistory(secondaryHistory)
}
}
}
private fun createDemoPage(engine: Engine): DesignBlock {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1920F)
return page
}
```
Manage undo and redo operations with the Android CreativeEngine API, subscribe to history updates with `Flow`, and isolate workflows with multiple history stacks.
> **Reading time:** 4 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-undo-and-history)
This guide focuses on the engine APIs. It assumes your app already has an initialized `Engine` and a scene or block to edit. Pass an existing page to `undoAndHistory()` to keep its active history isolated from the temporary guide history. When no page is passed, the standalone sample creates a small offscreen demo page and preserves that new scene's initial history. The highlighted code is the history logic you would apply to your own scene.
If you use the default Android editor UI, its undo and redo buttons call the same history APIs shown here. Custom UI can mirror that behavior by checking `canUndo()` and `canRedo()`.
## Creating an Undoable Change
When you make programmatic engine changes outside the default UI, commit the logical operation with `addUndoStep()`. The example creates a graphic block on an existing page and records that as one undoable checkpoint.
```kotlin highlight-android-create-block
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setPositionX(block, 140F)
engine.block.setPositionY(block, 95F)
engine.block.setWidth(block, 265F)
engine.block.setHeight(block, 265F)
val triangleShape = engine.block.createShape(ShapeType.Polygon)
engine.block.setInt(
triangleShape,
property = "shape/polygon/sides",
value = 3,
)
engine.block.setShape(block, triangleShape)
val triangleFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
triangleFill,
property = "fill/color/value",
value = Color.fromRGBA(0.2F, 0.5F, 0.9F, 1F),
)
engine.block.setFill(block, triangleFill)
engine.block.appendChild(page, block)
engine.editor.addUndoStep()
```
## Performing Undo and Redo Operations
Check `canUndo()` or `canRedo()` before calling the corresponding API. This matches the availability rules of the built-in Android UI.
```kotlin highlight-android-undo
if (engine.editor.canUndo()) {
engine.editor.undo()
}
```
```kotlin highlight-android-redo
if (engine.editor.canRedo()) {
engine.editor.redo()
}
```
## Subscribing to History Changes
`engine.editor.onHistoryUpdatedWithKind()` returns a `Flow` that fires after undo, redo, or any new committed change. Each event reports whether the active stack's snapshots changed (`UPDATED`) or whether `setActiveHistory()` swapped to a different stack without changing the scene (`ACTIVATED`). Collect it from a scope owned by the screen or workflow that exposes your custom undo and redo controls, and ignore `ACTIVATED` events in dirty-state or save-button logic.
```kotlin highlight-android-subscribe-history
// Subscribe to history updates.
engine.editor.onHistoryUpdatedWithKind().collect { kind ->
when (kind) {
HistoryUpdate.ACTIVATED -> println("Active history switched, scene unchanged.")
HistoryUpdate.UPDATED -> println(
"History updated: canUndo=${engine.editor.canUndo()}, " +
"canRedo=${engine.editor.canRedo()}",
)
}
}
```
In a real app, map the callback to Compose state, toolbar buttons, or analytics instead of just printing the current availability.
## Managing Undo Steps Manually
Use `addUndoStep()` when several engine calls should be treated as one logical action. `removeUndoStep()` lets you discard the latest checkpoint again.
```kotlin highlight-android-manual-step
engine.block.setPositionX(primaryBlock, 190F)
engine.editor.addUndoStep()
if (engine.editor.canUndo()) {
engine.editor.removeUndoStep()
}
engine.block.setPositionX(primaryBlock, 140F)
```
## Working with Multiple History Stacks
Multiple histories are useful when an Android flow needs isolated undo and redo behavior, such as a temporary overlay or guided sub-step.
### Creating and Switching History Stacks
Create a second stack with `createHistory()`, switch to it with `setActiveHistory()`, and switch back when the isolated edits are done.
```kotlin highlight-android-multiple-histories
val primaryHistory = engine.editor.getActiveHistory()
val secondaryHistory = engine.editor.createHistory()
try {
engine.editor.setActiveHistory(secondaryHistory)
val secondaryBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setPositionX(secondaryBlock, 440F)
engine.block.setPositionY(secondaryBlock, 95F)
engine.block.setWidth(secondaryBlock, 220F)
engine.block.setHeight(secondaryBlock, 220F)
val circleShape = engine.block.createShape(ShapeType.Ellipse)
engine.block.setShape(secondaryBlock, circleShape)
val circleFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
circleFill,
property = "fill/color/value",
value = Color.fromRGBA(0.9F, 0.3F, 0.3F, 1F),
)
engine.block.setFill(secondaryBlock, circleFill)
engine.block.appendChild(page, secondaryBlock)
engine.editor.addUndoStep()
} finally {
try {
engine.editor.setActiveHistory(primaryHistory)
} finally {
engine.editor.destroyHistory(secondaryHistory)
}
}
```
### Cleaning Up History Stacks
Destroy temporary stacks once the isolated workflow is complete. The preceding example performs that cleanup in a nested `finally` block, so it restores the primary stack and destroys the temporary stack even when the isolated workflow fails. Cleaning up unused stacks avoids leaving unused history handles around longer than necessary.
## Troubleshooting
- **Undo or redo stays disabled**: Call `addUndoStep()` after programmatic changes that should become undoable.
- **History updates never arrive**: Start the `Flow` collection from `onLoaded` in `editorContext.coroutineScope` or another editor-owned scope.
- **Undo affects the wrong workflow**: Check `getActiveHistory()` before calling `undo()` or `redo()` when multiple stacks are active.
## API Reference
| Method | Purpose |
| ------ | ------- |
| `engine.editor.createHistory()` | Create a new undo and redo history stack. |
| `engine.editor.destroyHistory()` | Destroy a history stack when an isolated workflow is complete. |
| `engine.editor.setActiveHistory()` | Switch undo and redo operations to a specific history stack. |
| `engine.editor.getActiveHistory()` | Get the history stack that currently receives undo and redo operations. |
| `engine.editor.addUndoStep()` | Commit the current editor state as a manual checkpoint. |
| `engine.editor.removeUndoStep()` | Remove the most recent manual checkpoint again. |
| `engine.editor.undo()` | Revert the latest committed change in the active history stack. |
| `engine.editor.redo()` | Restore the latest reverted change in the active history stack. |
| `engine.editor.canUndo()` | Check whether the active history stack currently has an undo step. |
| `engine.editor.canRedo()` | Check whether the active history stack currently has a redo step. |
| `engine.editor.onHistoryUpdatedWithKind()` | Observe history changes as a `Flow` that distinguishes `UPDATED` (snapshot change, undo, or redo) from `ACTIVATED` (a `setActiveHistory()` switch). |
## Next Steps
- [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) — subscribe to block creation, update, and deletion events alongside history updates
- [Editor State](https://img.ly/docs/cesdk/android/concepts/edit-modes-1f5b6c/) — combine edit mode changes with history state in custom Android UI
- [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — build or reset scenes before assigning them to specific history stacks
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Configuration"
description: "Learn how to configure CE.SDK to match your application's functional, visual, and performance requirements."
platform: android
url: "https://img.ly/docs/cesdk/android/configuration-2c1c3d/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/)
---
```kotlin file=@cesdk_android_examples/editor-guides-configuration-basics/BasicEditorSolution.kt reference-only
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import ly.img.editor.Editor
import ly.img.editor.EditorUiMode
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.engine.EngineRenderTarget
// Call this composable from your app's navigation layer.
@Composable
fun BasicEditorSolution(
license: String,
signedInUserId: String?,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
userId = signedInUserId,
baseUri = "file:///android_asset/assets/".toUri(),
engineRenderTarget = EngineRenderTarget.SURFACE_VIEW,
uiMode = EditorUiMode.SYSTEM,
configuration = {
EditorConfiguration.remember {
onLoaded = {
val editor = editorContext.engine.editor
editor.setRole("Creator")
Log.i("ConfigurationGuide", "Current role: ${editor.getRole()}")
editor.setSettingBoolean(
keypath = "doubleClickToCropEnabled",
value = false,
)
val doubleClickToCropEnabled = editor.getSettingBoolean(
keypath = "doubleClickToCropEnabled",
)
Log.i(
"ConfigurationGuide",
"Double-click crop enabled: $doubleClickToCropEnabled",
)
}
}
},
onClose = onClose,
)
}
```
Configure the Android editor with license validation, asset locations, user tracking, rendering behavior, and runtime Engine settings.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-basics)
The Android `Editor` composable owns Engine startup for the CE.SDK editor UI. Pass startup values directly to `Editor`, then use `EditorConfiguration` for callbacks, component customization, and runtime Engine settings through the existing `editorContext.engine`.
## Required Configuration
### License Key
Production apps should pass a CE.SDK license key. Passing `null` or an empty value starts evaluation mode, which keeps export watermarks active.
| Property | Type | Purpose |
| -------- | ---- | ------- |
| `license` | `String?` | License key used to unlock CE.SDK and remove export watermarks |
Get a free trial license at [https://img.ly/forms/free-trial](https://img.ly/forms/free-trial).
```kotlin highlight-android-license
license = license,
```
## Optional Configuration
These `Editor` parameters configure startup values, editor lifecycle hooks, and close handling. Values such as `userId`, `baseUri`, `host`, `engineRenderTarget`, and `uiMode` apply when the editor starts, while `configuration` and `onClose` cover callbacks and behavior throughout the editor lifecycle.
| Property | Type | Purpose |
| -------- | ---- | ------- |
| `userId` | `String?` | User identifier for Monthly Active User (MAU) tracking |
| `baseUri` | `Uri` | Base location for bundled or self-hosted CE.SDK assets; defaults to the top-level `ly.img.editor.defaultBaseUri` |
| `host` | `String` | Build host passed to Engine startup for license host matching; leave empty unless your license setup requires a specific host value |
| `engineRenderTarget` | `EngineRenderTarget` (`SURFACE_VIEW`, `TEXTURE_VIEW`) | Android view type used by the Engine renderer |
| `uiMode` | `EditorUiMode` (`SYSTEM`, `LIGHT`, `DARK`) | Light, dark, or system-following editor theme mode |
| `configuration` | `@Composable EditorScope.() -> EditorConfiguration` | Scoped configuration lambda for editor callbacks, UI components, asset library, and color palette |
| `onClose` | `(Throwable?) -> Unit` | Close/error callback used by your navigation layer |
Android routes editor and engine messages through Logcat. There is no initialization-time custom logger configuration option.
## Configuration Properties
### User ID
Set `userId` when your app has signed-in users. Pass the current user's stable application ID instead of a shared hard-coded value, or omit it for anonymous sessions. CE.SDK also uses the Android device ID for tracking accuracy, so include that collection in your Play Store Data safety form.
```kotlin highlight-android-user-id
userId = signedInUserId,
```
### Asset Base URI
`baseUri` is the base path for relative editor and Engine asset paths. If omitted, `Editor` uses the top-level `defaultBaseUri` from `ly.img.editor`, a versioned IMG.LY CDN URL defined by the Android SDK. For production apps, bundle the CE.SDK Android assets with your app or host them yourself, then point `baseUri` at that location instead of relying on the IMG.LY CDN.
For the local URI shown below, download the versioned CE.SDK Android asset bundle from [the IMG.LY CDN](https://cdn.img.ly/packages/imgly/cesdk-android/$UBQ_VERSION$/imgly-assets.zip) and extract it. The archive contains a top-level `assets/` directory; copy that directory into your app module's `src/main/assets/` folder so `file:///android_asset/assets/` points directly at the asset files. If you host the same asset folder, pass its HTTPS base URL as `baseUri` instead; the [Serve Assets From Your Server](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) guide covers that setup.
```kotlin highlight-android-base-uri
baseUri = "file:///android_asset/assets/".toUri(),
```
### Rendering and UI Mode
Use `engineRenderTarget` to choose the underlying Android render view:
- `EngineRenderTarget.SURFACE_VIEW` renders through `SurfaceView`.
- `EngineRenderTarget.TEXTURE_VIEW` renders through `TextureView`.
Use `uiMode` for the editor color scheme:
- `EditorUiMode.SYSTEM` follows the operating system setting.
- `EditorUiMode.LIGHT` displays the editor in light mode.
- `EditorUiMode.DARK` displays the editor in dark mode.
Advanced theme customization belongs in the [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) guide.
```kotlin highlight-android-rendering
engineRenderTarget = EngineRenderTarget.SURFACE_VIEW,
uiMode = EditorUiMode.SYSTEM,
```
## Runtime Configuration
Use `EditorConfiguration.remember` for callbacks that run inside the editor lifecycle. The default editor creation flow creates the initial scene for you, so this guide keeps that default and uses `onLoaded` to apply the role and settings after the editor is ready. If you override `onCreate`, create or load a scene before the callback completes.
Use `EditorConfiguration.remember` and `then` to chain additional configuration blocks and IMG.LY plugins onto an existing editor configuration.
```kotlin highlight-android-runtime-configuration
configuration = {
EditorConfiguration.remember {
onLoaded = {
val editor = editorContext.engine.editor
editor.setRole("Creator")
Log.i("ConfigurationGuide", "Current role: ${editor.getRole()}")
editor.setSettingBoolean(
keypath = "doubleClickToCropEnabled",
value = false,
)
val doubleClickToCropEnabled = editor.getSettingBoolean(
keypath = "doubleClickToCropEnabled",
)
Log.i(
"ConfigurationGuide",
"Double-click crop enabled: $doubleClickToCropEnabled",
)
}
}
},
```
`setRole()` changes the active editing role and applies role-dependent defaults. The CE.SDK editor UI applies `Adopter` before `onLoaded`; the sample sets `Creator` so the callback demonstrates a role change in the editor flow. Supported roles are `Creator`, `Adopter`, `Viewer`, and `Presenter`. On Android, read and write the role with `setRole()` and `getRole()` rather than a settings keypath.
Settings such as `doubleClickToCropEnabled` are available through the typed setting methods on `engine.editor`. The sample disables double-click crop so the readback logs a non-default value. Use the Boolean, Int, Float, String, Color, and Enum setter/getter pairs for known settings, and use `findAllSettings()` or `getSettingType()` when you need to inspect available settings dynamically.
> **Note:** For full event and callback handling, see [UI Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/). For toolbar and button layout changes, see [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/).
## Troubleshooting
| Issue | What to check |
| ----- | ------------- |
| Exports still contain a watermark | `null` or an empty `license` intentionally starts evaluation mode and keeps export watermarks active. If you pass a production key and startup fails, check for an expired, invalid, or host-mismatched key because Android license validation surfaces those cases as unlock errors instead of continuing in evaluation mode. |
| Assets are missing or fail to load | Check that `baseUri` points at the copied or hosted Android asset folder. For `file:///android_asset/assets/`, the extracted archive's top-level `assets/` directory must be copied directly into `src/main/assets/`. |
| The editor opens without a scene after custom callbacks | If you override `onCreate`, your callback owns scene creation or loading. Call the relevant `editorContext.engine.scene.create*` or `load*` API before the callback completes, or keep the default creation flow and use `onLoaded` for role and settings changes. |
## API Reference
| Method | Purpose |
| ------ | ------- |
| `Editor(license=_, userId=_, baseUri=_, host=_, engineRenderTarget=_, uiMode=_, configuration=_, onClose=_)` | Initialize the Android editor UI with startup configuration |
| `EditorConfiguration.remember(builder=_)` | Create editor lifecycle callbacks and component configuration with an optional builder lambda |
| `EditorConfiguration.remember(builderFactory=_, builder=_)` | Create editor lifecycle callbacks with a custom configuration builder |
| `existingConfiguration.then(builder=_)` | Chain another configuration block onto an existing editor configuration |
| `existingConfiguration.then(builderFactory=_, builder=_)` | Chain another configuration builder or plugin onto an existing editor configuration |
| `EditorConfigurationBuilder.onCreate=_` | Create or load the scene when overriding default editor creation |
| `EditorConfigurationBuilder.onLoaded=_` | Run post-load configuration after the editor scene is available |
| `EditorConfigurationBuilder.onExport=_` | Handle export button actions |
| `EditorConfigurationBuilder.onClose=_` | Handle editor close events inside the editor lifecycle |
| `EditorConfigurationBuilder.onEvent=_` | Handle editor events |
| `EditorConfigurationBuilder.onUpload=_` | Adjust or upload assets before they are added to upload asset sources |
| `EditorConfigurationBuilder.onError=_` | Handle errors captured by the editor |
| `EditorConfigurationBuilder.colorPalette=_` | Configure the editor UI color palette |
| `EditorConfigurationBuilder.assetLibrary=_` | Configure the asset library |
| `EditorConfigurationBuilder.dock=_` | Configure the bottom dock component |
| `EditorConfigurationBuilder.navigationBar=_` | Configure the top navigation bar component |
| `EditorConfigurationBuilder.inspectorBar=_` | Configure the inspector bar shown for selected blocks |
| `EditorConfigurationBuilder.canvasMenu=_` | Configure the canvas menu next to selected blocks |
| `EditorConfigurationBuilder.bottomPanel=_` | Configure a fixed bottom panel, such as a timeline panel |
| `EditorConfigurationBuilder.overlay=_` | Configure overlay UI above the editor |
| `engine.editor.setRole(role=_)` | Set the active editing role |
| `engine.editor.getRole()` | Read the active editing role |
| `engine.editor.onRoleChanged()` | Collect role changes after role defaults are applied |
| `engine.editor.setSettingBoolean(keypath=_, value=_)` | Set a boolean Engine setting |
| `engine.editor.getSettingBoolean(keypath=_)` | Read a boolean Engine setting |
| `engine.editor.setSettingInt(keypath=_, value=_)` | Set an integer Engine setting |
| `engine.editor.getSettingInt(keypath=_)` | Read an integer Engine setting |
| `engine.editor.setSettingFloat(keypath=_, value=_)` | Set a float Engine setting |
| `engine.editor.getSettingFloat(keypath=_)` | Read a float Engine setting |
| `engine.editor.setSettingString(keypath=_, value=_)` | Set a string Engine setting |
| `engine.editor.getSettingString(keypath=_)` | Read a string Engine setting |
| `engine.editor.setSettingColor(keypath=_, value=_)` | Set a color Engine setting |
| `engine.editor.getSettingColor(keypath=_)` | Read a color Engine setting |
| `engine.editor.setSettingEnum(keypath=_, value=_)` | Set an enum Engine setting |
| `engine.editor.getSettingEnum(keypath=_)` | Read an enum Engine setting |
| `engine.editor.getSettingEnumOptions(keypath=_)` | Read supported values for an enum Engine setting |
| `engine.editor.findAllSettings()` | List available Engine settings |
| `engine.editor.getSettingType(keypath=_)` | Read the type of an Engine setting |
| `engine.editor.onSettingsChanged()` | Collect Engine setting change events |
## Next Steps
- [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) - Customize the editor's visual appearance
- [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) - Configure languages and translations
- [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) - Configure asset sources and libraries
- [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) - Control editing capabilities with roles and scopes
- [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Use CE.SDK without the UI
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Conversion"
description: "Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools."
platform: android
url: "https://img.ly/docs/cesdk/android/conversion-c3fbb3/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/)
---
---
## Related Pages
- [Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) - Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools.
- [To Base64](https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/) - Convert CE.SDK exports to Base64-encoded strings for embedding in URLs, storing in databases, or transmitting via APIs.
- [To Blob](https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/) - Export CE.SDK design blocks to Android ByteBuffer data for saving, uploading, or sharing.
- [To PNG](https://img.ly/docs/cesdk/android/conversion/to-png-f1660c/) - Export designs and images to PNG format with compression settings and target dimensions using CE.SDK.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Overview"
description: "Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools."
platform: android
url: "https://img.ly/docs/cesdk/android/conversion/overview-44dc58/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/)
---
CreativeEditor SDK (CE.SDK) exports Android designs to formats such as PNG,
PDF, SVG, and MP4 so your app can prepare assets for sharing, printing,
storage, or publishing workflows.
You can trigger conversions programmatically with the Android Engine API or let
users start exports through the editor UI.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
## Supported Input and Output Formats
CE.SDK accepts a range of input formats when working with designs, including:
When it comes to exporting or converting designs, the SDK supports the following output formats:
| Category | Supported Formats |
| ------------ | ---------------------------------------------------- |
| **Images** | `.png`, `.jpeg`, `.tga` |
| **Vector** | `.svg` with text exported as paths |
| **Print** | `.pdf` with compatibility and underlayer options |
| **Video** | `.mp4` |
| **Scene** | serialized scene strings for `.scene` workflows |
| **Blocks** | proprietary block strings or block archive entries such as `blocks.blocks` |
| **Archive** | `.zip` archives with scenes or blocks and their assets |
| **Raw Data** | binary RGBA8888 image data through `MimeType.BINARY` |
Each format serves different use cases, giving you the flexibility to adapt designs for your application’s needs.
## Conversion Methods
Use the conversion path that matches how much control your Android app needs over
the export workflow.
| Method | Android API | Use it for |
| ------ | ----------- | ---------- |
| Programmatic image, vector, PDF, or raw binary export | `engine.block.export(...)` | Exporting a scene, page, group, or block from Kotlin with a selected `MimeType` and optional `ExportOptions`. |
| Programmatic video export | `engine.block.exportVideo(...)` | Exporting a page timeline to MP4 while receiving progress updates. |
| Scene and block serialization | `engine.scene.saveToString(scene=_)`, `engine.scene.saveToArchive(scene=_)`, `engine.block.saveToString(blocks=_)`, `engine.block.saveToArchive(blocks=_)` | Persisting editable CE.SDK content either as a serialized scene string or archive, or as proprietary block data or a block archive. |
| Color-mask export | `engine.block.exportWithColorMask(...)` | Creating image data plus a mask for workflows that need a separate color mask. |
| Editor UI export | `onExport` / built-in export action | Letting users export from the Android editor UI while your app controls the final export handling. |
Programmatic exports return binary data that your app can write to storage,
upload, share, or pass to another workflow. UI-driven exports are useful when
the editor should remain user-facing and the app only needs to customize what
happens after the user taps export.
## Customization Options
Android exports combine scene-level settings with `ExportOptions` for static
formats and `ExportVideoOptions` for MP4 output.
| Option | Applies to | Purpose |
| ------ | ---------- | ------- |
| `targetWidth` / `targetHeight` | Static and video exports | Render output at a specific size while preserving the block aspect ratio. |
| `scene/dpi` | Scene exports and design units | Set print metadata and the pixel-to-inch or millimeter conversion, either while creating an image scene with `engine.scene.createFromImage(imageUri = imageUri, dpi = 300F)` or later with `engine.block.setFloat(block = scene, property = "scene/dpi", value = 300F)`. |
| `pngCompressionLevel` | PNG | Balance file size and encode time without changing visual quality. |
| `jpegQuality` | JPEG | Control compression quality for JPEG exports. |
| `exportPdfWithHighCompatibility` | PDF | Rasterize effects and images for broader PDF viewer compatibility. |
| `exportPdfWithUnderlayer` and underlayer settings | PDF | Generate print underlayers for production workflows that require them. |
| `frameRate`, bitrate, and H.264 settings | MP4 | Tune video export quality, size, and encoding behavior. |
Check `engine.editor.getMaxExportSize()` before large raster exports so the
requested dimensions stay within the device-supported export limit.
Use `targetWidth` and `targetHeight` for explicit pixel dimensions; changing
`scene/dpi` does not replace those pixel-size controls.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To Base64"
description: "Convert CE.SDK exports to Base64-encoded strings for embedding in URLs, storing in databases, or transmitting via APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [To Base64](https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/)
---
```kotlin file=@cesdk_android_examples/engine-guides-conversion-to-base64/ToBase64.kt reference-only
import android.app.Application
import android.util.Base64
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
suspend fun exportDesignToBase64(
application: Application,
license: String?,
userId: String,
): List = withContext(Dispatchers.Main) {
Engine.init(application)
val engine = Engine.getInstance(id = "ly.img.engine.to-base64-guide")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1080)
try {
val scene = engine.scene.create()
val page = createSamplePage(engine)
engine.block.appendChild(parent = scene, child = page)
val mimeType = MimeType.PNG
val buffer = engine.block.export(block = page, mimeType = mimeType)
val base64 = buffer.toBase64()
val dataUri = "data:${mimeType.key};base64,$base64"
val inlineImageSource = "data:${mimeType.key};base64,$base64"
// Use inlineImageSource wherever your app expects a URI string,
// for example in a WebView, JSON payload, or HTML email template.
val jpegOptions = ExportOptions(jpegQuality = 0.7F, targetWidth = 720F)
val jpegBuffer = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = jpegOptions,
)
val jpegDataUri = jpegBuffer.toDataUri(MimeType.JPEG)
val compressedPngOptions = ExportOptions(pngCompressionLevel = 9, targetWidth = 720F)
val compressedPngBuffer = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = compressedPngOptions,
)
val compressedPngDataUri = compressedPngBuffer.toDataUri(MimeType.PNG)
val secondPage = createSamplePage(engine)
engine.block.appendChild(parent = scene, child = secondPage)
val pages = engine.scene.getPages()
val pageBuffers = engine.block.export(blocks = pages, mimeType = MimeType.PNG)
val pageDataUris = pageBuffers.map { pageBuffer ->
pageBuffer.toDataUri(MimeType.PNG)
}
check(inlineImageSource.startsWith("data:image/png;base64,"))
check(jpegDataUri.startsWith("data:image/jpeg;base64,"))
check(compressedPngDataUri.startsWith("data:image/png;base64,"))
check(pageDataUris.size == pages.size)
check(pageDataUris.all { it.startsWith("data:image/png;base64,") })
listOf(dataUri, jpegDataUri, compressedPngDataUri) + pageDataUris
} finally {
engine.stop()
}
}
private fun ByteBuffer.toBase64(): String {
val copy = asReadOnlyBuffer()
copy.rewind()
val bytes = ByteArray(copy.remaining())
copy.get(bytes)
return Base64.encodeToString(bytes, Base64.NO_WRAP)
}
private fun ByteBuffer.toDataUri(mimeType: MimeType): String = "data:${mimeType.key};base64,${toBase64()}"
private fun createSamplePage(engine: Engine): DesignBlock {
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1080F)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(background, color = Color.fromHex("#F4F0EA"))
engine.block.appendChild(parent = page, child = background)
engine.block.fillParent(background)
val text = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(text, text = "Base64 export")
engine.block.setPositionX(text, value = 96F)
engine.block.setPositionY(text, value = 456F)
engine.block.setWidth(text, value = 888F)
engine.block.setTextFontSize(text, fontSize = 96F)
engine.block.setTextColor(text, color = Color.fromHex("#23201D"))
engine.block.appendChild(parent = page, child = text)
return page
}
```
Convert CE.SDK exports to Base64-encoded strings for embedding in HTML, storing in databases, or transmitting via JSON APIs.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-conversion-to-base64)
Base64 encoding transforms binary image data into ASCII text. On Android, CE.SDK's `engine.block.export()` returns a `ByteBuffer`; convert that buffer to a Base64 string with Android's `Base64` API, then prepend the MIME type when you need a data URI.
## Export a Block to Base64
Export a design block as PNG and convert the resulting buffer to a Base64 string.
```kotlin highlight-android-export-base64
val mimeType = MimeType.PNG
val buffer = engine.block.export(block = page, mimeType = mimeType)
val base64 = buffer.toBase64()
val dataUri = "data:${mimeType.key};base64,$base64"
```
The export buffer contains the rendered image. `Base64.NO_WRAP` keeps the encoded output on one line, which is required for data URIs and JSON string values.
## Convert Buffer to Base64
Copy the readable bytes out of the `ByteBuffer`, then encode them as Base64 text.
```kotlin highlight-android-convert-buffer
private fun ByteBuffer.toBase64(): String {
val copy = asReadOnlyBuffer()
copy.rewind()
val bytes = ByteArray(copy.remaining())
copy.get(bytes)
return Base64.encodeToString(bytes, Base64.NO_WRAP)
}
private fun ByteBuffer.toDataUri(mimeType: MimeType): String = "data:${mimeType.key};base64,${toBase64()}"
```
The helper uses a read-only copy so converting the buffer does not consume the original buffer's position for later file writes or checks.
## Create a Data URI
Construct a data URI by combining the MIME type with the Base64 string. Data URIs embed image data directly in HTML, CSS, or WebView content without separate file references.
```kotlin highlight-android-data-uri
val inlineImageSource = "data:${mimeType.key};base64,$base64"
// Use inlineImageSource wherever your app expects a URI string,
// for example in a WebView, JSON payload, or HTML email template.
```
The resulting string follows the format `data:image/png;base64,...`. Use `MimeType.key` so the URI prefix always matches the export format.
## Work with Different MIME Types
CE.SDK supports multiple image formats, each with format-specific quality options through `ExportOptions`.
```kotlin highlight-android-mime-types
val jpegOptions = ExportOptions(jpegQuality = 0.7F, targetWidth = 720F)
val jpegBuffer = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = jpegOptions,
)
val jpegDataUri = jpegBuffer.toDataUri(MimeType.JPEG)
val compressedPngOptions = ExportOptions(pngCompressionLevel = 9, targetWidth = 720F)
val compressedPngBuffer = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = compressedPngOptions,
)
val compressedPngDataUri = compressedPngBuffer.toDataUri(MimeType.PNG)
```
| Format | Option | Default | Notes |
|--------|--------|---------|-------|
| PNG | `pngCompressionLevel` | `5` | Lossless, supports transparency |
| JPEG | `jpegQuality` | `0.9` | Lossy, smaller file size, no transparency |
> **Note:** Base64 increases data size by approximately 33%. For images larger than 100KB, consider storing the raw bytes or a file URI instead.
## Batch Process Multiple Pages
Export all pages in a scene to Base64 strings with the batch export API. Pass the full page list to `engine.block.export(blocks = pages, mimeType = MimeType.PNG)`, then encode each returned buffer.
```kotlin highlight-android-batch
val pages = engine.scene.getPages()
val pageBuffers = engine.block.export(blocks = pages, mimeType = MimeType.PNG)
val pageDataUris = pageBuffers.map { pageBuffer ->
pageBuffer.toDataUri(MimeType.PNG)
}
```
The batch API returns one buffer per input block in the same order, so keep the page list and encoded strings aligned when you persist metadata.
## When to Use Base64
Base64 encoding is useful for:
- Embedding images in HTML email templates or WebView content
- Storing image data in text-only databases or `SharedPreferences`
- Transmitting images through JSON APIs that don't support binary data
- Creating inline data URIs for CSS backgrounds
For large images or file storage, write the buffer bytes directly to disk instead of expanding them into Base64 text.
## Troubleshooting
**Base64 string too long** — Use JPEG with a lower `jpegQuality`, reduce dimensions with `targetWidth` and `targetHeight`, or store the raw bytes when your API accepts binary data.
**Image not displaying** — Verify the data URI starts with `data:${mimeType.key};base64,` and that the Base64 payload was not truncated during storage or transport.
**Memory pressure with batch exports** — Keep batch exports scoped to the pages or blocks you need. Convert each `ByteBuffer` through a read-only copy so checks or later writes do not depend on a consumed buffer position.
## API Reference
| API | Purpose |
|-----|---------|
| `engine.block.export(block=_, mimeType=_, options=_)` | Export one `DesignBlock` to a `ByteBuffer` with image format and size options |
| `engine.block.export(blocks=_, mimeType=_, options=_)` | Export multiple blocks and receive one `ByteBuffer` per block in input order |
| `engine.scene.getPages()` | Get the scene pages for page-by-page batch exports |
| `ExportOptions(jpegQuality=_, pngCompressionLevel=_, targetWidth=_, targetHeight=_)` | Configure compression quality and output dimensions for supported image exports |
| `Base64.encodeToString(input=_, flags=Base64.NO_WRAP)` | Encode exported bytes as a single-line Base64 string |
## Next Steps
- [Export Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Explore all available export formats and configuration
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) — Generate PDFs for print and document workflows
- [Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/) — Understand and configure limits on exported file dimensions or data size
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To Blob"
description: "Export CE.SDK design blocks to Android ByteBuffer data for saving, uploading, or sharing."
platform: android
url: "https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [To Blob](https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/)
---
```kotlin file=@cesdk_android_examples/engine-guides-to-blob/ToBlob.kt reference-only
import android.app.Application
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
data class ToBlobResult(
val pngData: ByteBuffer,
val jpegData: ByteBuffer,
val pageExports: List,
val savedPngFile: File,
)
fun toBlob(
application: Application,
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
outputDir: File,
): Deferred = CoroutineScope(Dispatchers.Main).async {
val engine = startToBlobEngine(
application = application,
license = license,
userId = userId,
)
try {
val pages = createExportScene(engine)
val page = pages.first()
val pngData = exportBlockToBinaryData(engine, page).copyForVerification()
val jpegData = exportWithOptions(engine, page).copyForVerification()
val pageExports = exportMultipleBlocks(engine).map(ByteBuffer::copyForVerification)
val savedPngFile = saveByteBufferToFile(
buffer = pngData,
outputFile = File(outputDir, "to-blob-page.png"),
)
ToBlobResult(
pngData = pngData,
jpegData = jpegData,
pageExports = pageExports,
savedPngFile = savedPngFile,
)
} finally {
engine.stop()
}
}
suspend fun startToBlobEngine(
application: Application,
license: String?,
userId: String,
): Engine {
Engine.init(application)
val engine = Engine.getInstance(id = "ly.img.engine.toBlob")
try {
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
return engine
} catch (error: Throwable) {
engine.stop()
throw error
}
}
private fun ByteBuffer.copyForVerification(): ByteBuffer {
val duplicate = asReadOnlyBuffer()
val bytes = ByteArray(duplicate.remaining())
duplicate.get(bytes)
return ByteBuffer.wrap(bytes).asReadOnlyBuffer()
}
private fun createExportScene(engine: Engine): List {
val scene = engine.scene.create()
return List(2) { pageIndex ->
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
addPageContent(engine, page, pageIndex)
page
}
}
private fun addPageContent(
engine: Engine,
page: DesignBlock,
pageIndex: Int,
) {
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, "To Blob background")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 1280F)
engine.block.setHeight(background, value = 720F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = background,
color = Color.fromHex("#FFF8FAFC"),
)
engine.block.appendChild(parent = page, child = background)
val panel = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(panel, "To Blob export panel")
engine.block.setShape(panel, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(panel, value = 820F)
engine.block.setHeight(panel, value = 420F)
engine.block.setPositionX(panel, value = 230F)
engine.block.setPositionY(panel, value = 150F)
engine.block.setFill(panel, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = panel,
color = if (pageIndex == 0) Color.fromHex("#FF1F6FEB") else Color.fromHex("#FFCF3E53"),
)
engine.block.appendChild(parent = page, child = panel)
val stripe = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(stripe, "To Blob accent stripe")
engine.block.setShape(stripe, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(stripe, value = 820F)
engine.block.setHeight(stripe, value = 72F)
engine.block.setPositionX(stripe, value = 230F)
engine.block.setPositionY(stripe, value = 498F)
engine.block.setFill(stripe, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = stripe,
color = Color.fromHex("#FF111827"),
)
engine.block.appendChild(parent = page, child = stripe)
}
suspend fun exportBlockToBinaryData(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
check(pngData.hasRemaining()) { "PNG export is empty" }
return pngData
}
suspend fun exportWithOptions(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(
jpegQuality = 0.8F,
targetWidth = 1920F,
targetHeight = 1080F,
)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "JPEG export is empty" }
return jpegData
}
suspend fun exportMultipleBlocks(engine: Engine): List {
val pages = engine.scene.getPages()
val pngBuffers = engine.block.export(
blocks = pages,
mimeType = MimeType.PNG,
)
check(pngBuffers.size == pages.size)
pngBuffers.forEachIndexed { index, pngData ->
check(pngData.hasRemaining()) { "PNG export ${index + 1} is empty" }
}
return pngBuffers
}
suspend fun saveByteBufferToFile(
buffer: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableBuffer = buffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
check(outputFile.length() > 0L) { "Saved export is empty" }
outputFile
}
```
Export design blocks to binary `ByteBuffer` data for saving to disk, uploading to a server, sharing, or converting to platform images.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-to-blob)
CE.SDK's `engine.block.export()` method renders a page, scene, or individual design block into binary data. Android returns a `ByteBuffer`, so your app can write the result to app-controlled storage, upload it, share it, or decode image exports with Android platform APIs.
## Export a Block to Binary Data
Call `engine.block.export(...)` with the block ID and target MIME type. This example exports an existing page to PNG and checks that the returned `ByteBuffer` contains data.
```kotlin highlight-android-export-png
suspend fun exportBlockToBinaryData(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
check(pngData.hasRemaining()) { "PNG export is empty" }
return pngData
}
```
Supported export MIME type constants include `MimeType.PNG`, `MimeType.JPEG`, `MimeType.TGA`, `MimeType.SVG`, `MimeType.PDF`, and `MimeType.BINARY`. Use `engine.block.exportVideo(...)` for MP4 video output.
## Configure Export Options
Pass `ExportOptions` when you need to control format-specific quality or output dimensions. This JPEG example uses `jpegQuality = 0.8F` and scales the rendered output to `1920 x 1080`.
```kotlin highlight-android-export-options
suspend fun exportWithOptions(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(
jpegQuality = 0.8F,
targetWidth = 1920F,
targetHeight = 1080F,
)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "JPEG export is empty" }
return jpegData
}
```
Format-specific options are ignored by other formats. For example, `jpegQuality` only affects JPEG exports, while `targetWidth` and `targetHeight` resize image output when both dimensions are set.
## Export Multiple Blocks
Use the batch overload when you need one binary export per page or block. The returned list follows the input order and the export reuses one worker engine for the batch.
```kotlin highlight-android-export-multiple
suspend fun exportMultipleBlocks(engine: Engine): List {
val pages = engine.scene.getPages()
val pngBuffers = engine.block.export(
blocks = pages,
mimeType = MimeType.PNG,
)
check(pngBuffers.size == pages.size)
pngBuffers.forEachIndexed { index, pngData ->
check(pngData.hasRemaining()) { "PNG export ${index + 1} is empty" }
}
return pngBuffers
}
```
## Save to Disk
Use a duplicate or read-only view before writing a `ByteBuffer` if the same buffer is also needed for later validation, upload, or decoding.
```kotlin highlight-android-save-to-file
suspend fun saveByteBufferToFile(
buffer: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableBuffer = buffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
check(outputFile.length() > 0L) { "Saved export is empty" }
outputFile
}
```
The sample writes into an app-controlled `File`. Use the same binary data with your own upload or share pipeline when the exported file should leave local storage.
## API Reference
| API | Purpose |
| --- | --- |
| `engine.block.export(block=_, mimeType=_, options=_)` | Export one block as a `ByteBuffer` |
| `engine.block.export(blocks=_, mimeType=_, options=_)` | Export several blocks as a `List` |
| `ExportOptions(jpegQuality=_, targetWidth=_, targetHeight=_)` | Configure output quality and dimensions |
| `engine.scene.getPages()` | Get pages for batch export examples |
## Next Steps
- [Conversion Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) - See all supported export formats.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To PNG"
description: "Export designs and images to PNG format with compression settings and target dimensions using CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/conversion/to-png-f1660c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [To PNG](https://img.ly/docs/cesdk/android/conversion/to-png-f1660c/)
---
```kotlin file=@cesdk_android_examples/engine-guides-conversion-to-png/ConversionToPng.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.nio.ByteBuffer
data class PngExport(
val label: String,
val pngData: ByteBuffer,
)
data class ConversionToPngResult(
val singlePage: PngExport,
val allPages: List,
val compressed: PngExport,
val targetDimensions: PngExport,
val textOverhang: PngExport,
) {
val allExports: List
get() = listOf(singlePage) +
allPages +
listOf(compressed, targetDimensions, textOverhang)
}
suspend fun conversionToPng(engine: Engine): ConversionToPngResult = withContext(engine.dispatcher) {
createSceneWithPages(engine)
val currentPage = engine.scene.getCurrentPage() ?: engine.scene.getPages().first()
ConversionToPngResult(
singlePage = PngExport("single page", exportSinglePage(engine, currentPage).readOnlyForVerification()),
allPages = exportAllPages(engine).mapIndexed { index, pngData ->
PngExport("page ${index + 1}", pngData.readOnlyForVerification())
},
compressed = PngExport("compressed", exportWithCompression(engine, currentPage).readOnlyForVerification()),
targetDimensions = PngExport(
"target dimensions",
exportWithTargetDimensions(engine, currentPage).readOnlyForVerification(),
),
textOverhang = PngExport(
"text overhang",
exportWithTextOverhang(engine, currentPage).readOnlyForVerification(),
),
)
}
private fun ByteBuffer.readOnlyForVerification(): ByteBuffer = asReadOnlyBuffer()
private fun createSceneWithPages(engine: Engine): DesignBlock {
val scene = engine.scene.create()
repeat(2) { pageIndex ->
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
addVisiblePageContent(engine, page, pageIndex)
}
return scene
}
private fun addVisiblePageContent(
engine: Engine,
page: DesignBlock,
pageIndex: Int,
) {
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, "PNG export background")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 800F)
engine.block.setHeight(background, value = 600F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = background,
color = Color.fromHex("#FFFFFBF1"),
)
engine.block.appendChild(parent = page, child = background)
val accent = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(accent, "PNG export accent")
engine.block.setShape(accent, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(accent, value = 520F)
engine.block.setHeight(accent, value = 280F)
engine.block.setPositionX(accent, value = 140F)
engine.block.setPositionY(accent, value = 150F)
engine.block.setFill(accent, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = accent,
color = if (pageIndex == 0) Color.fromHex("#FF2457D6") else Color.fromHex("#FFE15D2A"),
)
engine.block.appendChild(parent = page, child = accent)
val label = engine.block.create(DesignBlockType.Text)
engine.block.setName(label, "PNG export text")
engine.block.setPositionX(label, value = 190F)
engine.block.setPositionY(label, value = 245F)
engine.block.setWidthMode(label, mode = SizeMode.AUTO)
engine.block.setHeightMode(label, mode = SizeMode.AUTO)
engine.block.replaceText(label, text = "PNG ${pageIndex + 1}")
engine.block.setTextFontSize(label, fontSize = 88F)
engine.block.setTextColor(label, color = Color.fromHex("#FFFFFFFF"))
engine.block.appendChild(parent = page, child = label)
// This fixed frame gives the allowTextOverhang export option real text glyphs to preserve.
val overhangText = engine.block.create(DesignBlockType.Text)
engine.block.setName(overhangText, "Text overhang sample")
engine.block.setPositionX(overhangText, value = 84F)
engine.block.setPositionY(overhangText, value = 438F)
engine.block.setWidth(overhangText, value = 260F)
engine.block.setHeight(overhangText, value = 56F)
engine.block.replaceText(overhangText, text = "Jolly glyphs")
engine.block.setTextFontSize(overhangText, fontSize = 72F)
engine.block.setTextColor(overhangText, color = Color.fromHex("#FF0B1220"))
engine.block.appendChild(parent = page, child = overhangText)
}
suspend fun exportSinglePage(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
check(pngData.hasRemaining()) { "single page PNG export is empty" }
pngData
}
suspend fun exportAllPages(engine: Engine): List = withContext(engine.dispatcher) {
val pages = engine.scene.getPages()
val pngFiles = engine.block.export(
blocks = pages,
mimeType = MimeType.PNG,
)
check(pngFiles.size == pages.size)
pngFiles.forEachIndexed { index, pngData ->
check(pngData.hasRemaining()) { "page ${index + 1} PNG export is empty" }
}
pngFiles
}
suspend fun exportWithCompression(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val options = ExportOptions(pngCompressionLevel = 9)
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(pngData.hasRemaining()) { "compressed PNG export is empty" }
pngData
}
suspend fun exportWithTargetDimensions(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val options = ExportOptions(
targetWidth = 1200F,
targetHeight = 900F,
)
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(pngData.hasRemaining()) { "target dimensions PNG export is empty" }
pngData
}
suspend fun exportWithTextOverhang(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val options = ExportOptions(allowTextOverhang = true)
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(pngData.hasRemaining()) { "text overhang PNG export is empty" }
pngData
}
```
Export designs to PNG format with lossless quality and optional transparency support.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-conversion-to-png)
PNG is a lossless image format that preserves image quality and supports transparency. It's ideal for designs requiring pixel-perfect fidelity, logos, graphics with transparent backgrounds, and any content where quality cannot be compromised.
This guide covers how to export designs to PNG and configure export options using the Android Engine API.
## Export to PNG
Use `engine.block.export(...)` with `MimeType.PNG` to export a design block to PNG. The method returns a `ByteBuffer` containing the image data.
```kotlin highlight-android-export-single-page
suspend fun exportSinglePage(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
check(pngData.hasRemaining()) { "single page PNG export is empty" }
pngData
}
```
## Export All Pages
Export all pages in a scene using the batch export API. Pass the page list to `engine.block.export(...)`, which returns one `ByteBuffer` for each page in the same order.
```kotlin highlight-android-export-all-pages
suspend fun exportAllPages(engine: Engine): List = withContext(engine.dispatcher) {
val pages = engine.scene.getPages()
val pngFiles = engine.block.export(
blocks = pages,
mimeType = MimeType.PNG,
)
check(pngFiles.size == pages.size)
pngFiles.forEachIndexed { index, pngData ->
check(pngData.hasRemaining()) { "page ${index + 1} PNG export is empty" }
}
pngFiles
}
```
The batch API reuses a single worker engine for all exports, making it more memory efficient than exporting pages individually.
## Compression Level
Control the file size versus export speed tradeoff using `pngCompressionLevel` in `ExportOptions`. Valid values are 0-9, where higher values produce smaller files but take longer to export. Since PNG is lossless, image quality remains unchanged.
```kotlin highlight-android-compression-level
suspend fun exportWithCompression(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val options = ExportOptions(pngCompressionLevel = 9)
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(pngData.hasRemaining()) { "compressed PNG export is empty" }
pngData
}
```
The default compression level is 5, providing a good balance between file size and export speed.
## Target Dimensions
Resize the output by setting `targetWidth` and `targetHeight`. The block scales to fill the target dimensions while maintaining its aspect ratio.
```kotlin highlight-android-target-dimensions
suspend fun exportWithTargetDimensions(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val options = ExportOptions(
targetWidth = 1200F,
targetHeight = 900F,
)
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(pngData.hasRemaining()) { "target dimensions PNG export is empty" }
pngData
}
```
Set both values when you need predictable output dimensions. Leave both values `null` to use the block's native size.
## Text Overhang
Decorative fonts sometimes have glyphs that extend beyond their frame. Set `allowTextOverhang` to `true` to prevent clipping these glyphs during export.
```kotlin highlight-android-text-overhang
suspend fun exportWithTextOverhang(
engine: Engine,
page: DesignBlock,
): ByteBuffer = withContext(engine.dispatcher) {
val options = ExportOptions(allowTextOverhang = true)
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(pngData.hasRemaining()) { "text overhang PNG export is empty" }
pngData
}
```
## Troubleshooting
| Issue | Fix |
| --- | --- |
| Large PNG files | Increase `pngCompressionLevel` toward `9` to reduce file size. Higher compression can take longer, but PNG quality remains lossless. |
| Wrong output dimensions | Set `targetWidth` and `targetHeight` together when you need predictable output dimensions. Leave both values `null` only when you want the block's native size. |
| Decorative text appears clipped | Set `allowTextOverhang` to `true` when fonts extend outside their text frame. |
## API Reference
| API | Description |
| --- | --- |
| `engine.block.export(block=_, mimeType=MimeType.PNG, options=_)` | Exports a single block to `ByteBuffer` with the specified options |
| `engine.block.export(blocks=_, mimeType=MimeType.PNG, options=_)` | Exports multiple blocks, returning one `ByteBuffer` per block |
| `engine.scene.getCurrentPage()` | Returns the current page block ID or `null` |
| `engine.scene.getPages()` | Returns all page block IDs in the scene |
### Key Options
| Option | Description |
| --- | --- |
| `pngCompressionLevel` | Controls PNG compression from `0` to `9` |
| `targetWidth` and `targetHeight` | Resize the export when both dimensions are set |
| `allowTextOverhang` | Allows glyphs to extend beyond text frames during export |
## Next Steps
- [Conversion Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) - Learn about other export formats
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Understand the full export workflow
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Audio"
description: "Create audio blocks, extract tracks from video, control playback, generate waveforms, and manage audio timing in CE.SDK for Android."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-audio-audio/Audio.kt reference-only
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.flow.toList
import ly.img.engine.AudioFromVideoOptions
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import java.io.ByteArrayOutputStream
import kotlin.math.abs
private const val TAG = "AudioGuide"
suspend fun audio(engine: Engine): String {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 12.0)
val audioBlock = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = audioBlock)
engine.block.setUri(
block = audioBlock,
property = "audio/fileURI",
value = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a"),
)
engine.block.forceLoadAVResource(audioBlock)
val resourceDuration = engine.block.getAVResourceTotalDuration(audioBlock)
val videoBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"),
)
engine.block.setFill(videoBlock, fill = videoFill)
engine.block.appendChild(parent = page, child = videoBlock)
engine.block.forceLoadAVResource(videoFill)
val trackCountBeforeExtraction = engine.block.getAudioTrackCountFromVideo(videoFill)
check(trackCountBeforeExtraction > 0) {
"Video source must contain an audio track."
}
val extractedAudioBlock = engine.block.createAudioFromVideo(
videoFill = videoFill,
trackIndex = 0,
options = AudioFromVideoOptions(
keepTrimSettings = true,
muteOriginalVideo = true,
),
)
engine.block.appendChild(parent = page, child = extractedAudioBlock)
val allExtractedAudioBlocks = engine.block.createAudiosFromVideo(
videoFill = videoFill,
options = AudioFromVideoOptions(
keepTrimSettings = true,
muteOriginalVideo = true,
),
)
allExtractedAudioBlocks.forEach { audio ->
engine.block.appendChild(parent = page, child = audio)
}
val audioTrackCount = engine.block.getAudioTrackCountFromVideo(videoFill)
Log.i(TAG, "Video has $audioTrackCount audio track(s).")
engine.block.setPlaybackTime(page, time = 3.0)
val playbackTime = engine.block.getPlaybackTime(page)
engine.block.setPlaying(page, enabled = true)
val playing = engine.block.isPlaying(page)
engine.block.setPlaying(page, enabled = false)
val paused = !engine.block.isPlaying(page)
engine.block.setVolume(audioBlock, volume = 0.7F)
val volume = engine.block.getVolume(audioBlock)
engine.block.setMuted(audioBlock, muted = true)
val muted = engine.block.isMuted(audioBlock)
engine.block.setPlaybackSpeed(audioBlock, speed = 1.25F)
val playbackSpeed = engine.block.getPlaybackSpeed(audioBlock)
engine.block.setSoloPlaybackEnabled(audioBlock, enabled = true)
val soloPlaybackEnabled = engine.block.isSoloPlaybackEnabled(audioBlock)
engine.block.setSoloPlaybackEnabled(audioBlock, enabled = false)
val soloPlaybackDisabled = !engine.block.isSoloPlaybackEnabled(audioBlock)
engine.block.setPlaybackSpeed(audioBlock, speed = 1.0F)
engine.block.setTimeOffset(audioBlock, offset = 2.0)
val timeOffset = engine.block.getTimeOffset(audioBlock)
engine.block.setDuration(audioBlock, duration = 8.0)
engine.block.setTrimOffset(audioBlock, offset = 1.0)
val trimOffset = engine.block.getTrimOffset(audioBlock)
engine.block.setLooping(audioBlock, looping = true)
val looping = engine.block.isLooping(audioBlock)
engine.block.setTrimLength(audioBlock, length = 6.0)
val trimLength = engine.block.getTrimLength(audioBlock)
val blockDuration = engine.block.getDuration(audioBlock)
val waveformChunks = engine.block.generateAudioThumbnailSequence(
block = audioBlock,
samplesPerChunk = 4,
timeBegin = 0.0,
timeEnd = 4.0,
numberOfSamples = 16,
numberOfChannels = 1,
).toList()
val waveformSampleCount = waveformChunks.sumOf { chunk -> chunk.samples.size }
val transientAudioResources = engine.editor.findAllTransientResources()
transientAudioResources.forEach { (transientUri, _) ->
val resourceBytes = ByteArrayOutputStream()
engine.editor.getResourceData(
uri = transientUri,
chunkSize = 64 * 1024,
) { chunk ->
val copy = chunk.duplicate()
val bytes = ByteArray(copy.remaining())
copy.get(bytes)
resourceBytes.write(bytes)
true
}
val permanentUri = uploadTransientAudioResource(
sourceUri = transientUri,
data = resourceBytes.toByteArray(),
)
engine.editor.relocateResource(
currentUri = transientUri,
relocatedUri = permanentUri,
)
}
val remainingTransientAudioResources = engine.editor.findAllTransientResources()
val savedScene = engine.scene.saveToString(
scene = scene,
allowedResourceSchemes = listOf("http", "https"),
)
check(audioBlock != extractedAudioBlock)
check(audioTrackCount > 0)
check(allExtractedAudioBlocks.size == audioTrackCount)
check(abs(playbackTime - 3.0) < 0.001)
check(playing)
check(paused)
check(abs(volume - 0.7F) < 0.001F)
check(muted)
check(abs(playbackSpeed - 1.25F) < 0.001F)
check(soloPlaybackEnabled)
check(soloPlaybackDisabled)
check(abs(timeOffset - 2.0) < 0.001)
check(abs(blockDuration - 8.0) < 0.001)
check(abs(trimOffset - 1.0) < 0.001)
check(abs(trimLength - 6.0) < 0.001)
check(looping)
check(resourceDuration > 0.0)
check(waveformChunks.isNotEmpty())
check(waveformSampleCount > 0)
check(transientAudioResources.isNotEmpty())
check(remainingTransientAudioResources.isEmpty())
check(savedScene.isNotBlank())
return savedScene
}
private fun uploadTransientAudioResource(
sourceUri: Uri,
data: ByteArray,
): Uri {
check(data.isNotEmpty()) { "Cannot persist an empty audio resource." }
// Replace this with your app's storage client and return its permanent URI.
// Transient buffer URIs do not carry stable file names, so the app owns the storage key.
val sourceId = sourceUri.toString().hashCode().toString(radix = 16)
val fileName = "extracted-audio-$sourceId-${data.size}.m4a"
return Uri.parse("https://your-storage.example/audio/$fileName")
}
```
Add audio to video scenes, extract audio from video fills, control playback,
and generate waveform data with CE.SDK for Android.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-audio)
Audio blocks let you add background music, voice-overs, sound effects, and other standalone audio to video scenes. CE.SDK also exposes video audio track counts, playback controls, trim controls, and waveform generation through the Engine block API.
Playback examples require an Engine instance created with `AudioContext.AUTO`, which is the default. Engines created with `AudioContext.NONE` can still edit and export scenes, but they do not play audio.
## Use Cases
Use CE.SDK audio APIs when you need to add or manage:
- Background music
- Voice-overs
- Sound effects
- Podcast or narration tracks
## How Audio Works in CE.SDK
CE.SDK represents standalone audio as `DesignBlockType.Audio` blocks. Audio blocks are attached to a page, reference their media through `audio/fileURI`, and use the same timeline properties as other time-based blocks.
Each audio block can have:
- A source URI for standalone audio files
- Playback properties such as volume, mute state, playback time, and speed
- Timeline properties such as offset, duration, trim offset, and trim length
- Waveform sample data for custom timeline UIs
Extraction APIs create separate audio blocks from video fill tracks instead of changing an existing audio block's source.
### What Are the Time-Based Properties
Audio timing is expressed in seconds:
- **Offset**: when the audio block starts relative to its parent.
- **Duration**: how long the block stays active on the timeline.
- **Trim offset**: where playback starts inside the source audio.
- **Trim length**: how much of the source audio is used before it loops or stops. Use the looping APIs to choose that behavior.
### What Are Waveforms
Waveforms are sampled audio amplitudes that you can render in a custom UI. On Android, `generateAudioThumbnailSequence()` returns a `Flow` of `AudioThumbnailResult` chunks. Each chunk contains normalized samples in the range from 0 to 1. Stereo requests interleave left and right samples.
### When to Create vs. Extract Audio
Create an audio block when the source is an external audio file such as background music or a voice-over. Extract audio when the sound already exists inside a video fill and you need a separate audio block for editing, trimming, or muting the original video.
## Examples
The snippets below use a video scene with an existing page. Audio APIs run on the main thread through the same Engine instance as the rest of your scene edits.
### Create Audio
Create a standalone audio block, attach it to the page, set its source URI, and load the resource before reading media metadata.
```kotlin highlight-android-create-audio
val audioBlock = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = audioBlock)
engine.block.setUri(
block = audioBlock,
property = "audio/fileURI",
value = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a"),
)
engine.block.forceLoadAVResource(audioBlock)
val resourceDuration = engine.block.getAVResourceTotalDuration(audioBlock)
```
### Extract or Count Video Audio
Create a video fill and wait for the suspend `forceLoadAVResource()` API to complete before extracting or counting video audio. The tabs below use this loaded `videoFill`.
```kotlin highlight-android-video-fill-setup
val videoBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"),
)
engine.block.setFill(videoBlock, fill = videoFill)
engine.block.appendChild(parent = page, child = videoBlock)
engine.block.forceLoadAVResource(videoFill)
```
Check that the loaded source contains audio, and extract the first track into a new audio block. `AudioFromVideoOptions` keeps the trim settings and mutes the source video fill.
```kotlin highlight-android-extract-audio
val trackCountBeforeExtraction = engine.block.getAudioTrackCountFromVideo(videoFill)
check(trackCountBeforeExtraction > 0) {
"Video source must contain an audio track."
}
val extractedAudioBlock = engine.block.createAudioFromVideo(
videoFill = videoFill,
trackIndex = 0,
options = AudioFromVideoOptions(
keepTrimSettings = true,
muteOriginalVideo = true,
),
)
engine.block.appendChild(parent = page, child = extractedAudioBlock)
```
Use `createAudiosFromVideo()` when the source may contain multiple audio tracks and each track should become its own audio block.
```kotlin highlight-android-extract-all-audio
val allExtractedAudioBlocks = engine.block.createAudiosFromVideo(
videoFill = videoFill,
options = AudioFromVideoOptions(
keepTrimSettings = true,
muteOriginalVideo = true,
),
)
allExtractedAudioBlocks.forEach { audio ->
engine.block.appendChild(parent = page, child = audio)
}
```
Use `getAudioTrackCountFromVideo()` before extraction when your loaded source may be silent or contain multiple audio tracks.
When your app needs to choose a track by metadata, call `getAudioInfoFromVideo()` after the same load step to read each track's `AudioTrackInfo`, including audio codec, channel count, sample rate, audio duration, packet and frame counts, track name, container track index, and ISO 639-2T language. Pass the returned list position to `createAudioFromVideo(trackIndex=...)`; this parameter is the zero-based audio-track ordinal. Do not pass `AudioTrackInfo.trackIndex` directly, because that value is the original container track index and may not match the list position.
```kotlin highlight-android-track-info
val audioTrackCount = engine.block.getAudioTrackCountFromVideo(videoFill)
Log.i(TAG, "Video has $audioTrackCount audio track(s).")
```
### Control Audio Playback
Playback time and play or pause state are usually controlled on the page so all time-based blocks stay synchronized. Volume, mute state, and playback speed are set on the audio block itself.
```kotlin highlight-android-playback-control
engine.block.setPlaybackTime(page, time = 3.0)
val playbackTime = engine.block.getPlaybackTime(page)
engine.block.setPlaying(page, enabled = true)
val playing = engine.block.isPlaying(page)
engine.block.setPlaying(page, enabled = false)
val paused = !engine.block.isPlaying(page)
engine.block.setVolume(audioBlock, volume = 0.7F)
val volume = engine.block.getVolume(audioBlock)
engine.block.setMuted(audioBlock, muted = true)
val muted = engine.block.isMuted(audioBlock)
engine.block.setPlaybackSpeed(audioBlock, speed = 1.25F)
val playbackSpeed = engine.block.getPlaybackSpeed(audioBlock)
engine.block.setSoloPlaybackEnabled(audioBlock, enabled = true)
val soloPlaybackEnabled = engine.block.isSoloPlaybackEnabled(audioBlock)
engine.block.setSoloPlaybackEnabled(audioBlock, enabled = false)
val soloPlaybackDisabled = !engine.block.isSoloPlaybackEnabled(audioBlock)
```
Audio speed supports values from 0.25 to 3.0 for audio blocks. Changing speed also changes how long the block takes on the timeline.
### Manage Audio Timing
Use offset and duration to place the audio block on the scene timeline. Use trim offset and trim length to choose which part of the source file plays, and set looping when the trimmed source should repeat while the block remains active.
```kotlin highlight-android-timing
engine.block.setPlaybackSpeed(audioBlock, speed = 1.0F)
engine.block.setTimeOffset(audioBlock, offset = 2.0)
val timeOffset = engine.block.getTimeOffset(audioBlock)
engine.block.setDuration(audioBlock, duration = 8.0)
engine.block.setTrimOffset(audioBlock, offset = 1.0)
val trimOffset = engine.block.getTrimOffset(audioBlock)
engine.block.setLooping(audioBlock, looping = true)
val looping = engine.block.isLooping(audioBlock)
engine.block.setTrimLength(audioBlock, length = 6.0)
val trimLength = engine.block.getTrimLength(audioBlock)
val blockDuration = engine.block.getDuration(audioBlock)
```
Load the audio resource before trimming so CE.SDK can read the source duration and metadata.
### Generate Audio Thumbnails
Waveform generation emits a `Flow` of chunks. Choose `samplesPerChunk`, a time range, the total number of samples, and the number of channels you want to render.
```kotlin highlight-android-waveform
val waveformChunks = engine.block.generateAudioThumbnailSequence(
block = audioBlock,
samplesPerChunk = 4,
timeBegin = 0.0,
timeEnd = 4.0,
numberOfSamples = 16,
numberOfChannels = 1,
).toList()
val waveformSampleCount = waveformChunks.sumOf { chunk -> chunk.samples.size }
```
Render the returned sample values in your own timeline or waveform component.
### Save Audio Scenes
The current Android binding does not expose an audio-only export method. Persist audio edits by saving the scene, or export a video page with audio through the video export APIs when you need an MP4 result.
Extracted audio can reference a transient `buffer://` resource. Before calling `saveToString()`, read each transient resource, store it with your app's storage client, then call `relocateResource()` so the scene contains durable URIs only. The default `allowedResourceSchemes` list is `blob`, `bundle`, `file`, `http`, and `https`; `buffer://` is always transient. The sample narrows the list to `http` and `https`, so saving fails if any extracted `buffer://` resource or local `blob`, `bundle`, or `file` resource remains instead of being relocated.
```kotlin highlight-android-save-scene
val transientAudioResources = engine.editor.findAllTransientResources()
transientAudioResources.forEach { (transientUri, _) ->
val resourceBytes = ByteArrayOutputStream()
engine.editor.getResourceData(
uri = transientUri,
chunkSize = 64 * 1024,
) { chunk ->
val copy = chunk.duplicate()
val bytes = ByteArray(copy.remaining())
copy.get(bytes)
resourceBytes.write(bytes)
true
}
val permanentUri = uploadTransientAudioResource(
sourceUri = transientUri,
data = resourceBytes.toByteArray(),
)
engine.editor.relocateResource(
currentUri = transientUri,
relocatedUri = permanentUri,
)
}
val remainingTransientAudioResources = engine.editor.findAllTransientResources()
val savedScene = engine.scene.saveToString(
scene = scene,
allowedResourceSchemes = listOf("http", "https"),
)
```
The upload helper represents your app storage layer. Replace it with a real upload or local persistence implementation that returns a URI your app can load later.
```kotlin highlight-android-upload-helper
private fun uploadTransientAudioResource(
sourceUri: Uri,
data: ByteArray,
): Uri {
check(data.isNotEmpty()) { "Cannot persist an empty audio resource." }
// Replace this with your app's storage client and return its permanent URI.
// Transient buffer URIs do not carry stable file names, so the app owns the storage key.
val sourceId = sourceUri.toString().hashCode().toString(radix = 16)
val fileName = "extracted-audio-$sourceId-${data.size}.m4a"
return Uri.parse("https://your-storage.example/audio/$fileName")
}
```
## API Reference
The table below lists the Android APIs used in the examples above.
| Category | API | Purpose |
| --- | --- | --- |
| Engine audio context | `Engine.getInstance(id=_, audioContext=AudioContext.AUTO)` | Create an Engine instance that can play audio |
| Create blocks | `engine.block.create(blockType=DesignBlockType.Audio)` | Create a standalone audio block |
| Create blocks | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a block that can display the video fill |
| Create blocks | `engine.block.createShape(type=_)` | Create the video block shape |
| Create blocks | `engine.block.createFill(fillType=_)` | Create a video fill for extraction |
| Scene hierarchy | `engine.block.appendChild(parent=_, child=_)` | Attach audio, video, or extracted blocks to the scene hierarchy |
| Assign sources | `engine.block.setUri(block=_, property="audio/fileURI", value=_)` | Attach an audio file URI to an audio block |
| Assign sources | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Attach a video file URI to a video fill |
| Video fill setup | `engine.block.setShape(block=_, shape=_)` | Assign a shape to the video block |
| Video fill setup | `engine.block.setFill(block=_, fill=_)` | Assign the loaded video fill to the video block |
| Extract video audio | `engine.block.createAudioFromVideo(videoFill=_, trackIndex=_, options=_)` | Extract one audio track by zero-based audio-track ordinal from a video fill |
| Extract video audio | `engine.block.createAudiosFromVideo(videoFill=_, options=_)` | Extract every audio track from a video fill |
| Count video audio | `engine.block.getAudioTrackCountFromVideo(videoFill=_)` | Count audio tracks in a video fill |
| Inspect video audio | `engine.block.getAudioInfoFromVideo(videoFill=_)` | Read `AudioTrackInfo` metadata; use the returned list position for extraction because `AudioTrackInfo.trackIndex` is the container track index |
| Playback | `engine.block.setPlaying(block=_, enabled=_)` | Start or stop playback for a page or playable block |
| Playback | `engine.block.isPlaying(block=_)` | Read the current play or pause state |
| Playback | `engine.block.supportsPlaybackControl(block=_)` | Check whether playback control APIs are supported for a block |
| Playback | `engine.block.setPlaybackTime(block=_, time=_)` | Move playback to a timeline position |
| Playback | `engine.block.getPlaybackTime(block=_)` | Read the current playback time |
| Playback | `engine.block.supportsPlaybackTime(block=_)` | Check whether a block exposes a playback time cursor |
| Playback | `engine.block.setVolume(block=_, volume=_)` | Set volume from 0.0 to 1.0 |
| Playback | `engine.block.getVolume(block=_)` | Read the current volume |
| Playback | `engine.block.setMuted(block=_, muted=_)` | Mute or unmute audio |
| Playback | `engine.block.isMuted(block=_)` | Read whether audio is muted |
| Playback | `engine.block.setPlaybackSpeed(block=_, speed=_)` | Set audio speed from 0.25x to 3.0x |
| Playback | `engine.block.getPlaybackSpeed(block=_)` | Read the current playback speed |
| Playback | `engine.block.setSoloPlaybackEnabled(block=_, enabled=_)` | Preview one block while the rest of the scene stays paused |
| Playback | `engine.block.isSoloPlaybackEnabled(block=_)` | Read whether solo playback is enabled for a block |
| Timing | `engine.block.supportsTimeOffset(block=_)` | Check whether a block can be positioned on its parent's timeline |
| Timing | `engine.block.setTimeOffset(block=_, offset=_)` | Move the audio block on the timeline |
| Timing | `engine.block.getTimeOffset(block=_)` | Read where the audio block starts on the timeline |
| Timing | `engine.block.supportsDuration(block=_)` | Check whether a block exposes an active timeline duration |
| Timing | `engine.block.setDuration(block=_, duration=_)` | Set the active block duration |
| Timing | `engine.block.getDuration(block=_)` | Read the active block duration |
| Timing | `engine.block.supportsTrim(block=_)` | Check whether a block or fill exposes trim controls |
| Timing | `engine.block.setTrimOffset(block=_, offset=_)` | Start inside the source audio |
| Timing | `engine.block.getTrimOffset(block=_)` | Read the source trim start |
| Timing | `engine.block.setTrimLength(block=_, length=_)` | Limit the source range used for playback |
| Timing | `engine.block.getTrimLength(block=_)` | Read the source trim length |
| Timing | `engine.block.setLooping(block=_, looping=_)` | Loop the trimmed source while the block is active |
| Timing | `engine.block.isLooping(block=_)` | Read whether the source loops or stops |
| Resources | `engine.block.forceLoadAVResource(block=_)` | Load audio or video metadata before querying it |
| Resources | `engine.block.getAVResourceTotalDuration(block=_)` | Read the loaded audio or video source duration |
| Waveforms | `engine.block.generateAudioThumbnailSequence(block=_, samplesPerChunk=_, timeBegin=_, timeEnd=_, numberOfSamples=_, numberOfChannels=_)` | Generate waveform sample chunks |
| Persistence | `engine.editor.findAllTransientResources()` | Find extracted `buffer://` resources that must be persisted before saving |
| Persistence | `engine.editor.getResourceData(uri=_, chunkSize=_, onData=_)` | Read transient resource bytes for app storage |
| Persistence | `engine.editor.relocateResource(currentUri=_, relocatedUri=_)` | Replace a transient URI with a durable URI |
| Persistence | `engine.scene.saveToString(scene=_, allowedResourceSchemes=_)` | Serialize only scenes whose resources use allowed durable schemes |
## Next Steps
- [CE.SDK API Reference](https://img.ly/docs/cesdk/android/api-reference/overview-8f24e1/) - Review the full API surface.
- [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects using CE.SDK's audio block system.
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Create seamless repeating audio playback for background music and sound effects using CE.SDK's audio looping system.
- [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/) - Learn how to adjust audio playback speed in CE.SDK to create slow-motion, time-stretched, and fast-forward audio effects.
---
## Related Pages
- [Add Sound Effects](https://img.ly/docs/cesdk/android/create-audio/audio/add-sound-effects-9e984e/) - Learn how to use buffers with arbitrary data to generate sound effects programmatically.
- [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) - Add background music and audio tracks to Android video scenes using CE.SDK audio blocks.
- [Record Voiceover](https://img.ly/docs/cesdk/android/create-audio/audio/record-voiceover-07e8e1/) - Let users record voiceover clips directly in the Android editor UI.
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) - Learn how to adjust audio volume in CE.SDK for Android to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/) - Control audio playback speed from quarter-speed (0.25x) to triple-speed (3.0x) using the CE.SDK Android Engine API.
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) - Control audio looping behavior programmatically using CE.SDK's Android Engine API for audio processing and automated content workflows.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add Music"
description: "Add background music and audio tracks to Android video scenes using CE.SDK audio blocks."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) > [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-audio-add-music/AddMusic.kt reference-only
import android.net.Uri
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FindAssetsQuery
import kotlin.math.abs
data class AddMusic(
val pageWidth: Float,
val pageHeight: Float,
val pageDurationSeconds: Double,
val musicUri: Uri,
val timeOffsetSeconds: Double,
val durationSeconds: Double,
val computedMusicDurationSeconds: Double,
val volume: Float,
val availableAssetCount: Int,
val secondTrackUri: Uri,
val secondTrackTimeOffsetSeconds: Double,
val secondTrackDurationSeconds: Double,
val secondTrackVolume: Float,
val audioBlockCountAfterCleanup: Int,
)
suspend fun addMusic(engine: Engine): AddMusic {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1920F)
engine.block.setHeight(page, value = 1080F)
engine.block.setDuration(page, duration = 30.0)
val music = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = music)
val musicUri = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.audio/audios/far_from_home.m4a")
engine.block.setString(block = music, property = "audio/fileURI", value = musicUri.toString())
check(engine.block.getString(block = music, property = "audio/fileURI") == musicUri.toString())
engine.block.forceLoadAVResource(block = music)
val sourceDuration = engine.block.getAVResourceTotalDuration(block = music)
val musicDuration = minOf(sourceDuration, 30.0)
engine.block.setTimeOffset(block = music, offset = 0.0)
engine.block.setDuration(block = music, duration = musicDuration)
engine.block.setVolume(block = music, volume = 0.8F)
val musicVolume = engine.block.getVolume(block = music)
check(abs(musicVolume - 0.8F) < 0.001F)
val audioSourceId = "ly.img.audio"
val demoAssetsBaseUri = Uri.parse("https://cdn.img.ly/assets/demo/v3")
if (audioSourceId !in engine.asset.findAllSources()) {
engine.asset.addLocalSourceFromJSON(
contentUri = demoAssetsBaseUri.buildUpon()
.appendPath(audioSourceId)
.appendPath("content.json")
.build(),
)
}
val audioAssets = engine.asset.findAssets(
sourceId = audioSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
)
val secondAudioAsset = audioAssets.assets.first { it.id.endsWith("dance_harder") }
val secondAudioUri = Uri.parse(requireNotNull(secondAudioAsset.meta?.get("uri")))
val secondAudio = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = secondAudio)
engine.block.setString(block = secondAudio, property = "audio/fileURI", value = secondAudioUri.toString())
engine.block.forceLoadAVResource(block = secondAudio)
val secondAudioDuration = engine.block.getAVResourceTotalDuration(block = secondAudio)
// Start the secondary track inside the 30-second page so it is audible in this Android sample.
engine.block.setTimeOffset(block = secondAudio, offset = 10.0)
val secondAudioTimeOffset = engine.block.getTimeOffset(secondAudio)
engine.block.setDuration(block = secondAudio, duration = minOf(secondAudioDuration, 15.0))
val secondAudioPlaybackDuration = engine.block.getDuration(secondAudio)
engine.block.setVolume(block = secondAudio, volume = 0.5F)
val secondAudioVolume = engine.block.getVolume(secondAudio)
val audioBlocks = engine.block.findByType(DesignBlockType.Audio)
audioBlocks.forEach { audioBlock ->
println(
"Audio starts at ${engine.block.getTimeOffset(audioBlock)}s " +
"with volume ${engine.block.getVolume(audioBlock)}",
)
}
engine.block.destroy(secondAudio)
val remainingAudioBlocks = engine.block.findByType(DesignBlockType.Audio)
check(music in remainingAudioBlocks)
check(secondAudio !in remainingAudioBlocks)
return AddMusic(
pageWidth = engine.block.getWidth(page),
pageHeight = engine.block.getHeight(page),
pageDurationSeconds = engine.block.getDuration(page),
musicUri = musicUri,
timeOffsetSeconds = engine.block.getTimeOffset(music),
durationSeconds = engine.block.getDuration(music),
computedMusicDurationSeconds = musicDuration,
volume = musicVolume,
availableAssetCount = audioAssets.total,
secondTrackUri = secondAudioUri,
secondTrackTimeOffsetSeconds = secondAudioTimeOffset,
secondTrackDurationSeconds = secondAudioPlaybackDuration,
secondTrackVolume = secondAudioVolume,
audioBlockCountAfterCleanup = remainingAudioBlocks.size,
)
}
```
Add background music and audio tracks to video projects using CE.SDK audio
blocks.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-add-music)
Audio blocks are standalone time-based blocks that play alongside video content, independent of video fills. You can set an audio source URI, position the block in the timeline, configure volume, query audio assets, and layer multiple audio blocks in the same video scene.
This guide focuses on the Android Engine API. It assumes you already have an `Engine` instance and want to add music to an existing or newly created video scene.
## Creating a Video Scene
Create a video scene with a page that owns the timeline. The page duration defines the composition length that audio blocks play within.
```kotlin highlight-android-create-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1920F)
engine.block.setHeight(page, value = 1080F)
engine.block.setDuration(page, duration = 30.0)
```
Audio blocks must be children of a page. Set a page duration before adding audio so the timeline has a clear playback range.
## Programmatic Audio Creation
### Create Audio Block
Create an audio block with `DesignBlockType.Audio`, append it to the page, and assign the source file URI through the `audio/fileURI` property.
```kotlin highlight-android-create-audio-block
val music = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = music)
val musicUri = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.audio/audios/far_from_home.m4a")
engine.block.setString(block = music, property = "audio/fileURI", value = musicUri.toString())
```
Audio blocks support common audio formats including M4A, MP3, and WAV. The source URI can point to a network resource or a URI your app can resolve.
### Configure Time Position
Load the audio resource before reading its metadata. Then set the block's time offset and duration in seconds.
```kotlin highlight-android-configure-timeline
engine.block.forceLoadAVResource(block = music)
val sourceDuration = engine.block.getAVResourceTotalDuration(block = music)
val musicDuration = minOf(sourceDuration, 30.0)
engine.block.setTimeOffset(block = music, offset = 0.0)
engine.block.setDuration(block = music, duration = musicDuration)
```
`setTimeOffset()` controls when the music starts relative to the page timeline. `setDuration()` controls how long the block remains active during playback.
### Configure Volume
Set volume on the audio block with a value from 0.0 for silent playback to 1.0 for full volume.
```kotlin highlight-android-configure-volume
engine.block.setVolume(block = music, volume = 0.8F)
val musicVolume = engine.block.getVolume(block = music)
```
Volume changes affect preview and export. Use lower values for background music when other audio remains important.
## Working with Audio Assets
### Query Audio Assets
Use the Asset API to query the demo audio source. The sample registers the `ly.img.audio` source with `engine.asset.addLocalSourceFromJSON(...)` before querying it, which is the same source the editor uses for demo music.
```kotlin highlight-android-query-audio-assets
val audioSourceId = "ly.img.audio"
val demoAssetsBaseUri = Uri.parse("https://cdn.img.ly/assets/demo/v3")
if (audioSourceId !in engine.asset.findAllSources()) {
engine.asset.addLocalSourceFromJSON(
contentUri = demoAssetsBaseUri.buildUpon()
.appendPath(audioSourceId)
.appendPath("content.json")
.build(),
)
}
val audioAssets = engine.asset.findAssets(
sourceId = audioSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
)
val secondAudioAsset = audioAssets.assets.first { it.id.endsWith("dance_harder") }
val secondAudioUri = Uri.parse(requireNotNull(secondAudioAsset.meta?.get("uri")))
```
Each asset can carry metadata such as a URI, duration, MIME type, and tags. Use that metadata to build a custom picker or to select music programmatically.
## Adding Multiple Audio Tracks
Create additional audio blocks when a scene needs more than one music, voiceover, or sound-effect track. Each block has independent timing, duration, and volume.
```kotlin highlight-android-add-second-track
val secondAudio = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = secondAudio)
engine.block.setString(block = secondAudio, property = "audio/fileURI", value = secondAudioUri.toString())
engine.block.forceLoadAVResource(block = secondAudio)
val secondAudioDuration = engine.block.getAVResourceTotalDuration(block = secondAudio)
// Start the secondary track inside the 30-second page so it is audible in this Android sample.
engine.block.setTimeOffset(block = secondAudio, offset = 10.0)
val secondAudioTimeOffset = engine.block.getTimeOffset(secondAudio)
engine.block.setDuration(block = secondAudio, duration = minOf(secondAudioDuration, 15.0))
val secondAudioPlaybackDuration = engine.block.getDuration(secondAudio)
engine.block.setVolume(block = secondAudio, volume = 0.5F)
val secondAudioVolume = engine.block.getVolume(secondAudio)
```
Balance multiple tracks by keeping the primary track louder and lowering secondary tracks when they should sit behind it.
## Managing Audio Blocks
### List Audio Blocks
Use `findByType(DesignBlockType.Audio)` to retrieve all audio blocks in the scene.
```kotlin highlight-android-list-audio-blocks
val audioBlocks = engine.block.findByType(DesignBlockType.Audio)
audioBlocks.forEach { audioBlock ->
println(
"Audio starts at ${engine.block.getTimeOffset(audioBlock)}s " +
"with volume ${engine.block.getVolume(audioBlock)}",
)
}
```
Listing audio blocks is useful for custom timeline controls, validation, or batch changes across a composition.
### Remove Audio
Destroy audio blocks that are no longer part of the composition.
```kotlin highlight-android-remove-audio
engine.block.destroy(secondAudio)
val remainingAudioBlocks = engine.block.findByType(DesignBlockType.Audio)
```
Destroyed blocks are detached from the scene and no longer participate in playback or export.
## Troubleshooting
**Audio does not play:** Verify that the audio block is appended to a page and that the page duration covers the block's time offset and duration.
**Duration is unavailable:** Call `forceLoadAVResource()` before reading `getAVResourceTotalDuration()`.
**Volume sounds wrong after export:** Set volume on each audio block before exporting the page, and keep values in the 0.0 to 1.0 range.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a video scene for time-based playback |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create a page for the video scene |
| `engine.block.create(blockType=DesignBlockType.Audio)` | Create a new audio block |
| `engine.block.appendChild(parent=_, child=_)` | Attach a page or audio block to its parent |
| `engine.block.setWidth(block=_, value=_)` | Set the page width |
| `engine.block.getWidth(block=_)` | Read the page width |
| `engine.block.setHeight(block=_, value=_)` | Set the page height |
| `engine.block.getHeight(block=_)` | Read the page height |
| `engine.block.setString(block=_, property="audio/fileURI", value=_)` | Set the audio source URI |
| `engine.block.getString(block=_, property="audio/fileURI")` | Read the audio source URI |
| `engine.block.forceLoadAVResource(block=_)` | `suspend`; load audio metadata before reading duration |
| `engine.block.getAVResourceTotalDuration(block=_)` | Read the source audio duration in seconds |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when the audio starts on the page timeline |
| `engine.block.getTimeOffset(block=_)` | Read when the audio starts on the page timeline |
| `engine.block.setDuration(block=_, duration=_)` | Set how long the audio block plays |
| `engine.block.getDuration(block=_)` | Read how long a page or audio block plays |
| `engine.block.setVolume(block=_, volume=_)` | Set volume from 0.0 to 1.0 |
| `engine.block.getVolume(block=_)` | Read the current volume |
| `engine.asset.findAllSources()` | List registered asset source IDs |
| `engine.asset.addLocalSourceFromJSON(contentUri=_)` | `suspend`; register an audio asset source from a content JSON file |
| `engine.asset.findAssets(sourceId=_, query=_)` | `suspend`; query audio assets |
| `engine.block.findByType(type=DesignBlockType.Audio)` | Find all audio blocks in the scene |
| `engine.block.destroy(block=_)` | Remove an audio block |
## Next Steps
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Add Sound Effects](https://img.ly/docs/cesdk/android/create-audio/audio/add-sound-effects-9e984e/) — Learn how to add custom sound effects using audio buffers and raw PCM data
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Create seamless repeating audio playback for background music and sound effects using CE.SDK's audio looping system.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add Sound Effects"
description: "Learn how to use buffers with arbitrary data to generate sound effects programmatically."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio/add-sound-effects-9e984e/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) > [Add Sound Effects](https://img.ly/docs/cesdk/android/create-audio/audio/add-sound-effects-9e984e/)
---
```kotlin file=@cesdk_android_examples/engine-guides-add-sound-effects/AddSoundEffects.kt reference-only
import android.net.Uri
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.PI
import kotlin.math.roundToInt
import kotlin.math.sin
private const val SAMPLE_RATE = 48_000
private const val CHANNEL_COUNT = 2
private const val BITS_PER_SAMPLE = 16
private const val BYTES_PER_SAMPLE = BITS_PER_SAMPLE / 8
const val WAV_HEADER_SIZE = 44
private data class Note(
val frequencyHz: Double,
val startSeconds: Double,
val durationSeconds: Double,
)
private data class SoundEffect(
val notes: List,
val totalDurationSeconds: Double,
)
private object NoteFrequencies {
const val C4 = 261.63
const val E4 = 329.63
const val G4 = 392.0
const val A4 = 440.0
const val C5 = 523.25
const val D5 = 587.33
const val E5 = 659.25
const val F5 = 698.46
const val G5 = 783.99
const val A5 = 880.0
}
private val successChime =
SoundEffect(
notes = listOf(
Note(frequencyHz = NoteFrequencies.C4, startSeconds = 0.0, durationSeconds = 0.3),
Note(frequencyHz = NoteFrequencies.E4, startSeconds = 0.1, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.G4, startSeconds = 0.2, durationSeconds = 0.5),
Note(frequencyHz = NoteFrequencies.C5, startSeconds = 0.35, durationSeconds = 1.65),
Note(frequencyHz = NoteFrequencies.E5, startSeconds = 0.4, durationSeconds = 1.6),
Note(frequencyHz = NoteFrequencies.G5, startSeconds = 0.45, durationSeconds = 1.55),
),
totalDurationSeconds = 2.0,
)
private val notificationMelody =
SoundEffect(
notes = listOf(
Note(frequencyHz = NoteFrequencies.E5, startSeconds = 0.0, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.G5, startSeconds = 0.25, durationSeconds = 0.5),
Note(frequencyHz = NoteFrequencies.A5, startSeconds = 0.6, durationSeconds = 0.3),
Note(frequencyHz = NoteFrequencies.G5, startSeconds = 0.85, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.E5, startSeconds = 1.15, durationSeconds = 0.85),
),
totalDurationSeconds = 2.0,
)
private val alertTone =
SoundEffect(
notes = listOf(
Note(frequencyHz = NoteFrequencies.A5, startSeconds = 0.0, durationSeconds = 0.25),
Note(frequencyHz = NoteFrequencies.A5, startSeconds = 0.3, durationSeconds = 0.25),
Note(frequencyHz = NoteFrequencies.F5, startSeconds = 0.6, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.D5, startSeconds = 0.9, durationSeconds = 0.5),
Note(frequencyHz = NoteFrequencies.A4, startSeconds = 1.3, durationSeconds = 0.7),
),
totalDurationSeconds = 2.0,
)
data class GeneratedSoundEffect(
val name: String,
val block: DesignBlock,
val bufferUri: Uri,
val bufferLength: Int,
val startSeconds: Double,
val durationSeconds: Double,
val volume: Float,
)
data class SoundEffectsSummary(
val page: DesignBlock,
val totalDurationSeconds: Double,
val effects: List,
)
suspend fun addSoundEffects(engine: Engine): SoundEffectsSummary {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1920F)
engine.block.setHeight(page, value = 1080F)
val effectDurationSeconds = 2.0
val gapDurationSeconds = 0.5
val totalDurationSeconds = 3 * effectDurationSeconds + 2 * gapDurationSeconds
engine.block.setDuration(block = page, duration = totalDurationSeconds)
val chimeBuffer = engine.editor.createBuffer()
val chimeWav =
createWavBuffer(
sampleRate = SAMPLE_RATE,
durationSeconds = successChime.totalDurationSeconds,
) { timeSeconds ->
generateSoundEffectSample(
effect = successChime,
timeSeconds = timeSeconds,
attackSeconds = 0.02,
decaySeconds = 0.08,
sustainLevel = 0.7,
releaseSeconds = 0.25,
gain = 0.3,
harmonic2Gain = 0.25,
harmonic3Gain = 0.1,
)
}
engine.editor.setBufferData(uri = chimeBuffer, offset = 0, data = chimeWav)
val chimeBufferLength = engine.editor.getBufferLength(uri = chimeBuffer)
val riffHeader = engine.editor.getBufferData(uri = chimeBuffer, offset = 0, length = 4)
val riffBytes = ByteArray(size = 4)
riffHeader.get(riffBytes)
check(riffBytes.contentEquals("RIFF".toByteArray(Charsets.US_ASCII)))
val chimeBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = chimeBlock,
property = "audio/fileURI",
value = chimeBuffer,
)
engine.block.appendChild(parent = page, child = chimeBlock)
engine.block.forceLoadAVResource(block = chimeBlock)
val melodyBuffer = engine.editor.createBuffer()
val melodyWav =
createWavBuffer(
sampleRate = SAMPLE_RATE,
durationSeconds = notificationMelody.totalDurationSeconds,
) { timeSeconds ->
generateSoundEffectSample(
effect = notificationMelody,
timeSeconds = timeSeconds,
attackSeconds = 0.01,
decaySeconds = 0.06,
sustainLevel = 0.6,
releaseSeconds = 0.2,
gain = 0.4,
harmonic2Gain = 0.15,
harmonic3Gain = 0.0,
)
}
engine.editor.setBufferData(uri = melodyBuffer, offset = 0, data = melodyWav)
val melodyBufferLength = engine.editor.getBufferLength(uri = melodyBuffer)
val melodyBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = melodyBlock,
property = "audio/fileURI",
value = melodyBuffer,
)
engine.block.appendChild(parent = page, child = melodyBlock)
engine.block.forceLoadAVResource(block = melodyBlock)
val alertBuffer = engine.editor.createBuffer()
val alertWav =
createWavBuffer(
sampleRate = SAMPLE_RATE,
durationSeconds = alertTone.totalDurationSeconds,
) { timeSeconds ->
generateSoundEffectSample(
effect = alertTone,
timeSeconds = timeSeconds,
attackSeconds = 0.005,
decaySeconds = 0.05,
sustainLevel = 0.5,
releaseSeconds = 0.15,
gain = 0.35,
harmonic2Gain = 0.2,
harmonic3Gain = 0.15,
)
}
engine.editor.setBufferData(uri = alertBuffer, offset = 0, data = alertWav)
val alertBufferLength = engine.editor.getBufferLength(uri = alertBuffer)
val alertBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = alertBlock,
property = "audio/fileURI",
value = alertBuffer,
)
engine.block.appendChild(parent = page, child = alertBlock)
engine.block.forceLoadAVResource(block = alertBlock)
engine.block.setTimeOffset(block = chimeBlock, offset = 0.0)
engine.block.setDuration(block = chimeBlock, duration = successChime.totalDurationSeconds)
engine.block.setVolume(block = chimeBlock, volume = 0.8F)
engine.block.setTimeOffset(block = melodyBlock, offset = effectDurationSeconds + gapDurationSeconds)
engine.block.setDuration(block = melodyBlock, duration = notificationMelody.totalDurationSeconds)
engine.block.setVolume(block = melodyBlock, volume = 0.8F)
engine.block.setTimeOffset(block = alertBlock, offset = 2 * (effectDurationSeconds + gapDurationSeconds))
engine.block.setDuration(block = alertBlock, duration = alertTone.totalDurationSeconds)
engine.block.setVolume(block = alertBlock, volume = 0.75F)
val effects =
listOf(
GeneratedSoundEffect(
name = "Success chime",
block = chimeBlock,
bufferUri = chimeBuffer,
bufferLength = chimeBufferLength,
startSeconds = 0.0,
durationSeconds = successChime.totalDurationSeconds,
volume = 0.8F,
),
GeneratedSoundEffect(
name = "Notification melody",
block = melodyBlock,
bufferUri = melodyBuffer,
bufferLength = melodyBufferLength,
startSeconds = effectDurationSeconds + gapDurationSeconds,
durationSeconds = notificationMelody.totalDurationSeconds,
volume = 0.8F,
),
GeneratedSoundEffect(
name = "Alert tone",
block = alertBlock,
bufferUri = alertBuffer,
bufferLength = alertBufferLength,
startSeconds = 2 * (effectDurationSeconds + gapDurationSeconds),
durationSeconds = alertTone.totalDurationSeconds,
volume = 0.75F,
),
)
return SoundEffectsSummary(
page = page,
totalDurationSeconds = totalDurationSeconds,
effects = effects,
)
}
private fun createWavBuffer(
sampleRate: Int,
durationSeconds: Double,
generator: (timeSeconds: Double) -> Double,
): ByteBuffer {
val sampleCount = (durationSeconds * sampleRate).roundToInt()
val dataSize = sampleCount * CHANNEL_COUNT * BYTES_PER_SAMPLE
val wavFileSize = WAV_HEADER_SIZE + dataSize
val wavData = ByteBuffer.allocateDirect(wavFileSize).order(ByteOrder.LITTLE_ENDIAN)
wavData.put("RIFF".toByteArray(Charsets.US_ASCII))
wavData.putInt(wavFileSize - 8)
wavData.put("WAVE".toByteArray(Charsets.US_ASCII))
wavData.put("fmt ".toByteArray(Charsets.US_ASCII))
wavData.putInt(16)
wavData.putShort(1.toShort())
wavData.putShort(CHANNEL_COUNT.toShort())
wavData.putInt(sampleRate)
wavData.putInt(sampleRate * CHANNEL_COUNT * BYTES_PER_SAMPLE)
wavData.putShort((CHANNEL_COUNT * BYTES_PER_SAMPLE).toShort())
wavData.putShort(BITS_PER_SAMPLE.toShort())
wavData.put("data".toByteArray(Charsets.US_ASCII))
wavData.putInt(dataSize)
for (sampleIndex in 0 until sampleCount) {
val timeSeconds = sampleIndex / sampleRate.toDouble()
val clampedValue = generator(timeSeconds).coerceIn(-1.0, 1.0)
val pcmScale = if (clampedValue < 0.0) 32768.0 else 32767.0
val pcmSample = (clampedValue * pcmScale).roundToInt().toShort()
wavData.putShort(pcmSample)
wavData.putShort(pcmSample)
}
wavData.flip()
return wavData
}
private fun adsr(
timeSeconds: Double,
noteStartSeconds: Double,
noteDurationSeconds: Double,
attackSeconds: Double,
decaySeconds: Double,
sustainLevel: Double,
releaseSeconds: Double,
): Double {
val noteTime = timeSeconds - noteStartSeconds
if (noteTime < 0.0) return 0.0
val releaseStartSeconds = noteDurationSeconds - releaseSeconds
return when {
noteTime < attackSeconds -> noteTime / attackSeconds
noteTime < attackSeconds + decaySeconds ->
1.0 - ((noteTime - attackSeconds) / decaySeconds) * (1.0 - sustainLevel)
noteTime < releaseStartSeconds -> sustainLevel
noteTime < noteDurationSeconds -> sustainLevel * (1.0 - (noteTime - releaseStartSeconds) / releaseSeconds)
else -> 0.0
}
}
private fun generateSoundEffectSample(
effect: SoundEffect,
timeSeconds: Double,
attackSeconds: Double,
decaySeconds: Double,
sustainLevel: Double,
releaseSeconds: Double,
gain: Double,
harmonic2Gain: Double,
harmonic3Gain: Double,
): Double {
var sample = 0.0
for (note in effect.notes) {
val envelope =
adsr(
timeSeconds = timeSeconds,
noteStartSeconds = note.startSeconds,
noteDurationSeconds = note.durationSeconds,
attackSeconds = attackSeconds,
decaySeconds = decaySeconds,
sustainLevel = sustainLevel,
releaseSeconds = releaseSeconds,
)
if (envelope > 0.0) {
val fundamental = sin(2 * PI * note.frequencyHz * timeSeconds)
val secondHarmonic = sin(4 * PI * note.frequencyHz * timeSeconds) * harmonic2Gain
val thirdHarmonic = sin(6 * PI * note.frequencyHz * timeSeconds) * harmonic3Gain
sample += (fundamental + secondHarmonic + thirdHarmonic) * envelope * gain
}
}
return sample
}
fun destroyGeneratedSoundEffectBuffers(
engine: Engine,
effects: List,
) {
effects.forEach { effect ->
engine.editor.destroyBuffer(uri = effect.bufferUri)
}
}
```
Generate sound effects programmatically using buffers with arbitrary audio data. Create notification chimes, alert tones, and melodies without external files.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-add-sound-effects)
CE.SDK lets you create audio from code using buffers. This approach is useful for notification tones, procedural audio, or any scenario where you need to synthesize audio at runtime instead of shipping separate audio files.
This guide covers creating WAV data in memory, writing it into CE.SDK buffers, assigning the buffers to audio blocks, and positioning those blocks on a video timeline.
## Working with Buffers
CE.SDK provides a buffer API for creating and managing arbitrary binary data in memory. For the full buffer lifecycle, see [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/); this guide focuses on the audio path.
### Creating a Buffer
Create a buffer with `engine.editor.createBuffer()`, which returns a `buffer://` Uri that can be referenced by audio blocks:
```kotlin highlight-android-buffer-create
val chimeBuffer = engine.editor.createBuffer()
```
### Writing Data
Write data to a buffer with `engine.editor.setBufferData()`. On Android, the data must be a direct `ByteBuffer`, which the WAV helper below returns:
```kotlin highlight-android-buffer-write
engine.editor.setBufferData(uri = chimeBuffer, offset = 0, data = chimeWav)
```
### Reading Data
Read data back with `engine.editor.getBufferData()` and query the byte length with `engine.editor.getBufferLength()`:
```kotlin highlight-android-read-buffer
val chimeBufferLength = engine.editor.getBufferLength(uri = chimeBuffer)
val riffHeader = engine.editor.getBufferData(uri = chimeBuffer, offset = 0, length = 4)
val riffBytes = ByteArray(size = 4)
riffHeader.get(riffBytes)
check(riffBytes.contentEquals("RIFF".toByteArray(Charsets.US_ASCII)))
```
### Adding an Audio Track
Create an audio block, assign the buffer Uri to its `audio/fileURI` property, and append it to the page:
```kotlin highlight-android-audio-track
val chimeBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = chimeBlock,
property = "audio/fileURI",
value = chimeBuffer,
)
engine.block.appendChild(parent = page, child = chimeBlock)
engine.block.forceLoadAVResource(block = chimeBlock)
```
`forceLoadAVResource()` loads the generated WAV metadata so the engine can treat the buffer like any other audio resource.
### Cleanup
Destroy generated buffers only after the scene no longer needs them:
```kotlin highlight-android-cleanup
fun destroyGeneratedSoundEffectBuffers(
engine: Engine,
effects: List,
) {
effects.forEach { effect ->
engine.editor.destroyBuffer(uri = effect.bufferUri)
}
}
```
## Generating Audio Data
Audio buffers need valid audio file bytes. The example builds a stereo WAV file with a 44-byte RIFF header and 16-bit PCM samples at 48 kHz:
```kotlin highlight-android-wav-data
private fun createWavBuffer(
sampleRate: Int,
durationSeconds: Double,
generator: (timeSeconds: Double) -> Double,
): ByteBuffer {
val sampleCount = (durationSeconds * sampleRate).roundToInt()
val dataSize = sampleCount * CHANNEL_COUNT * BYTES_PER_SAMPLE
val wavFileSize = WAV_HEADER_SIZE + dataSize
val wavData = ByteBuffer.allocateDirect(wavFileSize).order(ByteOrder.LITTLE_ENDIAN)
wavData.put("RIFF".toByteArray(Charsets.US_ASCII))
wavData.putInt(wavFileSize - 8)
wavData.put("WAVE".toByteArray(Charsets.US_ASCII))
wavData.put("fmt ".toByteArray(Charsets.US_ASCII))
wavData.putInt(16)
wavData.putShort(1.toShort())
wavData.putShort(CHANNEL_COUNT.toShort())
wavData.putInt(sampleRate)
wavData.putInt(sampleRate * CHANNEL_COUNT * BYTES_PER_SAMPLE)
wavData.putShort((CHANNEL_COUNT * BYTES_PER_SAMPLE).toShort())
wavData.putShort(BITS_PER_SAMPLE.toShort())
wavData.put("data".toByteArray(Charsets.US_ASCII))
wavData.putInt(dataSize)
for (sampleIndex in 0 until sampleCount) {
val timeSeconds = sampleIndex / sampleRate.toDouble()
val clampedValue = generator(timeSeconds).coerceIn(-1.0, 1.0)
val pcmScale = if (clampedValue < 0.0) 32768.0 else 32767.0
val pcmSample = (clampedValue * pcmScale).roundToInt().toShort()
wavData.putShort(pcmSample)
wavData.putShort(pcmSample)
}
wavData.flip()
return wavData
}
```
The helper clamps generated samples to the -1.0 to 1.0 range and scales negative amplitudes to the full 16-bit PCM range.
### Shaping Notes
Use an ADSR envelope to avoid clicks at note boundaries. The envelope ramps each note in, sustains it, and fades it out before the next note:
```kotlin highlight-android-envelope
private fun adsr(
timeSeconds: Double,
noteStartSeconds: Double,
noteDurationSeconds: Double,
attackSeconds: Double,
decaySeconds: Double,
sustainLevel: Double,
releaseSeconds: Double,
): Double {
val noteTime = timeSeconds - noteStartSeconds
if (noteTime < 0.0) return 0.0
val releaseStartSeconds = noteDurationSeconds - releaseSeconds
return when {
noteTime < attackSeconds -> noteTime / attackSeconds
noteTime < attackSeconds + decaySeconds ->
1.0 - ((noteTime - attackSeconds) / decaySeconds) * (1.0 - sustainLevel)
noteTime < releaseStartSeconds -> sustainLevel
noteTime < noteDurationSeconds -> sustainLevel * (1.0 - (noteTime - releaseStartSeconds) / releaseSeconds)
else -> 0.0
}
}
```
### Defining Sound Effects
Define each effect as a set of notes with frequencies, start times, and durations:
```kotlin highlight-android-sound-definitions
private data class Note(
val frequencyHz: Double,
val startSeconds: Double,
val durationSeconds: Double,
)
private data class SoundEffect(
val notes: List,
val totalDurationSeconds: Double,
)
private object NoteFrequencies {
const val C4 = 261.63
const val E4 = 329.63
const val G4 = 392.0
const val A4 = 440.0
const val C5 = 523.25
const val D5 = 587.33
const val E5 = 659.25
const val F5 = 698.46
const val G5 = 783.99
const val A5 = 880.0
}
private val successChime =
SoundEffect(
notes = listOf(
Note(frequencyHz = NoteFrequencies.C4, startSeconds = 0.0, durationSeconds = 0.3),
Note(frequencyHz = NoteFrequencies.E4, startSeconds = 0.1, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.G4, startSeconds = 0.2, durationSeconds = 0.5),
Note(frequencyHz = NoteFrequencies.C5, startSeconds = 0.35, durationSeconds = 1.65),
Note(frequencyHz = NoteFrequencies.E5, startSeconds = 0.4, durationSeconds = 1.6),
Note(frequencyHz = NoteFrequencies.G5, startSeconds = 0.45, durationSeconds = 1.55),
),
totalDurationSeconds = 2.0,
)
private val notificationMelody =
SoundEffect(
notes = listOf(
Note(frequencyHz = NoteFrequencies.E5, startSeconds = 0.0, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.G5, startSeconds = 0.25, durationSeconds = 0.5),
Note(frequencyHz = NoteFrequencies.A5, startSeconds = 0.6, durationSeconds = 0.3),
Note(frequencyHz = NoteFrequencies.G5, startSeconds = 0.85, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.E5, startSeconds = 1.15, durationSeconds = 0.85),
),
totalDurationSeconds = 2.0,
)
private val alertTone =
SoundEffect(
notes = listOf(
Note(frequencyHz = NoteFrequencies.A5, startSeconds = 0.0, durationSeconds = 0.25),
Note(frequencyHz = NoteFrequencies.A5, startSeconds = 0.3, durationSeconds = 0.25),
Note(frequencyHz = NoteFrequencies.F5, startSeconds = 0.6, durationSeconds = 0.4),
Note(frequencyHz = NoteFrequencies.D5, startSeconds = 0.9, durationSeconds = 0.5),
Note(frequencyHz = NoteFrequencies.A4, startSeconds = 1.3, durationSeconds = 0.7),
),
totalDurationSeconds = 2.0,
)
```
The sample uses the same two-second success chime, notification melody, and alert tone as the other platform examples.
## Creating a Sound Effect
Combine the note definition, WAV helper, and buffer API to generate an audio block. This snippet creates the notification melody, writes it to a buffer, and creates the matching audio block:
```kotlin highlight-android-create-melody
val melodyBuffer = engine.editor.createBuffer()
val melodyWav =
createWavBuffer(
sampleRate = SAMPLE_RATE,
durationSeconds = notificationMelody.totalDurationSeconds,
) { timeSeconds ->
generateSoundEffectSample(
effect = notificationMelody,
timeSeconds = timeSeconds,
attackSeconds = 0.01,
decaySeconds = 0.06,
sustainLevel = 0.6,
releaseSeconds = 0.2,
gain = 0.4,
harmonic2Gain = 0.15,
harmonic3Gain = 0.0,
)
}
engine.editor.setBufferData(uri = melodyBuffer, offset = 0, data = melodyWav)
val melodyBufferLength = engine.editor.getBufferLength(uri = melodyBuffer)
val melodyBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = melodyBlock,
property = "audio/fileURI",
value = melodyBuffer,
)
engine.block.appendChild(parent = page, child = melodyBlock)
engine.block.forceLoadAVResource(block = melodyBlock)
```
The generator mixes overlapping notes with a light harmonic layer so the result sounds warmer than a pure sine wave:
```kotlin highlight-android-sample-generator
private fun generateSoundEffectSample(
effect: SoundEffect,
timeSeconds: Double,
attackSeconds: Double,
decaySeconds: Double,
sustainLevel: Double,
releaseSeconds: Double,
gain: Double,
harmonic2Gain: Double,
harmonic3Gain: Double,
): Double {
var sample = 0.0
for (note in effect.notes) {
val envelope =
adsr(
timeSeconds = timeSeconds,
noteStartSeconds = note.startSeconds,
noteDurationSeconds = note.durationSeconds,
attackSeconds = attackSeconds,
decaySeconds = decaySeconds,
sustainLevel = sustainLevel,
releaseSeconds = releaseSeconds,
)
if (envelope > 0.0) {
val fundamental = sin(2 * PI * note.frequencyHz * timeSeconds)
val secondHarmonic = sin(4 * PI * note.frequencyHz * timeSeconds) * harmonic2Gain
val thirdHarmonic = sin(6 * PI * note.frequencyHz * timeSeconds) * harmonic3Gain
sample += (fundamental + secondHarmonic + thirdHarmonic) * envelope * gain
}
}
return sample
}
```
## Positioning in Time
Audio blocks exist on the page timeline. Set the page duration, then give each sound effect a time offset, duration, and volume:
```kotlin highlight-android-timeline-setup
val effectDurationSeconds = 2.0
val gapDurationSeconds = 0.5
val totalDurationSeconds = 3 * effectDurationSeconds + 2 * gapDurationSeconds
engine.block.setDuration(block = page, duration = totalDurationSeconds)
```
```kotlin highlight-android-position-effects
engine.block.setTimeOffset(block = chimeBlock, offset = 0.0)
engine.block.setDuration(block = chimeBlock, duration = successChime.totalDurationSeconds)
engine.block.setVolume(block = chimeBlock, volume = 0.8F)
engine.block.setTimeOffset(block = melodyBlock, offset = effectDurationSeconds + gapDurationSeconds)
engine.block.setDuration(block = melodyBlock, duration = notificationMelody.totalDurationSeconds)
engine.block.setVolume(block = melodyBlock, volume = 0.8F)
engine.block.setTimeOffset(block = alertBlock, offset = 2 * (effectDurationSeconds + gapDurationSeconds))
engine.block.setDuration(block = alertBlock, duration = alertTone.totalDurationSeconds)
engine.block.setVolume(block = alertBlock, volume = 0.75F)
```
The sample spaces the effects with 0.5-second gaps:
```text
Timeline: |----|----|----|----|----|----|----|
0s 1s 2s 3s 4s 5s 6s 7s
Success: |====|
^ 0s (2s)
Melody: |====|
^ 2.5s (2s)
Alert: |====|
^ 5s (2s)
```
Each effect is two seconds long, and the page duration is seven seconds.
## Troubleshooting
### No Sound
- **Check scene setup** - Audio blocks need a video scene and a page with a duration greater than zero.
- **Verify the buffer data** - The buffer must contain a valid audio file, not raw PCM bytes without a header.
- **Load the resource** - Call `engine.block.forceLoadAVResource()` after assigning the buffer Uri.
### Audio Sounds Wrong
- **Clipping** - Keep generated sample values between -1.0 and 1.0 before PCM conversion.
- **Clicking** - Add attack and release envelope phases instead of starting or stopping samples abruptly.
- **Wrong pitch** - Use the same sample rate in the WAV header and in the frequency calculation.
### Buffer Errors
- **`setBufferData()` throws** - Use a direct `ByteBuffer`, such as one from `ByteBuffer.allocateDirect(...)`.
- **Invalid WAV** - Make sure the RIFF and data chunk sizes match the actual PCM byte count.
- **Missing data after saving** - Buffers are transient resources. See [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) for the `findAllTransientResources()` and `relocateResource()` flow before saving scenes that must survive the current engine session.
## API Reference
| Method | Description |
| --- | --- |
| `engine.editor.createBuffer()` | Create a new buffer resource and return its Uri. |
| `engine.editor.setBufferData(uri=_, offset=_, data=_)` | Write direct `ByteBuffer` data to a buffer at a byte offset. |
| `engine.editor.getBufferLength(uri=_)` | Get the current buffer size in bytes. |
| `engine.editor.getBufferData(uri=_, offset=_, length=_)` | Read a byte range from a buffer. |
| `engine.editor.destroyBuffer(uri=_)` | Destroy a buffer after no block needs it. |
| `engine.block.create(blockType=DesignBlockType.Audio)` | Create an audio block. |
| `engine.block.setUri(block=_, property="audio/fileURI", value=_)` | Assign a buffer Uri to an audio block source. |
| `engine.block.appendChild(parent=_, child=_)` | Add the audio block to the page timeline. |
| `engine.block.forceLoadAVResource(block=_)` | Load audio metadata for the generated resource. |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when the audio block starts. |
| `engine.block.setDuration(block=_, duration=_)` | Set how long the audio block plays. |
| `engine.block.setVolume(block=_, volume=_)` | Set the audio level from 0.0 to 1.0. |
## Next Steps
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Adjust Audio Playback Speed"
description: "Control audio playback speed from quarter-speed (0.25x) to triple-speed (3.0x) using the CE.SDK Android Engine API."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) > [Adjust Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-audio-adjust-speed/CreateAudioAdjustSpeed.kt reference-only
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
suspend fun createAudioAdjustSpeed(engine: Engine) {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 1280F)
engine.block.setHeight(block = page, value = 720F)
engine.block.setDuration(block = page, duration = 45.0)
val normalSpeedAudio = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = normalSpeedAudio)
// audio/fileURI is the standard Engine property key for an audio block's source URI.
engine.block.setString(
block = normalSpeedAudio,
property = "audio/fileURI",
value = "https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",
)
engine.block.forceLoadAVResource(block = normalSpeedAudio)
engine.block.setDuration(block = normalSpeedAudio, duration = 10.0)
val normalDuration = engine.block.getDuration(block = normalSpeedAudio)
engine.block.setPlaybackSpeed(block = normalSpeedAudio, speed = 1.0F)
val currentSpeed = engine.block.getPlaybackSpeed(block = normalSpeedAudio)
check(currentSpeed == 1.0F)
val slowMotionAudio = engine.block.duplicate(block = normalSpeedAudio)
engine.block.setTimeOffset(block = slowMotionAudio, offset = 11.0)
engine.block.forceLoadAVResource(block = slowMotionAudio)
engine.block.setDuration(block = slowMotionAudio, duration = normalDuration)
engine.block.setPlaybackSpeed(block = slowMotionAudio, speed = 0.5F)
val slowMotionDuration = engine.block.getDuration(block = slowMotionAudio)
check(engine.block.getPlaybackSpeed(block = slowMotionAudio) == 0.5F)
check(slowMotionDuration > normalDuration)
val maximumSpeedAudio = engine.block.duplicate(block = normalSpeedAudio)
engine.block.setTimeOffset(block = maximumSpeedAudio, offset = 32.0)
engine.block.forceLoadAVResource(block = maximumSpeedAudio)
engine.block.setDuration(block = maximumSpeedAudio, duration = normalDuration)
engine.block.setPlaybackSpeed(block = maximumSpeedAudio, speed = 3.0F)
val maximumSpeedDuration = engine.block.getDuration(block = maximumSpeedAudio)
check(engine.block.getPlaybackSpeed(block = maximumSpeedAudio) == 3.0F)
check(maximumSpeedDuration < normalDuration)
val doubleSpeedAudio = engine.block.duplicate(block = normalSpeedAudio)
engine.block.setTimeOffset(block = doubleSpeedAudio, offset = 37.0)
engine.block.forceLoadAVResource(block = doubleSpeedAudio)
engine.block.setDuration(block = doubleSpeedAudio, duration = normalDuration)
val durationBeforeSpeedChange = engine.block.getDuration(block = doubleSpeedAudio)
engine.block.setPlaybackSpeed(block = doubleSpeedAudio, speed = 2.0F)
val durationAfterSpeedChange = engine.block.getDuration(block = doubleSpeedAudio)
check(durationAfterSpeedChange < durationBeforeSpeedChange)
val sceneString = engine.scene.saveToString(scene = scene)
check(sceneString.isNotBlank())
}
```
Control audio playback speed programmatically using CE.SDK's Android Engine API,
from quarter-speed (0.25x) to triple-speed (3.0x).
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-adjust-speed)
Playback speed adjustment changes how fast or slow audio plays in the timeline. A speed multiplier of 1.0 represents normal speed, values below 1.0 slow down playback, and values above 1.0 speed it up.
This guide shows how to load an audio block, set and read playback speed, compare common speed presets, and serialize the resulting scene.
## Understanding Speed Concepts
CE.SDK supports playback speeds from **0.25x** (quarter speed) to **3.0x** (triple speed) for audio blocks, with **1.0x** as the default normal speed.
**Speed and Duration**: Adjusting speed automatically changes the block's duration with an inverse relationship: `perceived_duration = original_duration / speed_multiplier`. A 10-second block at 2.0x speed plays in 5 seconds; at 0.5x speed it takes 20 seconds.
**Common use cases**: Podcast playback controls, accessibility features, time-compressed narration, dramatic slow-motion audio effects, transcription work, and music tempo adjustments.
## Create a Video Scene
Audio blocks need a timeline. Start with a video scene, add a page, and give the page enough duration to contain the speed examples.
```kotlin highlight-android-create-video-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 1280F)
engine.block.setHeight(block = page, value = 720F)
engine.block.setDuration(block = page, duration = 45.0)
```
The page duration in this sample is intentionally longer than the first audio block because the slower duplicate takes more time on the timeline.
## Setting Up Audio for Speed Adjustment
### Loading Audio Files
Create an audio block, assign its `audio/fileURI` property, and force-load the resource before reading duration or changing speed.
```kotlin highlight-android-load-audio
val normalSpeedAudio = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = normalSpeedAudio)
// audio/fileURI is the standard Engine property key for an audio block's source URI.
engine.block.setString(
block = normalSpeedAudio,
property = "audio/fileURI",
value = "https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",
)
engine.block.forceLoadAVResource(block = normalSpeedAudio)
engine.block.setDuration(block = normalSpeedAudio, duration = 10.0)
val normalDuration = engine.block.getDuration(block = normalSpeedAudio)
```
Audio blocks store the file URI directly on the block. `forceLoadAVResource` makes CE.SDK load the audio metadata so duration and playback speed calculations are based on the resource.
## Adjusting Playback Speed
### Setting Normal Speed
Set speed to 1.0 to keep the original playback rate or reset a block after other speed changes.
```kotlin highlight-android-set-normal-speed
engine.block.setPlaybackSpeed(block = normalSpeedAudio, speed = 1.0F)
```
Normal speed is a useful baseline when you generate several variants from the same audio source.
### Querying Current Speed
Read the current multiplier with `getPlaybackSpeed` when you need to populate controls, validate a change, or base a relative adjustment on the existing speed.
```kotlin highlight-android-query-current-speed
val currentSpeed = engine.block.getPlaybackSpeed(block = normalSpeedAudio)
check(currentSpeed == 1.0F)
```
The value is returned as a `Float`, using the same multiplier scale that `setPlaybackSpeed` accepts.
## Common Speed Presets
### Slow Motion Audio (0.5x)
Slowing audio to half speed creates a slow-motion effect for careful listening or transcription.
```kotlin highlight-android-set-slow-motion
val slowMotionAudio = engine.block.duplicate(block = normalSpeedAudio)
engine.block.setTimeOffset(block = slowMotionAudio, offset = 11.0)
engine.block.forceLoadAVResource(block = slowMotionAudio)
engine.block.setDuration(block = slowMotionAudio, duration = normalDuration)
engine.block.setPlaybackSpeed(block = slowMotionAudio, speed = 0.5F)
val slowMotionDuration = engine.block.getDuration(block = slowMotionAudio)
check(engine.block.getPlaybackSpeed(block = slowMotionAudio) == 0.5F)
check(slowMotionDuration > normalDuration)
```
At 0.5x speed, a 10-second audio block takes about 20 seconds to play. The sample checks that the duration grows after the slower speed is applied.
### Maximum Speed (3.0x)
The maximum supported audio speed is 3.0x, three times the normal playback rate.
```kotlin highlight-android-set-maximum-speed
val maximumSpeedAudio = engine.block.duplicate(block = normalSpeedAudio)
engine.block.setTimeOffset(block = maximumSpeedAudio, offset = 32.0)
engine.block.forceLoadAVResource(block = maximumSpeedAudio)
engine.block.setDuration(block = maximumSpeedAudio, duration = normalDuration)
engine.block.setPlaybackSpeed(block = maximumSpeedAudio, speed = 3.0F)
val maximumSpeedDuration = engine.block.getDuration(block = maximumSpeedAudio)
check(engine.block.getPlaybackSpeed(block = maximumSpeedAudio) == 3.0F)
check(maximumSpeedDuration < normalDuration)
```
At 3.0x speed, a 10-second audio block finishes in about 3.33 seconds. This is useful for rapid review workflows, but validate app controls so they stay within the supported range.
## Speed and Block Duration
### Understanding Duration Changes
When you change playback speed, CE.SDK updates the block duration to reflect the new playback time.
```kotlin highlight-android-speed-and-duration
val doubleSpeedAudio = engine.block.duplicate(block = normalSpeedAudio)
engine.block.setTimeOffset(block = doubleSpeedAudio, offset = 37.0)
engine.block.forceLoadAVResource(block = doubleSpeedAudio)
engine.block.setDuration(block = doubleSpeedAudio, duration = normalDuration)
val durationBeforeSpeedChange = engine.block.getDuration(block = doubleSpeedAudio)
engine.block.setPlaybackSpeed(block = doubleSpeedAudio, speed = 2.0F)
val durationAfterSpeedChange = engine.block.getDuration(block = doubleSpeedAudio)
check(durationAfterSpeedChange < durationBeforeSpeedChange)
```
The before and after durations show the inverse relationship: increasing speed shortens the block, while decreasing speed lengthens it. This keeps audio timing aligned with other timeline content.
## Exporting Results
After adjusting audio speeds, serialize the scene to preserve the audio blocks and their speed settings.
```kotlin highlight-android-export
val sceneString = engine.scene.saveToString(scene = scene)
check(sceneString.isNotBlank())
```
The returned scene string can be loaded later for further editing or used as a template in automated processing workflows.
## Troubleshooting
- **Speed is not applied**: Call `forceLoadAVResource` before setting speed so the audio metadata is available.
- **Duration looks unchanged**: Read duration again after `setPlaybackSpeed`; speed changes update the block duration automatically.
- **Speed input is outside the supported range**: Validate controls before calling `setPlaybackSpeed`; Android audio blocks support 0.25x through 3.0x, and values outside that range throw an `EngineException` instead of being clamped.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a scene with video timeline support. |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create the page that hosts the audio blocks. |
| `engine.block.setWidth(block=_, value=_)` | Set the page width in scene units. |
| `engine.block.setHeight(block=_, value=_)` | Set the page height in scene units. |
| `engine.block.create(blockType=DesignBlockType.Audio)` | Create an audio block. |
| `engine.block.appendChild(parent=_, child=_)` | Add pages and audio blocks to the scene hierarchy. |
| `engine.block.setString(block=_, property="audio/fileURI", value=_)` | Set the source audio URI. |
| `engine.block.forceLoadAVResource(block=_)` | Load audio resource metadata before duration and speed operations. |
| `engine.block.setDuration(block=_, duration=_)` | Set the block's timeline duration in seconds. |
| `engine.block.getDuration(block=_)` | Read the block's timeline duration in seconds. |
| `engine.block.setTimeOffset(block=_, offset=_)` | Position an audio block on the timeline. |
| `engine.block.duplicate(block=_, attachToParent=_)` | Duplicate an audio block for another speed preset. |
| `engine.block.setPlaybackSpeed(block=_, speed=_)` | Set the speed multiplier. Valid range \[0.25, 3.0] for audio blocks. Also adjusts the block's trim and duration. |
| `engine.block.getPlaybackSpeed(block=_)` | Read the current speed multiplier. |
| `engine.scene.saveToString(scene=_)` | Serialize the scene with its audio speed settings. |
## Next Steps
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Create seamless repeating audio playback for background music and sound effects using CE.SDK's audio looping system.
- [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects using CE.SDK's audio block system.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Adjust Audio Volume"
description: "Learn how to adjust audio volume in CE.SDK for Android to control playback levels, mute audio, and balance multiple audio sources in video projects."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) > [Adjust Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-audio-audio-adjust-volume/AdjustVolume.kt reference-only
import android.net.Uri
import android.util.Log
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import kotlin.math.abs
private const val TAG = "AdjustVolumeGuide"
suspend fun adjustVolume(engine: Engine): AdjustVolumeResult {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 1280F)
engine.block.setHeight(block = page, value = 720F)
engine.block.setDuration(block = page, duration = 10.0)
val voiceoverAudio = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = voiceoverAudio)
engine.block.setDuration(block = voiceoverAudio, duration = 10.0)
val voiceoverUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a")
engine.block.setUri(
block = voiceoverAudio,
property = "audio/fileURI",
value = voiceoverUri,
)
engine.block.forceLoadAVResource(block = voiceoverAudio)
engine.block.setVolume(block = voiceoverAudio, volume = 0.8F)
val foregroundVolume = engine.block.getVolume(block = voiceoverAudio)
val backgroundMusic = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = backgroundMusic)
engine.block.setDuration(block = backgroundMusic, duration = 10.0)
val backgroundUri = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.audio/audios/dance_harder.m4a")
engine.block.setUri(
block = backgroundMusic,
property = "audio/fileURI",
value = backgroundUri,
)
engine.block.forceLoadAVResource(block = backgroundMusic)
engine.block.setVolume(block = backgroundMusic, volume = 0.3F)
val backgroundVolume = engine.block.getVolume(block = backgroundMusic)
val videoBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = videoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block = videoBlock, value = 1280F)
engine.block.setHeight(block = videoBlock, value = 720F)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
engine.block.setFill(block = videoBlock, fill = videoFill)
val videoTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = videoTrack)
engine.block.appendChild(parent = videoTrack, child = videoBlock)
engine.block.fillParent(block = videoTrack)
engine.block.forceLoadAVResource(block = videoFill)
engine.block.setVolume(block = videoFill, volume = 0.5F)
val videoFillVolume = engine.block.getVolume(block = videoFill)
engine.block.setMuted(block = voiceoverAudio, muted = true)
val muted = engine.block.isMuted(block = voiceoverAudio)
val mutedVolume = engine.block.getVolume(block = voiceoverAudio)
engine.block.setMuted(block = voiceoverAudio, muted = false)
val isMutedAfterUnmute = engine.block.isMuted(block = voiceoverAudio)
val currentVolume = engine.block.getVolume(block = voiceoverAudio)
val userMuted = engine.block.isMuted(block = voiceoverAudio)
val forceMuted = engine.block.isForceMuted(block = voiceoverAudio)
Log.i(TAG, "Audio volume: ${(currentVolume * 100).toInt()}%")
Log.i(TAG, "Muted by user: $userMuted")
Log.i(TAG, "Muted by engine: $forceMuted")
val sliderPercent = 75
val sliderVolume = sliderPercent / 100F
engine.block.setVolume(block = voiceoverAudio, volume = sliderVolume)
val displayedPercent = (engine.block.getVolume(block = voiceoverAudio) * 100).toInt()
val currentlyMuted = engine.block.isMuted(block = voiceoverAudio)
engine.block.setMuted(block = voiceoverAudio, muted = !currentlyMuted)
val volumeIconState = when {
engine.block.isForceMuted(block = voiceoverAudio) -> "force-muted"
engine.block.isMuted(block = voiceoverAudio) -> "muted"
else -> "volume"
}
check(abs(foregroundVolume - 0.8F) < 0.001F)
check(abs(backgroundVolume - 0.3F) < 0.001F)
check(abs(videoFillVolume - 0.5F) < 0.001F)
check(muted)
check(abs(mutedVolume - 0.8F) < 0.001F)
check(!isMutedAfterUnmute)
check(displayedPercent == sliderPercent)
check(volumeIconState == "muted")
return AdjustVolumeResult(
foregroundVolume = foregroundVolume,
backgroundVolume = backgroundVolume,
videoFillVolume = videoFillVolume,
mutedVolume = mutedVolume,
muted = muted,
isMutedAfterUnmute = isMutedAfterUnmute,
sliderVolume = sliderVolume,
sliderPercent = displayedPercent,
toggledMuted = engine.block.isMuted(block = voiceoverAudio),
forceMuted = forceMuted,
)
}
```
Control audio playback volume with CE.SDK's Engine API for Android, from silent
(0.0) to full volume (1.0).
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-audio-adjust-volume)
Volume control adjusts how loud or quiet audio plays during playback. CE.SDK uses a normalized 0.0-1.0 range where 0.0 is completely silent and 1.0 is full volume. This applies to both audio blocks and video fills with embedded audio. Volume settings are commonly used for balancing multiple audio sources, creating fade effects, and allowing users to adjust playback levels.
The built-in Android editor exposes volume controls for selected audio blocks and video fills through the inspector bar. Timeline clips also indicate muted audio, so users can see when a clip is silent while arranging video projects.
This guide covers how to adjust audio volume programmatically using the Engine API, mute and unmute audio, query volume and mute states, and map custom UI controls to CE.SDK volume values.
## Understanding Volume Concepts
CE.SDK supports volume levels from **0.0** (silent) to **1.0** (full volume), with **1.0** as the default for new audio blocks. Values in between represent proportional volume levels: 0.5 is half volume, and 0.25 is quarter volume.
**Volume vs muting**: Setting volume to 0.0 makes audio silent, but `setMuted()` is preferred when you want to temporarily silence audio without losing the volume setting. Unmuting restores the previous volume level.
**Common use cases**: background music mixing (0.3-0.5 under voiceover), user volume controls, audio balancing for multi-track projects, fade effects, and accessibility features.
## Setting Up Audio for Volume Control
### Loading Audio Files
Create an audio block and load an audio file by setting its `audio/fileURI` property.
```kotlin highlight-android-create-audio
val voiceoverAudio = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = voiceoverAudio)
engine.block.setDuration(block = voiceoverAudio, duration = 10.0)
val voiceoverUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a")
engine.block.setUri(
block = voiceoverAudio,
property = "audio/fileURI",
value = voiceoverUri,
)
engine.block.forceLoadAVResource(block = voiceoverAudio)
```
Unlike video or image blocks which use fills, audio blocks store the file URI directly on the block itself. `forceLoadAVResource()` ensures CE.SDK has downloaded the audio file and loaded its metadata before you manipulate it.
## Adjusting Volume
### Setting Volume
Set volume using `setVolume()` with a `Float` value between `0.0` and `1.0`.
```kotlin highlight-android-set-volume
engine.block.setVolume(block = voiceoverAudio, volume = 0.8F)
val foregroundVolume = engine.block.getVolume(block = voiceoverAudio)
```
Setting volume to 0.8 (80%) is useful when you want prominent audio that is not at maximum level, leaving headroom for other audio sources or preventing distortion.
### Setting Low Volume for Background Audio
For background music that should be audible but not prominent, use lower volume levels.
```kotlin highlight-android-set-low-volume
val backgroundMusic = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = backgroundMusic)
engine.block.setDuration(block = backgroundMusic, duration = 10.0)
val backgroundUri = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.audio/audios/dance_harder.m4a")
engine.block.setUri(
block = backgroundMusic,
property = "audio/fileURI",
value = backgroundUri,
)
engine.block.forceLoadAVResource(block = backgroundMusic)
engine.block.setVolume(block = backgroundMusic, volume = 0.3F)
val backgroundVolume = engine.block.getVolume(block = backgroundMusic)
```
At 0.3 (30%) volume, the audio remains clearly audible but stays in the background. This is a common level for background music under voiceover or dialogue.
### Volume on Video Fills
Video fills use the same volume API. Set volume on the video fill block to control the embedded audio track.
```kotlin highlight-android-video-fill-volume
val videoBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = videoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block = videoBlock, value = 1280F)
engine.block.setHeight(block = videoBlock, value = 720F)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
engine.block.setFill(block = videoBlock, fill = videoFill)
val videoTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = videoTrack)
engine.block.appendChild(parent = videoTrack, child = videoBlock)
engine.block.fillParent(block = videoTrack)
engine.block.forceLoadAVResource(block = videoFill)
engine.block.setVolume(block = videoFill, volume = 0.5F)
val videoFillVolume = engine.block.getVolume(block = videoFill)
```
Use this when a video clip should stay visible but its embedded audio needs to sit below other tracks, or when you replace the clip's audio with a separate audio block.
## Muting Audio
### Mute and Unmute
Use `setMuted()` to mute audio without changing its volume setting. This is useful for toggle controls.
```kotlin highlight-android-mute-audio
engine.block.setMuted(block = voiceoverAudio, muted = true)
val muted = engine.block.isMuted(block = voiceoverAudio)
val mutedVolume = engine.block.getVolume(block = voiceoverAudio)
engine.block.setMuted(block = voiceoverAudio, muted = false)
val isMutedAfterUnmute = engine.block.isMuted(block = voiceoverAudio)
```
When an audio block is muted, the volume setting is preserved. Unmuting later with `setMuted(block = voiceoverAudio, muted = false)` restores playback at the same volume level.
### Querying Volume and Mute States
Query current volume and mute states at any time.
```kotlin highlight-android-query-volume
val currentVolume = engine.block.getVolume(block = voiceoverAudio)
val userMuted = engine.block.isMuted(block = voiceoverAudio)
val forceMuted = engine.block.isForceMuted(block = voiceoverAudio)
Log.i(TAG, "Audio volume: ${(currentVolume * 100).toInt()}%")
Log.i(TAG, "Muted by user: $userMuted")
Log.i(TAG, "Muted by engine: $forceMuted")
```
Use `getVolume()` to read the current volume level, `isMuted()` to check if the block is muted by the user, and `isForceMuted()` to check if the engine has automatically muted the block due to playback rules.
## Mixing Multiple Audio Sources
### Balancing Tracks
When working with multiple audio sources, use different volume levels to create a balanced mix. A common approach is to keep voiceover or dialogue at higher levels (0.8-1.0) and background music at lower levels (0.3-0.5).
### Common Mixing Patterns
**Voiceover prominent**: set background music to 0.3 and voiceover to 1.0 for clear narration with musical accompaniment.
**Balanced dialogue and music**: set both to 0.6-0.7 when both elements are equally important.
**Sound effects as accents**: set sound effects to 0.5-0.8 depending on how prominent they should be in the mix.
## Building Volume Controls
### Volume Slider
When building a volume slider, map the slider value directly to the 0.0-1.0 range. Display percentages from 0-100% for user-friendly labels.
```kotlin highlight-android-volume-slider
val sliderPercent = 75
val sliderVolume = sliderPercent / 100F
engine.block.setVolume(block = voiceoverAudio, volume = sliderVolume)
val displayedPercent = (engine.block.getVolume(block = voiceoverAudio) * 100).toInt()
```
Android's built-in volume sheet uses the same 0.0-1.0 value range, so custom controls can write the slider value directly to the selected audio block or video fill.
### Mute Toggle
Implement mute buttons using `setMuted()` and indicate the current state using `isMuted()`. Show a separate state when `isForceMuted()` returns `true` to indicate the engine has automatically muted the audio.
```kotlin highlight-android-mute-toggle
val currentlyMuted = engine.block.isMuted(block = voiceoverAudio)
engine.block.setMuted(block = voiceoverAudio, muted = !currentlyMuted)
val volumeIconState = when {
engine.block.isForceMuted(block = voiceoverAudio) -> "force-muted"
engine.block.isMuted(block = voiceoverAudio) -> "muted"
else -> "volume"
}
```
This keeps temporary mute state separate from the saved volume value, which makes toggles reversible.
## Troubleshooting
### Volume Changes Not Audible
Check if the block is muted with `isMuted()` or force muted with `isForceMuted()`. Verify the audio resource has loaded successfully with `forceLoadAVResource()`.
### Force Muted State
Video fills running faster than 3.0x are automatically force muted by the engine. Reduce the playback speed to 3.0x or below to restore audio output.
### Volume Not Persisting
Ensure you are setting volume on the correct block ID. Volume settings are block-specific and do not propagate to duplicates or other instances.
## API Reference
| Method | Category | Purpose |
| --- | --- | --- |
| `engine.block.setVolume(block=_, volume=_)` | Block | Set volume level from 0.0 to 1.0 |
| `engine.block.getVolume(block=_)` | Block | Get the current volume level |
| `engine.block.setMuted(block=_, muted=_)` | Block | Mute or unmute audio |
| `engine.block.isMuted(block=_)` | Block | Check if audio is muted by the user |
| `engine.block.isForceMuted(block=_)` | Block | Check if the engine has force muted audio |
## Next Steps
- [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects using CE.SDK's audio block system.
- [Add Sound Effects](https://img.ly/docs/cesdk/android/create-audio/audio/add-sound-effects-9e984e/) — Learn how to add custom sound effects using audio buffers and raw PCM data
- [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/) — Learn how to adjust audio playback speed in CE.SDK to create slow-motion, time-stretched, and fast-forward audio effects.
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Create seamless repeating audio playback for background music and sound effects using CE.SDK's audio looping system.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Loop Audio"
description: "Control audio looping behavior programmatically using CE.SDK's Android Engine API for audio processing and automated content workflows."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) > [Loop](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-audio-loop/CreateAudioLoop.kt reference-only
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
data class CreateAudioLoop(
val audioDuration: Double,
val pageDuration: Double,
val loopingTimeOffset: Double,
val loopingEnabled: Boolean,
val loopingDuration: Double,
val nonLoopingTimeOffset: Double,
val nonLoopingEnabled: Boolean,
val nonLoopingDuration: Double,
val trimmedTimeOffset: Double,
val trimmedLoopingEnabled: Boolean,
val trimOffset: Double,
val trimLength: Double,
val trimmedDuration: Double,
val sceneString: String,
)
suspend fun createAudioLoop(engine: Engine): CreateAudioLoop {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
val audioUri = "https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a"
val audioBlock = engine.block.create(DesignBlockType.Audio)
// Android exposes the audio source as the `audio/fileURI` property key.
engine.block.setString(block = audioBlock, property = "audio/fileURI", value = audioUri)
engine.block.forceLoadAVResource(audioBlock)
val audioDuration = engine.block.getAVResourceTotalDuration(audioBlock)
println("Audio duration: $audioDuration seconds")
val loopingTimeOffset = 0.0
val loopingDuration = audioDuration * 3.0
val nonLoopingTimeOffset = loopingTimeOffset + loopingDuration + 1.0
val nonLoopingDuration = audioDuration + 12.0
val trimmedTimeOffset = nonLoopingTimeOffset + nonLoopingDuration + 1.0
val trimOffset = 1.0
val trimLength = 2.0
val trimmedDuration = trimLength * 4.0
val pageDuration = maxOf(
loopingTimeOffset + loopingDuration,
nonLoopingTimeOffset + nonLoopingDuration,
trimmedTimeOffset + trimmedDuration,
)
engine.block.setDuration(page, duration = pageDuration)
val loopingAudio = engine.block.duplicate(block = audioBlock, attachToParent = false)
engine.block.appendChild(parent = page, child = loopingAudio)
engine.block.setTimeOffset(loopingAudio, offset = loopingTimeOffset)
engine.block.setLooping(loopingAudio, looping = true)
engine.block.setDuration(loopingAudio, duration = loopingDuration)
val isLooping = engine.block.isLooping(loopingAudio)
println("Is looping: $isLooping")
val nonLoopingAudio = engine.block.duplicate(block = audioBlock, attachToParent = false)
engine.block.appendChild(parent = page, child = nonLoopingAudio)
engine.block.setTimeOffset(nonLoopingAudio, offset = nonLoopingTimeOffset)
engine.block.setLooping(nonLoopingAudio, looping = false)
engine.block.setDuration(nonLoopingAudio, duration = nonLoopingDuration)
val trimmedLoopAudio = engine.block.duplicate(block = audioBlock, attachToParent = false)
engine.block.appendChild(parent = page, child = trimmedLoopAudio)
engine.block.setTimeOffset(trimmedLoopAudio, offset = trimmedTimeOffset)
engine.block.setTrimOffset(trimmedLoopAudio, offset = trimOffset)
engine.block.setTrimLength(trimmedLoopAudio, length = trimLength)
engine.block.setLooping(trimmedLoopAudio, looping = true)
engine.block.setDuration(trimmedLoopAudio, duration = trimmedDuration)
engine.block.destroy(audioBlock)
val sceneString = engine.scene.saveToString(scene = scene)
println("Scene saved (${sceneString.length} characters)")
return CreateAudioLoop(
audioDuration = audioDuration,
pageDuration = engine.block.getDuration(page),
loopingTimeOffset = engine.block.getTimeOffset(loopingAudio),
loopingEnabled = isLooping,
loopingDuration = engine.block.getDuration(loopingAudio),
nonLoopingTimeOffset = engine.block.getTimeOffset(nonLoopingAudio),
nonLoopingEnabled = engine.block.isLooping(nonLoopingAudio),
nonLoopingDuration = engine.block.getDuration(nonLoopingAudio),
trimmedTimeOffset = engine.block.getTimeOffset(trimmedLoopAudio),
trimmedLoopingEnabled = engine.block.isLooping(trimmedLoopAudio),
trimOffset = engine.block.getTrimOffset(trimmedLoopAudio),
trimLength = engine.block.getTrimLength(trimmedLoopAudio),
trimmedDuration = engine.block.getDuration(trimmedLoopAudio),
sceneString = sceneString,
)
}
```
Control audio looping behavior programmatically for background music, sound
effects, and rhythmic audio segments.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-loop)
Audio looping restarts an audio block from the beginning when playback reaches the end. When you set a block duration longer than the audio length and enable looping, CE.SDK repeats the audio to fill the whole duration.
This guide covers how to enable and disable audio looping, read the current looping state, combine looping with duration settings, and loop trimmed audio segments using the Android Engine API.
## Setting Up the Scene
Start with a video scene and a page that acts as the timeline container. The sample sizes the page after loading audio metadata so the timeline covers every audio block.
```kotlin highlight-android-setup
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
```
Audio blocks must be children of the page to participate in the timeline. The sample appends playable copies to the page in the sections below.
## Understanding Audio Looping
When looping is enabled on an audio block, CE.SDK repeats the audio content from the beginning each time it reaches the end. This continues until the block's duration is filled. For example, setting a block duration to three times the source duration makes the clip play three complete times.
Loop transitions are immediate. The audio content determines how smooth the result sounds, so files with matching start and end points create the cleanest loops.
## Creating Audio Blocks
### Adding Audio Content
Audio blocks use file URIs to reference audio sources. We create a reusable source block and assign the audio URI with the `audio/fileURI` property.
```kotlin highlight-android-create-audio-block
val audioBlock = engine.block.create(DesignBlockType.Audio)
// Android exposes the audio source as the `audio/fileURI` property key.
engine.block.setString(block = audioBlock, property = "audio/fileURI", value = audioUri)
```
CE.SDK supports common audio formats including MP3, M4A, WAV, and AAC.
## Enabling Audio Looping
### Loading Audio Resources
Before working with audio metadata, load the resource so the engine can read its duration.
```kotlin highlight-android-load-audio-resource
engine.block.forceLoadAVResource(audioBlock)
val audioDuration = engine.block.getAVResourceTotalDuration(audioBlock)
println("Audio duration: $audioDuration seconds")
```
`getAVResourceTotalDuration()` returns the source audio duration in seconds. Use it when you need to calculate how many repetitions fit into a block duration.
### Sizing the Timeline
Derive block durations from the loaded source duration instead of assuming a fixed clip length. The sample also sets the page duration to the latest block end time, including each block's `timeOffset + duration`.
```kotlin highlight-android-size-timeline
val loopingTimeOffset = 0.0
val loopingDuration = audioDuration * 3.0
val nonLoopingTimeOffset = loopingTimeOffset + loopingDuration + 1.0
val nonLoopingDuration = audioDuration + 12.0
val trimmedTimeOffset = nonLoopingTimeOffset + nonLoopingDuration + 1.0
val trimOffset = 1.0
val trimLength = 2.0
val trimmedDuration = trimLength * 4.0
val pageDuration = maxOf(
loopingTimeOffset + loopingDuration,
nonLoopingTimeOffset + nonLoopingDuration,
trimmedTimeOffset + trimmedDuration,
)
engine.block.setDuration(page, duration = pageDuration)
```
The looping block spans three full source-length passes. The non-looping block lasts one source duration plus 12 seconds, so playback stops after the source ends and the rest remains silent.
### Setting Looping State
Enable looping by calling `setLooping()` with `true`. When the block duration is longer than the audio length, the audio repeats until that duration is filled.
```kotlin highlight-android-enable-looping
val loopingAudio = engine.block.duplicate(block = audioBlock, attachToParent = false)
engine.block.appendChild(parent = page, child = loopingAudio)
engine.block.setTimeOffset(loopingAudio, offset = loopingTimeOffset)
engine.block.setLooping(loopingAudio, looping = true)
engine.block.setDuration(loopingAudio, duration = loopingDuration)
```
In this example, the block duration is calculated as `audioDuration * 3.0`, so the loaded source loops three times.
## Querying and Controlling Looping
### Checking Looping State
Read the current looping state whenever your app manages multiple audio tracks or needs to reflect state in custom controls.
```kotlin highlight-android-query-looping-state
val isLooping = engine.block.isLooping(loopingAudio)
println("Is looping: $isLooping")
```
The returned Boolean reports whether the block is configured to restart from the beginning during playback.
### Disabling Looping
Set looping to `false` when an audio block should play once.
```kotlin highlight-android-non-looping-audio
val nonLoopingAudio = engine.block.duplicate(block = audioBlock, attachToParent = false)
engine.block.appendChild(parent = page, child = nonLoopingAudio)
engine.block.setTimeOffset(nonLoopingAudio, offset = nonLoopingTimeOffset)
engine.block.setLooping(nonLoopingAudio, looping = false)
engine.block.setDuration(nonLoopingAudio, duration = nonLoopingDuration)
```
With looping disabled and a duration longer than the source audio, playback stops after the source audio ends and leaves silence for the remaining block duration.
## Looping with Trim Settings
### Trimming Looped Audio
Combine trim settings with looping to repeat a short segment from a longer audio file.
```kotlin highlight-android-looping-with-trim
val trimmedLoopAudio = engine.block.duplicate(block = audioBlock, attachToParent = false)
engine.block.appendChild(parent = page, child = trimmedLoopAudio)
engine.block.setTimeOffset(trimmedLoopAudio, offset = trimmedTimeOffset)
engine.block.setTrimOffset(trimmedLoopAudio, offset = trimOffset)
engine.block.setTrimLength(trimmedLoopAudio, length = trimLength)
engine.block.setLooping(trimmedLoopAudio, looping = true)
engine.block.setDuration(trimmedLoopAudio, duration = trimmedDuration)
```
This trims the audio to a 2-second segment from 1.0s to 3.0s of the source. The 8-second block duration repeats that segment four times, and the page duration includes the trimmed block's offset plus its duration.
### Choosing Loop Points
For smoother loops, choose trim points where the audio flows naturally from end to beginning. Consistent rhythm, tone, and volume at trim boundaries reduce audible transitions.
## Exporting the Scene
After configuring audio looping, save the scene for storage, later editing, or rendering in another CE.SDK environment.
```kotlin highlight-android-export
val sceneString = engine.scene.saveToString(scene = scene)
println("Scene saved (${sceneString.length} characters)")
```
The serialized scene preserves the audio blocks, timing, trim values, and looping configuration.
## Troubleshooting
**Audio not looping**: Verify `isLooping()` returns `true` and that the block duration exceeds the source audio or trimmed segment length.
**Audible gaps at loop points**: Choose trim points where the end of the segment can flow back into the beginning. Matching volume and rhythm at both boundaries creates smoother loops.
**Resource metadata unavailable**: Call `forceLoadAVResource()` before reading source duration or changing trim values.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a video scene for timeline audio |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create the timeline page |
| `engine.block.create(blockType=DesignBlockType.Audio)` | Create an audio block |
| `engine.block.appendChild(parent=_, child=_)` | Add a block to the scene hierarchy |
| `engine.block.setWidth(block=_, value=_)` | Set the page width |
| `engine.block.setHeight(block=_, value=_)` | Set the page height |
| `engine.block.setString(block=_, property="audio/fileURI", value=_)` | Set the audio source URI |
| `engine.block.forceLoadAVResource(block=_)` | Load audio metadata |
| `engine.block.getAVResourceTotalDuration(block=_)` | Read the source audio duration |
| `engine.block.duplicate(block=_, attachToParent=false)` | Reuse the loaded audio source for another block |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when audio starts on the timeline |
| `engine.block.setLooping(block=_, looping=_)` | Enable or disable audio looping |
| `engine.block.isLooping(block=_)` | Check whether audio is set to loop |
| `engine.block.setDuration(block=_, duration=_)` | Set playback duration in seconds |
| `engine.block.getDuration(block=_)` | Read playback duration in seconds |
| `engine.block.setTrimOffset(block=_, offset=_)` | Set the start of the trimmed source segment |
| `engine.block.setTrimLength(block=_, length=_)` | Set the trimmed segment length |
| `engine.block.destroy(block=_)` | Remove a temporary block |
| `engine.scene.saveToString(scene=_)` | Serialize the scene with audio settings |
## Next Steps
- [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects using CE.SDK's audio block system.
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/) — Learn how to adjust audio playback speed in CE.SDK to create slow-motion, time-stretched, and fast-forward audio effects.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Record Voiceover"
description: "Let users record voiceover clips directly in the Android editor UI."
platform: android
url: "https://img.ly/docs/cesdk/android/create-audio/audio/record-voiceover-07e8e1/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) > [Record Voiceover](https://img.ly/docs/cesdk/android/create-audio/audio/record-voiceover-07e8e1/)
---
```kotlin file=@cesdk_android_examples/editor-guides-record-voiceover/RecordVoiceoverSolution.kt reference-only
import androidx.compose.runtime.Composable
import ly.img.editor.Editor
import ly.img.editor.core.component.Dock
import ly.img.editor.core.component.InspectorBar
import ly.img.editor.core.component.Timeline
import ly.img.editor.core.component.remember
import ly.img.editor.core.component.rememberDelete
import ly.img.editor.core.component.rememberVoiceover
import ly.img.editor.core.component.rememberVoiceoverRecord
import ly.img.editor.core.component.rememberVolume
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
// Add this composable to your NavHost
@Composable
fun RecordVoiceoverSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember {
bottomPanel = { Timeline.remember() }
dock = { rememberVoiceoverDock() }
inspectorBar = { rememberVoiceoverInspectorBar() }
}
},
onClose = onClose,
)
}
@Composable
private fun rememberVoiceoverDock() = Dock.remember {
listBuilder = {
Dock.ListBuilder.remember {
add { Dock.Button.rememberVoiceoverRecord() }
}
}
}
@Composable
private fun rememberVoiceoverInspectorBar() = InspectorBar.remember {
listBuilder = {
InspectorBar.ListBuilder.remember {
add { InspectorBar.Button.rememberVoiceover() }
add { InspectorBar.Button.rememberVolume() }
add { InspectorBar.Button.rememberDelete() }
}
}
}
```
Add CE.SDK's built-in voiceover recorder to your Android editor so users can
narrate clips without leaving the editor UI.
> **Reading time:** 3 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-record-voiceover)
Voiceover recording is an editor UI feature. The example uses the base
`Editor` with a timeline bottom panel so recorded takes are visible after the
user saves them.
If your app builds on the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/),
the default dock and inspector bar already include voiceover actions. The code
below is for apps that customize the base editor or replace those component
lists themselves.
## Configure the Editor
Start from `EditorConfiguration.remember` and add `Timeline.remember()` as the
bottom panel. The dock and inspector bar snippets below show the voiceover
actions to keep when your app customizes those lists.
```kotlin highlight-android-editor
Editor(
license = license, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember {
bottomPanel = { Timeline.remember() }
dock = { rememberVoiceoverDock() }
inspectorBar = { rememberVoiceoverInspectorBar() }
}
},
onClose = onClose,
)
```
## Declare the Microphone Permission
The voiceover recorder requests `Manifest.permission.RECORD_AUDIO` when the user
taps Record. Editor-only integrations must still declare the permission in the
app manifest; apps that already include the camera module may already have it.
```xml
```
## Open the Voiceover Recorder
Add `Dock.Button.rememberVoiceoverRecord()` to the dock list so users can open
the built-in recorder. If you customize the dock list, keep any other actions
your editor still needs alongside the voiceover button.
```kotlin highlight-android-dock
@Composable
private fun rememberVoiceoverDock() = Dock.remember {
listBuilder = {
Dock.ListBuilder.remember {
add { Dock.Button.rememberVoiceoverRecord() }
}
}
}
```
## Add Recordings from the Inspector
When a completed voiceover clip is selected,
`InspectorBar.Button.rememberVoiceover()` opens the same recorder for another
take at the current playback position. The recorder creates or reuses a draft
voiceover block for the new take; it does not append audio to the selected
completed clip.
```kotlin highlight-android-inspector
@Composable
private fun rememberVoiceoverInspectorBar() = InspectorBar.remember {
listBuilder = {
InspectorBar.ListBuilder.remember {
add { InspectorBar.Button.rememberVoiceover() }
add { InspectorBar.Button.rememberVolume() }
add { InspectorBar.Button.rememberDelete() }
}
}
}
```
## Recording Behavior
The recorder starts at the current playback position and creates an audio block
with the `voiceover` kind on the timeline. The recorder UI lets users record or
stop, cancel the draft take, and mute or unmute other playback audio while
recording.
Draft voiceover clips do not have a final audio resource until the recording is
committed. After the take is saved, the clip behaves like other timeline audio
for playback and export.

## API Reference
| API | Purpose |
| --- | --- |
| `Dock.Button.rememberVoiceoverRecord()` | Adds a dock button that opens the voiceover recorder. |
| `InspectorBar.Button.rememberVoiceover()` | Adds an inspector bar button for selected saved voiceover clips that opens the recorder for another take. |
## Next Steps
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/) — Control audio playback speed from quarter-speed (0.25x) to triple-speed (3.0x) using the CE.SDK Android Engine API.
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Create seamless repeating audio playback for background music and sound effects using CE.SDK's audio looping system.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Compositions"
description: "Combine and arrange multiple elements to create complex, multi-page, or layered design compositions."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition-db709c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/)
---
---
## Related Pages
- [Overview](https://img.ly/docs/cesdk/android/create-composition/overview-5b19c5/) - Combine and arrange multiple elements to create complex, multi-page, or layered design compositions.
- [Multi-Page Layouts](https://img.ly/docs/cesdk/android/create-composition/multi-page-4d2b50/) - Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene.
- [Create a Collage](https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/) - Create collages on Android by loading layout templates and transferring content between pages.
- [Design a Layout](https://img.ly/docs/cesdk/android/create-composition/layout-b66311/) - Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing.
- [Add a Background](https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/) - Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks.
- [Positioning and Alignment](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) - Precisely position, align, distribute, and snap objects using CE.SDK's layout APIs.
- [Group and Ungroup Objects](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually.
- [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Organize design elements using a layer stack for precise control over stacking and visibility.
- [Lock Design](https://img.ly/docs/cesdk/android/create-composition/lock-design-0a81de/) - Protect design elements from unwanted modifications using CE.SDK's scope-based permission system.
- [Blend Modes](https://img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519/) - Apply blend modes to elements to control how colors and layers interact visually.
- [Programmatic Creation](https://img.ly/docs/cesdk/android/create-composition/programmatic-a688bf/) - Build compositions entirely through code with the CE.SDK Engine for automation, batch processing, and headless rendering.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add a Background"
description: "Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Add a Background](https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/)
---
```kotlin file=@cesdk_android_examples/engine-guides-add-background/AddBackground.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GradientColorStop
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
suspend fun addBackground(engine: Engine) = withContext(engine.dispatcher) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
if (engine.block.supportsFill(page)) {
val gradientFill = engine.block.createFill(FillType.LinearGradient)
engine.block.setGradientColorStops(
block = gradientFill,
property = "fill/gradient/colors",
colorStops = listOf(
GradientColorStop(
color = Color.fromRGBA(r = 0.85F, g = 0.75F, b = 0.95F, a = 1F),
stop = 0F,
),
GradientColorStop(
color = Color.fromRGBA(r = 0.7F, g = 0.9F, b = 0.95F, a = 1F),
stop = 1F,
),
),
)
engine.block.setFill(page, fill = gradientFill)
}
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(textBlock, text = "Backgrounds")
engine.block.setTextFontSize(block = textBlock, fontSize = 48F)
engine.block.setWidth(textBlock, value = 280F)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
engine.block.setPositionX(textBlock, value = 66F)
engine.block.setPositionY(textBlock, value = 280F)
engine.block.appendChild(parent = page, child = textBlock)
if (engine.block.supportsBackgroundColor(textBlock)) {
engine.block.setBackgroundColorEnabled(textBlock, enabled = true)
engine.block.setBackgroundColor(
block = textBlock,
color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F),
)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingLeft", value = 16F)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingRight", value = 16F)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingTop", value = 10F)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingBottom", value = 10F)
engine.block.setFloat(textBlock, property = "backgroundColor/cornerRadius", value = 8F)
}
val imageBlock = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(imageBlock, shape = rectShape)
engine.block.setWidth(imageBlock, value = 340F)
engine.block.setHeight(imageBlock, value = 400F)
engine.block.setPositionX(imageBlock, value = 420F)
engine.block.setPositionY(imageBlock, value = 100F)
engine.block.appendChild(parent = page, child = imageBlock)
if (engine.block.supportsFill(imageBlock)) {
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(imageBlock, fill = imageFill)
}
engine.block.forceLoadResources(listOf(textBlock, imageBlock))
}
```
Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-add-background)
CE.SDK provides two approaches for adding backgrounds to design elements. Use fills for pages and graphic blocks, and use the background color API for text blocks that need padding and rounded corners behind their text.
## Setup
Create a scene with a page where we'll apply backgrounds.
```kotlin highlight-android-setup
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
## Fills
Fills are visual content applied to pages and graphic blocks. Supported fill types include solid colors, linear gradients, radial gradients, conical gradients, images, videos, and pixel streams.
### Check Fill Support
Before applying a fill, verify the block supports it with `supportsFill()`. Pages and graphic blocks typically support fills, while text blocks use text-specific APIs for their visible content.
### Apply a Gradient Fill
Create a fill with `createFill()` specifying the type, configure its color stops, then apply it with `setFill()`. The example below creates a linear gradient with two color stops transitioning from pastel purple to light cyan.
```kotlin highlight-android-page-fill
if (engine.block.supportsFill(page)) {
val gradientFill = engine.block.createFill(FillType.LinearGradient)
engine.block.setGradientColorStops(
block = gradientFill,
property = "fill/gradient/colors",
colorStops = listOf(
GradientColorStop(
color = Color.fromRGBA(r = 0.85F, g = 0.75F, b = 0.95F, a = 1F),
stop = 0F,
),
GradientColorStop(
color = Color.fromRGBA(r = 0.7F, g = 0.9F, b = 0.95F, a = 1F),
stop = 1F,
),
),
)
engine.block.setFill(page, fill = gradientFill)
}
```
### Apply an Image Fill
Image fills display images within the block's shape bounds. Create an image fill, set its URI, and apply it to a graphic block.
```kotlin highlight-android-shape-fill
if (engine.block.supportsFill(imageBlock)) {
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(imageBlock, fill = imageFill)
}
```
Image fills automatically scale to cover the shape area.
## Background Color
Background color is a dedicated property available specifically on text blocks. Unlike fills, background colors include configurable padding and corner radius, creating highlighted text effects without additional graphic blocks.
### Apply Background Color
Verify support with `supportsBackgroundColor()`, enable the background color with `setBackgroundColorEnabled()`, then configure its color, padding, and corner radius.
```kotlin highlight-android-background-color
if (engine.block.supportsBackgroundColor(textBlock)) {
engine.block.setBackgroundColorEnabled(textBlock, enabled = true)
engine.block.setBackgroundColor(
block = textBlock,
color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F),
)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingLeft", value = 16F)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingRight", value = 16F)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingTop", value = 10F)
engine.block.setFloat(textBlock, property = "backgroundColor/paddingBottom", value = 10F)
engine.block.setFloat(textBlock, property = "backgroundColor/cornerRadius", value = 8F)
}
```
The padding properties (`backgroundColor/paddingLeft`, `backgroundColor/paddingRight`, `backgroundColor/paddingTop`, `backgroundColor/paddingBottom`) control the space between the text and the background edge. The `backgroundColor/cornerRadius` property rounds the corners.
## Troubleshooting
### Fill Not Visible
If a fill doesn't appear:
- Ensure all color components (r, g, b) are between 0 and 1
- Check that the alpha component is greater than 0
- Verify the block supports fills with `supportsFill()`
### Background Color Not Appearing
If a background color doesn't appear:
- Confirm the block supports it with `supportsBackgroundColor()`
- Verify `setBackgroundColorEnabled(block, true)` was called
- Check that the color's alpha value is greater than 0
### Image Not Loading
If an image fill doesn't display:
- Verify the image URI is accessible to the Android app
- Ensure the app has permission to read local `content://` or file-backed URIs
- Ensure the image format is supported, such as PNG, JPEG, or WebP
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.supportsFill(block=_)` | Check if a block supports fills |
| `engine.block.createFill(fillType=FillType.LinearGradient)` | Create a linear gradient fill for page or shape backgrounds |
| `engine.block.createFill(fillType=FillType.Image)` | Create an image fill for graphic blocks |
| `engine.block.setGradientColorStops(block=_, property="fill/gradient/colors", colorStops=_)` | Set gradient color stops on a gradient fill |
| `engine.block.setString(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on an image fill |
| `engine.block.setFill(block=_, fill=_)` | Apply a fill to a block |
| `engine.block.getFill(block=_)` | Get the fill applied to a block |
| `engine.block.supportsBackgroundColor(block=_)` | Check if a block supports background color |
| `engine.block.setBackgroundColorEnabled(block=_, enabled=_)` | Enable or disable background color |
| `engine.block.isBackgroundColorEnabled(block=_)` | Check if background color is enabled |
| `engine.block.setBackgroundColor(block=_, color=_)` | Set the background color |
| `engine.block.setFloat(block=_, property="backgroundColor/paddingLeft", value=_)` | Set the left padding for the background color |
| `engine.block.setFloat(block=_, property="backgroundColor/paddingRight", value=_)` | Set the right padding for the background color |
| `engine.block.setFloat(block=_, property="backgroundColor/paddingTop", value=_)` | Set the top padding for the background color |
| `engine.block.setFloat(block=_, property="backgroundColor/paddingBottom", value=_)` | Set the bottom padding for the background color |
| `engine.block.setFloat(block=_, property="backgroundColor/cornerRadius", value=_)` | Set the corner radius for the background color |
## Next Steps
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) — Work with RGB, CMYK, and spot colors
- [Fills Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) — Learn about all fill types in depth
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Blend Modes"
description: "Apply blend modes to elements to control how colors and layers interact visually."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Blend Modes](https://img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519/)
---
```kotlin file=@cesdk_android_examples/engine-guides-blend-modes/BlendModes.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.BlendMode
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
suspend fun blendModes(engine: Engine) = withContext(engine.dispatcher) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
fun addColorBlock(
x: Float,
y: Float,
width: Float,
height: Float,
color: RGBAColor,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = width)
engine.block.setHeight(block, value = height)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(block, fill = fill)
engine.block.setFillSolidColor(block = block, color = color)
engine.block.appendChild(parent = page, child = block)
return block
}
// Create a base block first so the top block has content below it to blend with.
addColorBlock(
x = 80F,
y = 80F,
width = 420F,
height = 320F,
color = Color.fromRGBA(r = 0.12F, g = 0.35F, b = 0.95F, a = 1F),
)
val topBlock = addColorBlock(
x = 240F,
y = 180F,
width = 420F,
height = 320F,
color = Color.fromRGBA(r = 1F, g = 0.55F, b = 0.08F, a = 1F),
)
// Scope checks use engine scope key strings.
val canSetBlendMode =
engine.block.supportsBlendMode(topBlock) &&
engine.block.isAllowedByScope(topBlock, key = "layer/blendMode")
println("Can set blend mode: $canSetBlendMode")
if (canSetBlendMode) {
engine.block.setBlendMode(topBlock, blendMode = BlendMode.MULTIPLY)
}
val currentBlendMode = engine.block.getBlendMode(topBlock)
println("Current blend mode: $currentBlendMode")
check(currentBlendMode == BlendMode.MULTIPLY)
val canSetOpacity =
engine.block.supportsOpacity(topBlock) &&
engine.block.isAllowedByScope(topBlock, key = "layer/opacity")
if (canSetOpacity) {
engine.block.setOpacity(topBlock, value = 0.7F)
}
val currentOpacity = engine.block.getOpacity(topBlock)
println("Current opacity: $currentOpacity")
check(currentOpacity == 0.7F)
}
```
Control how design blocks visually blend with underlying layers using CE.SDK's
blend mode system for professional layered compositions.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-blend-modes)
Blend modes control how a block's colors combine with underlying layers, similar to blend modes in Photoshop or other design tools. CE.SDK provides 27 blend modes organized into categories: Normal, Darken, Lighten, Contrast, Inversion, and Component.
This guide covers how to check blend mode support, apply blend modes programmatically, understand the available blend mode options, and combine blend modes with opacity for fine control over layer compositing.
## Checking Blend Mode Support
Before applying a blend mode, verify that the block supports the property and allows writing it. `supportsBlendMode()` checks whether the block has a blend mode, while `isAllowedByScope(block, "layer/blendMode")` checks whether scoped content permits the setter.
```kotlin highlight-android-check-support
// Scope checks use engine scope key strings.
val canSetBlendMode =
engine.block.supportsBlendMode(topBlock) &&
engine.block.isAllowedByScope(topBlock, key = "layer/blendMode")
println("Can set blend mode: $canSetBlendMode")
```
Blend mode support is available for pages, groups, text blocks, and graphic blocks such as graphics with image, video, color, or shape content. Android represents shapes as `DesignBlockType.Graphic` blocks with a `ShapeType`, so check the exact block with `supportsBlendMode()` before setting a mode.
## Setting and Getting Blend Modes
Apply a blend mode with `setBlendMode()` and retrieve the current mode with `getBlendMode()`. Most blocks, including the graphic block in this sample, default to `BlendMode.NORMAL`, which displays the block without any blending effect. Groups default to `BlendMode.PASS_THROUGH` so their children can blend with layers below the group.
```kotlin highlight-android-set-blend-mode
if (canSetBlendMode) {
engine.block.setBlendMode(topBlock, blendMode = BlendMode.MULTIPLY)
}
```
After setting a blend mode, confirm the change by reading it back:
```kotlin highlight-android-get-blend-mode
val currentBlendMode = engine.block.getBlendMode(topBlock)
println("Current blend mode: $currentBlendMode")
check(currentBlendMode == BlendMode.MULTIPLY)
```
## Available Blend Modes
CE.SDK provides 27 blend modes organized into categories, each producing different visual results:
### Normal Modes
- **`BlendMode.PASS_THROUGH`** - Allows children of a group to blend with layers below the group
- **`BlendMode.NORMAL`** - Default mode with no blending effect
### Darken Modes
These modes darken the result by comparing the base and blend colors:
- **`BlendMode.DARKEN`** - Selects the darker of the base and blend colors
- **`BlendMode.MULTIPLY`** - Multiplies colors, producing darker results (great for shadows)
- **`BlendMode.COLOR_BURN`** - Darkens base color by increasing contrast
- **`BlendMode.LINEAR_BURN`** - Darkens base color by decreasing brightness
- **`BlendMode.DARKEN_COLOR`** - Selects the darker color based on luminosity
### Lighten Modes
These modes lighten the result by comparing colors:
- **`BlendMode.LIGHTEN`** - Selects the lighter of the base and blend colors
- **`BlendMode.SCREEN`** - Multiplies the inverse of colors, producing lighter results (great for highlights)
- **`BlendMode.COLOR_DODGE`** - Lightens base color by decreasing contrast
- **`BlendMode.LINEAR_DODGE`** - Lightens base color by increasing brightness
- **`BlendMode.LIGHTEN_COLOR`** - Selects the lighter color based on luminosity
### Contrast Modes
These modes increase midtone contrast:
- **`BlendMode.OVERLAY`** - Combines Multiply and Screen based on the base color
- **`BlendMode.SOFT_LIGHT`** - Similar to Overlay but with a softer effect
- **`BlendMode.HARD_LIGHT`** - Similar to Overlay but based on the blend color
- **`BlendMode.VIVID_LIGHT`** - Burns or dodges colors based on the blend color
- **`BlendMode.LINEAR_LIGHT`** - Increases or decreases brightness based on blend color
- **`BlendMode.PIN_LIGHT`** - Replaces colors based on the blend color
- **`BlendMode.HARD_MIX`** - Reduces colors to white, black, or primary colors
### Inversion Modes
These modes create inverted or subtracted effects:
- **`BlendMode.DIFFERENCE`** - Subtracts the darker from the lighter color
- **`BlendMode.EXCLUSION`** - Similar to Difference with lower contrast
- **`BlendMode.SUBTRACT`** - Subtracts blend color from base color
- **`BlendMode.DIVIDE`** - Divides base color by blend color
### Component Modes
These modes affect specific color components:
- **`BlendMode.HUE`** - Uses the hue of the blend color with base saturation and luminosity
- **`BlendMode.SATURATION`** - Uses the saturation of the blend color
- **`BlendMode.COLOR`** - Uses the hue and saturation of the blend color
- **`BlendMode.LUMINOSITY`** - Uses the luminosity of the blend color
## Combining Blend Modes with Opacity
For finer control over compositing, combine blend modes with opacity. Opacity reduces overall visibility while the blend mode affects color interaction with underlying layers. Check `supportsOpacity()` and the `layer/opacity` scope before calling `setOpacity()`, because support only confirms that the block has an opacity property.
```kotlin highlight-android-set-opacity
val canSetOpacity =
engine.block.supportsOpacity(topBlock) &&
engine.block.isAllowedByScope(topBlock, key = "layer/opacity")
if (canSetOpacity) {
engine.block.setOpacity(topBlock, value = 0.7F)
}
```
Read back the current opacity value to confirm changes or inspect existing state:
```kotlin highlight-android-get-opacity
val currentOpacity = engine.block.getOpacity(topBlock)
println("Current opacity: $currentOpacity")
check(currentOpacity == 0.7F)
```
> **Tip:** Start with full opacity (1.0) when experimenting with blend modes, then reduce
> opacity to soften the effect. Common values are 0.5-0.7 for subtle blending
> effects.
## Troubleshooting
### Blend Mode Has No Visible Effect
- Ensure the block has visible content, such as a color or image fill.
- Place visible blocks below the blended block; blend modes composite with underlying content.
- Read back the active mode with `getBlendMode()` to confirm it was applied to the expected block.
### Cannot Set Blend Mode
- Check `supportsBlendMode()` before calling `setBlendMode()`.
- Confirm `isAllowedByScope(block, "layer/blendMode")` returns `true`; locked template or editor content can support blend modes but deny writes.
- Make sure the `DesignBlock` still exists in the scene when you set the mode.
- Pass one of the Android `BlendMode` enum values listed above.
### Cannot Set Opacity
- Check `supportsOpacity()` before calling `setOpacity()`.
- Confirm `isAllowedByScope(block, "layer/opacity")` returns `true`; scoped content can expose opacity but block the setter.
### Unexpected Blending Results
- Verify the block order: only content below the block contributes to the blend result.
- Match the mode category to the intended effect, such as Darken, Lighten, or Contrast.
- Adjust opacity after setting the blend mode to soften strong results.
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.supportsBlendMode(block=_)` | Check if a block supports blend modes |
| `engine.block.isAllowedByScope(block=_, key="layer/blendMode")` | Check if the current scopes allow `setBlendMode()` |
| `engine.block.setBlendMode(block=_, blendMode=_)` | Set the blend mode for a block |
| `engine.block.getBlendMode(block=_)` | Get the current blend mode of a block |
| `engine.block.supportsOpacity(block=_)` | Check if a block supports opacity |
| `engine.block.isAllowedByScope(block=_, key="layer/opacity")` | Check if the current scopes allow `setOpacity()` |
| `engine.block.setOpacity(block=_, value=_)` | Set the opacity for a block (0-1) |
| `engine.block.getOpacity(block=_)` | Get the current opacity of a block |
## Next Steps
- [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control z-order and visibility of blocks
- [Add a Background](https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/) - Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks.
- [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Combine blocks to apply blend modes to groups
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create a Collage"
description: "Create collages on Android by loading layout templates and transferring content between pages."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Create a Collage](https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/)
---
```kotlin file=@cesdk_android_examples/engine-guides-collage/Collage.kt reference-only
import android.app.Application
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.ShapeType
import kotlin.math.roundToInt
fun collage(
application: Application,
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(Dispatchers.Main).launch {
Engine.init(application)
val engine = Engine.getInstance(id = "ly.img.engine.example")
var engineStarted = false
try {
engineStarted = engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
createAndApplyCollage(engine = engine)
} finally {
if (engineStarted) {
engine.stop()
}
}
}
private suspend fun createAndApplyCollage(engine: Engine) {
val scene = engine.scene.create()
val page = createCollagePage(engine = engine, width = 1080F, height = 1080F)
engine.block.appendChild(parent = scene, child = page)
addImageSlot(
engine = engine,
page = page,
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
x = 32F,
y = 32F,
width = 480F,
height = 480F,
)
addImageSlot(
engine = engine,
page = page,
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg"),
x = 568F,
y = 32F,
width = 480F,
height = 480F,
)
addTextBlock(
engine = engine,
page = page,
text = "Weekend trip",
x = 64F,
y = 830F,
)
val layoutPage = createCollagePage(engine = engine, width = 1080F, height = 1080F)
addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 32F, width = 1016F, height = 496F)
addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 560F, width = 492F, height = 360F)
addImageSlot(engine = engine, page = layoutPage, x = 556F, y = 560F, width = 492F, height = 360F)
addTextBlock(engine = engine, page = layoutPage, text = "Title", x = 64F, y = 952F)
val layoutBlocksString = engine.block.saveToString(blocks = listOf(layoutPage))
engine.block.destroy(layoutPage)
val collagePage = applyCollageLayout(
engine = engine,
currentPage = page,
layoutBlocksString = layoutBlocksString,
addUndoStep = true,
)
engine.scene.zoomToBlock(
block = collagePage,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
}
suspend fun applyCollageLayout(
engine: Engine,
currentPage: DesignBlock,
layoutBlocksString: String,
addUndoStep: Boolean = true,
): DesignBlock {
val previousDestroyScope = engine.editor.getGlobalScope("lifecycle/destroy")
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.ALLOW)
return try {
engine.block.findAllSelected().forEach { selectedBlock ->
engine.block.setSelected(block = selectedBlock, selected = false)
}
var oldPage: DesignBlock? = null
var loadedLayoutPage: DesignBlock? = null
try {
oldPage = engine.block.duplicate(block = currentPage, attachToParent = false)
loadedLayoutPage = engine.block.loadFromString(layoutBlocksString).first()
engine.block.getChildren(currentPage).forEach(engine.block::destroy)
engine.block.getChildren(loadedLayoutPage).forEach { child ->
engine.block.insertChild(parent = currentPage, child = child, index = engine.block.getChildren(currentPage).size)
}
transferCollageContent(engine = engine, fromPage = oldPage, toPage = currentPage)
} finally {
loadedLayoutPage?.let { engine.block.destroy(it) }
oldPage?.let { engine.block.destroy(it) }
}
if (addUndoStep) {
engine.editor.addUndoStep()
}
currentPage
} finally {
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = previousDestroyScope)
}
}
private fun transferCollageContent(
engine: Engine,
fromPage: DesignBlock,
toPage: DesignBlock,
) {
val sourceBlocks = visuallySortedBlocks(
engine = engine,
rootPage = fromPage,
blocks = collectChildrenTree(engine = engine, parent = fromPage),
)
val targetBlocks = visuallySortedBlocks(
engine = engine,
rootPage = toPage,
blocks = collectChildrenTree(engine = engine, parent = toPage),
)
val sourceImages = sourceBlocks.filter { isImageBlock(engine = engine, designBlock = it) }
val targetImages = targetBlocks.filter { isImageBlock(engine = engine, designBlock = it) }
sourceImages.zip(targetImages).forEach { (sourceImage, targetImage) ->
copyImageContent(engine = engine, sourceImage = sourceImage, targetImage = targetImage)
}
val sourceTexts = sourceBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key }
val targetTexts = targetBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key }
sourceTexts.zip(targetTexts).forEach { (sourceText, targetText) ->
copyTextContent(engine = engine, sourceText = sourceText, targetText = targetText)
}
}
private fun collectChildrenTree(
engine: Engine,
parent: DesignBlock,
): List = engine.block.getChildren(parent).flatMap { child ->
listOf(child) + collectChildrenTree(engine = engine, parent = child)
}
private data class UntransformedPagePosition(
val x: Float,
val y: Float,
)
private data class PositionedBlock(
val block: DesignBlock,
val position: UntransformedPagePosition,
)
private fun visuallySortedBlocks(
engine: Engine,
rootPage: DesignBlock,
blocks: List,
): List = blocks
.map { designBlock ->
PositionedBlock(
block = designBlock,
position = untransformedPagePosition(engine = engine, rootPage = rootPage, designBlock = designBlock),
)
}
.sortedWith(
compareBy { it.position.y.roundToInt() }
.thenBy { it.position.x.roundToInt() },
)
.map { it.block }
private fun untransformedPagePosition(
engine: Engine,
rootPage: DesignBlock,
designBlock: DesignBlock,
): UntransformedPagePosition {
var x = engine.block.getPositionX(designBlock)
var y = engine.block.getPositionY(designBlock)
var parent = engine.block.getParent(designBlock)
// This local-offset sort is for unrotated and unscaled layout slots.
while (parent != null && parent != rootPage) {
x += engine.block.getPositionX(parent)
y += engine.block.getPositionY(parent)
parent = engine.block.getParent(parent)
}
return UntransformedPagePosition(x = x, y = y)
}
private fun copyImageContent(
engine: Engine,
sourceImage: DesignBlock,
targetImage: DesignBlock,
) {
val sourceFill = engine.block.getFill(sourceImage)
val targetFill = engine.block.getFill(targetImage)
// Image fills use a generic property key, but Android keeps the value typed as Uri.
engine.block.setUri(
block = targetFill,
property = "fill/image/imageFileURI",
value = engine.block.getUri(sourceFill, property = "fill/image/imageFileURI"),
)
engine.block.setSourceSet(
block = targetFill,
property = "fill/image/sourceSet",
sourceSet = engine.block.getSourceSet(sourceFill, property = "fill/image/sourceSet"),
)
if (engine.block.supportsPlaceholderBehavior(sourceImage)) {
engine.block.setPlaceholderBehaviorEnabled(
block = targetImage,
enabled = engine.block.isPlaceholderBehaviorEnabled(sourceImage),
)
}
engine.block.resetCrop(targetImage)
}
private fun copyTextContent(
engine: Engine,
sourceText: DesignBlock,
targetText: DesignBlock,
) {
// Reading plain text still uses property access; replaceText keeps the write type-safe.
engine.block.replaceText(
block = targetText,
text = engine.block.getString(sourceText, property = "text/text"),
)
runCatching {
engine.block.setFont(
block = targetText,
fontFileUri = engine.block.getUri(sourceText, property = "text/fontFileUri"),
typeface = engine.block.getTypeface(sourceText),
)
}
engine.block.getTextColors(sourceText).firstOrNull()?.let { color ->
engine.block.setTextColor(block = targetText, color = color)
}
}
private fun isImageBlock(
engine: Engine,
designBlock: DesignBlock,
): Boolean = engine.block.supportsFill(designBlock) && engine.block.getType(engine.block.getFill(designBlock)) == FillType.Image.key
private fun createCollagePage(
engine: Engine,
width: Float,
height: Float,
): DesignBlock {
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = width)
engine.block.setHeight(page, value = height)
return page
}
private fun addImageSlot(
engine: Engine,
page: DesignBlock,
uri: Uri? = null,
x: Float,
y: Float,
width: Float,
height: Float,
): DesignBlock {
val image = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(image, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(image, value = x)
engine.block.setPositionY(image, value = y)
engine.block.setWidth(image, value = width)
engine.block.setHeight(image, value = height)
val fill = engine.block.createFill(FillType.Image)
if (uri != null) {
// Image fills use a generic property key, but Android keeps the value typed as Uri.
engine.block.setUri(block = fill, property = "fill/image/imageFileURI", value = uri)
}
engine.block.setFill(block = image, fill = fill)
engine.block.appendChild(parent = page, child = image)
return image
}
private fun addTextBlock(
engine: Engine,
page: DesignBlock,
text: String,
x: Float,
y: Float,
): DesignBlock {
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(block = textBlock, text = text)
engine.block.setPositionX(textBlock, value = x)
engine.block.setPositionY(textBlock, value = y)
engine.block.setWidth(textBlock, value = 640F)
engine.block.setHeight(textBlock, value = 80F)
engine.block.setTextColor(
block = textBlock,
color = Color.fromRGBA(r = 0.08F, g = 0.08F, b = 0.08F, a = 1F),
)
engine.block.appendChild(parent = page, child = textBlock)
return textBlock
}
```
Create a collage on Android by loading a layout page and transferring existing images and text into the new structure.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-collage)
Layouts are predefined page structures that arrange images and text in a composition. Unlike templates, which usually replace the whole scene, this workflow keeps the user's content and maps it into a new layout.
The Android example uses the Engine directly. You can call the same layout application function from your own Compose UI, an asset source callback, or any other Android workflow that lets users choose a layout.
## What You'll Learn
In this guide, you will learn how to:
- Define a layout page that contains image slots and text placeholders.
- Load the layout from a saved block string.
- Replace the current page structure while preserving existing image and text content.
- Sort blocks visually so content maps top-to-bottom and left-to-right.
## When to Use Layouts
Use layout-based collages when your app needs to keep the current content but change its arrangement. Common examples include:
- Photo collages
- Grid layouts
- Magazine spreads
- Social media posts
## Difference Between Layouts and Templates
Layouts can be represented as [custom assets](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/), but the apply behavior is different from a full template load:
- **Templates** load a complete design and replace the current scene.
- **Layouts** provide a new page structure while your code transfers the current content into matching slots.
Preserving assets while changing the layout is app logic. CE.SDK provides the block, fill, text, and scene APIs you need to implement that logic.
## How Collages Work
When a user chooses a collage layout, your app performs this sequence:
1. Load a layout page from a saved block string.
2. Duplicate the current page as a temporary content source.
3. Replace the current page's children with the layout's children.
4. Copy images and text from the old page into the new layout in visual order.
5. Clean up temporary blocks and add an undo step.
Visual order is important. The sample sorts simple, untransformed layout slots by their accumulated page coordinates so content maps predictably between the old page and the new layout.
## Create Page Helpers
The sample uses small helpers to create pages, image slots, and text blocks. The page helper only sets page dimensions.
```kotlin highlight-android-create-page-helper
private fun createCollagePage(
engine: Engine,
width: Float,
height: Float,
): DesignBlock {
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = width)
engine.block.setHeight(page, value = height)
return page
}
```
Image slots use graphic blocks with rectangular shapes and image fills. In your app, the `Uri` values can come from local resources, remote media, or user-selected files.
```kotlin highlight-android-image-slot-helper
private fun addImageSlot(
engine: Engine,
page: DesignBlock,
uri: Uri? = null,
x: Float,
y: Float,
width: Float,
height: Float,
): DesignBlock {
val image = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(image, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(image, value = x)
engine.block.setPositionY(image, value = y)
engine.block.setWidth(image, value = width)
engine.block.setHeight(image, value = height)
val fill = engine.block.createFill(FillType.Image)
if (uri != null) {
// Image fills use a generic property key, but Android keeps the value typed as Uri.
engine.block.setUri(block = fill, property = "fill/image/imageFileURI", value = uri)
}
engine.block.setFill(block = image, fill = fill)
engine.block.appendChild(parent = page, child = image)
return image
}
```
Text blocks keep the layout example readable by isolating the text creation and initial color setup.
```kotlin highlight-android-text-block-helper
private fun addTextBlock(
engine: Engine,
page: DesignBlock,
text: String,
x: Float,
y: Float,
): DesignBlock {
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(block = textBlock, text = text)
engine.block.setPositionX(textBlock, value = x)
engine.block.setPositionY(textBlock, value = y)
engine.block.setWidth(textBlock, value = 640F)
engine.block.setHeight(textBlock, value = 80F)
engine.block.setTextColor(
block = textBlock,
color = Color.fromRGBA(r = 0.08F, g = 0.08F, b = 0.08F, a = 1F),
)
engine.block.appendChild(parent = page, child = textBlock)
return textBlock
}
```
## Define a Layout
A layout is a page with positioned image slots and optional text blocks. In production, save these pages as scene or block files and load them from your app bundle, backend, or asset source.
```kotlin highlight-android-define-layout
val layoutPage = createCollagePage(engine = engine, width = 1080F, height = 1080F)
addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 32F, width = 1016F, height = 496F)
addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 560F, width = 492F, height = 360F)
addImageSlot(engine = engine, page = layoutPage, x = 556F, y = 560F, width = 492F, height = 360F)
addTextBlock(engine = engine, page = layoutPage, text = "Title", x = 64F, y = 952F)
val layoutBlocksString = engine.block.saveToString(blocks = listOf(layoutPage))
engine.block.destroy(layoutPage)
```
The example saves the layout page with `block.saveToString()` and later restores it with `block.loadFromString()`. The same pattern works when the string comes from a remote layout file.
## Apply the Collage
Call the app-owned layout helper with the current page, the Engine instance, and the saved layout data. Pass `addUndoStep = true` when the action should become a single undoable editor operation.
```kotlin highlight-android-apply-layout
val collagePage = applyCollageLayout(
engine = engine,
currentPage = page,
layoutBlocksString = layoutBlocksString,
addUndoStep = true,
)
```
The function returns the page that now contains the collage structure and transferred content.
## Replace the Page Structure
Applying a layout temporarily allows block deletion, clears the current selection, duplicates the old page, loads the layout page, and moves the layout children into the current page.
```kotlin highlight-android-layout-workflow
suspend fun applyCollageLayout(
engine: Engine,
currentPage: DesignBlock,
layoutBlocksString: String,
addUndoStep: Boolean = true,
): DesignBlock {
val previousDestroyScope = engine.editor.getGlobalScope("lifecycle/destroy")
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.ALLOW)
return try {
engine.block.findAllSelected().forEach { selectedBlock ->
engine.block.setSelected(block = selectedBlock, selected = false)
}
var oldPage: DesignBlock? = null
var loadedLayoutPage: DesignBlock? = null
try {
oldPage = engine.block.duplicate(block = currentPage, attachToParent = false)
loadedLayoutPage = engine.block.loadFromString(layoutBlocksString).first()
engine.block.getChildren(currentPage).forEach(engine.block::destroy)
engine.block.getChildren(loadedLayoutPage).forEach { child ->
engine.block.insertChild(parent = currentPage, child = child, index = engine.block.getChildren(currentPage).size)
}
transferCollageContent(engine = engine, fromPage = oldPage, toPage = currentPage)
} finally {
loadedLayoutPage?.let { engine.block.destroy(it) }
oldPage?.let { engine.block.destroy(it) }
}
if (addUndoStep) {
engine.editor.addUndoStep()
}
currentPage
} finally {
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = previousDestroyScope)
}
}
```
Key details:
- Store and restore the previous `lifecycle/destroy` scope so the surrounding editor state remains unchanged.
- Keep the duplicate backup unattached so it cannot leak into the scene if transfer fails.
- Destroy the temporary old and layout pages in cleanup after transfer.
## Transfer Content
Collect all descendants from the old page and the new page, sort both lists by visual position, then pair source and target blocks by type.
```kotlin highlight-android-transfer-content
private fun transferCollageContent(
engine: Engine,
fromPage: DesignBlock,
toPage: DesignBlock,
) {
val sourceBlocks = visuallySortedBlocks(
engine = engine,
rootPage = fromPage,
blocks = collectChildrenTree(engine = engine, parent = fromPage),
)
val targetBlocks = visuallySortedBlocks(
engine = engine,
rootPage = toPage,
blocks = collectChildrenTree(engine = engine, parent = toPage),
)
val sourceImages = sourceBlocks.filter { isImageBlock(engine = engine, designBlock = it) }
val targetImages = targetBlocks.filter { isImageBlock(engine = engine, designBlock = it) }
sourceImages.zip(targetImages).forEach { (sourceImage, targetImage) ->
copyImageContent(engine = engine, sourceImage = sourceImage, targetImage = targetImage)
}
val sourceTexts = sourceBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key }
val targetTexts = targetBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key }
sourceTexts.zip(targetTexts).forEach { (sourceText, targetText) ->
copyTextContent(engine = engine, sourceText = sourceText, targetText = targetText)
}
}
```
If the source has more images than the layout has slots, extra images are ignored. If the layout has more slots, the remaining slots keep their placeholder content.
## Sort Blocks Visually
Block positions are local to their parent. For unrotated and unscaled layout slots, the example accumulates each block's ancestor offsets, rounds those coordinates, then sorts by Y before X. If your layout slots live under rotated or scaled parents, use the Engine's global bounding-box APIs for the sort instead of this local-offset helper.
```kotlin highlight-android-visual-sort
private fun collectChildrenTree(
engine: Engine,
parent: DesignBlock,
): List = engine.block.getChildren(parent).flatMap { child ->
listOf(child) + collectChildrenTree(engine = engine, parent = child)
}
private data class UntransformedPagePosition(
val x: Float,
val y: Float,
)
private data class PositionedBlock(
val block: DesignBlock,
val position: UntransformedPagePosition,
)
private fun visuallySortedBlocks(
engine: Engine,
rootPage: DesignBlock,
blocks: List,
): List = blocks
.map { designBlock ->
PositionedBlock(
block = designBlock,
position = untransformedPagePosition(engine = engine, rootPage = rootPage, designBlock = designBlock),
)
}
.sortedWith(
compareBy { it.position.y.roundToInt() }
.thenBy { it.position.x.roundToInt() },
)
.map { it.block }
private fun untransformedPagePosition(
engine: Engine,
rootPage: DesignBlock,
designBlock: DesignBlock,
): UntransformedPagePosition {
var x = engine.block.getPositionX(designBlock)
var y = engine.block.getPositionY(designBlock)
var parent = engine.block.getParent(designBlock)
// This local-offset sort is for unrotated and unscaled layout slots.
while (parent != null && parent != rootPage) {
x += engine.block.getPositionX(parent)
y += engine.block.getPositionY(parent)
parent = engine.block.getParent(parent)
}
return UntransformedPagePosition(x = x, y = y)
}
```
Keep layout slots in distinct positions when possible. Blocks with the same accumulated Y position map left-to-right.
## Copy Images
Copy the image URI with Android's `Uri` property APIs and copy the source set from the old image fill to the new image fill. Resetting the crop lets the image fit the new slot dimensions.
```kotlin highlight-android-copy-images
private fun copyImageContent(
engine: Engine,
sourceImage: DesignBlock,
targetImage: DesignBlock,
) {
val sourceFill = engine.block.getFill(sourceImage)
val targetFill = engine.block.getFill(targetImage)
// Image fills use a generic property key, but Android keeps the value typed as Uri.
engine.block.setUri(
block = targetFill,
property = "fill/image/imageFileURI",
value = engine.block.getUri(sourceFill, property = "fill/image/imageFileURI"),
)
engine.block.setSourceSet(
block = targetFill,
property = "fill/image/sourceSet",
sourceSet = engine.block.getSourceSet(sourceFill, property = "fill/image/sourceSet"),
)
if (engine.block.supportsPlaceholderBehavior(sourceImage)) {
engine.block.setPlaceholderBehaviorEnabled(
block = targetImage,
enabled = engine.block.isPlaceholderBehaviorEnabled(sourceImage),
)
}
engine.block.resetCrop(targetImage)
}
```
The placeholder behavior calls preserve placeholder state when the source block supports it.
## Identify Image Slots
The transfer code treats a graphic block with an image fill as an image slot. This keeps the mapping independent from custom metadata or asset-source IDs.
```kotlin highlight-android-image-slot-check
private fun isImageBlock(
engine: Engine,
designBlock: DesignBlock,
): Boolean = engine.block.supportsFill(designBlock) && engine.block.getType(engine.block.getFill(designBlock)) == FillType.Image.key
```
## Copy Text
Text transfer reads the source string, writes it with `replaceText()`, and copies the first text color with the typed text color APIs. Reading plain text still uses property access because Android does not expose a dedicated text getter. Typeface and font file URI preservation is best-effort so unresolved fonts do not block the text content transfer.
```kotlin highlight-android-copy-text
private fun copyTextContent(
engine: Engine,
sourceText: DesignBlock,
targetText: DesignBlock,
) {
// Reading plain text still uses property access; replaceText keeps the write type-safe.
engine.block.replaceText(
block = targetText,
text = engine.block.getString(sourceText, property = "text/text"),
)
runCatching {
engine.block.setFont(
block = targetText,
fontFileUri = engine.block.getUri(sourceText, property = "text/fontFileUri"),
typeface = engine.block.getTypeface(sourceText),
)
}
engine.block.getTextColors(sourceText).firstOrNull()?.let { color ->
engine.block.setTextColor(block = targetText, color = color)
}
}
```
Use the same visual pairing rule for text as for images so captions and titles stay in their expected order.
## Connect It to Your UI
The Engine workflow is UI-agnostic. A typical Android integration stores each layout with:
| Field | Purpose |
| --- | --- |
| `id` | Stable layout identifier for your app |
| `label` | Display name in your own layout picker |
| `uri` | Scene or block file containing the layout page |
| `thumbnailUri` | Preview image shown in your UI |
When a user selects a layout, load its file, pass the saved block string into your app's `applyCollageLayout(engine = ...)` helper, and keep the UI code separate from the content transfer logic.
## Troubleshooting
| Issue | What to Check |
| --- | --- |
| Layout does not apply | Verify the saved layout string loads with `engine.block.loadFromString(block=_)` and returns a page block before moving children into the current page. |
| Content maps to the wrong slots | Keep the sortable image and text slots unrotated and unscaled, and check the accumulated coordinates used by visual sorting. Blocks with the same accumulated Y coordinate map left-to-right, so keep slots distinct enough for predictable ordering. |
| Images stay empty or lose variants | Ensure both source and target blocks use image fills, then copy the image URI with `getUri()` / `setUri()` and copy `fill/image/sourceSet` before calling `engine.block.resetCrop(block=_)`. |
| Text copies without its expected font | Treat font transfer as best-effort. Copy the text string first, then wrap `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` so unresolved font URIs do not block the collage update. |
| Undo or cleanup behaves unexpectedly | Restore the previous `lifecycle/destroy` scope in a `finally` block, destroy temporary duplicate/layout pages, and add the undo step only after transfer completes. |
| Slot counts do not match | Pair source and target blocks with `zip`. Extra source content is ignored, and extra layout slots keep their placeholder content. |
## API Reference
| API | Category | Purpose |
| --- | --- | --- |
| `engine.block.saveToString(blocks=_)` | Layout data | Serialize a layout page so it can be stored as a layout asset or file. |
| `engine.block.loadFromString(block=_)` | Layout data | Load a saved layout page before moving its children into the current page. |
| `engine.block.duplicate(block=_, attachToParent=_)` | Backup | Copy the current page without attaching the duplicate to the scene. |
| `engine.block.getChildren(block=_)` | Hierarchy | Read child blocks before clearing or moving a page structure. |
| `engine.block.insertChild(parent=_, child=_, index=_)` | Hierarchy | Move layout children into the current page in order. |
| `engine.block.destroy(block=_)` | Lifecycle | Remove old children and temporary pages during cleanup. |
| `engine.editor.getGlobalScope(key="lifecycle/destroy")` | Lifecycle | Store the current deletion scope before the layout swap. |
| `engine.editor.setGlobalScope(key="lifecycle/destroy", globalScope=_)` | Lifecycle | Temporarily allow block deletion, then restore the previous scope. |
| `engine.block.findAllSelected()` | Selection | Find selected blocks so the layout change can clear selection first. |
| `engine.block.setSelected(block=_, selected=_)` | Selection | Deselect blocks before replacing the page structure. |
| `engine.block.getFill(block=_)` | Images | Access the image fill that stores URI and source-set properties. |
| `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Images | Read the source image URI from an image fill. |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Images | Copy the image URI to the target fill. |
| `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Images | Read responsive image variants from the source fill. |
| `engine.block.setSourceSet(block=_, property="fill/image/sourceSet", sourceSet=_)` | Images | Preserve responsive image variants on the target fill. |
| `engine.block.resetCrop(block=_)` | Images | Refit the transferred image inside the new slot. |
| `engine.block.supportsPlaceholderBehavior(block=_)` | Placeholders | Check whether placeholder state can be copied. |
| `engine.block.setPlaceholderBehaviorEnabled(block=_, enabled=_)` | Placeholders | Apply the source placeholder behavior to the target image block. |
| `engine.block.getString(block=_, property="text/text")` | Text | Read text content from the source block. Android exposes typed text write and style APIs, but not a dedicated plain-text getter. |
| `engine.block.replaceText(block=_, text=_)` | Text | Copy text content into the target block. |
| `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Text | Preserve the source font when the URI and typeface resolve. |
| `engine.block.getTextColors(block=_)` | Text | Read the source text colors. |
| `engine.block.setTextColor(block=_, color=_)` | Text | Apply the source text color to the target block. |
| `engine.block.getParent(block=_)` | Sorting | Walk ancestors while accumulating untransformed page coordinates. |
| `engine.block.getPositionX(block=_)` | Sorting | Read local X positions while calculating left-to-right ordering. |
| `engine.block.getPositionY(block=_)` | Sorting | Read local Y positions while calculating top-to-bottom ordering. |
| `engine.editor.addUndoStep()` | Undo | Commit the completed layout swap as one undoable operation. |
## Next Steps
Now that you can create collages with layouts, explore these related guides:
- [Templates Overview](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) - Work with templates instead of layouts
- [Basics](https://img.ly/docs/cesdk/android/import-media/asset-panel/basics-f29078/) - Explore how the asset library connects sources, categories, and dock buttons
- [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) - Show or hide side panels
- [Insert Images](https://img.ly/docs/cesdk/android/insert-media/images-63848a/) - Manage image blocks and fills
- [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Load and save scenes
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Group and Ungroup Objects"
description: "Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Group and Ungroup Objects](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/)
---
```kotlin file=@cesdk_android_examples/engine-guides-grouping/Grouping.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
suspend fun grouping(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val block1 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block1, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block1, value = 120F)
engine.block.setHeight(block1, value = 120F)
engine.block.setPositionX(block1, value = 200F)
engine.block.setPositionY(block1, value = 240F)
engine.block.setFill(block1, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block1,
color = Color.fromRGBA(r = 0.4F, g = 0.6F, b = 0.9F, a = 1.0F),
)
engine.block.appendChild(parent = page, child = block1)
val block2 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block2, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block2, value = 120F)
engine.block.setHeight(block2, value = 120F)
engine.block.setPositionX(block2, value = 340F)
engine.block.setPositionY(block2, value = 240F)
engine.block.setFill(block2, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block2,
color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.4F, a = 1.0F),
)
engine.block.appendChild(parent = page, child = block2)
val block3 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block3, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block3, value = 120F)
engine.block.setHeight(block3, value = 120F)
engine.block.setPositionX(block3, value = 480F)
engine.block.setPositionY(block3, value = 240F)
engine.block.setFill(block3, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block3,
color = Color.fromRGBA(r = 0.5F, g = 0.8F, b = 0.5F, a = 1.0F),
)
engine.block.appendChild(parent = page, child = block3)
val blocks = listOf(block1, block2, block3)
val canGroup = engine.block.isGroupable(blocks)
val group = if (canGroup) {
engine.block.group(blocks)
} else {
error("Select blocks that can be grouped before calling group(...).")
}
engine.block.setSelected(group, selected = true)
engine.block.enterGroup(group)
// enterGroup selects the first member by default; select another member when needed.
engine.block.select(block2)
engine.block.exitGroup(block2)
val allGroups = engine.block.findByType(DesignBlockType.Group)
val groupType = engine.block.getType(group)
val members = engine.block.getChildren(group)
engine.block.ungroup(group)
val groupsAfterUngroup = engine.block.findByType(DesignBlockType.Group)
check(allGroups.isNotEmpty())
check(groupType == DesignBlockType.Group.key)
check(members.size == blocks.size)
check(groupsAfterUngroup.isEmpty())
}
```
Group multiple blocks to move, scale, and transform them as a single unit;
ungroup to edit them individually.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-grouping)
Use groups when related blocks should stay linked through later edits. Nested groups help model larger composition units, such as cards, badges, or reusable layout sections.
This guide covers how to check if blocks can be grouped, create and dissolve groups, navigate into groups to select individual members, and find existing groups in a scene.
## Understanding Groups
Groups are blocks with type `DesignBlockType.Group`, and their child blocks are the group members. Android exposes groups through the same block APIs used for other containers, so you can inspect a group's type and children after creation.
Use nested groups only after each candidate selection passes `engine.block.isGroupable(...)`; blocks that already belong to another group need to be ungrouped first.
## Create the Blocks
We first create several graphic blocks that we'll group together. Each block has a different color fill to make them visually distinct.
```kotlin highlight-android-create-blocks
val block1 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block1, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block1, value = 120F)
engine.block.setHeight(block1, value = 120F)
engine.block.setPositionX(block1, value = 200F)
engine.block.setPositionY(block1, value = 240F)
engine.block.setFill(block1, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block1,
color = Color.fromRGBA(r = 0.4F, g = 0.6F, b = 0.9F, a = 1.0F),
)
engine.block.appendChild(parent = page, child = block1)
```
The remaining two blocks are created with the same pattern and appended to the page.
## Check If Blocks Can Be Grouped
Before grouping, verify that the selected blocks can be grouped using `engine.block.isGroupable(...)`. For valid block IDs, this method returns `true` only when the list is non-empty, no block is a scene or page, no block already belongs to another group, and all blocks are attached to the same page or all are detached from pages. Invalid block IDs fail instead of returning `false`.
```kotlin highlight-android-check-groupable
val blocks = listOf(block1, block2, block3)
val canGroup = engine.block.isGroupable(blocks)
```
## Create a Group
Use `engine.block.group(...)` to combine multiple blocks into a new group. Guard the call with the `isGroupable(...)` result so invalid selections do not reach `group(...)`. When the guard passes, the method returns the ID of the newly created group block, and the group inherits the combined bounding box of its members. The later snippets use that returned group.
```kotlin highlight-android-create-group
val group = if (canGroup) {
engine.block.group(blocks)
} else {
error("Select blocks that can be grouped before calling group(...).")
}
engine.block.setSelected(group, selected = true)
```
## Navigate Group Selection
CE.SDK provides methods to navigate into and out of groups while editing.
### Enter a Group
When a group is selected, use `engine.block.enterGroup(...)` to enter editing mode for that group. This allows you to select and modify individual members within the group.
```kotlin highlight-android-enter-group
engine.block.enterGroup(group)
// enterGroup selects the first member by default; select another member when needed.
engine.block.select(block2)
```
### Exit a Group
When editing a member inside a group, use `engine.block.exitGroup(...)` to return selection to the parent group. This method takes a member block ID and selects its parent group.
```kotlin highlight-android-exit-group
engine.block.exitGroup(block2)
```
## Find and Inspect Groups
Discover groups in a scene and inspect their contents using `engine.block.findByType(...)`, `engine.block.getType(...)`, and `engine.block.getChildren(...)`.
```kotlin highlight-android-find-groups
val allGroups = engine.block.findByType(DesignBlockType.Group)
val groupType = engine.block.getType(group)
val members = engine.block.getChildren(group)
```
Use `engine.block.findByType(DesignBlockType.Group)` to get all group blocks in the current scene. Use `engine.block.getType(...)` to check if a specific block is a group by comparing the result to `DesignBlockType.Group.key`. Use `engine.block.getChildren(...)` to get the member blocks of a group.
## Ungroup Blocks
Use `engine.block.ungroup(...)` to dissolve a group and release its children back to the parent container. The children maintain their current positions in the scene.
```kotlin highlight-android-ungroup
engine.block.ungroup(group)
val groupsAfterUngroup = engine.block.findByType(DesignBlockType.Group)
```
## Troubleshooting
### Blocks Cannot Be Grouped
If `engine.block.isGroupable(...)` returns `false`:
- Check that none of the blocks is a scene or page block, because scenes and pages cannot be grouped
- Check that all blocks are attached to the same page, or that all are detached from pages
- Check whether a block already belongs to a group by calling `engine.block.getParent(...)`, then checking that parent with `engine.block.getType(...) == DesignBlockType.Group.key`
If the list contains an invalid block ID, Android reports an error instead of returning `false`. Recreate the selection from current blocks before calling `isGroupable(...)`.
### Enter Group Has No Effect
If `engine.block.enterGroup(...)` does not change selection:
- Verify that the selected block is a group with `engine.block.getType(...)`
- Ensure the `editor/select` scope is enabled for the current editing context
### Group Not Visible After Creation
If a newly created group is not visible:
- Check that each member block was visible before grouping
- Verify the group's opacity with `engine.block.getOpacity(...)`
## API Reference
| Method | Description |
| ----------------------------------------------------- | ----------------------------------------- |
| `engine.block.create(blockType=_)` | Create a block |
| `engine.block.createShape(type=_)` | Create a shape block |
| `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block |
| `engine.block.setWidth(block=_, value=_)` | Set the block width |
| `engine.block.setHeight(block=_, value=_)` | Set the block height |
| `engine.block.setPositionX(block=_, value=_)` | Set the block's x position |
| `engine.block.setPositionY(block=_, value=_)` | Set the block's y position |
| `engine.block.createFill(fillType=_)` | Create a fill block |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill to a block |
| `engine.block.setFillSolidColor(block=_, color=_)` | Set a color fill |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an RGBA color value |
| `engine.block.appendChild(parent=_, child=_)` | Add a block to a parent |
| `engine.block.isGroupable(blocks=_)` | Check if blocks can be grouped together |
| `engine.block.group(blocks=_)` | Create a group from multiple blocks |
| `engine.block.setSelected(block=_, selected=_)` | Add a block to the selection |
| `engine.block.enterGroup(block=_)` | Enter group editing mode |
| `engine.block.select(block=_)` | Select one block |
| `engine.block.exitGroup(block=_)` | Exit group editing mode |
| `engine.block.findByType(type=DesignBlockType.Group)` | Find all blocks of a specific type |
| `engine.block.getType(block=_)` | Get the type string of a block |
| `engine.block.getParent(block=_)` | Get the parent block |
| `engine.block.getChildren(block=_)` | Get child blocks of a container |
| `engine.block.ungroup(block=_)` | Dissolve a group and release its children |
| `engine.block.getOpacity(block=_)` | Get a block's opacity |
## Next Steps
- [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control z-order and visibility of blocks
- [Position and Align](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) - Arrange blocks precisely on the canvas
- [Lock Design](https://img.ly/docs/cesdk/android/create-composition/lock-design-0a81de/) - Protect design elements from unwanted modifications
using CE.SDK's scope-based permission system. Control which properties users can
edit at both global and block levels.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Layer Management"
description: "Organize design elements using a layer stack for precise control over stacking and visibility."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Layers](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/)
---
```kotlin file=@cesdk_android_examples/engine-guides-layer-management/LayerManagement.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
suspend fun layerManagement(engine: Engine) = withContext(engine.dispatcher) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val backBlock = createLayerBlock(
engine = engine,
x = 120F,
y = 120F,
color = Color.fromRGBA(r = 0.10F, g = 0.40F, b = 0.95F, a = 1F),
)
val middleBlock = createLayerBlock(
engine = engine,
x = 190F,
y = 190F,
color = Color.fromRGBA(r = 0.20F, g = 0.75F, b = 0.45F, a = 1F),
)
val frontBlock = createLayerBlock(
engine = engine,
x = 260F,
y = 260F,
color = Color.fromRGBA(r = 0.95F, g = 0.30F, b = 0.25F, a = 1F),
)
engine.block.appendChild(parent = page, child = backBlock)
engine.block.appendChild(parent = page, child = middleBlock)
engine.block.appendChild(parent = page, child = frontBlock)
val parent = engine.block.getParent(middleBlock)
val children = engine.block.getChildren(page)
val insertedBlock = createLayerBlock(
engine = engine,
x = 330F,
y = 330F,
color = Color.fromRGBA(r = 0.98F, g = 0.78F, b = 0.20F, a = 1F),
)
engine.block.insertChild(parent = page, child = insertedBlock, index = 0)
engine.block.bringToFront(backBlock)
engine.block.sendToBack(frontBlock)
engine.block.bringForward(insertedBlock)
engine.block.sendBackward(middleBlock)
val isVisible = engine.block.isVisible(insertedBlock)
engine.block.setVisible(block = insertedBlock, visible = !isVisible)
engine.block.setVisible(block = insertedBlock, visible = true)
val duplicate = engine.block.duplicate(middleBlock)
engine.block.setPositionX(duplicate, value = 430F)
engine.block.setPositionY(duplicate, value = 140F)
val duplicateIsValid = engine.block.isValid(duplicate)
engine.block.destroy(frontBlock)
val frontBlockIsValid = engine.block.isValid(frontBlock)
// Keep values live for the compiled guide sample.
check(parent == page)
check(children.containsAll(listOf(backBlock, middleBlock, frontBlock)))
check(duplicateIsValid)
check(!frontBlockIsValid)
engine.scene.zoomToBlock(
page,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
}
private fun createLayerBlock(
engine: Engine,
x: Float,
y: Float,
color: RGBAColor,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block, value = 180F)
engine.block.setHeight(block, value = 180F)
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = block, color = color)
return block
}
```
Organize design elements in CE.SDK using a hierarchical layer stack to control
stacking order, visibility, and element relationships.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-layer-management)
Design elements in CE.SDK are organized in a hierarchical parent-child
structure. Children of a block render in order, with the last child appearing on
top. This guide covers how to navigate the block hierarchy, reorder elements,
toggle visibility, duplicate blocks, and remove blocks.
## Using the Built-in Layer Panel UI
The Android editor exposes layer controls for the selected block in the layer
sheet. The sheet includes z-order actions, duplicate, delete, opacity, and blend
mode controls when the selected block supports them.
For a custom layer panel or automation workflow, use the CreativeEngine block
APIs shown below. They operate on the same scene hierarchy that the editor UI
manipulates.
## Creating Visual Blocks
To demonstrate layer ordering, we create colored rectangle blocks that overlap
on the page. Each block is a graphic with a rectangle shape, color fill, size,
and position.
```kotlin highlight-android-create-block
val backBlock = createLayerBlock(
engine = engine,
x = 120F,
y = 120F,
color = Color.fromRGBA(r = 0.10F, g = 0.40F, b = 0.95F, a = 1F),
)
val middleBlock = createLayerBlock(
engine = engine,
x = 190F,
y = 190F,
color = Color.fromRGBA(r = 0.20F, g = 0.75F, b = 0.45F, a = 1F),
)
val frontBlock = createLayerBlock(
engine = engine,
x = 260F,
y = 260F,
color = Color.fromRGBA(r = 0.95F, g = 0.30F, b = 0.25F, a = 1F),
)
```
The sample uses one helper to keep block creation consistent:
```kotlin highlight-android-create-helper
private fun createLayerBlock(
engine: Engine,
x: Float,
y: Float,
color: RGBAColor,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block, value = 180F)
engine.block.setHeight(block, value = 180F)
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = block, color = color)
return block
}
```
## Navigating the Block Hierarchy Programmatically
CE.SDK organizes blocks in a parent-child tree. Every block can have one parent
and multiple children.
### Getting a Block's Parent
Retrieve the parent of any block using `engine.block.getParent()`. This returns
the parent's block ID, or `null` if the block has no parent.
```kotlin highlight-android-get-parent
val parent = engine.block.getParent(middleBlock)
```
### Listing Child Blocks
Get all direct children of a block using `engine.block.getChildren()`. Children
are returned sorted in rendering order, where the last child renders in front of
other children.
```kotlin highlight-android-get-children
val children = engine.block.getChildren(page)
```
This is useful when iterating over all elements on a page or within a group.
## Adding and Positioning Blocks
When you create a new block, it exists independently until you add it to the
hierarchy. Attach blocks by appending them to the end or inserting them at a
specific index.
### Appending a Block
Add a block as the last child of a parent using `engine.block.appendChild()`.
Since the last child renders on top, the appended block becomes the topmost
element.
```kotlin highlight-android-append-child
engine.block.appendChild(parent = page, child = backBlock)
engine.block.appendChild(parent = page, child = middleBlock)
engine.block.appendChild(parent = page, child = frontBlock)
```
### Inserting at a Specific Position
Insert a block at a specific index in the layer stack using
`engine.block.insertChild()`. Index `0` places the block at the back, behind all
other children.
```kotlin highlight-android-insert-child
val insertedBlock = createLayerBlock(
engine = engine,
x = 330F,
y = 330F,
color = Color.fromRGBA(r = 0.98F, g = 0.78F, b = 0.20F, a = 1F),
)
engine.block.insertChild(parent = page, child = insertedBlock, index = 0)
```
### Reparenting Blocks
When you add a block to a new parent with `appendChild()` or `insertChild()`, it
is automatically removed from its previous parent.
## Changing Z-Order
Once blocks are in the hierarchy, you can change their stacking order without
removing and re-adding them. CE.SDK provides four methods for z-order
manipulation.
### Bring to Front
Move an element to the top of its siblings using
`engine.block.bringToFront()`. This gives the block the highest stacking order
among its siblings.
```kotlin highlight-android-bring-to-front
engine.block.bringToFront(backBlock)
```
### Send to Back
Move an element behind all its siblings using `engine.block.sendToBack()`. This
gives the block the lowest stacking order among its siblings.
```kotlin highlight-android-send-to-back
engine.block.sendToBack(frontBlock)
```
### Move Forward One Layer
Move an element one position forward using `engine.block.bringForward()`. This
swaps the block with its immediate sibling in front.
```kotlin highlight-android-bring-forward
engine.block.bringForward(insertedBlock)
```
### Move Backward One Layer
Move an element one position backward using `engine.block.sendBackward()`. This
swaps the block with its immediate sibling behind.
```kotlin highlight-android-send-backward
engine.block.sendBackward(middleBlock)
```
These incremental operations are useful for fine-tuning the layer order without
jumping to extremes.
## Controlling Visibility
Visibility lets you hide elements without removing them from the scene. Hidden
elements remain in the hierarchy and preserve their properties, but they are not
rendered.
```kotlin highlight-android-visibility
val isVisible = engine.block.isVisible(insertedBlock)
engine.block.setVisible(block = insertedBlock, visible = !isVisible)
engine.block.setVisible(block = insertedBlock, visible = true)
```
## Managing Block Lifecycle
CE.SDK provides methods for duplicating blocks to create copies and destroying
blocks to remove them permanently.
### Duplicating Blocks
Create a copy of a block and its children using `engine.block.duplicate()`. By
default, the duplicate is attached to the same parent as the original.
```kotlin highlight-android-duplicate
val duplicate = engine.block.duplicate(middleBlock)
engine.block.setPositionX(duplicate, value = 430F)
engine.block.setPositionY(duplicate, value = 140F)
```
The duplicated block starts at the same position as the original. Reposition it
to make it visible as a separate element.
### Checking Block Validity
Before performing operations on a block, verify it still exists using
`engine.block.isValid()`. A block becomes invalid after it has been destroyed.
```kotlin highlight-android-is-valid
val duplicateIsValid = engine.block.isValid(duplicate)
```
### Removing Blocks
Permanently remove a block and all its children from the scene using
`engine.block.destroy()`.
```kotlin highlight-android-destroy
engine.block.destroy(frontBlock)
val frontBlockIsValid = engine.block.isValid(frontBlock)
```
After destruction, any reference to the block becomes invalid. Attempting to use
an invalid block ID results in errors.
## Framing the Result
After making layer changes, zoom to fit the page in the viewport so the
composition is clearly visible.
```kotlin highlight-android-zoom
engine.scene.zoomToBlock(
page,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
```
## Troubleshooting
**Block not visible after appendChild**: The block may be behind other elements.
Use `engine.block.bringToFront()` or adjust the insert index.
**getParent returns null**: The block is not attached to any parent. Attach it
with `engine.block.appendChild()` or `engine.block.insertChild()`.
**Changes not reflected**: The block handle may be invalid. Check with
`engine.block.isValid()` before operations.
**Duplicate not appearing**: If `attachToParent` is `false`, the duplicate is not
attached automatically. Set it to `true` or manually attach the duplicate.
## API Reference
| Method | Category | Description |
| --- | --- | --- |
| `engine.block.getParent(block=_)` | Hierarchy | Get the parent block of a block |
| `engine.block.getChildren(block=_)` | Hierarchy | Get all child blocks in rendering order |
| `engine.block.appendChild(parent=_, child=_)` | Hierarchy | Append a block as the last child |
| `engine.block.insertChild(parent=_, child=_, index=_)` | Hierarchy | Insert a block at a specific position |
| `engine.block.bringToFront(block=_)` | Z-Order | Bring a block to the front of its siblings |
| `engine.block.sendToBack(block=_)` | Z-Order | Send a block to the back of its siblings |
| `engine.block.bringForward(block=_)` | Z-Order | Move a block one position forward |
| `engine.block.sendBackward(block=_)` | Z-Order | Move a block one position backward |
| `engine.block.isVisible(block=_)` | Visibility | Check if a block is visible |
| `engine.block.setVisible(block=_, visible=_)` | Visibility | Set the visibility of a block |
| `engine.block.duplicate(block=_, attachToParent=_)` | Lifecycle | Duplicate a block and its children |
| `engine.block.destroy(block=_)` | Lifecycle | Remove a block and its children |
| `engine.block.isValid(block=_)` | Lifecycle | Check if a block handle is valid |
## Next Steps
- [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) — Group multiple blocks to move or transform them together
- [Position and Align](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) — Precisely position elements on the canvas
- [Multi-Page Layouts](https://img.ly/docs/cesdk/android/create-composition/multi-page-4d2b50/) — Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Design a Layout"
description: "Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/layout-b66311/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Design a Layout](https://img.ly/docs/cesdk/android/create-composition/layout-b66311/)
---
```kotlin file=@cesdk_android_examples/engine-guides-layout/Layout.kt reference-only
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.SceneLayout
import ly.img.engine.ShapeType
fun layout(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example.layout")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
// Create a scene with VerticalStack layout. Pages appended to the stack
// container arrange top-to-bottom automatically.
engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK)
// Get the stack container that was created with the scene.
val stack = engine.block.findByType(DesignBlockType.Stack).first()
// Create two pages that will stack vertically.
val page1 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page1, value = 400F)
engine.block.setHeight(page1, value = 300F)
engine.block.appendChild(parent = stack, child = page1)
val page2 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page2, value = 400F)
engine.block.setHeight(page2, value = 300F)
engine.block.appendChild(parent = stack, child = page2)
// Configure spacing between stacked pages.
engine.block.setFloat(stack, property = "stack/spacing", value = 20F)
engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true)
// Add an image block to the first page.
val block1 = engine.block.create(DesignBlockType.Graphic)
val shape1 = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block1, shape = shape1)
engine.block.setWidth(block1, value = 350F)
engine.block.setHeight(block1, value = 250F)
engine.block.setPositionX(block1, value = 25F)
engine.block.setPositionY(block1, value = 25F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(block1, fill = imageFill)
engine.block.appendChild(parent = page1, child = block1)
// Add a colored rectangle to the second page.
val block2 = engine.block.create(DesignBlockType.Graphic)
val shape2 = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block2, shape = shape2)
engine.block.setWidth(block2, value = 350F)
engine.block.setHeight(block2, value = 250F)
engine.block.setPositionX(block2, value = 25F)
engine.block.setPositionY(block2, value = 25F)
engine.block.setFill(block2, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block2,
color = Color.fromRGBA(r = 0.3F, g = 0.6F, b = 0.9F, a = 1F),
)
engine.block.appendChild(parent = page2, child = block2)
// Switch to a horizontal stack. Existing pages reposition left-to-right.
engine.scene.setLayout(SceneLayout.HORIZONTAL_STACK)
// Verify the layout type.
val currentLayout = engine.scene.getLayout()
println("Current layout: $currentLayout")
// Append a new page to the existing stack. It snaps to the end with the
// configured spacing.
val page3 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page3, value = 400F)
engine.block.setHeight(page3, value = 300F)
engine.block.appendChild(parent = stack, child = page3)
// Add content to the new page.
val block3 = engine.block.create(DesignBlockType.Graphic)
val shape3 = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block3, shape = shape3)
engine.block.setWidth(block3, value = 350F)
engine.block.setHeight(block3, value = 250F)
engine.block.setPositionX(block3, value = 25F)
engine.block.setPositionY(block3, value = 25F)
engine.block.setFill(block3, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block3,
color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.3F, a = 1F),
)
engine.block.appendChild(parent = page3, child = block3)
// Move page3 to the first position using insertChild.
engine.block.insertChild(parent = stack, child = page3, index = 0)
// Verify the new order.
val pageOrder = engine.block.getChildren(stack)
println("Page order after reordering: $pageOrder")
// Update the spacing between stacked pages.
engine.block.setFloat(stack, property = "stack/spacing", value = 40F)
// Verify the spacing value.
val updatedSpacing = engine.block.getFloat(stack, property = "stack/spacing")
println("Updated spacing: $updatedSpacing")
// Switch back to a free layout to position pages manually.
engine.scene.setLayout(SceneLayout.FREE)
// Position a page directly; stacks no longer manage placement.
val page = engine.block.findByType(DesignBlockType.Page).first()
engine.block.setPositionX(page, value = 100F)
engine.block.setPositionY(page, value = 200F)
engine.stop()
}
```
Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-layout)
Stack layouts arrange pages automatically with consistent spacing. Vertical stacks arrange pages top-to-bottom, while horizontal stacks arrange them left-to-right. This eliminates manual positioning for compositions like photo collages, product catalogs, or social media carousels.
This guide covers how to:
- Create vertical and horizontal stack layouts
- Add pages and blocks to stacks
- Configure spacing between stacked pages
- Reorder pages within a stack
- Switch between stack and free layouts
## Create a Vertical Stack Layout
Vertical stacks arrange pages from top to bottom. Create a scene with `SceneLayout.VERTICAL_STACK`, then append pages to the stack container.
```kotlin highlight-android-vertical-stack
// Create a scene with VerticalStack layout. Pages appended to the stack
// container arrange top-to-bottom automatically.
engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK)
// Get the stack container that was created with the scene.
val stack = engine.block.findByType(DesignBlockType.Stack).first()
// Create two pages that will stack vertically.
val page1 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page1, value = 400F)
engine.block.setHeight(page1, value = 300F)
engine.block.appendChild(parent = stack, child = page1)
val page2 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page2, value = 400F)
engine.block.setHeight(page2, value = 300F)
engine.block.appendChild(parent = stack, child = page2)
// Configure spacing between stacked pages.
engine.block.setFloat(stack, property = "stack/spacing", value = 20F)
engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true)
```
When you create a scene with `SceneLayout.VERTICAL_STACK`, CE.SDK automatically adds a stack container. Pages appended to this container position themselves with the configured spacing. The `stack/spacingInScreenspace` property keeps spacing visually consistent at any zoom level.
## Add Blocks to Pages
Each page can contain multiple blocks. Create blocks with a shape and fill, position them inside the page, then append them as children.
```kotlin highlight-android-add-blocks
// Add an image block to the first page.
val block1 = engine.block.create(DesignBlockType.Graphic)
val shape1 = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block1, shape = shape1)
engine.block.setWidth(block1, value = 350F)
engine.block.setHeight(block1, value = 250F)
engine.block.setPositionX(block1, value = 25F)
engine.block.setPositionY(block1, value = 25F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(block1, fill = imageFill)
engine.block.appendChild(parent = page1, child = block1)
// Add a colored rectangle to the second page.
val block2 = engine.block.create(DesignBlockType.Graphic)
val shape2 = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block2, shape = shape2)
engine.block.setWidth(block2, value = 350F)
engine.block.setHeight(block2, value = 250F)
engine.block.setPositionX(block2, value = 25F)
engine.block.setPositionY(block2, value = 25F)
engine.block.setFill(block2, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block2,
color = Color.fromRGBA(r = 0.3F, g = 0.6F, b = 0.9F, a = 1F),
)
engine.block.appendChild(parent = page2, child = block2)
```
Graphic blocks require both a shape and a fill to be visible. Use an image fill with `fill/image/imageFileURI` for image content, or a color fill for solid colors. Position blocks inside their parent page with `setPositionX` and `setPositionY`.
## Switch to Horizontal Layout
Change the layout direction at any time with `setLayout`. Horizontal stacks arrange pages left-to-right instead of top-to-bottom.
```kotlin highlight-android-horizontal-stack
// Switch to a horizontal stack. Existing pages reposition left-to-right.
engine.scene.setLayout(SceneLayout.HORIZONTAL_STACK)
// Verify the layout type.
val currentLayout = engine.scene.getLayout()
println("Current layout: $currentLayout")
```
Horizontal layouts suit carousels, timelines, and horizontal galleries. Existing pages reposition automatically when you change the layout type.
## Add Pages to Existing Stacks
Append new pages to an existing stack at any time. Pages snap to the end of the stack with the configured spacing.
```kotlin highlight-android-add-page
// Append a new page to the existing stack. It snaps to the end with the
// configured spacing.
val page3 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page3, value = 400F)
engine.block.setHeight(page3, value = 300F)
engine.block.appendChild(parent = stack, child = page3)
// Add content to the new page.
val block3 = engine.block.create(DesignBlockType.Graphic)
val shape3 = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block3, shape = shape3)
engine.block.setWidth(block3, value = 350F)
engine.block.setHeight(block3, value = 250F)
engine.block.setPositionX(block3, value = 25F)
engine.block.setPositionY(block3, value = 25F)
engine.block.setFill(block3, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = block3,
color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.3F, a = 1F),
)
engine.block.appendChild(parent = page3, child = block3)
```
The stack container handles positioning automatically. You can populate the new page with content before or after appending it.
## Reorder Pages
Change page order with `insertChild` to place a page at a specific index inside the stack.
```kotlin highlight-android-reorder
// Move page3 to the first position using insertChild.
engine.block.insertChild(parent = stack, child = page3, index = 0)
// Verify the new order.
val pageOrder = engine.block.getChildren(stack)
println("Page order after reordering: $pageOrder")
```
Removing a page from its current slot and reinserting it at index 0 moves it to the first position. The remaining pages shift to make room.
## Change Stack Spacing
Adjust spacing between pages by setting the `stack/spacing` property on the stack block.
```kotlin highlight-android-spacing
// Update the spacing between stacked pages.
engine.block.setFloat(stack, property = "stack/spacing", value = 40F)
// Verify the spacing value.
val updatedSpacing = engine.block.getFloat(stack, property = "stack/spacing")
println("Updated spacing: $updatedSpacing")
```
Spacing updates take effect immediately and pages reposition automatically. Read the current value back with `getFloat`.
## Switch to Free Layout
For manual positioning, switch to `SceneLayout.FREE`. Pages keep their current positions but stop auto-arranging.
```kotlin highlight-android-free-layout
// Switch back to a free layout to position pages manually.
engine.scene.setLayout(SceneLayout.FREE)
// Position a page directly; stacks no longer manage placement.
val page = engine.block.findByType(DesignBlockType.Page).first()
engine.block.setPositionX(page, value = 100F)
engine.block.setPositionY(page, value = 200F)
```
Free layout gives full control over page positions. Use this when you need precise placement that stack layouts cannot provide.
## Troubleshooting
**Pages not arranging automatically** — Verify the scene layout is `SceneLayout.VERTICAL_STACK` or `SceneLayout.HORIZONTAL_STACK` with `getLayout()`.
**Spacing not applying** — Set `stack/spacing` on the stack block, not the scene. Use `findByType(DesignBlockType.Stack)` to locate the container.
**Pages overlapping** — Ensure pages are direct children of the stack container. Nested pages do not auto-arrange.
**Can't position manually** — Stack layouts override manual positions. Switch to `SceneLayout.FREE` for manual control.
**Wrong stacking order** — Child order determines position. Use `insertChild(parent = ..., child = ..., index = ...)` to move pages to a specific slot.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.scene.create(sceneLayout=_)` | Create a scene with the specified layout (`SceneLayout.FREE`, `SceneLayout.VERTICAL_STACK`, `SceneLayout.HORIZONTAL_STACK`, `SceneLayout.DEPTH_STACK`). |
| `engine.scene.setLayout(layout=_)` | Change the layout of the current scene. |
| `engine.scene.getLayout()` | Get the current scene layout. |
| `engine.block.findByType(type=DesignBlockType.Stack)` | Find the stack container block. |
| `engine.block.setFloat(block=_, property="stack/spacing", value=_)` | Set spacing between stacked pages. |
| `engine.block.getFloat(block=_, property="stack/spacing")` | Get the current spacing value. |
| `engine.block.setBoolean(block=_, property="stack/spacingInScreenspace", value=_)` | Set whether spacing is measured in screen pixels. |
| `engine.block.appendChild(parent=_, child=_)` | Append a page to the stack. |
| `engine.block.insertChild(parent=_, child=_, index=_)` | Insert a page at a specific position. |
| `engine.block.getChildren(block=_)` | Get child blocks in order. |
## Next Steps
- [Auto-resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/) — Make blocks fit parent containers
- [Manual Positioning](https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/) — Position blocks in free layouts
- [Layer Hierarchies](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) — Organize blocks in hierarchical structures
- [Create a Collage](https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/) — Create collages by applying layout templates and transferring content between scenes.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Lock Design"
description: "Protect design elements from unwanted modifications using CE.SDK's scope-based permission system."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/lock-design-0a81de/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Lock Design](https://img.ly/docs/cesdk/android/create-composition/lock-design-0a81de/)
---
```kotlin file=@cesdk_android_examples/engine-guides-lock-design/LockDesign.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
suspend fun lockDesign(
engine: Engine,
restoreGlobalScopes: Boolean = false,
) = withContext(engine.dispatcher) {
val previousGlobalScopes = if (restoreGlobalScopes) {
engine.editor.findAllScopes().associateWith { scope ->
engine.editor.getGlobalScope(key = scope)
}
} else {
emptyMap()
}
try {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.setWidthMode(textBlock, mode = SizeMode.AUTO)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
engine.block.replaceText(textBlock, text = "Editable headline")
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionY(imageBlock, value = 160F)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 200F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(imageBlock, fill = imageFill)
engine.block.appendChild(parent = page, child = imageBlock)
engine.block.forceLoadResources(listOf(textBlock, imageBlock))
val scopes = engine.editor.findAllScopes()
scopes.forEach { scope ->
engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY)
}
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(textBlock, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(imageBlock, key = "editor/select", enabled = true)
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(textBlock, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(textBlock, key = "text/character", enabled = true)
engine.block.setScopeEnabled(textBlock, key = "fill/change", enabled = true)
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(imageBlock, key = "fill/change", enabled = true)
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/rotate", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(imageBlock, key = "layer/move", enabled = true)
engine.block.setScopeEnabled(imageBlock, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(imageBlock, key = "layer/rotate", enabled = true)
val canEditText = engine.block.isAllowedByScope(textBlock, key = "text/edit")
val canChangeTextColor = engine.block.isAllowedByScope(textBlock, key = "fill/change")
val canMoveText = engine.block.isAllowedByScope(textBlock, key = "layer/move")
val canMoveImage = engine.block.isAllowedByScope(imageBlock, key = "layer/move")
val canResizeImage = engine.block.isAllowedByScope(imageBlock, key = "layer/resize")
val canRotateImage = engine.block.isAllowedByScope(imageBlock, key = "layer/rotate")
val textEditScopeEnabled = engine.block.isScopeEnabled(textBlock, key = "text/edit")
val textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit")
require(canEditText)
require(canChangeTextColor)
require(!canMoveText)
require(canMoveImage)
require(canResizeImage)
require(canRotateImage)
require(textEditScopeEnabled)
require(textEditGlobalScope == GlobalScope.DEFER)
val availableScopes = engine.editor.findAllScopes()
val currentScopeSettings = availableScopes.associateWith { scope ->
engine.editor.getGlobalScope(key = scope)
}
require("text/edit" in availableScopes)
require(currentScopeSettings["text/edit"] == GlobalScope.DEFER)
} finally {
previousGlobalScopes.forEach { (scope, globalScope) ->
engine.editor.setGlobalScope(key = scope, globalScope = globalScope)
}
}
}
```
Protect design elements from unwanted modifications using CE.SDK's scope-based permission system.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-lock-design)
CE.SDK uses a two-layer scope system to control editing permissions. Global scopes set defaults for the entire scene, while block-level scopes override when the global setting is `GlobalScope.DEFER`. This enables flexible permission models from fully locked to selectively editable designs.
This guide covers how to lock entire designs, selectively enable specific editing capabilities, and check permissions programmatically.
## Understanding the Scope Permission Model
Scopes control what operations users can perform on design elements. CE.SDK combines global scope settings with block-level settings to determine the final permission.
| Global Scope | Block Scope | Result |
| ------------------- | ----------- | --------- |
| `GlobalScope.ALLOW` | any | Permitted |
| `GlobalScope.DENY` | any | Blocked |
| `GlobalScope.DEFER` | enabled | Permitted |
| `GlobalScope.DEFER` | disabled | Blocked |
Global scopes have three possible values:
- **`GlobalScope.ALLOW`**: The operation is always permitted, regardless of block-level settings
- **`GlobalScope.DENY`**: The operation is always blocked, regardless of block-level settings
- **`GlobalScope.DEFER`**: The permission depends on the block-level scope setting
Block-level scopes are binary: enabled or disabled. They only take effect when the global scope is set to `GlobalScope.DEFER`.
## Locking an Entire Design
To lock all editing operations, iterate through all available scopes and set each to `GlobalScope.DENY`. We use `engine.editor.findAllScopes()` to discover all scope names dynamically.
```kotlin highlight-android-lock-entire-design
val scopes = engine.editor.findAllScopes()
scopes.forEach { scope ->
engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY)
}
```
When all scopes are set to `GlobalScope.DENY`, users cannot modify any aspect of the design. This includes selecting, moving, editing text, or changing any visual properties.
## Enabling Selection for Interactive Blocks
Before users can interact with any block, you must enable the `editor/select` scope. Without selection, users cannot click on or access any blocks, even if other editing capabilities are enabled.
```kotlin highlight-android-enable-selection
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(textBlock, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(imageBlock, key = "editor/select", enabled = true)
```
Setting the global `editor/select` scope to `GlobalScope.DEFER` delegates the decision to each block. We then enable selection only on the specific blocks users should be able to interact with.
## Selective Locking Patterns
Lock everything first, then selectively enable specific capabilities on chosen blocks. This pattern provides fine-grained control over what users can modify.
### Text-Only Editing
To allow users to edit text content while protecting everything else, enable the `text/edit` scope. For text styling changes like font and size, also enable `text/character`; for text color changes, enable `fill/change`.
```kotlin highlight-android-text-editing
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(textBlock, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(textBlock, key = "text/character", enabled = true)
engine.block.setScopeEnabled(textBlock, key = "fill/change", enabled = true)
```
Users can now type new text content in the designated text block but cannot move, resize, or delete it.
### Image Replacement
To allow users to swap images while protecting layout and position, enable the `fill/change` scope on placeholder blocks.
```kotlin highlight-android-image-replacement
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(imageBlock, key = "fill/change", enabled = true)
```
Users can replace the image content but the block's position, dimensions, and other properties remain locked.
### Position Adjustments
To allow repositioning of specific elements, enable `layer/move` and then opt selected blocks into that scope. Add `layer/resize` and `layer/rotate` when users should also change dimensions or rotation.
```kotlin highlight-android-position-adjustments
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/rotate", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(imageBlock, key = "layer/move", enabled = true)
engine.block.setScopeEnabled(imageBlock, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(imageBlock, key = "layer/rotate", enabled = true)
```
Users can now move, resize, and rotate the selected image block while the text block remains locked for those operations.
## Checking Permissions
Verify whether operations are permitted using `engine.block.isAllowedByScope()`. This method evaluates both global and block-level settings to return the effective permission state.
```kotlin highlight-android-check-permissions
val canEditText = engine.block.isAllowedByScope(textBlock, key = "text/edit")
val canChangeTextColor = engine.block.isAllowedByScope(textBlock, key = "fill/change")
val canMoveText = engine.block.isAllowedByScope(textBlock, key = "layer/move")
val canMoveImage = engine.block.isAllowedByScope(imageBlock, key = "layer/move")
val canResizeImage = engine.block.isAllowedByScope(imageBlock, key = "layer/resize")
val canRotateImage = engine.block.isAllowedByScope(imageBlock, key = "layer/rotate")
val textEditScopeEnabled = engine.block.isScopeEnabled(textBlock, key = "text/edit")
val textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit")
require(canEditText)
require(canChangeTextColor)
require(!canMoveText)
require(canMoveImage)
require(canResizeImage)
require(canRotateImage)
require(textEditScopeEnabled)
require(textEditGlobalScope == GlobalScope.DEFER)
```
The distinction between checking methods is:
- `isAllowedByScope()` returns the **effective permission** after evaluating all scope levels
- `isScopeEnabled()` returns only the **block-level setting**
- `getGlobalScope()` returns only the **global setting**
## Discovering Available Scopes
To work with scopes programmatically, you can discover all available scope names and check their current settings.
```kotlin highlight-android-get-scopes
val availableScopes = engine.editor.findAllScopes()
val currentScopeSettings = availableScopes.associateWith { scope ->
engine.editor.getGlobalScope(key = scope)
}
require("text/edit" in availableScopes)
require(currentScopeSettings["text/edit"] == GlobalScope.DEFER)
```
## Available Scopes Reference
| Scope | Description |
| ------------------------ | ---------------------------------------- |
| `layer/move` | Move block position |
| `layer/resize` | Resize block dimensions |
| `layer/rotate` | Rotate block |
| `layer/flip` | Flip block horizontally or vertically |
| `layer/crop` | Crop block content |
| `layer/opacity` | Change block opacity |
| `layer/blendMode` | Change blend mode |
| `layer/visibility` | Toggle block visibility |
| `layer/clipping` | Change clipping behavior |
| `fill/change` | Change fill content or text color |
| `fill/changeType` | Change fill type |
| `stroke/change` | Change stroke properties |
| `shape/change` | Change shape type |
| `text/edit` | Edit text content |
| `text/character` | Change text styling such as font or size |
| `appearance/adjustments` | Change color adjustments |
| `appearance/filter` | Apply or change filters |
| `appearance/effect` | Apply or change effects |
| `appearance/blur` | Apply or change blur |
| `appearance/shadow` | Apply or change shadows |
| `appearance/animation` | Apply or change animations |
| `lifecycle/destroy` | Delete the block |
| `lifecycle/duplicate` | Duplicate the block |
| `editor/add` | Add new blocks |
| `editor/select` | Select blocks |
## Troubleshooting
| Issue | Cause | Solution |
| ---------------------------------- | ------------------------------------ | ----------------------------------------------------------- |
| Block is still editable | The global scope is `GlobalScope.ALLOW` | Set the global scope to `GlobalScope.DENY` or `GlobalScope.DEFER` |
| Block is unexpectedly locked | The global scope is `GlobalScope.DENY` | Set the global scope to `GlobalScope.DEFER` and enable the block-level scope |
| Users cannot interact with a block | `editor/select` is still locked | Enable `editor/select` for blocks users should select |
| Permission check returns `false` | The code checks the wrong scope level | Use `isAllowedByScope()` for the effective permission |
## API Reference
| Method | Purpose |
| ------ | ------- |
| `engine.editor.findAllScopes()` | Get all available scope names |
| `engine.editor.setGlobalScope(key=_, globalScope=_)` | Set a global scope to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER` |
| `engine.editor.getGlobalScope(key=_)` | Get the current global setting for one scope |
| `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Enable or disable a scope on one block |
| `engine.block.isScopeEnabled(block=_, key=_)` | Check only the block-level scope setting |
| `engine.block.isAllowedByScope(block=_, key=_)` | Check the effective permission after global and block-level scopes are evaluated |
## Next Steps
- [Lock Content](https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/) - Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system
- [Lock Templates](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Lock templates for consistent reuse
- [Rules Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) - Understand the broader rules system
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Multi-Page Layouts"
description: "Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/multi-page-4d2b50/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Multi-Page Layouts](https://img.ly/docs/cesdk/android/create-composition/multi-page-4d2b50/)
---
```kotlin file=@cesdk_android_examples/engine-guides-multi-page/MultiPage.kt reference-only
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.SceneLayout
import ly.img.engine.ShapeType
suspend fun multiPage(engine: Engine) = withContext(engine.dispatcher) {
// Create a scene with HorizontalStack layout.
engine.scene.create(sceneLayout = SceneLayout.HORIZONTAL_STACK)
// Get the stack container that owns pages in stack layouts.
val stack = engine.block.findByType(DesignBlockType.Stack).first()
// Create the first page.
val firstPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(firstPage, value = 800F)
engine.block.setHeight(firstPage, value = 600F)
engine.block.appendChild(parent = stack, child = firstPage)
// Add spacing between pages (20 pixels in screen space).
engine.block.setFloat(stack, property = "stack/spacing", value = 20F)
engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true)
// Add content to the first page.
val imageBlock1 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock1, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock1, value = 300F)
engine.block.setHeight(imageBlock1, value = 200F)
engine.block.setPositionX(imageBlock1, value = 250F)
engine.block.setPositionY(imageBlock1, value = 200F)
val imageFill1 = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill1,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg",
)
engine.block.setFill(imageBlock1, fill = imageFill1)
engine.block.appendChild(parent = firstPage, child = imageBlock1)
// Create a second page with different content.
val secondPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(secondPage, value = 800F)
engine.block.setHeight(secondPage, value = 600F)
engine.block.appendChild(parent = stack, child = secondPage)
// Add a different image to the second page.
val imageBlock2 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock2, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock2, value = 300F)
engine.block.setHeight(imageBlock2, value = 200F)
engine.block.setPositionX(imageBlock2, value = 250F)
engine.block.setPositionY(imageBlock2, value = 200F)
val imageFill2 = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill2,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_2.jpg",
)
engine.block.setFill(imageBlock2, fill = imageFill2)
engine.block.appendChild(parent = secondPage, child = imageBlock2)
val pages = engine.scene.getPages()
println("Pages: ${pages.size}")
val currentPage = engine.scene.getCurrentPage()
println("Current page: $currentPage")
val duplicatedPage = engine.block.duplicate(firstPage)
engine.block.insertChild(parent = stack, child = secondPage, index = 0)
if (engine.scene.getPages().size > 1) {
engine.block.destroy(duplicatedPage)
}
engine.scene.zoomToBlock(
block = firstPage,
paddingLeft = 20F,
paddingTop = 20F,
paddingRight = 20F,
paddingBottom = 20F,
)
val nearestPage = engine.scene.findNearestToViewPortCenterByType(
DesignBlockType.Page,
).firstOrNull()
println("Nearest page: $nearestPage")
engine.block.forceLoadResources(listOf(imageBlock1, imageBlock2))
}
```
Create multi-page designs in CE.SDK for brochures, presentations, catalogs,
and other documents requiring multiple pages within a single scene.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-multi-page)
Multi-page layouts allow you to create documents with multiple pages within a single scene. Each page is an independent canvas that can contain different content while sharing the same scene context. CE.SDK provides scene layout modes that arrange pages vertically, horizontally, or in a free-form canvas.
This guide covers how to create multi-page scenes, add and manage pages, configure spacing between pages, and focus the viewport on a page.
## Using the Built-in Page Management UI
The CE.SDK Android editor includes a pages mode that displays page thumbnails in a grid. Users can add pages, move the selected page up or down, duplicate supported pages, delete pages when more than one page remains, resize pages, and switch back to editing a selected page.
Page thumbnails make the document structure visible at a glance. Selecting a page updates the current page, and opening it in edit mode focuses the editor on that page.
## Creating Multi-Page Scenes Programmatically
We can create scenes with multiple pages using the engine API. The scene acts as the document container, and each page can hold independent content blocks.
### Creating a Scene with Pages
We create a new scene with `engine.scene.create(sceneLayout = SceneLayout.HORIZONTAL_STACK)`. Stack layouts create a stack block that owns the pages, so we append page blocks to that stack.
```kotlin highlight-android-create-scene
// Create a scene with HorizontalStack layout.
engine.scene.create(sceneLayout = SceneLayout.HORIZONTAL_STACK)
// Get the stack container that owns pages in stack layouts.
val stack = engine.block.findByType(DesignBlockType.Stack).first()
// Create the first page.
val firstPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(firstPage, value = 800F)
engine.block.setHeight(firstPage, value = 600F)
engine.block.appendChild(parent = stack, child = firstPage)
```
The scene uses `SceneLayout.HORIZONTAL_STACK`, so pages are arranged side by side from left to right. The first page is created with 800 x 600 dimensions and appended to the stack container.
### Configuring Page Spacing
We can add spacing between pages in a stack layout with the `stack/spacing` property. This creates visual separation between adjacent pages.
```kotlin highlight-android-stack-spacing
// Add spacing between pages (20 pixels in screen space).
engine.block.setFloat(stack, property = "stack/spacing", value = 20F)
engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true)
```
Setting `stack/spacingInScreenspace` to `true` interprets the spacing value as screen pixels, so the visual spacing stays consistent while zooming.
### Adding More Pages
To add another page, we create a new page block, set its dimensions, and append it to the same stack container.
```kotlin highlight-android-add-page
// Create a second page with different content.
val secondPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(secondPage, value = 800F)
engine.block.setHeight(secondPage, value = 600F)
engine.block.appendChild(parent = stack, child = secondPage)
// Add a different image to the second page.
val imageBlock2 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock2, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock2, value = 300F)
engine.block.setHeight(imageBlock2, value = 200F)
engine.block.setPositionX(imageBlock2, value = 250F)
engine.block.setPositionY(imageBlock2, value = 200F)
val imageFill2 = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill2,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_2.jpg",
)
engine.block.setFill(imageBlock2, fill = imageFill2)
engine.block.appendChild(parent = secondPage, child = imageBlock2)
```
Each page can contain different content. Here the second page receives a separate image block, showing that page contents are independent.
### Managing Pages
Existing pages are available through `engine.scene.getPages()`. Use this list for page counts, page pickers, and operations that need a stable page order.
```kotlin highlight-android-list-pages
val pages = engine.scene.getPages()
println("Pages: ${pages.size}")
```
The current page comes from the selected content when that page is visible enough; otherwise CE.SDK returns the page nearest to the viewport center.
```kotlin highlight-android-current-page
val currentPage = engine.scene.getCurrentPage()
println("Current page: $currentPage")
```
`engine.block.duplicate()` copies a page and its children.
```kotlin highlight-android-duplicate-page
val duplicatedPage = engine.block.duplicate(firstPage)
```
`engine.block.insertChild()` moves a page to a specific index in the stack, and `engine.block.destroy()` removes a page. Keep at least one page in the scene.
```kotlin highlight-android-reorder-delete-pages
engine.block.insertChild(parent = stack, child = secondPage, index = 0)
if (engine.scene.getPages().size > 1) {
engine.block.destroy(duplicatedPage)
}
```
## Scene Layout Types
CE.SDK supports different layout modes that control how pages are arranged on the canvas. You specify the layout type when creating the scene with `engine.scene.create(sceneLayout = ...)` or change it later with `engine.scene.setLayout(...)`.
**Free Layout** (`SceneLayout.FREE`) is the default where pages can be positioned anywhere on the canvas. This provides complete control over page placement.
**VerticalStack Layout** (`SceneLayout.VERTICAL_STACK`) arranges pages automatically in a vertical stack from top to bottom. This is useful for scroll-based document previews.
**HorizontalStack Layout** (`SceneLayout.HORIZONTAL_STACK`) arranges pages side by side from left to right. This is useful for carousel-style presentations or side-by-side comparisons.
## Navigating Between Pages
We can focus the viewport on a specific page with the suspending `engine.scene.zoomToBlock(...)` API. Padding values are interpreted in screen pixels.
```kotlin highlight-android-zoom-to-page
engine.scene.zoomToBlock(
block = firstPage,
paddingLeft = 20F,
paddingTop = 20F,
paddingRight = 20F,
paddingBottom = 20F,
)
```
To derive navigation state from the viewport, find pages sorted by distance to the viewport center and use the first result.
```kotlin highlight-android-nearest-page
val nearestPage = engine.scene.findNearestToViewPortCenterByType(
DesignBlockType.Page,
).firstOrNull()
println("Nearest page: $nearestPage")
```
## Troubleshooting
**Page not visible after creation**: Ensure the page is attached to the stack with `appendChild(...)` and has valid dimensions set with `setWidth(...)` and `setHeight(...)`.
**Cannot add content to page**: Verify you're appending blocks to the page block, not the scene directly. Content blocks should be children of pages.
**Pages overlapping**: When using stack layouts, make sure pages are appended to the stack container, found with `findByType(DesignBlockType.Stack)`, not directly to the scene.
**Spacing not visible**: Check that `stack/spacing` is set to a positive value and that you're using a stack layout (`SceneLayout.HORIZONTAL_STACK` or `SceneLayout.VERTICAL_STACK`).
## API Reference
| Method | Purpose |
| ------ | ------- |
| `engine.scene.create(sceneLayout=_)` | Create a scene with a free, vertical stack, or horizontal stack layout. |
| `engine.scene.setLayout(layout=_)` | Change the current scene layout. |
| `engine.scene.getPages()` | Return the sorted list of pages in the current scene. |
| `engine.scene.getCurrentPage()` | Return the selected or viewport-centered current page. |
| `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Focus the viewport on a page or another block. |
| `engine.scene.findNearestToViewPortCenterByType(blockType=DesignBlockType.Page)` | Find pages sorted by distance to the viewport center. |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create a page block. |
| `engine.block.appendChild(parent=_, child=_)` | Attach a page to the scene or stack container. |
| `engine.block.insertChild(parent=_, child=_, index=_)` | Move a page to a specific child index. |
| `engine.block.duplicate(block=_)` | Copy a page and its children. |
| `engine.block.destroy(block=_)` | Remove a page or block from the scene. |
| `engine.block.setWidth(block=_, value=_)` | Set a page width. |
| `engine.block.setHeight(block=_, value=_)` | Set a page height. |
| `engine.block.setFloat(block=_, property="stack/spacing", value=_)` | Configure spacing between pages in stack layouts. |
| `engine.block.setBoolean(block=_, property="stack/spacingInScreenspace", value=_)` | Keep stack spacing fixed in screen pixels. |
## Next Steps
- [Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Explore export options, supported formats, and configuration features for sharing or rendering output.
- [Design a Layout](https://img.ly/docs/cesdk/android/create-composition/layout-b66311/) — Create structured compositions using scene layouts, positioning systems, and hierarchical block organization for collages, magazines, and multi-page documents.
- [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Organize design elements using a layer stack for precise control over stacking and visibility.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Overview"
description: "Combine and arrange multiple elements to create complex, multi-page, or layered design compositions."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/overview-5b19c5/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Overview](https://img.ly/docs/cesdk/android/create-composition/overview-5b19c5/)
---
In CreativeEditor SDK (CE.SDK), a *composition* is an arrangement of multiple design elements—such as images, text, shapes, graphics, and effects—combined into a single, cohesive visual layout. Unlike working with isolated elements, compositions allow you to design complex, multi-element visuals that tell a richer story or support more advanced use cases.
On Android, composition processing runs on the device, keeping editing responsive without requiring server infrastructure.
You can use compositions to create a wide variety of projects, including social media posts, marketing materials, collages, and multi-page exports like PDFs. Whether you build layouts manually through the Android editor UI or generate them with CreativeEngine APIs, compositions give you the flexibility and control to design at scale.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
## Exporting Compositions
CE.SDK compositions can be exported in several formats:
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Programmatic Creation"
description: "Build compositions entirely through code with the CE.SDK Engine for automation, batch processing, and headless rendering."
platform: android
url: "https://img.ly/docs/cesdk/android/create-composition/programmatic-a688bf/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Programmatic Creation](https://img.ly/docs/cesdk/android/create-composition/programmatic-a688bf/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-composition-programmatic/CreateCompositionProgrammatic.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.Font
import ly.img.engine.FontStyle
import ly.img.engine.FontWeight
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import ly.img.engine.Typeface
import java.io.File
import java.util.UUID
suspend fun exportProgrammaticComposition(engine: Engine): File = withContext(engine.dispatcher) {
buildProgrammaticComposition(engine)
}
private suspend fun buildProgrammaticComposition(engine: Engine): File {
val robotoBase = "https://cdn.img.ly/assets/v3/ly.img.typeface/fonts/Roboto"
val robotoTypeface = Typeface(
name = "Roboto",
fonts = listOf(
Font(
uri = Uri.parse("$robotoBase/Roboto-Regular.ttf"),
subFamily = "Regular",
weight = FontWeight.NORMAL,
style = FontStyle.NORMAL,
),
Font(
uri = Uri.parse("$robotoBase/Roboto-Bold.ttf"),
subFamily = "Bold",
weight = FontWeight.BOLD,
style = FontStyle.NORMAL,
),
Font(
uri = Uri.parse("$robotoBase/Roboto-Italic.ttf"),
subFamily = "Italic",
weight = FontWeight.NORMAL,
style = FontStyle.ITALIC,
),
Font(
uri = Uri.parse("$robotoBase/Roboto-BoldItalic.ttf"),
subFamily = "Bold Italic",
weight = FontWeight.BOLD,
style = FontStyle.ITALIC,
),
),
)
val robotoRegular = robotoTypeface.fonts.first {
it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL
}
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = page, fill = backgroundFill)
engine.block.setFillSolidColor(
block = page,
color = Color.fromRGBA(r = 0.94F, g = 0.93F, b = 0.98F, a = 1F),
)
val headline = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(headline, text = "Integrate\nCreative Editing\ninto your App")
engine.block.setFont(headline, fontFileUri = robotoRegular.uri, typeface = robotoTypeface)
engine.block.setTextLineHeight(headline, lineHeight = 0.78F)
if (engine.block.canToggleBoldFont(headline)) {
engine.block.toggleBoldFont(headline)
}
engine.block.setTextColor(headline, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F))
engine.block.setWidthMode(headline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(headline, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(headline, value = 960F)
engine.block.setHeight(headline, value = 300F)
// The Android binding has no typed helper for this text option yet.
engine.block.setBoolean(headline, property = "text/automaticFontSizeEnabled", value = true)
engine.block.setPositionX(headline, value = 60F)
engine.block.setPositionY(headline, value = 80F)
engine.block.appendChild(parent = page, child = headline)
val tagline = engine.block.create(DesignBlockType.Text)
val taglineText = "in hours,\nnot months."
engine.block.replaceText(tagline, text = taglineText)
engine.block.setFont(tagline, fontFileUri = robotoRegular.uri, typeface = robotoTypeface)
engine.block.setTextLineHeight(tagline, lineHeight = 0.78F)
engine.block.setTextColor(
tagline,
color = Color.fromRGBA(r = 0.2F, g = 0.2F, b = 0.8F, a = 1F),
from = 0,
to = 9,
)
if (engine.block.canToggleItalicFont(tagline, from = 0, to = 9)) {
engine.block.toggleItalicFont(tagline, from = 0, to = 9)
}
engine.block.setTextColor(
tagline,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F),
from = 10,
to = 21,
)
if (engine.block.canToggleBoldFont(tagline, from = 10, to = 21)) {
engine.block.toggleBoldFont(tagline, from = 10, to = 21)
}
engine.block.setWidthMode(tagline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(tagline, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(tagline, value = 960F)
engine.block.setHeight(tagline, value = 220F)
// The Android binding has no typed helper for this text option yet.
engine.block.setBoolean(tagline, property = "text/automaticFontSizeEnabled", value = true)
engine.block.setPositionX(tagline, value = 60F)
engine.block.setPositionY(tagline, value = 551F)
engine.block.appendChild(parent = page, child = tagline)
val ctaTitle = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(ctaTitle, text = "Start a Free Trial")
engine.block.setFont(ctaTitle, fontFileUri = robotoRegular.uri, typeface = robotoTypeface)
engine.block.setTextFontSize(ctaTitle, fontSize = 80F)
engine.block.setTextLineHeight(ctaTitle, lineHeight = 1F)
if (engine.block.canToggleBoldFont(ctaTitle)) {
engine.block.toggleBoldFont(ctaTitle)
}
engine.block.setTextColor(ctaTitle, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F))
engine.block.setWidthMode(ctaTitle, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(ctaTitle, mode = SizeMode.AUTO)
engine.block.setWidth(ctaTitle, value = 664.6F)
engine.block.setPositionX(ctaTitle, value = 64F)
engine.block.setPositionY(ctaTitle, value = 952F)
engine.block.appendChild(parent = page, child = ctaTitle)
val ctaUrl = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(ctaUrl, text = "www.img.ly")
engine.block.setFont(ctaUrl, fontFileUri = robotoRegular.uri, typeface = robotoTypeface)
engine.block.setTextFontSize(ctaUrl, fontSize = 80F)
engine.block.setTextLineHeight(ctaUrl, lineHeight = 1F)
engine.block.setTextColor(ctaUrl, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F))
engine.block.setWidthMode(ctaUrl, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(ctaUrl, mode = SizeMode.AUTO)
engine.block.setWidth(ctaUrl, value = 664.6F)
engine.block.setPositionX(ctaUrl, value = 64F)
engine.block.setPositionY(ctaUrl, value = 1006F)
engine.block.appendChild(parent = page, child = ctaUrl)
val dividerLine = engine.block.create(DesignBlockType.Graphic)
val lineShape = engine.block.createShape(ShapeType.Line)
engine.block.setShape(block = dividerLine, shape = lineShape)
val lineFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = dividerLine, fill = lineFill)
engine.block.setFillSolidColor(
block = dividerLine,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F),
)
engine.block.setWidth(dividerLine, value = 418F)
// Line shapes use block height as the visible stroke thickness.
engine.block.setHeight(dividerLine, value = 11.3F)
engine.block.setPositionX(dividerLine, value = 64F)
engine.block.setPositionY(dividerLine, value = 460F)
engine.block.appendChild(parent = page, child = dividerLine)
val logo = engine.block.create(DesignBlockType.Graphic)
val logoShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = logo, shape = logoShape)
val logoFill = engine.block.createFill(FillType.Image)
// Image fills currently expose their URI through the generic property API.
engine.block.setUri(
block = logoFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg"),
)
engine.block.setFill(block = logo, fill = logoFill)
engine.block.setContentFillMode(logo, mode = ContentFillMode.CONTAIN)
engine.block.setWidth(logo, value = 200F)
engine.block.setHeight(logo, value = 65F)
engine.block.setPositionX(logo, value = 820F)
engine.block.setPositionY(logo, value = 960F)
engine.block.appendChild(parent = page, child = logo)
val exportOptions = ExportOptions(targetWidth = 1080F, targetHeight = 1080F)
// Ensure remote font files and the image fill are ready before the offscreen export.
engine.block.forceLoadResources(listOf(page, headline, tagline, ctaTitle, ctaUrl, logo))
val blob = engine.block.export(page, mimeType = MimeType.PNG, options = exportOptions)
return withContext(Dispatchers.IO) {
val outputFile = File.createTempFile("composition-${UUID.randomUUID()}", ".png")
val data = blob.asReadOnlyBuffer()
outputFile.outputStream().channel.use { channel ->
while (data.hasRemaining()) {
channel.write(data)
}
}
outputFile
}
}
```
Build compositions entirely through code using the CE.SDK Engine for automation, batch processing, and headless rendering.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-composition-programmatic)
CE.SDK provides a complete Engine API for building designs through code. Instead of relying on user interactions through an editor UI, you can create scenes, add blocks like text, images, and shapes, and position them programmatically. This approach enables automation workflows, batch processing, headless rendering, and integration with custom interfaces.
This guide covers how to create a scene structure with social media dimensions, set background colors, add text with mixed styling, line shapes, images, and export the finished composition.
The page dimensions and `ExportOptions(targetWidth = 1080F, targetHeight = 1080F)` shown later control the exported PNG dimensions.
## Create Scene Structure
We create the foundation of the composition with social media dimensions (1080x1080 pixels for Instagram). A scene contains one or more pages, and pages contain the design blocks.
```kotlin highlight-android-create-scene
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
```
`engine.scene.create()` returns a scene handle. Create a page with `engine.block.create(DesignBlockType.Page)`, set its dimensions with `setWidth()` and `setHeight()`, then attach it to the scene with `appendChild()`.
## Set Page Background
We set the page background using a color fill. This demonstrates how to create and assign fills to blocks.
```kotlin highlight-android-add-background
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = page, fill = backgroundFill)
engine.block.setFillSolidColor(
block = page,
color = Color.fromRGBA(r = 0.94F, g = 0.93F, b = 0.98F, a = 1F),
)
```
We create a color fill using `createFill(FillType.Color)`, assign it to the page with `setFill()`, then set the solid fill color on the target block via `setFillSolidColor(block=_, color=_)`.
## Add Text Blocks
Text blocks allow you to add and style text content. We demonstrate three different approaches to text sizing and styling.
### Select the Font Variant
Before styling text, define a `Typeface` with the variants the sample needs and select the regular font by weight and style. The bold and italic toggles can only apply variants that exist in this typeface.
```kotlin highlight-android-font-setup
val robotoBase = "https://cdn.img.ly/assets/v3/ly.img.typeface/fonts/Roboto"
val robotoTypeface = Typeface(
name = "Roboto",
fonts = listOf(
Font(
uri = Uri.parse("$robotoBase/Roboto-Regular.ttf"),
subFamily = "Regular",
weight = FontWeight.NORMAL,
style = FontStyle.NORMAL,
),
Font(
uri = Uri.parse("$robotoBase/Roboto-Bold.ttf"),
subFamily = "Bold",
weight = FontWeight.BOLD,
style = FontStyle.NORMAL,
),
Font(
uri = Uri.parse("$robotoBase/Roboto-Italic.ttf"),
subFamily = "Italic",
weight = FontWeight.NORMAL,
style = FontStyle.ITALIC,
),
Font(
uri = Uri.parse("$robotoBase/Roboto-BoldItalic.ttf"),
subFamily = "Bold Italic",
weight = FontWeight.BOLD,
style = FontStyle.ITALIC,
),
),
)
val robotoRegular = robotoTypeface.fonts.first {
it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL
}
```
### Create Text and Set Content
Create a text block, set its content with `replaceText()`, then bind the selected Roboto font and typeface:
```kotlin highlight-android-text-create
val headline = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(headline, text = "Integrate\nCreative Editing\ninto your App")
engine.block.setFont(headline, fontFileUri = robotoRegular.uri, typeface = robotoTypeface)
engine.block.setTextLineHeight(headline, lineHeight = 0.78F)
```
### Style Entire Text Block
Apply styling to the entire text block using `toggleBoldFont()` and `setTextColor()`:
```kotlin highlight-android-text-style-block
if (engine.block.canToggleBoldFont(headline)) {
engine.block.toggleBoldFont(headline)
}
engine.block.setTextColor(headline, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F))
```
### Enable Automatic Font Sizing
Configure the text block to automatically scale its font size to fit within fixed dimensions:
```kotlin highlight-android-text-auto-size
engine.block.setWidthMode(headline, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(headline, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(headline, value = 960F)
engine.block.setHeight(headline, value = 300F)
// The Android binding has no typed helper for this text option yet.
engine.block.setBoolean(headline, property = "text/automaticFontSizeEnabled", value = true)
```
Android exposes the block sizing modes through typed APIs. The automatic font-size switch uses the generic Boolean property API because no public typed Android setter exists for `text/automaticFontSizeEnabled` yet.
### Range-based Text Styling
Apply different styles to specific character ranges within a single text block:
```kotlin highlight-android-text-range-style
engine.block.setTextColor(
tagline,
color = Color.fromRGBA(r = 0.2F, g = 0.2F, b = 0.8F, a = 1F),
from = 0,
to = 9,
)
if (engine.block.canToggleItalicFont(tagline, from = 0, to = 9)) {
engine.block.toggleItalicFont(tagline, from = 0, to = 9)
}
engine.block.setTextColor(
tagline,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F),
from = 10,
to = 21,
)
if (engine.block.canToggleBoldFont(tagline, from = 10, to = 21)) {
engine.block.toggleBoldFont(tagline, from = 10, to = 21)
}
```
Android's range-based overloads take start-inclusive and end-exclusive UTF-16 code unit indices (`[from, to)`):
- `setTextColor(block, color, from, to)` - apply color to a specific UTF-16 range
- `canToggleBoldFont(block, from, to)` / `toggleBoldFont(block, from, to)` - toggle bold styling for a range
- `canToggleItalicFont(block, from, to)` / `toggleItalicFont(block, from, to)` - toggle italic styling for a range
### Fixed Font Size
Set an explicit font size with `setTextFontSize()` instead of using automatic sizing:
```kotlin highlight-android-text-fixed-size
val ctaTitle = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(ctaTitle, text = "Start a Free Trial")
engine.block.setFont(ctaTitle, fontFileUri = robotoRegular.uri, typeface = robotoTypeface)
engine.block.setTextFontSize(ctaTitle, fontSize = 80F)
engine.block.setTextLineHeight(ctaTitle, lineHeight = 1F)
```
## Add Shapes
We create shapes using graphic blocks. CE.SDK supports `Rect`, `Line`, `Ellipse`, `Polygon`, `Star`, and `VectorPath` shapes through `ShapeType` object constants.
### Create a Shape Block
Create a graphic block and assign a shape to it:
```kotlin highlight-android-shape-create
val dividerLine = engine.block.create(DesignBlockType.Graphic)
val lineShape = engine.block.createShape(ShapeType.Line)
engine.block.setShape(block = dividerLine, shape = lineShape)
```
### Apply Fill to Shape
Create a color fill, assign it to the graphic block, then set the line color with `setFillSolidColor(block=_, color=_)`:
```kotlin highlight-android-shape-fill
val lineFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = dividerLine, fill = lineFill)
engine.block.setFillSolidColor(
block = dividerLine,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F),
)
```
## Add Images
We add images using graphic blocks with image fills.
### Create an Image Block
Create a graphic block with a rect shape and an image fill:
```kotlin highlight-android-image-create
val logo = engine.block.create(DesignBlockType.Graphic)
val logoShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = logo, shape = logoShape)
val logoFill = engine.block.createFill(FillType.Image)
// Image fills currently expose their URI through the generic property API.
engine.block.setUri(
block = logoFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg"),
)
engine.block.setFill(block = logo, fill = logoFill)
```
We set the image URL with `setUri()` and pass the image URI to the `fill/image/imageFileURI` property. Image fills currently expose this URI through the generic property API instead of a dedicated typed setter.
## Position and Size Blocks
All blocks use the same positioning and sizing APIs:
```kotlin highlight-android-block-position
engine.block.setContentFillMode(logo, mode = ContentFillMode.CONTAIN)
engine.block.setWidth(logo, value = 200F)
engine.block.setHeight(logo, value = 65F)
engine.block.setPositionX(logo, value = 820F)
engine.block.setPositionY(logo, value = 960F)
engine.block.appendChild(parent = page, child = logo)
```
- `setWidth()` / `setHeight()` - set block dimensions
- `setPositionX()` / `setPositionY()` - set block position
- `setContentFillMode()` - control how content fills the block (`ContentFillMode.CONTAIN`, `ContentFillMode.COVER`, `ContentFillMode.CROP`)
- `appendChild()` - add the block to the page hierarchy
## Export the Composition
We export the finished composition using the engine API.
### Export Using the Engine API
`engine.block.export()` returns the rendered bytes as a `ByteBuffer`:
```kotlin highlight-android-export-api
val exportOptions = ExportOptions(targetWidth = 1080F, targetHeight = 1080F)
// Ensure remote font files and the image fill are ready before the offscreen export.
engine.block.forceLoadResources(listOf(page, headline, tagline, ctaTitle, ctaUrl, logo))
val blob = engine.block.export(page, mimeType = MimeType.PNG, options = exportOptions)
```
The sample preloads the page and resource-bearing text and image blocks before export, so remote font files and image fills are resolved before the offscreen renderer captures the PNG.
### Write to File System
Write the returned `ByteBuffer` to disk from an IO dispatcher. The sample streams a read-only view of the remaining bytes directly through a file channel, writes them to a temporary PNG file, and returns that file for verification.
```kotlin highlight-android-export-file
return withContext(Dispatchers.IO) {
val outputFile = File.createTempFile("composition-${UUID.randomUUID()}", ".png")
val data = blob.asReadOnlyBuffer()
outputFile.outputStream().channel.use { channel ->
while (data.hasRemaining()) {
channel.write(data)
}
}
outputFile
}
```
## API Reference
| API | Category | Purpose |
| --- | --- | --- |
| `engine.scene.create()` | Scene | Create a scene for programmatic composition. |
| `engine.block.create(blockType=_)` | Block | Create pages, text blocks, and graphic blocks. |
| `engine.block.appendChild(parent=_, child=_)` | Block | Attach a block to the scene or page hierarchy. |
| `engine.block.setWidth(block=_, value=_)` | Layout | Set a block width. |
| `engine.block.setHeight(block=_, value=_)` | Layout | Set a block height. |
| `engine.block.setWidthMode(block=_, mode=_)` | Layout | Set how a block's width is resolved. |
| `engine.block.setHeightMode(block=_, mode=_)` | Layout | Set how a block's height is resolved. |
| `engine.block.setPositionX(block=_, value=_)` | Layout | Set a block's horizontal position. |
| `engine.block.setPositionY(block=_, value=_)` | Layout | Set a block's vertical position. |
| `engine.block.createFill(fillType=_)` | Fill | Create a color or image fill. |
| `engine.block.setFill(block=_, fill=_)` | Fill | Assign a fill to a block. |
| `engine.block.setFillSolidColor(block=_, color=_)` | Fill | Set a solid fill color on a block. |
| `engine.block.createShape(type=_)` | Shape | Create a shape for a graphic block. |
| `engine.block.setShape(block=_, shape=_)` | Shape | Assign a shape to a graphic block. |
| `engine.block.replaceText(block=_, text=_)` | Text | Set text content. |
| `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Text | Bind a typeface and font file to a text block. |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Text | Set a fixed font size. |
| `engine.block.setTextLineHeight(block=_, lineHeight=_)` | Text | Set the line height multiplier for text paragraphs. |
| `engine.block.setTextColor(block=_, color=_)` | Text | Set text color for the full text block. |
| `engine.block.setTextColor(block=_, color=_, from=_, to=_)` | Text | Set text color for a character range. |
| `engine.block.setBoolean(block=_, property="text/automaticFontSizeEnabled", value=_)` | Text | Enable or disable automatic font sizing through the generic property API. |
| `engine.block.canToggleBoldFont(block=_)` | Text | Check whether bold styling can be toggled. |
| `engine.block.toggleBoldFont(block=_)` | Text | Toggle bold styling for the full text block. |
| `engine.block.canToggleBoldFont(block=_, from=_, to=_)` | Text | Check whether bold styling can be toggled for a range. |
| `engine.block.toggleBoldFont(block=_, from=_, to=_)` | Text | Toggle bold styling for a character range. |
| `engine.block.canToggleItalicFont(block=_, from=_, to=_)` | Text | Check whether italic styling can be toggled for a range. |
| `engine.block.toggleItalicFont(block=_, from=_, to=_)` | Text | Toggle italic styling for a character range. |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Image | Set the image URI on an image fill. |
| `engine.block.setContentFillMode(block=_, mode=_)` | Image | Control how image content fits the block. |
| `engine.block.forceLoadResources(blocks=_)` | Export | Resolve referenced fonts and image fills before exporting. |
| `engine.block.export(block=_, mimeType=_, options=_)` | Export | Export the page to image bytes. |
## Troubleshooting
- **Blocks not appearing**: Verify that `appendChild()` attaches blocks to the page. Blocks must be part of the scene hierarchy to render.
- **Text styling not applied**: Verify ranges are correct for range-based APIs. Android uses start-inclusive and end-exclusive UTF-16 code unit indices for the selected range.
- **Image stretched**: Use `setContentFillMode(block, ContentFillMode.CONTAIN)` to maintain the image's aspect ratio.
- **Export fails**: Verify that page dimensions are set before export. The export requires valid dimensions.
- **Typeface missing variants**: If `canToggleBoldFont()` or `canToggleItalicFont()` returns `false`, check that the configured `Typeface` includes the matching weight or style.
## Next Steps
- [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control block stacking and organization
- [Positioning and Alignment](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) - Precise block placement
- [Group and Ungroup](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Group blocks for unified transforms
- [Blend Modes](https://img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519/) - Control how blocks interact visually
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Export options and formats
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Templates"
description: "Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates-3aef79/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/)
---
---
## Related Pages
- [Overview](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) - Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK.
- [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build reusable design templates programmatically using CE.SDK's APIs. Create scenes, add text and graphic blocks, configure placeholders and variables, apply editing constraints, and save templates for reuse.
- [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/) - Load and import design templates into CE.SDK from Android URLs, archives, and serialized scene strings.
- [Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/) - Use variables and placeholders to inject dynamic data into templates at design or runtime.
- [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Restrict editing access to specific elements or properties in a template to enforce design rules.
- [Edit or Remove Templates](https://img.ly/docs/cesdk/android/create-templates/edit-or-remove-38a8be/) - Modify existing templates and manage template lifecycle by loading, editing, saving, and removing templates from asset sources.
- [Add to Template Library](https://img.ly/docs/cesdk/android/create-templates/add-to-template-library-8bfbc7/) - Save and organize templates in an Android asset source so your app can query, manage, and apply them.
- [Overview](https://img.ly/docs/cesdk/android/use-templates/overview-ae74e1/) - Learn how to browse, apply, and dynamically populate templates in CE.SDK to streamline design workflows.
- [Template Library](https://img.ly/docs/cesdk/android/use-templates/library-b3c704/) - Learn how to provide a set of predefined templates in the CreativeEditor SDK.
- [Apply a Template](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) - Apply template scenes to an existing scene with the CE.SDK Engine API for Android, preserving your page dimensions and design unit.
- [Generate From Templates](https://img.ly/docs/cesdk/android/use-templates/generate-334e15/) - Learn how to load and populate CE.SDK templates in Kotlin for Android applications.
- [Replace Content](https://img.ly/docs/cesdk/android/use-templates/replace-content-4c482b/) - Dynamically replace text, images, and placeholder content within templates using CE.SDK's placeholder and variable systems.
- [Use Templates Programmatically](https://img.ly/docs/cesdk/android/use-templates/programmatic-9349f3/) - Work with templates programmatically through CE.SDK's engine APIs to load existing templates, build new templates from scratch, modify template structures, and populate templates with dynamic data for batch processing and automation.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Dynamic Content"
description: "Use variables and placeholders to inject dynamic data into templates at design or runtime."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/)
---
Dynamic content turns static template layouts into data-driven designs. Use
text variables, placeholders, form-based editing patterns, and editing
constraints to personalize content while keeping the layout predictable.
This overview maps each dynamic-content capability to the Android implementation
area or app pattern you use. Continue with the dedicated guides below when you
are ready to implement each part in detail.
## Dynamic Content Capabilities
CE.SDK templates can expose controlled customization points in four ways:
- **Text Variables** - Insert `{{tokens}}` into text blocks and set their
values from Kotlin.
- **Placeholders** - Mark supported blocks as drop zones for swappable content.
- **Form-Based Editing** - Build app UI that maps structured inputs to
variables and placeholders.
- **Editing Constraints** - Lock specific block properties so users can update
content without breaking the design.
## Text Variables
Text variables enable data-driven text personalization. Add tokens such as
`{{firstName}}` to text blocks, then set the corresponding values through
`engine.variable.set(key=_, value=_)`.
Keep the required variable keys in template or app metadata, or inspect text
blocks for `{{...}}` references when your app needs to derive them from a scene.
`engine.variable.findAll()` only lists variables that have already been stored
in the Engine. Use it for readback flows together with
`engine.variable.get(key=_)`, and use
`engine.block.referencesAnyVariables(block=_)` when you need to detect whether a
specific text block depends on any variables.
> **Note:** Variable keys are case-sensitive. `{{Name}}` and `{{name}}` reference
> different variables.
## Placeholders
Placeholders turn supported blocks into replacement targets. They are useful for
template areas where adopters should replace an image, video, or other supported
content while the surrounding layout stays fixed.
Placeholder interaction and placeholder behavior target different objects. Use
`engine.block.setPlaceholderEnabled(block=_, enabled=_)` on the placeholder
block itself. For image or video placeholders, check and enable placeholder
behavior on the supporting image or video fill block; for text placeholders, do
that on the text block. Call
`engine.block.supportsPlaceholderBehavior(block=_)` on that fill or text block
before `engine.block.setPlaceholderBehaviorEnabled(block=_, enabled=_)`. Use the
placeholder control APIs on the placeholder block when your editor surface
should show overlay or button affordances.
## Form-Based Editing
Form-based editing is an app-level pattern on Android. Instead of asking users
to select blocks on the canvas, your app can present Compose fields or custom
controls that read template variables and placeholder blocks, then write the
updated values back through the same Engine APIs.
This pattern works well for guided template adoption, batch workflows, and
non-designer editing surfaces. Keep the form model data-driven: variable names,
placeholder blocks, labels, validation rules, and submitted values should come
from your template metadata or app data model.
## Editing Constraints
Editing constraints protect template structure by limiting which operations are
allowed on each block. CE.SDK evaluates effective permissions from the active
role, global scopes, and block-level scopes. In the default Creator role, global
scopes usually allow edits, so a block-level
`engine.block.setScopeEnabled(block=_, key=_, enabled=_)` call alone does not
necessarily enforce a lock.
To make block-level scopes matter, switch to role defaults such as
`engine.editor.setRole("Adopter")`, or set the relevant global scope explicitly
with `engine.editor.setGlobalScope(key=_, globalScope=GlobalScope.DEFER)`.
Then use `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` for
per-block permissions such as `layer/move`, `layer/resize`, `fill/change`, or
`text/edit`. Use `GlobalScope.DENY` only when an operation should be blocked for
every block regardless of block-level flags. Use
`engine.block.isAllowedByScope(block=_, key=_)` when you need to check the final
effective permission.
Combine constraints with variables and placeholders to allow targeted
customization. For example, a template can let users replace a hero image and
update text variables while the logo, layout, and protected brand elements stay
locked.
## Combining Capabilities
Production templates often combine these capabilities:
1. **Text Variables + Placeholders** - Personalize both text and media in one
template.
2. **Placeholders + Constraints** - Allow replacement while protecting size and
position.
3. **Variables + Form-Based Editing** - Expose template text as structured input
fields.
4. **All Together** - Build guided editing flows where users enter data, replace
assets, and export a design without direct layout editing.
## Choosing the Right Capability
| Need | Capability | Android focus |
| --- | --- | --- |
| Dynamic text content | Text Variables | `engine.variable.set(key=_, value=_)`, `engine.variable.get(key=_)` |
| Swappable images, videos, or supported blocks | Placeholders | Placeholder behavior and control APIs |
| Simplified editing UI | Form-Based Editing | Custom app UI backed by variables and placeholders |
| Locked template structure | Editing Constraints | Roles, global scopes, and block-level scopes |
## Next Steps
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values during design generation.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Use placeholders to mark editable image, video, or text areas within a locked template layout.
- [Form-Based Editing](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/form-based-editing-a8a779/) - Build custom form interfaces for template customization using CE.SDK variables and placeholders.
- [Set Editing Constraints](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Learn how to control editing capabilities in CE.SDK templates using the Scope system to lock positions, prevent transformations, and create guided editing experiences.
- [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) - Generate personalized designs from templates by merging external data using text variables and placeholder blocks.
---
## Related Pages
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values during design generation.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Use placeholders to mark editable image, video, or text areas within a locked template layout.
- [Set Editing Constraints](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Control what users can edit in templates with CE.SDK scopes on Android.
- [Form-Based Editing](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/form-based-editing-a8a779/) - Build custom native form workflows that populate template variables and placeholders on Android.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Form-Based Editing"
description: "Build custom native form workflows that populate template variables and placeholders on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/form-based-editing-a8a779/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/) > [Form-Based Editing](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/form-based-editing-a8a779/)
---
```kotlin file=@cesdk_android_examples/engine-guides-form-based-editing/FormBasedEditing.kt reference-only
import android.net.Uri
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.nio.ByteBuffer
data class TemplateFormState(
val headline: String,
val subline: String,
val heroImageUri: Uri,
)
data class FormBasedEditingResult(
val variableKeys: List,
val placeholderNames: List,
val initialValues: Map,
val resolvedVariables: Map,
val initialHeroImageUri: Uri,
val pngData: ByteBuffer,
)
suspend fun formBasedEditing(engine: Engine): FormBasedEditingResult {
engine.variable.findAll().forEach { key -> engine.variable.remove(key) }
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1350F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(background, fill = backgroundFill)
engine.block.setFillSolidColor(background, color = Color.fromHex("#F8F4EC"))
engine.block.appendChild(parent = page, child = background)
engine.block.fillParent(background)
val heroImage = engine.block.create(DesignBlockType.Graphic)
val heroFill = engine.block.createFill(FillType.Image)
engine.block.setName(heroImage, name = "hero-image")
engine.block.setShape(heroImage, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(heroImage, value = 72F)
engine.block.setPositionY(heroImage, value = 72F)
engine.block.setWidth(heroImage, value = 936F)
engine.block.setHeight(heroImage, value = 612F)
engine.block.setContentFillMode(heroImage, mode = ContentFillMode.COVER)
engine.block.setUri(
block = heroFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(heroImage, fill = heroFill)
engine.block.appendChild(parent = page, child = heroImage)
engine.block.setPlaceholderEnabled(heroImage, enabled = true)
val headline = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(headline, text = "{{headline}}")
engine.block.setPositionX(headline, value = 96F)
engine.block.setPositionY(headline, value = 760F)
engine.block.setWidth(headline, value = 888F)
engine.block.setHeightMode(headline, mode = SizeMode.AUTO)
engine.block.setTextFontSize(headline, fontSize = 30F)
engine.block.setTextColor(headline, color = Color.fromHex("#26211C"))
engine.block.appendChild(parent = page, child = headline)
val subline = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(subline, text = "{{subline}}")
engine.block.setPositionX(subline, value = 96F)
engine.block.setPositionY(subline, value = 1030F)
engine.block.setWidth(subline, value = 790F)
engine.block.setHeightMode(subline, mode = SizeMode.AUTO)
engine.block.setTextFontSize(subline, fontSize = 15F)
engine.block.setTextColor(subline, color = Color.fromHex("#5A5046"))
engine.block.appendChild(parent = page, child = subline)
engine.variable.set(key = "headline", value = "Spring Workshop")
engine.variable.set(key = "subline", value = "Reserve your place today.")
val variableKeys = engine.variable.findAll().sorted()
val imagePlaceholders = engine.block.findByType(DesignBlockType.Graphic)
.filter { block ->
engine.block.isPlaceholderEnabled(block) && engine.block.supportsFill(block)
}
val placeholderNames = imagePlaceholders.map { block -> engine.block.getName(block) }
val initialValues = variableKeys.associateWith { key -> engine.variable.get(key) }
val heroPlaceholder = imagePlaceholders.first { block ->
engine.block.getName(block) == "hero-image"
}
val currentHeroFill = engine.block.getFill(heroPlaceholder)
val initialHeroImageUri = engine.block.getUri(
block = currentHeroFill,
property = "fill/image/imageFileURI",
)
val submittedForm = TemplateFormState(
headline = "Launch Workshop",
subline = "Join the live product walkthrough.",
heroImageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg"),
)
val missingRequiredFields = listOfNotNull(
"headline".takeIf { submittedForm.headline.isBlank() },
"subline".takeIf { submittedForm.subline.isBlank() },
"heroImageUri".takeIf { submittedForm.heroImageUri.toString().isBlank() },
)
check(missingRequiredFields.isEmpty()) {
"Missing required form fields: ${missingRequiredFields.joinToString()}"
}
engine.variable.set(key = "headline", value = submittedForm.headline)
engine.variable.set(key = "subline", value = submittedForm.subline)
val updatedHeroFill = engine.block.getFill(heroPlaceholder)
engine.block.setUri(
block = updatedHeroFill,
property = "fill/image/imageFileURI",
value = submittedForm.heroImageUri,
)
engine.block.resetCrop(heroPlaceholder)
val resolvedVariables = variableKeys.associateWith { key -> engine.variable.get(key) }
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
return FormBasedEditingResult(
variableKeys = variableKeys,
placeholderNames = placeholderNames,
initialValues = initialValues,
resolvedVariables = resolvedVariables,
initialHeroImageUri = initialHeroImageUri,
pngData = pngData,
)
}
```
Build custom Android form interfaces that populate template variables and
image placeholders through the CreativeEngine API.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-form-based-editing)
Form-based editing turns template customization into structured data entry. Instead of asking users to edit blocks directly on the canvas, your Android UI collects values in native controls and applies them to a template with variables and placeholders.
## Understanding Form-Based Editing
Variables control text content. Image placeholders control replaceable image blocks. A form-based workflow maps each native input field to one of those template fields, validates the submitted values, and updates the engine so the template preview or export reflects the form state.
CE.SDK does not ship a dedicated Android form panel. If you want a full editor preview beside your form, use the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) as the CE.SDK editor UI and drive the same Engine APIs from your own Compose controls.
## Discovering Template Metadata
Start from a loaded template that already contains text variables and image placeholders. Query the variable store and filter placeholder blocks to determine which form controls your Android UI needs.
```kotlin highlight-android-discover-fields
val variableKeys = engine.variable.findAll().sorted()
val imagePlaceholders = engine.block.findByType(DesignBlockType.Graphic)
.filter { block ->
engine.block.isPlaceholderEnabled(block) && engine.block.supportsFill(block)
}
val placeholderNames = imagePlaceholders.map { block -> engine.block.getName(block) }
```
`engine.variable.findAll()` returns the variable keys currently stored in the engine. The placeholder lookup uses typed `DesignBlockType.Graphic` queries and checks `isPlaceholderEnabled()` so the form only targets blocks that the template author marked as replaceable.
## Working with Variables
Represent the values from your native controls in an app-owned data model. CE.SDK only receives the final strings and image URIs; TextField state, validation messages, and image pickers stay in your Android UI layer.
```kotlin highlight-android-form-state
data class TemplateFormState(
val headline: String,
val subline: String,
val heroImageUri: Uri,
)
```
### Reading Current Values
Read existing variable values to prefill your form when a user opens a template that already contains default content.
```kotlin highlight-android-read-values
val initialValues = variableKeys.associateWith { key -> engine.variable.get(key) }
val heroPlaceholder = imagePlaceholders.first { block ->
engine.block.getName(block) == "hero-image"
}
val currentHeroFill = engine.block.getFill(heroPlaceholder)
val initialHeroImageUri = engine.block.getUri(
block = currentHeroFill,
property = "fill/image/imageFileURI",
)
```
The same step can read the current image URI from a placeholder fill, which lets your UI show the currently assigned image before the user selects a replacement.
### Updating Variables
After your Compose controls produce a submitted form state, assign each text field to the matching variable key.
```kotlin highlight-android-collected-values
val submittedForm = TemplateFormState(
headline = "Launch Workshop",
subline = "Join the live product walkthrough.",
heroImageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg"),
)
```
```kotlin highlight-android-update-variables
engine.variable.set(key = "headline", value = submittedForm.headline)
engine.variable.set(key = "subline", value = submittedForm.subline)
```
Text blocks that reference `{{headline}}` or `{{subline}}` update from the variable store during preview and export.
## Replacing Placeholder Content
For image fields, locate the placeholder block, get its fill, and update the image URI stored on that fill.
```kotlin highlight-android-replace-placeholder
val updatedHeroFill = engine.block.getFill(heroPlaceholder)
engine.block.setUri(
block = updatedHeroFill,
property = "fill/image/imageFileURI",
value = submittedForm.heroImageUri,
)
engine.block.resetCrop(heroPlaceholder)
```
Resetting the crop after the replacement keeps the placeholder framing consistent when the new image has different dimensions than the original asset.
## Building the Form UI
Build the visible form with normal Android UI primitives such as Compose `TextField`, image picker launchers, dropdowns, or validation labels. Keep that UI state in your app, then pass the submitted values into the mapping layer shown above.
The important boundary is that CE.SDK does not need to own the form controls. Your code discovers the editable template fields, shows matching native controls, and calls `engine.variable.set()` for text fields or `engine.block.setUri()` for image fields when the user changes content.
## Error Handling
Validate the form before export or before enabling a final action. Required text fields and image selections should be checked in your UI state before you mutate the template.
```kotlin highlight-android-validate
val missingRequiredFields = listOfNotNull(
"headline".takeIf { submittedForm.headline.isBlank() },
"subline".takeIf { submittedForm.subline.isBlank() },
"heroImageUri".takeIf { submittedForm.heroImageUri.toString().isBlank() },
)
check(missingRequiredFields.isEmpty()) {
"Missing required form fields: ${missingRequiredFields.joinToString()}"
}
```
Then export the populated page once the data is complete.
```kotlin highlight-android-export
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
```
Handle these cases in the same validation layer:
- Missing variables: compare discovered keys with the fields your form requires.
- Invalid images: check MIME type and URI availability before assigning a file to an image placeholder.
- Missing placeholders: keep stable block names or metadata for required image fields.
- Export failures: report which field or asset prevented the final output.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.variable.findAll()` | List variable keys stored on the engine |
| `engine.variable.get(key=_)` | Read the current value for a variable key |
| `engine.variable.set(key=_, value=_)` | Set or update a text variable |
| `engine.block.findByType(type=_)` | Find blocks by typed design-block type |
| `engine.block.isPlaceholderEnabled(block=_)` | Check whether a block is enabled as a placeholder |
| `engine.block.supportsFill(block=_)` | Check whether a block can carry a fill |
| `engine.block.getName(block=_)` | Read the semantic name assigned to a block |
| `engine.block.getFill(block=_)` | Get the fill block attached to a design block |
| `engine.block.getUri(block=_, property=_)` | Read URI-backed block or fill properties such as image file URIs |
| `engine.block.setUri(block=_, property=_, value=_)` | Update URI-backed block or fill properties such as image file URIs |
| `engine.block.resetCrop(block=_)` | Reset crop values after replacing an image |
| `engine.block.export(block=_, mimeType=_)` | Export the populated template page |
## Next Steps
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) — Deep dive into variable management
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Understand placeholder configuration
- [Lock Templates](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) — Combine forms with locked designs
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Placeholders"
description: "Use placeholders to mark editable image, video, or text areas within a locked template layout."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/) > [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/)
---
```kotlin file=@cesdk_android_examples/engine-guides-placeholders/Placeholders.kt reference-only
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
data class Placeholders(
val imagePlaceholder: DesignBlock,
val textPlaceholder: DesignBlock,
val imageBehaviorSupported: Boolean,
val textBehaviorSupported: Boolean,
val imageControlsSupported: Boolean,
val textControlsSupported: Boolean,
val imageBehaviorEnabled: Boolean,
val textBehaviorEnabled: Boolean,
val imagePlaceholderEnabled: Boolean,
val textPlaceholderEnabled: Boolean,
val overlayEnabled: Boolean,
val buttonEnabled: Boolean,
val batchImageBlock: DesignBlock,
val batchImageFill: DesignBlock,
val batchTextBlock: DesignBlock,
val placeholders: List,
)
fun placeholders(engine: Engine): Placeholders {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
val imagePlaceholder = engine.block.create(DesignBlockType.Graphic)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setName(imagePlaceholder, name = "image-placeholder")
engine.block.setShape(imagePlaceholder, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(imagePlaceholder, fill = imageFill)
engine.block.appendChild(parent = page, child = imagePlaceholder)
val textPlaceholder = engine.block.create(DesignBlockType.Text)
engine.block.setName(textPlaceholder, name = "text-placeholder")
engine.block.replaceText(textPlaceholder, text = "Replace this text")
engine.block.appendChild(parent = page, child = textPlaceholder)
val imagePlaceholderFill = engine.block.getFill(imagePlaceholder)
val imageBehaviorSupported = engine.block.supportsPlaceholderBehavior(imagePlaceholderFill)
val imageControlsSupported = engine.block.supportsPlaceholderControls(imagePlaceholder)
val textBehaviorSupported = engine.block.supportsPlaceholderBehavior(textPlaceholder)
val textControlsSupported = engine.block.supportsPlaceholderControls(textPlaceholder)
val imageBehaviorEnabled = if (imageBehaviorSupported) {
engine.block.setPlaceholderBehaviorEnabled(imagePlaceholderFill, enabled = true)
engine.block.isPlaceholderBehaviorEnabled(imagePlaceholderFill)
} else {
false
}
val textBehaviorEnabled = if (textBehaviorSupported) {
engine.block.setPlaceholderBehaviorEnabled(textPlaceholder, enabled = true)
engine.block.isPlaceholderBehaviorEnabled(textPlaceholder)
} else {
false
}
engine.block.setPlaceholderEnabled(imagePlaceholder, enabled = true)
engine.block.setPlaceholderEnabled(textPlaceholder, enabled = true)
val imagePlaceholderEnabled = engine.block.isPlaceholderEnabled(imagePlaceholder)
val textPlaceholderEnabled = engine.block.isPlaceholderEnabled(textPlaceholder)
val overlayEnabled: Boolean
val buttonEnabled: Boolean
if (imageControlsSupported) {
engine.block.setPlaceholderControlsOverlayEnabled(imagePlaceholder, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(imagePlaceholder, enabled = true)
overlayEnabled = engine.block.isPlaceholderControlsOverlayEnabled(imagePlaceholder)
buttonEnabled = engine.block.isPlaceholderControlsButtonEnabled(imagePlaceholder)
} else {
overlayEnabled = false
buttonEnabled = false
}
engine.block.setScopeEnabled(imagePlaceholder, key = "fill/change", enabled = true)
engine.block.setScopeEnabled(textPlaceholder, key = "text/edit", enabled = true)
val batchImageBlock = engine.block.create(DesignBlockType.Graphic)
val batchImageFill = engine.block.createFill(FillType.Image)
engine.block.setShape(batchImageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(batchImageBlock, fill = batchImageFill)
engine.block.appendChild(parent = page, child = batchImageBlock)
val batchTextBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(batchTextBlock, text = "Replace this text too")
engine.block.appendChild(parent = page, child = batchTextBlock)
val batchTargets = listOf(
Triple(batchImageBlock, batchImageFill, "fill/change"),
Triple(batchTextBlock, batchTextBlock, "text/edit"),
)
batchTargets.forEach { (block, behaviorTarget, contentScope) ->
if (engine.block.supportsPlaceholderBehavior(behaviorTarget)) {
engine.block.setPlaceholderBehaviorEnabled(behaviorTarget, enabled = true)
engine.block.setPlaceholderEnabled(block, enabled = true)
engine.block.setScopeEnabled(block, key = contentScope, enabled = true)
}
if (engine.block.supportsPlaceholderControls(block)) {
engine.block.setPlaceholderControlsOverlayEnabled(block, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(block, enabled = true)
}
}
val placeholders = engine.block.findAllPlaceholders()
return Placeholders(
imagePlaceholder = imagePlaceholder,
textPlaceholder = textPlaceholder,
imageBehaviorSupported = imageBehaviorSupported,
textBehaviorSupported = textBehaviorSupported,
imageControlsSupported = imageControlsSupported,
textControlsSupported = textControlsSupported,
imageBehaviorEnabled = imageBehaviorEnabled,
textBehaviorEnabled = textBehaviorEnabled,
imagePlaceholderEnabled = imagePlaceholderEnabled,
textPlaceholderEnabled = textPlaceholderEnabled,
overlayEnabled = overlayEnabled,
buttonEnabled = buttonEnabled,
batchImageBlock = batchImageBlock,
batchImageFill = batchImageFill,
batchTextBlock = batchTextBlock,
placeholders = placeholders,
)
}
```
Turn image, video, or text content into replaceable areas while keeping the
surrounding template layout under your control.

> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-placeholders)
Placeholders separate replaceable content from the template block that positions and styles it. This guide checks support, enables placeholder behavior and interaction, configures visual controls, and applies placeholder settings to multiple blocks.
## Placeholder Fundamentals
Placeholder configuration has three parts:
- **Placeholder behavior** marks content as replaceable.
- **Placeholder interaction** marks the containing block as available to an Adopter.
- **Placeholder controls** display the overlay and Replace button in an editor UI.
### Block-Level vs Fill-Level Behavior
The behavior target depends on the block type:
- For graphic blocks with image or video content, enable placeholder behavior on the fill returned by `engine.block.getFill`. Enable interaction and controls on the graphic block.
- For text blocks, enable behavior and interaction on the text block. Text blocks do not support placeholder controls.
## Checking Placeholder Support
Check each target before enabling its placeholder feature. The image fill supports behavior, the graphic block supports controls, and the text block supports behavior directly.
```kotlin highlight-android-check-support
val imagePlaceholderFill = engine.block.getFill(imagePlaceholder)
val imageBehaviorSupported = engine.block.supportsPlaceholderBehavior(imagePlaceholderFill)
val imageControlsSupported = engine.block.supportsPlaceholderControls(imagePlaceholder)
val textBehaviorSupported = engine.block.supportsPlaceholderBehavior(textPlaceholder)
val textControlsSupported = engine.block.supportsPlaceholderControls(textPlaceholder)
```
The support predicates let your app skip unsupported block types without relying on type assumptions.
## Enabling Placeholder Behavior
Placeholder behavior turns supported content into a replacement target.
### For Graphic Blocks
Read the graphic block's fill, then enable and verify behavior on that fill.
```kotlin highlight-android-enable-image-behavior
val imageBehaviorEnabled = if (imageBehaviorSupported) {
engine.block.setPlaceholderBehaviorEnabled(imagePlaceholderFill, enabled = true)
engine.block.isPlaceholderBehaviorEnabled(imagePlaceholderFill)
} else {
false
}
```
The same pattern applies to image and video fills.
### For Text Blocks
Text content belongs directly to the text block, so use that block as the behavior target.
```kotlin highlight-android-enable-text-behavior
val textBehaviorEnabled = if (textBehaviorSupported) {
engine.block.setPlaceholderBehaviorEnabled(textPlaceholder, enabled = true)
engine.block.isPlaceholderBehaviorEnabled(textPlaceholder)
} else {
false
}
```
## Enabling Adopter Interaction
Enable placeholder interaction on the block a user selects or replaces, not on its fill.
```kotlin highlight-android-enable-interaction
engine.block.setPlaceholderEnabled(imagePlaceholder, enabled = true)
engine.block.setPlaceholderEnabled(textPlaceholder, enabled = true)
val imagePlaceholderEnabled = engine.block.isPlaceholderEnabled(imagePlaceholder)
val textPlaceholderEnabled = engine.block.isPlaceholderEnabled(textPlaceholder)
```
`setPlaceholderEnabled` marks the block as a placeholder for Adopter workflows and prepares its interaction state for the role switch:
- **Adopter selectability:** While the current role is Creator, `setPlaceholderEnabled(block, enabled)` writes the same value to the block-level `editor/select` scope. Adopter global scopes defer to this setting, so enabling keeps the block selectable after the switch, while disabling makes it unselectable.
- **Creator selection:** The current Creator can still select the block because Creator scopes are globally allowed.
- **Image and video replacement:** After an Adopter replaces an image or video asset through the CE.SDK editor UI, the editor clears the block's placeholder flag.
- **Text editing:** When an Adopter edits placeholder text interactively through the CE.SDK editor UI, the editor disables placeholder behavior on the text block without clearing its placeholder flag. Creator edits and programmatic `replaceText` calls do not trigger this automatic change.
## Configuring Visual Feedback
Graphic placeholders can show an overlay pattern and a Replace button. Enable these controls only after `supportsPlaceholderControls` returns `true`.
```kotlin highlight-android-enable-controls
val overlayEnabled: Boolean
val buttonEnabled: Boolean
if (imageControlsSupported) {
engine.block.setPlaceholderControlsOverlayEnabled(imagePlaceholder, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(imagePlaceholder, enabled = true)
overlayEnabled = engine.block.isPlaceholderControlsOverlayEnabled(imagePlaceholder)
buttonEnabled = engine.block.isPlaceholderControlsButtonEnabled(imagePlaceholder)
} else {
overlayEnabled = false
buttonEnabled = false
}
```
The controls appear in editor preview surfaces and are excluded from exports.
> **Note:** See the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) for a complete
> Android editor UI that can host template workflows.
## Scope Requirements
Scopes determine whether an Adopter may replace placeholder content. Enable `fill/change` for graphic placeholders and `text/edit` for text placeholders.
```kotlin highlight-android-enable-scopes
engine.block.setScopeEnabled(imagePlaceholder, key = "fill/change", enabled = true)
engine.block.setScopeEnabled(textPlaceholder, key = "text/edit", enabled = true)
```
The active role and global scope settings also participate in the final permission. Use editing constraints to protect movement, resizing, and other layout operations separately.
## Working with Multiple Placeholders
After creating candidate graphic and text blocks, apply the same placeholder configuration to them in one pass. Pair each selectable block with the target that owns its placeholder behavior: the fill for graphic content, or the block itself for text content.
```kotlin highlight-android-batch-placeholders
val batchTargets = listOf(
Triple(batchImageBlock, batchImageFill, "fill/change"),
Triple(batchTextBlock, batchTextBlock, "text/edit"),
)
batchTargets.forEach { (block, behaviorTarget, contentScope) ->
if (engine.block.supportsPlaceholderBehavior(behaviorTarget)) {
engine.block.setPlaceholderBehaviorEnabled(behaviorTarget, enabled = true)
engine.block.setPlaceholderEnabled(block, enabled = true)
engine.block.setScopeEnabled(block, key = contentScope, enabled = true)
}
if (engine.block.supportsPlaceholderControls(block)) {
engine.block.setPlaceholderControlsOverlayEnabled(block, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(block, enabled = true)
}
}
```
For each block whose target supports placeholder behavior, the loop enables behavior on that target plus interaction and the required content scope on the containing block; visual controls are enabled only on blocks that support them.
### Discovering Configured Placeholders
After configuration, use `findAllPlaceholders` to retrieve the blocks whose placeholder interaction flag is enabled.
```kotlin highlight-android-find-placeholders
val placeholders = engine.block.findAllPlaceholders()
```
Use the returned collection to inspect or validate the configured placeholder set. It is a discovery API; it does not identify blocks that still need placeholder configuration.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.block.getFill(block=_)` | Get the fill that carries replaceable image or video content. |
| `engine.block.supportsPlaceholderBehavior(block=_)` | Check whether a fill or text block supports placeholder behavior. |
| `engine.block.setPlaceholderBehaviorEnabled(block=_, enabled=_)` | Enable or disable placeholder behavior on a supported target. |
| `engine.block.isPlaceholderBehaviorEnabled(block=_)` | Read the placeholder behavior state. |
| `engine.block.setPlaceholderEnabled(block=_, enabled=_)` | Enable or disable Adopter interaction on a placeholder block. |
| `engine.block.isPlaceholderEnabled(block=_)` | Read the block's placeholder interaction state. |
| `engine.block.supportsPlaceholderControls(block=_)` | Check whether a block supports placeholder controls. |
| `engine.block.setPlaceholderControlsOverlayEnabled(block=_, enabled=_)` | Show or hide the placeholder overlay. |
| `engine.block.isPlaceholderControlsOverlayEnabled(block=_)` | Read the overlay visibility state. |
| `engine.block.setPlaceholderControlsButtonEnabled(block=_, enabled=_)` | Show or hide the Replace button. |
| `engine.block.isPlaceholderControlsButtonEnabled(block=_)` | Read the Replace button visibility state. |
| `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Configure a block-level editing scope. |
| `engine.block.isScopeEnabled(block=_, key=_)` | Read whether a block-level editing scope is enabled. |
| `engine.block.findAllPlaceholders()` | Find all instantiated placeholder blocks, including blocks not attached to a scene. |
## Next Steps
- [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Restrict editing access to specific elements or properties to enforce design rules.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Set Editing Constraints"
description: "Control what users can edit in templates with CE.SDK scopes on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/) > [Set Editing Constraints](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/)
---
```kotlin file=@cesdk_android_examples/engine-guides-set-editing-constraints/SetEditingConstraints.kt reference-only
import android.util.Log
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.ShapeType
private const val MOVE_SCOPE = "layer/move"
private const val RESIZE_SCOPE = "layer/resize"
private const val DESTROY_SCOPE = "lifecycle/destroy"
private const val DUPLICATE_SCOPE = "lifecycle/duplicate"
suspend fun setEditingConstraints(engine: Engine): SetEditingConstraintsResult {
// Demo scaffolding: a scene and page to hold the constrained blocks.
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1200F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
// Keep these scopes deferred so the returned scene uses block-level constraints.
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/duplicate", globalScope = GlobalScope.DEFER)
// Demo scaffolding: two renderable graphic blocks, constrained independently.
val positionLocked = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(positionLocked, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(positionLocked, fill = engine.block.createFill(FillType.Color))
engine.block.setWidth(positionLocked, value = 200F)
engine.block.setHeight(positionLocked, value = 200F)
engine.block.appendChild(parent = page, child = positionLocked)
val deletionLocked = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(deletionLocked, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(deletionLocked, fill = engine.block.createFill(FillType.Color))
engine.block.setWidth(deletionLocked, value = 200F)
engine.block.setHeight(deletionLocked, value = 200F)
engine.block.appendChild(parent = page, child = deletionLocked)
engine.block.setScopeEnabled(positionLocked, key = "lifecycle/destroy", enabled = true)
engine.block.setScopeEnabled(positionLocked, key = "lifecycle/duplicate", enabled = true)
engine.block.setScopeEnabled(positionLocked, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(positionLocked, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(deletionLocked, key = "layer/move", enabled = true)
engine.block.setScopeEnabled(deletionLocked, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(deletionLocked, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(deletionLocked, key = "lifecycle/duplicate", enabled = false)
val moveScopeEnabled = engine.block.isScopeEnabled(positionLocked, key = "layer/move")
Log.i("SetEditingConstraints", "layer/move enabled at block level: $moveScopeEnabled")
val moveAllowed = engine.block.isAllowedByScope(positionLocked, key = "layer/move")
Log.i("SetEditingConstraints", "layer/move allowed: $moveAllowed")
val resizeAllowed = engine.block.isAllowedByScope(positionLocked, key = RESIZE_SCOPE)
val positionLockedDestroyAllowed = engine.block.isAllowedByScope(positionLocked, key = DESTROY_SCOPE)
val positionLockedDuplicateAllowed = engine.block.isAllowedByScope(positionLocked, key = DUPLICATE_SCOPE)
val destroyAllowed = engine.block.isAllowedByScope(deletionLocked, key = DESTROY_SCOPE)
val duplicateAllowed = engine.block.isAllowedByScope(deletionLocked, key = DUPLICATE_SCOPE)
val deletionLockedMoveAllowed = engine.block.isAllowedByScope(deletionLocked, key = MOVE_SCOPE)
return SetEditingConstraintsResult(
availableScopes = engine.editor.findAllScopes(),
moveScopeEnabled = moveScopeEnabled,
moveAllowed = moveAllowed,
resizeAllowed = resizeAllowed,
positionLockedDestroyAllowed = positionLockedDestroyAllowed,
positionLockedDuplicateAllowed = positionLockedDuplicateAllowed,
destroyAllowed = destroyAllowed,
duplicateAllowed = duplicateAllowed,
deletionLockedMoveAllowed = deletionLockedMoveAllowed,
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-set-editing-constraints/SetEditingConstraintsResult.kt reference-only
data class SetEditingConstraintsResult(
val availableScopes: List,
val moveScopeEnabled: Boolean,
val moveAllowed: Boolean,
val resizeAllowed: Boolean,
val positionLockedDestroyAllowed: Boolean,
val positionLockedDuplicateAllowed: Boolean,
val destroyAllowed: Boolean,
val duplicateAllowed: Boolean,
val deletionLockedMoveAllowed: Boolean,
)
```
Control what users can edit in templates by setting fine-grained permissions
on individual blocks or globally across the scene with the CE.SDK Scope
system.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-set-editing-constraints)
Editing constraints let you lock specific properties of design elements while keeping others editable. Scopes cover movement, resizing, rotation, fill changes, text editing, lifecycle operations, and other editor capabilities. Use them to protect brand templates, guide template adoption, and build form-based workflows where users can personalize only selected fields.
## Understanding Scopes
### What Are Scopes?
A scope is a permission key that controls one editing capability. Each scope represents a distinct action, such as moving blocks (`"layer/move"`), changing fills (`"fill/change"`), or editing text content (`"text/edit"`).
Scopes exist at two levels:
- **Block-level scopes**: Per-block permissions set with `engine.block.setScopeEnabled(...)`.
- **Global scopes**: Scene-wide defaults set with `engine.editor.setGlobalScope(...)`.
Global scope defaults depend on the editor role. Under the default Creator role, global scopes are allowed, so block-level restrictions are not consulted. To make a block-level setting take effect, set the matching global scope to `GlobalScope.DEFER`, either through your editor role setup or with `engine.editor.setGlobalScope(...)`.
### Available Scope Categories
CE.SDK groups scopes into logical categories. Retrieve the full list at runtime with `engine.editor.findAllScopes()`.
| Category | Purpose | Example Scopes |
| --- | --- | --- |
| **Text Editing** | Control text content and formatting | `text/edit`, `text/character` |
| **Fill & Stroke** | Manage colors, fills, and strokes | `fill/change`, `fill/changeType`, `stroke/change` |
| **Shape** | Modify shape properties | `shape/change` |
| **Layer Transform** | Control position and dimensions | `layer/move`, `layer/resize`, `layer/rotate`, `layer/flip`, `layer/crop` |
| **Layer Appearance** | Manage visual properties | `layer/opacity`, `layer/blendMode`, `layer/visibility`, `layer/clipping` |
| **Effects & Filters** | Apply visual effects | `appearance/adjustments`, `appearance/filter`, `appearance/effect`, `appearance/blur`, `appearance/shadow` |
| **Lifecycle** | Control deletion and duplication | `lifecycle/destroy`, `lifecycle/duplicate` |
| **Editor** | Manage scene-level actions | `editor/add`, `editor/select` |
## Scope Configuration
### Global Scope Modes
Global scopes set the default behavior for all blocks in the scene. They have three modes:
| Mode | Behavior |
| --- | --- |
| `GlobalScope.ALLOW` | Always allow the action, overriding block-level settings |
| `GlobalScope.DENY` | Always deny the action, overriding block-level settings |
| `GlobalScope.DEFER` | Use the block-level setting for each block |
To make block-level constraints take effect, defer the relevant global scopes to the block level:
```kotlin highlight-android-global-scopes
// Keep these scopes deferred so the returned scene uses block-level constraints.
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/duplicate", globalScope = GlobalScope.DEFER)
```
Defer only the scopes that your app controls at the block level. When a block should keep a deferred capability,
explicitly enable that scope on the block.
### Scope Resolution Priority
When both global and block-level scopes apply, CE.SDK resolves permissions in this order:
1. **Global `GlobalScope.DENY`** blocks the action.
2. **Global `GlobalScope.ALLOW`** permits the action.
3. **Global `GlobalScope.DEFER`** uses the block-level setting for each block.
## Setting Block-Level Constraints
### Locking Position
Prevent users from moving a block while keeping resizing available:
```kotlin highlight-android-lock-position
engine.block.setScopeEnabled(positionLocked, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(positionLocked, key = "layer/move", enabled = false)
```
Disabling `layer/move` locks the block position. Because the full sample also defers resizing and lifecycle scopes,
the block can still resize, delete, and duplicate when those block-level scopes remain enabled.
### Preventing Deletion
Protect a block from being deleted or duplicated while keeping transform edits available:
```kotlin highlight-android-prevent-deletion
engine.block.setScopeEnabled(deletionLocked, key = "layer/move", enabled = true)
engine.block.setScopeEnabled(deletionLocked, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(deletionLocked, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(deletionLocked, key = "lifecycle/duplicate", enabled = false)
```
Use this for essential template elements that must remain present. Movement and resizing can remain enabled independently because lifecycle scopes are separate permissions.
### Checking Scope State
Query the block-level setting for any scope:
```kotlin highlight-android-check-scope
val moveScopeEnabled = engine.block.isScopeEnabled(positionLocked, key = "layer/move")
Log.i("SetEditingConstraints", "layer/move enabled at block level: $moveScopeEnabled")
```
`engine.block.isScopeEnabled(...)` returns only the block-level flag. It does not consider the current global scope mode.
### Checking Effective Permissions
Check the effective permission after global and block-level settings resolve:
```kotlin highlight-android-check-allowed
val moveAllowed = engine.block.isAllowedByScope(positionLocked, key = "layer/move")
Log.i("SetEditingConstraints", "layer/move allowed: $moveAllowed")
```
Use `engine.block.isAllowedByScope(...)` when your app needs to know whether an action is actually permitted.
## API Reference
| Method | Description |
| --- | --- |
| `engine.editor.findAllScopes()` | List all available scope keys |
| `engine.editor.setGlobalScope(key=_, globalScope=_)` | Set a scope to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER` |
| `engine.editor.getGlobalScope(key=_)` | Read the global setting for one scope |
| `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Enable or disable a block-level scope |
| `engine.block.isScopeEnabled(block=_, key=_)` | Check whether a scope is enabled at the block level |
| `engine.block.isAllowedByScope(block=_, key=_)` | Check the resolved permission after global and block-level settings are evaluated |
## Troubleshooting
- **A disabled block scope still appears editable**: Check the matching global scope. `GlobalScope.ALLOW` overrides block-level restrictions, so set that scope to `GlobalScope.DEFER` when the block setting should decide the result.
- **All blocks lose the same editing capability**: Check for `GlobalScope.DENY`. A denied global scope disables that action for every block, even when individual blocks have the scope enabled.
- **Constraints seem to reset after reloading**: Scope settings are stored with the scene. If a reloaded scene behaves differently, verify that your app saved the constrained scene after calling `engine.block.setScopeEnabled(...)`.
- **The editor UI shows unavailable controls**: The CE.SDK editor UI reflects denied scopes by disabling or hiding controls depending on the surface. Use `engine.block.isAllowedByScope(...)` to confirm the resolved permission that the UI should follow.
## Next Steps
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Mark editable image, video, or text areas within a locked template layout.
- [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Restrict editing access to specific elements or properties in a template.
- [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build reusable design templates programmatically using CE.SDK APIs.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Text Variables"
description: "Define dynamic text elements that can be populated with custom values during design generation."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/) > [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/)
---
```kotlin file=@cesdk_android_examples/engine-guides-text-variables/TextVariables.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.MimeType
import ly.img.engine.SizeMode
suspend fun textVariables(engine: Engine): TextVariablesResult {
val sampleKeys = listOf("firstName", "lastName")
sampleKeys.forEach { key ->
if (engine.variable.findAll().contains(key)) {
engine.variable.remove(key)
}
}
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 400F)
engine.block.appendChild(parent = scene, child = page)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(
block = textBlock,
text = "Certificate for {{firstName}} {{lastName}}",
)
engine.block.setPositionX(textBlock, value = 120F)
engine.block.setPositionY(textBlock, value = 164F)
engine.block.setWidth(textBlock, value = 560F)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
engine.block.setTextFontSize(textBlock, fontSize = 42F)
engine.block.setTextColor(textBlock, color = Color.fromHex("#FF14171F"))
engine.block.appendChild(parent = page, child = textBlock)
val recipientData = mapOf(
"firstName" to "Alex",
"lastName" to "Smith",
)
recipientData.forEach { (key, value) ->
engine.variable.set(key = key, value = value)
}
val variableNames = engine.variable.findAll().sorted()
val firstName = engine.variable.get(key = "firstName")
val hasVariableReferences = engine.block.referencesAnyVariables(textBlock)
val tokenRegex = Regex("""\{\{\s*([^{}]+?)\s*\}\}""")
val tokenKeys = engine.block.findByType(DesignBlockType.Text)
.flatMap { block ->
tokenRegex.findAll(engine.block.getString(block, property = "text/text"))
.map { match -> match.groupValues[1].trim() }
.toList()
}
.distinct()
val pagePngData = engine.block.export(page, mimeType = MimeType.PNG)
engine.variable.remove(key = "lastName")
val remainingVariableNames = engine.variable.findAll().sorted()
val result = TextVariablesResult(
variablesAfterUpdate = variableNames,
firstName = firstName,
hasVariableReferences = hasVariableReferences,
tokenKeys = tokenKeys,
variablesAfterRemoval = remainingVariableNames,
templateText = engine.block.getString(textBlock, property = "text/text"),
templateTextBlockIsAttached = engine.block.getParent(textBlock) == page,
pagePngData = pagePngData,
)
engine.variable.remove(key = "firstName")
return result
}
```
```kotlin file=@cesdk_android_examples/engine-guides-text-variables/TextVariablesResult.kt reference-only
import java.nio.ByteBuffer
data class TextVariablesResult(
val variablesAfterUpdate: List,
val firstName: String,
val hasVariableReferences: Boolean,
val tokenKeys: List,
val variablesAfterRemoval: List,
val templateText: String,
val templateTextBlockIsAttached: Boolean,
val pagePngData: ByteBuffer,
)
```
Create reusable Android templates whose text content is populated from data at runtime.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-variables)
Text variables separate a design's text layout from the values your app supplies. Put tokens such as `{{firstName}}` into text blocks, then update the variable store with matching keys before preview or export.
This guide focuses on the Android CreativeEngine APIs for creating tokenized text, setting values, reading them back, and validating which blocks still reference variables.
## Variables and Tokens
Tokens and variables are related, but they are not the same thing. A token is the `{{key}}` placeholder written into a text block, while a variable is the key-value entry stored on the engine.
Tokens do not automatically create variable entries. Use `engine.variable.set()` to seed the store before you expect a token to resolve.
## Binding Tokens to Text Blocks
Use the same text editing APIs you already use for normal text blocks. The token syntax can appear on its own or inside a longer string. Add the text block to the page that belongs to your template scene so it becomes visible and exportable content.
```kotlin highlight-android-bind-tokens
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(
block = textBlock,
text = "Certificate for {{firstName}} {{lastName}}",
)
engine.block.setPositionX(textBlock, value = 120F)
engine.block.setPositionY(textBlock, value = 164F)
engine.block.setWidth(textBlock, value = 560F)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
engine.block.setTextFontSize(textBlock, fontSize = 42F)
engine.block.setTextColor(textBlock, color = Color.fromHex("#FF14171F"))
engine.block.appendChild(parent = page, child = textBlock)
```
The stored text remains the template string. When the engine renders the block, matching variable values replace the token positions.
## Setting Variable Values
Populate the variable store with `engine.variable.set()`. Calling `set()` for an existing key updates the value, so the same template can be reused for multiple data records.
```kotlin highlight-android-set-values
val recipientData = mapOf(
"firstName" to "Alex",
"lastName" to "Smith",
)
recipientData.forEach { (key, value) ->
engine.variable.set(key = key, value = value)
}
```
Variable keys are case-sensitive. Keep the key names in your data model and text tokens identical.
## Discovering Variables
Use `engine.variable.findAll()` to list the keys currently stored on the engine. This reports variables that have been set, not every token that appears in text.
```kotlin highlight-android-discover-variables
val variableNames = engine.variable.findAll().sorted()
```
Use this for values you set in the current engine session. If you load a saved scene and need variables stored with that scene, pass `overrideEditorConfig = true` to `engine.scene.load(...)`; the default load options do not restore persisted editor configuration variables.
## Reading Variable Values
Read an existing value with `engine.variable.get()`. Call it after you know the key exists, for example from your own data model or from `findAll()`.
```kotlin highlight-android-read-variable
val firstName = engine.variable.get(key = "firstName")
```
If a key is missing, the engine reports an error instead of returning an empty string.
## Detecting Variable References
Use `engine.block.referencesAnyVariables()` to check whether a text block contains any `{{...}}` tokens.
```kotlin highlight-android-detect-references
val hasVariableReferences = engine.block.referencesAnyVariables(textBlock)
```
The check applies to the block you pass in. If you need to validate a whole scene, iterate over text blocks and check each one.
## Scanning Token Names
Because `findAll()` lists stored variables only, scan text block content when you need to discover token names that have not been seeded yet.
```kotlin highlight-android-scan-tokens
val tokenRegex = Regex("""\{\{\s*([^{}]+?)\s*\}\}""")
val tokenKeys = engine.block.findByType(DesignBlockType.Text)
.flatMap { block ->
tokenRegex.findAll(engine.block.getString(block, property = "text/text"))
.map { match -> match.groupValues[1].trim() }
.toList()
}
.distinct()
```
The example reads the `text/text` property from each text block and extracts the tokens used in this guide, `firstName` and `lastName`. The regex scans the text between `{{` and `}}`, so it also handles names such as `user.name`, `campaign-id`, or `full_name`.
## Removing Variables
Remove a variable with `engine.variable.remove()` when it is no longer part of the current scene or data record.
```kotlin highlight-android-remove-variable
engine.variable.remove(key = "lastName")
val remainingVariableNames = engine.variable.findAll().sorted()
```
Removing a variable does not remove tokens from text blocks. If a block still contains `{{lastName}}`, that token remains in the template text until you change the text content or set the variable again.
## API Reference
| Method | Purpose |
|--------|---------|
| `engine.block.create(blockType=DesignBlockType.Text)` | Create a text block for tokenized copy |
| `engine.block.replaceText(block=_, text=_)` | Replace text content, including `{{key}}` tokens |
| `engine.block.setPositionX(block=_, value=_)` | Set the text block's horizontal position on the template page |
| `engine.block.setPositionY(block=_, value=_)` | Set the text block's vertical position on the template page |
| `engine.block.setWidth(block=_, value=_)` | Set the text block width inside the template page |
| `engine.block.setHeightMode(block=_, mode=_)` | Let the text block height adapt to the inserted copy |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the text block font size |
| `engine.block.setTextColor(block=_, color=_)` | Set the text block color |
| `engine.block.appendChild(parent=_, child=_)` | Attach the tokenized text block to the template page |
| `engine.variable.set(key=_, value=_)` | Create or update a text variable value |
| `engine.variable.findAll()` | List variable keys currently stored on the engine |
| `engine.variable.get(key=_)` | Read an existing variable value |
| `engine.block.referencesAnyVariables(block=_)` | Check whether one block contains variable tokens |
| `engine.block.findByType(type=DesignBlockType.Text)` | Find text blocks for scene-level validation |
| `engine.block.getString(block=_, property="text/text")` | Read a text block's stored template string |
| `engine.variable.remove(key=_)` | Remove an existing variable value |
## Troubleshooting
**A token appears in the output**: Confirm the token name exactly matches a key passed to `engine.variable.set()`, including case.
**`findAll()` returns fewer names than expected**: `findAll()` lists stored variables only. It does not scan text blocks for tokens.
**`get()` fails for a variable**: Check that the key exists before reading it. You can seed optional variables with an empty string when you want unresolved text to disappear.
**`referencesAnyVariables()` returns `false`**: Pass the actual text block. The method does not inspect children of a parent block.
## Next Steps
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Mark editable media or text areas inside locked template layouts.
- [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) — Merge external records into templates with variables and named placeholder blocks.
- [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) — Understand reusable scene templates with dynamic text and placeholder media.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add to Template Library"
description: "Save and organize templates in an Android asset source so your app can query, manage, and apply them."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/add-to-template-library-8bfbc7/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Add to Template Library](https://img.ly/docs/cesdk/android/create-templates/add-to-template-library-8bfbc7/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-add-to-template-library/AddToTemplateLibrary.kt reference-only
import android.net.Uri
import ly.img.engine.AssetDefinition
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FindAssetsQuery
import ly.img.engine.assetBaseUri
@Suppress("DEPRECATION")
suspend fun addToTemplateLibrary(
engine: Engine,
assetBaseUri: Uri = Engine.assetBaseUri,
) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
val headline = engine.block.create(DesignBlockType.Text)
engine.block.setString(headline, property = "text/text", value = "Seasonal Sale")
engine.block.setPositionX(headline, value = 120F)
engine.block.setPositionY(headline, value = 180F)
engine.block.setWidth(headline, value = 840F)
engine.block.setHeight(headline, value = 140F)
engine.block.appendChild(parent = page, child = headline)
val currentScene = engine.scene.get() ?: error("Create or load a scene before saving it.")
val templateString = engine.scene.saveToString(scene = currentScene)
check(templateString.isNotBlank())
val templateArchive = engine.scene.saveToArchive(scene = currentScene)
val savedArchiveBytes = templateArchive.remaining()
check(savedArchiveBytes > 0)
val templateSourceId = "my-templates"
if (templateSourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = templateSourceId)
}
engine.asset.addLocalSource(
sourceId = templateSourceId,
supportedMimeTypes = emptyList(),
applyAsset = { asset ->
val templateUri = asset.meta?.get("uri")?.let(Uri::parse)
?: error("Template asset ${asset.id} is missing meta.uri")
engine.scene.applyTemplate(templateUri = templateUri)
null
},
)
val postcardTemplateUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("templates")
.appendPath("cesdk_postcard_1.scene")
.build()
val postcardThumbnailUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("thumbnails")
.appendPath("cesdk_postcard_1.jpg")
.build()
val businessCardTemplateUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("templates")
.appendPath("cesdk_business_card_1.scene")
.build()
val businessCardThumbnailUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("thumbnails")
.appendPath("cesdk_business_card_1.jpg")
.build()
val templates = listOf(
AssetDefinition(
id = "template-postcard",
label = mapOf("en" to "Postcard"),
meta = mapOf(
"uri" to postcardTemplateUri.toString(),
"thumbUri" to postcardThumbnailUri.toString(),
),
),
AssetDefinition(
id = "template-business-card",
label = mapOf("en" to "Business Card"),
meta = mapOf(
"uri" to businessCardTemplateUri.toString(),
"thumbUri" to businessCardThumbnailUri.toString(),
),
),
)
templates.forEach { template ->
engine.asset.addAsset(sourceId = templateSourceId, asset = template)
}
engine.asset.assetSourceContentsChanged(sourceId = templateSourceId)
val postcardAsset = engine.asset.fetchAsset(
sourceId = templateSourceId,
assetId = "template-postcard",
) ?: error("Template asset not found.")
val createdBlock = engine.asset.applyAssetSourceAsset(
sourceId = templateSourceId,
asset = postcardAsset,
)
check(createdBlock == null)
check(engine.scene.get() != null)
check(engine.block.findByType(DesignBlockType.Page).isNotEmpty())
val jsonSourceId = "my-json-templates"
if (jsonSourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = jsonSourceId)
}
val loadedJsonSourceId = engine.asset.addLocalSourceFromJSON(
contentJSON = """
{
"version": "2.0.0",
"id": "$jsonSourceId",
"assets": [
{
"id": "template-flyer",
"label": { "en": "Flyer" },
"meta": {
"uri": "$postcardTemplateUri",
"thumbUri": "$postcardThumbnailUri"
}
}
]
}
""".trimIndent(),
basePath = null,
matcher = null,
)
check(loadedJsonSourceId == jsonSourceId)
val sources = engine.asset.findAllSources()
check(templateSourceId in sources)
val queryResult = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
)
engine.asset.removeAsset(sourceId = templateSourceId, assetId = "template-business-card")
engine.asset.assetSourceContentsChanged(sourceId = templateSourceId)
val remainingTemplateIds = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
).assets.map { asset -> asset.id }
check(queryResult.total == templates.size)
check(remainingTemplateIds == listOf("template-postcard"))
}
```
Create a template library that stores reusable CE.SDK templates in a local Android asset source.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-templates-add-to-template-library)
Templates in CE.SDK are stored and accessed through the asset system. A template library is a local asset source that holds template definitions, including the template URI, thumbnail URI, and localized label.
This guide covers how to save scenes as templates, create a template asset source, add template metadata, load a JSON catalog, and manage the registered templates.
## Saving Templates
Save the scene you want to reuse before you register it as a template. CE.SDK supports lightweight string templates and self-contained archive templates.
### String Format
Use `engine.scene.saveToString()` when the template can keep referencing external resources by URI. This is the most compact format for templates whose assets are already hosted by your app or CDN.
```kotlin highlight-android-save-string
val currentScene = engine.scene.get() ?: error("Create or load a scene before saving it.")
val templateString = engine.scene.saveToString(scene = currentScene)
```
### Archive Format
Use `engine.scene.saveToArchive()` when the template should include its referenced assets. Persist the returned archive bytes in your app storage or upload them to your backend. The template source below applies `.scene` URIs; archive exports need a separate archive-loading callback instead of this `applyTemplate(templateUri=_)` path.
```kotlin highlight-android-save-archive
val templateArchive = engine.scene.saveToArchive(scene = currentScene)
```
## Creating a Template Asset Source
Register a local asset source with `engine.asset.addLocalSource()`. The `applyAsset` callback reads the selected asset's `meta.uri` value and applies that template to the current scene.
```kotlin highlight-android-create-source
val templateSourceId = "my-templates"
if (templateSourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = templateSourceId)
}
engine.asset.addLocalSource(
sourceId = templateSourceId,
supportedMimeTypes = emptyList(),
applyAsset = { asset ->
val templateUri = asset.meta?.get("uri")?.let(Uri::parse)
?: error("Template asset ${asset.id} is missing meta.uri")
engine.scene.applyTemplate(templateUri = templateUri)
null
},
)
```
The Android scene API exposes template loading as `engine.scene.applyTemplate(templateUri=_)` for URI-based templates and `engine.scene.applyTemplate(template=_)` for serialized strings. Because applying a template updates the active scene instead of creating a block, the callback returns `null`.
## Adding Templates to the Source
Add template definitions with `engine.asset.addAsset()`. Each template asset needs a stable `id`, a localized `label`, and `meta` values that point to the `.scene` template file and thumbnail. Build those URIs from the asset base URI used by your Android integration so the sample follows the installed SDK version.
```kotlin highlight-android-add-templates
val postcardTemplateUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("templates")
.appendPath("cesdk_postcard_1.scene")
.build()
val postcardThumbnailUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("thumbnails")
.appendPath("cesdk_postcard_1.jpg")
.build()
val businessCardTemplateUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("templates")
.appendPath("cesdk_business_card_1.scene")
.build()
val businessCardThumbnailUri = assetBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("thumbnails")
.appendPath("cesdk_business_card_1.jpg")
.build()
val templates = listOf(
AssetDefinition(
id = "template-postcard",
label = mapOf("en" to "Postcard"),
meta = mapOf(
"uri" to postcardTemplateUri.toString(),
"thumbUri" to postcardThumbnailUri.toString(),
),
),
AssetDefinition(
id = "template-business-card",
label = mapOf("en" to "Business Card"),
meta = mapOf(
"uri" to businessCardTemplateUri.toString(),
"thumbUri" to businessCardThumbnailUri.toString(),
),
),
)
templates.forEach { template ->
engine.asset.addAsset(sourceId = templateSourceId, asset = template)
}
engine.asset.assetSourceContentsChanged(sourceId = templateSourceId)
```
Each template asset uses:
- `id` - Unique identifier for the template.
- `label` - Localized display name.
- `meta.uri` - URI of the `.scene` template loaded by this source's `applyAsset` callback.
- `meta.thumbUri` - URI of the preview image shown by clients that render the source.
## Applying a Template
Fetch a template asset from the source and pass it to `engine.asset.applyAssetSourceAsset()`. This invokes the source's `applyAsset` callback and applies the template to the active scene.
```kotlin highlight-android-apply-template
val postcardAsset = engine.asset.fetchAsset(
sourceId = templateSourceId,
assetId = "template-postcard",
) ?: error("Template asset not found.")
val createdBlock = engine.asset.applyAssetSourceAsset(
sourceId = templateSourceId,
asset = postcardAsset,
)
```
For template sources, a `null` return value is expected because no new design block is created.
## Loading Templates from JSON
Use `engine.asset.addLocalSourceFromJSON()` when you keep template metadata in a JSON catalog. This creates a local source from the JSON definition; use `addLocalSource()` plus `addAsset()` when the source needs a custom `applyAsset` callback.
```kotlin highlight-android-json-source
val jsonSourceId = "my-json-templates"
if (jsonSourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = jsonSourceId)
}
val loadedJsonSourceId = engine.asset.addLocalSourceFromJSON(
contentJSON = """
{
"version": "2.0.0",
"id": "$jsonSourceId",
"assets": [
{
"id": "template-flyer",
"label": { "en": "Flyer" },
"meta": {
"uri": "$postcardTemplateUri",
"thumbUri": "$postcardThumbnailUri"
}
}
]
}
""".trimIndent(),
basePath = null,
matcher = null,
)
```
The JSON format contains a `version`, an `id` for the asset source, and an `assets` array with the same fields you would pass through `AssetDefinition`.
## Managing Templates
After registration, query, remove, and refresh templates through the asset API.
```kotlin highlight-android-manage-templates
val sources = engine.asset.findAllSources()
check(templateSourceId in sources)
val queryResult = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
)
engine.asset.removeAsset(sourceId = templateSourceId, assetId = "template-business-card")
engine.asset.assetSourceContentsChanged(sourceId = templateSourceId)
val remainingTemplateIds = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
).assets.map { asset -> asset.id }
```
Use `engine.asset.findAllSources()` to confirm that the source exists. Query templates with `engine.asset.findAssets()`, remove stale entries with `engine.asset.removeAsset()`, and call `engine.asset.assetSourceContentsChanged()` after mutating the source so listeners can refresh their view of the catalog.
## Troubleshooting
| Issue | Cause | Solution |
| --- | --- | --- |
| Query returns no templates | The source ID is wrong or assets were not added before querying | Confirm the ID with `findAllSources()` and call `addAsset()` before `findAssets()` |
| Template fails to apply | The asset is missing a valid `.scene` URI in `meta.uri` | Store a reachable `.scene` URI for this `applyTemplate(templateUri=_)` callback |
| Apply callback is not triggered | The source was loaded from JSON without a custom callback | Use `addLocalSource()` for sources that need custom template application behavior |
| Source updates are not visible to listeners | The source changed without a refresh notification | Call `assetSourceContentsChanged(sourceId=_)` after adding or removing assets |
## API Reference
| Method | Description |
| --- | --- |
| `engine.scene.get()` | Return the active scene before saving it |
| `engine.scene.saveToString(scene=_)` | Serialize a scene to a lightweight string template |
| `engine.scene.saveToArchive(scene=_)` | Save a scene with its accessible assets as an archive for storage or upload |
| `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_, applyAsset=_)` | Register a local template source with custom selection behavior |
| `engine.scene.applyTemplate(templateUri=_)` | Apply a template from a URI to the current scene |
| `engine.scene.applyTemplate(template=_)` | Apply a template from a serialized scene string |
| `engine.asset.addAsset(sourceId=_, asset=_)` | Add a template definition to a local source |
| `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Fetch one template asset from a source |
| `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Apply one asset through the source's selection callback |
| `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_, matcher=_)` | Create a local source from a JSON asset catalog |
| `engine.asset.findAllSources()` | List registered asset source IDs |
| `engine.asset.findAssets(sourceId=_, query=_)` | Query templates from a source |
| `engine.asset.removeAsset(sourceId=_, assetId=_)` | Remove one template definition from a source |
| `engine.asset.removeSource(sourceId=_)` | Remove a local source before recreating it |
| `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify listeners that source contents changed |
## Next Steps
- [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build templates manually in the editor.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Add dynamic text content to templates.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Create editable image and video areas.
- [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-panel/customize-c9a4de/) - Configure asset library appearance.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Edit or Remove Templates"
description: "Modify existing templates and manage template lifecycle by loading, editing, saving, and removing templates from asset sources."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/edit-or-remove-38a8be/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Edit or Remove Templates](https://img.ly/docs/cesdk/android/create-templates/edit-or-remove-38a8be/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-edit-or-remove/EditOrRemoveTemplates.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.AssetDefinition
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.FindAssetsQuery
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.io.File
import java.nio.ByteBuffer
suspend fun editOrRemoveTemplates(engine: Engine): TemplateManagementSummary {
val templateSourceId = "android-guide-templates"
val templateContentKey = "template/content"
val storedThumbnailFiles = mutableListOf()
if (templateSourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = templateSourceId)
}
suspend fun writeTemplateThumbnailFile(
prefix: String,
pngData: ByteBuffer,
): File {
val thumbnailFile = withContext(Dispatchers.IO) {
File.createTempFile(prefix, ".png").apply {
outputStream().use { output ->
val thumbnailBuffer = pngData.asReadOnlyBuffer()
while (thumbnailBuffer.hasRemaining()) {
output.channel.write(thumbnailBuffer)
}
}
}
}
return thumbnailFile
}
try {
val templateMimeType = MimeType.BINARY.key
engine.asset.addLocalSource(
sourceId = templateSourceId,
supportedMimeTypes = listOf(templateMimeType),
applyAsset = { asset ->
val templateContent = requireNotNull(asset.meta?.get(templateContentKey))
engine.scene.applyTemplate(template = templateContent)
null
},
)
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1200F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val backgroundBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(backgroundBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(backgroundBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(backgroundBlock, color = Color.fromHex("#FFF4F0EA"))
engine.block.setWidth(backgroundBlock, value = 1200F)
engine.block.setHeight(backgroundBlock, value = 600F)
engine.block.appendChild(parent = page, child = backgroundBlock)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(imageBlock, name = "template-image")
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = 360F)
engine.block.setHeight(imageBlock, value = 360F)
engine.block.setPositionX(imageBlock, value = 744F)
engine.block.setPositionY(imageBlock, value = 120F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER)
engine.block.setPlaceholderEnabled(block = imageBlock, enabled = true)
engine.block.appendChild(parent = page, child = imageBlock)
val titleBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(titleBlock, name = "template-title")
engine.block.replaceText(titleBlock, text = "Original Template")
engine.block.setTextFontSize(titleBlock, fontSize = 18F)
engine.block.setTextColor(titleBlock, color = Color.fromHex("#FF23201D"))
engine.block.setWidth(titleBlock, value = 960F)
engine.block.setWidthMode(titleBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(titleBlock, mode = SizeMode.AUTO)
engine.block.setBoolean(titleBlock, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(titleBlock, value = 96F)
engine.block.setPositionY(titleBlock, value = 176F)
engine.block.appendChild(parent = page, child = titleBlock)
val subtitleBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(subtitleBlock, name = "template-subtitle")
engine.block.replaceText(subtitleBlock, text = "Stored in a local template source")
engine.block.setTextFontSize(subtitleBlock, fontSize = 9F)
engine.block.setTextColor(subtitleBlock, color = Color.fromHex("#FF5F5953"))
engine.block.setWidth(subtitleBlock, value = 960F)
engine.block.setWidthMode(subtitleBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(subtitleBlock, mode = SizeMode.AUTO)
engine.block.setBoolean(subtitleBlock, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(subtitleBlock, value = 96F)
engine.block.setPositionY(subtitleBlock, value = 270F)
engine.block.appendChild(parent = page, child = subtitleBlock)
val originalTemplate = engine.scene.saveToString(scene = scene)
val originalTemplateThumbnail = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = 1200F, targetHeight = 600F),
)
val originalTemplateThumbnailFile = writeTemplateThumbnailFile(
prefix = "original-template-thumbnail",
pngData = originalTemplateThumbnail,
)
val originalAsset = AssetDefinition(
id = "template-original",
label = mapOf("en" to "Original Template"),
tags = mapOf("en" to listOf("template", "brand")),
groups = listOf("brand"),
meta = mapOf(
templateContentKey to originalTemplate,
"thumbUri" to originalTemplateThumbnailFile.toURI().toString(),
"mimeType" to templateMimeType,
),
)
engine.asset.addAsset(sourceId = templateSourceId, asset = originalAsset)
storedThumbnailFiles += originalTemplateThumbnailFile
val originalResults = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(perPage = 20, page = 0, locale = "en"),
)
check(originalResults.assets.any { it.id == "template-original" })
val storedTemplate = requireNotNull(
engine.asset.fetchAsset(
sourceId = templateSourceId,
assetId = "template-original",
),
)
val editableTemplate = requireNotNull(storedTemplate.meta?.get(templateContentKey))
engine.scene.load(scene = editableTemplate, waitForResources = true)
val editableTitle = engine.block.findByName("template-title").first()
val editableSubtitle = engine.block.findByName("template-subtitle").first()
engine.block.replaceText(editableTitle, text = "Updated Template")
engine.block.replaceText(editableSubtitle, text = "Campaign: {{campaign}}")
val editableImage = engine.block.findByName("template-image").first()
val editableImageFill = engine.block.getFill(editableImage)
engine.block.setUri(
block = editableImageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg"),
)
engine.block.setPlaceholderEnabled(block = editableImage, enabled = false)
engine.block.setPositionX(block = editableImage, value = 720F)
val editableBackground = engine.block.getChildren(engine.scene.getPages().first()).first()
engine.block.setFillSolidColor(editableBackground, color = Color.fromHex("#FFEAF4F8"))
engine.variable.set(key = "campaign", value = "Spring Launch")
val requiredBlocks = listOf("template-title", "template-subtitle", "template-image")
val templateHasRequiredBlocks = requiredBlocks.all { name ->
engine.block.findByName(name).isNotEmpty()
}
val templateHasCampaignVariable = "campaign" in engine.variable.findAll()
val templateHasCampaignText = engine.block
.getString(block = editableSubtitle, property = "text/text")
.contains("{{campaign}}")
check(templateHasRequiredBlocks)
check(templateHasCampaignVariable)
check(templateHasCampaignText)
check(!engine.block.isPlaceholderEnabled(editableImage))
val updatedScene = requireNotNull(engine.scene.get())
val updatedTemplate = engine.scene.saveToString(scene = updatedScene)
val updatedTemplateArchive = engine.scene.saveToArchive(scene = updatedScene)
// Archive APIs load from a URI, so write the buffer to app-owned storage first.
val updatedTemplateArchiveFile = File.createTempFile("updated-template", ".zip")
withContext(Dispatchers.IO) {
updatedTemplateArchiveFile.outputStream().use { output ->
val archiveBuffer = updatedTemplateArchive.asReadOnlyBuffer()
while (archiveBuffer.hasRemaining()) {
output.channel.write(archiveBuffer)
}
}
}
val archivedTemplateScene = engine.scene.loadArchive(
archiveUri = Uri.fromFile(updatedTemplateArchiveFile),
waitForResources = true,
)
val archiveLoaded = archivedTemplateScene == engine.scene.get()
runCatching { updatedTemplateArchiveFile.delete() }
val updatedTemplateThumbnail = engine.block.export(
block = engine.scene.getPages().first(),
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = 1200F, targetHeight = 600F),
)
val updatedTemplateThumbnailFile = writeTemplateThumbnailFile(
prefix = "updated-template-thumbnail",
pngData = updatedTemplateThumbnail,
)
engine.asset.addAsset(
sourceId = templateSourceId,
asset = AssetDefinition(
id = "template-updated",
label = mapOf("en" to "Spring Launch Template"),
tags = mapOf("en" to listOf("template", "spring", "updated")),
groups = listOf("brand"),
meta = mapOf(
templateContentKey to updatedTemplate,
"thumbUri" to updatedTemplateThumbnailFile.toURI().toString(),
"mimeType" to templateMimeType,
),
),
)
storedThumbnailFiles += updatedTemplateThumbnailFile
check(updatedTemplate.isNotBlank())
check(updatedTemplateArchive.remaining() > 0)
engine.asset.addAsset(
sourceId = templateSourceId,
asset = originalAsset.copy(
id = "template-temporary",
label = mapOf("en" to "Temporary Template"),
),
)
engine.asset.removeAsset(
sourceId = templateSourceId,
assetId = "template-temporary",
)
val afterRemoval = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(perPage = 20, page = 0, locale = "en"),
)
val temporaryTemplateRemoved = afterRemoval.assets.none { it.id == "template-temporary" }
check(temporaryTemplateRemoved)
engine.scene.load(scene = updatedTemplate, waitForResources = true)
val finalSubtitle = engine.block.findByName("template-subtitle").first()
engine.block.replaceText(finalSubtitle, text = "Updated again with new content")
val refreshedScene = requireNotNull(engine.scene.get())
val refreshedTemplate = engine.scene.saveToString(scene = refreshedScene)
val refreshedPage = engine.scene.getPages().first()
val refreshedTemplatePreview = engine.block.export(
block = refreshedPage,
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = 1200F, targetHeight = 600F),
)
val refreshedTemplateThumbnailFile = writeTemplateThumbnailFile(
prefix = "refreshed-template-thumbnail",
pngData = refreshedTemplatePreview,
)
engine.asset.removeAsset(sourceId = templateSourceId, assetId = "template-updated")
engine.asset.addAsset(
sourceId = templateSourceId,
asset = AssetDefinition(
id = "template-updated",
label = mapOf("en" to "Spring Launch Template"),
tags = mapOf("en" to listOf("template", "spring", "updated")),
groups = listOf("brand"),
meta = mapOf(
templateContentKey to refreshedTemplate,
"thumbUri" to refreshedTemplateThumbnailFile.toURI().toString(),
"mimeType" to templateMimeType,
),
),
)
storedThumbnailFiles += refreshedTemplateThumbnailFile
val finalResults = engine.asset.findAssets(
sourceId = templateSourceId,
query = FindAssetsQuery(perPage = 20, page = 0, locale = "en"),
)
val refreshedAsset = finalResults.assets.first { it.id == "template-updated" }
val appliedTemplateBlock = engine.asset.applyAssetSourceAsset(
sourceId = templateSourceId,
asset = refreshedAsset,
)
val templateAppliedFromSource = appliedTemplateBlock == null &&
engine.block.findByName("template-subtitle").isNotEmpty()
return TemplateManagementSummary(
sourceId = templateSourceId,
originalTemplateCount = originalResults.assets.size,
finalTemplateCount = finalResults.assets.size,
updatedTemplateLabel = refreshedAsset.label.orEmpty(),
temporaryTemplateRemoved = temporaryTemplateRemoved,
templateAppliedFromSource = templateAppliedFromSource,
refreshedTemplateContent = requireNotNull(refreshedAsset.meta?.get(templateContentKey)),
refreshedTemplateContentKey = templateContentKey,
refreshedTemplatePreview = refreshedTemplatePreview,
archiveByteCount = updatedTemplateArchive.remaining(),
archiveLoaded = archiveLoaded,
validatedBeforeSaving = templateHasRequiredBlocks && templateHasCampaignVariable && templateHasCampaignText,
)
} finally {
if (templateSourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = templateSourceId)
}
storedThumbnailFiles.forEach { thumbnailFile ->
runCatching { thumbnailFile.delete() }
}
}
}
```
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-edit-or-remove/TemplateManagementSummary.kt reference-only
import java.nio.ByteBuffer
data class TemplateManagementSummary(
val sourceId: String,
val originalTemplateCount: Int,
val finalTemplateCount: Int,
val updatedTemplateLabel: String,
val temporaryTemplateRemoved: Boolean,
val templateAppliedFromSource: Boolean,
val refreshedTemplateContent: String,
val refreshedTemplateContentKey: String,
val refreshedTemplatePreview: ByteBuffer,
val archiveByteCount: Int,
val archiveLoaded: Boolean,
val validatedBeforeSaving: Boolean,
)
```
Modify existing templates and manage their lifecycle in Android asset sources
with CE.SDK Engine.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-templates-edit-or-remove)
Templates evolve as designs change. You might need to update brand copy, fix content errors, remove outdated entries, or replace one stored version with another.
This guide uses a headless Engine workflow: create a local source, save a template scene into asset metadata, load it back for editing, update block content and template metadata, validate the result, remove stale entries, and re-add updated versions.
## Adding Templates
Use an app-owned storage helper for thumbnails generated from exported page previews. The file URI returned by this helper is what the sample stores in `meta.thumbUri`.
```kotlin highlight-android-write-thumbnail-file
suspend fun writeTemplateThumbnailFile(
prefix: String,
pngData: ByteBuffer,
): File {
val thumbnailFile = withContext(Dispatchers.IO) {
File.createTempFile(prefix, ".png").apply {
outputStream().use { output ->
val thumbnailBuffer = pngData.asReadOnlyBuffer()
while (thumbnailBuffer.hasRemaining()) {
output.channel.write(thumbnailBuffer)
}
}
}
}
return thumbnailFile
}
```
Create a local asset source for the templates your app manages. The sample stores serialized scene strings directly in a `template/content` metadata key, so the apply callback reads that string and passes it to `engine.scene.applyTemplate(template = ...)`.
```kotlin highlight-android-create-source
val templateMimeType = MimeType.BINARY.key
engine.asset.addLocalSource(
sourceId = templateSourceId,
supportedMimeTypes = listOf(templateMimeType),
applyAsset = { asset ->
val templateContent = requireNotNull(asset.meta?.get(templateContentKey))
engine.scene.applyTemplate(template = templateContent)
null
},
)
```
Applying a template updates the active scene instead of creating a new design block, so the callback returns `null`.
Next, create the template scene. Named text blocks make later edits stable because the update code can find them by name instead of relying on block order.
```kotlin highlight-android-create-template
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1200F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val backgroundBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(backgroundBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(backgroundBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(backgroundBlock, color = Color.fromHex("#FFF4F0EA"))
engine.block.setWidth(backgroundBlock, value = 1200F)
engine.block.setHeight(backgroundBlock, value = 600F)
engine.block.appendChild(parent = page, child = backgroundBlock)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(imageBlock, name = "template-image")
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = 360F)
engine.block.setHeight(imageBlock, value = 360F)
engine.block.setPositionX(imageBlock, value = 744F)
engine.block.setPositionY(imageBlock, value = 120F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER)
engine.block.setPlaceholderEnabled(block = imageBlock, enabled = true)
engine.block.appendChild(parent = page, child = imageBlock)
val titleBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(titleBlock, name = "template-title")
engine.block.replaceText(titleBlock, text = "Original Template")
engine.block.setTextFontSize(titleBlock, fontSize = 18F)
engine.block.setTextColor(titleBlock, color = Color.fromHex("#FF23201D"))
engine.block.setWidth(titleBlock, value = 960F)
engine.block.setWidthMode(titleBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(titleBlock, mode = SizeMode.AUTO)
engine.block.setBoolean(titleBlock, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(titleBlock, value = 96F)
engine.block.setPositionY(titleBlock, value = 176F)
engine.block.appendChild(parent = page, child = titleBlock)
val subtitleBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(subtitleBlock, name = "template-subtitle")
engine.block.replaceText(subtitleBlock, text = "Stored in a local template source")
engine.block.setTextFontSize(subtitleBlock, fontSize = 9F)
engine.block.setTextColor(subtitleBlock, color = Color.fromHex("#FF5F5953"))
engine.block.setWidth(subtitleBlock, value = 960F)
engine.block.setWidthMode(subtitleBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(subtitleBlock, mode = SizeMode.AUTO)
engine.block.setBoolean(subtitleBlock, property = "text/clipLinesOutsideOfFrame", value = false)
engine.block.setPositionX(subtitleBlock, value = 96F)
engine.block.setPositionY(subtitleBlock, value = 270F)
engine.block.appendChild(parent = page, child = subtitleBlock)
```
Save the scene with `engine.scene.saveToString`, keep the returned string as template content, export the page preview for `meta.thumbUri`, and add both values to the local source with `engine.asset.addAsset`.
```kotlin highlight-android-add-to-source
val originalTemplate = engine.scene.saveToString(scene = scene)
val originalTemplateThumbnail = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = 1200F, targetHeight = 600F),
)
val originalTemplateThumbnailFile = writeTemplateThumbnailFile(
prefix = "original-template-thumbnail",
pngData = originalTemplateThumbnail,
)
val originalAsset = AssetDefinition(
id = "template-original",
label = mapOf("en" to "Original Template"),
tags = mapOf("en" to listOf("template", "brand")),
groups = listOf("brand"),
meta = mapOf(
templateContentKey to originalTemplate,
"thumbUri" to originalTemplateThumbnailFile.toURI().toString(),
"mimeType" to templateMimeType,
),
)
engine.asset.addAsset(sourceId = templateSourceId, asset = originalAsset)
```
The `template/content` metadata field contains the serialized template string. `meta.thumbUri` points to the thumbnail shown by asset-library UIs that read this source.
## Editing Templates
Fetch the stored asset, read the serialized template from `template/content`, and load it with `engine.scene.load(scene = ...)` before making changes. The snippet then finds the named text blocks, updates the title, and inserts a `{{campaign}}` token into the subtitle.
```kotlin highlight-android-edit-template
val storedTemplate = requireNotNull(
engine.asset.fetchAsset(
sourceId = templateSourceId,
assetId = "template-original",
),
)
val editableTemplate = requireNotNull(storedTemplate.meta?.get(templateContentKey))
engine.scene.load(scene = editableTemplate, waitForResources = true)
val editableTitle = engine.block.findByName("template-title").first()
val editableSubtitle = engine.block.findByName("template-subtitle").first()
engine.block.replaceText(editableTitle, text = "Updated Template")
engine.block.replaceText(editableSubtitle, text = "Campaign: {{campaign}}")
```
Templates can contain the same block types and template behaviors as regular scenes. Update image fills, shape styling, placeholder state, positions, and the variable that resolves the subtitle token with the matching Android Engine APIs before saving.
```kotlin highlight-android-edit-media-and-placeholders
val editableImage = engine.block.findByName("template-image").first()
val editableImageFill = engine.block.getFill(editableImage)
engine.block.setUri(
block = editableImageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg"),
)
engine.block.setPlaceholderEnabled(block = editableImage, enabled = false)
engine.block.setPositionX(block = editableImage, value = 720F)
val editableBackground = engine.block.getChildren(engine.scene.getPages().first()).first()
engine.block.setFillSolidColor(editableBackground, color = Color.fromHex("#FFEAF4F8"))
engine.variable.set(key = "campaign", value = "Spring Launch")
```
Before persisting changes, validate that required blocks and variables still exist and that template-only controls are in the expected state. Keep these checks close to the save step so invalid template edits fail before your app updates the asset source.
```kotlin highlight-android-validate-template
val requiredBlocks = listOf("template-title", "template-subtitle", "template-image")
val templateHasRequiredBlocks = requiredBlocks.all { name ->
engine.block.findByName(name).isNotEmpty()
}
val templateHasCampaignVariable = "campaign" in engine.variable.findAll()
val templateHasCampaignText = engine.block
.getString(block = editableSubtitle, property = "text/text")
.contains("{{campaign}}")
check(templateHasRequiredBlocks)
check(templateHasCampaignVariable)
check(templateHasCampaignText)
check(!engine.block.isPlaceholderEnabled(editableImage))
```
Use `engine.scene.saveToString` for lightweight string storage and `engine.scene.saveToArchive` when the template should be bundled with reachable resources. Archive output is loaded through `engine.scene.loadArchive`.
```kotlin highlight-android-save-options
val updatedScene = requireNotNull(engine.scene.get())
val updatedTemplate = engine.scene.saveToString(scene = updatedScene)
val updatedTemplateArchive = engine.scene.saveToArchive(scene = updatedScene)
// Archive APIs load from a URI, so write the buffer to app-owned storage first.
val updatedTemplateArchiveFile = File.createTempFile("updated-template", ".zip")
withContext(Dispatchers.IO) {
updatedTemplateArchiveFile.outputStream().use { output ->
val archiveBuffer = updatedTemplateArchive.asReadOnlyBuffer()
while (archiveBuffer.hasRemaining()) {
output.channel.write(archiveBuffer)
}
}
}
val archivedTemplateScene = engine.scene.loadArchive(
archiveUri = Uri.fromFile(updatedTemplateArchiveFile),
waitForResources = true,
)
val archiveLoaded = archivedTemplateScene == engine.scene.get()
```
After editing and validation, save the scene again and add it as a new asset entry. This keeps the original template available while introducing the updated version.
Update asset metadata at the same time as the serialized scene: replace the `template/content` string with the newly saved template, change `label` for the displayed name, update `tags` for search and filtering, and set `meta.thumbUri` to the exported thumbnail shown in asset-library UIs.
```kotlin highlight-android-update-metadata
val updatedTemplateThumbnail = engine.block.export(
block = engine.scene.getPages().first(),
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = 1200F, targetHeight = 600F),
)
val updatedTemplateThumbnailFile = writeTemplateThumbnailFile(
prefix = "updated-template-thumbnail",
pngData = updatedTemplateThumbnail,
)
engine.asset.addAsset(
sourceId = templateSourceId,
asset = AssetDefinition(
id = "template-updated",
label = mapOf("en" to "Spring Launch Template"),
tags = mapOf("en" to listOf("template", "spring", "updated")),
groups = listOf("brand"),
meta = mapOf(
templateContentKey to updatedTemplate,
"thumbUri" to updatedTemplateThumbnailFile.toURI().toString(),
"mimeType" to templateMimeType,
),
),
)
```
## Removing Templates
Use `engine.asset.removeAsset` to remove an asset entry from a local source. Removal is permanent for that source, so maintain your own backup or soft-delete state if users need recovery.
```kotlin highlight-android-remove-template
engine.asset.removeAsset(
sourceId = templateSourceId,
assetId = "template-temporary",
)
```
## Saving Updated Templates
Android local asset sources do not replace an existing asset ID in place. To update a template under the same ID, remove the old asset first, then add the refreshed asset definition.
```kotlin highlight-android-update-in-source
engine.scene.load(scene = updatedTemplate, waitForResources = true)
val finalSubtitle = engine.block.findByName("template-subtitle").first()
engine.block.replaceText(finalSubtitle, text = "Updated again with new content")
val refreshedScene = requireNotNull(engine.scene.get())
val refreshedTemplate = engine.scene.saveToString(scene = refreshedScene)
val refreshedPage = engine.scene.getPages().first()
val refreshedTemplatePreview = engine.block.export(
block = refreshedPage,
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = 1200F, targetHeight = 600F),
)
val refreshedTemplateThumbnailFile = writeTemplateThumbnailFile(
prefix = "refreshed-template-thumbnail",
pngData = refreshedTemplatePreview,
)
engine.asset.removeAsset(sourceId = templateSourceId, assetId = "template-updated")
engine.asset.addAsset(
sourceId = templateSourceId,
asset = AssetDefinition(
id = "template-updated",
label = mapOf("en" to "Spring Launch Template"),
tags = mapOf("en" to listOf("template", "spring", "updated")),
groups = listOf("brand"),
meta = mapOf(
templateContentKey to refreshedTemplate,
"thumbUri" to refreshedTemplateThumbnailFile.toURI().toString(),
"mimeType" to templateMimeType,
),
),
)
```
The built-in `engine.asset.removeAsset` and `engine.asset.addAsset` calls notify listeners after successful local-source changes. Call `engine.asset.assetSourceContentsChanged` only when your app mutates a custom or external source outside those built-in add and remove APIs.
## Best Practices
### Versioning Strategies
When managing template updates, choose a predictable versioning strategy:
- **Replace in place**: Remove the existing asset, then add the updated template with the same ID.
- **Version suffixes**: Add new entries such as `template-v2` while keeping older versions available.
- **Archive old versions**: Move deprecated templates to a separate source before removing them from the active library.
### Batch Operations
When adding, updating, or removing many templates through `engine.asset.addAsset` and `engine.asset.removeAsset`, each successful call updates the local source and notifies listeners. If your app changes a custom or external source through its own storage layer, finish the batch first, then call `engine.asset.assetSourceContentsChanged` once for that source.
### Template IDs
Use descriptive, stable IDs that reflect the template purpose. Consistent names make templates easier to fetch, update, and remove programmatically.
### Thumbnails
Store a meaningful `thumbUri` for every template asset. Good thumbnails improve discoverability when the same source is shown in an asset library.
### Storage Considerations
Keep serialized template strings in your app's persistence layer, for example a database row or backend record keyed by the asset ID. Store `.scene` URIs only when your source really points to local, content, or remote scene files.
## Troubleshooting
**Template not loading**: Pass serialized template strings from `template/content` to `engine.scene.load(scene = ..., waitForResources = true)` when editing a stored template. Use `engine.scene.applyTemplate(template = ...)` inside the asset-source apply callback for string templates. Use `engine.scene.load(sceneUri = ...)` or `engine.scene.applyTemplate(templateUri = ...)` only for real local, content, or remote `.scene` URIs, and keep archive files on `engine.scene.loadArchive(archiveUri = ...)`.
**Duplicate asset ID**: `engine.asset.addAsset` fails if the local source already contains that ID. Remove the old asset before adding the updated definition.
**Template source not found**: Confirm the source ID exists in `engine.asset.findAllSources()` before adding, fetching, or removing assets.
**Library not refreshed**: If your UI reads from a custom source that changes outside `addAsset` and `removeAsset`, call `engine.asset.assetSourceContentsChanged(sourceId = ...)`.
## API Reference
| Method | Description |
| --- | --- |
| `engine.asset.findAllSources()` | List registered asset source IDs. |
| `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_, applyAsset=_)` | Create the local source used to manage templates and provide the callback that applies selected template assets. |
| `engine.asset.removeSource(sourceId=_)` | Remove the local template source during cleanup. |
| `engine.scene.create()` | Create the scene that becomes the template. |
| `engine.block.create(blockType=_)` | Create page, graphic, and text blocks for the template. |
| `engine.block.createShape(type=_)` | Create the rectangle shape used by the template background. |
| `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block. |
| `engine.block.createFill(fillType=_)` | Create the fill used by the template background. |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill to a block. |
| `engine.block.getFill(block=_)` | Read an existing fill block before updating its resource URI. |
| `engine.block.setUri(block=_, property=_, value=_)` | Update URI-backed properties such as image fill resources. |
| `engine.block.setFillSolidColor(block=_, color=_)` | Set the background fill color. |
| `engine.block.setContentFillMode(block=_, mode=_)` | Control how image fills fit inside a graphic block. |
| `engine.block.setWidth(block=_, value=_)` | Set fixed page, background, and text frame widths. |
| `engine.block.setHeight(block=_, value=_)` | Set fixed page and background heights. |
| `engine.block.appendChild(parent=_, child=_)` | Add page, graphic, and text blocks to the scene hierarchy. |
| `engine.block.getChildren(block=_)` | Read child blocks from a page before updating an existing block. |
| `engine.block.export(block=_, mimeType=_, options=_)` | Export a page preview that can be stored as the template thumbnail. |
| `engine.block.setName(block=_, name=_)` | Assign stable names to blocks that later update code needs to find. |
| `engine.block.replaceText(block=_, text=_)` | Replace text content in a template block. |
| `engine.block.setPlaceholderEnabled(block=_, enabled=_)` | Enable or disable placeholder behavior for a template block. |
| `engine.block.isPlaceholderEnabled(block=_)` | Validate placeholder state before saving. |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the text size for template text blocks. |
| `engine.block.setTextColor(block=_, color=_, from=_, to=_)` | Set text color; optional `from` and `to` values target UTF-16 code unit offsets in the text. |
| `engine.block.setWidthMode(block=_, mode=_)` | Use fixed text frame widths for predictable exported layout. |
| `engine.block.setHeightMode(block=_, mode=SizeMode.AUTO)` | Let text blocks size to their content vertically. |
| `engine.block.setBoolean(block=_, property=_, value=_)` | Set text frame behavior such as line clipping. |
| `engine.block.setPositionX(block=_, value=_)` | Position a block on the page horizontally. |
| `engine.block.setPositionY(block=_, value=_)` | Position a block on the page vertically. |
| `engine.block.getString(block=_, property=_)` | Validate that the edited text block still contains the expected variable token. |
| `engine.scene.saveToString(scene=_)` | Serialize a scene so it can be stored as template content. |
| `engine.scene.saveToArchive(scene=_)` | Serialize a scene and its reachable resources as a self-contained archive. |
| `engine.scene.loadArchive(archiveUri=_)` | Load a scene archive from a file or content URI. |
| `engine.scene.load(scene=_, waitForResources=_)` | Load serialized template scene content before editing it. |
| `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a stored local, content, or remote `.scene` template before editing it. |
| `engine.asset.addAsset(sourceId=_, asset=_)` | Add a template asset definition to a local source. |
| `engine.asset.findAssets(sourceId=_, query=_)` | Query templates in an asset source. |
| `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Fetch one stored template asset by ID. |
| `engine.scene.applyTemplate(template=_)` | Apply serialized template scene content. |
| `engine.scene.applyTemplate(templateUri=_)` | Apply a template from a local, content, or remote `.scene` URI. |
| `engine.scene.get()` | Read the currently loaded scene before saving updates. |
| `engine.scene.getPages()` | Read the pages in the loaded scene before editing page children or exporting a preview. |
| `engine.block.findByName(name=_)` | Find named blocks in the loaded template. |
| `engine.variable.set(key=_, value=_)` | Update a text variable used by a template. |
| `engine.variable.findAll()` | Validate that required variables exist before saving. |
| `engine.asset.removeAsset(sourceId=_, assetId=_)` | Remove a template asset from a local source. |
| `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify listeners after custom or external source mutations that happen outside `addAsset` and `removeAsset`. |
## Next Steps
- [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build new templates programmatically.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Add dynamic text content to templates.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Configure image, video, or text placeholders.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create From Scratch"
description: "Build reusable design templates programmatically using CE.SDK's APIs. Create scenes, add text and graphic blocks, configure placeholders and variables, apply editing constraints, and save templates for reuse."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-from-scratch/CreateTemplateFromScratch.kt reference-only
import android.net.Uri
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.Font
import ly.img.engine.FontStyle
import ly.img.engine.FontUnit
import ly.img.engine.FontWeight
import ly.img.engine.GlobalScope
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import ly.img.engine.Typeface
suspend fun createTemplateFromScratch(engine: Engine): TemplateFromScratchResult {
val scopeKeys = listOf("layer/move", "layer/resize", "fill/change")
val previousGlobalScopes = scopeKeys.associateWith(engine.editor::getGlobalScope)
val previousVariables = engine.variable.findAll().associateWith { key ->
engine.variable.get(key)
}
return try {
previousVariables.keys.forEach(engine.variable::remove)
val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL)
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 1000F)
engine.block.appendChild(parent = scene, child = page)
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = backgroundFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.98F, g = 0.98F, b = 0.99F, a = 1F),
)
engine.block.setFill(block = page, fill = backgroundFill)
val bundledAssetsBaseUri = "file:///android_asset/imgly-assets"
val brandTypeface = Typeface(
name = "Brand Sans",
fonts = listOf(
Font(
uri = Uri.parse("$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf"),
subFamily = "Regular",
weight = FontWeight.NORMAL,
style = FontStyle.NORMAL,
),
Font(
uri = Uri.parse("$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Bold.ttf"),
subFamily = "Bold",
weight = FontWeight.BOLD,
style = FontStyle.NORMAL,
),
),
)
val brandRegularFont = brandTypeface.fonts.firstOrNull {
it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL
} ?: error("Brand Sans must include a regular font.")
val headline = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(headline, text = "{{title}}")
engine.block.setFont(headline, fontFileUri = brandRegularFont.uri, typeface = brandTypeface)
engine.block.setTextFontSize(headline, fontSize = 72F)
engine.block.setTextColor(headline, color = Color.fromHex("#171717"))
engine.block.setPositionX(headline, value = 72F)
engine.block.setPositionY(headline, value = 96F)
engine.block.setWidth(headline, value = 656F)
engine.block.setHeightMode(headline, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = headline)
val subtitle = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(subtitle, text = "{{subtitle}}")
engine.block.setFont(subtitle, fontFileUri = brandRegularFont.uri, typeface = brandTypeface)
engine.block.setTextFontSize(subtitle, fontSize = 32F)
engine.block.setTextColor(subtitle, color = Color.fromHex("#525252"))
engine.block.setPositionX(subtitle, value = 72F)
engine.block.setPositionY(subtitle, value = 194F)
engine.block.setWidth(subtitle, value = 620F)
engine.block.setHeightMode(subtitle, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = subtitle)
val cta = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(cta, text = "{{cta}}")
engine.block.setFont(cta, fontFileUri = brandRegularFont.uri, typeface = brandTypeface)
engine.block.setTextFontSize(cta, fontSize = 36F)
engine.block.setTextColor(cta, color = Color.fromHex("#171717"))
engine.block.setPositionX(cta, value = 72F)
engine.block.setPositionY(cta, value = 858F)
engine.block.setWidth(cta, value = 400F)
engine.block.setHeightMode(cta, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = cta)
engine.variable.set(key = "title", value = "Summer Sale")
engine.variable.set(key = "subtitle", value = "Up to 50% off all items")
engine.variable.set(key = "cta", value = "Learn More")
val variableNames = engine.variable.findAll()
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(imageBlock, name = "hero-image")
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 72F)
engine.block.setPositionY(imageBlock, value = 300F)
engine.block.setWidth(imageBlock, value = 656F)
engine.block.setHeight(imageBlock, value = 500F)
engine.block.setContentFillMode(imageBlock, mode = ContentFillMode.COVER)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "$bundledAssetsBaseUri/ly.img.image/images/sample_1.jpg",
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.appendChild(parent = page, child = imageBlock)
val placeholderFill = engine.block.getFill(imageBlock)
if (engine.block.supportsPlaceholderBehavior(placeholderFill)) {
engine.block.setPlaceholderBehaviorEnabled(placeholderFill, enabled = true)
}
engine.block.setPlaceholderEnabled(imageBlock, enabled = true)
if (engine.block.supportsPlaceholderControls(imageBlock)) {
engine.block.setPlaceholderControlsOverlayEnabled(imageBlock, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(imageBlock, enabled = true)
}
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
listOf(headline, subtitle, cta, imageBlock).forEach { block ->
engine.block.setScopeEnabled(block = block, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(block = block, key = "layer/resize", enabled = false)
}
engine.block.setScopeEnabled(imageBlock, key = "fill/change", enabled = true)
engine.block.forceLoadResources(blocks = listOf(page))
val templateString = engine.scene.saveToString(scene = scene)
val templateArchive = engine.scene.saveToArchive(scene = scene)
val previewPng = engine.block.export(page, mimeType = MimeType.PNG)
TemplateFromScratchResult(
scene = scene,
page = page,
titleBlock = headline,
imageBlock = imageBlock,
templateString = templateString,
templateArchive = templateArchive,
previewPng = previewPng,
variables = variableNames,
placeholderBehaviorEnabled = engine.block.isPlaceholderBehaviorEnabled(placeholderFill),
placeholderEnabled = engine.block.isPlaceholderEnabled(imageBlock),
overlayEnabled = engine.block.isPlaceholderControlsOverlayEnabled(imageBlock),
buttonEnabled = engine.block.isPlaceholderControlsButtonEnabled(imageBlock),
imageCanMove = engine.block.isAllowedByScope(imageBlock, key = "layer/move"),
imageCanResize = engine.block.isAllowedByScope(imageBlock, key = "layer/resize"),
imageFillCanChange = engine.block.isAllowedByScope(imageBlock, key = "fill/change"),
)
} finally {
previousGlobalScopes.forEach { (key, scope) ->
engine.editor.setGlobalScope(key = key, globalScope = scope)
}
engine.variable.findAll().forEach { key ->
runCatching { engine.variable.remove(key) }
}
previousVariables.forEach(engine.variable::set)
}
}
```
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-from-scratch/TemplateFromScratchResult.kt reference-only
import ly.img.engine.DesignBlock
import java.nio.ByteBuffer
data class TemplateFromScratchResult(
val scene: DesignBlock,
val page: DesignBlock,
val titleBlock: DesignBlock,
val imageBlock: DesignBlock,
val templateString: String,
val templateArchive: ByteBuffer,
val previewPng: ByteBuffer,
val variables: List,
val placeholderBehaviorEnabled: Boolean,
val placeholderEnabled: Boolean,
val overlayEnabled: Boolean,
val buttonEnabled: Boolean,
val imageCanMove: Boolean,
val imageCanResize: Boolean,
val imageFillCanChange: Boolean,
)
```
Build reusable design templates entirely through code for automation, batch
generation, and custom template creation tools.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-templates-from-scratch)
CE.SDK lets you create a template without starting from an existing scene. You can define the page size, add text and graphic blocks, bind text variables, mark media as a placeholder, restrict editing with scopes, and save the result for reuse.
This guide uses a promotional card template with variable-driven text and one swappable image area. The sample assumes an existing `Engine` instance and focuses on the scene and block APIs that create the reusable template.
## Create a Blank Scene
Create the scene with pixel units for layout and text sizing, add a page, and define the template canvas size. On Android, page dimensions are set on the page block after the scene is created.
```kotlin highlight-android-create-scene
val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL)
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 1000F)
engine.block.appendChild(parent = scene, child = page)
```
## Set Page Background
Use a color fill for the page background so every generated template starts from the same base appearance.
```kotlin highlight-android-add-background
val backgroundFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = backgroundFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.98F, g = 0.98F, b = 0.99F, a = 1F),
)
engine.block.setFill(block = page, fill = backgroundFill)
```
The fill stores the color value, and `setFill` assigns that fill to the page.
## Add Text Blocks
Text blocks hold the template copy. The sample uses a bundled font, variable tokens for dynamic content, and fixed positions so the layout remains predictable.
```kotlin highlight-android-add-text
val bundledAssetsBaseUri = "file:///android_asset/imgly-assets"
val brandTypeface = Typeface(
name = "Brand Sans",
fonts = listOf(
Font(
uri = Uri.parse("$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf"),
subFamily = "Regular",
weight = FontWeight.NORMAL,
style = FontStyle.NORMAL,
),
Font(
uri = Uri.parse("$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Bold.ttf"),
subFamily = "Bold",
weight = FontWeight.BOLD,
style = FontStyle.NORMAL,
),
),
)
val brandRegularFont = brandTypeface.fonts.firstOrNull {
it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL
} ?: error("Brand Sans must include a regular font.")
val headline = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(headline, text = "{{title}}")
engine.block.setFont(headline, fontFileUri = brandRegularFont.uri, typeface = brandTypeface)
engine.block.setTextFontSize(headline, fontSize = 72F)
engine.block.setTextColor(headline, color = Color.fromHex("#171717"))
engine.block.setPositionX(headline, value = 72F)
engine.block.setPositionY(headline, value = 96F)
engine.block.setWidth(headline, value = 656F)
engine.block.setHeightMode(headline, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = headline)
val subtitle = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(subtitle, text = "{{subtitle}}")
engine.block.setFont(subtitle, fontFileUri = brandRegularFont.uri, typeface = brandTypeface)
engine.block.setTextFontSize(subtitle, fontSize = 32F)
engine.block.setTextColor(subtitle, color = Color.fromHex("#525252"))
engine.block.setPositionX(subtitle, value = 72F)
engine.block.setPositionY(subtitle, value = 194F)
engine.block.setWidth(subtitle, value = 620F)
engine.block.setHeightMode(subtitle, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = subtitle)
val cta = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(cta, text = "{{cta}}")
engine.block.setFont(cta, fontFileUri = brandRegularFont.uri, typeface = brandTypeface)
engine.block.setTextFontSize(cta, fontSize = 36F)
engine.block.setTextColor(cta, color = Color.fromHex("#171717"))
engine.block.setPositionX(cta, value = 72F)
engine.block.setPositionY(cta, value = 858F)
engine.block.setWidth(cta, value = 400F)
engine.block.setHeightMode(cta, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = cta)
```
Each text block is appended to the page after its content, typography, position, and sizing are configured.
## Add Text Variables
Variables populate the `{{title}}`, `{{subtitle}}`, and `{{cta}}` tokens in the text blocks. Set defaults while authoring the template so the design has meaningful preview content.
```kotlin highlight-android-add-variables
engine.variable.set(key = "title", value = "Summer Sale")
engine.variable.set(key = "subtitle", value = "Up to 50% off all items")
engine.variable.set(key = "cta", value = "Learn More")
val variableNames = engine.variable.findAll()
```
When your app applies the template later, call `engine.variable.set` again with the runtime values.
## Add Graphic Blocks
Graphic blocks are the image containers in the template. Give the image block a name so your app can find it later, then assign a rectangle shape and image fill.
```kotlin highlight-android-add-graphic
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(imageBlock, name = "hero-image")
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 72F)
engine.block.setPositionY(imageBlock, value = 300F)
engine.block.setWidth(imageBlock, value = 656F)
engine.block.setHeight(imageBlock, value = 500F)
engine.block.setContentFillMode(imageBlock, mode = ContentFillMode.COVER)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "$bundledAssetsBaseUri/ly.img.image/images/sample_1.jpg",
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.appendChild(parent = page, child = imageBlock)
```
The sample uses an Android asset URI for the preview image. In your app, replace it with a stable local or remote URI that can be resolved when the template is loaded.
## Configure Placeholders
Placeholder behavior makes the image fill act as swappable content, while placeholder controls make the containing block interactive in editor UI workflows.
```kotlin highlight-android-configure-placeholder
val placeholderFill = engine.block.getFill(imageBlock)
if (engine.block.supportsPlaceholderBehavior(placeholderFill)) {
engine.block.setPlaceholderBehaviorEnabled(placeholderFill, enabled = true)
}
engine.block.setPlaceholderEnabled(imageBlock, enabled = true)
if (engine.block.supportsPlaceholderControls(imageBlock)) {
engine.block.setPlaceholderControlsOverlayEnabled(imageBlock, enabled = true)
engine.block.setPlaceholderControlsButtonEnabled(imageBlock, enabled = true)
}
```
Enable placeholder behavior on the fill, then enable placeholder interaction and controls on the graphic block.
## Apply Editing Constraints
Scopes protect the layout while keeping the image content replaceable. Defer the global scopes to block-level settings, then lock movement and resizing on the template elements.
```kotlin highlight-android-apply-constraints
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
listOf(headline, subtitle, cta, imageBlock).forEach { block ->
engine.block.setScopeEnabled(block = block, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(block = block, key = "layer/resize", enabled = false)
}
engine.block.setScopeEnabled(imageBlock, key = "fill/change", enabled = true)
```
The image block keeps `fill/change` enabled so users or automation can replace the image without moving the placeholder.
## Save the Template
Save the finished scene as a string when assets stay externally resolvable, or as an archive when you need a portable package with accessible assets included.
```kotlin highlight-android-save-template
engine.block.forceLoadResources(blocks = listOf(page))
val templateString = engine.scene.saveToString(scene = scene)
val templateArchive = engine.scene.saveToArchive(scene = scene)
```
`saveToString` is compact and works well for templates stored alongside stable asset URLs. `saveToArchive` creates a ZIP archive that bundles assets the engine can access at save time.
## Troubleshooting
**Blocks do not appear**: Verify that every page, text block, and graphic block is attached with `engine.block.appendChild`.
**Variables do not resolve**: Check that each text token uses the same key as `engine.variable.set`, including the double curly braces in the text.
**The placeholder is not interactive**: Enable placeholder behavior on the fill and placeholder interaction on the graphic block.
**Constraints are not enforced**: Set the relevant global scope to `GlobalScope.DEFER` before applying block-level scope settings.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.create(designUnit=DesignUnit.PIXEL, fontSizeUnit=FontUnit.PIXEL)` | Create the empty template scene with pixel measurements and pixel text sizing. |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create the template page. |
| `engine.block.create(blockType=DesignBlockType.Text)` | Create text blocks for variable-driven copy. |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create the image placeholder block. |
| `engine.block.setWidth(block=_, value=_)` | Set page or block width. |
| `engine.block.setHeight(block=_, value=_)` | Set page or block height. |
| `engine.block.setPositionX(block=_, value=_)` | Set a block's horizontal position. |
| `engine.block.setPositionY(block=_, value=_)` | Set a block's vertical position. |
| `engine.block.setHeightMode(block=_, mode=SizeMode.AUTO)` | Let text block height fit its content. |
| `engine.block.appendChild(parent=_, child=_)` | Add a page or block to the scene hierarchy. |
| `engine.block.createFill(fillType=FillType.Color)` | Create the page background fill. |
| `engine.block.createFill(fillType=FillType.Image)` | Create the image fill used by the placeholder. |
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set the background fill color. |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill to a page or graphic block. |
| `engine.block.replaceText(block=_, text=_)` | Set text block content. |
| `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Apply the selected font to a text block. |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text size. |
| `engine.block.setTextColor(block=_, color=_)` | Set text color. |
| `engine.variable.set(key=_, value=_)` | Set a text variable value. |
| `engine.variable.findAll()` | Read the variables registered in the scene. |
| `engine.block.setName(block=_, name=_)` | Name a block for later lookup. |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular shape for the graphic block. |
| `engine.block.setShape(block=_, shape=_)` | Assign the shape to the graphic block. |
| `engine.block.setContentFillMode(block=_, mode=ContentFillMode.COVER)` | Crop the image to cover its placeholder bounds. |
| `engine.block.setString(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on the image fill. |
| `engine.block.getFill(block=_)` | Read the fill block attached to the graphic block. |
| `engine.block.supportsPlaceholderBehavior(block=_)` | Check whether the fill supports placeholder behavior. |
| `engine.block.setPlaceholderBehaviorEnabled(block=_, enabled=_)` | Enable placeholder behavior on the fill. |
| `engine.block.setPlaceholderEnabled(block=_, enabled=_)` | Enable placeholder interaction on the graphic block. |
| `engine.block.supportsPlaceholderControls(block=_)` | Check whether the graphic block supports placeholder controls. |
| `engine.block.setPlaceholderControlsOverlayEnabled(block=_, enabled=_)` | Show the placeholder overlay. |
| `engine.block.setPlaceholderControlsButtonEnabled(block=_, enabled=_)` | Show the placeholder action button. |
| `engine.editor.setGlobalScope(key=_, globalScope=GlobalScope.DEFER)` | Defer a scope to block-level settings. |
| `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Allow or deny a scope on one block. |
| `engine.block.forceLoadResources(blocks=_)` | Ensure referenced assets are loaded before saving. |
| `engine.scene.saveToString(scene=_)` | Serialize the template scene as a string. |
| `engine.scene.saveToArchive(scene=_)` | Save the template scene and accessible assets as an archive. |
## Next Steps
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Configure placeholder behavior and visual controls in depth.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Implement dynamic text personalization with variables.
- [Add to Template Library](https://img.ly/docs/cesdk/android/create-templates/add-to-template-library-8bfbc7/) — Save and organize templates in an asset source for users to browse and apply from the template library.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Import Templates"
description: "Load and import design templates into CE.SDK from Android URLs, archives, and serialized scene strings."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/import-e50084/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/)
---
```kotlin file=@cesdk_android_examples/engine-guides-import-templates/ImportTemplates.kt reference-only
import android.net.Uri
import ly.img.editor.defaultBaseUri
import ly.img.engine.Engine
import java.io.File
import kotlin.math.abs
private const val CURRENT_PAGE_WIDTH = 1080F
private const val CURRENT_PAGE_HEIGHT = 1350F
private const val PAGE_SIZE_TOLERANCE = 0.01F
private val templateSceneUri: Uri
get() = defaultBaseUri.buildUpon()
.appendPath("ly.img.templates")
.appendPath("templates")
.appendPath("cesdk_business_card_1.scene")
.build()
data class ImportTemplatesResult(
val loadedPageCount: Int,
val appliedFromUriPageSize: PageSize,
val appliedFromStringPageSize: PageSize,
)
data class PageSize(
val width: Float,
val height: Float,
)
suspend fun importTemplates(engine: Engine): ImportTemplatesResult {
val sceneUri = templateSceneUri
val archiveFile = File.createTempFile("imported-template", ".zip")
try {
val sceneFromUrl = engine.scene.load(
sceneUri = sceneUri,
waitForResources = true,
)
val templateString = engine.scene.saveToString(scene = sceneFromUrl)
val archiveBuffer = engine.scene.saveToArchive(scene = sceneFromUrl)
archiveBuffer.rewind()
archiveFile.outputStream().channel.use { channel ->
while (archiveBuffer.hasRemaining()) {
channel.write(archiveBuffer)
}
}
val archiveUri = Uri.fromFile(archiveFile)
engine.scene.loadArchive(
archiveUri = archiveUri,
waitForResources = true,
)
engine.scene.load(
scene = templateString,
waitForResources = true,
)
val loadedScene = requireNotNull(engine.scene.get()) {
"Template scene was not loaded."
}
val pages = engine.scene.getPages()
check(pages.isNotEmpty()) { "Template did not contain any pages." }
val currentPage = pages.first()
engine.block.setFloat(
block = loadedScene,
property = "scene/pageDimensions/width",
value = CURRENT_PAGE_WIDTH,
)
engine.block.setFloat(
block = loadedScene,
property = "scene/pageDimensions/height",
value = CURRENT_PAGE_HEIGHT,
)
engine.block.setWidth(block = currentPage, value = CURRENT_PAGE_WIDTH)
engine.block.setHeight(block = currentPage, value = CURRENT_PAGE_HEIGHT)
engine.scene.applyTemplate(templateUri = sceneUri)
val appliedFromUriPage = engine.scene.getPages().first()
val appliedFromUriPageSize = PageSize(
width = engine.block.getWidth(appliedFromUriPage),
height = engine.block.getHeight(appliedFromUriPage),
)
check(abs(appliedFromUriPageSize.width - CURRENT_PAGE_WIDTH) < PAGE_SIZE_TOLERANCE) {
"Applied URI template width ${appliedFromUriPageSize.width} did not preserve $CURRENT_PAGE_WIDTH."
}
check(abs(appliedFromUriPageSize.height - CURRENT_PAGE_HEIGHT) < PAGE_SIZE_TOLERANCE) {
"Applied URI template height ${appliedFromUriPageSize.height} did not preserve $CURRENT_PAGE_HEIGHT."
}
engine.scene.applyTemplate(template = templateString)
val appliedFromStringPage = engine.scene.getPages().first()
val appliedFromStringPageSize = PageSize(
width = engine.block.getWidth(appliedFromStringPage),
height = engine.block.getHeight(appliedFromStringPage),
)
check(abs(appliedFromStringPageSize.width - CURRENT_PAGE_WIDTH) < PAGE_SIZE_TOLERANCE) {
"Applied string template width ${appliedFromStringPageSize.width} did not preserve $CURRENT_PAGE_WIDTH."
}
check(abs(appliedFromStringPageSize.height - CURRENT_PAGE_HEIGHT) < PAGE_SIZE_TOLERANCE) {
"Applied string template height ${appliedFromStringPageSize.height} did not preserve $CURRENT_PAGE_HEIGHT."
}
val currentScene = requireNotNull(engine.scene.get()) {
"Applied template scene was not available."
}
engine.scene.zoomToBlock(
block = currentScene,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
return ImportTemplatesResult(
loadedPageCount = pages.size,
appliedFromUriPageSize = appliedFromUriPageSize,
appliedFromStringPageSize = appliedFromStringPageSize,
)
} finally {
archiveFile.delete()
}
}
```
Load design templates into CE.SDK from archive URIs, scene URLs, and
serialized strings in Android apps.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-templates)
Templates are pre-designed scenes that provide starting points for user projects. On Android, the Scene API can replace the current scene with a scene file, a self-contained archive, or serialized scene content.
This guide covers how to load templates from archives, URLs, and strings, and how to inspect the loaded scene.
## Load from Archive
Load an archive with `engine.scene.loadArchive`. Archives are `.zip` files that bundle the scene with its assets, which makes them portable and suitable for offline or self-hosted template delivery.
```kotlin highlight-android-load-from-archive
engine.scene.loadArchive(
archiveUri = archiveUri,
waitForResources = true,
)
```
The sample prepares `archiveUri` from a saved archive so the guide can run without a backend. In production, use a `Uri` that points to your stored template archive, such as a local file or your app's own download URL.
## Load from URL
Load a remote `.scene` file with `engine.scene.load(sceneUri = ...)`. Scene files are JSON-based and can reference assets by URL, so the referenced assets must remain reachable when the scene loads.
```kotlin highlight-android-load-from-url
val sceneFromUrl = engine.scene.load(
sceneUri = sceneUri,
waitForResources = true,
)
```
## Load from String
For templates stored in databases or received from APIs, load the serialized scene string with `engine.scene.load(scene = ...)`. The string should come from content previously saved with `engine.scene.saveToString(scene = ...)`.
```kotlin highlight-android-load-from-string
engine.scene.load(
scene = templateString,
waitForResources = true,
)
```
## Apply to the Current Scene
Loading a template replaces the current scene with the template scene. Applying a template keeps the current scene, design unit, and page dimensions, then adapts the template page content to fit those dimensions.
Use `engine.scene.applyTemplate` when your app already created the target scene or page size and only wants to import the template contents. Android supports applying `.scene` templates from a `Uri` or from serialized scene-string content. Keep archive-backed `.zip` inputs on `engine.scene.loadArchive(...)`; `applyTemplate(templateUri = ...)` is for scene-file URIs, not scene archives.
### Apply from a Scene URI
Apply a remote or local `.scene` template with `engine.scene.applyTemplate(templateUri = ...)`.
```kotlin highlight-android-apply-template-uri
engine.scene.applyTemplate(templateUri = sceneUri)
```
### Apply from a Scene String
Apply serialized template content with `engine.scene.applyTemplate(template = ...)` when the template was already read from storage or returned by an API.
```kotlin highlight-android-apply-template-string
engine.scene.applyTemplate(template = templateString)
```
## Working with the Loaded Scene
After loading a template, retrieve the active scene, inspect its pages, and adjust the viewport when your integration renders the engine output.
### Verify the Scene
Use `engine.scene.get()` to retrieve the current scene block. Pair it with `engine.scene.getPages()` to confirm the template contains pages before you continue with edits or exports.
```kotlin highlight-android-get-scene
val loadedScene = requireNotNull(engine.scene.get()) {
"Template scene was not loaded."
}
val pages = engine.scene.getPages()
check(pages.isNotEmpty()) { "Template did not contain any pages." }
```
### Zoom to Content
Fit the loaded template in the viewport with `engine.scene.zoomToBlock`. The padding parameters add screen-pixel spacing around the focused block.
```kotlin highlight-android-zoom-to-scene
val currentScene = requireNotNull(engine.scene.get()) {
"Applied template scene was not available."
}
engine.scene.zoomToBlock(
block = currentScene,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
```
## Handle Loading Failures
Template loading calls are suspend functions and can throw when the URI is unreachable, the file is not a valid scene or archive, or referenced assets cannot be resolved. Catch those exceptions at your app boundary and show a retry or fallback template instead of continuing with a missing scene.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.scene.loadArchive(archiveUri=_, waitForResources=_)` | Load a scene from a self-contained archive URI |
| `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a scene from a `.scene` file URI |
| `engine.scene.load(scene=_, waitForResources=_)` | Load a scene from serialized scene content |
| `engine.scene.applyTemplate(templateUri=_)` | Apply a `.scene` template URI to the current scene while preserving the current page dimensions |
| `engine.scene.applyTemplate(template=_)` | Apply serialized template scene content to the current scene while preserving the current page dimensions |
| `engine.scene.saveToString(scene=_)` | Serialize a scene so it can be stored or loaded again later |
| `engine.scene.saveToArchive(scene=_)` | Serialize a scene with its assets into an archive buffer |
| `engine.scene.get()` | Get the current scene block, or `null` when no scene is loaded |
| `engine.scene.getPages()` | Get the sorted pages in the current scene |
| `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Focus the viewport on the loaded scene or another block |
## Next Steps
- [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build reusable design templates programmatically using CE.SDK APIs
- [Apply a Template](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) - Apply template scenes via API while preserving page dimensions
---
## Related Pages
- [Import Templates from Scene Files](https://img.ly/docs/cesdk/android/create-templates/import/from-scene-file-52a01e/) - Load and import design templates from scene files in Android applications.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Import Templates from Scene Files"
description: "Load and import design templates from scene files in Android applications."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/import/from-scene-file-52a01e/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/) > [From Scene File](https://img.ly/docs/cesdk/android/create-templates/import/from-scene-file-52a01e/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-import-from-scene-file/ImportTemplatesFromSceneFile.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import ly.img.engine.MimeType
import ly.img.engine.SceneLayout
import java.io.File
suspend fun importTemplatesFromSceneFile(engine: Engine): ImportedTemplateSummary {
val cdnAssetsBaseUri = ly.img.editor.defaultBaseUri
engine.editor.setSettingString("basePath", cdnAssetsBaseUri.toString())
val sceneUri = Uri.parse(
"$cdnAssetsBaseUri/ly.img.templates/templates/cesdk_business_card_1.scene",
)
val sceneFromUrl = engine.scene.load(
sceneUri = sceneUri,
waitForResources = true,
)
val sceneUrlPageCount = engine.scene.getPages().size
val templateString = engine.scene.saveToString(scene = sceneFromUrl)
val sceneFromString = engine.scene.load(
scene = templateString,
waitForResources = true,
)
check(engine.scene.get() == sceneFromString)
val stringPageCount = engine.scene.getPages().size
val templateArchiveFile = File.createTempFile("template", ".zip")
val templateArchive = engine.scene.saveToArchive(scene = sceneFromString)
withContext(Dispatchers.IO) {
templateArchiveFile.outputStream().use { output ->
val archiveBuffer = templateArchive.asReadOnlyBuffer()
val channel = output.channel
while (archiveBuffer.hasRemaining()) {
channel.write(archiveBuffer)
}
}
}
val archiveUri = Uri.fromFile(templateArchiveFile)
val sceneFromArchive = engine.scene.loadArchive(
archiveUri = archiveUri,
waitForResources = true,
)
check(engine.scene.get() == sceneFromArchive)
val archivePageCount = engine.scene.getPages().size
val existingScene = engine.scene.create(
designUnit = DesignUnit.PIXEL,
sceneLayout = SceneLayout.FREE,
)
engine.block.setBoolean(block = existingScene, property = "scene/aspectRatioLock", value = false)
engine.block.setFloat(block = existingScene, property = "scene/pageDimensions/width", value = 1920F)
engine.block.setFloat(block = existingScene, property = "scene/pageDimensions/height", value = 1080F)
engine.scene.applyTemplate(templateUri = sceneUri)
val appliedPage = engine.scene.getPages().first()
val appliedPageWidth = engine.block.getWidth(appliedPage)
val appliedPageHeight = engine.block.getHeight(appliedPage)
val previewPngData = engine.block.export(
block = appliedPage,
mimeType = MimeType.PNG,
)
val activeScene = requireNotNull(engine.scene.get())
val appliedPageCount = engine.scene.getPages().size
val designUnit = engine.scene.getDesignUnit()
engine.scene.zoomToBlock(
block = activeScene,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
return ImportedTemplateSummary(
sceneUrlPageCount = sceneUrlPageCount,
stringPageCount = stringPageCount,
archivePageCount = archivePageCount,
appliedPageCount = appliedPageCount,
appliedPageWidth = appliedPageWidth,
appliedPageHeight = appliedPageHeight,
designUnit = designUnit,
previewPngData = previewPngData,
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-create-templates-import-from-scene-file/ImportedTemplateSummary.kt reference-only
import ly.img.engine.DesignUnit
import java.nio.ByteBuffer
data class ImportedTemplateSummary(
val sceneUrlPageCount: Int,
val stringPageCount: Int,
val archivePageCount: Int,
val appliedPageCount: Int,
val appliedPageWidth: Float,
val appliedPageHeight: Float,
val designUnit: DesignUnit,
val previewPngData: ByteBuffer,
)
```
Load complete design templates from scene files in Android applications with CE.SDK.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-templates-import-from-scene-file)
Scene files are portable templates that preserve the design structure, pages, blocks, assets, styles, and layout. This guide covers how to load templates from scene URLs, serialized strings, and archives, then apply template content while preserving the current page dimensions.
## Scene File Formats
CE.SDK supports two scene file formats for importing templates.
### Scene Format (.scene)
Scene files are JSON-based representations of design structures. They are lightweight because referenced assets stay outside the file and must remain reachable through their stored URIs.
**When to use:**
- Templates stored in databases
- Templates with hosted assets
- Lightweight transfer between your backend and app
### Archive Format (.archive or .zip)
Archive files bundle the scene structure with referenced assets in a ZIP file. Use archives when templates must remain portable or work when the original asset URLs are not available.
**When to use:**
- Template distribution
- Offline-capable templates
- Complete portability
- Self-contained handoff between systems
## Load Scene from URL
Use `engine.scene.load(sceneUri=_)` to load a `.scene` file from a remote or local URI. This replaces the active scene and adopts the template's page dimensions.
Set `basePath` to `ly.img.editor.defaultBaseUri` before loading this sample scene so relative resources in the scene file resolve against the CE.SDK Android package asset CDN on the IMG.LY CDN.
```kotlin highlight-android-template-source
val cdnAssetsBaseUri = ly.img.editor.defaultBaseUri
engine.editor.setSettingString("basePath", cdnAssetsBaseUri.toString())
val sceneUri = Uri.parse(
"$cdnAssetsBaseUri/ly.img.templates/templates/cesdk_business_card_1.scene",
)
```
```kotlin highlight-android-load-from-url
val sceneFromUrl = engine.scene.load(
sceneUri = sceneUri,
waitForResources = true,
)
```
Scene files reference external assets by URI. If those images, fonts, videos, or other assets are no longer reachable, the scene can load but appear incomplete.
## Load Scene from String
Use `engine.scene.load(scene=_)` when your app already has the serialized scene content, for example from a database or API response. The string should come from a scene previously saved with `engine.scene.saveToString(scene=_)`.
```kotlin highlight-android-load-from-string
val templateString = engine.scene.saveToString(scene = sceneFromUrl)
val sceneFromString = engine.scene.load(
scene = templateString,
waitForResources = true,
)
```
This path is useful for lightweight storage, but it has the same external asset requirement as a `.scene` URL.
## Load Scene from Archive
Use `engine.scene.loadArchive(archiveUri=_)` for self-contained templates. The archive URI can point to a ZIP file from your backend, Android content URI, local app asset, or a file created from `engine.scene.saveToArchive(scene=_)`.
This sample creates a local archive file from the previously loaded scene so the archive load is version-stable and does not depend on a package CDN URL.
```kotlin highlight-android-prepare-archive-uri
val templateArchiveFile = File.createTempFile("template", ".zip")
val templateArchive = engine.scene.saveToArchive(scene = sceneFromString)
withContext(Dispatchers.IO) {
templateArchiveFile.outputStream().use { output ->
val archiveBuffer = templateArchive.asReadOnlyBuffer()
val channel = output.channel
while (archiveBuffer.hasRemaining()) {
channel.write(archiveBuffer)
}
}
}
```
Load the archive URI with `engine.scene.loadArchive(archiveUri=_)`.
```kotlin highlight-android-load-from-archive
val archiveUri = Uri.fromFile(templateArchiveFile)
val sceneFromArchive = engine.scene.loadArchive(
archiveUri = archiveUri,
waitForResources = true,
)
```
When you load from an archive:
- CE.SDK extracts the ZIP file.
- The scene structure becomes the active scene.
- Embedded assets are resolved from the archive contents.
- The returned `DesignBlock` is the loaded scene.
## Apply Template vs Load Scene
CE.SDK provides two approaches for working with templates.
### Load Scene
When you use `engine.scene.load(sceneUri=_)`, `engine.scene.load(scene=_)`, or `engine.scene.loadArchive(archiveUri=_)`, CE.SDK:
- Replaces the active scene.
- Uses the template's page dimensions and design unit.
- Loads the template content as authored.
This is appropriate when starting a new project from a template.
### Apply Template
When you use `engine.scene.applyTemplate(templateUri=_)` or `engine.scene.applyTemplate(template=_)`, CE.SDK:
- Keeps the current scene's design unit.
- Preserves the current page dimensions.
- Adjusts template content to fit the existing page.
This is useful when your app needs a fixed output size and only wants to import the template content.
Set the scene-level `scene/pageDimensions/width` and `scene/pageDimensions/height` properties before applying the template so the imported content is fitted to the intended output size.
```kotlin highlight-android-apply-template
val existingScene = engine.scene.create(
designUnit = DesignUnit.PIXEL,
sceneLayout = SceneLayout.FREE,
)
engine.block.setBoolean(block = existingScene, property = "scene/aspectRatioLock", value = false)
engine.block.setFloat(block = existingScene, property = "scene/pageDimensions/width", value = 1920F)
engine.block.setFloat(block = existingScene, property = "scene/pageDimensions/height", value = 1080F)
engine.scene.applyTemplate(templateUri = sceneUri)
val appliedPage = engine.scene.getPages().first()
val appliedPageWidth = engine.block.getWidth(appliedPage)
val appliedPageHeight = engine.block.getHeight(appliedPage)
```
## Get Scene Information
After loading or applying a template, use the scene APIs to retrieve the active scene, count pages, inspect the design unit, or fit the scene into the viewport.
```kotlin highlight-android-inspect-scene
val activeScene = requireNotNull(engine.scene.get())
val appliedPageCount = engine.scene.getPages().size
val designUnit = engine.scene.getDesignUnit()
engine.scene.zoomToBlock(
block = activeScene,
paddingLeft = 40F,
paddingTop = 40F,
paddingRight = 40F,
paddingBottom = 40F,
)
```
## Error Handling
Template imports can fail for the same reasons as other scene loads:
- **Network errors:** the URL is unreachable or the device has no network access.
- **Invalid scene data:** the `.scene` string or archive contents cannot be parsed.
- **Missing assets:** a `.scene` file loads, but referenced assets are unavailable.
Prefer archives for templates that must be portable. Wrap loading calls with your app's normal coroutine error handling, show a user-facing fallback, and keep a blank or default scene available when remote templates cannot load.
## Performance Considerations
Archive size directly affects loading time because CE.SDK downloads or reads the ZIP and resolves its assets before the scene is ready. Show loading state for medium and large templates, and avoid loading several unrelated templates sequentially on the main user path.
## URL Access Considerations
Android does not enforce browser CORS rules, but the engine still needs reachable URLs. Host scene files and archives over HTTPS, keep referenced asset URLs stable, and use archives when assets should travel with the template instead of being fetched from their original locations.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.editor.setSettingString(keypath="basePath", value=_)` | Configure the asset base URI used by resources referenced from scene files. |
| `engine.scene.loadArchive(archiveUri=_, waitForResources=_)` | Load a complete scene from a ZIP archive URI. |
| `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a scene from a local or remote `.scene` URI. |
| `engine.scene.load(scene=_, waitForResources=_)` | Load a scene from serialized scene content. |
| `engine.scene.applyTemplate(templateUri=_)` | Apply a template scene from a URI while preserving the current page dimensions. |
| `engine.scene.applyTemplate(template=_)` | Apply serialized template content while preserving the current page dimensions. |
| `engine.scene.create(designUnit=_, sceneLayout=_)` | Create a new scene, which then receives the applied template content. |
| `engine.scene.saveToString(scene=_)` | Serialize a scene to a string for storage or transport. |
| `engine.scene.saveToArchive(scene=_)` | Serialize a scene and its reachable assets into a ZIP archive. |
| `engine.scene.get()` | Return the active scene block, or `null` when no scene is loaded. |
| `engine.scene.getPages()` | Return the pages in the active scene. |
| `engine.scene.getDesignUnit()` | Return the active scene's design unit. |
| `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Fit a scene or block into the viewport with padding. |
| `engine.block.setBoolean(block=_, property="scene/aspectRatioLock", value=_)` | Control whether scene-level page dimensions keep their current aspect ratio. |
| `engine.block.setFloat(block=_, property="scene/pageDimensions/width", value=_)` | Set the scene-level page width used by applied templates. |
| `engine.block.setFloat(block=_, property="scene/pageDimensions/height", value=_)` | Set the scene-level page height used by applied templates. |
| `engine.block.getWidth(block=_)` | Read back the page width after applying a template. |
| `engine.block.getHeight(block=_)` | Read back the page height after applying a template. |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Lock the Template"
description: "Restrict editing access to specific elements or properties in a template to enforce design rules."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/lock-131489/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/)
---
```kotlin file=@cesdk_android_examples/engine-guides-lock-template/LockTemplate.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
fun lockTemplate(engine: Engine) {
engine.editor.setRole("Creator")
val creatorRole = engine.editor.getRole()
require(creatorRole == "Creator")
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 720F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, "Template background")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 720F)
engine.block.setHeight(background, value = 1080F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(background, color = Color.fromHex("#FFF7F9FC"))
engine.block.appendChild(parent = page, child = background)
val brandBanner = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(brandBanner, "Locked brand banner")
engine.block.setShape(brandBanner, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(brandBanner, value = 640F)
engine.block.setHeight(brandBanner, value = 220F)
engine.block.setPositionX(brandBanner, value = 40F)
engine.block.setPositionY(brandBanner, value = 48F)
engine.block.setFill(brandBanner, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(brandBanner, color = Color.fromHex("#FF11203A"))
engine.block.appendChild(parent = page, child = brandBanner)
val brandName = engine.block.create(DesignBlockType.Text)
engine.block.setName(brandName, "Locked brand name")
engine.block.setWidthMode(brandName, mode = SizeMode.AUTO)
engine.block.setHeightMode(brandName, mode = SizeMode.AUTO)
engine.block.setPositionX(brandName, value = 88F)
engine.block.setPositionY(brandName, value = 124F)
engine.block.replaceText(brandName, text = "Brand Studio")
engine.block.setTextColor(brandName, color = Color.fromHex("#FFFFFFFF"))
engine.block.appendChild(parent = page, child = brandName)
val headline = engine.block.create(DesignBlockType.Text)
engine.block.setName(headline, "Editable campaign headline")
engine.block.setWidthMode(headline, mode = SizeMode.AUTO)
engine.block.setHeightMode(headline, mode = SizeMode.AUTO)
engine.block.setPositionX(headline, value = 88F)
engine.block.setPositionY(headline, value = 404F)
engine.block.replaceText(headline, text = "Spring Launch")
engine.block.setTextColor(headline, color = Color.fromHex("#FF11203A"))
engine.block.setBackgroundColor(headline, color = Color.fromRGBA(231, 240, 255, 255))
engine.block.setBackgroundColorEnabled(headline, enabled = true)
engine.block.setFloat(headline, property = "backgroundColor/paddingLeft", value = 24F)
engine.block.setFloat(headline, property = "backgroundColor/paddingTop", value = 20F)
engine.block.setFloat(headline, property = "backgroundColor/paddingRight", value = 24F)
engine.block.setFloat(headline, property = "backgroundColor/paddingBottom", value = 20F)
engine.block.setFloat(headline, property = "backgroundColor/cornerRadius", value = 18F)
engine.block.appendChild(parent = page, child = headline)
val templateScopes = listOf(
"editor/select",
"text/edit",
"text/character",
"fill/change",
"layer/move",
"layer/resize",
"layer/rotate",
"lifecycle/destroy",
)
listOf(page, background, brandBanner, brandName).forEach { lockedBlock ->
templateScopes.forEach { scope ->
engine.block.setScopeEnabled(block = lockedBlock, key = scope, enabled = false)
}
}
engine.block.setScopeEnabled(block = headline, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(block = headline, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(block = headline, key = "text/character", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "fill/change", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "layer/resize", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "layer/rotate", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "lifecycle/destroy", enabled = false)
engine.editor.setRole("Creator")
val creatorCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val creatorCanEditHeadline = engine.block.isAllowedByScope(headline, key = "text/edit")
require(creatorCanSelectBrandBanner)
require(creatorCanEditHeadline)
engine.editor.setRole("Adopter")
engine.editor.setGlobalScope(key = "editor/add", globalScope = GlobalScope.DENY)
val adopterCanAddBlocks = engine.block.isAllowedByScope(headline, key = "editor/add")
val adopterCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val adopterCanEditHeadline = engine.block.isAllowedByScope(headline, key = "text/edit")
val adopterCanRestyleHeadline = engine.block.isAllowedByScope(headline, key = "text/character")
val adopterCanMoveHeadline = engine.block.isAllowedByScope(headline, key = "layer/move")
val adopterCanDeleteHeadline = engine.block.isAllowedByScope(headline, key = "lifecycle/destroy")
require(!adopterCanAddBlocks)
require(!adopterCanSelectBrandBanner)
require(adopterCanEditHeadline)
require(!adopterCanRestyleHeadline)
require(!adopterCanMoveHeadline)
require(!adopterCanDeleteHeadline)
engine.editor.setRole("Viewer")
val viewerRole = engine.editor.getRole()
require(viewerRole == "Viewer")
engine.editor.setRole("Creator")
}
```
Set up a two-surface template workflow where creators build and lock layouts
while adopters customize only the areas you allow.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-lock-template)
Many integrations need two editing experiences: one for designers or admins who build templates, and one for end users who fill them in. CE.SDK models this with roles and scopes. Use a Creator surface to prepare the template. In production, save that configured template and load it in an Adopter surface; the sample keeps both roles in one scene so it can focus on the permission rules.
The snippets below use the Android Engine API directly. The same role and scope settings are respected by the CE.SDK editor UI; the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) is a complete Android UI surface you can configure around this workflow.
For detailed scope configuration patterns, see [Lock Content](https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/).
## Understanding the Two-Surface Pattern
Template-based workflows usually split users by responsibility:
| Surface | Users | Role | What they can do |
| ------- | ----- | ---- | ---------------- |
| Creator surface | Designers, admins | `Creator` | Build templates, set locks, and save the result |
| Adopter surface | End users, marketers | `Adopter` | Modify only blocks with enabled scopes |
This separation protects the template's brand and layout rules while still allowing personalization. The Creator role has full access. The Adopter role applies its default permissions and evaluates block-level scopes.
## Setting Up the Creator Surface
Set the engine role to `Creator` while building or updating the template. Creators can configure locked brand elements, editable text, and other scope rules before saving the scene.
```kotlin highlight-android-creator-surface
engine.editor.setRole("Creator")
val creatorRole = engine.editor.getRole()
require(creatorRole == "Creator")
```
In an app, this surface can be a dedicated admin screen, a separate editor configuration, or an internal template-building flow.
## Configuring What Users Can Edit
Scopes decide which operations Adopters can perform. In `Creator`, disable operations on locked blocks and enable only the scopes that should be available on each editable block.
```kotlin highlight-android-configure-scopes
val templateScopes = listOf(
"editor/select",
"text/edit",
"text/character",
"fill/change",
"layer/move",
"layer/resize",
"layer/rotate",
"lifecycle/destroy",
)
listOf(page, background, brandBanner, brandName).forEach { lockedBlock ->
templateScopes.forEach { scope ->
engine.block.setScopeEnabled(block = lockedBlock, key = scope, enabled = false)
}
}
engine.block.setScopeEnabled(block = headline, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(block = headline, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(block = headline, key = "text/character", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "fill/change", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "layer/resize", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "layer/rotate", enabled = false)
engine.block.setScopeEnabled(block = headline, key = "lifecycle/destroy", enabled = false)
```
The example keeps the brand banner, background, and brand name locked. The campaign headline can be selected and edited, but it cannot be moved, resized, restyled, or deleted.
## Checking Creator Permissions
After setting block-level scopes, switch back to `Creator` to confirm the template-building surface still has full access. Creator permissions ignore the locks that constrain Adopters.
```kotlin highlight-android-check-creator-permissions
engine.editor.setRole("Creator")
val creatorCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val creatorCanEditHeadline = engine.block.isAllowedByScope(headline, key = "text/edit")
require(creatorCanSelectBrandBanner)
require(creatorCanEditHeadline)
```
This check is useful in tooling that lets designers preview the template before publishing it.
## Setting Up the Adopter Surface
For production surfaces, load the saved template in the end-user surface, then set the role to `Adopter`. In this sample, the same scene switches to `Adopter` after scope setup. Because `setRole("Adopter")` applies the role's default global scopes, deny `editor/add` after setting the role when adopters should only edit existing template areas.
```kotlin highlight-android-adopter-surface
engine.editor.setRole("Adopter")
engine.editor.setGlobalScope(key = "editor/add", globalScope = GlobalScope.DENY)
val adopterCanAddBlocks = engine.block.isAllowedByScope(headline, key = "editor/add")
val adopterCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val adopterCanEditHeadline = engine.block.isAllowedByScope(headline, key = "text/edit")
val adopterCanRestyleHeadline = engine.block.isAllowedByScope(headline, key = "text/character")
val adopterCanMoveHeadline = engine.block.isAllowedByScope(headline, key = "layer/move")
val adopterCanDeleteHeadline = engine.block.isAllowedByScope(headline, key = "lifecycle/destroy")
require(!adopterCanAddBlocks)
require(!adopterCanSelectBrandBanner)
require(adopterCanEditHeadline)
require(!adopterCanRestyleHeadline)
require(!adopterCanMoveHeadline)
require(!adopterCanDeleteHeadline)
```
In this state, users can edit the headline text but cannot add new blocks, select the locked brand banner, or move the headline.
## When to Use This Pattern
Use separate Creator and Adopter surfaces when your integration needs controlled customization:
- **Brand template systems**: Teams personalize approved layouts without changing brand assets.
- **Design approval workflows**: Reviewers can inspect a template without accidentally changing protected blocks.
- **Self-service customization**: Customers edit designated fields inside fixed layout rules.
- **White-label products**: Tenant-specific surfaces expose only the areas each tenant may change.
For simpler integrations where every user has the same permissions, a single role may be enough.
## Viewer Role for Read-Only Access
For preview or approval screens where no editing should happen, use the `Viewer` role instead of `Adopter`.
```kotlin highlight-android-viewer-surface
engine.editor.setRole("Viewer")
val viewerRole = engine.editor.getRole()
require(viewerRole == "Viewer")
```
Use `Viewer` for read-only display. Use `Adopter` when users should edit selected placeholders, text blocks, or media areas.
## Troubleshooting
| Issue | Cause | Solution |
| ----- | ----- | -------- |
| Adopters can edit everything | The surface is still in `Creator`, or the locked blocks still have operation scopes enabled | Set the role to `Adopter` and disable the relevant block scopes in the Creator surface |
| Adopters cannot select an editable block | `editor/select` is disabled on that block | Enable `editor/select` on every block users should interact with |
| A block is selectable but cannot be changed | The operation-specific scope is disabled | Enable the matching scope, such as `text/edit` for text content or `fill/change` for image replacement |
| Adopters can add new blocks | `editor/add` is still allowed by the Adopter role defaults | After setting the role to `Adopter`, set `editor/add` to `GlobalScope.DENY` |
| Creator tooling appears locked | The active role is not `Creator` | Switch the template-building surface back to `Creator` before editing locks |
| Template locks do not appear after loading a template | The production template was not saved after configuring scopes | Save the scene/template after setting block scopes in the Creator surface, then load that saved template in the Adopter surface. |
## API Reference
| Method | Purpose |
| ------ | ------- |
| `engine.editor.setRole(role=_)` | Set the active user role, such as `Creator`, `Adopter`, or `Viewer`. |
| `engine.editor.getRole()` | Read the active user role. |
| `engine.editor.setGlobalScope(key=_,globalScope=_)` | Allow, deny, or defer an operation globally. |
| `engine.block.setScopeEnabled(block=_,key=_,enabled=_)` | Enable or disable a scope on one block. |
| `engine.block.isAllowedByScope(block=_,key=_)` | Check the final permission after role, global scope, and block-level scope evaluation. |
### Common Scopes
| Scope | Description |
| ----- | ----------- |
| `editor/add` | Allow adding new blocks. |
| `editor/select` | Allow selecting the block. |
| `text/edit` | Allow editing text content. |
| `text/character` | Allow changing text styling such as font or size. |
| `fill/change` | Allow changing fill content or text color. |
| `layer/move` | Allow moving the block. |
| `layer/resize` | Allow resizing the block. |
| `layer/rotate` | Allow rotating the block. |
| `lifecycle/destroy` | Allow deleting the block. |
## Next Steps
- [Lock Content](https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/) - Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system
- [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) - Control editing access with roles and scopes
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Mark editable image, video, or text areas within a locked template layout
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Overview"
description: "Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Overview](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/)
---
Get a high-level map of CE.SDK template workflows on Android, from creation
and import to dynamic content and asset libraries.
In CE.SDK, a *template* is a scene or video composition prepared with editable areas, constraints, and optional dynamic inputs. It gives users specific places to change text or media while the surrounding layout, branding, and export workflow stay predictable.
Unlike a regular editable design, a template narrows editing freedom through placeholders, variables, and permissions. You decide which elements users can change, which parts stay locked, and whether the result is created through the CE.SDK UI, the API, or both.
Templates can produce static outputs such as PNG and PDF, as well as video outputs such as MP4. They are a core part of design automation, personalization, and streamlined creative workflows in Android apps.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
For Android, templates are loaded as CE.SDK scene files (`.scene`) or archive
templates. Photoshop (`.psd`) and InDesign (`.idml`) files are not imported
directly on Android. Convert those files with the Browser/Web or Node.js/Server
importers first, save the result as a scene or archive, and then load that
converted template in your Android app.
These imported designs can then be adapted into editable, structured templates inside CE.SDK.
## Next Steps
- [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build reusable design templates programmatically using CE.SDK APIs.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values.
- [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Mark editable image, video, or text areas within a locked template layout.
- [Set Editing Constraints](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Learn how to control editing capabilities in CE.SDK templates using the Scope system to lock positions, prevent transformations, and create guided editing experiences
- [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) - Manage how users browse, preview, and insert templates and other assets.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Videos"
description: "Learn how to create and customize videos in CE.SDK using scenes, assets, and time-based editing."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video-c41a08/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/)
---
---
## Related Pages
- [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) - Learn how Android video projects work in CE.SDK and choose the right guide for UI-based or programmatic video workflows.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame.
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Learn to play, pause, seek, and preview audio and video content in CE.SDK using playback controls and solo mode.
- [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) - Learn how to trim video and audio clips in CE.SDK for Android using the Video Editor starter kit timeline and Engine APIs.
- [Force Trim](https://img.ly/docs/cesdk/android/edit-video/force-trim-3c1e8a/) - Enforce minimum and maximum video durations in the editor UI.
- [Split Video and Audio](https://img.ly/docs/cesdk/android/edit-video/split-464167/) - Learn how to split video and audio clips at specific time points in CE.SDK for Android.
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK.
- [Transform Videos](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) - Learn how Android video transforms use block geometry, crop transforms, groups, animations, and transform permissions.
- [Apply Transitions](https://img.ly/docs/cesdk/android/create-video/apply-transitions-146026/) - Blend adjacent video clips with clip-to-clip transitions such as cross-fades, pushes, and wipes using CE.SDK's transitions API.
- [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Add synchronized captions to Android video scenes with CE.SDK.
- [Update Caption Presets](https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/) - Extend video captions with custom caption preset files and asset source manifests on Android.
- [Add Watermark](https://img.ly/docs/cesdk/android/edit-video/add-watermark-762ce6/) - Add text and image watermarks to videos with timeline duration, positioning, opacity, and visibility controls in Android.
- [Annotation](https://img.ly/docs/cesdk/android/edit-video/annotation-e9cbad/) - Add timed text, shapes, and highlights to video scenes on Android.
- [Redact Sensitive Content in Videos](https://img.ly/docs/cesdk/android/edit-video/redaction-cf6d03/) - Redact sensitive video content on Android using blur, pixelization, solid overlays, and timeline controls.
- [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) - Record reactions to a base video and compose them into an editable Android video scene.
- [Lock Design](https://img.ly/docs/cesdk/android/create-video/lock-design-e92ce4/) - Protect video designs from unwanted modifications using CE.SDK's scope-based permission system.
- [Programmatic Creation](https://img.ly/docs/cesdk/android/create-video/programmatic-2b243c/) - Create and export video scenes entirely through code with the CE.SDK Engine on Android.
- [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) - Edit video scenes with CE.SDK Engine APIs on Android.
- [Video Limitations](https://img.ly/docs/cesdk/android/create-video/limitations-6a740d/) - Understand resolution limits, duration constraints, codec support, and device-specific restrictions when working with video in CE.SDK for Android.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Apply Transitions"
description: "Blend adjacent video clips with clip-to-clip transitions such as cross-fades, pushes, and wipes using CE.SDK's transitions API."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/apply-transitions-146026/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Apply Transitions](https://img.ly/docs/cesdk/android/create-video/apply-transitions-146026/)
---
Blend adjacent video clips into each other with clip-to-clip transitions such as cross-fades, pushes, and wipes using CE.SDK's transitions API.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/main/engine-guides-apply-transitions)
A transition belongs to the **outgoing clip** and blends it into the following clip on the same track. When you assign one, the engine overlaps the two clips for the transition duration: the incoming clip—and every clip after it—moves earlier on the timeline, and audio cross-fades linearly over the same window.
Transitions are blocks. You create one with `createTransition`, attach it to a clip with `setTransition`, and configure it through the same generic setters you use for other blocks. Once assigned, the transition is owned by its clip: it is destroyed together with the clip, and the engine also destroys it automatically when the two clips stop being timeline-adjacent.
```kotlin file=@cesdk_android_examples/engine-guides-apply-transitions/ApplyTransitions.kt reference-only
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.yield
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.TransitionType
private const val TAG = "ApplyTransitions"
// The engine lays out track children on its scheduled update pass, so derived values such as time
// offsets only settle once that pass has run. Offscreen runs like this sample have to wait for it.
private suspend fun waitForScheduledEngineUpdate() = yield()
suspend fun applyTransitions(engine: Engine): TransitionSummary {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = page, value = 1280F)
engine.block.setHeight(block = page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
// Build a sequence of four clips on a single track.
val videoUris = listOf(
"https://img.ly/static/ubq_video_samples/bbb.mp4",
"https://img.ly/static/ubq_video_samples/test30.mp4",
"https://img.ly/static/ubq_video_samples/multitrack_output.mp4",
"https://img.ly/static/ubq_video_samples/bbb.mp4",
)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
val clips = videoUris.map { videoUri ->
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = clip, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(block = videoFill, property = "fill/video/fileURI", value = Uri.parse(videoUri))
engine.block.setFill(block = clip, fill = videoFill)
engine.block.setDuration(block = clip, duration = 4.0)
engine.block.appendChild(parent = track, child = clip)
clip
}
engine.block.fillParent(track)
val (clipA, clipB, clipC) = clips
// Individual clips placed directly in a video track support transitions, no matter whether they
// show a video, an image, a shape, a sticker or text.
val clipsSupportTransitions =
engine.block.supportsTransition(clipA) && engine.block.supportsTransition(clipB)
Log.i(TAG, "Clips support transitions: $clipsSupportTransitions")
// The duration defines how long the two clips overlap.
val crossFade = engine.block.createTransition(TransitionType.CrossFade)
engine.block.setDuration(block = crossFade, duration = 1.0)
// Assign the transition to the outgoing clip. It blends this clip into the next clip
// on the same track.
engine.block.setTransition(block = clipA, transition = crossFade)
waitForScheduledEngineUpdate()
// Assigning a transition overlaps the two clips: the incoming clip and everything after it
// move earlier by the transition duration.
val incomingClipOffset = engine.block.getTimeOffset(clipB)
Log.i(TAG, "Clip B now starts at $incomingClipOffset seconds (was 4)")
// Per-type properties use the generic block setters with "transition/{type}/{property}" keys.
// Discover what a type exposes with findAllProperties.
val push = engine.block.createTransition(TransitionType.Push)
engine.block.setDuration(block = push, duration = 1.0)
engine.block.setTransition(block = clipB, transition = push)
val pushProperties = engine.block.findAllProperties(push)
Log.i(TAG, "Push properties: $pushProperties")
engine.block.setEnum(block = push, property = "transition/push/direction", value = "Left")
// With morph enabled, position, rotation, scale, and shape are interpolated between the
// outgoing and incoming clip.
engine.block.setBoolean(block = push, property = "transition/push/morph", value = true)
// An unset relation returns an invalid block, so check the result with isValid.
val assigned = engine.block.getTransition(clipA)
val assignedType = if (engine.block.isValid(assigned)) engine.block.getType(assigned) else null
Log.i(TAG, "Clip A transitions with: $assignedType")
val fadeToBlack = engine.block.createTransition(TransitionType.FadeToBlack)
engine.block.setDuration(block = fadeToBlack, duration = 1.0)
engine.block.setTransition(block = clipC, transition = fadeToBlack)
// removeTransition detaches the block and restores the original clip timing, but does not
// destroy it: the detached block stays valid until you destroy it yourself.
engine.block.removeTransition(clipC)
val detachedTransitionIsValid = engine.block.isValid(fadeToBlack)
engine.block.destroy(fadeToBlack)
val colorWipe = engine.block.createTransition(TransitionType.ColorWipe)
engine.block.setDuration(block = colorWipe, duration = 1.0)
engine.block.setTransition(block = clipC, transition = colorWipe)
engine.block.setEnum(block = colorWipe, property = "transition/color-wipe/direction", value = "Up")
engine.block.setColor(
block = colorWipe,
property = "transition/color-wipe/color",
value = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F),
)
// Fit the page duration to the reflowed sequence and park the playhead inside the first
// overlap window so the cross-fade blend is the visible frame.
val lastClip = clips.last()
engine.block.setDuration(
block = page,
duration = engine.block.getTimeOffset(lastClip) + engine.block.getDuration(lastClip),
)
engine.block.setPlaybackTime(block = page, time = 3.5)
return TransitionSummary(
clipsSupportTransitions = clipsSupportTransitions,
incomingClipOffset = incomingClipOffset,
pushProperties = pushProperties,
assignedTransitionType = assignedType,
replacedTransitionType = engine.block.getType(engine.block.getTransition(clipC)),
detachedTransitionIsValid = detachedTransitionIsValid,
)
}
```
This guide covers how to check transition support, create and assign transitions between clips, understand the timeline overlap they introduce, configure per-type properties, and replace or remove them.
## Creating the Video Timeline
Transitions need a video scene with at least two adjacent clips on a track. We create the scene first.
```kotlin highlight-android-setup
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(block = page, value = 1280F)
engine.block.setHeight(block = page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
```
Next, we build a sequence of four clips. Each clip is a graphic block with a video fill, and appending the clips to a track makes them play one after another.
```kotlin highlight-android-create-clips
// Build a sequence of four clips on a single track.
val videoUris = listOf(
"https://img.ly/static/ubq_video_samples/bbb.mp4",
"https://img.ly/static/ubq_video_samples/test30.mp4",
"https://img.ly/static/ubq_video_samples/multitrack_output.mp4",
"https://img.ly/static/ubq_video_samples/bbb.mp4",
)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
val clips = videoUris.map { videoUri ->
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = clip, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(block = videoFill, property = "fill/video/fileURI", value = Uri.parse(videoUri))
engine.block.setFill(block = clip, fill = videoFill)
engine.block.setDuration(block = clip, duration = 4.0)
engine.block.appendChild(parent = track, child = clip)
clip
}
engine.block.fillParent(track)
val (clipA, clipB, clipC) = clips
```
Each clip plays for 4 seconds, so the clips initially start at 0, 4, 8, and 12 seconds.
## Checking Transition Support
Before assigning a transition, we verify that both the outgoing and the incoming clip support one. Any individual clip placed directly in a video track qualifies, no matter whether it shows a video, an image, a shape, a sticker or text. Audio, group, caption, and cutout blocks report `false`, as do blocks outside a video track.
```kotlin highlight-android-check-support
// Individual clips placed directly in a video track support transitions, no matter whether they
// show a video, an image, a shape, a sticker or text.
val clipsSupportTransitions =
engine.block.supportsTransition(clipA) && engine.block.supportsTransition(clipB)
Log.i(TAG, "Clips support transitions: $clipsSupportTransitions")
```
`setTransition` throws when either clip fails this check, so testing upfront lets you disable the option instead of handling an error later.
## Creating and Assigning a Transition
We create a cross-fade and give it a duration. The duration defines how long the two clips overlap.
```kotlin highlight-android-create-transition
// The duration defines how long the two clips overlap.
val crossFade = engine.block.createTransition(TransitionType.CrossFade)
engine.block.setDuration(block = crossFade, duration = 1.0)
```
The new block is standalone until we assign it. Assigning it to the first clip blends that clip into the one that follows it on the track.
```kotlin highlight-android-set-transition
// Assign the transition to the outgoing clip. It blends this clip into the next clip
// on the same track.
engine.block.setTransition(block = clipA, transition = crossFade)
```
A transition block can only be assigned to one clip. Assigning a different transition to a clip that already has one detaches the previous block without destroying it.
## Understanding the Timeline Overlap
Assigning a transition reflows the timeline. The incoming clip moves earlier by the transition duration so the two clips overlap, and all downstream clips shift with it.
```kotlin highlight-android-timeline-reflow
// Assigning a transition overlaps the two clips: the incoming clip and everything after it
// move earlier by the transition duration.
val incomingClipOffset = engine.block.getTimeOffset(clipB)
Log.i(TAG, "Clip B now starts at $incomingClipOffset seconds (was 4)")
```
With a 1-second cross-fade on the first clip, the second clip's time offset changes from 4 to 3 seconds. The effective overlap is clamped: it never exceeds half of either neighboring clip's duration, so a long transition between short clips plays shorter than requested. Removing the transition restores the original offsets.
## Configuring Transition Properties
Each transition type exposes its own properties under `transition/{type}/{property}` keys, which you set through the generic block setters. Use `findAllProperties` to discover what a specific type offers.
```kotlin highlight-android-configure-properties
// Per-type properties use the generic block setters with "transition/{type}/{property}" keys.
// Discover what a type exposes with findAllProperties.
val push = engine.block.createTransition(TransitionType.Push)
engine.block.setDuration(block = push, duration = 1.0)
engine.block.setTransition(block = clipB, transition = push)
val pushProperties = engine.block.findAllProperties(push)
Log.i(TAG, "Push properties: $pushProperties")
engine.block.setEnum(block = push, property = "transition/push/direction", value = "Left")
```
Directional types such as `push`, `slide`, `wipe`, and `color-wipe` expose a `direction` enum. Others expose numeric controls, for example `transition/cross-spin/intensity` or `transition/cross-warp/zoom`, and `color-wipe` exposes a `transition/color-wipe/color` color property.
## Morphing Between Clips
Most transition types expose a `morph` flag—only `clock-wipe` and `cross-spin` don't. When enabled, the engine interpolates position, rotation, scale, and shape between the outgoing and incoming clip during the transition.
```kotlin highlight-android-morph
// With morph enabled, position, rotation, scale, and shape are interpolated between the
// outgoing and incoming clip.
engine.block.setBoolean(block = push, property = "transition/push/morph", value = true)
```
Morphing is most visible when the two clips differ in framing—for example when one clip is scaled or rotated relative to the other.
## Reading Back a Transition
`getTransition` returns the transition assigned to a clip, or an invalid block when the clip has none. Check the result with `isValid` before using it.
```kotlin highlight-android-get-transition
// An unset relation returns an invalid block, so check the result with isValid.
val assigned = engine.block.getTransition(clipA)
val assignedType = if (engine.block.isValid(assigned)) engine.block.getType(assigned) else null
Log.i(TAG, "Clip A transitions with: $assignedType")
```
## Replacing or Removing a Transition
`removeTransition` detaches a clip's transition and restores the original clip timing, but does not destroy the detached block. Destroy it yourself when you no longer need it, then assign a replacement.
```kotlin highlight-android-remove-transition
val fadeToBlack = engine.block.createTransition(TransitionType.FadeToBlack)
engine.block.setDuration(block = fadeToBlack, duration = 1.0)
engine.block.setTransition(block = clipC, transition = fadeToBlack)
// removeTransition detaches the block and restores the original clip timing, but does not
// destroy it: the detached block stays valid until you destroy it yourself.
engine.block.removeTransition(clipC)
val detachedTransitionIsValid = engine.block.isValid(fadeToBlack)
engine.block.destroy(fadeToBlack)
val colorWipe = engine.block.createTransition(TransitionType.ColorWipe)
engine.block.setDuration(block = colorWipe, duration = 1.0)
engine.block.setTransition(block = clipC, transition = colorWipe)
engine.block.setEnum(block = colorWipe, property = "transition/color-wipe/direction", value = "Up")
engine.block.setColor(
block = colorWipe,
property = "transition/color-wipe/color",
value = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F),
)
```
Removing from a clip without an assigned transition is a no-op.
## Transition Types
`createTransition` takes a `TransitionType`:
- **Blends**: `CrossFade`, `CrossBlur`, `CrossSpin`, `CrossZoom`, `CrossWarp`
- **Movement**: `Push`, `Slide`, `Stack`, `Splice`, `DiagonalSplice`
- **Fades**: `Fade`, `FadeToBlack`, `FadeToWhite`, `GradientFade`
- **Wipes**: `Wipe`, `LineWipe`, `ClockWipe`, `ColorWipe`
- **Patterns**: `Chop`, `TwoStripes`
- **None**: `None`
## Troubleshooting
### Transition Has No Visible Effect
Check that both clips sit next to each other on the same track and that the playhead is inside the overlap window. The overlap starts at the incoming clip's time offset and ends at the outgoing clip's end.
### setTransition Throws
Verify `supportsTransition` returns `true` for both the outgoing and the incoming clip, and that the transition block isn't already assigned to another clip.
### Transition Disappeared
The engine destroys an assigned transition when the two clips stop being timeline-adjacent—for example after a gap is introduced between them—or when the owning clip is destroyed.
### Transition Plays Shorter Than Requested
The effective duration is clamped to half of either neighboring clip's duration. Shorten the transition or lengthen the clips.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create the video scene the clips and transitions live in. |
| `engine.block.create(blockType=_)` | Create the page, track, and clip blocks. |
| `engine.block.appendChild(parent=_, child=_)` | Add the page, track, and clips in timeline order. |
| `engine.block.setWidth(block=_, value=_)` | Set the page width. |
| `engine.block.setHeight(block=_, value=_)` | Set the page height. |
| `engine.block.createShape(type=_)` | Create the rectangle shape a clip is drawn with. |
| `engine.block.setShape(block=_, shape=_)` | Assign the shape to a clip. |
| `engine.block.createFill(fillType=_)` | Create the video fill that carries a clip's media. |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Assign a video URI to a video fill. |
| `engine.block.setFill(block=_, fill=_)` | Assign the video fill to a clip. |
| `engine.block.fillParent(block=_)` | Size the track to fill the page. |
| `engine.block.createTransition(type=_)` | Create a standalone transition block of the given `TransitionType`. |
| `engine.block.supportsTransition(block=_)` | Check whether a clip can own an outgoing transition. |
| `engine.block.setTransition(block=_, transition=_)` | Assign the outgoing transition of a clip. |
| `engine.block.getTransition(block=_)` | Read the assigned transition (invalid block if unset). |
| `engine.block.removeTransition(block=_)` | Detach the outgoing transition of a clip. |
| `engine.block.setDuration(block=_, duration=_)` | Set clip or transition duration in seconds. |
| `engine.block.getTimeOffset(block=_)` | Read a clip's timeline start. |
| `engine.block.setEnum(block=_, property="transition/push/direction", value=_)` | Configure a directional transition. |
| `engine.block.setColor(block=_, property="transition/color-wipe/color", value=_)` | Configure the wipe color. |
| `engine.block.setBoolean(block=_, property="transition/push/morph", value=_)` | Enable morphing between the two clips. |
| `engine.block.findAllProperties(block=_)` | List the properties a transition exposes. |
| `engine.block.getType(block=_)` | Read the type of an assigned transition. |
| `engine.block.isValid(block=_)` | Check a `getTransition` result. |
| `engine.block.destroy(block=_)` | Destroy a detached transition block. |
## Next Steps
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) — Build the clip sequence transitions operate on
- [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Control which portion of a clip plays back
- [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) — Entrance and exit effects for individual blocks
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Control Audio and Video"
description: "Learn to play, pause, seek, and preview audio and video content in CE.SDK using playback controls and solo mode."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/control-daba54/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/)
---
```kotlin file=@cesdk_android_examples/engine-guides-control-av/ControlAudioVideo.kt reference-only
import android.net.Uri
import android.util.Log
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
private const val TAG = "ControlAudioVideo"
private const val SAMPLE_VIDEO_URI = "https://img.ly/static/ubq_video_samples/bbb.mp4"
suspend fun controlAudioVideo(engine: Engine) {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1920F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
val videoBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(videoBlock, value = 1920F)
engine.block.setHeight(videoBlock, value = 1080F)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse(SAMPLE_VIDEO_URI),
)
engine.block.setFill(block = videoBlock, fill = videoFill)
engine.block.appendChild(parent = track, child = videoBlock)
engine.block.setDuration(videoBlock, duration = 10.0)
engine.block.forceLoadAVResource(videoFill)
val videoWidth = engine.block.getVideoWidth(videoFill)
val videoHeight = engine.block.getVideoHeight(videoFill)
val totalDuration = engine.block.getAVResourceTotalDuration(videoFill)
Log.i(TAG, "Video dimensions: ${videoWidth}x$videoHeight")
Log.i(TAG, "Total duration: ${totalDuration}s")
if (engine.block.supportsPlaybackTime(page)) {
engine.block.setPlaying(block = page, enabled = true)
Log.i(TAG, "Is playing: ${engine.block.isPlaying(page)}")
engine.block.setPlaying(block = page, enabled = false)
Log.i(TAG, "Is playing after pause: ${engine.block.isPlaying(page)}")
}
if (engine.block.supportsPlaybackTime(page)) {
engine.block.setPlaybackTime(block = page, time = 1.0)
Log.i(TAG, "Playback time: ${engine.block.getPlaybackTime(page)}s")
}
Log.i(
TAG,
"Visible at current time: ${engine.block.isVisibleAtCurrentPlaybackTime(videoBlock)}",
)
if (engine.block.supportsPlaybackTime(videoFill)) {
engine.block.setSoloPlaybackEnabled(block = videoFill, enabled = true)
Log.i(TAG, "Solo enabled: ${engine.block.isSoloPlaybackEnabled(videoFill)}")
engine.block.setSoloPlaybackEnabled(block = videoFill, enabled = false)
}
}
```
Play, pause, seek, and preview audio and video content programmatically using CE.SDK's playback control APIs.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-control-av)
CE.SDK provides playback control for audio and video through the Block API. Playback state, seeking, and solo preview are controlled programmatically. Resources must be loaded before accessing metadata like duration and dimensions.
This guide covers how to play and pause media, seek to specific positions, preview individual blocks with solo mode, check visibility at playback time, and access video resource metadata.
## Force Loading Resources
Media resource metadata is unavailable until the resource is loaded. Call `forceLoadAVResource()` on a video fill or audio block before reading duration, dimensions, or trim values.
```kotlin highlight-android-force-load
engine.block.forceLoadAVResource(videoFill)
```
Without loading the resource first, accessing properties like duration, dimensions, or trim values throws an error.
## Getting Video Metadata
Once the resource is loaded, query the video dimensions and total source duration.
```kotlin highlight-android-get-metadata
val videoWidth = engine.block.getVideoWidth(videoFill)
val videoHeight = engine.block.getVideoHeight(videoFill)
val totalDuration = engine.block.getAVResourceTotalDuration(videoFill)
Log.i(TAG, "Video dimensions: ${videoWidth}x$videoHeight")
Log.i(TAG, "Total duration: ${totalDuration}s")
```
`getVideoWidth()` and `getVideoHeight()` return the original video dimensions in pixels. `getAVResourceTotalDuration()` returns the full duration of the source media in seconds.
## Playing and Pausing
Check if the block supports playback time using `supportsPlaybackTime()`, then start or stop playback with `setPlaying()`.
```kotlin highlight-android-playback-control
if (engine.block.supportsPlaybackTime(page)) {
engine.block.setPlaying(block = page, enabled = true)
Log.i(TAG, "Is playing: ${engine.block.isPlaying(page)}")
engine.block.setPlaying(block = page, enabled = false)
Log.i(TAG, "Is playing after pause: ${engine.block.isPlaying(page)}")
}
```
`isPlaying()` returns the current playback state for the same block.
## Seeking
To jump to a specific playback position, use `setPlaybackTime()`. First, check if the block supports playback time with `supportsPlaybackTime()`.
```kotlin highlight-android-seeking
if (engine.block.supportsPlaybackTime(page)) {
engine.block.setPlaybackTime(block = page, time = 1.0)
Log.i(TAG, "Playback time: ${engine.block.getPlaybackTime(page)}s")
}
```
Playback time is specified in seconds. `getPlaybackTime()` returns the current position.
## Visibility at Current Time
Check if a block is visible at the current playback position using `isVisibleAtCurrentPlaybackTime()`. This is useful when blocks have different time offsets or durations.
```kotlin highlight-android-visibility
Log.i(
TAG,
"Visible at current time: ${engine.block.isVisibleAtCurrentPlaybackTime(videoBlock)}",
)
```
## Solo Playback
Solo playback allows you to preview an individual video fill or audio block while the rest of the scene stays frozen. Check `supportsPlaybackTime()` before changing the solo playback state.
```kotlin highlight-android-solo-playback
if (engine.block.supportsPlaybackTime(videoFill)) {
engine.block.setSoloPlaybackEnabled(block = videoFill, enabled = true)
Log.i(TAG, "Solo enabled: ${engine.block.isSoloPlaybackEnabled(videoFill)}")
engine.block.setSoloPlaybackEnabled(block = videoFill, enabled = false)
}
```
Enabling solo on one block automatically disables it on all others. Disable solo playback again when returning to full-scene playback.
## Troubleshooting
### Properties Unavailable Before Resource Load
**Symptom**: Accessing duration, dimensions, or trim values throws an error.
**Cause**: Media resource not yet loaded.
**Solution**: Always call `engine.block.forceLoadAVResource()` before accessing these properties.
### Block Not Playing
**Symptom**: Calling `setPlaying(true)` has no effect.
**Cause**: The block does not support playback time, or the scene is not in active playback.
**Solution**: Check that `supportsPlaybackTime()` returns `true` before setting playback state.
### Solo Playback Not Working
**Symptom**: Enabling solo does not isolate the block.
**Cause**: Solo playback was applied to an unsupported block type or to a block that is not visible at the current playback time.
**Solution**: Apply solo playback to a video fill or audio block and ensure the block is active at the current playback time.
## API Reference
| Method | Category | Purpose |
| --- | --- | --- |
| `engine.block.setPlaying(block=_, enabled=_)` | Playback | Enable or disable block playback |
| `engine.block.isPlaying(block=_)` | Playback | Check if a block is playing |
| `engine.block.setSoloPlaybackEnabled(block=_, enabled=_)` | Playback | Enable or disable solo playback mode |
| `engine.block.isSoloPlaybackEnabled(block=_)` | Playback | Check if solo playback is enabled |
| `engine.block.supportsPlaybackTime(block=_)` | Playback | Check support for play/pause, solo playback, and seeking |
| `engine.block.setPlaybackTime(block=_, time=_)` | Seeking | Set the current playback position in seconds |
| `engine.block.getPlaybackTime(block=_)` | Seeking | Get the current playback position in seconds |
| `engine.block.isVisibleAtCurrentPlaybackTime(block=_)` | Visibility | Check if a block is visible at the current time |
| `engine.block.supportsPlaybackControl(block=_)` | Support | Check support for looping, muting, volume, and playback speed |
| `engine.block.forceLoadAVResource(block=_)` | Resource | Load audio or video resource metadata |
| `engine.block.getAVResourceTotalDuration(block=_)` | Resource | Get source media duration in seconds |
| `engine.block.getVideoWidth(videoFill=_)` | Resource | Get video width in pixels |
| `engine.block.getVideoHeight(videoFill=_)` | Resource | Get video height in pixels |
## Next Steps
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Create seamless repeating audio playback for background music and sound effects using CE.SDK's audio looping system.
- [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Learn how to adjust audio volume in CE.SDK to control playback levels, mute audio, and balance multiple audio sources in video projects.
- [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-speed-908d57/) - Learn how to adjust audio playback speed in CE.SDK to create slow-motion, time-stretched, and fast-forward audio effects.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Video Limitations"
description: "Understand resolution limits, duration constraints, codec support, and device-specific restrictions when working with video in CE.SDK for Android."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/limitations-6a740d/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Limitations](https://img.ly/docs/cesdk/android/create-video/limitations-6a740d/)
---
CE.SDK processes video on the Android device, so playback, editing, and export
performance depend on the available memory, GPU, and Android media codecs.
This reference helps you plan video workflows that stay within those device
limits.
Plan around the native device capabilities that vary across Android phones and
tablets: codec availability, maximum texture size, available memory, and the
amount of work required for the chosen resolution, frame rate, and duration.
## Resolution Limits
CE.SDK supports up to 4K UHD playback and export on capable Android hardware.
Higher resolutions need more GPU memory and processing time, so validate export
presets on the device classes your app supports.
Use `engine.editor.getMaxExportSize()` before exporting large videos. The value
is the maximum supported dimension in pixels for both width and height. When
Android returns `Int.MAX_VALUE`, the limit is unknown, not unlimited. Apply your
own conservative caps instead of comparing requested dimensions against that
sentinel.
## Duration Limits
Video duration affects editing responsiveness, memory use, and export time.
CE.SDK is optimized for short-form content while still allowing longer videos
when the device has enough resources.
- Stories and reels up to 2 minutes are the recommended target for smooth
editing.
- Videos up to 10 minutes can work well on modern Android devices, with longer
export times.
- Longer videos are possible but should be tested on representative devices
before shipping.
For long-form workflows, split content into shorter scenes or segments, reduce
preview resolution where possible, and keep export progress visible in your UI.
## Frame Rate Support
Frame rate affects preview playback, rendering, and encoding. 30 FPS at 1080p is
the safest baseline for broad Android support.
60 FPS exports and high-resolution combinations depend on hardware acceleration
and encoder support. When you expose advanced export presets, pair frame rate
choices with conservative resolution and bitrate defaults so older devices can
still complete the export.
Variable frame rate source videos can introduce timing precision issues. For
predictable edits and exports, transcode variable frame rate footage to constant
frame rate before importing it.
## Supported Codecs
Android codec support is provided through the device media stack. A format can
be listed as supported by CE.SDK and still fail on a specific device if the
decoder or encoder is missing or cannot handle the requested resolution.
### Video Codecs
H.264/AVC in `.mp4` containers is the most reliable choice for Android import,
playback, and export.
H.265/HEVC in `.mp4` or `.mov` containers is device-dependent. Use it only after
testing on the Android devices you target, and provide an H.264 fallback when
you cannot control the source media.
### Audio Codecs
MP3 is supported as a standalone `.mp3` file or inside supported video
containers.
AAC is supported in `.m4a`, `.mp4`, or `.mov` containers. For exported MP4
videos, the default `ExportVideoOptions.audioBitrate = 0` lets CE.SDK choose 128
kbps for stereo AAC. Raise the audio bitrate only after testing the target
device class.
## Runtime Restrictions
The restrictions to plan for are:
- Device-specific encoder and decoder availability through Android media codecs
- GPU texture size limits that cap maximum export dimensions
- Memory pressure from high-resolution video, multiple tracks, effects, and
overlays
- Thermal throttling or battery-saving modes that can slow long exports
## Hardware Requirements
Device capabilities directly affect video editing performance. Recent Android
phones and tablets provide the best results, especially for 4K, 60 FPS, or
multi-track projects.
### Recommended Hardware
Use phones and tablets released in the last 4 years as the practical baseline
for smooth video editing and export.
### GPU Considerations
Hardware acceleration improves decoding, rendering, and encoding performance.
The most visible gains appear when exporting high-resolution video, high frame
rates, or scenes with multiple video layers.
Integrated mobile GPUs can handle common short-form editing workflows. For
heavier compositions, reduce the target resolution, frame rate, or number of
simultaneous video tracks.
## Memory Constraints
Video processing consumes device memory. Large source files, multiple tracks,
effects, and high export dimensions all increase memory pressure.
Use `engine.editor.getUsedMemory()` and `engine.editor.getAvailableMemory()` as
coarse telemetry while profiling video workflows. On Android,
`getUsedMemory()` is based on the app process memory reported by the system, and
`getAvailableMemory()` reports currently available system memory. The engine
marks both values as testing and debugging signals whose results can be
unreliable, so treat them as heuristics instead of a hard preload or export
gate.
## Export Size Limitations
Export dimensions are bounded by the current device's rendering and encoding
capabilities. Always query `engine.editor.getMaxExportSize()` before offering or
starting large exports.
Both `targetWidth` and `targetHeight` must stay at or below the reported finite
limit. If the value is `Int.MAX_VALUE`, treat the limit as unknown and use
app-defined presets such as a 1080p baseline and 4K only for tested device
classes. An export can still fail for memory or codec reasons, so combine the
size check with conservative defaults for frame rate and bitrate.
## Troubleshooting
Common Android video limitation issues:
| Issue | Cause | Solution |
| ------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Export size is rejected | Requested width or height exceeds the device limit | Query `getMaxExportSize()` and reduce `targetWidth` or `targetHeight` |
| Export fails on a specific device | The Android encoder cannot handle the requested codec, resolution, frame rate, or bitrate | Use H.264 MP4, lower the preset, and test on that device class |
| Playback stutters at high resolution | The device cannot decode or render the source fast enough | Use lower-resolution previews or reduce the number of simultaneous video tracks |
| Large videos cause memory pressure | Source files, overlays, or effects exceed available memory | Shorten the video, split it into segments, or lower source and export resolution |
| HEVC media does not import or play | The device lacks compatible HEVC decoder support | Prefer H.264 MP4 sources or provide an H.264 fallback |
| Long exports are slow | Resolution, frame rate, duration, and effects exceed the device's comfortable workload | Use shorter segments, lower export presets, and display export progress |
## API Reference
| Method | Description |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `engine.editor.getMaxExportSize()` | Returns the current device's finite maximum export dimension in pixels, or `Int.MAX_VALUE` when Android cannot report a limit. |
| `engine.editor.getAvailableMemory()` | Returns coarse available system memory in bytes for testing, debugging, and telemetry. |
| `engine.editor.getUsedMemory()` | Returns coarse app process memory usage in bytes for testing, debugging, and telemetry. |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_)` | Exports a page timeline to video and reports rendering and encoding progress. |
## Next Steps
Explore related guides to build complete Android video workflows:
- [Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/) — Understand and configure limits on exported file dimensions or data size
- [Video Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) — Fundamentals of editing video with CE.SDK
- [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) — Detailed compatibility matrix for
images, videos, and audio
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Fundamentals of exporting from CE.SDK
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Lock Design"
description: "Protect video designs from unwanted modifications using CE.SDK's scope-based permission system."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/lock-design-e92ce4/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Lock Design](https://img.ly/docs/cesdk/android/create-video/lock-design-e92ce4/)
---
```kotlin file=@cesdk_android_examples/engine-guides-lock-video-design/LockVideoDesign.kt reference-only
import android.app.Application
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
suspend fun lockVideoDesign(
application: Application,
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = withContext(Dispatchers.Main) {
Engine.init(application)
val engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1280, height = 720)
try {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 12.0)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
val videoClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(videoClip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setDuration(videoClip, duration = 12.0)
val videoFill = engine.block.createFill(FillType.Video)
// Video source URIs are currently set through the fill property key.
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4",
),
)
engine.block.setFill(videoClip, fill = videoFill)
engine.block.appendChild(parent = track, child = videoClip)
engine.block.fillParent(track)
val titleOverlay = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = titleOverlay)
engine.block.setWidthMode(titleOverlay, mode = SizeMode.AUTO)
engine.block.setHeightMode(titleOverlay, mode = SizeMode.AUTO)
engine.block.setPositionX(titleOverlay, value = 80F)
engine.block.setPositionY(titleOverlay, value = 80F)
engine.block.setDuration(titleOverlay, duration = 12.0)
engine.block.replaceText(titleOverlay, text = "Editable title")
val watermarkOverlay = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = watermarkOverlay)
engine.block.setWidthMode(watermarkOverlay, mode = SizeMode.AUTO)
engine.block.setHeightMode(watermarkOverlay, mode = SizeMode.AUTO)
engine.block.setPositionX(watermarkOverlay, value = 980F)
engine.block.setPositionY(watermarkOverlay, value = 640F)
engine.block.setDuration(watermarkOverlay, duration = 12.0)
engine.block.replaceText(watermarkOverlay, text = "LOCKED")
val scopes = engine.editor.findAllScopes()
scopes.forEach { scope ->
engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY)
}
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(videoClip, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(watermarkOverlay, key = "editor/select", enabled = false)
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(titleOverlay, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "text/character", enabled = true)
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(videoClip, key = "fill/change", enabled = true)
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/rotate", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(titleOverlay, key = "layer/move", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "layer/rotate", enabled = true)
val lockedOverlayScopes = listOf(
"editor/select",
"text/edit",
"text/character",
"fill/change",
"layer/move",
"layer/resize",
"layer/rotate",
"lifecycle/destroy",
)
lockedOverlayScopes.forEach { scope ->
engine.block.setScopeEnabled(watermarkOverlay, key = scope, enabled = false)
}
val canSelectVideoClip = engine.block.isAllowedByScope(videoClip, key = "editor/select")
val canReplaceVideoClip = engine.block.isAllowedByScope(videoClip, key = "fill/change")
val canMoveVideoClip = engine.block.isAllowedByScope(videoClip, key = "layer/move")
val canEditTitle = engine.block.isAllowedByScope(titleOverlay, key = "text/edit")
val canMoveTitle = engine.block.isAllowedByScope(titleOverlay, key = "layer/move")
val canSelectWatermark = engine.block.isAllowedByScope(watermarkOverlay, key = "editor/select")
val titleTextScopeEnabled = engine.block.isScopeEnabled(titleOverlay, key = "text/edit")
val textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit")
require(canSelectVideoClip)
require(canReplaceVideoClip)
require(!canMoveVideoClip)
require(canEditTitle)
require(canMoveTitle)
require(!canSelectWatermark)
require(titleTextScopeEnabled)
require(textEditGlobalScope == GlobalScope.DEFER)
val availableScopes = engine.editor.findAllScopes()
val currentScopeSettings = availableScopes.associateWith { scope ->
engine.editor.getGlobalScope(key = scope)
}
require("editor/select" in availableScopes)
require("fill/change" in availableScopes)
require(currentScopeSettings["editor/select"] == GlobalScope.DEFER)
} finally {
engine.stop()
}
}
```
Protect video clips, overlays, and placeholders from unwanted edits using CE.SDK's scope-based permission system.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-lock-video-design)
CE.SDK uses global scopes and block-level scopes to decide which editing operations are allowed. For video designs, the same model can lock the whole scene, keep watermarks protected, and make only selected clips or overlays editable.
The backing sample creates a small headless video scene with one clip, one editable title overlay, and one locked watermark. Those blocks provide context for the scope calls below.
## Understanding Scope Permissions
Scopes control what operations users can perform on video clips, text overlays, watermarks, and other design blocks. CE.SDK combines global scope settings with block-level settings to determine the effective permission.
| Global Scope | Block Scope | Result |
| ------------------- | ----------- | --------- |
| `GlobalScope.ALLOW` | any | Permitted |
| `GlobalScope.DENY` | any | Blocked |
| `GlobalScope.DEFER` | enabled | Permitted |
| `GlobalScope.DEFER` | disabled | Blocked |
Global scopes have three possible values:
- **`GlobalScope.ALLOW`**: The operation is always permitted, regardless of block-level settings
- **`GlobalScope.DENY`**: The operation is always blocked, regardless of block-level settings
- **`GlobalScope.DEFER`**: The permission depends on the block-level scope setting
Block-level scopes are binary. They only take effect when the matching global scope is set to `GlobalScope.DEFER`.
## Lock the Entire Video Design
To lock all editing operations, discover the current scope keys with `engine.editor.findAllScopes()` and set each global scope to `GlobalScope.DENY`.
```kotlin highlight-android-lock-video-design
val scopes = engine.editor.findAllScopes()
scopes.forEach { scope ->
engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY)
}
```
When all scopes are denied, users cannot select, move, edit text, replace fills, or delete blocks. This also prevents changes to video clips and overlays until you explicitly defer a scope.
## Enable Selection for Editable Video Blocks
Before users can interact with any block, enable `editor/select`. Setting the global scope to `GlobalScope.DEFER` delegates the decision to each block, so only selected clips or overlays become interactive.
```kotlin highlight-android-enable-selection
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(videoClip, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(watermarkOverlay, key = "editor/select", enabled = false)
```
## Selective Video Locking Patterns
Lock everything first, then selectively enable the capabilities that each video block needs. This keeps the default state restrictive while allowing controlled editing.
### Text Overlay Editing
Enable `text/edit` for text changes and `text/character` when users should also adjust text styling. The sample applies both scopes only to the title overlay.
```kotlin highlight-android-text-overlay-editing
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(titleOverlay, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "text/character", enabled = true)
```
The title text can now be edited while unrelated layout, fill, and lifecycle operations stay locked unless another section enables them.
### Video Clip Replacement
Enable `fill/change` on a video placeholder when users may replace the media source but should not change layout. The sample keeps movement, resize, and rotation denied on the video clip.
```kotlin highlight-android-video-replacement
engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(videoClip, key = "fill/change", enabled = true)
```
### Overlay Layout Adjustments
Enable layout scopes only for blocks that users may reposition. The sample allows moving, resizing, and rotating the title overlay while keeping the video clip layout fixed.
```kotlin highlight-android-layout-adjustments
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/rotate", globalScope = GlobalScope.DEFER)
engine.block.setScopeEnabled(titleOverlay, key = "layer/move", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "layer/resize", enabled = true)
engine.block.setScopeEnabled(titleOverlay, key = "layer/rotate", enabled = true)
```
### Protected Overlays
Keep scopes disabled for watermarks, legal text, brand marks, or other protected overlays. Explicitly disabling the relevant block-level scopes makes the intent clear when the matching global scopes are deferred elsewhere.
```kotlin highlight-android-protect-overlay
val lockedOverlayScopes = listOf(
"editor/select",
"text/edit",
"text/character",
"fill/change",
"layer/move",
"layer/resize",
"layer/rotate",
"lifecycle/destroy",
)
lockedOverlayScopes.forEach { scope ->
engine.block.setScopeEnabled(watermarkOverlay, key = scope, enabled = false)
}
```
## Check Effective Permissions
Use `engine.block.isAllowedByScope()` to verify what the current scope configuration actually permits. This method evaluates both global and block-level settings.
```kotlin highlight-android-check-permissions
val canSelectVideoClip = engine.block.isAllowedByScope(videoClip, key = "editor/select")
val canReplaceVideoClip = engine.block.isAllowedByScope(videoClip, key = "fill/change")
val canMoveVideoClip = engine.block.isAllowedByScope(videoClip, key = "layer/move")
val canEditTitle = engine.block.isAllowedByScope(titleOverlay, key = "text/edit")
val canMoveTitle = engine.block.isAllowedByScope(titleOverlay, key = "layer/move")
val canSelectWatermark = engine.block.isAllowedByScope(watermarkOverlay, key = "editor/select")
val titleTextScopeEnabled = engine.block.isScopeEnabled(titleOverlay, key = "text/edit")
val textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit")
require(canSelectVideoClip)
require(canReplaceVideoClip)
require(!canMoveVideoClip)
require(canEditTitle)
require(canMoveTitle)
require(!canSelectWatermark)
require(titleTextScopeEnabled)
require(textEditGlobalScope == GlobalScope.DEFER)
```
The distinction between checking methods is:
- `isAllowedByScope()` returns the **effective permission** after evaluating both levels
- `isScopeEnabled()` returns only the **block-level setting**
- `getGlobalScope()` returns only the **global setting**
## Discover Available Scopes
Use `engine.editor.findAllScopes()` instead of hardcoding a complete scope list. This keeps locking code aligned with the scopes available in the current engine.
```kotlin highlight-android-discover-scopes
val availableScopes = engine.editor.findAllScopes()
val currentScopeSettings = availableScopes.associateWith { scope ->
engine.editor.getGlobalScope(key = scope)
}
require("editor/select" in availableScopes)
require("fill/change" in availableScopes)
require(currentScopeSettings["editor/select"] == GlobalScope.DEFER)
```
## Available Scopes Reference
| Scope | Description |
| ------------------------ | ---------------------------------------- |
| `layer/move` | Move block position |
| `layer/resize` | Resize block dimensions |
| `layer/rotate` | Rotate block |
| `layer/flip` | Flip block horizontally or vertically |
| `layer/crop` | Crop block content |
| `layer/opacity` | Change block opacity |
| `layer/blendMode` | Change blend mode |
| `layer/visibility` | Toggle block visibility |
| `layer/clipping` | Change clipping behavior |
| `fill/change` | Change fill content or text color |
| `fill/changeType` | Change fill type |
| `stroke/change` | Change stroke properties |
| `shape/change` | Change shape type |
| `text/edit` | Edit text content |
| `text/character` | Change text styling such as font or size |
| `appearance/adjustments` | Change color adjustments |
| `appearance/filter` | Apply or change filters |
| `appearance/effect` | Apply or change effects |
| `appearance/blur` | Apply or change blur |
| `appearance/shadow` | Apply or change shadows |
| `appearance/animation` | Apply or change animations |
| `lifecycle/destroy` | Delete the block |
| `lifecycle/duplicate` | Duplicate the block |
| `editor/add` | Add new blocks |
| `editor/select` | Select blocks |
## Troubleshooting
| Issue | Cause | Solution |
| ---------------------------------- | --------------------------------------- | ----------------------------------------------------------- |
| Block is still editable | The global scope is `GlobalScope.ALLOW` | Set the global scope to `GlobalScope.DENY` or `GlobalScope.DEFER` |
| Block is unexpectedly locked | The global scope is `GlobalScope.DENY` | Set the global scope to `GlobalScope.DEFER` and enable the block-level scope |
| Users cannot select a block | `editor/select` is still locked | Enable `editor/select` for blocks users should select |
| Permission check returns `false` | The code checks the wrong scope level | Use `isAllowedByScope()` for the effective permission |
| New scopes are not locked | The code uses a hardcoded scope list | Use `findAllScopes()` to discover scopes dynamically |
## API Reference
| Method | Purpose |
| ------ | ------- |
| `engine.editor.findAllScopes()` | Get all available scope names |
| `engine.editor.setGlobalScope(key=_, globalScope=_)` | Set a global scope to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER` |
| `engine.editor.getGlobalScope(key=_)` | Get the current global setting for one scope |
| `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Enable or disable a scope on one block |
| `engine.block.isScopeEnabled(block=_, key=_)` | Check only the block-level scope setting |
| `engine.block.isAllowedByScope(block=_, key=_)` | Check the effective permission after global and block-level scopes are evaluated |
## Next Steps
- [Lock Content](https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/) - Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system
- [Lock Templates](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Lock templates for consistent reuse
- [Rules Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) - Understand the broader rules system
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Videos Overview"
description: "Learn how Android video projects work in CE.SDK and choose the right guide for UI-based or programmatic video workflows."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/overview-b06512/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/)
---
Understand Android video projects in CE.SDK before choosing an editor UI,
Engine API workflow, or focused implementation guide.
CE.SDK video projects add time-based editing to the same scene and block model
used for static designs. A video scene can contain pages, timeline tracks,
media-backed clips, overlays, captions, audio, and export settings.
Use this overview to decide where your Android integration should start. Start
with the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) when you need an
interactive timeline UI. Use Engine APIs for automation, custom controls,
template-driven output, or server-assisted workflows that prepare scenes before
opening the editor.
## Core Video Concepts
Video scenes are timeline-based scenes. Each page defines an independent
timeline and the output frame size for that timeline. Blocks placed on the page
or inside tracks become active for a duration, and the page playback time
determines which frame is visible.
Scenes own the time-based project and shared resources. Pages define timelines
and frame sizes. Clips are timed blocks, often backed by video media. Tracks
group clips and can sequence them over time. Audio can come from video media or
from standalone audio blocks. Export turns the page timeline into a shareable
video output.
Timeline values are measured in seconds. Duration controls how long a block or
media source is active, time offset controls when it starts in its parent
timeline, and trim values control which part of a media source plays. These
concepts show up across the focused Android video guides.
## UI-Based Editing
Use the CE.SDK editor UI when users need to assemble or adjust videos
interactively. The Android video UI covers common editing tasks such as
arranging clips on a timeline, trimming media, adding overlays, editing
captions, placing watermarks, previewing playback, and balancing audio.
Apps usually customize that UI around their product workflow: available tools,
asset sources, export actions, brand controls, permissions, and app-specific
navigation. Keep those choices in your editor configuration, then use Engine
APIs for any scene preparation or post-processing that should happen before or
after the user edits.
## Programmatic Editing
Use Engine APIs when your app needs deterministic video output or custom
automation. Programmatic workflows can create video scenes, add pages and
tracks, load media, arrange clips, set durations and offsets, adjust trim
ranges, control audio, generate previews, and export finished pages.
For step-by-step code, continue with the focused guides for programmatic
editing, timeline editing, trimming, arranging clips, captions, watermarks, and
export.
## Platform Support and Constraints
Android video workflows run on the device and depend on Android media,
rendering, storage, and codec support. Test the codecs, resolutions, frame
rates, memory usage, and export settings that match your target devices,
especially for long videos, large source files, or high-resolution output.
Some behavior is platform-specific. For example, export options, media loading,
file access, permissions, and available codecs differ between Android, iOS, Web,
and server-side environments. Use the Android-specific implementation guides
when a workflow touches device storage, media URIs, playback performance, or
export configuration.
## Audio in Video Projects
Video projects can include audio embedded in video media and standalone audio
blocks. Common Android workflows include muting audio embedded in video fills,
adding background music, recording or importing voiceover, placing sound
effects on the timeline, and adjusting volume across multiple sources.
Standalone audio blocks use the same timeline concepts as visual blocks:
duration determines how long the audio plays, time offset determines when it
starts, and trim values can select the source-media segment. On Android, include
audio in a rendered video by keeping those audio sources on the page and
exporting the page as video.
## Export and Output
Export turns a page timeline into an output artifact. Android video export uses
Engine video export APIs to render a page range and return encoded video
content. Export options can control details such as frame rate, bitrate, target
dimensions, and H.264 settings.
The right export setup depends on the product: a social video may prioritize
size and speed, while a template workflow may prioritize resolution and
repeatable output. Use dedicated export guides for format, compression,
progress, storage, and URI-resolution details.
## AI and App-Specific Workflows
AI-assisted and app-specific video features fit around the same scene model.
Your app can generate scripts, suggest edits, create captions, prepare assets,
or call external services before applying the result to a CE.SDK scene. Treat
those workflows as integrations unless a focused CE.SDK guide documents them as
built-in platform behavior.
## Next Steps
- [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) - Start from the Android timeline
UI for interactive editing.
- [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) - Create and modify video scenes
with Engine APIs.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Arrange clips, audio, overlays, and
timeline previews.
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine clips into sequences
and organize them on tracks.
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Play, pause, seek, and preview
audio or video content.
- [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Add synchronized captions to Android video
scenes.
- [Add Watermark](https://img.ly/docs/cesdk/android/edit-video/add-watermark-762ce6/) - Add text or image watermarks to exported
videos.
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Render output for sharing or publishing.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Programmatic Creation"
description: "Create and export video scenes entirely through code with the CE.SDK Engine on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/programmatic-2b243c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Programmatic Creation](https://img.ly/docs/cesdk/android/create-video/programmatic-2b243c/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-video-programmatic/CreateVideoProgrammatic.kt reference-only
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportVideoOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
suspend fun createVideoProgrammatic(engine: Engine): File = withContext(Dispatchers.Main) {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
val introClip = createVideoClip(
engine,
Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
val detailClip = createVideoClip(
engine,
Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"),
)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.appendChild(parent = track, child = introClip.block)
engine.block.appendChild(parent = track, child = detailClip.block)
engine.block.fillParent(track)
// Keep the guide export short; use the clip length your app needs.
val sampleClipDurationSeconds = 2.0
engine.block.forceLoadAVResource(introClip.fill)
val introDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(introClip.fill))
check(introDuration > 0.0) { "The intro video must contain playable media." }
engine.block.setDuration(introClip.block, duration = introDuration)
engine.block.forceLoadAVResource(detailClip.fill)
val detailDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(detailClip.fill))
check(detailDuration > 0.0) { "The detail video must contain playable media." }
engine.block.setDuration(detailClip.block, duration = detailDuration)
val pageDuration = introDuration + detailDuration
engine.block.setDuration(page, duration = pageDuration)
val logTag = "CreateVideoGuide"
// Export a compact preview file; use your delivery size and frame rate in production.
val previewExportWidth = 640F
val previewExportHeight = 360F
val previewFrameRate = 15F
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = { progress ->
Log.i(
logTag,
"Rendered ${progress.renderedFrames} frames and encoded ${progress.encodedFrames} " +
"frames out of ${progress.totalFrames} frames",
)
},
options = ExportVideoOptions(
targetWidth = previewExportWidth,
targetHeight = previewExportHeight,
frameRate = previewFrameRate,
),
)
val outputFile = withContext(Dispatchers.IO) {
val outputFile = File.createTempFile("programmatic-video-", ".mp4")
val bytes = ByteArray(videoBytes.remaining())
videoBytes.get(bytes)
outputFile.outputStream().use { output ->
output.write(bytes)
}
outputFile
}
check(outputFile.length() > 0L) { "The exported MP4 file must not be empty." }
outputFile
}
private data class VideoClip(
val block: DesignBlock,
val fill: DesignBlock,
)
private fun createVideoClip(
engine: Engine,
videoUri: Uri,
): VideoClip {
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
// Video fills read their media source from this Engine property key.
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(clip, fill = videoFill)
return VideoClip(block = clip, fill = videoFill)
}
suspend fun createSingleSourceVideoScene(engine: Engine): DesignBlock {
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
val scene = engine.scene.createFromVideo(videoUri)
return scene
}
```
Create a video scene entirely through code, add clips to a track, set
durations, and export the result as an MP4 file.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-video-programmatic)
CE.SDK video scenes can be built without opening the editor UI. This is useful
for automation, template-driven rendering, and app flows that create media from
known inputs.
This guide uses the Android Engine API to create a video scene, arrange clips on
a track, load media metadata, set durations, and export the page as MP4.
Use [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) when you need trim, split,
timed overlay, or other edit recipes after the initial scene is created.
For Engine initialization and offscreen rendering setup, see [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/).
## Create a Video Scene
Create a timeline-enabled scene with `engine.scene.createForVideo()`. A page
holds the video composition and defines the canvas dimensions.
```kotlin highlight-android-create-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
```
For a one-source video scene, use `engine.scene.createFromVideo(videoUri)` as a
shortcut. The main sample uses `createForVideo()` because it shows tracks,
multiple clips, and export timing.
```kotlin highlight-android-create-from-video
suspend fun createSingleSourceVideoScene(engine: Engine): DesignBlock {
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
val scene = engine.scene.createFromVideo(videoUri)
return scene
}
```
## Add Video Clips
Each clip is a graphic block with a rectangular shape and a video fill. The
helper returns both handles because later timing APIs operate on the graphic
block, while media metadata APIs operate on the video fill.
```kotlin highlight-android-create-video-clip-helper
private data class VideoClip(
val block: DesignBlock,
val fill: DesignBlock,
)
private fun createVideoClip(
engine: Engine,
videoUri: Uri,
): VideoClip {
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
// Video fills read their media source from this Engine property key.
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(clip, fill = videoFill)
return VideoClip(block = clip, fill = videoFill)
}
```
Create the clips from source URLs:
```kotlin highlight-android-add-video-clips
val introClip = createVideoClip(
engine,
Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
val detailClip = createVideoClip(
engine,
Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"),
)
```
## Arrange Clips on a Track
Append the clips to a `DesignBlockType.Track` in playback order. The track
sequences its children from their durations, and `fillParent(track)` sizes the
track block to its parent page. For groups and tracks, `fillParent(...)` also
fills their child blocks against the nearest parent that is not a group or
track, so the unsized clip graphics in this sample fill the page frame before
the track is sized.
```kotlin highlight-android-arrange-track
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.appendChild(parent = track, child = introClip.block)
engine.block.appendChild(parent = track, child = detailClip.block)
engine.block.fillParent(track)
```
## Load Media and Set Durations
Load each video resource before reading metadata. Duration values use seconds.
```kotlin highlight-android-load-media-and-timing
// Keep the guide export short; use the clip length your app needs.
val sampleClipDurationSeconds = 2.0
engine.block.forceLoadAVResource(introClip.fill)
val introDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(introClip.fill))
check(introDuration > 0.0) { "The intro video must contain playable media." }
engine.block.setDuration(introClip.block, duration = introDuration)
engine.block.forceLoadAVResource(detailClip.fill)
val detailDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(detailClip.fill))
check(detailDuration > 0.0) { "The detail video must contain playable media." }
engine.block.setDuration(detailClip.block, duration = detailDuration)
val pageDuration = introDuration + detailDuration
engine.block.setDuration(page, duration = pageDuration)
```
The sample derives safe clip durations from the source media duration and sets
the page duration to the combined clip length.
## Export the Video
Export the page with `engine.block.exportVideo(...)`. The sample exports MP4,
reports encoding progress, and uses export options to produce a smaller
verification file while keeping the page at 1280x720.
```kotlin highlight-android-export-video
val logTag = "CreateVideoGuide"
// Export a compact preview file; use your delivery size and frame rate in production.
val previewExportWidth = 640F
val previewExportHeight = 360F
val previewFrameRate = 15F
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = { progress ->
Log.i(
logTag,
"Rendered ${progress.renderedFrames} frames and encoded ${progress.encodedFrames} " +
"frames out of ${progress.totalFrames} frames",
)
},
options = ExportVideoOptions(
targetWidth = previewExportWidth,
targetHeight = previewExportHeight,
frameRate = previewFrameRate,
),
)
```
Write the returned `ByteBuffer` to an MP4 file and check that the export is
non-empty before returning it.
```kotlin highlight-android-write-file
val outputFile = withContext(Dispatchers.IO) {
val outputFile = File.createTempFile("programmatic-video-", ".mp4")
val bytes = ByteArray(videoBytes.remaining())
videoBytes.get(bytes)
outputFile.outputStream().use { output ->
output.write(bytes)
}
outputFile
}
check(outputFile.length() > 0L) { "The exported MP4 file must not be empty." }
```
## API Reference
| API | Category | Purpose |
| --- | --- | --- |
| `engine.scene.createForVideo()` | Scene | Create an empty video scene with timeline support. |
| `engine.scene.createFromVideo(videoUri=_)` | Scene | Create a one-source video scene from a `Uri`. |
| `engine.block.create(blockType=_)` | Block | Create pages, tracks, graphics, and other blocks. |
| `engine.block.createShape(type=_)` | Shape | Create a shape for a graphic block. |
| `engine.block.setShape(block=_, shape=_)` | Shape | Assign a shape to a graphic block. |
| `engine.block.createFill(fillType=_)` | Fill | Create a video fill. |
| `engine.block.setFill(block=_, fill=_)` | Fill | Assign a fill to a block. |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Fill | Set the source URI on a video fill. |
| `engine.block.appendChild(parent=_, child=_)` | Hierarchy | Attach scene, page, track, and clip blocks. |
| `engine.block.fillParent(block=_)` | Layout | Resize and position the passed block to fill its parent; for groups and tracks, fill child blocks against the nearest non-group/non-track parent first. |
| `engine.block.setWidth(block=_, value=_)` | Layout | Set a block width. |
| `engine.block.setHeight(block=_, value=_)` | Layout | Set a block height. |
| `engine.block.setDuration(block=_, duration=_)` | Timing | Set clip or page duration in seconds. |
| `engine.block.getDuration(block=_)` | Timing | Read the page duration passed to the video export. |
| `engine.block.forceLoadAVResource(block=_)` | Media | Load a video fill before reading metadata. |
| `engine.block.getAVResourceTotalDuration(block=_)` | Media | Read the source media duration in seconds. |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Export | Export a page timeline as video bytes. |
## Troubleshooting
- **Engine reference is unavailable**: Initialize CE.SDK and obtain an `Engine`
instance before running the scene creation code; the Engine reference note
above links to the setup flow.
- **Clip not visible**: Verify that the graphic block has a shape and fill, and
that it is appended to a track or page.
- **Source duration is zero**: Call `forceLoadAVResource(...)` on the video
fill before metadata APIs.
- **Export is empty**: Set a positive page duration and pass the same duration
to `exportVideo(...)`.
- **Remote media does not load**: Check that the URL is reachable and that the
device runtime supports the media format.
## Next Steps
- [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) - Understand video scenes and time-based editing
- [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) - Modify timelines with trim, split, and timed overlay recipes
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Build interactive video timelines
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK.
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Configure playback, trim, and resource control
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Export images, videos, and other output formats
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Record Reaction"
description: "Record reactions to a base video and compose them into an editable Android video scene."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/)
---
```kotlin file=@cesdk_android_examples/editor-guides-record-reaction/RecordReaction.kt reference-only
import android.graphics.RectF
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.Composable
import ly.img.camera.core.CameraLayoutMode
import ly.img.camera.core.CameraMode
import ly.img.camera.core.CameraResult
import ly.img.camera.core.CaptureMedia
import ly.img.camera.core.EngineConfiguration
import ly.img.camera.core.Recording
import ly.img.camera.core.Video
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import kotlin.time.DurationUnit
data class ReactionSceneComposition(
val page: DesignBlock,
val baseVideoBlock: DesignBlock,
val reactionTrack: DesignBlock,
val reactionBlocks: List,
val durationSeconds: Double,
)
private fun handleReactionCameraResult(
result: CameraResult?,
onReactionReady: (CameraResult.Reaction) -> Unit,
onDismissed: () -> Unit,
) {
when (result) {
null -> onDismissed()
is CameraResult.Reaction -> onReactionReady(result)
else -> Unit
}
}
@Composable
fun rememberRecordReactionLauncher(
baseVideoUri: Uri,
license: String?,
userId: String?,
onReactionReady: (CameraResult.Reaction) -> Unit,
onDismissed: () -> Unit = {},
): () -> Unit {
val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result ->
handleReactionCameraResult(result, onReactionReady, onDismissed)
}
return {
val input = CaptureMedia.Input(
engineConfiguration = EngineConfiguration(
license = license,
userId = userId,
),
cameraMode = CameraMode.Reaction(
video = baseVideoUri,
cameraLayoutMode = CameraLayoutMode.Vertical,
positionsSwapped = false,
),
)
cameraLauncher.launch(input)
}
}
suspend fun createReactionVideoScene(
engine: Engine,
cameraResult: CameraResult.Reaction,
): ReactionSceneComposition {
val firstReactionVideo = cameraResult.reaction
.firstNotNullOfOrNull { recording -> recording.videos.firstOrNull() }
?: error("Reaction result does not contain a recorded video.")
check(engine.scene.get() == null) { "Call this before loading another scene." }
engine.scene.createFromVideo(cameraResult.video.uri)
val page = checkNotNull(engine.scene.getCurrentPage())
val sceneFrame = RectF(cameraResult.video.rect).apply {
union(firstReactionVideo.rect)
}
setFrame(engine = engine, designBlock = page, rect = sceneFrame)
val baseVideoBlock = engine.block.findByType(DesignBlockType.Graphic).first()
setFrame(engine = engine, designBlock = baseVideoBlock, rect = cameraResult.video.rect)
val reactionTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = reactionTrack)
val baseFill = engine.block.getFill(baseVideoBlock)
engine.block.forceLoadAVResource(baseFill)
val baseDurationSeconds = engine.block.getAVResourceTotalDuration(baseFill)
val reactionBlocks = mutableListOf()
var reactionOffsetSeconds = 0.0
for (recording in cameraResult.reaction) {
val remainingSeconds = baseDurationSeconds - reactionOffsetSeconds
if (remainingSeconds <= 0.0) break
val reactionVideo = recording.videos.firstOrNull() ?: continue
val reactionBlock = addReactionRecording(
engine = engine,
recording = recording,
reactionVideo = reactionVideo,
parent = reactionTrack,
)
val recordingDurationSeconds = recording.duration.toDouble(DurationUnit.SECONDS)
val clipDurationSeconds = minOf(recordingDurationSeconds, remainingSeconds)
if (clipDurationSeconds < recordingDurationSeconds) {
engine.block.setDuration(reactionBlock, duration = clipDurationSeconds)
}
reactionOffsetSeconds += clipDurationSeconds
reactionBlocks += reactionBlock
}
val finalDurationSeconds = minOf(reactionOffsetSeconds, baseDurationSeconds)
engine.block.setTrimOffset(baseFill, offset = 0.0)
engine.block.setTrimLength(baseFill, length = finalDurationSeconds)
engine.block.setDuration(baseVideoBlock, duration = finalDurationSeconds)
return ReactionSceneComposition(
page = page,
baseVideoBlock = baseVideoBlock,
reactionTrack = reactionTrack,
reactionBlocks = reactionBlocks,
durationSeconds = finalDurationSeconds,
)
}
private fun addReactionRecording(
engine: Engine,
recording: Recording,
reactionVideo: Video,
parent: DesignBlock,
): DesignBlock {
val reactionBlock = engine.block.create(DesignBlockType.Graphic)
val shape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = reactionBlock, shape = shape)
setFrame(engine = engine, designBlock = reactionBlock, rect = reactionVideo.rect)
val fill = engine.block.createFill(FillType.Video)
// Point the video fill at the recorded reaction segment.
engine.block.setUri(
block = fill,
property = "fill/video/fileURI",
value = reactionVideo.uri,
)
engine.block.setFill(block = reactionBlock, fill = fill)
engine.block.setDuration(reactionBlock, duration = recording.duration.toDouble(DurationUnit.SECONDS))
engine.block.appendChild(parent = parent, child = reactionBlock)
return reactionBlock
}
private fun setFrame(
engine: Engine,
designBlock: DesignBlock,
rect: RectF,
) {
engine.block.setWidth(block = designBlock, value = rect.width())
engine.block.setHeight(block = designBlock, value = rect.height())
engine.block.setPositionX(block = designBlock, value = rect.left)
engine.block.setPositionY(block = designBlock, value = rect.top)
}
```
Record the user while a base video plays, then compose the base video and the
reaction clips into an editable picture-in-picture video scene.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-record-reaction)
Reaction mode is a camera workflow. Before launching it, add the `implementation "ly.img:camera:$UBQ_VERSION$"` dependency to your application module and complete [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/).
CE.SDK returns the original video and the recorded reaction segments; your app then places those assets in the video editor.
| Android type | Purpose |
| --- | --- |
| `CameraMode.Reaction` | Opens the camera while playing the video the user reacts to. |
| `CameraResult.Reaction` | Returns the base `Video` and one or more reaction `Recording` segments. |
| `Recording` | Stores segment duration and its recorded `Video` entries. |
| `Video` | Stores the recorded URI and the preview `rect` used by the camera layout. |
## Launch Reaction Mode
Create a launcher with the `CaptureVideo` Activity Result contract. The sample receives a base video URI from your app, delegates the camera result to the handler below and starts `CameraMode.Reaction` with a vertical preview layout.
```kotlin highlight-android-launch-reaction
@Composable
fun rememberRecordReactionLauncher(
baseVideoUri: Uri,
license: String?,
userId: String?,
onReactionReady: (CameraResult.Reaction) -> Unit,
onDismissed: () -> Unit = {},
): () -> Unit {
val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result ->
handleReactionCameraResult(result, onReactionReady, onDismissed)
}
return {
val input = CaptureMedia.Input(
engineConfiguration = EngineConfiguration(
license = license,
userId = userId,
),
cameraMode = CameraMode.Reaction(
video = baseVideoUri,
cameraLayoutMode = CameraLayoutMode.Vertical,
positionsSwapped = false,
),
)
cameraLauncher.launch(input)
}
}
```
Use `cameraLayoutMode` to switch between vertical and horizontal previews. Set `positionsSwapped` when the reaction camera should take the base video's preview position.
## Handle the Reaction Result
When the user finishes recording, handle the `CameraResult.Reaction` branch in the Activity Result callback. The `Record` branch is included for exhaustiveness, but a launcher started in Reaction mode should call `onReactionReady` with the reaction result. Pass that result to your editor flow; the following sections use it as `cameraResult`.
```kotlin highlight-android-handle-result
private fun handleReactionCameraResult(
result: CameraResult?,
onReactionReady: (CameraResult.Reaction) -> Unit,
onDismissed: () -> Unit,
) {
when (result) {
null -> onDismissed()
is CameraResult.Reaction -> onReactionReady(result)
else -> Unit
}
}
```
The result contains `video`, which is the base video, and `reaction`, which is the list of recorded reaction segments. A segment list can contain multiple entries when the user pauses and resumes recording.
## Preserve Preview Rects
The camera stores each preview position as an Android `RectF`. Pass the `Engine`, target block and rect to the helper, then map the rect to CE.SDK block size and position to preserve the camera preview layout in the editor.
```kotlin highlight-android-rect-frame
private fun setFrame(
engine: Engine,
designBlock: DesignBlock,
rect: RectF,
) {
engine.block.setWidth(block = designBlock, value = rect.width())
engine.block.setHeight(block = designBlock, value = rect.height())
engine.block.setPositionX(block = designBlock, value = rect.left)
engine.block.setPositionY(block = designBlock, value = rect.top)
}
```
## Build the Editable Video Scene
Use the editor `Engine` to create a video scene from the base video URI. The sample reads the first reaction video used for the page bounds, sizes the page to include both preview rectangles, positions the base video block at its recorded rect and creates a separate track for reaction clips.
```kotlin highlight-android-build-scene
val firstReactionVideo = cameraResult.reaction
.firstNotNullOfOrNull { recording -> recording.videos.firstOrNull() }
?: error("Reaction result does not contain a recorded video.")
check(engine.scene.get() == null) { "Call this before loading another scene." }
engine.scene.createFromVideo(cameraResult.video.uri)
val page = checkNotNull(engine.scene.getCurrentPage())
val sceneFrame = RectF(cameraResult.video.rect).apply {
union(firstReactionVideo.rect)
}
setFrame(engine = engine, designBlock = page, rect = sceneFrame)
val baseVideoBlock = engine.block.findByType(DesignBlockType.Graphic).first()
setFrame(engine = engine, designBlock = baseVideoBlock, rect = cameraResult.video.rect)
val reactionTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = reactionTrack)
```
Call this after the camera returns and before loading another scene.
## Add Reaction Clips
Each reaction segment becomes a graphic block with a rectangle shape and a video fill. The helper receives the `Engine`, `Recording`, reaction `Video` and parent track explicitly so copied code has all required inputs.
```kotlin highlight-android-add-reaction-clips
private fun addReactionRecording(
engine: Engine,
recording: Recording,
reactionVideo: Video,
parent: DesignBlock,
): DesignBlock {
val reactionBlock = engine.block.create(DesignBlockType.Graphic)
val shape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = reactionBlock, shape = shape)
setFrame(engine = engine, designBlock = reactionBlock, rect = reactionVideo.rect)
val fill = engine.block.createFill(FillType.Video)
// Point the video fill at the recorded reaction segment.
engine.block.setUri(
block = fill,
property = "fill/video/fileURI",
value = reactionVideo.uri,
)
engine.block.setFill(block = reactionBlock, fill = fill)
engine.block.setDuration(reactionBlock, duration = recording.duration.toDouble(DurationUnit.SECONDS))
engine.block.appendChild(parent = parent, child = reactionBlock)
return reactionBlock
}
```
Android has no typed public binding for this video fill URI key today, so the sample passes `"fill/video/fileURI"` to `engine.block.setUri(...)` and keeps the value URI-typed.
## Keep Timing Synchronized
Force-load the base video fill before reading its duration. The sample trims any reaction segment that would exceed the base video, then applies the final duration to the base fill and base video block.
```kotlin highlight-android-sync-duration
val baseFill = engine.block.getFill(baseVideoBlock)
engine.block.forceLoadAVResource(baseFill)
val baseDurationSeconds = engine.block.getAVResourceTotalDuration(baseFill)
val reactionBlocks = mutableListOf()
var reactionOffsetSeconds = 0.0
for (recording in cameraResult.reaction) {
val remainingSeconds = baseDurationSeconds - reactionOffsetSeconds
if (remainingSeconds <= 0.0) break
val reactionVideo = recording.videos.firstOrNull() ?: continue
val reactionBlock = addReactionRecording(
engine = engine,
recording = recording,
reactionVideo = reactionVideo,
parent = reactionTrack,
)
val recordingDurationSeconds = recording.duration.toDouble(DurationUnit.SECONDS)
val clipDurationSeconds = minOf(recordingDurationSeconds, remainingSeconds)
if (clipDurationSeconds < recordingDurationSeconds) {
engine.block.setDuration(reactionBlock, duration = clipDurationSeconds)
}
reactionOffsetSeconds += clipDurationSeconds
reactionBlocks += reactionBlock
}
val finalDurationSeconds = minOf(reactionOffsetSeconds, baseDurationSeconds)
engine.block.setTrimOffset(baseFill, offset = 0.0)
engine.block.setTrimLength(baseFill, length = finalDurationSeconds)
engine.block.setDuration(baseVideoBlock, duration = finalDurationSeconds)
```
This keeps the final composition no longer than the video the user reacted to. Tracks place the reaction segments one after another based on the block durations.
## Persist Reaction Files
The base video URI is the URI your app passed into Reaction mode. Reaction clip URIs are files created by the camera in app-local storage, so copy those reaction files to your app's long-term storage if you need them after the current editing workflow.
For lower-level access to durations, URIs and rects, see the [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) guide.
## Next Steps
- [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) - Add CE.SDK camera capture to your Android app.
- [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) - Lock camera modes and configure capture behavior.
- [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) - Inspect recorded durations, URIs and preview rects.
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Arrange video clips, audio and timeline content in the editor.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Timeline Editor"
description: "Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/)
---
```kotlin file=@cesdk_android_examples/engine-guides-timeline-editor/TimelineEditor.kt reference-only
import android.net.Uri
import kotlinx.coroutines.flow.toList
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
data class TimelineEditor(
val pageDuration: Double,
val primaryClipDuration: Double,
val overlayStartTime: Double,
val videoThumbnailCount: Int,
val audioWaveformChunkCount: Int,
val exportedVideoDuration: Double,
val exportedVideoBytes: Int,
)
suspend fun timelineEditor(engine: Engine): TimelineEditor {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 10.0)
val primaryTrack = engine.block.create(DesignBlockType.Track)
val overlayTrack = engine.block.create(DesignBlockType.Track)
val audioTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = primaryTrack)
engine.block.appendChild(parent = page, child = overlayTrack)
engine.block.appendChild(parent = page, child = audioTrack)
// No type-safe Android helper exists for this track property yet.
engine.block.setBoolean(
block = overlayTrack,
property = "track/automaticallyManageBlockOffsets",
value = false,
)
val primaryClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(primaryClip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(primaryClip, value = 0F)
engine.block.setPositionY(primaryClip, value = 0F)
engine.block.setWidth(primaryClip, value = 1280F)
engine.block.setHeight(primaryClip, value = 720F)
val primaryFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = primaryFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
engine.block.setFill(primaryClip, fill = primaryFill)
engine.block.appendChild(parent = primaryTrack, child = primaryClip)
val overlayClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(overlayClip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(overlayClip, value = 820F)
engine.block.setPositionY(overlayClip, value = 80F)
engine.block.setWidth(overlayClip, value = 360F)
engine.block.setHeight(overlayClip, value = 220F)
val overlayFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = overlayFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4",
),
)
engine.block.setFill(overlayClip, fill = overlayFill)
engine.block.appendChild(parent = overlayTrack, child = overlayClip)
val audioClip = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = audioClip,
property = "audio/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",
),
)
engine.block.appendChild(parent = audioTrack, child = audioClip)
engine.block.forceLoadAVResource(primaryFill)
engine.block.forceLoadAVResource(overlayFill)
engine.block.forceLoadAVResource(audioClip)
engine.block.setDuration(primaryClip, duration = 8.0)
engine.block.setTrimOffset(primaryFill, offset = 2.0)
engine.block.setTrimLength(primaryFill, length = 8.0)
engine.block.setLooping(primaryFill, looping = false)
engine.block.setMuted(primaryFill, muted = true)
engine.block.setTimeOffset(overlayClip, offset = 3.0)
engine.block.setDuration(overlayClip, duration = 4.0)
engine.block.setTimeOffset(audioClip, offset = 0.0)
engine.block.setDuration(audioClip, duration = 10.0)
engine.block.setPlaybackTime(page, time = 3.5)
check(engine.block.isVisibleAtCurrentPlaybackTime(overlayClip))
engine.block.setPlaying(page, enabled = true)
check(engine.block.isPlaying(page))
engine.block.setPlaying(page, enabled = false)
val videoThumbnails = engine.block.generateVideoThumbnailSequence(
block = primaryFill,
thumbnailHeight = 72,
timeBegin = 0.0,
timeEnd = 8.0,
numberOfFrames = 4,
).toList()
val audioWaveformChunks = engine.block.generateAudioThumbnailSequence(
block = audioClip,
samplesPerChunk = 40,
timeBegin = 0.0,
timeEnd = 10.0,
numberOfSamples = 160,
numberOfChannels = 2,
).toList()
val exportDuration = engine.block.getDuration(page)
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = exportDuration,
mimeType = MimeType.MP4,
progressCallback = { progress ->
println("Encoded ${progress.encodedFrames} of ${progress.totalFrames} frames")
},
)
return TimelineEditor(
pageDuration = engine.block.getDuration(page),
primaryClipDuration = engine.block.getDuration(primaryClip),
overlayStartTime = engine.block.getTimeOffset(overlayClip),
videoThumbnailCount = videoThumbnails.size,
audioWaveformChunkCount = audioWaveformChunks.size,
exportedVideoDuration = exportDuration,
exportedVideoBytes = videoBytes.remaining(),
)
}
```
Build Android video timelines with CE.SDK by arranging tracks, clips, trim ranges, playback controls, thumbnails, and MP4 export from Kotlin.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-timeline-editor)
The Android [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) already renders the built-in `Timeline` component in its bottom panel. Use the Engine APIs in this guide when you need to prepare a video scene programmatically, build a custom timeline surface, or automate timeline edits before opening or exporting the scene.
## Timeline Hierarchy
CE.SDK represents video timelines through the same block hierarchy that the editor UI renders:
```text
Scene
└── Page
├── Track
│ ├── Clip
│ └── Clip
├── Overlay track
└── Audio track
```
Use a video scene for time-based playback. A page defines the composition duration, tracks group parallel lanes, and each clip controls its own duration, trim range, and time offset.
## Create a Video Scene
Start with `engine.scene.createForVideo()`, then add a page with the final frame size and duration. The page is the block you play, scrub, and export.
```kotlin highlight-android-create-video-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 10.0)
```
## Create Tracks
Tracks organize clips into timeline lanes. The primary track can keep automatic offset management so clips play one after another, while overlay tracks often disable automatic offsets so you can position clips freely in time.
```kotlin highlight-android-create-tracks
val primaryTrack = engine.block.create(DesignBlockType.Track)
val overlayTrack = engine.block.create(DesignBlockType.Track)
val audioTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = primaryTrack)
engine.block.appendChild(parent = page, child = overlayTrack)
engine.block.appendChild(parent = page, child = audioTrack)
// No type-safe Android helper exists for this track property yet.
engine.block.setBoolean(
block = overlayTrack,
property = "track/automaticallyManageBlockOffsets",
value = false,
)
```
## Add Video and Audio Clips
Video clips are graphic blocks with a video fill. Audio clips use `DesignBlockType.Audio` and can live on their own track so the timeline UI can present them as an audio lane.
```kotlin highlight-android-add-video-clips
val primaryClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(primaryClip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(primaryClip, value = 0F)
engine.block.setPositionY(primaryClip, value = 0F)
engine.block.setWidth(primaryClip, value = 1280F)
engine.block.setHeight(primaryClip, value = 720F)
val primaryFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = primaryFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
engine.block.setFill(primaryClip, fill = primaryFill)
engine.block.appendChild(parent = primaryTrack, child = primaryClip)
val overlayClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(overlayClip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(overlayClip, value = 820F)
engine.block.setPositionY(overlayClip, value = 80F)
engine.block.setWidth(overlayClip, value = 360F)
engine.block.setHeight(overlayClip, value = 220F)
val overlayFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = overlayFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4",
),
)
engine.block.setFill(overlayClip, fill = overlayFill)
engine.block.appendChild(parent = overlayTrack, child = overlayClip)
```
```kotlin highlight-android-add-audio
val audioClip = engine.block.create(DesignBlockType.Audio)
engine.block.setUri(
block = audioClip,
property = "audio/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",
),
)
engine.block.appendChild(parent = audioTrack, child = audioClip)
```
## Trim and Position Clips
Load media resources before reading source durations or setting trim ranges. `setTrimOffset()` chooses where playback starts inside the source file, `setTrimLength()` chooses how much source media is used, and `setTimeOffset()` places the clip on the page timeline.
```kotlin highlight-android-trim-and-position
engine.block.forceLoadAVResource(primaryFill)
engine.block.forceLoadAVResource(overlayFill)
engine.block.forceLoadAVResource(audioClip)
engine.block.setDuration(primaryClip, duration = 8.0)
engine.block.setTrimOffset(primaryFill, offset = 2.0)
engine.block.setTrimLength(primaryFill, length = 8.0)
engine.block.setLooping(primaryFill, looping = false)
engine.block.setMuted(primaryFill, muted = true)
engine.block.setTimeOffset(overlayClip, offset = 3.0)
engine.block.setDuration(overlayClip, duration = 4.0)
engine.block.setTimeOffset(audioClip, offset = 0.0)
engine.block.setDuration(audioClip, duration = 10.0)
```
In this sample, the primary video skips the first two seconds, uses an eight-second source range, and does not loop after that range. It also mutes the primary video fill so the source audio does not compete with the dedicated audio track. The overlay clip starts three seconds into the page timeline.
## Control Playback
Use page playback time for scrubbing and `setPlaying()` for preview playback. After seeking, `isVisibleAtCurrentPlaybackTime()` lets a custom timeline or preview UI confirm whether a clip is active at the playhead, while `isPlaying()` reports whether the page is currently in active playback.
```kotlin highlight-android-playback
engine.block.setPlaybackTime(page, time = 3.5)
check(engine.block.isVisibleAtCurrentPlaybackTime(overlayClip))
engine.block.setPlaying(page, enabled = true)
check(engine.block.isPlaying(page))
engine.block.setPlaying(page, enabled = false)
```
## Generate Timeline Thumbnails
Generate video thumbnails from a video fill or block and waveform data from an audio block. Video thumbnail data is returned as RGBA pixel buffers; audio waveform samples are returned as channel-interleaved float values.
```kotlin highlight-android-thumbnails
val videoThumbnails = engine.block.generateVideoThumbnailSequence(
block = primaryFill,
thumbnailHeight = 72,
timeBegin = 0.0,
timeEnd = 8.0,
numberOfFrames = 4,
).toList()
val audioWaveformChunks = engine.block.generateAudioThumbnailSequence(
block = audioClip,
samplesPerChunk = 40,
timeBegin = 0.0,
timeEnd = 10.0,
numberOfSamples = 160,
numberOfChannels = 2,
).toList()
```
Use the emitted frame and chunk indices as stable positions in your custom timeline cache. Start a new request when the zoom range, clip trim, or visible time window changes.
## Export the Timeline
Export the page with `engine.block.exportVideo()`. Use the page duration when you want to encode the complete timeline, and use the progress callback to update your UI while frames are rendered and encoded. Pass `options` for H.264 profile, level, bitrate, frame rate, and target dimensions. Use `onPreExport` for export-time setup on the background engine and `uriResolver` when bundled or relative URIs need rewriting before export.
```kotlin highlight-android-export
val exportDuration = engine.block.getDuration(page)
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = exportDuration,
mimeType = MimeType.MP4,
progressCallback = { progress ->
println("Encoded ${progress.encodedFrames} of ${progress.totalFrames} frames")
},
)
```
## API Reference
| Method | Description |
| --- | --- |
| `engine.scene.createForVideo()` | Creates a video-mode scene with timeline playback support. |
| `engine.block.create(blockType=_)` | Creates pages, tracks, graphics, and audio blocks. |
| `engine.block.createFill(fillType=_)` | Creates a video fill for a graphic clip. |
| `engine.block.appendChild(parent=_, child=_)` | Adds pages, tracks, and clips to the timeline hierarchy. |
| `engine.block.setDuration(block=_, duration=_)` | Sets how long a page or clip is active in seconds. |
| `engine.block.setTimeOffset(block=_, offset=_)` | Places a clip on its parent timeline in seconds. |
| `engine.block.forceLoadAVResource(block=_)` | Loads a video fill or audio block before duration, trim, or thumbnail queries. |
| `engine.block.setTrimOffset(block=_, offset=_)` | Sets the source-media start position for playback. |
| `engine.block.setTrimLength(block=_, length=_)` | Sets the length of source media used by the clip. |
| `engine.block.setPlaybackTime(block=_, time=_)` | Moves the playhead for a page or other playback-time block. |
| `engine.block.setPlaying(block=_, enabled=_)` | Starts or pauses playback for a page or media block. |
| `engine.block.isVisibleAtCurrentPlaybackTime(block=_)` | Returns whether a block should be visible at the current playhead position. |
| `engine.block.isPlaying(block=_)` | Returns whether a page or media block is currently in active playback. |
| `engine.block.generateVideoThumbnailSequence(block=_, thumbnailHeight=_, timeBegin=_, timeEnd=_, numberOfFrames=_)` | Emits frame thumbnails for timeline strips. |
| `engine.block.generateAudioThumbnailSequence(block=_, samplesPerChunk=_, timeBegin=_, timeEnd=_, numberOfSamples=_, numberOfChannels=_)` | Emits waveform chunks for audio lanes. |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Exports a page timeline to MP4 with optional encoder settings, background-engine setup, and URI rewriting. |
## Troubleshooting
- **Trim calls fail:** call `forceLoadAVResource()` on the video fill or audio block before setting trim offset or length.
- **Clips ignore manual offsets:** disable `"track/automaticallyManageBlockOffsets"` on tracks where clips need gaps or overlaps.
- **Export is blank near the end:** make sure the page duration does not exceed the end time of the visible clips unless blank frames are intentional.
- **Thumbnail generation stalls during playback:** pause playback before regenerating dense thumbnail or waveform ranges.
## Next Steps
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Learn to play, pause, seek, and preview audio and video content in CE.SDK using playback controls and solo mode.
- [Compress Exports for Smaller Files](https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/) - Learn how to reduce file sizes during export from CE.SDK for Android by tuning format-specific compression settings in Kotlin.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Update Caption Presets"
description: "Extend video captions with custom caption preset files and asset source manifests on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Update Caption Presets](https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/)
---
```kotlin file=@cesdk_android_examples/engine-guides-update-caption-presets/UpdateCaptionPresets.kt reference-only
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import ly.img.engine.AssetColorProperty
import ly.img.engine.AssetDefinition
import ly.img.engine.AssetPayload
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FindAssetsQuery
import ly.img.engine.SizeMode
import org.json.JSONObject
import java.io.File
import android.graphics.Color as AndroidColor
private const val CaptionPresetSourceId = "ly.img.caption.presets"
private const val CaptionPresetAssetId = "ly.img.caption.presets.neon-glow"
private const val ExistingCaptionPresetAssetId = "ly.img.caption.presets.existing"
private val CaptionPresetContentJson = """
{
"version": "3.0.0",
"id": "ly.img.caption.presets",
"assets": [
{
"id": "ly.img.caption.presets.neon-glow",
"label": { "en": "Neon Glow" },
"meta": {
"uri": "{{base_url}}/ly.img.caption.presets/presets/neon-glow.preset",
"thumbUri": "{{base_url}}/ly.img.caption.presets/thumbnails/neon-glow.png",
"mimeType": "application/ubq-blocks-string"
},
"payload": {
"properties": [
{
"type": "Color",
"property": "fill/solid/color",
"value": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 1.0 },
"defaultValue": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 1.0 }
},
{
"type": "Color",
"property": "dropShadow/color",
"value": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 0.8 },
"defaultValue": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 0.8 }
},
{
"type": "Color",
"property": "backgroundColor/color",
"value": { "r": 0.0, "g": 0.0, "b": 0.1, "a": 0.7 },
"defaultValue": { "r": 0.0, "g": 0.0, "b": 0.1, "a": 0.7 }
}
]
}
}
]
}
""".trimIndent()
data class CaptionPresetSummary(
val serializedPresetLength: Int,
val presetFileUri: String,
val presetFileLength: Long,
val loadedPresetCount: Int,
val loadedPresetId: String,
val loadedPresetUri: String,
)
fun updateCaptionPresets(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
): Job = CoroutineScope(Dispatchers.Main).launch {
runUpdateCaptionPresets(license, userId)
}
suspend fun runUpdateCaptionPresets(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
): CaptionPresetSummary {
val engine = Engine.getInstance(id = "ly.img.engine.update-caption-presets")
try {
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1280, height = 720)
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.replaceText(textBlock, text = "NEON GLOW")
engine.block.setPositionX(textBlock, value = 50F)
engine.block.setPositionY(textBlock, value = 200F)
engine.block.setWidth(textBlock, value = 600F)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
val neonCyan = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 1F)
engine.block.setTextColor(block = textBlock, color = neonCyan)
engine.block.setFillSolidColor(block = textBlock, color = neonCyan)
engine.block.setTextFontSize(block = textBlock, fontSize = 48F)
engine.block.setDropShadowEnabled(block = textBlock, enabled = true)
engine.block.setDropShadowColor(
block = textBlock,
color = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 0.8F),
)
engine.block.setDropShadowBlurRadiusX(block = textBlock, blurRadiusX = 20F)
engine.block.setDropShadowBlurRadiusY(block = textBlock, blurRadiusY = 20F)
engine.block.setDropShadowOffsetX(block = textBlock, offsetX = 0F)
engine.block.setDropShadowOffsetY(block = textBlock, offsetY = 0F)
engine.block.setBackgroundColorEnabled(block = textBlock, enabled = true)
engine.block.setBackgroundColor(
block = textBlock,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0.1F, a = 0.7F),
)
val serializedPreset = engine.block.saveToString(
blocks = listOf(textBlock),
allowedResourceSchemes = listOf("bundle", "file", "http", "https"),
)
check(serializedPreset.isNotBlank()) { "Serialized caption preset was empty." }
val localPresetSource = createLocalCaptionPresetSource(serializedPreset)
check(localPresetSource.presetFile.exists()) {
"Caption preset file was not written."
}
check(localPresetSource.presetFile.length() > 0) {
"Caption preset file was empty."
}
check(localPresetSource.presetFile.readText() == serializedPreset) {
"Caption preset file did not match the serialized preset."
}
seedExistingCaptionPresetSource(engine, localPresetSource.baseUri)
val loadedPresets = loadCaptionPresetSource(
engine = engine,
assetsBaseUri = localPresetSource.baseUri,
)
check(loadedPresets.loadedPresetId == CaptionPresetAssetId)
val loadedPresetFile = File(
checkNotNull(Uri.parse(loadedPresets.loadedPresetUri).path) {
"Loaded caption preset URI did not resolve to a file path."
},
)
check(loadedPresetFile.canonicalFile == localPresetSource.presetFile.canonicalFile) {
"Loaded caption preset URI did not point to the generated preset file."
}
check(loadedPresetFile.exists() && loadedPresetFile.length() > 0) {
"Loaded caption preset file was missing or empty."
}
val existingPreset = engine.asset.fetchAsset(
sourceId = CaptionPresetSourceId,
assetId = ExistingCaptionPresetAssetId,
)
check(existingPreset?.id == ExistingCaptionPresetAssetId) {
"Existing caption preset was removed while loading a custom preset."
}
return CaptionPresetSummary(
serializedPresetLength = serializedPreset.length,
presetFileUri = localPresetSource.presetUri.toString(),
presetFileLength = localPresetSource.presetFile.length(),
loadedPresetCount = loadedPresets.loadedPresetCount,
loadedPresetId = loadedPresets.loadedPresetId,
loadedPresetUri = loadedPresets.loadedPresetUri,
)
} finally {
engine.stop()
}
}
fun createCaptionPresetAssetDefinitionWithProperties(
presetUri: String,
thumbnailUri: String,
): AssetDefinition = AssetDefinition(
id = CaptionPresetAssetId,
label = mapOf("en" to "Neon Glow"),
meta = mapOf(
"uri" to presetUri,
"thumbUri" to thumbnailUri,
"mimeType" to "application/ubq-blocks-string",
),
payload = AssetPayload(
properties = listOf(
AssetColorProperty(
property = "fill/solid/color",
value = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 1F),
defaultValue = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 1F),
),
AssetColorProperty(
property = "backgroundColor/color",
value = Color.fromRGBA(r = 0F, g = 0F, b = 0.1F, a = 0.7F),
defaultValue = Color.fromRGBA(r = 0F, g = 0F, b = 0.1F, a = 0.7F),
),
AssetColorProperty(
property = "dropShadow/color",
value = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 0.8F),
defaultValue = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 0.8F),
),
),
),
)
private data class LocalCaptionPresetSource(
val baseUri: Uri,
val presetFile: File,
val presetUri: Uri,
)
private fun createLocalCaptionPresetSource(serializedPreset: String): LocalCaptionPresetSource {
val baseDirectory = File.createTempFile("caption-presets", "").apply {
delete()
mkdirs()
}
val sourceDirectory = File(baseDirectory, CaptionPresetSourceId).apply { mkdirs() }
File(sourceDirectory, "content.json").writeText(CaptionPresetContentJson)
val presetsDirectory = File(sourceDirectory, "presets").apply { mkdirs() }
val presetFile = File(presetsDirectory, "neon-glow.preset")
presetFile.writeText(serializedPreset)
val thumbnailsDirectory = File(sourceDirectory, "thumbnails").apply { mkdirs() }
writeCaptionPresetThumbnail(File(thumbnailsDirectory, "neon-glow.png"))
return LocalCaptionPresetSource(
baseUri = Uri.fromFile(baseDirectory),
presetFile = presetFile,
presetUri = Uri.fromFile(presetFile),
)
}
private fun seedExistingCaptionPresetSource(
engine: Engine,
assetsBaseUri: Uri,
) {
if (engine.asset.findAllSources().contains(CaptionPresetSourceId)) {
engine.asset.removeAsset(
sourceId = CaptionPresetSourceId,
assetId = ExistingCaptionPresetAssetId,
)
} else {
engine.asset.addLocalSource(CaptionPresetSourceId, emptyList())
}
val sourceBaseUri = assetsBaseUri
.buildUpon()
.appendPath(CaptionPresetSourceId)
.build()
engine.asset.addAsset(
sourceId = CaptionPresetSourceId,
asset = AssetDefinition(
id = ExistingCaptionPresetAssetId,
label = mapOf("en" to "Existing Caption Preset"),
meta = mapOf(
"uri" to sourceBaseUri
.buildUpon()
.appendPath("presets")
.appendPath("neon-glow.preset")
.build()
.toString(),
"thumbUri" to sourceBaseUri
.buildUpon()
.appendPath("thumbnails")
.appendPath("neon-glow.png")
.build()
.toString(),
"mimeType" to "application/ubq-blocks-string",
),
),
)
}
private fun writeCaptionPresetThumbnail(outputFile: File) {
val bitmap = Bitmap.createBitmap(320, 180, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(AndroidColor.rgb(4, 8, 20))
val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = AndroidColor.CYAN
textAlign = Paint.Align.CENTER
textSize = 48F
setShadowLayer(18F, 0F, 0F, AndroidColor.CYAN)
}
val baseline = (bitmap.height - glowPaint.descent() - glowPaint.ascent()) / 2F
canvas.drawText("NEON GLOW", bitmap.width / 2F, baseline, glowPaint)
outputFile.outputStream().use { stream ->
check(bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
"Failed to write caption preset thumbnail."
}
}
check(outputFile.length() > 0) { "Caption preset thumbnail was empty." }
}
data class LoadedCaptionPresets(
val loadedPresetCount: Int,
val loadedPresetId: String,
val loadedPresetUri: String,
)
suspend fun loadCaptionPresetSource(
engine: Engine,
assetsBaseUri: Uri,
): LoadedCaptionPresets {
val contentJsonUri = assetsBaseUri
.buildUpon()
.appendPath(CaptionPresetSourceId)
.appendPath("content.json")
.build()
val customPresetIds = JSONObject(CaptionPresetContentJson)
.getJSONArray("assets")
.let { assets ->
(0 until assets.length()).map { index ->
assets.getJSONObject(index).getString("id")
}
}
val sourceExists = engine.asset.findAllSources().contains(CaptionPresetSourceId)
if (sourceExists) {
// Remove every preset defined by this manifest before reloading it.
// Other presets in the same source stay available.
customPresetIds.forEach { assetId ->
engine.asset.removeAsset(
sourceId = CaptionPresetSourceId,
assetId = assetId,
)
}
}
engine.asset.addLocalSourceFromJSON(contentUri = contentJsonUri)
val presets = engine.asset.findAssets(
sourceId = CaptionPresetSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
)
val loadedPresetAssets = customPresetIds.map { assetId ->
checkNotNull(
engine.asset.fetchAsset(
sourceId = CaptionPresetSourceId,
assetId = assetId,
),
) { "Caption preset $assetId was not loaded." }
}
val loadedPreset = loadedPresetAssets.first()
val loadedPresetUri = checkNotNull(loadedPreset.meta?.get("uri")) {
"Loaded caption preset was missing a preset URI."
}
return LoadedCaptionPresets(
loadedPresetCount = presets.assets.size,
loadedPresetId = loadedPreset.id,
loadedPresetUri = loadedPresetUri,
)
}
```
Extend CE.SDK video captions with custom preset files by styling a text block,
serializing it, and publishing a caption preset content.json manifest.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-update-caption-presets)
Caption presets are serialized text or caption blocks plus metadata that points
to a thumbnail and optional customizable properties. Android can create the
preset file and load the asset definitions for your app code; the built-in
Android editor UI does not currently include a caption presets panel.
This guide covers the caption presets folder structure, how to create a styled
text block, how to serialize it as a preset file, how to define customizable
colors in content.json, and how to load the custom asset source on Android.
## Understanding the Caption Presets Structure
### Folder Organization
Caption presets use an asset source folder that contains a manifest, serialized
preset files, and thumbnails. Host the folder that contains
`ly.img.caption.presets/` and pass that folder as the base URI when loading the
asset source.
```text
assets/v5/ly.img.caption.presets/
├── content.json
├── presets/
│ └── neon-glow.preset
└── thumbnails/
└── neon-glow.png
```
The `content.json` file lists the preset IDs and metadata. The `presets/`
folder stores the string returned by `engine.block.saveToString()`, and the
`thumbnails/` folder stores preview images for your picker UI.
### content.json Format
The manifest needs a version, the caption presets source ID, and one asset entry
per preset. Use `{{base_url}}` in URI fields so the Android loader can replace
it with the base URI you provide.
```json file=@cesdk_android_examples/engine-guides-update-caption-presets/assets/ly.img.caption.presets/content.json
{
"version": "3.0.0",
"id": "ly.img.caption.presets",
"assets": [
{
"id": "ly.img.caption.presets.neon-glow",
"label": { "en": "Neon Glow" },
"meta": {
"uri": "{{base_url}}/ly.img.caption.presets/presets/neon-glow.preset",
"thumbUri": "{{base_url}}/ly.img.caption.presets/thumbnails/neon-glow.png",
"mimeType": "application/ubq-blocks-string"
},
"payload": {
"properties": [
{
"type": "Color",
"property": "fill/solid/color",
"value": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 1.0 },
"defaultValue": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 1.0 }
},
{
"type": "Color",
"property": "dropShadow/color",
"value": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 0.8 },
"defaultValue": { "r": 0.0, "g": 1.0, "b": 1.0, "a": 0.8 }
},
{
"type": "Color",
"property": "backgroundColor/color",
"value": { "r": 0.0, "g": 0.0, "b": 0.1, "a": 0.7 },
"defaultValue": { "r": 0.0, "g": 0.0, "b": 0.1, "a": 0.7 }
}
]
}
}
]
}
```
Each asset entry needs a stable ID, localized label, preset URI, thumbnail URI,
and `application/ubq-blocks-string` mime type. The optional
`payload.properties` array describes which colors your own preset UI can expose
for customization.
## Creating Custom Caption Presets
### Designing a Caption Style
Start with a video scene, a page, and a text block because caption presets are
based on text styling. The sample positions the block in a video-sized scene so
the serialized preset has a real block frame and sample caption text.
```kotlin highlight-android-create-text-block
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.appendChild(parent = page, child = textBlock)
engine.block.replaceText(textBlock, text = "NEON GLOW")
engine.block.setPositionX(textBlock, value = 50F)
engine.block.setPositionY(textBlock, value = 200F)
engine.block.setWidth(textBlock, value = 600F)
engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO)
```
The text block is the preset source. When you save the preset string, CE.SDK
keeps its text, frame, and style properties.
### Styling with Colors and Font Size
Set the text range color and the preset fill color to the same cyan value. The
manifest lists the customizable fill as `fill/solid/color` because it stores
engine property paths.
```kotlin highlight-android-style-text-color
val neonCyan = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 1F)
engine.block.setTextColor(block = textBlock, color = neonCyan)
engine.block.setFillSolidColor(block = textBlock, color = neonCyan)
```
Set the font size on the text range so captions that use the preset have the
same typography.
```kotlin highlight-android-style-font
engine.block.setTextFontSize(block = textBlock, fontSize = 48F)
```
### Adding Visual Effects
Drop shadow creates the neon glow. The sample uses the same cyan value as the
text color and increases the blur radius while keeping the offset at zero.
```kotlin highlight-android-style-drop-shadow
engine.block.setDropShadowEnabled(block = textBlock, enabled = true)
engine.block.setDropShadowColor(
block = textBlock,
color = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 0.8F),
)
engine.block.setDropShadowBlurRadiusX(block = textBlock, blurRadiusX = 20F)
engine.block.setDropShadowBlurRadiusY(block = textBlock, blurRadiusY = 20F)
engine.block.setDropShadowOffsetX(block = textBlock, offsetX = 0F)
engine.block.setDropShadowOffsetY(block = textBlock, offsetY = 0F)
```
Add a semi-transparent background color when the caption needs contrast against
busy video frames.
```kotlin highlight-android-style-background
engine.block.setBackgroundColorEnabled(block = textBlock, enabled = true)
engine.block.setBackgroundColor(
block = textBlock,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0.1F, a = 0.7F),
)
```
### Serializing the Preset
Serialize the styled block with `engine.block.saveToString()`. Save the returned
string as the file referenced by `meta.uri`, for example
`presets/neon-glow.preset`.
```kotlin highlight-android-serialize-preset
val serializedPreset = engine.block.saveToString(
blocks = listOf(textBlock),
allowedResourceSchemes = listOf("bundle", "file", "http", "https"),
)
check(serializedPreset.isNotBlank()) { "Serialized caption preset was empty." }
```
The serialized string contains the block properties and references to allowed
resource schemes. Keep the preset file and thumbnail reachable from the same
base folder as `content.json`.
## Defining Customizable Properties
### Color Properties
The `payload.properties` array describes the color controls your integration can
show for a preset. Each color property includes the engine property path plus
the current and default RGBA values in the 0-1 range.
Use these property paths for caption color customization:
- `fill/solid/color`: Text fill color
- `backgroundColor/color`: Background color behind the text
- `dropShadow/color`: Drop shadow color
- `stroke/color`: Stroke or outline color
Android's generic asset source loader preserves preset labels and metadata from
content.json. It does not map `payload.properties` into
`AssetDefinition.payload.properties`, so parse that section in your app when
loading a JSON manifest. If you add preset entries yourself, construct the same
metadata with `AssetColorProperty` values:
```kotlin highlight-android-manual-color-properties
fun createCaptionPresetAssetDefinitionWithProperties(
presetUri: String,
thumbnailUri: String,
): AssetDefinition = AssetDefinition(
id = CaptionPresetAssetId,
label = mapOf("en" to "Neon Glow"),
meta = mapOf(
"uri" to presetUri,
"thumbUri" to thumbnailUri,
"mimeType" to "application/ubq-blocks-string",
),
payload = AssetPayload(
properties = listOf(
AssetColorProperty(
property = "fill/solid/color",
value = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 1F),
defaultValue = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 1F),
),
AssetColorProperty(
property = "backgroundColor/color",
value = Color.fromRGBA(r = 0F, g = 0F, b = 0.1F, a = 0.7F),
defaultValue = Color.fromRGBA(r = 0F, g = 0F, b = 0.1F, a = 0.7F),
),
AssetColorProperty(
property = "dropShadow/color",
value = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 0.8F),
defaultValue = Color.fromRGBA(r = 0F, g = 1F, b = 1F, a = 0.8F),
),
),
),
)
```
## Updating the content.json File
### Adding a New Preset Entry
Add one object to the `assets` array for every preset. Keep the ID unique within
the `ly.img.caption.presets` namespace and make the URI fields point to the
serialized preset file and thumbnail.
The complete example above adds the `ly.img.caption.presets.neon-glow` preset
with customizable text, shadow, and background colors. Use the same structure
for additional caption styles.
### Complete content.json Example
Use the `content.json` file shown earlier in this guide as a starting point for
your hosted manifest.
## Hosting and Serving Custom Presets
### Server Setup
Prepare the asset folder on your server or in your Android app assets:
1. Create a folder that contains `ly.img.caption.presets/content.json`.
2. Save each serialized preset string in `ly.img.caption.presets/presets/`.
3. Save each PNG thumbnail in `ly.img.caption.presets/thumbnails/`.
4. Serve remote files over HTTP or HTTPS.
5. If the same manifest is shared with the Web SDK, configure CORS headers for
browser access.
### Verifying File Access
Before loading the source, make sure `content.json`, each preset file, and each
thumbnail URL returns the expected file. If you bundle the files in Android
assets, use a `file:///android_asset` base URI.
## Loading Custom Presets into CE.SDK
### Base URI Configuration
Load the manifest with `engine.asset.addLocalSourceFromJSON()`. Point `contentUri`
at the `content.json` inside your `ly.img.caption.presets/` folder; the loader
resolves `{{base_url}}` placeholders relative to that file's parent directory.
```kotlin highlight-android-load-custom-presets
val contentJsonUri = assetsBaseUri
.buildUpon()
.appendPath(CaptionPresetSourceId)
.appendPath("content.json")
.build()
val customPresetIds = JSONObject(CaptionPresetContentJson)
.getJSONArray("assets")
.let { assets ->
(0 until assets.length()).map { index ->
assets.getJSONObject(index).getString("id")
}
}
val sourceExists = engine.asset.findAllSources().contains(CaptionPresetSourceId)
if (sourceExists) {
// Remove every preset defined by this manifest before reloading it.
// Other presets in the same source stay available.
customPresetIds.forEach { assetId ->
engine.asset.removeAsset(
sourceId = CaptionPresetSourceId,
assetId = assetId,
)
}
}
engine.asset.addLocalSourceFromJSON(contentUri = contentJsonUri)
val presets = engine.asset.findAssets(
sourceId = CaptionPresetSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
)
val loadedPresetAssets = customPresetIds.map { assetId ->
checkNotNull(
engine.asset.fetchAsset(
sourceId = CaptionPresetSourceId,
assetId = assetId,
),
) { "Caption preset $assetId was not loaded." }
}
```
This makes the preset entries discoverable through `engine.asset.findAssets()`.
When reloading your manifest, remove every custom preset asset ID from that
manifest before adding it again so existing presets in the same source stay
available. Use the result in your own Android caption preset picker or share
the same hosted manifest with platforms that provide a built-in preset UI.
## Troubleshooting
### Preset Not Loading
- Verify `content.json` is reachable at
`{baseUri}/ly.img.caption.presets/content.json`.
- Confirm `meta.mimeType` is `application/ubq-blocks-string`.
- Use `{{base_url}}` only in URI fields that should be resolved from the base
URI.
### Preset Styles Not Applying
- Serialize a text block or caption block; other block types are not caption
preset sources.
- Save the exact string returned by `engine.block.saveToString()`.
- Keep property paths in `payload.properties` aligned with real caption or text
block properties.
### Thumbnail Not Displaying
- Check that `meta.thumbUri` points to an existing PNG file.
- Keep the thumbnail under the same hosted or bundled base folder as
`content.json`.
### Custom Colors Not Working
- Make each customizable property use `type: "Color"`.
- Store `value` and `defaultValue` with `r`, `g`, `b`, and `a` values from 0 to
1\.
- Apply those values from your custom UI to caption blocks with the matching
engine property paths.
## API Reference
| Method | Category | Purpose |
| ---------------------------------------------------------------------- | -------- | -------------------------------------------- |
| `engine.block.create(blockType=DesignBlockType.Text)` | Block | Create the text block used as preset source |
| `engine.block.replaceText(block=_, text=_)` | Block | Set the sample caption text |
| `engine.block.setTextColor(block=_, color=_)` | Block | Set the text fill color |
| `engine.block.setFillSolidColor(block=_, color=_)` | Block | Set the customizable fill color |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Block | Set the caption font size |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Block | Enable a glow or shadow effect |
| `engine.block.setDropShadowColor(block=_, color=_)` | Block | Set the shadow color |
| `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Block | Set the shadow blur on the x axis |
| `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Block | Set the shadow blur on the y axis |
| `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Block | Set the shadow x offset |
| `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Block | Set the shadow y offset |
| `engine.block.setBackgroundColorEnabled(block=_, enabled=_)` | Block | Enable a background behind the text |
| `engine.block.setBackgroundColor(block=_, color=_)` | Block | Set the background color |
| `engine.block.saveToString(blocks=_, allowedResourceSchemes=_)` | Block | Serialize the styled block as preset data |
| `engine.asset.removeAsset(sourceId=_, assetId=_)` | Asset | Reload custom presets without clearing source |
| `engine.asset.addLocalSourceFromJSON(contentUri=_)` | Asset | Load preset asset definitions from JSON |
| `engine.asset.findAssets(sourceId=_, query=_)` | Asset | Read loaded preset entries |
| `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Asset | Verify one preset entry by ID |
## Next Steps
- [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Add captions to videos and understand caption tracks
- [Import Remote Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/remote-asset-484685/) - Load asset definitions from remote JSON files hosted on CDNs or servers into CE.SDK's asset library with filtering and custom base URLs.
- [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Edit Image"
description: "Use CE.SDK to crop, transform, annotate, or enhance images with editing tools and programmatic APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image-c64912/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/)
---
---
## Related Pages
- [Android Image Editor SDK](https://img.ly/docs/cesdk/android/edit-image/overview-5249ea/) - The CreativeEditor SDK provides a robust and user-friendly solution for photo and image editing.
- [Replace Colors](https://img.ly/docs/cesdk/android/edit-image/replace-colors-6ede17/) - Replace specific colors in images using CE.SDK's Recolor and Green Screen effects with programmatic control.
- [Background Removal](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) - Add a background removal action to the CE.SDK Android editor.
- [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) - Crop, resize, rotate, scale, or flip images using CE.SDK's built-in transformation tools.
- [Annotation](https://img.ly/docs/cesdk/android/edit-image/annotation-142604/) - Add shape-based annotations to images and designs with CreativeEngine.
- [Add Watermark](https://img.ly/docs/cesdk/android/edit-image/add-watermark-679de0/) - Add text and image watermarks to protect images, indicate ownership, or add branding using CE.SDK in Android.
- [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-image/programmatic-dd4348/) - Edit images with CE.SDK Engine APIs for automated or custom Android workflows.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add Watermark"
description: "Add text and image watermarks to protect images, indicate ownership, or add branding using CE.SDK in Android."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/add-watermark-679de0/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Add Watermark](https://img.ly/docs/cesdk/android/edit-image/add-watermark-679de0/)
---
```kotlin file=@cesdk_android_examples/engine-guides-edit-image-add-watermark/AddImageWatermark.kt reference-only
import android.net.Uri
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.HorizontalAlignment
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import kotlin.math.abs
suspend fun addImageWatermark(engine: Engine): ImageWatermarkResult {
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
engine.scene.createFromImage(imageUri)
val page = requireNotNull(engine.scene.getCurrentPage()) {
"Expected createFromImage() to create a page."
}
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val textWatermark = engine.block.create(DesignBlockType.Text)
engine.block.setWidthMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.replaceText(block = textWatermark, text = "All rights reserved")
engine.block.appendChild(parent = page, child = textWatermark)
engine.block.setTextFontSize(block = textWatermark, fontSize = 28F)
engine.block.setTextColor(block = textWatermark, color = Color.fromRGBA(1F, 1F, 1F, 1F))
engine.block.setTextHorizontalAlignment(block = textWatermark, alignment = HorizontalAlignment.Left)
engine.block.setOpacity(block = textWatermark, value = 0.7F)
val logoWatermark = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = logoWatermark, shape = rectShape)
val logoFill = engine.block.createFill(FillType.Image)
val logoUri = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg")
engine.block.setUri(
block = logoFill,
property = "fill/image/imageFileURI",
value = logoUri,
)
engine.block.setFill(block = logoWatermark, fill = logoFill)
engine.block.setContentFillMode(block = logoWatermark, mode = ContentFillMode.CONTAIN)
engine.block.appendChild(parent = page, child = logoWatermark)
val logoSize = pageWidth * 0.14F
engine.block.setWidth(block = logoWatermark, value = logoSize)
engine.block.setHeight(block = logoWatermark, value = logoSize)
engine.block.setOpacity(block = logoWatermark, value = 0.62F)
val spacing = 18F
val bottomPadding = 36F
val textWidth = engine.block.getFrameWidth(textWatermark)
val textHeight = engine.block.getFrameHeight(textWatermark)
val totalWatermarkWidth = logoSize + spacing + textWidth
val startX = (pageWidth - totalWatermarkWidth) / 2F
val centerY = pageHeight - bottomPadding - maxOf(logoSize, textHeight) / 2F
engine.block.setPositionX(block = logoWatermark, value = startX)
engine.block.setPositionY(block = logoWatermark, value = centerY - logoSize / 2F)
engine.block.setPositionX(block = textWatermark, value = startX + logoSize + spacing)
engine.block.setPositionY(block = textWatermark, value = centerY - textHeight / 2F)
listOf(textWatermark, logoWatermark).forEach { watermark ->
if (engine.block.supportsDropShadow(watermark)) {
engine.block.setDropShadowEnabled(block = watermark, enabled = true)
engine.block.setDropShadowColor(block = watermark, color = Color.fromRGBA(0F, 0F, 0F, 0.55F))
engine.block.setDropShadowOffsetX(block = watermark, offsetX = 3F)
engine.block.setDropShadowOffsetY(block = watermark, offsetY = 3F)
engine.block.setDropShadowBlurRadiusX(block = watermark, blurRadiusX = 6F)
engine.block.setDropShadowBlurRadiusY(block = watermark, blurRadiusY = 6F)
}
}
val exportedPng = engine.block.export(block = page, mimeType = MimeType.PNG)
val exportedJpeg = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = ExportOptions(jpegQuality = 0.86F),
)
check(exportedPng.hasRemaining()) { "PNG export is empty." }
check(exportedJpeg.hasRemaining()) { "JPEG export is empty." }
val textOpacity = engine.block.getOpacity(textWatermark)
val logoOpacity = engine.block.getOpacity(logoWatermark)
check(abs(textOpacity - 0.7F) < 0.0001F)
check(abs(logoOpacity - 0.62F) < 0.0001F)
return ImageWatermarkResult(
pageWidth = pageWidth,
pageHeight = pageHeight,
textWatermark = textWatermark,
logoWatermark = logoWatermark,
textOpacity = textOpacity,
logoOpacity = logoOpacity,
textShadowEnabled = engine.block.isDropShadowEnabled(textWatermark),
logoUri = engine.block.getUri(
block = logoFill,
property = "fill/image/imageFileURI",
),
exportedPng = exportedPng.asReadOnlyBuffer(),
exportedJpeg = exportedJpeg.asReadOnlyBuffer(),
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-edit-image-add-watermark/ImageWatermarkResult.kt reference-only
import android.net.Uri
import ly.img.engine.DesignBlock
import java.nio.ByteBuffer
data class ImageWatermarkResult(
val pageWidth: Float,
val pageHeight: Float,
val textWatermark: DesignBlock,
val logoWatermark: DesignBlock,
val textOpacity: Float,
val logoOpacity: Float,
val textShadowEnabled: Boolean,
val logoUri: Uri,
val exportedPng: ByteBuffer,
val exportedJpeg: ByteBuffer,
)
```
Add text and image watermarks to designs programmatically using CE.SDK's block API in Android.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-edit-image-add-watermark)
Watermarks protect intellectual property, indicate ownership, add branding, or mark content as drafts. CE.SDK supports two types of watermarks: **text watermarks** created from text blocks for copyright notices and brand names, and **image watermarks** created from graphic blocks with image fills for logos and symbols.
This guide covers how to create text and logo watermarks, position them on a design, style them for visibility, and export the watermarked result.
## Setup and Prerequisites
Start from an image scene and read the page dimensions. The page provides the canvas where we add and position watermarks.
```kotlin highlight-android-setup
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
engine.scene.createFromImage(imageUri)
val page = requireNotNull(engine.scene.getCurrentPage()) {
"Expected createFromImage() to create a page."
}
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
```
`createFromImage()` creates a scene from the source image. `getCurrentPage()`, `getWidth()`, and `getHeight()` provide the page values used for placement calculations later.
## Creating Text Watermarks
Text watermarks display copyright notices, URLs, or brand names. We create a text block, set its content, and add it to the page.
```kotlin highlight-android-create-text-watermark
val textWatermark = engine.block.create(DesignBlockType.Text)
engine.block.setWidthMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.replaceText(block = textWatermark, text = "All rights reserved")
engine.block.appendChild(parent = page, child = textWatermark)
```
`DesignBlockType.Text` creates a text block. Auto width and height keep the block frame tied to the rendered text.
### Styling the Text
Configure the font size, color, alignment, and opacity to make the watermark visible without dominating the image.
```kotlin highlight-android-style-text-watermark
engine.block.setTextFontSize(block = textWatermark, fontSize = 28F)
engine.block.setTextColor(block = textWatermark, color = Color.fromRGBA(1F, 1F, 1F, 1F))
engine.block.setTextHorizontalAlignment(block = textWatermark, alignment = HorizontalAlignment.Left)
engine.block.setOpacity(block = textWatermark, value = 0.7F)
```
Key styling options:
- **Font size** - Use a size that remains readable at the target export dimensions.
- **Text color** - White or black usually works best, depending on the image background.
- **Opacity** - Values between 0.5 and 0.7 provide a balanced semi-transparent appearance.
## Creating Logo Watermarks
Logo watermarks use graphic blocks with image fills to display brand symbols or company logos.
```kotlin highlight-android-create-logo-watermark
val logoWatermark = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = logoWatermark, shape = rectShape)
val logoFill = engine.block.createFill(FillType.Image)
val logoUri = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg")
engine.block.setUri(
block = logoFill,
property = "fill/image/imageFileURI",
value = logoUri,
)
engine.block.setFill(block = logoWatermark, fill = logoFill)
engine.block.setContentFillMode(block = logoWatermark, mode = ContentFillMode.CONTAIN)
engine.block.appendChild(parent = page, child = logoWatermark)
```
We create a graphic block, assign a rect shape, then create an image fill with the logo URI. `ContentFillMode.CONTAIN` keeps the logo inside its frame without cropping.
### Sizing the Logo
Set dimensions for the logo and apply opacity to match the text watermark.
```kotlin highlight-android-size-logo-watermark
val logoSize = pageWidth * 0.14F
engine.block.setWidth(block = logoWatermark, value = logoSize)
engine.block.setHeight(block = logoWatermark, value = logoSize)
engine.block.setOpacity(block = logoWatermark, value = 0.62F)
```
A useful rule is to size logos to 10-20% of the page width. This keeps them visible without covering the main image content.
### Positioning the Watermarks
Calculate positions from the current page dimensions. This sample places the logo and text side by side near the bottom center.
```kotlin highlight-android-position-watermarks
val spacing = 18F
val bottomPadding = 36F
val textWidth = engine.block.getFrameWidth(textWatermark)
val textHeight = engine.block.getFrameHeight(textWatermark)
val totalWatermarkWidth = logoSize + spacing + textWidth
val startX = (pageWidth - totalWatermarkWidth) / 2F
val centerY = pageHeight - bottomPadding - maxOf(logoSize, textHeight) / 2F
engine.block.setPositionX(block = logoWatermark, value = startX)
engine.block.setPositionY(block = logoWatermark, value = centerY - logoSize / 2F)
engine.block.setPositionX(block = textWatermark, value = startX + logoSize + spacing)
engine.block.setPositionY(block = textWatermark, value = centerY - textHeight / 2F)
```
The sample reads the rendered text frame dimensions with `getFrameWidth()` and `getFrameHeight()`, combines them with the logo size and spacing, then centers the group horizontally.
## Enhancing Visibility with Drop Shadows
Drop shadows improve watermark readability against varied backgrounds by adding contrast.
```kotlin highlight-android-add-drop-shadow
listOf(textWatermark, logoWatermark).forEach { watermark ->
if (engine.block.supportsDropShadow(watermark)) {
engine.block.setDropShadowEnabled(block = watermark, enabled = true)
engine.block.setDropShadowColor(block = watermark, color = Color.fromRGBA(0F, 0F, 0F, 0.55F))
engine.block.setDropShadowOffsetX(block = watermark, offsetX = 3F)
engine.block.setDropShadowOffsetY(block = watermark, offsetY = 3F)
engine.block.setDropShadowBlurRadiusX(block = watermark, blurRadiusX = 6F)
engine.block.setDropShadowBlurRadiusY(block = watermark, blurRadiusY = 6F)
}
}
```
Drop shadow parameters:
- **Offset X/Y** - Distance from the block; 2-4 px works well for subtle watermarks.
- **Blur Radius X/Y** - Softness of the shadow; 4-8 px adds contrast without harsh edges.
- **Color** - Black with partial alpha provides contrast without overpowering the image.
## Exporting Watermarked Images
After adding watermarks, export the complete page as an image file.
```kotlin highlight-android-export-watermarked
val exportedPng = engine.block.export(block = page, mimeType = MimeType.PNG)
val exportedJpeg = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = ExportOptions(jpegQuality = 0.86F),
)
```
`engine.block.export()` renders the page with all watermarks and returns binary image data. The Android API exposes PNG and JPEG export through `MimeType`; pass `ExportOptions` when a format needs extra configuration such as JPEG quality.
## Troubleshooting
**Watermark not visible**
- Verify the block is within page bounds using position values.
- Check opacity is between 0.3 and 1.0.
- Ensure `appendChild()` was called to add the block to the page.
**Position appears incorrect**
- Recalculate positions using the current page width and height.
- Account for watermark dimensions when calculating corner or centered positions.
- Remember that coordinates start from the top-left corner.
**Text not legible**
- Increase the font size for the target export dimensions.
- Add a drop shadow for contrast against complex backgrounds.
- Increase opacity if the watermark is too faint.
**Logo quality issues**
- Use a higher-resolution source image for the logo.
- Avoid scaling the logo beyond its original dimensions.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createFromImage(imageUri=_)` | Create a scene from an image URI |
| `engine.scene.getCurrentPage()` | Get the page created for the image scene |
| `engine.block.getWidth(block=_)` | Read the page width for placement calculations |
| `engine.block.getHeight(block=_)` | Read the page height for placement calculations |
| `engine.block.create(blockType=DesignBlockType.Text)` | Create a text watermark block |
| `engine.block.setWidthMode(block=_, mode=SizeMode.AUTO)` | Let a text block size itself to its content |
| `engine.block.setHeightMode(block=_, mode=SizeMode.AUTO)` | Let a text block size itself to its content |
| `engine.block.replaceText(block=_, text=_)` | Set text watermark content |
| `engine.block.appendChild(parent=_, child=_)` | Add the watermark to the page |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text size |
| `engine.block.setTextColor(block=_, color=_)` | Set text color |
| `engine.block.setTextHorizontalAlignment(block=_, alignment=_)` | Set paragraph alignment |
| `engine.block.setOpacity(block=_, value=_)` | Set watermark transparency |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create an image watermark block |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular graphic shape |
| `engine.block.setShape(block=_, shape=_)` | Apply the rectangular shape to the graphic block |
| `engine.block.createFill(fillType=FillType.Image)` | Create an image fill for a logo |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the logo image URI |
| `engine.block.setFill(block=_, fill=_)` | Apply the image fill to the graphic block |
| `engine.block.setContentFillMode(block=_, mode=ContentFillMode.CONTAIN)` | Fit the logo inside its frame |
| `engine.block.setWidth(block=_, value=_)` | Set watermark width |
| `engine.block.setHeight(block=_, value=_)` | Set watermark height |
| `engine.block.getFrameWidth(block=_)` | Read rendered text width for placement |
| `engine.block.getFrameHeight(block=_)` | Read rendered text height for placement |
| `engine.block.setPositionX(block=_, value=_)` | Set horizontal position |
| `engine.block.setPositionY(block=_, value=_)` | Set vertical position |
| `engine.block.supportsDropShadow(block=_)` | Check whether a block supports drop shadows |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable drop shadow |
| `engine.block.setDropShadowColor(block=_, color=_)` | Set shadow color and alpha |
| `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set horizontal shadow offset |
| `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set vertical shadow offset |
| `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Set horizontal shadow blur |
| `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Set vertical shadow blur |
| `engine.block.export(block=_, mimeType=_, options=_)` | Export the watermarked page |
## Next Steps
- [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Style text blocks with fonts, colors, and effects
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Export options and formats for watermarked images
- [Crop Images](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) - Transform images before watermarking
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Annotation"
description: "Add shape-based annotations to images and designs with CreativeEngine."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/annotation-142604/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Annotation](https://img.ly/docs/cesdk/android/edit-image/annotation-142604/)
---
```kotlin file=@cesdk_android_examples/engine-guides-annotation/ImageAnnotation.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.StrokeStyle
data class ImageAnnotationResult(
val page: DesignBlock,
val highlight: DesignBlock,
val callout: DesignBlock,
val underline: DesignBlock,
val redaction: DesignBlock,
)
fun imageAnnotation(engine: Engine): ImageAnnotationResult {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
val imageArea = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageArea, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(imageArea, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = imageArea,
color = Color.fromRGBA(r = 0.92F, g = 0.94F, b = 0.96F, a = 1F),
)
engine.block.setPositionX(imageArea, value = 40F)
engine.block.setPositionY(imageArea, value = 40F)
engine.block.setWidth(imageArea, value = 720F)
engine.block.setHeight(imageArea, value = 520F)
engine.block.appendChild(parent = page, child = imageArea)
val highlight = addRectangleAnnotation(engine = engine, page = page)
val callout = addCircleAnnotation(engine = engine, page = page)
val underline = addLineAnnotation(engine = engine, page = page)
val redaction = addRedactionBox(engine = engine, page = page)
styleRectangleAnnotationAppearance(engine = engine, rectangle = highlight)
check(engine.block.isValid(highlight))
check(engine.block.isValid(callout))
check(engine.block.isValid(underline))
check(engine.block.isValid(redaction))
check(engine.block.getOpacity(highlight) == 0.5F)
check(engine.block.isStrokeEnabled(callout))
return ImageAnnotationResult(
page = page,
highlight = highlight,
callout = callout,
underline = underline,
redaction = redaction,
)
}
fun addRectangleAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val highlight = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = highlight, shape = rectShape)
engine.block.setPositionX(highlight, value = 100F)
engine.block.setPositionY(highlight, value = 100F)
engine.block.setWidth(highlight, value = 220F)
engine.block.setHeight(highlight, value = 90F)
engine.block.setFill(highlight, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = highlight,
color = Color.fromRGBA(r = 1F, g = 0.82F, b = 0F, a = 0.4F),
)
engine.block.appendChild(parent = page, child = highlight)
return highlight
}
fun addCircleAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val callout = engine.block.create(DesignBlockType.Graphic)
val ellipseShape = engine.block.createShape(ShapeType.Ellipse)
engine.block.setShape(block = callout, shape = ellipseShape)
engine.block.setPositionX(callout, value = 360F)
engine.block.setPositionY(callout, value = 155F)
engine.block.setWidth(callout, value = 120F)
engine.block.setHeight(callout, value = 120F)
engine.block.setFillEnabled(block = callout, enabled = false)
engine.block.setStrokeEnabled(block = callout, enabled = true)
engine.block.setStrokeColor(
block = callout,
color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setStrokeWidth(block = callout, width = 4F)
engine.block.appendChild(parent = page, child = callout)
return callout
}
fun addLineAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val underline = engine.block.create(DesignBlockType.Graphic)
val lineShape = engine.block.createShape(ShapeType.Line)
engine.block.setShape(block = underline, shape = lineShape)
engine.block.setPositionX(underline, value = 85F)
engine.block.setPositionY(underline, value = 430F)
engine.block.setWidth(underline, value = 320F)
val lineThickness = 8F
engine.block.setHeight(underline, value = lineThickness)
engine.block.setStrokeEnabled(block = underline, enabled = true)
engine.block.setStrokeColor(
block = underline,
color = Color.fromRGBA(r = 0.05F, g = 0.25F, b = 0.95F, a = 1F),
)
engine.block.setStrokeWidth(block = underline, width = lineThickness)
engine.block.appendChild(parent = page, child = underline)
return underline
}
fun addRedactionBox(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val redaction = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(redaction, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(redaction, value = 500F)
engine.block.setPositionY(redaction, value = 360F)
engine.block.setWidth(redaction, value = 180F)
engine.block.setHeight(redaction, value = 34F)
engine.block.setFill(redaction, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = redaction,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F),
)
engine.block.appendChild(parent = page, child = redaction)
return redaction
}
fun styleRectangleAnnotationAppearance(
engine: Engine,
rectangle: DesignBlock,
) {
engine.block.setOpacity(block = rectangle, value = 0.5F)
val shape = engine.block.getShape(rectangle)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusTL", value = 10F)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusTR", value = 10F)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusBL", value = 10F)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusBR", value = 10F)
engine.block.setStrokeEnabled(block = rectangle, enabled = true)
engine.block.setStrokeStyle(block = rectangle, style = StrokeStyle.DASHED)
engine.block.setStrokeWidth(block = rectangle, width = 3F)
engine.block.setStrokeColor(
block = rectangle,
color = Color.fromRGBA(r = 0.9F, g = 0.35F, b = 0F, a = 1F),
)
}
```
Add rectangles, circles, lines, and redaction boxes on top of images or
designs with shape blocks.

> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-annotation)
Annotations in CE.SDK are graphic blocks with shape geometry. Use them to highlight important areas, circle details, underline content, hide sensitive information, or add visual review marks on top of existing page content.
The examples below append annotations to an existing page. Position and dimensions are set on the graphic block, while rectangle-specific properties such as corner radius are set on the attached shape block.
## Add Rectangle Annotation
Create a graphic block, attach a rectangle shape, position it, and give it a semi-transparent color fill. A transparent fill keeps the underlying image or design visible while drawing attention to a region.
```kotlin highlight-android-rectangle-annotation
fun addRectangleAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val highlight = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = highlight, shape = rectShape)
engine.block.setPositionX(highlight, value = 100F)
engine.block.setPositionY(highlight, value = 100F)
engine.block.setWidth(highlight, value = 220F)
engine.block.setHeight(highlight, value = 90F)
engine.block.setFill(highlight, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = highlight,
color = Color.fromRGBA(r = 1F, g = 0.82F, b = 0F, a = 0.4F),
)
engine.block.appendChild(parent = page, child = highlight)
return highlight
}
```
## Add Circle Annotation
Use an ellipse shape for circles and callouts. Disable the fill and enable a stroke when the annotation should outline an item without covering it.
```kotlin highlight-android-circle-annotation
fun addCircleAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val callout = engine.block.create(DesignBlockType.Graphic)
val ellipseShape = engine.block.createShape(ShapeType.Ellipse)
engine.block.setShape(block = callout, shape = ellipseShape)
engine.block.setPositionX(callout, value = 360F)
engine.block.setPositionY(callout, value = 155F)
engine.block.setWidth(callout, value = 120F)
engine.block.setHeight(callout, value = 120F)
engine.block.setFillEnabled(block = callout, enabled = false)
engine.block.setStrokeEnabled(block = callout, enabled = true)
engine.block.setStrokeColor(
block = callout,
color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setStrokeWidth(block = callout, width = 4F)
engine.block.appendChild(parent = page, child = callout)
return callout
}
```
Use equal width and height for a circle. Different values produce an ellipse.
## Add Line Annotation
Use a line shape for underlines, separators, and simple markup strokes. The block width controls the line length.
```kotlin highlight-android-line-annotation
fun addLineAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val underline = engine.block.create(DesignBlockType.Graphic)
val lineShape = engine.block.createShape(ShapeType.Line)
engine.block.setShape(block = underline, shape = lineShape)
engine.block.setPositionX(underline, value = 85F)
engine.block.setPositionY(underline, value = 430F)
engine.block.setWidth(underline, value = 320F)
val lineThickness = 8F
engine.block.setHeight(underline, value = lineThickness)
engine.block.setStrokeEnabled(block = underline, enabled = true)
engine.block.setStrokeColor(
block = underline,
color = Color.fromRGBA(r = 0.05F, g = 0.25F, b = 0.95F, a = 1F),
)
engine.block.setStrokeWidth(block = underline, width = lineThickness)
engine.block.appendChild(parent = page, child = underline)
return underline
}
```
Line shapes use stroke width for the visible thickness. Keep the block height in sync with the stroke width so the line bounds match the rendered stroke.
## Add a Visual Redaction Overlay
To visually cover content in flattened image output, place a solid rectangle over the sensitive area. Use an opaque fill so the covered content is not visible in the final flattened export.
The original block remains underneath the overlay in the editable CE.SDK scene and in layered or reusable outputs. For sensitive content, remove, crop, or replace the source content before sharing an editable scene.
```kotlin highlight-android-redaction-box
fun addRedactionBox(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val redaction = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(redaction, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(redaction, value = 500F)
engine.block.setPositionY(redaction, value = 360F)
engine.block.setWidth(redaction, value = 180F)
engine.block.setHeight(redaction, value = 34F)
engine.block.setFill(redaction, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = redaction,
color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F),
)
engine.block.appendChild(parent = page, child = redaction)
return redaction
}
```
## Style Annotation Appearance
Common rectangle styling changes include opacity, rounded corners, and dashed strokes. Apply opacity and stroke settings to the graphic block, then update rectangle-specific corner geometry on the attached shape block.
```kotlin highlight-android-style-appearance
fun styleRectangleAnnotationAppearance(
engine: Engine,
rectangle: DesignBlock,
) {
engine.block.setOpacity(block = rectangle, value = 0.5F)
val shape = engine.block.getShape(rectangle)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusTL", value = 10F)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusTR", value = 10F)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusBL", value = 10F)
engine.block.setFloat(shape, property = "shape/rect/cornerRadiusBR", value = 10F)
engine.block.setStrokeEnabled(block = rectangle, enabled = true)
engine.block.setStrokeStyle(block = rectangle, style = StrokeStyle.DASHED)
engine.block.setStrokeWidth(block = rectangle, width = 3F)
engine.block.setStrokeColor(
block = rectangle,
color = Color.fromRGBA(r = 0.9F, g = 0.35F, b = 0F, a = 1F),
)
}
```
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Shape not visible | Check that the block is appended to the page and that fill or stroke is enabled. |
| Shape positioned incorrectly | Verify x and y positions on the graphic block, not the shape block. |
| Stroke not showing | Enable stroke and set a stroke width greater than `0F`. |
| Rounded corners not changing | Set the corner radius properties on the rectangle shape returned by `getShape()`. |
| Shape covers too much content | Lower block opacity or use a stroke-only annotation. |
## API Reference
| Method | Description |
|--------|-------------|
| `engine.block.create(blockType=_)` | Create a design block such as a graphic block. |
| `engine.block.createShape(type=_)` | Create a shape block such as `ShapeType.Rect`, `ShapeType.Ellipse`, or `ShapeType.Line`. |
| `engine.block.setShape(block=_, shape=_)` | Attach a shape block to a graphic block. |
| `engine.block.setPositionX(block=_, value=_)` | Set the graphic block's x position. |
| `engine.block.setPositionY(block=_, value=_)` | Set the graphic block's y position. |
| `engine.block.setWidth(block=_, value=_)` | Set the graphic block's width. |
| `engine.block.setHeight(block=_, value=_)` | Set the graphic block's height. |
| `engine.block.createFill(fillType=_)` | Create a fill block such as `FillType.Color`. |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill to a graphic block. |
| `engine.block.setFillSolidColor(block=_, color=_)` | Set the solid color of a graphic block's fill. |
| `engine.block.setFillEnabled(block=_, enabled=_)` | Enable or disable the graphic block's fill. |
| `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable the graphic block's stroke. |
| `engine.block.setStrokeColor(block=_, color=_)` | Set the graphic block's stroke color. |
| `engine.block.setStrokeWidth(block=_, width=_)` | Set the graphic block's stroke width. |
| `engine.block.setStrokeStyle(block=_, style=_)` | Set a stroke style such as `StrokeStyle.DASHED`. |
| `engine.block.setOpacity(block=_, value=_)` | Set block opacity from `0F` to `1F`. |
| `engine.block.getShape(block=_)` | Get the shape block attached to a graphic block. |
| `engine.block.setFloat(block=_, property="shape/rect/cornerRadiusTL", value=_)` | Set float shape properties such as rectangle corner radius. |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create a normalized RGBA color. |
| `engine.block.appendChild(parent=_, child=_)` | Add the annotation block to a page or container. |
## Next Steps
- [Transform Images](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) - Crop, resize, rotate, scale, or flip image content.
- [Edit Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) - Modify shape geometry, color, size, position, and corner radius.
- [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Group multiple annotations together.
- [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control annotation stacking order.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Android Image Editor SDK"
description: "The CreativeEditor SDK provides a robust and user-friendly solution for photo and image editing."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/overview-5249ea/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Overview](https://img.ly/docs/cesdk/android/edit-image/overview-5249ea/)
---
Build image editing workflows on Android with CE.SDK's customizable editor UI
and the CreativeEngine API.
CE.SDK edits images on the device, so user content stays in your app while the
editor provides real-time previews and export-ready output. You can start with
the [Photo Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/), customize the CE.SDK
editor UI for your product, or drive the same image operations programmatically
with CreativeEngine.
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
## Core Capabilities
CE.SDK combines interactive image editing tools with the same engine APIs that
power automated workflows. Typical Android image editing features include:
- **Transformations**: Crop, rotate, resize, scale, move, and flip image content.
- **Adjustments and effects**: Apply filters, blur, brightness, contrast, color,
and style effects.
- **Background removal**: Add plugin-powered background removal to isolate
subjects for compositing.
- **Layers and compositing**: Combine images with text, shapes, graphics,
overlays, and masks.
- **Programmatic editing**: Create, update, and export scenes through
CreativeEngine when edits should run without direct user input.
## Supported Input Formats
Android integrations can import common image formats from app storage, remote
URLs, asset sources, or user-selected content.
| Category | Supported Formats |
| ---------- | -------------------------------------------------------- |
| **Images** | `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` |
For color-sensitive workflows, treat the file format and the color model as
separate concerns. CE.SDK supports design colors across screen and print
workflows, including sRGB, Display P3, CMYK, and spot colors. Imported raster
image profile handling depends on the source asset and Android's media
decoding path, so test profile-sensitive images on your target devices. See
[Colors](https://img.ly/docs/cesdk/android/colors/overview-16a177/) for color-space behavior in CE.SDK.
## Output and Export Options
Export edited scenes using the public Android `MimeType` values exposed by
CE.SDK.
| Category | Supported Formats |
| -------------- | -------------------------------------------------- |
| **Images** | `.png` (with transparency), `.jpeg`, `.tga` |
| **Vector** | `.svg` (scalable vector graphics with text paths) |
| **Print** | `.pdf` (supports underlayer printing and spot colors) |
| **Video** | `.mp4` (for video scenes) |
| **Binary data** | `application/octet-stream` for binary export data |
Screen-oriented exports such as PNG, JPEG, TGA, SVG, and MP4 should be treated
as RGB output. For print workflows, PDF export supports spot colors, but CE.SDK
does not produce true CMYK PDF or PDF/X output. See
[Colors](https://img.ly/docs/cesdk/android/colors/overview-16a177/) for the current color conversion and print-color
behavior.
You can choose export MIME type, target dimensions, compression quality, and
whether the result includes transparency. Practical export limits depend on the
device's GPU capabilities and available memory.
## UI-Based vs. Programmatic Editing
Use the CE.SDK editor UI when users should directly adjust images through
inspectors, canvas gestures, asset panels, and tool controls. The Android editor
is built with Jetpack Compose and can be themed, localized, and configured for
your product.
Use CreativeEngine when edits should be driven by app logic, templates, or
automation. Programmatic workflows can create image blocks, update block
properties, compose layers, and export the final scene without exposing every
operation in the UI.
Many apps combine both approaches: users edit visually, while app code applies
defaults, validates output, or performs batch operations in the background.
## Customizing the Image Editor
The Android editor surface can be adapted to match your app's workflow:
- **Tool availability**: Show, hide, or reorder editor controls for the tasks
your users need.
- **UI appearance**: Apply your theme, icons, typography, and localization.
- **Asset and plugin setup**: Add app-specific asset sources, quick actions, or
plugin features such as background removal.
- **Workflow integration**: Open existing app images, save edited output back to
your storage layer, or continue with CreativeEngine after a user finishes.
## Performance Considerations
Image editing is client-side and performance depends on the Android device. For
large images, test target devices with representative files and set conservative
resolution defaults when needed.
Keep these limits in mind:
- Higher input or export resolutions require more GPU memory.
- Real-time previews are optimized for interactive editing, but complex scenes
with many layers can still increase memory pressure.
- Exporting at very high dimensions may fail on devices with smaller maximum
texture sizes.
## Working with Image Blocks
Images in CE.SDK are represented as blocks in a scene. A page can contain one
image or a layered composition with text, graphics, shapes, masks, and effects.
The editor UI exposes common controls for these blocks, while CreativeEngine
lets your app update the same scene data through code.
Use image blocks when you need direct control over the image content itself, and
combine them with other block types when your workflow needs overlays,
watermarks, annotations, or templates.
## Next Steps
- [Transform Images](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) - Crop, resize, rotate, scale, or flip image content.
- [Photo Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) - Start from a complete Android photo editing UI.
- [Background Removal](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) - Add subject extraction to the Android editor.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Programmatic Editing"
description: "Edit images with CE.SDK Engine APIs for automated or custom Android workflows."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/programmatic-dd4348/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-image/programmatic-dd4348/)
---
```kotlin file=@cesdk_android_examples/engine-guides-edit-image-programmatic/EditImageProgrammatically.kt reference-only
import android.net.Uri
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
suspend fun editImageProgrammatically(engine: Engine): ProgrammaticImageEditResult {
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
engine.scene.createFromImage(imageUri)
val page = engine.block.findByType(DesignBlockType.Page).first()
val imageFill = engine.block.getFill(page)
require(engine.block.getType(imageFill) == FillType.Image.key) {
"Expected the imported page to use an image fill."
}
engine.block.setWidth(block = page, value = 900F)
engine.block.setHeight(block = page, value = 600F)
engine.block.setContentFillMode(block = page, mode = ContentFillMode.COVER)
val adjustment = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.appendEffect(block = page, effectBlock = adjustment)
engine.block.setFloat(adjustment, property = "effect/adjustments/brightness", value = 0.08F)
engine.block.setFloat(adjustment, property = "effect/adjustments/contrast", value = 0.18F)
val editedPng = engine.block.export(block = page, mimeType = MimeType.PNG)
check(editedPng.hasRemaining()) { "PNG export is empty." }
return ProgrammaticImageEditResult(
width = engine.block.getWidth(page),
height = engine.block.getHeight(page),
fillMode = engine.block.getContentFillMode(page),
brightness = engine.block.getFloat(adjustment, property = "effect/adjustments/brightness"),
contrast = engine.block.getFloat(adjustment, property = "effect/adjustments/contrast"),
imageFillType = engine.block.getType(imageFill),
exportedPng = editedPng.asReadOnlyBuffer(),
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-edit-image-programmatic/ProgrammaticImageEditResult.kt reference-only
import ly.img.engine.ContentFillMode
import java.nio.ByteBuffer
data class ProgrammaticImageEditResult(
val width: Float,
val height: Float,
val fillMode: ContentFillMode,
val brightness: Float,
val contrast: Float,
val imageFillType: String,
val exportedPng: ByteBuffer,
)
```
Edit images with CE.SDK Engine APIs when your app needs automation,
template-driven output, or custom controls outside the editor UI.

> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-edit-image-programmatic)
Programmatic image editing works directly on the Engine scene graph. Use this
approach when app logic should load an image, apply a known edit recipe, and
export the result without asking the user to perform every operation in the
CE.SDK editor UI. For an interactive product surface, start with the
[Photo Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) instead.
This guide loads an image scene, locates the image-backed page, applies a small
set of block edits, adds a lightweight adjustment effect, and exports a PNG.
Dedicated Android guides cover deeper transform, crop, watermark, and export
workflows.
## Load an Image Scene
Start by creating a scene from an image URI. CE.SDK imports the image into a
scene with a page that can be edited through the Block API.
```kotlin highlight-android-load-image-scene
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
engine.scene.createFromImage(imageUri)
```
`createFromImage()` accepts Android `Uri` values, including app storage,
resource, file, and remote URLs that your app is allowed to load.
## Find the Image Block
After loading, find the page and read its fill. In this workflow, the imported
page is the image-bearing block that later edits and export calls target.
```kotlin highlight-android-find-image-block
val page = engine.block.findByType(DesignBlockType.Page).first()
val imageFill = engine.block.getFill(page)
require(engine.block.getType(imageFill) == FillType.Image.key) {
"Expected the imported page to use an image fill."
}
```
The type check keeps the sample explicit: the page fill must be an image fill
before the code applies image-editing operations to the page.
## Apply Basic Image Edits
Set the output frame and choose how the image fits that frame. These are
representative block-level edits; use the focused transform and crop guides
when your app needs full transform controls.
```kotlin highlight-android-apply-basic-edits
engine.block.setWidth(block = page, value = 900F)
engine.block.setHeight(block = page, value = 600F)
engine.block.setContentFillMode(block = page, mode = ContentFillMode.COVER)
```
`ContentFillMode.COVER` scales the image to cover the page frame and may crop
the edges. Use `ContentFillMode.CONTAIN` when the whole image must remain
visible inside the frame.
## Add an Adjustment Effect
Effects let you combine visual edits with the same scene workflow. This sample
adds an adjustments effect and writes two image adjustment properties.
```kotlin highlight-android-add-adjustment
val adjustment = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.appendEffect(block = page, effectBlock = adjustment)
engine.block.setFloat(adjustment, property = "effect/adjustments/brightness", value = 0.08F)
engine.block.setFloat(adjustment, property = "effect/adjustments/contrast", value = 0.18F)
```
Keep guide-level adjustment recipes small. The
[Adjust Colors](https://img.ly/docs/cesdk/android/colors/adjust-590d1e/) guide covers adjustment properties,
effect ordering, and reset behavior in more detail.
## Export the Edited Image
Export the edited page as PNG once all programmatic edits are applied. The
returned `ByteBuffer` contains the image bytes your app can save, upload, or
decode.
```kotlin highlight-android-export-image
val editedPng = engine.block.export(block = page, mimeType = MimeType.PNG)
check(editedPng.hasRemaining()) { "PNG export is empty." }
```
Use the broader [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) guide for format selection,
target dimensions, compression options, and export constraints.
## API Reference
| Android API | Purpose |
| --- | --- |
| `engine.scene.createFromImage(imageUri=_)` | Create a scene from an image URI. |
| `engine.block.findByType(type=_)` | Locate pages or other typed blocks in the current scene. |
| `engine.block.getFill(block=_)` | Read the fill assigned to the image-bearing page. |
| `engine.block.getType(block=_)` | Check that the loaded fill is an image fill. |
| `engine.block.setWidth(block=_, value=_)` | Set the page width in scene units. |
| `engine.block.setHeight(block=_, value=_)` | Set the page height in scene units. |
| `engine.block.setContentFillMode(block=_, mode=_)` | Control how the image fits inside the page frame. |
| `engine.block.createEffect(type=_)` | Create an effect block such as `EffectType.Adjustments`. |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Add an effect to a block's effect stack. |
| `engine.block.setFloat(block=_, property="effect/adjustments/brightness", value=_)` | Set adjustment brightness. |
| `engine.block.setFloat(block=_, property="effect/adjustments/contrast", value=_)` | Set adjustment contrast. |
| `engine.block.export(block=_, mimeType=_)` | Render the edited page to image bytes. |
## Next Steps
- [Transform Images](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) - Crop, resize, rotate, scale, or flip images using CE.SDK's built-in transformation tools.
- [Crop Images](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) - Cut out specific areas of an image to focus on key content or change aspect ratio.
- [Add Watermark](https://img.ly/docs/cesdk/android/edit-image/add-watermark-679de0/) - Add text and image watermarks to protect images, indicate ownership, or add branding using CE.SDK in Android.
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Explore export options, supported formats, and configuration features for sharing or rendering output.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Background Removal"
description: "Add a background removal action to the CE.SDK Android editor."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Remove Background](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) > [Plugins](https://img.ly/docs/cesdk/android/plugins-693c48/) > [Background Removal](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/)
---
```kotlin file=@cesdk_android_examples/editor-guides-background-removal/BackgroundRemovalEditorSolution.kt reference-only
import android.graphics.Bitmap
import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import ly.img.editor.Editor
import ly.img.editor.configuration.photo.PhotoConfigurationBuilder
import ly.img.editor.configuration.photo.callback.onCreate
import ly.img.editor.core.EditorScope
import ly.img.editor.core.component.Dock
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.configuration.then
import ly.img.editor.plugin.backgroundRemoval.BackgroundRemovalConfig
import ly.img.editor.plugin.backgroundRemoval.BackgroundRemovalMask
import ly.img.editor.plugin.backgroundRemoval.BackgroundRemovalPlugin
import ly.img.editor.plugin.backgroundRemoval.GoogleBackgroundRemovalConfig
import ly.img.editor.plugin.backgroundRemoval.GoogleBackgroundRemovalPlugin
import ly.img.editor.plugin.backgroundRemoval.IMGLYBackgroundRemovalConfig
import ly.img.editor.plugin.backgroundRemoval.IMGLYBackgroundRemovalPlugin
import ly.img.editor.plugin.backgroundRemoval.rememberBackgroundRemoval
import ly.img.editor.plugin.backgroundRemoval.remover.BackgroundRemover
import okhttp3.OkHttpClient
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.TimeUnit
// Add this composable to your NavHost.
@Composable
fun BackgroundRemovalEditorSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder) {
onCreate = {
onCreate(
createScene = {
editorContext.engine.scene.createFromImage(
imageUri = "https://images.unsplash.com/photo-1438761681033-6461ffad8d80".toUri(),
)
},
)
}
}
.then(::IMGLYBackgroundRemovalPlugin)
},
onClose = onClose,
)
}
@Composable
private fun BackgroundRemovalEditorSolutionWithIMGLYConfiguration(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::IMGLYBackgroundRemovalPlugin) {
config = IMGLYBackgroundRemovalConfig(
model = IMGLYBackgroundRemovalConfig.Model.FP16,
modelBaseUri = "https://staticimgly.com/imgly/plugin-mobile-background-removal/1.0.0".toUri(),
loadMode = IMGLYBackgroundRemovalConfig.LoadMode.EAGER,
httpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build(),
)
dockModifier = {
addFirst { Dock.Button.rememberBackgroundRemoval(config = it) }
}
}
},
onClose = onClose,
)
}
@Composable
private fun BackgroundRemovalEditorSolutionWithGoogle(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::GoogleBackgroundRemovalPlugin)
},
onClose = onClose,
)
}
@Composable
private fun BackgroundRemovalEditorSolutionWithGoogleConfiguration(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::GoogleBackgroundRemovalPlugin) {
config = GoogleBackgroundRemovalConfig(
httpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build(),
)
dockModifier = {
addFirst { Dock.Button.rememberBackgroundRemoval(config = it) }
}
}
},
onClose = onClose,
)
}
private data class CustomBackgroundRemovalConfig(
override val httpClient: OkHttpClient = OkHttpClient(),
) : BackgroundRemovalConfig {
override val remover: BackgroundRemover<*> = CustomBackgroundRemover()
}
private class CustomBackgroundRemover : BackgroundRemover {
override fun EditorScope.initialize() {
// Prepare local models, SDK clients, or service credentials here.
}
override suspend fun EditorScope.processImage(bitmap: Bitmap): BackgroundRemovalMask {
val width = bitmap.width
val height = bitmap.height
val buffer = ByteBuffer
.allocateDirect(width * height * Float.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
// Dummy data filled with 1s.
repeat(width * height) {
buffer.putFloat(1f)
}
buffer.rewind()
return BackgroundRemovalMask(
buffer = buffer,
width = width,
height = height,
)
}
}
@Composable
private fun BackgroundRemovalEditorSolutionWithCustomRemover(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::BackgroundRemovalPlugin) {
config = CustomBackgroundRemovalConfig()
}
},
onClose = onClose,
)
}
@Composable
private fun BackgroundRemovalEditorSolutionWithCustomDockModifier(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::BackgroundRemovalPlugin) {
config = CustomBackgroundRemovalConfig()
dockModifier = {
addFirst { Dock.Button.rememberBackgroundRemoval(config = it) }
}
}
},
onClose = onClose,
)
}
```
Add a background removal action to your Android editor so users can remove an image background and continue editing the result.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-background-removal)
The Background Removal plugin adds a dock action that processes the current page image and replaces its fill with a transparent-background result. This guide explores the [IMG.LY ONNX implementation](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/#imgly-implementation), the [Google ML Kit implementation](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/#google-implementation), and a [custom remover implementation](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/#custom-background-remover).
> **Warning:** Background removal is applied to the current page block. The page must use an image fill for the plugin to process it.
For a complete Photo Editor setup, see the [Photo Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/).
## IMG.LY Implementation
The IMG.LY implementation runs an ONNX segmentation model on the device. Use it when you want to control the model variant, model asset location, and model loading behavior.
```groovy
dependencies {
implementation("ly.img:plugin-background-removal-imgly:$UBQ_VERSION$")
}
```
### Minimal Implementation
Compose `IMGLYBackgroundRemovalPlugin` with your editor configuration. The plugin inserts the dock button and uses the default IMG.LY configuration.
```kotlin highlight-android-imgly-minimal
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder) {
onCreate = {
onCreate(
createScene = {
editorContext.engine.scene.createFromImage(
imageUri = "https://images.unsplash.com/photo-1438761681033-6461ffad8d80".toUri(),
)
},
)
}
}
.then(::IMGLYBackgroundRemovalPlugin)
},
onClose = onClose,
)
```
### Configuration Options
Configure `IMGLYBackgroundRemovalPlugin` when you need to select a model, change where model files are loaded from, control eager or lazy loading, customize network timeouts, or move the dock button.
```kotlin highlight-android-imgly-configuration
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::IMGLYBackgroundRemovalPlugin) {
config = IMGLYBackgroundRemovalConfig(
model = IMGLYBackgroundRemovalConfig.Model.FP16,
modelBaseUri = "https://staticimgly.com/imgly/plugin-mobile-background-removal/1.0.0".toUri(),
loadMode = IMGLYBackgroundRemovalConfig.LoadMode.EAGER,
httpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build(),
)
dockModifier = {
addFirst { Dock.Button.rememberBackgroundRemoval(config = it) }
}
}
},
onClose = onClose,
)
```
| Option | Default | Description |
| --- | --- | --- |
| `config.model` | `IMGLYBackgroundRemovalConfig.Model.FP16` | Chooses the ONNX model variant. `FP32` favors quality and file size is largest, `FP16` is the default balance, and `QUINT8` is smaller and faster with a stronger quality trade-off. |
| `config.modelBaseUri` | `https://staticimgly.com/imgly/plugin-mobile-background-removal/1.0.0` | Base URI used to resolve the selected model file. The plugin appends `config.model.key`, for example `isnet_fp16.onnx`. You can download the models and store them in your own CDN, or download the files into the assets folder of your app and set `modelBaseUri = "file:///android_asset".toUri()`. |
| `config.loadMode` | `IMGLYBackgroundRemovalConfig.LoadMode.EAGER` | `EAGER` starts loading the model during plugin initialization. `LAZY` waits until the first removal action. |
| `config.httpClient` | Default `OkHttpClient` | HTTP client used to load source images and remote model assets. Override it for app-specific timeouts or interceptors. |
| `dockModifier` | Adds the button first | Changes where `Dock.Button.rememberBackgroundRemoval()` is inserted in the dock. |
> **Note:** You can improve the initial wait time further by pre-downloading the model outside the editor. Simply create an instance of `IMGLYBackgroundRemover` with the desired config and call `forceDownloadModel(context)` function.
## Google Implementation
The Google implementation uses Google's on-device ML Kit segmentation backend. Use it when you want an on-device segmentation option with the same dock button placement controls.
```groovy
dependencies {
implementation("ly.img:plugin-background-removal-google:$UBQ_VERSION$")
}
```
### Minimal Implementation
Compose `GoogleBackgroundRemovalPlugin` with your editor configuration.
```kotlin highlight-android-google-minimal
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::GoogleBackgroundRemovalPlugin)
},
onClose = onClose,
)
```
### Configuration Options
Configure `GoogleBackgroundRemovalPlugin` when you need to customize the HTTP client or move the dock button.
```kotlin highlight-android-google-configuration
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::GoogleBackgroundRemovalPlugin) {
config = GoogleBackgroundRemovalConfig(
httpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build(),
)
dockModifier = {
addFirst { Dock.Button.rememberBackgroundRemoval(config = it) }
}
}
},
onClose = onClose,
)
```
| Option | Default | Description |
| --- | --- | --- |
| `config.httpClient` | Default `OkHttpClient` | HTTP client used by the plugin when it loads source images. Override it for app-specific timeouts or interceptors. |
| `dockModifier` | Adds the button first | Changes where `Dock.Button.rememberBackgroundRemoval()` is inserted in the dock. |
## Custom Background Remover
For custom segmentation, include the base plugin package and implement `BackgroundRemover`. Your config object selects the remover, and `BackgroundRemovalPlugin` keeps the same dock action and editor integration.
```groovy
dependencies {
implementation("ly.img:plugin-background-removal:$UBQ_VERSION$")
}
```
### Minimal Implementation
Implement `BackgroundRemovalConfig` to select your remover, then compose the base plugin with your custom config. `initialize()` prepares your backend, and `processImage()` returns the foreground mask.
```kotlin highlight-android-custom-remover
private data class CustomBackgroundRemovalConfig(
override val httpClient: OkHttpClient = OkHttpClient(),
) : BackgroundRemovalConfig {
override val remover: BackgroundRemover<*> = CustomBackgroundRemover()
}
private class CustomBackgroundRemover : BackgroundRemover {
override fun EditorScope.initialize() {
// Prepare local models, SDK clients, or service credentials here.
}
override suspend fun EditorScope.processImage(bitmap: Bitmap): BackgroundRemovalMask {
val width = bitmap.width
val height = bitmap.height
val buffer = ByteBuffer
.allocateDirect(width * height * Float.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
// Dummy data filled with 1s.
repeat(width * height) {
buffer.putFloat(1f)
}
buffer.rewind()
return BackgroundRemovalMask(
buffer = buffer,
width = width,
height = height,
)
}
}
```
```kotlin highlight-android-custom-plugin
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::BackgroundRemovalPlugin) {
config = CustomBackgroundRemovalConfig()
}
},
onClose = onClose,
)
```
### Configuration Options
Configure `BackgroundRemovalPlugin` when you need to move the dock button.
```kotlin highlight-android-custom-dock-modifier
Editor(
license = license,
configuration = {
EditorConfiguration
.remember(::PhotoConfigurationBuilder)
.then(::BackgroundRemovalPlugin) {
config = CustomBackgroundRemovalConfig()
dockModifier = {
addFirst { Dock.Button.rememberBackgroundRemoval(config = it) }
}
}
},
onClose = onClose,
)
```
| Option | Default | Description |
| --- | --- | --- |
| `dockModifier` | Adds the button first | Changes where `Dock.Button.rememberBackgroundRemoval()` is inserted in the dock. |
## Troubleshooting
- If the button is missing, verify that the selected plugin is composed with the editor configuration.
- If removal fails, check that the current page block has an image fill.
- If the IMG.LY implementation cannot load a model, verify that `modelBaseUri` plus the selected model filename resolves to a readable `.onnx` file.
- If the first IMG.LY run feels slow, use `LoadMode.EAGER` or bundle the selected model in app assets.
## API Reference
| API | Purpose |
| --- | --- |
| `IMGLYBackgroundRemovalPlugin` | Adds the background removal action with the IMG.LY ONNX Runtime backend. |
| `IMGLYBackgroundRemovalConfig` | Configures IMG.LY model selection, model URI, loading behavior, and HTTP loading. |
| `GoogleBackgroundRemovalPlugin` | Adds the background removal action with Google's on-device segmentation backend. |
| `GoogleBackgroundRemovalConfig` | Configures HTTP loading for the Google implementation. |
| `BackgroundRemovalPlugin` | Base plugin used with a custom `BackgroundRemovalConfig`. |
| `BackgroundRemover.initialize()` | Prepares a custom remover before use. |
| `BackgroundRemover.processImage(bitmap=_)` | Produces the foreground mask for an image. |
| `Dock.Button.rememberBackgroundRemoval(config=_)` | Creates the reusable background removal dock button. |
| `EditorConfiguration.then()` | Composes the selected plugin with the base editor configuration. |
## Next Steps
- [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) - Configure the editor for your app.
- [Photo Editor](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) - Start from the Photo Editor starter kit.
- [Open the Editor From an Image](https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/) - Start the editor with image content.
- [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Customize dock items and ordering.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Replace Colors"
description: "Replace specific colors in images using CE.SDK's Recolor and Green Screen effects with programmatic control."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/replace-colors-6ede17/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Replace Colors](https://img.ly/docs/cesdk/android/edit-image/replace-colors-6ede17/)
---
```kotlin file=@cesdk_android_examples/engine-guides-colors-replace/ColorsReplace.kt reference-only
import android.net.Uri
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
fun colorsReplace(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
fun addImageBlock(
x: Float,
y: Float,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 150F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block, fill = fill)
return block
}
val recolorBlock = addImageBlock(x = 50F, y = 50F)
val recolorEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0.5F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = recolorBlock, effectBlock = recolorEffect)
check(engine.block.getEffects(recolorBlock) == listOf(recolorEffect))
val tolerancesBlock = addImageBlock(x = 300F, y = 50F)
val tolerancesEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.6F, b = 0.4F, a = 1F),
)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.3F, g = 0.7F, b = 0.3F, a = 1F),
)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/colorMatch", value = 0.3F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/brightnessMatch", value = 0.2F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/smoothness", value = 0.1F)
engine.block.appendEffect(block = tolerancesBlock, effectBlock = tolerancesEffect)
check(engine.block.getFloat(tolerancesEffect, property = "effect/recolor/colorMatch") == 0.3F)
check(engine.block.getFloat(tolerancesEffect, property = "effect/recolor/brightnessMatch") == 0.2F)
check(engine.block.getFloat(tolerancesEffect, property = "effect/recolor/smoothness") == 0.1F)
val greenScreenBlock = addImageBlock(x = 550F, y = 50F)
val greenScreenEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = greenScreenEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = greenScreenBlock, effectBlock = greenScreenEffect)
check(engine.block.getEffects(greenScreenBlock) == listOf(greenScreenEffect))
val spillBlock = addImageBlock(x = 50F, y = 250F)
val spillEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = spillEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0.2F, g = 0.8F, b = 0.3F, a = 1F),
)
engine.block.setFloat(spillEffect, property = "effect/green_screen/colorMatch", value = 0.4F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/smoothness", value = 0.2F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/spill", value = 0.5F)
engine.block.appendEffect(block = spillBlock, effectBlock = spillEffect)
check(engine.block.getFloat(spillEffect, property = "effect/green_screen/colorMatch") == 0.4F)
check(engine.block.getFloat(spillEffect, property = "effect/green_screen/smoothness") == 0.2F)
check(engine.block.getFloat(spillEffect, property = "effect/green_screen/spill") == 0.5F)
val stackedBlock = addImageBlock(x = 300F, y = 250F)
val redToBlue = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = redToBlue)
val stackedGreenScreen = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = stackedGreenScreen,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = stackedGreenScreen)
val stackedEffects = engine.block.getEffects(stackedBlock)
check(stackedEffects == listOf(redToBlue, stackedGreenScreen))
engine.block.setEffectEnabled(effectBlock = stackedEffects[0], enabled = false)
val isFirstEffectEnabled = engine.block.isEffectEnabled(stackedEffects[0])
engine.block.removeEffect(block = stackedBlock, index = 1)
engine.block.destroy(stackedGreenScreen)
check(!isFirstEffectEnabled)
check(engine.block.getEffects(stackedBlock) == listOf(redToBlue))
val batchBlock = addImageBlock(x = 550F, y = 250F)
val allGraphicBlocks = engine.block.findByType(type = DesignBlockType.Graphic)
for (block in allGraphicBlocks) {
if (engine.block.getEffects(block).isNotEmpty()) {
continue
}
val batchRecolor = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.7F, b = 0.6F, a = 1F),
)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.6F, g = 0.7F, b = 0.9F, a = 1F),
)
engine.block.setFloat(batchRecolor, property = "effect/recolor/colorMatch", value = 0.25F)
engine.block.appendEffect(block = block, effectBlock = batchRecolor)
}
check(engine.block.getEffects(batchBlock).size == 1)
}
```
Transform images by swapping specific colors with the Recolor effect or by removing background colors with the Green Screen effect in CE.SDK.

> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-replace)
CE.SDK offers two color replacement effects. The Recolor effect swaps one color for another while preserving image details. The Green Screen effect removes matching colors so the background can become transparent.
This guide covers the default Android effects UI and the Engine APIs you can use when your app needs to apply the same color replacement programmatically.
## Using the Built-in Effects UI
The default Android editor exposes effects through its built-in appearance controls when users select a compatible image-backed graphic block. Users can choose Recolor and pick source and replacement colors, or choose Green Screen and pick the source color to remove. They can then adjust matching controls and preview the result immediately.
The built-in controls write to the same effect blocks and properties shown below. Use the Engine APIs when you need presets, batch processing, or app-specific automation.
## Programmatic Color Replacement
### Prepare an Image Block
Color replacement effects attach to blocks that support effects. This sample creates a scene, adds a page, and uses a helper to create image-backed graphic blocks for the later examples.
```kotlin highlight-android-prepare-scene
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
```kotlin highlight-android-create-image-blocks
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
fun addImageBlock(
x: Float,
y: Float,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 150F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block, fill = fill)
return block
}
```
### Creating and Applying Recolor Effects
The Recolor effect swaps one color for another throughout an image. Create an `EffectType.Recolor` block, write the source and target colors, then append the effect to the image block.
```kotlin highlight-android-create-recolor
val recolorBlock = addImageBlock(x = 50F, y = 50F)
val recolorEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = recolorEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0.5F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = recolorBlock, effectBlock = recolorEffect)
```
Color values use `Color.fromRGBA()` with channel values from `0F` to `1F`. The `fromColor` property selects the color to match, and `toColor` defines the replacement color.
### Configuring Color Matching
Tune Recolor matching with float properties on the effect block. Higher `colorMatch` values include a broader range of source colors, while higher `brightnessMatch` values require pixels to be closer in brightness. Lower `brightnessMatch` when the matched color should include brighter or darker areas of the same hue and saturation.
```kotlin highlight-android-configure-recolor
val tolerancesBlock = addImageBlock(x = 300F, y = 50F)
val tolerancesEffect = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.6F, b = 0.4F, a = 1F),
)
engine.block.setColor(
block = tolerancesEffect,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.3F, g = 0.7F, b = 0.3F, a = 1F),
)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/colorMatch", value = 0.3F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/brightnessMatch", value = 0.2F)
engine.block.setFloat(tolerancesEffect, property = "effect/recolor/smoothness", value = 0.1F)
engine.block.appendEffect(block = tolerancesBlock, effectBlock = tolerancesEffect)
```
The Recolor effect supports these matching properties:
| Property | Description |
| --- | --- |
| `effect/recolor/colorMatch` | Controls how broadly source colors can differ from `fromColor` |
| `effect/recolor/brightnessMatch` | Controls how closely pixel brightness must match; lower values allow more brightness variation |
| `effect/recolor/smoothness` | Blends replacement edges to reduce artifacts |
### Creating and Applying Green Screen Effects
The Green Screen effect removes pixels that match a source color. Create an `EffectType.GreenScreen` block, set `effect/green_screen/fromColor`, and append it to the image block.
```kotlin highlight-android-create-green-screen
val greenScreenBlock = addImageBlock(x = 550F, y = 50F)
val greenScreenEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = greenScreenEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = greenScreenBlock, effectBlock = greenScreenEffect)
```
This is useful for solid-color backgrounds and keyed product images where the matched color should become transparent.
### Fine-Tuning Green Screen Removal
Adjust the Green Screen tolerance, edge blending, and spill suppression through float properties on the effect block.
```kotlin highlight-android-configure-green-screen
val spillBlock = addImageBlock(x = 50F, y = 250F)
val spillEffect = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = spillEffect,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0.2F, g = 0.8F, b = 0.3F, a = 1F),
)
engine.block.setFloat(spillEffect, property = "effect/green_screen/colorMatch", value = 0.4F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/smoothness", value = 0.2F)
engine.block.setFloat(spillEffect, property = "effect/green_screen/spill", value = 0.5F)
engine.block.appendEffect(block = spillBlock, effectBlock = spillEffect)
```
The Green Screen effect supports these properties:
| Property | Description |
| --- | --- |
| `effect/green_screen/colorMatch` | Controls how broadly background colors can differ from `fromColor` |
| `effect/green_screen/smoothness` | Feathers the edge between kept and removed pixels |
| `effect/green_screen/spill` | Reduces color contamination from the removed background |
### Managing Multiple Effects
Blocks keep effects in an ordered stack. Read the stack with `getEffects()`, toggle effects for before/after previews, and remove effects by their stack index when they are no longer needed.
```kotlin highlight-android-manage-effects
val stackedBlock = addImageBlock(x = 300F, y = 250F)
val redToBlue = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setColor(
block = redToBlue,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = redToBlue)
val stackedGreenScreen = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.setColor(
block = stackedGreenScreen,
property = "effect/green_screen/fromColor",
value = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F),
)
engine.block.appendEffect(block = stackedBlock, effectBlock = stackedGreenScreen)
val stackedEffects = engine.block.getEffects(stackedBlock)
check(stackedEffects == listOf(redToBlue, stackedGreenScreen))
engine.block.setEffectEnabled(effectBlock = stackedEffects[0], enabled = false)
val isFirstEffectEnabled = engine.block.isEffectEnabled(stackedEffects[0])
engine.block.removeEffect(block = stackedBlock, index = 1)
engine.block.destroy(stackedGreenScreen)
```
Effect order matters because CE.SDK renders effects sequentially. Use `insertEffect()` instead of `appendEffect()` when a new effect needs a specific position in the stack.
### Batch Processing Multiple Images
For presets or automated workflows, find image blocks and apply the same effect configuration to each compatible target.
```kotlin highlight-android-batch-processing
val allGraphicBlocks = engine.block.findByType(type = DesignBlockType.Graphic)
for (block in allGraphicBlocks) {
if (engine.block.getEffects(block).isNotEmpty()) {
continue
}
val batchRecolor = engine.block.createEffect(type = EffectType.Recolor)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/fromColor",
value = Color.fromRGBA(r = 0.8F, g = 0.7F, b = 0.6F, a = 1F),
)
engine.block.setColor(
block = batchRecolor,
property = "effect/recolor/toColor",
value = Color.fromRGBA(r = 0.6F, g = 0.7F, b = 0.9F, a = 1F),
)
engine.block.setFloat(batchRecolor, property = "effect/recolor/colorMatch", value = 0.25F)
engine.block.appendEffect(block = block, effectBlock = batchRecolor)
}
```
This pattern is useful for product variants, brand color updates, or bulk image cleanup. The sample skips blocks that already have effects so it does not overwrite earlier examples.
## Troubleshooting
| Issue | Fix |
| --- | --- |
| Effect is not visible | Check that the target block supports effects, verify the effect is attached with `getEffects()`, and confirm the effect is enabled. |
| Wrong colors are replaced | Lower `colorMatch` for a narrower match or adjust `brightnessMatch` for Recolor effects with lighting variation. |
| Edges look harsh | Increase `smoothness`; for Green Screen effects, adjust `spill` to reduce background color contamination. |
| Multiple effects render unexpectedly | Inspect the order returned by `getEffects()` and use `insertEffect()` when an effect needs a specific stack index. |
## API Reference
| API | Description |
| --- | --- |
| `engine.scene.create()` | Creates the scene that contains the image examples |
| `engine.block.create(blockType=DesignBlockType.Page)` | Creates the page that holds the image blocks |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Creates the image-backed graphic blocks used by the sample |
| `engine.block.createShape(type=ShapeType.Rect)` | Creates the rectangular shape for each graphic block |
| `engine.block.setShape(block=_, shape=_)` | Assigns the shape to the graphic block |
| `engine.block.setWidth(block=_, value=_)` | Sets page and block width values |
| `engine.block.setHeight(block=_, value=_)` | Sets page and block height values |
| `engine.block.setPositionX(block=_, value=_)` | Positions a graphic block horizontally |
| `engine.block.setPositionY(block=_, value=_)` | Positions a graphic block vertically |
| `engine.block.appendChild(parent=_, child=_)` | Adds pages and blocks to the scene hierarchy |
| `engine.block.createFill(fillType=FillType.Image)` | Creates an image fill for the graphic block |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Assigns the image URI used by the fill |
| `engine.block.setFill(block=_, fill=_)` | Applies the image fill to the graphic block |
| `engine.block.supportsEffects(block=_)` | Checks whether a block can render effects |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Creates normalized RGBA colors for effect properties |
| `engine.block.createEffect(type=EffectType.Recolor)` | Creates a Recolor effect block |
| `engine.block.createEffect(type=EffectType.GreenScreen)` | Creates a Green Screen effect block |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Adds an effect to the end of a block's effect stack |
| `engine.block.insertEffect(block=_, effectBlock=_, index=_)` | Inserts an effect at a specific stack index |
| `engine.block.getEffects(block=_)` | Returns the effects attached to a block |
| `engine.block.removeEffect(block=_, index=_)` | Removes the effect at the specified stack index |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Returns whether an effect block is enabled |
| `engine.block.setColor(block=_, property="effect/recolor/fromColor", value=_)` | Writes the source color that Recolor matches |
| `engine.block.setColor(block=_, property="effect/recolor/toColor", value=_)` | Writes the replacement color for Recolor |
| `engine.block.setColor(block=_, property="effect/green_screen/fromColor", value=_)` | Writes the color that Green Screen removes |
| `engine.block.getColor(block=_, property="effect/recolor/fromColor")` | Reads the source color that Recolor matches |
| `engine.block.getColor(block=_, property="effect/recolor/toColor")` | Reads the replacement color for Recolor |
| `engine.block.getColor(block=_, property="effect/green_screen/fromColor")` | Reads the color that Green Screen removes |
| `engine.block.setFloat(block=_, property="effect/recolor/colorMatch", value=_)` | Writes Recolor color matching tolerance |
| `engine.block.setFloat(block=_, property="effect/recolor/brightnessMatch", value=_)` | Writes Recolor brightness matching tolerance |
| `engine.block.setFloat(block=_, property="effect/recolor/smoothness", value=_)` | Writes Recolor edge smoothing |
| `engine.block.getFloat(block=_, property="effect/recolor/colorMatch")` | Reads Recolor color matching tolerance |
| `engine.block.getFloat(block=_, property="effect/recolor/brightnessMatch")` | Reads Recolor brightness matching tolerance |
| `engine.block.getFloat(block=_, property="effect/recolor/smoothness")` | Reads Recolor edge smoothing |
| `engine.block.setFloat(block=_, property="effect/green_screen/colorMatch", value=_)` | Writes Green Screen color matching tolerance |
| `engine.block.setFloat(block=_, property="effect/green_screen/smoothness", value=_)` | Writes Green Screen edge smoothing |
| `engine.block.setFloat(block=_, property="effect/green_screen/spill", value=_)` | Writes Green Screen spill suppression |
| `engine.block.getFloat(block=_, property="effect/green_screen/colorMatch")` | Reads Green Screen color matching tolerance |
| `engine.block.getFloat(block=_, property="effect/green_screen/smoothness")` | Reads Green Screen edge smoothing |
| `engine.block.getFloat(block=_, property="effect/green_screen/spill")` | Reads Green Screen spill suppression |
| `engine.block.findByType(type=DesignBlockType.Graphic)` | Finds graphic blocks for batch processing |
| `engine.block.destroy(block=_)` | Destroys unused effect blocks after removal |
## Next Steps
- [Adjust Colors](https://img.ly/docs/cesdk/android/colors/adjust-590d1e/) - Fine-tune image-backed graphic blocks by adjusting brightness, contrast, saturation, exposure, and other color properties.
- [Export Designs](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Save your color-replaced images in various formats.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Transform"
description: "Crop, resize, rotate, scale, or flip images using CE.SDK's built-in transformation tools."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/)
---
---
## Related Pages
- [Move](https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/) - Position an image relative to its parent using either percentage or units
- [Crop Images in Android](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) - Cut out specific areas of an image to focus on key content or change aspect ratio.
- [Rotate](https://img.ly/docs/cesdk/android/edit-image/transform/rotate-5f39c9/) - Documentation for Rotate
- [Resize Images](https://img.ly/docs/cesdk/android/edit-image/transform/resize-407242/) - Change image dimensions with absolute values, percentage sizing, crop-preserving resize operations, and group resizing.
- [Scale in Android (Kotlin)](https://img.ly/docs/cesdk/android/edit-image/transform/scale-ebe367/) - Resize images uniformly in your Android app using Kotlin.
- [Flip Images](https://img.ly/docs/cesdk/android/edit-image/transform/flip-035e9f/) - Flip images horizontally or vertically.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Crop Images in Android"
description: "Cut out specific areas of an image to focus on key content or change aspect ratio."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) > [Crop](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/)
---
The CreativeEditor SDK (CE.SDK) offers both interactive UI components and powerful Kotlin APIs for cropping images. Image cropping is an essential feature for any Android photo editing app, allowing users to focus on important content and fit images to specific dimensions. Whether you need simple aspect ratio adjustments or advanced programmatic, follow this guide to learn how to integrate cropping into your Android app.
## Interactive crop interface
The SDK includes ready-to-use crop controls that integrate seamlessly with your Android app. These components:
- Handle tap gestures and aspect ratio selection.
- Provide immediate visual feedback to users.
This is particularly useful for:
- Apps targeting social media formats.
- Maintaining consistent visual branding.

### How users interact with crop tools
1. **Tap the image** to select it for editing.
2. **Tap the crop button** in your app's editing interface.
3. **Drag handles** at corners and edges to define the crop region.
4. **Apply transformations** like flip or rotate before finalizing.
5. **Confirm changes** to complete the crop operation.

Once cropped, your image:
- Updates in the editor.
- Preserves the original data and transformation history for future adjustments.
### Configuring crop capabilities
By default, cropping is enabled in the editor UI. When building custom interfaces or specialized editing flows, you can control crop availability through configuration settings:
```kotlin
engine.editor.setSettingBoolean("doubleClickToCropEnabled", true)
engine.editor.setSettingBoolean("controlGizmo/showCropHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showCropScaleHandles", true)
```
The cropping handles are only available when a selected block has a fill of type `FillType.Image`. Otherwise setting the edit mode of the `engine.editor` to crop has no effect.
## Crop images with Kotlin code
For advanced Android applications, you'll often need precise control over cropping operations through code. This approach suits very well:
- Batch processing
- Automated workflows
- Custom editing interfaces implementations.
The SDK automatically handles image fitting when you load content into blocks – if your image dimensions don't match the container, intelligent cropping is applied automatically.
When implementing crop operations in your Kotlin code, keep in mind that you're manipulating the underlying image's scale, position, and orientation properties. The examples shown typically modify both x and y axes uniformly, but you can adjust them independently for creative distortion effects.
### Reset Crop
When an image is initially placed into a block it will get crop scale and crop translation values. Resetting the crop will return the image to the original values.

This is a block (called `imageBlock` in the example code) with the following elements:
- Dimensions of 400 × 400.
- Filled with an image that has dimensions of 600 × 530.
- The image has slight scaling and translation applied so that it fills the block evenly.
At any time, the code can execute the reset crop command to return it to this stage.
```kotlin
engine.block.resetCrop(imageBlock)
```
### Crop Translation
The **translation values**:
- Adjust the placement of the **origin point** of an image.
- Can be read and changed.
- Aren't pixel units or centimeters, but are scaled percentages.
An image that has its origin point at the origin point of the crop block will have a translation value of 0.0 for x and y.

```kotlin
engine.block.setCropTranslationX(imageBlock, 0.25f)
```
This image:
- Has had its translation in the x direction set to 0.25.
- Was moved 1/4 of its width to the right as a result.
Setting the value to -0.25 would shift the origin to the left.
These are absolute values. Setting the x value to 0.25 and then setting it to -0.25 does not move the image to an offset of 0.0.
How values might affect the image:
- `setCropTranslationY(block: DesignBlock, translationY: Float)` function adjusts the translation of the image in the **vertical direction**.
- **Negative** values move the image **up**.
- **Positive** values move the image **down**.
To read the current crop translation values you can use the convenience getters for the x and y values.
```kotlin
val currentX = engine.block.getCropTranslationX(imageBlock)
val currentY = engine.block.getCropTranslationY(imageBlock)
```
### Crop Scale
The scale values:
- Adjust the height and width of the underlying image.
- Make the image **larger** when greater than **1.0**.
- Make the image **smaller** when less than 1.0.
Unless the image also has offsetting translation applied, the center of the image will move.

This image has been scaled by 1.5 in the x and y directions, but the origin point has not been translated. So, the center of the image has moved.
```kotlin
engine.block.setCropScaleX(imageBlock, 1.5f)
engine.block.setCropScaleY(imageBlock, 1.5f)
```
To read the current crop scale values you can use the convenience getters for the x and y values.
```kotlin
val currentX = engine.block.getCropScaleX(imageBlock)
val currentY = engine.block.getCropScaleY(imageBlock)
```
## Crop Rotate
Similar to rotating blocks, the crop rotation function uses radians in the following way:
- **Positive** values rotate clockwise.
- **Negative** values rotate counterclockwise.
- The image rotates around its **center**.

```kotlin
import kotlin.math.PI
engine.block.setCropRotation(imageBlock, (PI / 4.0).toFloat())
```
For working with radians, Kotlin has a constant defined for pi. It can be used as `PI` from `kotlin.math.PI`. Because the `setCropRotation` function takes a `Float` for the rotation value, you can use `.toFloat()` to convert the Double to Float.
### Crop to Scale Ratio
To center crop an image, you can use the scale ratio. This will adjust the x and y scales of the image evenly, and adjust the translation to keep it centered.

This image has been scaled by 2.0 in the x and y directions. Its translation has been adjusted by -0.5 in the x and y directions to keep the image centered.
```kotlin
engine.block.setCropScaleRatio(imageBlock, 2.0f)
```
Using the crop scale ratio function is the same as calling the translation and scale functions, but in one line.
```kotlin
engine.block.setCropScaleX(imageBlock, 2.0f)
engine.block.setCropScaleY(imageBlock, 2.0f)
engine.block.setCropTranslationX(imageBlock, -0.5f)
engine.block.setCropTranslationY(imageBlock, -0.5f)
```
### Chained Crops
Crop operations can be chained together. The order of the chaining impacts the final image.

```kotlin
import kotlin.math.PI
engine.block.setCropScaleRatio(imageBlock, 2.0f)
engine.block.setCropRotation(imageBlock, (PI / 3.0).toFloat())
```

```kotlin
import kotlin.math.PI
engine.block.setCropRotation(imageBlock, (PI / 3.0).toFloat())
engine.block.setCropScaleRatio(imageBlock, 2.0f)
```
### Flipping the Crop
There are two functions for crop flipping the image:
- Horizontal
- Vertical
They each flip the image along its center.

```kotlin
engine.block.flipCropVertical(imageBlock)
engine.block.flipCropHorizontal(imageBlock)
```
The image will be crop flipped every time the function gets called. So calling the function an even number of times will return the image to its original orientation.
### Filling the Frame
When the various crop operations cause the background of the crop block to be displayed, such as in the **Crop Translation** example above, the function
```kotlin
engine.block.adjustCropToFillFrame(imageBlock, minScaleRatio = 1.0f)
```
will adjust the translation values and the scale values of the image so that the entire crop block is filled. This is not the same as resetting the crop.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Flip Images"
description: "Flip images horizontally or vertically."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform/flip-035e9f/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) > [Flip](https://img.ly/docs/cesdk/android/edit-image/transform/flip-035e9f/)
---
The CreativeEditor SDK includes a **flipping** feature that you can add to your **Android** app. Flipping is a powerful transformation that Android photo apps use for creating mirror effects, correcting orientation, and achieving symmetrical layouts. Learn in this guide how to implement both **interactive** flip controls and **programmatic** flipping, through clean **Kotlin** APIs.
## Flip features
The flip feature enables the following actions:
- Horizontal and vertical image mirroring
- Integration with template systems and creative workflows
- Programmatic flip state management and toggling
## Flip applications
Implement flipping for:
- Correcting selfie camera orientation in Android camera apps
- Creating mirror effects for product photography
- Building symmetrical design layouts and compositions
***
## Flip horizontally or vertically
Use the `flip/horizontal` and `flip/vertical` properties to control mirroring. These are **boolean** properties with defined helper functions. All flips occur around the center point of a block.
```kotlin
engine.block.setFlipVertical(imageBlock, true)
engine.block.setFlipHorizontal(imageBlock, true)
```
To determine if a block has been flipped, you can either:
- Query the **properties**.
- Use **helper functions**.
```kotlin
val isFlippedVertical = engine.block.getFlipVertical(imageBlock)
val isFlippedHorizontal = engine.block.getFlipHorizontal(imageBlock)
```
***
## Toggle flipping
To toggle the flip state, the code reads the current flip value and sets it to its opposite:
```kotlin
val currentVerticalFlip = engine.block.getFlipVertical(imageBlock)
engine.block.setFlipVertical(imageBlock, !currentVerticalFlip)
val currentHorizontalFlip = engine.block.getFlipHorizontal(imageBlock)
engine.block.setFlipHorizontal(imageBlock, !currentHorizontalFlip)
```
## Reset flipping
To reset all flips:
```kotlin
engine.block.setFlipVertical(imageBlock, false)
engine.block.setFlipHorizontal(imageBlock, false)
```
***
## Flip multiple elements
Group elements to flip them together:
```kotlin
val groupId = engine.block.group(listOf(imageBlock, textBlock))
engine.block.setFlipHorizontal(groupId, true)
```
***
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Move"
description: "Position an image relative to its parent using either percentage or units"
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) > [Move](https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/)
---
The CreativeEditor SDK provides a **positioning** feature you can add to your Android app. Positioning images accurately is fundamental to creating **professional layouts**. Both drag-and-drop interfaces and precise coordinate-based positioning are available through Kotlin. Whether you're building **grid layouts**, **freeform canvases**, or **template-based designs**, this guide covers all positioning needs.
## Movement Capabilities
The positioning feature in CE.SDK enables the following movements:
- **Precise positioning** with Kotlin coordinate APIs.
- **Drag-and-drop** interface for user interaction.
- Canvas-based absolute and **percentage** positioning.
- Group movement for **maintaining element relationships**.
- Position constraints for **template protection**.
## Position control scenarios
Implement positioning to:
- Create pixel-perfect layouts for Android interfaces.
- Enable intuitive drag-and-drop editing experiences.
- Create snap-to-grid or guided positioning systems.
***
## Move an image block programmatically
Image position is controlled using the `position/x` and `position/y` properties. They can use either absolute or relative (percentage) values. Helper functions are also available for setting properties.
For example, the following code moves the image to coordinates (150, 100) on the canvas.
```kotlin
engine.block.setFloat(imageBlock, "position/x", 150f)
engine.block.setFloat(imageBlock, "position/y", 100f)
```
or
```kotlin
engine.block.setPositionX(imageBlock, 150f)
engine.block.setPositionY(imageBlock, 100f)
```
For percentage-based positioning, the following code moves the image to the center of the canvas, regardless of the dimensions of the canvas:
```kotlin
import ly.img.engine.PositionMode
engine.block.setPositionXMode(imageBlock, PositionMode.PERCENT)
engine.block.setPositionYMode(imageBlock, PositionMode.PERCENT)
engine.block.setPositionX(imageBlock, 0.5f)
engine.block.setPositionY(imageBlock, 0.5f)
```
As with setting position, you can update or check the mode using `position/x/mode` and `position/y/mode` properties.
```kotlin
val xPosition = engine.block.getPositionX(imageBlock)
val yPosition = engine.block.getPositionY(imageBlock)
```
***
## Move images with the UI
Users can drag and drop elements directly in the editor canvas.
***
## Move multiple elements together
Group elements before moving to keep them aligned:
```kotlin
val groupId = engine.block.group(listOf(imageBlock, textBlock))
engine.block.setPositionX(groupId, 200f)
```
The preceding code moves the entire group to 200 from the left edge.
***
## Move relative to current position
To nudge an image instead of setting an absolute position:
```kotlin
val xPosition = engine.block.getPositionX(imageBlock)
engine.block.setPositionX(imageBlock, xPosition + 20f)
```
The preceding code moves the image 20 points to the right.
***
## Lock movement (optional)
When building templates, you might want to lock movement to protect the layout:
```kotlin
engine.block.setScopeEnabled(imageBlock, "layer/move", false)
```
You can also disable all transformations by locking, this is regardless of working with a template.
```kotlin
engine.block.setTransformLocked(imageBlock, true)
```
***
## Troubleshooting
| Issue | Solution |
| ------------------------ | ----------------------------------------------------- |
| Image not moving | Ensure it is not constrained or locked |
| Unexpected position | Check canvas coordinates and alignment settings |
| Grouped items misaligned | Confirm all items share the same reference point |
| Can't move via UI | Ensure the move feature is enabled in the UI settings |
***
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Resize Images"
description: "Change image dimensions with absolute values, percentage sizing, crop-preserving resize operations, and group resizing."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform/resize-407242/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) > [Resize](https://img.ly/docs/cesdk/android/edit-image/transform/resize-407242/)
---
```kotlin file=@cesdk_android_examples/engine-guides-edit-image-transform-resize/ResizeImages.kt reference-only
import android.net.Uri
import kotlinx.coroutines.yield
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.HandleVisibility
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import ly.img.engine.getResizeHandlesVisibility
import ly.img.engine.setResizeHandlesVisibility
data class ResizeImages(
val absoluteWidth: Float,
val absoluteHeight: Float,
val percentWidth: Float,
val percentHeight: Float,
val percentWidthMode: SizeMode,
val percentHeightMode: SizeMode,
val frameWidth: Float,
val frameHeight: Float,
val cropModeAfterResize: ContentFillMode,
val groupWidth: Float,
val pageWidthAfterContentAwareResize: Float,
val resizeHandlesVisibility: HandleVisibility,
val resizeScopeEnabled: Boolean,
val transformLocked: Boolean,
)
suspend fun resizeImages(engine: Engine): ResizeImages {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = createImageBlock(engine, page)
engine.editor.setResizeHandlesVisibility(HandleVisibility.ALWAYS)
val resizeHandlesVisibility = engine.editor.getResizeHandlesVisibility()
engine.block.setWidthMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(imageBlock, value = 400F)
engine.block.setHeight(imageBlock, value = 300F)
val absoluteWidth = engine.block.getWidth(imageBlock)
val absoluteHeight = engine.block.getHeight(imageBlock)
engine.block.setWidthMode(imageBlock, mode = SizeMode.PERCENT)
engine.block.setHeightMode(imageBlock, mode = SizeMode.PERCENT)
engine.block.setWidth(imageBlock, value = 0.5F)
engine.block.setHeight(imageBlock, value = 0.5F)
val percentWidth = engine.block.getWidth(imageBlock)
val percentHeight = engine.block.getHeight(imageBlock)
val percentWidthMode = engine.block.getWidthMode(imageBlock)
val percentHeightMode = engine.block.getHeightMode(imageBlock)
// Let the offscreen engine resolve one layout pass before reading frame dimensions.
yield()
val frameWidth = engine.block.getFrameWidth(imageBlock)
val frameHeight = engine.block.getFrameHeight(imageBlock)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CROP)
engine.block.setWidthMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(imageBlock, value = 520F, maintainCrop = true)
engine.block.setHeight(imageBlock, value = 320F, maintainCrop = true)
val cropModeAfterResize = engine.block.getContentFillMode(imageBlock)
val secondImageBlock = createImageBlock(engine, page).also { block ->
engine.block.setPositionX(block, value = 460F)
}
val group = engine.block.group(listOf(imageBlock, secondImageBlock))
engine.block.setWidth(group, value = 600F)
val groupWidth = engine.block.getWidth(group)
engine.block.resizeContentAware(blocks = listOf(page), width = 1080F, height = 1080F)
val pageWidthAfterContentAwareResize = engine.block.getWidth(page)
engine.block.setScopeEnabled(block = group, key = "layer/resize", enabled = false)
val resizeScopeEnabled = engine.block.isScopeEnabled(block = group, key = "layer/resize")
engine.block.setTransformLocked(block = group, locked = true)
val transformLocked = engine.block.isTransformLocked(group)
return ResizeImages(
absoluteWidth = absoluteWidth,
absoluteHeight = absoluteHeight,
percentWidth = percentWidth,
percentHeight = percentHeight,
percentWidthMode = percentWidthMode,
percentHeightMode = percentHeightMode,
frameWidth = frameWidth,
frameHeight = frameHeight,
cropModeAfterResize = cropModeAfterResize,
groupWidth = groupWidth,
pageWidthAfterContentAwareResize = pageWidthAfterContentAwareResize,
resizeHandlesVisibility = resizeHandlesVisibility,
resizeScopeEnabled = resizeScopeEnabled,
transformLocked = transformLocked,
)
}
private fun createImageBlock(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = 320F)
engine.block.setHeight(imageBlock, value = 240F)
engine.block.setPositionX(imageBlock, value = 120F)
engine.block.setPositionY(imageBlock, value = 120F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.appendChild(parent = page, child = imageBlock)
return imageBlock
}
```
Change image dimensions by setting exact width and height values, switching
size modes, or resizing grouped blocks together.

> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-edit-image-transform-resize)
Image resizing changes a block's dimensions rather than applying a scale multiplier. Use `engine.block.setWidth()` and `engine.block.setHeight()` for individual dimensions, and choose a `SizeMode` to control how each value is interpreted.
This guide covers resizing image blocks with absolute or percentage sizing, preserving crop state during resize, and locking resize permissions for templates.
## Create an Image Block
Create a graphic block with an image fill before applying resize operations:
```kotlin highlight-android-create-image-block
private fun createImageBlock(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = 320F)
engine.block.setHeight(imageBlock, value = 240F)
engine.block.setPositionX(imageBlock, value = 120F)
engine.block.setPositionY(imageBlock, value = 120F)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.appendChild(parent = page, child = imageBlock)
return imageBlock
}
```
The resize APIs operate on the block frame. The image fill remains attached to the graphic block and follows the crop and fill mode settings.
## Understanding Size Modes
Size values are interpreted in three modes. `SizeMode.ABSOLUTE` uses fixed design units, `SizeMode.PERCENT` uses parent-relative values from `0.0F` to `1.0F`, and `SizeMode.AUTO` lets CE.SDK calculate the size from content where supported.
Use `engine.block.getWidth()` and `engine.block.getHeight()` to read configured values. Use `engine.block.getFrameWidth()` and `engine.block.getFrameHeight()` when you need the calculated layout dimensions after CE.SDK resolves the block.
## Using the Built-In Resize UI
The CE.SDK editor UI shows resize handles when a selectable block supports resize operations. You can hide, show, or defer the non-proportional edge handles with the typed resize-handle visibility API:
```kotlin highlight-android-resize-handles
engine.editor.setResizeHandlesVisibility(HandleVisibility.ALWAYS)
val resizeHandlesVisibility = engine.editor.getResizeHandlesVisibility()
```
This API controls handle visibility only. Programmatic resize APIs and other transform controls still follow the block's scopes and transform lock state.
For a complete photo editing surface, see the [Photo Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/).
## Setting Absolute Dimensions
Set explicit dimensions by switching both axes to `SizeMode.ABSOLUTE`, then writing width and height values:
```kotlin highlight-android-absolute-size
engine.block.setWidthMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(imageBlock, value = 400F)
engine.block.setHeight(imageBlock, value = 300F)
val absoluteWidth = engine.block.getWidth(imageBlock)
val absoluteHeight = engine.block.getHeight(imageBlock)
```
Absolute sizing is the most direct option for fixed layouts, templates, and exports where the target design units are known.
## Percentage Sizing
Use percentage mode for responsive sizing. A value of `1.0F` means 100 percent of the parent on that axis:
```kotlin highlight-android-percent-size
engine.block.setWidthMode(imageBlock, mode = SizeMode.PERCENT)
engine.block.setHeightMode(imageBlock, mode = SizeMode.PERCENT)
engine.block.setWidth(imageBlock, value = 0.5F)
engine.block.setHeight(imageBlock, value = 0.5F)
val percentWidth = engine.block.getWidth(imageBlock)
val percentHeight = engine.block.getHeight(imageBlock)
val percentWidthMode = engine.block.getWidthMode(imageBlock)
val percentHeightMode = engine.block.getHeightMode(imageBlock)
```
Percentage sizing adapts when the parent dimensions change, which makes it useful for reusable templates and generated layouts.
## Getting Frame Dimensions
Configured width and height can differ from the rendered frame in percentage and auto modes. Read the frame dimensions when you need the final layout size:
```kotlin highlight-android-frame-dimensions
val frameWidth = engine.block.getFrameWidth(imageBlock)
val frameHeight = engine.block.getFrameHeight(imageBlock)
```
Frame dimensions are only available after CE.SDK has resolved layout for the block. In an offscreen sample, let the engine process one layout or update pass after changing percentage or auto sizes before calling these getters; the complete source does that immediately before the highlighted read.
## Maintaining Crop During Resize
When you resize an image block, you change the image frame. The fill and crop state control how the image content remains framed inside that new size.
For reusable code that may receive other block types, call `engine.block.supportsContentFillMode()` before reading or setting content fill mode.
Pass `maintainCrop = true` when the current crop should stay visually stable while the frame changes:
```kotlin highlight-android-maintain-crop
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CROP)
engine.block.setWidthMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setHeightMode(imageBlock, mode = SizeMode.ABSOLUTE)
engine.block.setWidth(imageBlock, value = 520F, maintainCrop = true)
engine.block.setHeight(imageBlock, value = 320F, maintainCrop = true)
val cropModeAfterResize = engine.block.getContentFillMode(imageBlock)
```
Use this for user-adjusted images or layout changes where visual continuity matters. Leave `maintainCrop` at its default when crop values should stay unadjusted and the visible framing can change according to the current fill and crop state.
## Resizing Groups
Group multiple blocks, then resize the group block to keep the members together:
```kotlin highlight-android-group-resize
val group = engine.block.group(listOf(imageBlock, secondImageBlock))
engine.block.setWidth(group, value = 600F)
val groupWidth = engine.block.getWidth(group)
```
When a group is resized, CE.SDK keeps the group aspect ratio and updates both dimensions proportionally.
## Content-Aware Resizing
Use `resizeContentAware()` when changing page dimensions for another output format:
```kotlin highlight-android-content-aware-resize
engine.block.resizeContentAware(blocks = listOf(page), width = 1080F, height = 1080F)
val pageWidthAfterContentAwareResize = engine.block.getWidth(page)
```
This keeps full-page blocks attached to the page and scales other content proportionally.
## Locking Resize Operations
Disable the `layer/resize` scope when a template block should stay at its configured size. Use a transform lock when users should not move, rotate, or resize the block at all:
```kotlin highlight-android-lock-resize
engine.block.setScopeEnabled(block = group, key = "layer/resize", enabled = false)
val resizeScopeEnabled = engine.block.isScopeEnabled(block = group, key = "layer/resize")
engine.block.setTransformLocked(block = group, locked = true)
val transformLocked = engine.block.isTransformLocked(group)
```
## Troubleshooting
### Image Not Resizing
Check whether `layer/resize` is disabled or the block is transform-locked. Then verify that the block exists and that the width and height modes match the values you write.
### Unexpected Size Values
Read `getWidthMode()` and `getHeightMode()` before interpreting numeric values. In percentage mode, `0.5F` means 50 percent of the parent, not 0.5 design units.
### Image Appears Cropped
Resize changes the frame around the image content. Use `maintainCrop = true` to preserve the existing crop framing, or adjust the crop after resize when you want a different composition.
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.create(blockType=_)` | Create a graphic or page block |
| `engine.block.createShape(type=_)` | Create the shape used by a graphic block |
| `engine.block.setShape(block=_, shape=_)` | Attach a shape to a graphic block |
| `engine.block.setPositionX(block=_, value=_)` | Set a block's x position |
| `engine.block.setPositionY(block=_, value=_)` | Set a block's y position |
| `engine.block.createFill(fillType=_)` | Create an image fill |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on the fill |
| `engine.block.setFill(block=_, fill=_)` | Attach a fill to a block |
| `engine.block.appendChild(parent=_, child=_)` | Add the image block to the page |
| `engine.editor.setResizeHandlesVisibility(value=_)` | Set when editor resize handles are shown |
| `engine.editor.getResizeHandlesVisibility()` | Read when editor resize handles are shown |
| `engine.block.setWidthMode(block=_, mode=_)` | Set how CE.SDK interprets the width value |
| `engine.block.setHeightMode(block=_, mode=_)` | Set how CE.SDK interprets the height value |
| `engine.block.setWidth(block=_, value=_, maintainCrop=_)` | Set a block width and optionally preserve crop state |
| `engine.block.setHeight(block=_, value=_, maintainCrop=_)` | Set a block height and optionally preserve crop state |
| `engine.block.getWidth(block=_)` | Read the configured width value |
| `engine.block.getHeight(block=_)` | Read the configured height value |
| `engine.block.getWidthMode(block=_)` | Read the width mode |
| `engine.block.getHeightMode(block=_)` | Read the height mode |
| `engine.block.getFrameWidth(block=_)` | Read the resolved frame width after layout |
| `engine.block.getFrameHeight(block=_)` | Read the resolved frame height after layout |
| `engine.block.supportsContentFillMode(block=_)` | Check whether a block exposes content fill mode |
| `engine.block.setContentFillMode(block=_, mode=_)` | Set how image content fills its frame |
| `engine.block.getContentFillMode(block=_)` | Read how image content fills its frame |
| `engine.block.group(blocks=_)` | Group blocks before resizing them together |
| `engine.block.resizeContentAware(blocks=_, width=_, height=_)` | Resize blocks while adjusting contained content |
| `engine.block.setScopeEnabled(block=_, key="layer/resize", enabled=_)` | Enable or disable resize permission for a block |
| `engine.block.isScopeEnabled(block=_, key="layer/resize")` | Read whether resize permission is enabled |
| `engine.block.setTransformLocked(block=_, locked=_)` | Lock or unlock all transforms on a block |
| `engine.block.isTransformLocked(block=_)` | Read the transform lock state |
## Next Steps
- Resize images proportionally with [Scale](https://img.ly/docs/cesdk/android/edit-image/transform/scale-ebe367/).
- Control image framing and visible content with [Crop](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/).
- Apply resizing across complete designs with [Auto-Resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/).
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Rotate"
description: "Documentation for Rotate"
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform/rotate-5f39c9/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) > [Rotate](https://img.ly/docs/cesdk/android/edit-image/transform/rotate-5f39c9/)
---
The CreativeEditor SDK provides a **rotation** feature for Android apps. Image rotation is a core editing feature that Android users expect in photo apps.
The CreativeEditor SDK offers straightforward methods for incorporating both **touch-based rotation** controls and precise **programmatic rotation** using **Kotlin**. This guide covers everything from basic rotation gestures to advanced group transformations and rotation constraints.
## Key rotation features
- Touch-based rotation with intuitive drag handles
- Precise angle control through Kotlin APIs
- Rotation locking for template protection
- Multi-element rotation as grouped objects
### Touch-based image rotation
Users can rotate images naturally using the built-in rotation handles. When you select an image:
1. Rotation controls automatically appear.
2. You can now freely rotate the image through drag gestures.

### Implementing rotation in Kotlin
Your Android app can control image rotation programmatically using the `setRotation` method. Pass the block ID and rotation angle in radians:
```kotlin
import kotlin.math.PI
engine.block.setRotation(imageBlock, (PI / 4).toFloat())
```
Since Android developers often work with degrees, here are handy conversion utilities:
```kotlin
val angleInRadians: Float = (angleInDegrees * PI / 180).toFloat()
val angleInDegrees: Float = (angleInRadians * 180 / PI).toFloat()
```
To read the current rotation state in your app:
```kotlin
val currentRotation = engine.block.getRotation(imageBlock)
```
> **Note:** This rotates the entire block. If you want to rotate an image that is filling
> a block but not the block, explore the
> [crop rotate](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) function.
### Locking Rotation
You can remove the rotation handle from the UI by changing the setting for the engine. This will affect *all* blocks.
```kotlin
engine.editor.setSettingBoolean("controlGizmo/showRotateHandles", false)
```
Although the code makes the rotation handle invisible, the user can still use the two-finger rotation gesture on a touch device. You can turn off that gesture with the following setting:
```kotlin
engine.editor.setSettingBoolean("touch/rotateAction", false)
```
When you want to lock only certain blocks, you can toggle the transform lock property. This will apply to all transformations for the block.
```kotlin
engine.block.setTransformLocked(imageBlock, true)
```
### Rotating As a Group
To rotate multiple elements together, first add them to a `group` and then rotate the group.
```kotlin
import kotlin.math.PI
val groupId = engine.block.group(listOf(imageBlock, textBlock))
engine.block.setRotation(groupId, (PI / 2).toFloat())
```
### Troubleshooting
Troubleshooting
| Issue | Solution |
| ----------------------------------- | ------------------------------------------------------------------------------- |
| Image appears offset after rotation | Make sure the pivot point is centered (default is center). |
| Rotation not applying | Confirm that the image block is inserted and rendered before applying rotation. |
| Rotation handle not visible | Check that interactive UI controls are enabled in the settings. |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Scale in Android (Kotlin)"
description: "Resize images uniformly in your Android app using Kotlin."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-image/transform/scale-ebe367/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/android/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) > [Scale](https://img.ly/docs/cesdk/android/edit-image/transform/scale-ebe367/)
---
Scaling lets users enlarge or shrink a block directly on the canvas. In CE.SDK, scaling is a transform property that applies uniformly to most block types. This guide shows how to scale images using CE.SDK in your Android app using Kotlin. You'll learn how to scale image blocks proportionally, scale groups, and apply scaling constraints to protect template structure.
The standard UI already supports pinch-to-zoom and on-screen scale handles. Scaling programmatically gives you finer control. This is ideal for automation, custom UI, or template-driven apps.
When you want to scale the image **inside** the block and leave the block dimensions unchanged, you'll use [crop scale](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) instead.
## What You'll Learn
- Scale images programmatically using Kotlin.
- Scale images proportionally or non-uniformly.
- Scale grouped elements.
- Enable or disable scaling via pinch gestures or gizmo handles.
## When to Use
Use image scaling when your UI needs to:
- Let users zoom artwork smoothly without cropping
- Enforce a canonical image size in templates
- Support controls like sliders instead of gestures
- Scale multiple elements together (logos, product bundles, captions)
## Scaling Basics
On Android, you scale blocks using the **block API**. The main pieces you'll use are:
- `engine.block.scale(block: Int, scaleX: Float, scaleY: Float, anchorX: Float = 0f, anchorY: Float = 0f)`
- `width` / `height` and their modes (`setWidthMode`, `setHeightMode`)
- crop-related functions like `setCropScaleX`, `setCropScaleY`, and `setCropTranslationX` / `Y`
Control the size with the following scale values:
- `1.0f`: represents the **original** size.
- Larger than `1.0f`: **increases** the size.
- Smaller than `1.0f`: **shrinks** the size.
> **Note:** The examples below use image blocks, but this same approach works for shapes, text, stickers, and groups as long as you have their block ID.
## Scale an Image Uniformly
Uniform scaling uses the `scale()` function. A scale value of `1.0f` is the original scale. Values larger than `1.0f` increase the scale of the block and values lower than `1.0f` scale the block smaller. A value of `2.0f`, for example makes the block twice as large.
This scales the image to 150% of its original size. Because the default anchor point is the top-left corner, the block grows outward from that corner.
```kotlin
import ly.img.engine.Engine
engine.block.scale(imageBlock, scaleX = 1.5f, scaleY = 1.5f)
```

By default, the anchor point for the image when scaling is the origin point on the top left. The scale function has two optional parameters to move the anchor point in the x and y direction. They can have values between `0.0f` and `1.0f`
This scales the image to 150% of its original size. The origin anchor point is 0.5, 0.5 so the image expands from the center.
```kotlin
engine.block.scale(imageBlock, scaleX = 1.5f, scaleY = 1.5f, anchorX = 0.5f, anchorY = 0.5f)
```

## Scale Non-Uniformly
To stretch or compress only one axis, thus distorting an image, use this combination:
- The crop scale function
- The width or height function
How you decide to make the adjustment will have different results. Below are three examples of scaling the original image in the x direction only.

```kotlin
import ly.img.engine.Engine
import ly.img.engine.SizeMode
engine.block.setWidthMode(imageBlock, mode = SizeMode.AUTO)
val newWidth = engine.block.getWidth(imageBlock) * 1.5f
engine.block.setWidth(imageBlock, value = newWidth)
```
The image continues respecting its fill mode (usually `.COVER`), so the content scales automatically as the frame widens.

```kotlin
engine.block.setCropScaleX(imageBlock, scaleX = 1.5f)
engine.block.setWidthMode(imageBlock, mode = SizeMode.AUTO)
val newWidth = engine.block.getWidth(imageBlock) * 1.5f
engine.block.setWidth(imageBlock, value = newWidth)
```
This uses crop scale to scale the image in a single direction and then adjusts the block's width to match the change. The change in width does not take the crop into account and so distorts the image as it's scaling the scaled image.

```kotlin
engine.block.setCropScaleX(imageBlock, scaleX = 1.5f)
engine.block.setWidthMode(imageBlock, mode = SizeMode.AUTO)
val newWidth = engine.block.getWidth(imageBlock) * 1.5f
engine.block.setWidth(imageBlock, value = newWidth, maintainCrop = true)
```
By setting the `maintainCrop` parameter to true, expanding the width of the image by the scale factor respects the crop scale and the image is less distorted.
## Scale Images with Built-In Gestures or Gizmos
The CE.SDK UI supports these interactions automatically:
### Pinch to Zoom
Enabled by default:
```kotlin
import ly.img.engine.Engine
engine.editor.setSettingBoolean("touch/pinchAction", value = true)
```
Setting this to false disables pinch scaling entirely. For environments with keyboard and mouse a similar property exists:
```kotlin
engine.editor.setSettingBoolean("mouse/enableZoom", value = true)
```
### Gizmo Scale Handles
The UI can show corner handles for drag-scaling:
```kotlin
engine.editor.setSettingBoolean("controlGizmo/showScaleHandles", value = true)
```
This mirrors the behavior of native editors.
> **Note:** Changing these settings affects how the CE.SDK interprets user input. It doesn't prevent you from scaling blocks programmatically with `scale()`.
## Scale Multiple Elements Together
If you combine multiple blocks into a group, scaling the group scales every member:
```kotlin
import ly.img.engine.Engine
val groupId = engine.block.group(listOf(imageBlock, textBlock))
engine.block.scale(groupId, scaleX = 0.75f, scaleY = 0.75f)
```
This scales the entire group to 75%.
## Lock Scaling
When working with templates, you can lock a block from scaling by setting its scope. The [guide on locking](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) provides more information.
```kotlin
import ly.img.engine.Engine
engine.block.setScopeEnabled(imageBlock, key = "layer/resize", enabled = false)
```
To prevent users from applying **any** transform to a block:
```kotlin
engine.block.setTransformLocked(imageBlock, locked = true)
```
## Complete Scaling Example
Here's a complete example showing different scaling operations in a single function:
```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
fun scaleImageExample(
license: String,
userId: String
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.scale")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
// Create scene and page
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
// Create an image block
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setString(
block = imageFill,
property = "fill/image/imageFileURI",
value = "https://img.ly/static/ubq_samples/sample_1.jpg"
)
engine.block.setFill(imageBlock, fill = imageFill)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 300F)
engine.block.appendChild(parent = page, child = imageBlock)
// Example 1: Scale uniformly from top-left
engine.block.scale(imageBlock, scaleX = 1.5f, scaleY = 1.5f)
// Example 2: Scale uniformly from center
engine.block.scale(
imageBlock,
scaleX = 0.75f,
scaleY = 0.75f,
anchorX = 0.5f,
anchorY = 0.5f
)
// Example 3: Non-uniform scaling with crop
engine.block.setCropScaleX(imageBlock, scaleX = 1.5f)
engine.block.setWidthMode(imageBlock, mode = SizeMode.AUTO)
val newWidth = engine.block.getWidth(imageBlock) * 1.5f
engine.block.setWidth(imageBlock, value = newWidth, maintainCrop = true)
// Example 4: Lock scaling
engine.block.setScopeEnabled(imageBlock, key = "layer/resize", enabled = false)
engine.stop()
}
```
## Troubleshooting
|Symptom|Likely Cause|Fix|
|---|---|---|
|"Property not found: transform/scale/x"|Using old spec property names that no longer exist.|Replace with `engine.block.scale()` for uniform scale. See [Crop](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) for more on how crop scale affects scaling results.|
|Image changes size but looks oddly distorted|Combining crop and width changes in a surprising way.|Use a simpler pattern: either change width alone, or use a controlled crop/scaleX + width approach and test with sample images.|
|Pinch does nothing on canvas|Pinch scaling disabled|Ensure "touch/pinchAction" is true (or not overridden in settings).|
|Scale handles don't appear|Gizmo handles disabled in editor settings|Set `controlGizmo/showScaleHandles` to true.|
|Image won't scale at all|Block is transform-locked or scope-locked|Check `transformLocked` and any related scopes like "layer/resize". Unlock or re-enable scope if needed.|
## Next Steps
Once you're comfortable scaling images, explore the other transform tools:
- [Resize](https://img.ly/docs/cesdk/android/edit-image/transform/resize-407242/) for changing the size of a block's frame.
- [Crop](https://img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47/) for changing what part of the image is visible.
- [Rotate](https://img.ly/docs/cesdk/android/edit-image/transform/rotate-5f39c9/) for rotating images around an anchor.
- [Flip](https://img.ly/docs/cesdk/android/edit-image/transform/flip-035e9f/) to mirror images horizontally or vertically.
- [Move](https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/) to reposition blocks on the canvas.
Together, these guides give you a complete picture of how to position and transform images in CE.SDK on Android.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add Captions"
description: "Add synchronized captions to Android video scenes with CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/)
---
```kotlin file=@cesdk_android_examples/engine-guides-captions/Captions.kt reference-only
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.AnimationType
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.PositionMode
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.nio.ByteBuffer
private const val TAG = "CaptionsGuide"
suspend fun editVideoCaptions(engine: Engine): ByteBuffer = withContext(Dispatchers.Main) {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.editor.setSettingBoolean(keypath = "features/videoCaptionsEnabled", value = true)
engine.block.setDuration(page, duration = 20.0)
val video = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
engine.block.setFill(video, fill = videoFill)
engine.block.setDuration(video, duration = 20.0)
val videoTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = videoTrack)
engine.block.appendChild(parent = videoTrack, child = video)
engine.block.fillParent(videoTrack)
val captionTrack = engine.block.create(DesignBlockType.CaptionTrack)
engine.block.appendChild(parent = page, child = captionTrack)
val manageOffsetsAutomatically = false
engine.block.setBoolean(
block = captionTrack,
property = "track/automaticallyManageBlockOffsets",
value = manageOffsetsAutomatically,
)
val caption1 = engine.block.create(DesignBlockType.Caption)
engine.block.setString(caption1, property = "caption/text", value = "Caption text 1")
val caption2 = engine.block.create(DesignBlockType.Caption)
engine.block.setString(caption2, property = "caption/text", value = "Caption text 2")
engine.block.appendChild(parent = captionTrack, child = caption1)
engine.block.appendChild(parent = captionTrack, child = caption2)
engine.block.setDuration(caption1, duration = 3.0)
engine.block.setDuration(caption2, duration = 5.0)
engine.block.setTimeOffset(caption1, offset = 0.0)
engine.block.setTimeOffset(caption2, offset = 3.0)
// Captions can also be loaded from a caption file, i.e., from SRT and VTT files.
// The text and timing of the captions are read from the file.
val captions = engine.block.createCaptionsFromURI("https://img.ly/static/examples/captions.srt")
for (caption in captions) {
engine.block.appendChild(parent = captionTrack, child = caption)
}
// Position and size sync only with caption blocks under the same caption track.
engine.block.setPositionX(caption1, 0.05F)
engine.block.setPositionXMode(caption1, PositionMode.PERCENT)
engine.block.setPositionY(caption1, 0.8F)
engine.block.setPositionYMode(caption1, PositionMode.PERCENT)
engine.block.setHeight(caption1, 0.15F)
engine.block.setHeightMode(caption1, SizeMode.PERCENT)
engine.block.setWidth(caption1, 0.9F)
engine.block.setWidthMode(caption1, SizeMode.PERCENT)
// Style properties sync only with caption blocks under the same caption track.
engine.block.setTextColor(caption1, color = Color.fromRGBA(0.9F, 0.9F, 0F, 1F))
engine.block.setDropShadowEnabled(caption1, enabled = true)
engine.block.setDropShadowColor(caption1, color = Color.fromRGBA(0F, 0F, 0F, 0.8F))
engine.block.setBackgroundColorEnabled(caption1, enabled = true)
engine.block.setBackgroundColor(caption1, color = Color.fromRGBA(0F, 0F, 0F, 0.7F))
// Use property-keyed setters for caption automatic font sizing properties.
engine.block.setBoolean(caption1, property = "caption/automaticFontSizeEnabled", value = true)
engine.block.setFloat(caption1, property = "caption/minAutomaticFontSize", value = 24F)
engine.block.setFloat(caption1, property = "caption/maxAutomaticFontSize", value = 72F)
val fadeInAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setDuration(fadeInAnimation, duration = 0.3)
engine.block.setInAnimation(caption1, animation = fadeInAnimation)
// Export page as mp4 video.
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = {
Log.i(
TAG,
"Rendered ${it.renderedFrames} frames and encoded ${it.encodedFrames} frames out of ${it.totalFrames} frames",
)
},
)
check(videoBytes.remaining() > 0)
videoBytes
}
```
Add synchronized captions to video scenes with CE.SDK's caption tracks, caption blocks, subtitle import, styling properties, and video export.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-captions)
Captions in CE.SDK use the same block hierarchy as other video content: a page contains a video track and a caption track, and the caption track contains caption blocks. Each caption block stores text, a time offset, a duration, and styling properties.
This guide focuses on the Android Engine APIs. The sample adds a video clip, overlays captions, and exports the page. If your Android app needs caption-specific controls, wire your own UI to these APIs and keep the scene hierarchy shown below.
## Creating a Video Scene
Create a video scene, add a page, set the page dimensions, and enable video captions before creating caption blocks.
```kotlin highlight-android-setup-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.editor.setSettingBoolean(keypath = "features/videoCaptionsEnabled", value = true)
```
## Setting Page Duration
The page duration defines the time range where captions can appear. This sample uses a 20 second page.
```kotlin highlight-android-set-page-duration
engine.block.setDuration(page, duration = 20.0)
```
## Adding a Video Clip
Create a graphic block with a video fill, give it the page duration, place it on a normal video track, and fill the page. This gives the captions actual video content to overlay during preview and export.
```kotlin highlight-android-add-video
val video = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
engine.block.setFill(video, fill = videoFill)
engine.block.setDuration(video, duration = 20.0)
val videoTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = videoTrack)
engine.block.appendChild(parent = videoTrack, child = video)
engine.block.fillParent(videoTrack)
```
## Creating a Caption Track
A caption track groups caption blocks under the page. Create it before adding manual or imported captions so every caption has the correct parent.
```kotlin highlight-android-create-caption-track
val captionTrack = engine.block.create(DesignBlockType.CaptionTrack)
engine.block.appendChild(parent = page, child = captionTrack)
```
Caption tracks can either keep each caption's explicit time offset or manage offsets automatically from child durations. Keep `track/automaticallyManageBlockOffsets` set to `false` when you import SRT/VTT captions or set custom offsets yourself; set it to `true` when captions should play back sequentially without gaps.
```kotlin highlight-android-manage-caption-offsets
val manageOffsetsAutomatically = false
engine.block.setBoolean(
block = captionTrack,
property = "track/automaticallyManageBlockOffsets",
value = manageOffsetsAutomatically,
)
```
## Adding Captions
### Creating Caption Blocks
Create caption blocks with `DesignBlockType.Caption`, write their text with the `caption/text` property, and append them to the caption track.
```kotlin highlight-android-create-captions
val caption1 = engine.block.create(DesignBlockType.Caption)
engine.block.setString(caption1, property = "caption/text", value = "Caption text 1")
val caption2 = engine.block.create(DesignBlockType.Caption)
engine.block.setString(caption2, property = "caption/text", value = "Caption text 2")
engine.block.appendChild(parent = captionTrack, child = caption1)
engine.block.appendChild(parent = captionTrack, child = caption2)
```
### Importing Captions from Subtitle Files
Use `createCaptionsFromURI` to parse SRT or VTT files. CE.SDK creates caption blocks with the parsed text and timing values.
```kotlin highlight-android-import-captions
// Captions can also be loaded from a caption file, i.e., from SRT and VTT files.
// The text and timing of the captions are read from the file.
val captions = engine.block.createCaptionsFromURI("https://img.ly/static/examples/captions.srt")
for (caption in captions) {
engine.block.appendChild(parent = captionTrack, child = caption)
}
```
Imported captions are still normal caption blocks, so you can append them to a caption track and style them with the same APIs as manually created captions.
## Modifying Captions
### Timing
With manual offsets, set the duration and time offset on each caption block. Time values are in seconds.
```kotlin highlight-android-set-timing
engine.block.setDuration(caption1, duration = 3.0)
engine.block.setDuration(caption2, duration = 5.0)
engine.block.setTimeOffset(caption1, offset = 0.0)
engine.block.setTimeOffset(caption2, offset = 3.0)
```
### Position and Size
Caption position and size are synchronized between caption blocks that share the same caption track, so set the shared layout on one caption per track.
```kotlin highlight-android-position-size
// Position and size sync only with caption blocks under the same caption track.
engine.block.setPositionX(caption1, 0.05F)
engine.block.setPositionXMode(caption1, PositionMode.PERCENT)
engine.block.setPositionY(caption1, 0.8F)
engine.block.setPositionYMode(caption1, PositionMode.PERCENT)
engine.block.setHeight(caption1, 0.15F)
engine.block.setHeightMode(caption1, SizeMode.PERCENT)
engine.block.setWidth(caption1, 0.9F)
engine.block.setWidthMode(caption1, SizeMode.PERCENT)
```
Percentage modes keep the caption box proportional when the output resolution changes.
### Styling
Caption styling properties are also synchronized between caption blocks that share the same caption track. The sample changes text color, background, and drop shadow with dedicated styling setters, then uses property-keyed setters for caption automatic font sizing properties.
```kotlin highlight-android-style-captions
// Style properties sync only with caption blocks under the same caption track.
engine.block.setTextColor(caption1, color = Color.fromRGBA(0.9F, 0.9F, 0F, 1F))
engine.block.setDropShadowEnabled(caption1, enabled = true)
engine.block.setDropShadowColor(caption1, color = Color.fromRGBA(0F, 0F, 0F, 0.8F))
engine.block.setBackgroundColorEnabled(caption1, enabled = true)
engine.block.setBackgroundColor(caption1, color = Color.fromRGBA(0F, 0F, 0F, 0.7F))
// Use property-keyed setters for caption automatic font sizing properties.
engine.block.setBoolean(caption1, property = "caption/automaticFontSizeEnabled", value = true)
engine.block.setFloat(caption1, property = "caption/minAutomaticFontSize", value = 24F)
engine.block.setFloat(caption1, property = "caption/maxAutomaticFontSize", value = 72F)
```
If your app registers a caption-preset asset source, you can query it with `engine.asset.findAssets(...)` and apply a returned asset with `engine.asset.applyAssetSourceAsset(...)`. The compiled sample uses direct block properties so it does not depend on a particular preset source being registered.
## Caption Animations
Caption blocks support the same animation APIs as other animatable blocks. Create an animation and assign it as an in, loop, or out animation.
```kotlin highlight-android-add-animation
val fadeInAnimation = engine.block.createAnimation(AnimationType.Fade)
engine.block.setDuration(fadeInAnimation, duration = 0.3)
engine.block.setInAnimation(caption1, animation = fadeInAnimation)
```
Use entry animations sparingly for captions; timing and readability usually matter more than motion.
## Exporting Videos with Captions
Exporting the page as MP4 burns captions into the rendered video frames. The returned `ByteBuffer` contains the encoded MP4 data with caption pixels at the time offsets defined on each caption block.
```kotlin highlight-android-export-video
// Export page as mp4 video.
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = {
Log.i(
TAG,
"Rendered ${it.renderedFrames} frames and encoded ${it.encodedFrames} frames out of ${it.totalFrames} frames",
)
},
)
```
The export callback reports render and encode progress. Changes made after export starts are not reflected in the exported file because CE.SDK freezes the scene state for that export.
## Troubleshooting
| Issue | Cause | Solution |
| --- | --- | --- |
| Captions are not visible | The caption is not under a caption track on the page | Check the hierarchy: page, caption track, caption block |
| Caption appears at the wrong time | The time offset or duration is wrong, or automatic offset management is enabled when custom offsets are expected | Read back `getTimeOffset(...)` and `getDuration(...)`, then check `track/automaticallyManageBlockOffsets` on the caption track |
| Subtitle import fails | The URI does not resolve to a valid SRT or VTT file | Verify the URI is reachable from the Android app and that the file format is valid |
| Styling is not applied | The property key is not a caption or text property | Use caption properties such as `caption/text`, `caption/automaticFontSizeEnabled`, and text styling properties |
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a video scene |
| `engine.editor.setSettingBoolean(keypath="features/videoCaptionsEnabled", value=_)` | Enable caption editing features |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create the page that contains the video and caption tracks |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a block for the video clip |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular shape for the video block |
| `engine.block.setShape(block=_, shape=_)` | Assign the shape to the video block |
| `engine.block.createFill(fillType=FillType.Video)` | Create a video fill |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Set the video file URI |
| `engine.block.setFill(block=_, fill=_)` | Assign the video fill to the video block |
| `engine.block.create(blockType=DesignBlockType.Track)` | Create a video track |
| `engine.block.create(blockType=DesignBlockType.CaptionTrack)` | Create a caption track |
| `engine.block.setBoolean(block=_, property="track/automaticallyManageBlockOffsets", value=_)` | Enable or disable automatic caption offset management |
| `engine.block.create(blockType=DesignBlockType.Caption)` | Create a caption block |
| `engine.block.createCaptionsFromURI(uri=_)` | Import SRT or VTT captions |
| `engine.block.appendChild(parent=_, child=_)` | Add tracks and blocks to the hierarchy |
| `engine.block.fillParent(block=_)` | Size a track to the page |
| `engine.block.setString(block=_, property="caption/text", value=_)` | Set caption text |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when a caption appears |
| `engine.block.setDuration(block=_, duration=_)` | Set page, video, caption, or animation duration |
| `engine.block.getTimeOffset(block=_)` | Read when a caption appears |
| `engine.block.getDuration(block=_)` | Read a page, video, caption, or animation duration |
| `engine.block.setPositionX(block=_, value=_)` | Set the caption box x position |
| `engine.block.setPositionXMode(block=_, mode=_)` | Set the caption box x position mode |
| `engine.block.setPositionY(block=_, value=_)` | Set the caption box y position |
| `engine.block.setPositionYMode(block=_, mode=_)` | Set the caption box y position mode |
| `engine.block.setWidth(block=_, value=_)` | Set the page width or caption box width |
| `engine.block.setWidthMode(block=_, mode=_)` | Set the caption box width mode |
| `engine.block.setHeight(block=_, value=_)` | Set the page height or caption box height |
| `engine.block.setHeightMode(block=_, mode=_)` | Set the caption box height mode |
| `engine.block.setTextColor(block=_, color=_)` | Set caption text color |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable the caption drop shadow |
| `engine.block.setDropShadowColor(block=_, color=_)` | Set caption drop shadow color |
| `engine.block.setBackgroundColorEnabled(block=_, enabled=_)` | Enable the caption background |
| `engine.block.setBackgroundColor(block=_, color=_)` | Set caption background color |
| `engine.block.setBoolean(block=_, property="caption/automaticFontSizeEnabled", value=_)` | Enable automatic caption font sizing |
| `engine.block.setFloat(block=_, property="caption/minAutomaticFontSize", value=_)` | Set the minimum automatic font size |
| `engine.block.setFloat(block=_, property="caption/maxAutomaticFontSize", value=_)` | Set the maximum automatic font size |
| `engine.asset.findAssets(sourceId=_, query=_)` | Query registered caption preset assets |
| `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_, block=_)` | Apply a preset asset to an existing caption block |
| `engine.block.createAnimation(type=_)` | Create an animation block |
| `engine.block.setInAnimation(block=_, animation=_)` | Assign an entry animation |
| `engine.block.setLoopAnimation(block=_, animation=_)` | Assign a looping animation |
| `engine.block.setOutAnimation(block=_, animation=_)` | Assign an exit animation |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_)` | Export the page with burned-in captions |
## Next Steps
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK.
- [Video Timeline Overview](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Add Watermark"
description: "Add text and image watermarks to videos with timeline duration, positioning, opacity, and visibility controls in Android."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/add-watermark-762ce6/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Add Watermark](https://img.ly/docs/cesdk/android/edit-video/add-watermark-762ce6/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-video-add-watermark/AddWatermark.kt reference-only
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.BlendMode
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.HorizontalAlignment
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
fun addWatermark(
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.add.watermark.example")
try {
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1280, height = 720)
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
engine.scene.createFromVideo(videoUri)
val page = requireNotNull(engine.scene.getCurrentPage()) {
"Expected createFromVideo() to create a page."
}
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val videoDuration = engine.block.getDuration(page)
val textWatermark = engine.block.create(DesignBlockType.Text)
engine.block.setWidthMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.replaceText(block = textWatermark, text = "All rights reserved 2025")
val textPadding = 20F
engine.block.setPositionX(block = textWatermark, value = textPadding)
engine.block.setPositionY(block = textWatermark, value = pageHeight - textPadding - 28F)
engine.block.setTextFontSize(block = textWatermark, fontSize = 20F)
engine.block.setTextColor(block = textWatermark, color = Color.fromRGBA(1F, 1F, 1F, 1F))
engine.block.setTextHorizontalAlignment(block = textWatermark, alignment = HorizontalAlignment.Left)
engine.block.setOpacity(block = textWatermark, value = 0.7F)
engine.block.setDropShadowEnabled(block = textWatermark, enabled = true)
engine.block.setDropShadowColor(block = textWatermark, color = Color.fromRGBA(0F, 0F, 0F, 0.8F))
engine.block.setDropShadowOffsetX(block = textWatermark, offsetX = 2F)
engine.block.setDropShadowOffsetY(block = textWatermark, offsetY = 2F)
engine.block.setDropShadowBlurRadiusX(block = textWatermark, blurRadiusX = 4F)
engine.block.setDropShadowBlurRadiusY(block = textWatermark, blurRadiusY = 4F)
engine.block.setDuration(block = textWatermark, duration = videoDuration)
engine.block.setTimeOffset(block = textWatermark, offset = 0.0)
engine.block.appendChild(parent = page, child = textWatermark)
val logoWatermark = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = logoWatermark, shape = rectShape)
val imageFill = engine.block.createFill(FillType.Image)
val logoUri = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg")
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = logoUri,
)
engine.block.setFill(block = logoWatermark, fill = imageFill)
engine.block.setContentFillMode(block = logoWatermark, mode = ContentFillMode.CONTAIN)
val logoSize = 80F
val logoPadding = 20F
engine.block.setWidth(block = logoWatermark, value = logoSize)
engine.block.setHeight(block = logoWatermark, value = logoSize)
engine.block.setPositionX(block = logoWatermark, value = pageWidth - logoSize - logoPadding)
engine.block.setPositionY(block = logoWatermark, value = logoPadding)
engine.block.setOpacity(block = logoWatermark, value = 0.6F)
engine.block.setBlendMode(block = logoWatermark, blendMode = BlendMode.NORMAL)
engine.block.setDuration(block = logoWatermark, duration = videoDuration)
engine.block.setTimeOffset(block = logoWatermark, offset = 0.0)
engine.block.appendChild(parent = page, child = logoWatermark)
check(videoDuration > 0.0) { "Expected the page duration to match the source video." }
check(engine.block.getDuration(textWatermark) == videoDuration)
check(engine.block.getDuration(logoWatermark) == videoDuration)
} finally {
engine.stop()
}
}
```
Add text and image watermarks to video content for copyright protection, branding, and content attribution using CE.SDK's time-aware block system.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-video-add-watermark)
Video watermarks in CE.SDK are design blocks positioned over video content. Text watermarks display copyright notices, URLs, or branding text, while image watermarks show logos or graphics. Both watermark types need their duration set so they remain visible throughout video playback.
This guide shows how to create text and image watermarks with the Android Engine API, position them on the page, style them for visibility, and configure their timing to span the full video.
## Creating the Scene
Start from a video scene and read the page dimensions and duration. The dimensions drive placement calculations, while the duration is reused for each watermark block.
```kotlin highlight-android-create-video-scene
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
engine.scene.createFromVideo(videoUri)
val page = requireNotNull(engine.scene.getCurrentPage()) {
"Expected createFromVideo() to create a page."
}
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val videoDuration = engine.block.getDuration(page)
```
`createFromVideo()` creates the scene and current page from the source video. `getDuration()` returns the page duration in seconds, which the sample applies to both watermark blocks.
## Creating a Text Watermark
Text watermarks are regular text blocks. We create one, let it size itself to its content, then place it near the bottom-left corner with padding from the page edges.
```kotlin highlight-android-create-text-watermark
val textWatermark = engine.block.create(DesignBlockType.Text)
engine.block.setWidthMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = textWatermark, mode = SizeMode.AUTO)
engine.block.replaceText(block = textWatermark, text = "All rights reserved 2025")
val textPadding = 20F
engine.block.setPositionX(block = textWatermark, value = textPadding)
engine.block.setPositionY(block = textWatermark, value = pageHeight - textPadding - 28F)
```
Auto width and height keep the watermark frame tied to its text, so the sample only needs position values.
## Styling Text Watermarks
Style the text for readability across changing video backgrounds.
```kotlin highlight-android-style-text-watermark
engine.block.setTextFontSize(block = textWatermark, fontSize = 20F)
engine.block.setTextColor(block = textWatermark, color = Color.fromRGBA(1F, 1F, 1F, 1F))
engine.block.setTextHorizontalAlignment(block = textWatermark, alignment = HorizontalAlignment.Left)
engine.block.setOpacity(block = textWatermark, value = 0.7F)
```
The sample uses white text, left alignment, and 70% opacity. `setTextFontSize()` applies the font size through the typed Android text API.
## Adding Drop Shadow for Visibility
Drop shadows help the text stay legible over both light and dark frames.
```kotlin highlight-android-text-drop-shadow
engine.block.setDropShadowEnabled(block = textWatermark, enabled = true)
engine.block.setDropShadowColor(block = textWatermark, color = Color.fromRGBA(0F, 0F, 0F, 0.8F))
engine.block.setDropShadowOffsetX(block = textWatermark, offsetX = 2F)
engine.block.setDropShadowOffsetY(block = textWatermark, offsetY = 2F)
engine.block.setDropShadowBlurRadiusX(block = textWatermark, blurRadiusX = 4F)
engine.block.setDropShadowBlurRadiusY(block = textWatermark, blurRadiusY = 4F)
```
The black shadow uses 80% alpha, 2 px offsets, and 4 px blur radii to add contrast without making the watermark dominate the video.
## Setting Text Watermark Duration
Set the text block duration to match the page duration and start it at the beginning of the timeline.
```kotlin highlight-android-text-timeline
engine.block.setDuration(block = textWatermark, duration = videoDuration)
engine.block.setTimeOffset(block = textWatermark, offset = 0.0)
engine.block.appendChild(parent = page, child = textWatermark)
```
`setDuration()` controls how long the block is active during playback. `setTimeOffset()` starts the watermark at 0 seconds, and `appendChild()` places it above the video content on the page.
## Creating an Image Watermark
Image watermarks use a graphic block with a shape and image fill.
```kotlin highlight-android-create-image-watermark
val logoWatermark = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(block = logoWatermark, shape = rectShape)
val imageFill = engine.block.createFill(FillType.Image)
val logoUri = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg")
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = logoUri,
)
engine.block.setFill(block = logoWatermark, fill = imageFill)
engine.block.setContentFillMode(block = logoWatermark, mode = ContentFillMode.CONTAIN)
```
The image URI is assigned to the fill through the `fill/image/imageFileURI` property. `ContentFillMode.CONTAIN` keeps the logo inside its frame without cropping.
## Positioning Image Watermarks
Position logos where they do not cover important content.
```kotlin highlight-android-position-image-watermark
val logoSize = 80F
val logoPadding = 20F
engine.block.setWidth(block = logoWatermark, value = logoSize)
engine.block.setHeight(block = logoWatermark, value = logoSize)
engine.block.setPositionX(block = logoWatermark, value = pageWidth - logoSize - logoPadding)
engine.block.setPositionY(block = logoWatermark, value = logoPadding)
```
The sample sizes the logo to 80 x 80 units and places it in the top-right corner with 20 units of padding.
## Configuring Opacity and Blend Mode
Control how the logo integrates with the video.
```kotlin highlight-android-image-opacity-blend
engine.block.setOpacity(block = logoWatermark, value = 0.6F)
engine.block.setBlendMode(block = logoWatermark, blendMode = BlendMode.NORMAL)
```
The logo uses 60% opacity for a visible but subtle overlay. `BlendMode.NORMAL` displays the logo without additional compositing effects.
## Setting Image Watermark Duration
Image watermarks need the same timeline configuration as text watermarks.
```kotlin highlight-android-image-timeline
engine.block.setDuration(block = logoWatermark, duration = videoDuration)
engine.block.setTimeOffset(block = logoWatermark, offset = 0.0)
engine.block.appendChild(parent = page, child = logoWatermark)
```
Matching the page duration keeps the logo visible for the full video. A time offset of 0 seconds starts it with the first frame.
## Watermark Positioning Strategies
Choose positions based on the watermark purpose:
- Bottom-right corner: common for copyright notices and less intrusive branding.
- Top-right corner: useful for logos that should stay visible but avoid typical lower-third content.
- Bottom-left corner: a good alternative for text when the opposite corner contains important content.
- Center: strongest protection for drafts or previews, but it obstructs the video.
Calculate positions from the current page dimensions so the same code works across video aspect ratios.
## Best Practices
### Visibility
- Use drop shadows on text watermarks for contrast against changing backgrounds.
- Keep opacity between 50-70% for visible but unobtrusive branding.
- Test watermark placement against representative video frames.
### Time Management
- Match watermark duration to the page duration for full-video coverage.
- Use a 0-second time offset for watermarks that should appear from the start.
- For time-based variations, create separate watermark blocks with different offsets and durations.
### Performance
- Use appropriately sized logo assets instead of scaling very large source images.
- Keep the number of watermark blocks low when rendering many videos.
- Reuse the same watermark creation logic for batch workflows.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.createFromVideo(videoUri=_)` | Create a video scene from a URI |
| `engine.scene.getCurrentPage()` | Get the page created for the video scene |
| `engine.block.getWidth(block=_)` | Read the page width for placement calculations |
| `engine.block.getHeight(block=_)` | Read the page height for placement calculations |
| `engine.block.getDuration(block=_)` | Read the page or watermark duration in seconds |
| `engine.block.create(blockType=DesignBlockType.Text)` | Create a text watermark block |
| `engine.block.setWidthMode(block=_, mode=SizeMode.AUTO)` | Let a text block size itself to its content |
| `engine.block.setHeightMode(block=_, mode=SizeMode.AUTO)` | Let a text block size itself to its content |
| `engine.block.replaceText(block=_, text=_)` | Set text watermark content |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text size |
| `engine.block.setTextColor(block=_, color=_)` | Set text color |
| `engine.block.setTextHorizontalAlignment(block=_, alignment=_)` | Set paragraph alignment |
| `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable drop shadow |
| `engine.block.setDropShadowColor(block=_, color=_)` | Set shadow color and alpha |
| `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set horizontal shadow offset |
| `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set vertical shadow offset |
| `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Set horizontal shadow blur |
| `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Set vertical shadow blur |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create an image watermark block |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular graphic shape |
| `engine.block.setShape(block=_, shape=_)` | Apply the rectangular shape to the graphic block |
| `engine.block.createFill(fillType=FillType.Image)` | Create an image fill for a logo |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the logo image URI |
| `engine.block.setFill(block=_, fill=_)` | Apply the image fill to the graphic block |
| `engine.block.setContentFillMode(block=_, mode=ContentFillMode.CONTAIN)` | Fit the logo inside its frame |
| `engine.block.setWidth(block=_, value=_)` | Set watermark width |
| `engine.block.setHeight(block=_, value=_)` | Set watermark height |
| `engine.block.setPositionX(block=_, value=_)` | Set horizontal position |
| `engine.block.setPositionY(block=_, value=_)` | Set vertical position |
| `engine.block.setOpacity(block=_, value=_)` | Set watermark transparency |
| `engine.block.setBlendMode(block=_, blendMode=_)` | Set image watermark blend mode |
| `engine.block.setDuration(block=_, duration=_)` | Set timeline duration |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set timeline start time |
| `engine.block.appendChild(parent=_, child=_)` | Add the watermark to the page |
## Next Steps
- [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Restrict editing access to watermark elements or properties in templates
- [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/) - Export video compositions as MP4 files with configurable encoding options, progress tracking, and resolution control.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Arrange and edit video clips, audio, and animations frame by frame
- [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Annotation"
description: "Add timed text, shapes, and highlights to video scenes on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/annotation-e9cbad/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Annotation](https://img.ly/docs/cesdk/android/edit-video/annotation-e9cbad/)
---
```kotlin file=@cesdk_android_examples/engine-guides-annotation/Annotation.kt reference-only
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
fun annotation(engine: Engine) {
val page = createAnnotationScene(engine)
val text = addTextAnnotation(engine = engine, page = page)
val highlight = addShapeAnnotation(engine = engine, page = page)
val annotations = listOf(text, highlight)
val sync = TimelineSync(engine = engine, page = page)
sync.refresh(annotations)
seekToAnnotation(engine = engine, page = page, annotation = highlight)
setAnnotationPlayback(engine = engine, page = page, playing = true, looping = true)
updateAnnotationText(engine = engine, annotation = text, text = "Replay this part")
moveAnnotation(engine = engine, annotation = highlight, x = 780F, y = 260F)
updateAnnotationTiming(engine = engine, annotation = highlight, start = 13.0, duration = 3.0)
removeAnnotation(engine = engine, annotation = text)
}
fun createAnnotationScene(engine: Engine): DesignBlock {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 20.0)
val video = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
val videoUri = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
)
// Video fills expose a URI-valued file property; set it with the typed URI API.
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(video, fill = videoFill)
val videoTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = videoTrack)
engine.block.appendChild(parent = videoTrack, child = video)
engine.block.fillParent(videoTrack)
return page
}
fun addTextAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val text = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(text, text = "Watch this part!")
engine.block.setTextFontSize(text, fontSize = 32F)
engine.block.setWidthMode(text, mode = SizeMode.AUTO)
engine.block.setHeightMode(text, mode = SizeMode.AUTO)
engine.block.setPositionX(text, value = 160F)
engine.block.setPositionY(text, value = 560F)
engine.block.setTimeOffset(text, offset = 5.0)
engine.block.setDuration(text, duration = 5.0)
engine.block.appendChild(parent = page, child = text)
return text
}
fun addShapeAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val highlight = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(highlight, shape = engine.block.createShape(ShapeType.Star))
engine.block.setWidth(highlight, value = 140F)
engine.block.setHeight(highlight, value = 140F)
engine.block.setPositionX(highlight, value = 700F)
engine.block.setPositionY(highlight, value = 240F)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(highlight, fill = fill)
engine.block.setFillSolidColor(
block = highlight,
color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setTimeOffset(highlight, offset = 12.0)
engine.block.setDuration(highlight, duration = 4.0)
engine.block.appendChild(parent = page, child = highlight)
return highlight
}
data class AnnotationTimelineState(
val currentTime: Double,
val activeAnnotation: DesignBlock?,
)
class TimelineSync(
private val engine: Engine,
private val page: DesignBlock,
) {
private val mutableState = MutableStateFlow(
AnnotationTimelineState(
currentTime = 0.0,
activeAnnotation = null,
),
)
val state: StateFlow = mutableState.asStateFlow()
private var pollingJob: Job? = null
// Call this from UI code that owns a lifecycle scope.
fun start(
annotations: List,
scope: CoroutineScope,
) {
pollingJob?.cancel()
pollingJob = scope.launch(Dispatchers.Main.immediate) {
while (isActive) {
refresh(annotations)
delay(200)
}
}
}
fun refresh(annotations: List) {
val currentTime = engine.block.getPlaybackTime(page)
val active = annotations.firstOrNull { annotation ->
engine.block.isValid(annotation) &&
engine.block.isVisibleAtCurrentPlaybackTime(annotation)
}
mutableState.value = AnnotationTimelineState(
currentTime = currentTime,
activeAnnotation = active,
)
}
fun stop() {
pollingJob?.cancel()
pollingJob = null
}
}
fun seekToAnnotation(
engine: Engine,
page: DesignBlock,
annotation: DesignBlock,
) {
if (!engine.block.supportsPlaybackTime(page)) return
val start = engine.block.getTimeOffset(annotation)
engine.block.setPlaybackTime(block = page, time = start)
}
fun updateAnnotationText(
engine: Engine,
annotation: DesignBlock,
text: String,
) {
engine.block.replaceText(annotation, text = text)
}
fun moveAnnotation(
engine: Engine,
annotation: DesignBlock,
x: Float,
y: Float,
) {
engine.block.setPositionX(annotation, value = x)
engine.block.setPositionY(annotation, value = y)
}
fun updateAnnotationTiming(
engine: Engine,
annotation: DesignBlock,
start: Double,
duration: Double,
) {
engine.block.setTimeOffset(annotation, offset = start)
engine.block.setDuration(annotation, duration = duration)
}
fun removeAnnotation(
engine: Engine,
annotation: DesignBlock,
) {
engine.block.destroy(annotation)
}
fun setAnnotationPlayback(
engine: Engine,
page: DesignBlock,
playing: Boolean,
looping: Boolean,
): Pair {
engine.block.setPlaying(block = page, enabled = playing)
val isPlaying = engine.block.isPlaying(page)
engine.block.setLooping(block = page, looping = looping)
val isLooping = engine.block.isLooping(page)
return isPlaying to isLooping
}
```
Annotations are timed visual overlays such as text labels, shapes, highlights, stickers, or images. On Android, they are ordinary blocks placed above video content and made visible for a specific timeline range.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-annotation)
## When to Use Annotations
Use annotations for tutorials, sports analysis, education, product demos, and other video workflows where viewers should notice a specific moment. Use captions instead when the content is synchronized spoken text.
Annotations use the same Block API as other visual content. Timing is controlled in seconds with `setTimeOffset` and `setDuration`, and playback sync reads the page's current playback time.
## Annotation Blocks and Timeline Placement
For a standalone video scene, create a video page with explicit dimensions and duration, add the media to a track, and append annotation blocks to the page after the media. Page children added later render above earlier content.
```kotlin highlight-android-timeline-placement
fun createAnnotationScene(engine: Engine): DesignBlock {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 20.0)
val video = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
val videoUri = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
)
// Video fills expose a URI-valued file property; set it with the typed URI API.
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(video, fill = videoFill)
val videoTrack = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = videoTrack)
engine.block.appendChild(parent = videoTrack, child = video)
engine.block.fillParent(videoTrack)
return page
}
```
## Add a Text Annotation
A text annotation is a `DesignBlockType.Text` block. Position it like any other text block, then set the timeline range before appending it to the page.
```kotlin highlight-android-text-annotation
fun addTextAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val text = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(text, text = "Watch this part!")
engine.block.setTextFontSize(text, fontSize = 32F)
engine.block.setWidthMode(text, mode = SizeMode.AUTO)
engine.block.setHeightMode(text, mode = SizeMode.AUTO)
engine.block.setPositionX(text, value = 160F)
engine.block.setPositionY(text, value = 560F)
engine.block.setTimeOffset(text, offset = 5.0)
engine.block.setDuration(text, duration = 5.0)
engine.block.appendChild(parent = page, child = text)
return text
}
```
The example starts at `5.0` seconds and lasts `5.0` seconds, so it is visible from 5s to 10s on the page timeline.
## Add a Shape Annotation
A shape annotation uses a graphic block with a vector shape and fill. This example creates a red star that appears after the text annotation.
```kotlin highlight-android-shape-annotation
fun addShapeAnnotation(
engine: Engine,
page: DesignBlock,
): DesignBlock {
val highlight = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(highlight, shape = engine.block.createShape(ShapeType.Star))
engine.block.setWidth(highlight, value = 140F)
engine.block.setHeight(highlight, value = 140F)
engine.block.setPositionX(highlight, value = 700F)
engine.block.setPositionY(highlight, value = 240F)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(highlight, fill = fill)
engine.block.setFillSolidColor(
block = highlight,
color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
engine.block.setTimeOffset(highlight, offset = 12.0)
engine.block.setDuration(highlight, duration = 4.0)
engine.block.appendChild(parent = page, child = highlight)
return highlight
}
```
Any visual block can serve as an annotation. Use text for labels, graphics for highlights, and image or sticker blocks for branded markers.
## Synchronize Annotation UI with Playback
Custom UI can poll the page playback time and mark whichever annotation is visible at that moment. Keep the polling interval modest, for example 100-200 ms, so the UI stays responsive.
```kotlin highlight-android-playback-sync
data class AnnotationTimelineState(
val currentTime: Double,
val activeAnnotation: DesignBlock?,
)
class TimelineSync(
private val engine: Engine,
private val page: DesignBlock,
) {
private val mutableState = MutableStateFlow(
AnnotationTimelineState(
currentTime = 0.0,
activeAnnotation = null,
),
)
val state: StateFlow = mutableState.asStateFlow()
private var pollingJob: Job? = null
// Call this from UI code that owns a lifecycle scope.
fun start(
annotations: List,
scope: CoroutineScope,
) {
pollingJob?.cancel()
pollingJob = scope.launch(Dispatchers.Main.immediate) {
while (isActive) {
refresh(annotations)
delay(200)
}
}
}
fun refresh(annotations: List) {
val currentTime = engine.block.getPlaybackTime(page)
val active = annotations.firstOrNull { annotation ->
engine.block.isValid(annotation) &&
engine.block.isVisibleAtCurrentPlaybackTime(annotation)
}
mutableState.value = AnnotationTimelineState(
currentTime = currentTime,
activeAnnotation = active,
)
}
fun stop() {
pollingJob?.cancel()
pollingJob = null
}
}
```
> **Note:** * Engine calls must stay on the main thread.
> * Store the active annotation ID in UI state and render your own list, marker, or toolbar state from that value.
## Seek to an Annotation
Seek on the page, not on the annotation block itself. Read the annotation start with `getTimeOffset`, then set the page playback time.
```kotlin highlight-android-seek-to-annotation
fun seekToAnnotation(
engine: Engine,
page: DesignBlock,
annotation: DesignBlock,
) {
if (!engine.block.supportsPlaybackTime(page)) return
val start = engine.block.getTimeOffset(annotation)
engine.block.setPlaybackTime(block = page, time = start)
}
```
## Controlling Playback (Play/Pause, Loop)
Use page playback controls as supporting APIs for preview UI. For broader audio and video playback controls, see [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/).
```kotlin highlight-android-playback-controls
fun setAnnotationPlayback(
engine: Engine,
page: DesignBlock,
playing: Boolean,
looping: Boolean,
): Pair {
engine.block.setPlaying(block = page, enabled = playing)
val isPlaying = engine.block.isPlaying(page)
engine.block.setLooping(block = page, looping = looping)
val isLooping = engine.block.isLooping(page)
return isPlaying to isLooping
}
```
## Edit & Remove Annotations
Text, position, timing, and deletion use the same block APIs after an annotation exists. Keep these operations focused so your UI can call them from list actions or inspector controls.
```kotlin highlight-android-edit-annotation
fun updateAnnotationText(
engine: Engine,
annotation: DesignBlock,
text: String,
) {
engine.block.replaceText(annotation, text = text)
}
```
```kotlin highlight-android-move-annotation
fun moveAnnotation(
engine: Engine,
annotation: DesignBlock,
x: Float,
y: Float,
) {
engine.block.setPositionX(annotation, value = x)
engine.block.setPositionY(annotation, value = y)
}
```
```kotlin highlight-android-retime-annotation
fun updateAnnotationTiming(
engine: Engine,
annotation: DesignBlock,
start: Double,
duration: Double,
) {
engine.block.setTimeOffset(annotation, offset = start)
engine.block.setDuration(annotation, duration = duration)
}
```
```kotlin highlight-android-remove-annotation
fun removeAnnotation(
engine: Engine,
annotation: DesignBlock,
) {
engine.block.destroy(annotation)
}
```
## API Reference
| API | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a video scene that supports timeline playback. |
| `engine.block.create(blockType=_)` | Create text, graphic, page, and track blocks. |
| `engine.block.setWidth(block=_, value=_)` / `engine.block.setHeight(block=_, value=_)` | Size pages and visual annotation blocks. |
| `engine.block.setWidthMode(block=_, mode=_)` / `engine.block.setHeightMode(block=_, mode=_)` | Auto-size text annotations to their content. |
| `engine.block.replaceText(block=_, text=_)` | Set or update text annotation content. |
| `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text annotation size. |
| `engine.block.createShape(type=_)` / `engine.block.setShape(block=_, shape=_)` | Create a shape annotation. |
| `engine.block.createFill(fillType=_)` / `engine.block.setFill(block=_, fill=_)` / `engine.block.setFillSolidColor(block=_, color=_)` | Style graphic annotations. |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Attach video media to a video fill. |
| `engine.block.setPositionX(block=_, value=_)` / `engine.block.setPositionY(block=_, value=_)` | Place annotations on the page. |
| `engine.block.setTimeOffset(block=_, offset=_)` / `engine.block.getTimeOffset(block=_)` | Set or read the annotation start time in seconds. |
| `engine.block.setDuration(block=_, duration=_)` / `engine.block.getDuration(block=_)` | Set or read the annotation duration in seconds. |
| `engine.block.appendChild(parent=_, child=_)` | Add annotations to the page hierarchy. |
| `engine.block.fillParent(block=_)` | Make a track fill its parent page. |
| `engine.block.isValid(block=_)` | Ignore annotations that were removed before a UI refresh. |
| `engine.block.supportsPlaybackTime(block=_)` | Check whether a page can be seeked. |
| `engine.block.setPlaybackTime(block=_, time=_)` / `engine.block.getPlaybackTime(block=_)` | Seek or read timeline playback time. |
| `engine.block.isVisibleAtCurrentPlaybackTime(block=_)` | Determine whether an annotation is active at the current page time. |
| `engine.block.setPlaying(block=_, enabled=_)` / `engine.block.isPlaying(block=_)` | Start or query playback. |
| `engine.block.setLooping(block=_, looping=_)` / `engine.block.isLooping(block=_)` | Control loop behavior. |
| `engine.block.destroy(block=_)` | Remove an annotation. |
## Troubleshooting
- **Annotation does not show up:** append it to the page after the media track, and keep its `timeOffset` plus `duration` inside the page duration.
- **Seek jumps do nothing:** call `setPlaybackTime` on the page block, not on the annotation block.
- **UI feels sluggish:** poll at 5-10 Hz and keep engine calls on the main thread.
- **Export differs from preview:** verify the video scene duration and test very small text or heavy effects in exported MP4 output.
## Next Steps
- [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Use caption blocks and tracks for synchronized spoken text.
- [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Populate labels from dynamic values such as usernames or scores.
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Control timeline playback, trim ranges, looping, and resources.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Build timeline interfaces for arranging clips and overlays.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Force Trim"
description: "Enforce minimum and maximum video durations in the editor UI."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/force-trim-3c1e8a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Force Trim](https://img.ly/docs/cesdk/android/edit-video/force-trim-3c1e8a/)
---
```kotlin file=@cesdk_android_examples/editor-guides-video-force-trim/ForceTrimVideoSolution.kt reference-only
import androidx.compose.runtime.Composable
import ly.img.editor.Editor
import ly.img.editor.configuration.video.VideoConfigurationBuilder
import ly.img.editor.configuration.video.callback.onLoaded
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.event.EditorEvent
import kotlin.time.Duration.Companion.seconds
// Add this composable to your NavHost
@Composable
fun ForceTrimVideoSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember(::VideoConfigurationBuilder) {
onLoaded = {
val event = EditorEvent.ApplyVideoDurationConstraints(
minDuration = 1.seconds,
maxDuration = 5.seconds,
)
editorContext.eventHandler.send(event)
onLoaded()
}
}
},
onClose = onClose,
)
}
```
Force trim lets you enforce minimum and maximum video durations in the timeline UI. The editor clamps export to the maximum duration and shows labels to communicate the limits.
> **Reading time:** 2 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-video-force-trim)
## Configure duration constraints
We apply constraints in `EngineConfiguration.onLoaded` after the scene has loaded. Keep `minimumVideoDuration` and `maximumVideoDuration` in seconds and ensure the max is not smaller than the min.
```kotlin highlight-android-constraints
val event = EditorEvent.ApplyVideoDurationConstraints(
minDuration = 1.seconds,
maxDuration = 5.seconds,
)
editorContext.eventHandler.send(event)
```
## Launch the video editor
Use the default video scene and the standard video UI. You can call the setter again later to switch presets at runtime.
```kotlin highlight-android-editor
Editor(
license = license, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember(::VideoConfigurationBuilder) {
onLoaded = {
val event = EditorEvent.ApplyVideoDurationConstraints(
minDuration = 1.seconds,
maxDuration = 5.seconds,
)
editorContext.eventHandler.send(event)
onLoaded()
}
}
},
onClose = onClose,
)
```

## Timeline and export behavior
When the scene duration is below the minimum, the min label stays visible and the editor blocks export with a dialog. When the duration exceeds the maximum, the playhead sticks to the max position and export is clamped to that duration.
## Full Code
This full sample uses `VideoConfigurationBuilder` from the [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/), applies duration constraints by dispatching `EditorEvent.ApplyVideoDurationConstraints` from `onLoaded`, and then calls `onLoaded()` from the video preset callback to keep the default video setup behavior.
```kotlin file=@cesdk_android_examples/editor-guides-video-force-trim/ForceTrimVideoSolution.kt
import androidx.compose.runtime.Composable
import ly.img.editor.Editor
import ly.img.editor.configuration.video.VideoConfigurationBuilder
import ly.img.editor.configuration.video.callback.onLoaded
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.event.EditorEvent
import kotlin.time.Duration.Companion.seconds
// Add this composable to your NavHost
@Composable
fun ForceTrimVideoSolution(
license: String,
onClose: (Throwable?) -> Unit,
) {
Editor(
license = license, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember(::VideoConfigurationBuilder) {
onLoaded = {
val event = EditorEvent.ApplyVideoDurationConstraints(
minDuration = 1.seconds,
maxDuration = 5.seconds,
)
editorContext.eventHandler.send(event)
onLoaded()
}
}
},
onClose = onClose,
)
}
```
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Join and Arrange Video Clips"
description: "Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Join and Arrange](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/)
---
```kotlin file=@cesdk_android_examples/engine-guides-join-and-arrange-video/JoinAndArrangeVideo.kt reference-only
import android.app.Application
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
data class TrackClipState(
val name: String,
val timeOffset: Double,
val duration: Double,
)
data class JoinAndArrangeVideoResult(
val initialTrackClips: List,
val reorderedTrackClips: List,
val pageDuration: Double,
val mainTrackDuration: Double,
val overlayTrackOffset: Double,
val overlayTrackDuration: Double,
val overlayClipCount: Int,
)
suspend fun joinAndArrangeVideoClips(
application: Application,
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
): JoinAndArrangeVideoResult = withContext(Dispatchers.Main) {
var engine: Engine? = null
var engineStarted = false
try {
Engine.init(application)
val currentEngine = Engine.getInstance(id = "ly.img.engine.join-and-arrange-video.example")
engine = currentEngine
engineStarted = currentEngine.start(license = license, userId = userId)
currentEngine.bindOffscreen(width = 1920, height = 1080)
val scene = currentEngine.scene.createForVideo()
val page = currentEngine.block.create(DesignBlockType.Page)
currentEngine.block.appendChild(parent = scene, child = page)
currentEngine.block.setWidth(page, value = 1920F)
currentEngine.block.setHeight(page, value = 1080F)
currentEngine.block.setDuration(page, duration = 15.0)
val videoUri = Uri.parse(
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/" +
"pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
)
val clipA = createVideoClip(
engine = currentEngine,
name = "Clip A",
videoUri = videoUri,
width = 1920F,
height = 1080F,
)
val clipB = createVideoClip(
engine = currentEngine,
name = "Clip B",
videoUri = videoUri,
width = 1920F,
height = 1080F,
)
val clipC = createVideoClip(
engine = currentEngine,
name = "Clip C",
videoUri = videoUri,
width = 1920F,
height = 1080F,
)
val track = currentEngine.block.create(DesignBlockType.Track)
currentEngine.block.appendChild(parent = page, child = track)
currentEngine.block.setBoolean(
block = track,
property = "track/automaticallyManageBlockOffsets",
value = false,
)
currentEngine.block.appendChild(parent = track, child = clipA)
currentEngine.block.appendChild(parent = track, child = clipB)
currentEngine.block.appendChild(parent = track, child = clipC)
currentEngine.block.fillParent(track)
val initialTrackChildren = currentEngine.block.getChildren(track)
check(initialTrackChildren == listOf(clipA, clipB, clipC))
currentEngine.block.setDuration(clipA, duration = 5.0)
currentEngine.block.setDuration(clipB, duration = 5.0)
currentEngine.block.setDuration(clipC, duration = 5.0)
currentEngine.block.setDuration(track, duration = 15.0)
currentEngine.block.setTimeOffset(clipA, offset = 0.0)
currentEngine.block.setTimeOffset(clipB, offset = 5.0)
currentEngine.block.setTimeOffset(clipC, offset = 10.0)
val initialTrackDuration = currentEngine.block.getDuration(track)
check(initialTrackDuration == 15.0)
val initialClipStates = currentEngine.block.getChildren(track).map { clip ->
TrackClipState(
name = currentEngine.block.getName(clip),
timeOffset = currentEngine.block.getTimeOffset(clip),
duration = currentEngine.block.getDuration(clip),
)
}
currentEngine.block.insertChild(parent = track, child = clipC, index = 0)
currentEngine.block.setTimeOffset(clipC, offset = 0.0)
currentEngine.block.setTimeOffset(clipA, offset = 5.0)
currentEngine.block.setTimeOffset(clipB, offset = 10.0)
val reorderedTrackDuration = currentEngine.block.getDuration(track)
check(reorderedTrackDuration == 15.0)
val reorderedClipStates = currentEngine.block.getChildren(track).map { clip ->
TrackClipState(
name = currentEngine.block.getName(clip),
timeOffset = currentEngine.block.getTimeOffset(clip),
duration = currentEngine.block.getDuration(clip),
)
}
val finalClipOrder = currentEngine.block.getChildren(track).map { clip ->
currentEngine.block.getName(clip)
}
val finalClipOffsets = currentEngine.block.getChildren(track).map { clip ->
currentEngine.block.getTimeOffset(clip)
}
check(finalClipOrder == listOf("Clip C", "Clip A", "Clip B"))
check(finalClipOffsets == listOf(0.0, 5.0, 10.0))
val overlayTrack = currentEngine.block.create(DesignBlockType.Track)
currentEngine.block.appendChild(parent = page, child = overlayTrack)
currentEngine.block.setTimeOffset(overlayTrack, offset = 2.0)
val overlayClip = createVideoClip(
engine = currentEngine,
name = "Overlay Clip",
videoUri = videoUri,
width = 1920F / 4F,
height = 1080F / 4F,
)
currentEngine.block.setDuration(overlayClip, duration = 5.0)
currentEngine.block.appendChild(parent = overlayTrack, child = overlayClip)
currentEngine.block.setPositionX(overlayClip, value = 1920F - 1920F / 4F - 40F)
currentEngine.block.setPositionY(overlayClip, value = 1080F - 1080F / 4F - 40F)
JoinAndArrangeVideoResult(
initialTrackClips = initialClipStates,
reorderedTrackClips = reorderedClipStates,
pageDuration = currentEngine.block.getDuration(page),
mainTrackDuration = reorderedTrackDuration,
overlayTrackOffset = currentEngine.block.getTimeOffset(overlayTrack),
overlayTrackDuration = currentEngine.block.getDuration(overlayTrack),
overlayClipCount = currentEngine.block.getChildren(overlayTrack).size,
)
} finally {
if (engineStarted) {
engine?.stop()
}
}
}
private suspend fun createVideoClip(
engine: Engine,
name: String,
videoUri: Uri,
width: Float,
height: Float,
): DesignBlock {
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(clip, name)
engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(clip, value = width)
engine.block.setHeight(clip, value = height)
val videoFill = engine.block.createFill(FillType.Video)
// The Android binding has no typed property helper for video fill URIs yet.
engine.block.setUri(block = videoFill, property = "fill/video/fileURI", value = videoUri)
engine.block.setFill(block = clip, fill = videoFill)
engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER)
engine.block.forceLoadAVResource(block = videoFill)
return clip
}
```
Combine multiple video clips into a sequence and organize them in the
composition using CE.SDK tracks, durations, and time offsets on Android.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-join-and-arrange-video)
Video compositions in CE.SDK use a **Scene > Page > Track > Clip** hierarchy.
Tracks group clips for timed playback. This guide sets clip durations and time
offsets explicitly so the sequence is deterministic and mirrors the Node.js
guide flow.
This guide covers the built-in timeline behavior at a high level, then uses the
CreativeEngine API to build a three-clip montage, reorder it, and add an overlay
track for a picture-in-picture composition.
## Joining Clips via UI
CE.SDK's Android video editor includes timeline controls for arranging clips.
Start from the [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) when you need
an interactive timeline UI. Use the Engine API sections below when you need to
prepare scenes programmatically.
### Adding Clips to Timeline
Users add clips from the asset library to the timeline. Adding a clip to an
existing track joins it to that sequence; adding it to an empty area creates a
separate track.
The timeline displays clip duration visually, so longer clips take more
horizontal space and the sequence is easy to scan.
### Reordering Clips
Users can drag clips within a track to reorder them. The timeline updates the
sequence and its time offsets so clips remain packed without gaps.
### Creating Additional Tracks
Additional tracks create layered compositions. Tracks later in the page's child
order render on top, which enables overlays, titles, and picture-in-picture
layouts.
## Programmatic Clip Joining
### Creating the Scene
Create a video scene, add a page, set a 16:9 frame, and make the page long
enough for three 5-second clips.
```kotlin highlight-android-create-scene
val scene = currentEngine.scene.createForVideo()
val page = currentEngine.block.create(DesignBlockType.Page)
currentEngine.block.appendChild(parent = scene, child = page)
currentEngine.block.setWidth(page, value = 1920F)
currentEngine.block.setHeight(page, value = 1080F)
currentEngine.block.setDuration(page, duration = 15.0)
```
### Creating Video Clips
Create each video clip by building the block structure directly: a graphic
block, a rectangle shape, and a video fill. The helper loads the video resource
before returning the clip.
```kotlin highlight-android-create-video-helper
private suspend fun createVideoClip(
engine: Engine,
name: String,
videoUri: Uri,
width: Float,
height: Float,
): DesignBlock {
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(clip, name)
engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(clip, value = width)
engine.block.setHeight(clip, value = height)
val videoFill = engine.block.createFill(FillType.Video)
// The Android binding has no typed property helper for video fill URIs yet.
engine.block.setUri(block = videoFill, property = "fill/video/fileURI", value = videoUri)
engine.block.setFill(block = clip, fill = videoFill)
engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER)
engine.block.forceLoadAVResource(block = videoFill)
return clip
}
```
Then create the three clips with the same 1920 x 1080 size used by the montage.
The next section assigns their playback duration.
```kotlin highlight-android-create-clips
val clipA = createVideoClip(
engine = currentEngine,
name = "Clip A",
videoUri = videoUri,
width = 1920F,
height = 1080F,
)
val clipB = createVideoClip(
engine = currentEngine,
name = "Clip B",
videoUri = videoUri,
width = 1920F,
height = 1080F,
)
val clipC = createVideoClip(
engine = currentEngine,
name = "Clip C",
videoUri = videoUri,
width = 1920F,
height = 1080F,
)
```
### Creating Tracks
Create a track, attach it to the page, and disable automatic offset management
because this sample writes the clip offsets directly.
```kotlin highlight-android-create-track
val track = currentEngine.block.create(DesignBlockType.Track)
currentEngine.block.appendChild(parent = page, child = track)
currentEngine.block.setBoolean(
block = track,
property = "track/automaticallyManageBlockOffsets",
value = false,
)
```
### Adding Clips to Track
Append the clips to the track so they share a timeline container. Calling
`fillParent()` on the track after appending the clips fills the child clips
against the page frame first, then resizes and positions the track itself.
```kotlin highlight-android-add-clips-to-track
currentEngine.block.appendChild(parent = track, child = clipA)
currentEngine.block.appendChild(parent = track, child = clipB)
currentEngine.block.appendChild(parent = track, child = clipC)
currentEngine.block.fillParent(track)
val initialTrackChildren = currentEngine.block.getChildren(track)
check(initialTrackChildren == listOf(clipA, clipB, clipC))
```
After appending, `getChildren()` verifies that the clips are attached. The next
section sets their playback offsets.
### Setting Clip Durations
Set each clip duration in seconds. The track duration covers the full 15-second
sequence.
```kotlin highlight-android-set-clip-durations
currentEngine.block.setDuration(clipA, duration = 5.0)
currentEngine.block.setDuration(clipB, duration = 5.0)
currentEngine.block.setDuration(clipC, duration = 5.0)
currentEngine.block.setDuration(track, duration = 15.0)
```
## Arranging Clips
### Time Offsets
Time offsets control when each block becomes active relative to its parent. In a
manual sequence, set each child clip's offset from the cumulative duration of
the preceding clips.
```kotlin highlight-android-time-offsets
currentEngine.block.setTimeOffset(clipA, offset = 0.0)
currentEngine.block.setTimeOffset(clipB, offset = 5.0)
currentEngine.block.setTimeOffset(clipC, offset = 10.0)
val initialTrackDuration = currentEngine.block.getDuration(track)
check(initialTrackDuration == 15.0)
val initialClipStates = currentEngine.block.getChildren(track).map { clip ->
TrackClipState(
name = currentEngine.block.getName(clip),
timeOffset = currentEngine.block.getTimeOffset(clip),
duration = currentEngine.block.getDuration(clip),
)
}
```
Clip A starts at 0 seconds, Clip B at 5 seconds, and Clip C at 10 seconds. With
5-second durations, this creates a continuous 15-second sequence.
### Reordering Clips
Use `insertChild()` to move an existing clip to a specific index. The track
child order changes immediately. Update the time offsets after the move so
playback follows the new order without gaps.
```kotlin highlight-android-reorder-clips
currentEngine.block.insertChild(parent = track, child = clipC, index = 0)
currentEngine.block.setTimeOffset(clipC, offset = 0.0)
currentEngine.block.setTimeOffset(clipA, offset = 5.0)
currentEngine.block.setTimeOffset(clipB, offset = 10.0)
val reorderedTrackDuration = currentEngine.block.getDuration(track)
check(reorderedTrackDuration == 15.0)
val reorderedClipStates = currentEngine.block.getChildren(track).map { clip ->
TrackClipState(
name = currentEngine.block.getName(clip),
timeOffset = currentEngine.block.getTimeOffset(clip),
duration = currentEngine.block.getDuration(clip),
)
}
```
Moving Clip C to index 0 changes the order from A-B-C to C-A-B. The resulting
offsets are C at 0 seconds, A at 5 seconds, and B at 10 seconds.
### Querying Track Children
Use `getChildren()` to inspect track hierarchy and rendering order while
debugging a custom timeline. Pair that readback with `getTimeOffset()` to
persist the playback sequence.
```kotlin highlight-android-query-track-children
val finalClipOrder = currentEngine.block.getChildren(track).map { clip ->
currentEngine.block.getName(clip)
}
val finalClipOffsets = currentEngine.block.getChildren(track).map { clip ->
currentEngine.block.getTimeOffset(clip)
}
check(finalClipOrder == listOf("Clip C", "Clip A", "Clip B"))
check(finalClipOffsets == listOf(0.0, 5.0, 10.0))
```
## Multi-Track Compositions
### Adding Multiple Tracks
Create layered compositions by adding more tracks to the page. This sample adds
an overlay track that starts at 2 seconds and contains a smaller clip in the
bottom-right corner.
```kotlin highlight-android-multi-track
val overlayTrack = currentEngine.block.create(DesignBlockType.Track)
currentEngine.block.appendChild(parent = page, child = overlayTrack)
currentEngine.block.setTimeOffset(overlayTrack, offset = 2.0)
val overlayClip = createVideoClip(
engine = currentEngine,
name = "Overlay Clip",
videoUri = videoUri,
width = 1920F / 4F,
height = 1080F / 4F,
)
currentEngine.block.setDuration(overlayClip, duration = 5.0)
currentEngine.block.appendChild(parent = overlayTrack, child = overlayClip)
currentEngine.block.setPositionX(overlayClip, value = 1920F - 1920F / 4F - 40F)
currentEngine.block.setPositionY(overlayClip, value = 1080F - 1080F / 4F - 40F)
```
### Track Rendering Order
CE.SDK renders page children in order. The first track appears behind later
tracks, so add background video first and overlays or titles later.
- **Background layers**: Full-frame clips on the first track.
- **Overlays**: Smaller clips positioned on later tracks.
- **Titles**: Text or graphics added above the video tracks.
## Troubleshooting
### Clips Not Appearing
Verify that every clip is attached to a track and that the track is attached to
the page. `engine.block.getParent()` and `engine.block.getChildren()` are the
quickest checks for hierarchy issues.
### Wrong Playback Order
Check the track child order first. Default Android tracks automatically pack
child offsets from that order and each clip's duration. When you manage timing
manually, disable `track/automaticallyManageBlockOffsets` and write the offsets
with `setTimeOffset()`. CE.SDK still prevents overlaps inside a single track;
use separate tracks for overlapping or layered clips.
### Video Not Loading
Check that the video URL is reachable and uses a supported format. For Android
samples that use explicit video fills, call `engine.block.forceLoadAVResource()`
on the video fill before you depend on media metadata.
## API Reference
| Method | Description |
| --- | --- |
| `engine.scene.createForVideo()` | Create a scene configured for video playback. |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create the page that holds the video composition. |
| `engine.block.create(blockType=DesignBlockType.Track)` | Create a track for sequential or layered clips. |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create the graphic block used as a video clip. |
| `engine.block.createFill(fillType=FillType.Video)` | Create the video fill attached to a clip block. |
| `engine.block.appendChild(parent=_, child=_)` | Add a clip to a track or a track to a page hierarchy. |
| `engine.block.insertChild(parent=_, child=_, index=_)` | Move or insert a child at a specific rendering-order index. |
| `engine.block.getChildren(block=_)` | Read child blocks in rendering order. |
| `engine.block.setDuration(block=_, duration=_)` | Set a page, track, or clip duration in seconds. |
| `engine.block.getDuration(block=_)` | Read a block duration in seconds. |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when a block starts relative to its parent. |
| `engine.block.getTimeOffset(block=_)` | Read a block's time offset in seconds. |
| `engine.block.setBoolean(block=_, property="track/automaticallyManageBlockOffsets", value=_)` | Switch a track between automatic and manual child offset management. |
| `engine.block.forceLoadAVResource(block=_)` | Load audio or video metadata for a video fill or audio block. |
| `engine.block.fillParent(block=_)` | Resize and position a block to fill its parent frame; when the block is a group or track, child blocks are filled against the enclosing frame first. |
## Next Steps
Now that you can join and arrange clips, continue with related video editing
features:
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Master playback timing and audio mixing
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Understand the complete timeline editing system
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Programmatic Editing"
description: "Edit video scenes with CE.SDK Engine APIs on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-video-edit-programmatic/EditVideoProgrammatically.kt reference-only
import android.app.Application
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportVideoOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SplitOptions
import java.nio.ByteBuffer
fun editVideoProgrammatically(
application: Application,
license: String?, // pass null or empty for evaluation mode with watermark
userId: String,
) = CoroutineScope(Dispatchers.Main).launch {
exportProgrammaticVideoEdit(application = application, license = license, userId = userId)
}
suspend fun exportProgrammaticVideoEdit(
application: Application,
license: String?,
userId: String,
): ProgrammaticVideoEditResult = withContext(Dispatchers.Main) {
var engine: Engine? = null
var engineStarted = false
try {
Engine.init(application)
val currentEngine = Engine.getInstance(id = "ly.img.engine.programmatic-video-editing")
engine = currentEngine
engineStarted = currentEngine.start(license = license, userId = userId)
currentEngine.bindOffscreen(width = 1280, height = 720)
buildProgrammaticVideoEdit(currentEngine)
} finally {
if (engineStarted) {
engine?.stop()
}
}
}
private suspend fun buildProgrammaticVideoEdit(engine: Engine): ProgrammaticVideoEditResult {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 4.0)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.fillParent(track)
val firstClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(firstClip, shape = engine.block.createShape(ShapeType.Rect))
val firstVideoFill = engine.block.createFill(FillType.Video)
// Video fill sources use the generic property-keyed URI setter.
engine.block.setUri(
block = firstVideoFill,
property = "fill/video/fileURI",
value = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
engine.block.setFill(block = firstClip, fill = firstVideoFill)
engine.block.setContentFillMode(firstClip, mode = ContentFillMode.COVER)
val secondClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(secondClip, shape = engine.block.createShape(ShapeType.Rect))
val secondVideoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = secondVideoFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
engine.block.setFill(block = secondClip, fill = secondVideoFill)
engine.block.setContentFillMode(secondClip, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = track, child = firstClip)
engine.block.appendChild(parent = track, child = secondClip)
engine.block.forceLoadAVResource(block = firstVideoFill)
engine.block.forceLoadAVResource(block = secondVideoFill)
check(engine.block.getAVResourceTotalDuration(firstVideoFill) >= 3.0)
engine.block.setDuration(block = firstClip, duration = 2.0)
engine.block.setDuration(block = secondClip, duration = 2.0)
engine.block.setTrimOffset(block = firstVideoFill, offset = 1.0)
engine.block.setTrimLength(block = firstVideoFill, length = 2.0)
val secondSegment = engine.block.split(
block = secondClip,
atTime = 1.0,
options = SplitOptions(selectNewBlock = false),
)
val overlay = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(overlay, shape = engine.block.createShape(ShapeType.Rect))
val overlayFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = overlay, fill = overlayFill)
engine.block.setFillSolidColor(
block = overlay,
color = Color.fromRGBA(r = 1F, g = 0.82F, b = 0.1F, a = 0.85F),
)
engine.block.setWidth(overlay, value = 1280F)
engine.block.setHeight(overlay, value = 72F)
engine.block.setPositionY(overlay, value = 648F)
engine.block.setTimeOffset(block = overlay, offset = 1.25)
engine.block.setDuration(block = overlay, duration = 1.5)
engine.block.appendChild(parent = page, child = overlay)
val editedVideo = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = { progress ->
println(
"Rendered ${progress.renderedFrames} of ${progress.totalFrames} frames",
)
},
options = ExportVideoOptions(
videoBitrate = 8_000_000,
audioBitrate = 128_000,
frameRate = 30F,
targetWidth = 1280F,
targetHeight = 720F,
),
)
check(editedVideo.remaining() > 0)
return ProgrammaticVideoEditResult(
exportedVideo = editedVideo,
pageDuration = engine.block.getDuration(page),
firstClipTrimOffset = engine.block.getTrimOffset(firstVideoFill),
firstClipTrimLength = engine.block.getTrimLength(firstVideoFill),
splitSegmentDuration = engine.block.getDuration(secondSegment),
overlayTimeOffset = engine.block.getTimeOffset(overlay),
)
}
data class ProgrammaticVideoEditResult(
val exportedVideo: ByteBuffer,
val pageDuration: Double,
val firstClipTrimOffset: Double,
val firstClipTrimLength: Double,
val splitSegmentDuration: Double,
val overlayTimeOffset: Double,
)
```
Edit video scenes with CE.SDK Engine APIs when your app needs automation,
custom controls, or template-driven video output.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-video-edit-programmatic)
Programmatic editing works directly on the Engine scene graph instead of the
built-in timeline UI. The Android [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/)
already includes a timeline UI, and the [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/)
guide covers timeline-focused UI and Engine concepts. Use the programmatic
approach when your app needs to create or modify video timelines from code, for
example when generating variants, applying a known edit recipe, or exporting a
scene without opening the editor.
This guide builds a compact two-clip montage, trims the first clip, splits the second clip, adds a timed graphic overlay, and exports the edited page as MP4.
## Understand the Video Timeline
A video scene contains one or more pages. A page owns tracks and timed blocks, and each track arranges its child clips in sequence. In a typical video timeline:
- The scene is created in video mode with `engine.scene.createForVideo()`.
- A page defines the canvas size and the exported playback duration.
- A track contains graphic clip blocks with video fills.
- Audio blocks or overlay blocks can sit on the page timeline beside the track.
- Audio-only media uses `DesignBlockType.Audio`; video fills and audio blocks can both be muted or mixed with volume controls.
Timeline values are measured in seconds. `setDuration()` controls how long a block is active, `setTimeOffset()` controls when a block starts within its parent timeline, and trim values control which source-media segment plays.
## Create a Video Scene
Create a video scene, add a page, set the page size, and set the page duration. The page is the block exported later.
```kotlin highlight-android-create-video-scene
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 4.0)
```
`createForVideo()` enables timeline behavior. The page duration here is `4.0` seconds, so the export renders the first four seconds of the edited page.
## Add and Arrange Clips
Create a track, attach it to the page, and append two graphic blocks with video fills. The track uses the child order to play clips sequentially.
```kotlin highlight-android-add-clips
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.fillParent(track)
val firstClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(firstClip, shape = engine.block.createShape(ShapeType.Rect))
val firstVideoFill = engine.block.createFill(FillType.Video)
// Video fill sources use the generic property-keyed URI setter.
engine.block.setUri(
block = firstVideoFill,
property = "fill/video/fileURI",
value = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
engine.block.setFill(block = firstClip, fill = firstVideoFill)
engine.block.setContentFillMode(firstClip, mode = ContentFillMode.COVER)
val secondClip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(secondClip, shape = engine.block.createShape(ShapeType.Rect))
val secondVideoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = secondVideoFill,
property = "fill/video/fileURI",
value = Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
engine.block.setFill(block = secondClip, fill = secondVideoFill)
engine.block.setContentFillMode(secondClip, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = track, child = firstClip)
engine.block.appendChild(parent = track, child = secondClip)
```
Video fill sources use the generic property-keyed `setUri()` API because there is no convenience method dedicated to `fill/video/fileURI`. The rest of the sample uses typed constants such as `DesignBlockType.Track`, `DesignBlockType.Graphic`, `FillType.Video`, and `ShapeType.Rect`.
The sample sets `ContentFillMode.COVER` so each clip fills the page frame. Pick the mode that matches how the source video should fit into the graphic block:
| Mode | Effect |
| --- | --- |
| `ContentFillMode.CROP` | Uses manual crop positioning. |
| `ContentFillMode.COVER` | Scales content to cover the full block frame and can crop the edges. |
| `ContentFillMode.CONTAIN` | Scales content to fit inside the block frame and can leave empty space. |
## Change Timing and Trim
Load AV metadata before changing trim values or reading duration metadata. Then set each clip's timeline duration and trim the first fill to start one second into its source media.
```kotlin highlight-android-change-timing-trim
engine.block.forceLoadAVResource(block = firstVideoFill)
engine.block.forceLoadAVResource(block = secondVideoFill)
check(engine.block.getAVResourceTotalDuration(firstVideoFill) >= 3.0)
engine.block.setDuration(block = firstClip, duration = 2.0)
engine.block.setDuration(block = secondClip, duration = 2.0)
engine.block.setTrimOffset(block = firstVideoFill, offset = 1.0)
engine.block.setTrimLength(block = firstVideoFill, length = 2.0)
```
Use these APIs for different timing layers:
| API | Effect |
| --- | --- |
| `engine.block.setDuration(block=_, duration=_)` | Sets how long the block is active on the timeline. |
| `engine.block.setTimeOffset(block=_, offset=_)` | Sets when the block starts within its parent timeline. |
| `engine.block.setTrimOffset(block=_, offset=_)` | Sets where source media playback starts. |
| `engine.block.setTrimLength(block=_, length=_)` | Sets how much source media is used for playback. |
## Split a Clip
Split the second clip at one second. The original block becomes the first segment, and `split()` returns the new second segment.
```kotlin highlight-android-split-clip
val secondSegment = engine.block.split(
block = secondClip,
atTime = 1.0,
options = SplitOptions(selectNewBlock = false),
)
```
The sample passes `SplitOptions(selectNewBlock = false)` because a headless workflow does not need to change editor selection after the split.
`SplitOptions` controls how CE.SDK attaches and selects the new segment:
| Field | Default | Effect |
| --- | --- | --- |
| `attachToParent` | `true` | Attaches the returned segment to the same parent as the original block. |
| `createParentTrackIfNeeded` | `false` | Creates a parent track when the split block needs one and `attachToParent` is enabled. |
| `selectNewBlock` | `true` | Selects the returned segment after splitting. |
## Apply a Timed Overlay
Add a short graphic overlay directly to the page timeline. Its `timeOffset` and `duration` make it appear only for part of the exported video.
```kotlin highlight-android-timed-overlay
val overlay = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(overlay, shape = engine.block.createShape(ShapeType.Rect))
val overlayFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = overlay, fill = overlayFill)
engine.block.setFillSolidColor(
block = overlay,
color = Color.fromRGBA(r = 1F, g = 0.82F, b = 0.1F, a = 0.85F),
)
engine.block.setWidth(overlay, value = 1280F)
engine.block.setHeight(overlay, value = 72F)
engine.block.setPositionY(overlay, value = 648F)
engine.block.setTimeOffset(block = overlay, offset = 1.25)
engine.block.setDuration(block = overlay, duration = 1.5)
engine.block.appendChild(parent = page, child = overlay)
```
This same pattern works for other programmatic edits: create or find the target block, set its timing, then apply the specific block or fill properties your workflow needs.
## Export the Edited Video
Export the page as MP4 with `exportVideo()`. The progress callback reports rendered and total frame counts while the export runs, and `ExportVideoOptions` controls output size, frame rate, and bitrate.
```kotlin highlight-android-export-video
val editedVideo = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = { progress ->
println(
"Rendered ${progress.renderedFrames} of ${progress.totalFrames} frames",
)
},
options = ExportVideoOptions(
videoBitrate = 8_000_000,
audioBitrate = 128_000,
frameRate = 30F,
targetWidth = 1280F,
targetHeight = 720F,
),
)
check(editedVideo.remaining() > 0)
```
The backing sample asserts that the returned `ByteBuffer` is non-empty so the automated check verifies a real export result.
`ExportVideoOptions` exposes these public fields:
| Field | Purpose |
| --- | --- |
| `h264Profile` | Selects the H.264 encoder profile. |
| `h264Level` | Selects the H.264 encoder level, for example `52` for level 5.2. |
| `videoBitrate` | Sets video bitrate in bits per second, or `0` for automatic selection. |
| `audioBitrate` | Sets audio bitrate in bits per second, or `0` for automatic selection. |
| `frameRate` | Sets the target export frame rate in Hz. |
| `targetWidth` | Sets the target output width when used with `targetHeight`. |
| `targetHeight` | Sets the target output height when used with `targetWidth`. |
| `allowTextOverhang` | Includes glyph overhang bounds to avoid clipping text during export. |
## API Reference
| Android API | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a scene in video mode. |
| `engine.block.create(blockType=_)` | Create pages, tracks, graphics, audio-only timeline blocks, and other blocks. |
| `engine.block.createFill(fillType=_)` | Create video or color fills. |
| `engine.block.createShape(type=_)` | Create a shape for a graphic block. |
| `engine.block.appendChild(parent=_, child=_)` | Add pages, tracks, clips, and overlays to the hierarchy. |
| `engine.block.fillParent(block=_)` | Resize a block to fill its parent frame. |
| `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block. |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill to a graphic block. |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Set the source URI on a video fill. |
| `engine.block.setContentFillMode(block=_, mode=_)` | Control how video content fits inside the graphic block. |
| `engine.block.setFillSolidColor(block=_, color=_)` | Set the color value on a color fill. |
| `engine.block.setWidth(block=_, value=_)` | Set a block's width in scene units. |
| `engine.block.setHeight(block=_, value=_)` | Set a block's height in scene units. |
| `engine.block.setPositionY(block=_, value=_)` | Set a block's vertical position in scene units. |
| `engine.block.setDuration(block=_, duration=_)` | Set page or block playback duration in seconds. |
| `engine.block.getDuration(block=_)` | Read a block's playback duration in seconds. |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when a block starts in its parent timeline. |
| `engine.block.forceLoadAVResource(block=_)` | Load audio or video metadata before trim and duration queries. |
| `engine.block.getAVResourceTotalDuration(block=_)` | Read the loaded media duration. |
| `engine.block.setTrimOffset(block=_, offset=_)` | Set the source media start offset. |
| `engine.block.setTrimLength(block=_, length=_)` | Set the source media playback length. |
| `engine.block.split(block=_, atTime=_, options=_)` | Split a timed block and return the second segment. |
| `engine.block.setMuted(block=_, muted=_)` | Mute audio on a video fill or audio block. |
| `engine.block.setVolume(block=_, volume=_)` | Set audio volume from `0F` to `1F` on a video fill or audio block. |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Export one edited page as video bytes. |
| `engine.block.exportVideo(blocks=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Export multiple pages sequentially while reusing one worker engine. |
## Next Steps
- [Split Video and Audio](https://img.ly/docs/cesdk/android/edit-video/split-464167/) - Learn how to split video and audio clips at specific time points in CE.SDK, creating two independent segments from a single clip.
- [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) - Control playback range without splitting.
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK.
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Learn to play, pause, seek, and preview audio and video content in CE.SDK using playback controls and solo mode.
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Explore export options, supported formats, and configuration features for sharing or rendering output.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Redact Sensitive Content in Videos"
description: "Redact sensitive video content on Android using blur, pixelization, solid overlays, and timeline controls."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/redaction-cf6d03/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Redaction](https://img.ly/docs/cesdk/android/edit-video/redaction-cf6d03/)
---
```kotlin file=@cesdk_android_examples/engine-guides-redaction/Redaction.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.BlurType
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
private const val PAGE_WIDTH = 1280F
private const val PAGE_HEIGHT = 720F
private const val SEGMENT_DURATION = 5.0
data class Redaction(
val pageDuration: Double,
val fullBlockBlurEnabled: Boolean,
val partialBlurEnabled: Boolean,
val partialRedactionX: Float,
val partialRedactionY: Float,
val partialRedactionWidth: Float,
val partialRedactionHeight: Float,
val partialCropScaleX: Float,
val partialCropScaleY: Float,
val partialCropTranslationX: Float,
val partialCropTranslationY: Float,
val pixelizationEnabled: Boolean,
val solidOverlayDuration: Double,
val timedSourceOffset: Double,
val timedSourceDuration: Double,
val timedRedactionOffset: Double,
val timedRedactionDuration: Double,
val radialBlurEnabled: Boolean,
)
suspend fun redaction(engine: Engine): Redaction = withContext(Dispatchers.Main) {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = PAGE_WIDTH)
engine.block.setHeight(page, value = PAGE_HEIGHT)
engine.block.setDuration(page, duration = SEGMENT_DURATION * 5)
val videoUris = listOf(
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-taryn-elliott-8713114.mp4",
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-taryn-elliott-7108793.mp4",
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-taryn-elliott-7108801.mp4",
"https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-taryn-elliott-8713109.mp4",
)
val videos = videoUris.map { uri ->
createRedactionVideoBlock(engine, uri)
}
videos.forEachIndexed { index, video ->
engine.block.setPositionX(video, value = 0F)
engine.block.setPositionY(video, value = 0F)
engine.block.setDuration(video, duration = SEGMENT_DURATION)
engine.block.setTimeOffset(video, offset = index * SEGMENT_DURATION)
engine.block.appendChild(parent = page, child = video)
}
val radialVideo = videos[0]
val fullBlurVideo = videos[1]
val pixelVideo = videos[2]
val partialBlurVideo = videos[3]
val timedVideo = videos[4]
if (engine.block.supportsBlur(fullBlurVideo)) {
val uniformBlur = engine.block.createBlur(type = BlurType.Uniform)
engine.block.setFloat(
block = uniformBlur,
property = "blur/uniform/intensity",
value = 0.7F,
)
engine.block.setBlur(block = fullBlurVideo, blurBlock = uniformBlur)
engine.block.setBlurEnabled(block = fullBlurVideo, enabled = true)
}
var partialBlurEnabled = false
val partialBlurRedaction = engine.block.duplicate(block = partialBlurVideo)
if (
engine.block.supportsBlur(partialBlurRedaction) &&
engine.block.supportsCrop(partialBlurRedaction)
) {
val redactionX = PAGE_WIDTH * 0.22F
val redactionY = PAGE_HEIGHT * 0.18F
val redactionWidth = PAGE_WIDTH * 0.35F
val redactionHeight = PAGE_HEIGHT * 0.28F
val cropBlur = engine.block.createBlur(type = BlurType.Uniform)
engine.block.setFloat(
block = cropBlur,
property = "blur/uniform/intensity",
value = 0.75F,
)
engine.block.setBlur(block = partialBlurRedaction, blurBlock = cropBlur)
engine.block.setBlurEnabled(block = partialBlurRedaction, enabled = true)
engine.block.setWidth(block = partialBlurRedaction, value = redactionWidth)
engine.block.setHeight(block = partialBlurRedaction, value = redactionHeight)
engine.block.setPositionX(block = partialBlurRedaction, value = redactionX)
engine.block.setPositionY(block = partialBlurRedaction, value = redactionY)
// The smaller frame bounds the redaction; crop keeps its source pixels aligned.
engine.block.setCropScaleX(block = partialBlurRedaction, scaleX = PAGE_WIDTH / redactionWidth)
engine.block.setCropScaleY(block = partialBlurRedaction, scaleY = PAGE_HEIGHT / redactionHeight)
engine.block.setCropTranslationX(
block = partialBlurRedaction,
translationX = -redactionX / redactionWidth,
)
engine.block.setCropTranslationY(
block = partialBlurRedaction,
translationY = -redactionY / redactionHeight,
)
partialBlurEnabled = engine.block.isBlurEnabled(partialBlurRedaction)
}
var pixelizationEnabled = false
if (engine.block.supportsEffects(pixelVideo)) {
val pixelizeEffect = engine.block.createEffect(type = EffectType.Pixelize)
engine.block.setInt(
block = pixelizeEffect,
property = "effect/pixelize/horizontalPixelSize",
value = 24,
)
engine.block.setInt(
block = pixelizeEffect,
property = "effect/pixelize/verticalPixelSize",
value = 24,
)
engine.block.appendEffect(block = pixelVideo, effectBlock = pixelizeEffect)
engine.block.setEffectEnabled(effectBlock = pixelizeEffect, enabled = true)
pixelizationEnabled = engine.block.isEffectEnabled(pixelizeEffect)
}
val overlay = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(type = ShapeType.Rect)
engine.block.setShape(block = overlay, shape = rectShape)
val solidFill = engine.block.createFill(fillType = FillType.Color)
engine.block.setColor(
block = solidFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.1F, g = 0.1F, b = 0.1F, a = 1.0F),
)
engine.block.setFill(block = overlay, fill = solidFill)
engine.block.setWidth(overlay, value = PAGE_WIDTH * 0.4F)
engine.block.setHeight(overlay, value = PAGE_HEIGHT * 0.3F)
engine.block.setPositionX(overlay, value = PAGE_WIDTH * 0.55F)
engine.block.setPositionY(overlay, value = PAGE_HEIGHT * 0.65F)
engine.block.appendChild(parent = page, child = overlay)
engine.block.setTimeOffset(overlay, offset = 3 * SEGMENT_DURATION)
engine.block.setDuration(overlay, duration = SEGMENT_DURATION)
val timedRedaction = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(
block = timedRedaction,
shape = engine.block.createShape(type = ShapeType.Rect),
)
val timedFill = engine.block.createFill(fillType = FillType.Color)
engine.block.setColor(
block = timedFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.05F, g = 0.05F, b = 0.05F, a = 1.0F),
)
engine.block.setFill(block = timedRedaction, fill = timedFill)
engine.block.setWidth(timedRedaction, value = PAGE_WIDTH * 0.35F)
engine.block.setHeight(timedRedaction, value = PAGE_HEIGHT * 0.22F)
engine.block.setPositionX(timedRedaction, value = PAGE_WIDTH * 0.32F)
engine.block.setPositionY(timedRedaction, value = PAGE_HEIGHT * 0.24F)
engine.block.setTimeOffset(timedRedaction, offset = 4 * SEGMENT_DURATION)
engine.block.setDuration(timedRedaction, duration = SEGMENT_DURATION)
engine.block.appendChild(parent = page, child = timedRedaction)
if (engine.block.supportsBlur(radialVideo)) {
val radialBlur = engine.block.createBlur(type = BlurType.Radial)
// Radial blur leaves the radius clear; use it to protect content outside that focus area.
engine.block.setFloat(radialBlur, property = "blur/radial/blurRadius", value = 50F)
engine.block.setFloat(radialBlur, property = "blur/radial/radius", value = 75F)
engine.block.setFloat(radialBlur, property = "blur/radial/gradientRadius", value = 80F)
engine.block.setFloat(radialBlur, property = "blur/radial/x", value = 0.5F)
engine.block.setFloat(radialBlur, property = "blur/radial/y", value = 0.45F)
engine.block.setBlur(block = radialVideo, blurBlock = radialBlur)
engine.block.setBlurEnabled(block = radialVideo, enabled = true)
}
Redaction(
pageDuration = engine.block.getDuration(page),
fullBlockBlurEnabled = engine.block.isBlurEnabled(fullBlurVideo),
partialBlurEnabled = partialBlurEnabled,
partialRedactionX = engine.block.getPositionX(partialBlurRedaction),
partialRedactionY = engine.block.getPositionY(partialBlurRedaction),
partialRedactionWidth = engine.block.getWidth(partialBlurRedaction),
partialRedactionHeight = engine.block.getHeight(partialBlurRedaction),
partialCropScaleX = engine.block.getCropScaleX(partialBlurRedaction),
partialCropScaleY = engine.block.getCropScaleY(partialBlurRedaction),
partialCropTranslationX = engine.block.getCropTranslationX(partialBlurRedaction),
partialCropTranslationY = engine.block.getCropTranslationY(partialBlurRedaction),
pixelizationEnabled = pixelizationEnabled,
solidOverlayDuration = engine.block.getDuration(overlay),
timedSourceOffset = engine.block.getTimeOffset(timedVideo),
timedSourceDuration = engine.block.getDuration(timedVideo),
timedRedactionOffset = engine.block.getTimeOffset(timedRedaction),
timedRedactionDuration = engine.block.getDuration(timedRedaction),
radialBlurEnabled = engine.block.isBlurEnabled(radialVideo),
)
}
private fun createRedactionVideoBlock(
engine: Engine,
uri: String,
): DesignBlock {
val video = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect))
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse(uri),
)
engine.block.setFill(video, fill = videoFill)
engine.block.setWidth(video, value = PAGE_WIDTH)
engine.block.setHeight(video, value = PAGE_HEIGHT)
return video
}
```
Redact sensitive video content using blur, pixelization, or solid overlays for privacy protection.
> **Reading time:** 12 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-redaction)
The Android [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) is the recommended starting point for UI-based video editing workflows. Use the Engine APIs in this guide when you need to prepare redacted video content programmatically, automate privacy edits, or apply redactions before export.
CE.SDK applies effects to blocks themselves, not as overlays affecting content beneath. This means full-block redaction applies the effect directly to the video block, while partial redaction uses a duplicated video block that is resized and positioned as a bounded patch. Crop values align the duplicate's source pixels with the original video inside that patch.
This guide covers the built-in Android editor controls for blur and pixelization, then shows how to create full-block, partial, solid, and time-based redactions programmatically with the CreativeEngine.
## Understanding Redaction in CE.SDK
### How Effects Work
Effects in CE.SDK modify the block's appearance directly rather than creating transparent overlays that affect content beneath. When you blur a video block, the entire block becomes blurred.
### Choosing a Redaction Technique
Select your technique based on privacy requirements and visual impact:
- **Full-block blur**: Complete obscuration for backgrounds or placeholder content
- **Partial blur**: A duplicated video patch that is resized, positioned, blurred, and crop-aligned over one sensitive region
- **Radial blur**: Circular blur patterns that keep a focus region clear while obscuring surrounding content
- **Pixelization**: Clearly intentional censoring that is faster to render than heavy blur
- **Solid overlays**: Complete blocking for highly sensitive information like documents or credentials
## Using the Built-in UI
### Accessing Blur Controls
In the Android editor, select a video block and open the blur controls from the block options. The editor exposes blur presets such as uniform, radial, linear, and mirrored blur, with adjustment controls for the selected blur type.
Uniform blur applies consistent intensity across the entire block. Higher intensity creates stronger privacy protection but increases rendering work.
### Accessing Pixelization Controls
Select a video block, open the effects controls, and choose the pixelize effect. Adjust the horizontal and vertical pixel sizes to control the mosaic block dimensions.
Larger pixel sizes create stronger obscuration but are more visually disruptive. Values between 15 and 30 pixels work well for standard redaction scenarios.
### Creating Partial Redactions
For face, license-plate, or screen-detail redaction, duplicate the original video block first. Apply blur or pixelization to the duplicate, resize and position its frame over the sensitive region, then adjust crop values so the duplicate shows the same source area as the original underneath.
## Programmatic Redaction
### Full-Block Blur
When the entire video needs obscuring, apply blur directly to the original block without duplication. Check that the block supports blur, create a uniform blur, configure its intensity, attach it to the video block, and enable it.
```kotlin highlight-android-full-block-blur
if (engine.block.supportsBlur(fullBlurVideo)) {
val uniformBlur = engine.block.createBlur(type = BlurType.Uniform)
engine.block.setFloat(
block = uniformBlur,
property = "blur/uniform/intensity",
value = 0.7F,
)
engine.block.setBlur(block = fullBlurVideo, blurBlock = uniformBlur)
engine.block.setBlurEnabled(block = fullBlurVideo, enabled = true)
}
```
The uniform blur intensity ranges from 0.0 to 1.0. Higher values create stronger blur.
### Partial Blur
When only one region needs obscuring, duplicate the original video block and apply blur to the duplicate. Then resize and position the duplicate as the redaction rectangle, and use crop scale and translation values to keep the source pixels aligned with the original video.
```kotlin highlight-android-partial-blur
val partialBlurRedaction = engine.block.duplicate(block = partialBlurVideo)
if (
engine.block.supportsBlur(partialBlurRedaction) &&
engine.block.supportsCrop(partialBlurRedaction)
) {
val redactionX = PAGE_WIDTH * 0.22F
val redactionY = PAGE_HEIGHT * 0.18F
val redactionWidth = PAGE_WIDTH * 0.35F
val redactionHeight = PAGE_HEIGHT * 0.28F
val cropBlur = engine.block.createBlur(type = BlurType.Uniform)
engine.block.setFloat(
block = cropBlur,
property = "blur/uniform/intensity",
value = 0.75F,
)
engine.block.setBlur(block = partialBlurRedaction, blurBlock = cropBlur)
engine.block.setBlurEnabled(block = partialBlurRedaction, enabled = true)
engine.block.setWidth(block = partialBlurRedaction, value = redactionWidth)
engine.block.setHeight(block = partialBlurRedaction, value = redactionHeight)
engine.block.setPositionX(block = partialBlurRedaction, value = redactionX)
engine.block.setPositionY(block = partialBlurRedaction, value = redactionY)
// The smaller frame bounds the redaction; crop keeps its source pixels aligned.
engine.block.setCropScaleX(block = partialBlurRedaction, scaleX = PAGE_WIDTH / redactionWidth)
engine.block.setCropScaleY(block = partialBlurRedaction, scaleY = PAGE_HEIGHT / redactionHeight)
engine.block.setCropTranslationX(
block = partialBlurRedaction,
translationX = -redactionX / redactionWidth,
)
engine.block.setCropTranslationY(
block = partialBlurRedaction,
translationY = -redactionY / redactionHeight,
)
partialBlurEnabled = engine.block.isBlurEnabled(partialBlurRedaction)
}
```
`duplicate()` keeps the copy on top of the original block. The duplicate's frame bounds the visible redaction area; crop scale and translation align the duplicated video content inside that smaller frame. The same bounded-duplicate workflow also works with pixelization.
### Pixelization
Pixelization creates a mosaic effect that is clearly intentional. Use the effect system rather than the blur system for pixelization.
```kotlin highlight-android-pixelization
if (engine.block.supportsEffects(pixelVideo)) {
val pixelizeEffect = engine.block.createEffect(type = EffectType.Pixelize)
engine.block.setInt(
block = pixelizeEffect,
property = "effect/pixelize/horizontalPixelSize",
value = 24,
)
engine.block.setInt(
block = pixelizeEffect,
property = "effect/pixelize/verticalPixelSize",
value = 24,
)
engine.block.appendEffect(block = pixelVideo, effectBlock = pixelizeEffect)
engine.block.setEffectEnabled(effectBlock = pixelizeEffect, enabled = true)
pixelizationEnabled = engine.block.isEffectEnabled(pixelizeEffect)
}
```
Check `supportsEffects()` before creating the pixelize effect. The horizontal and vertical pixel sizes control the mosaic block dimensions.
### Solid Overlays
For complete blocking without any visual hint of the underlying content, create an opaque shape overlay. This approach does not require video block duplication.
```kotlin highlight-android-solid-overlay
val overlay = engine.block.create(DesignBlockType.Graphic)
val rectShape = engine.block.createShape(type = ShapeType.Rect)
engine.block.setShape(block = overlay, shape = rectShape)
val solidFill = engine.block.createFill(fillType = FillType.Color)
engine.block.setColor(
block = solidFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.1F, g = 0.1F, b = 0.1F, a = 1.0F),
)
engine.block.setFill(block = overlay, fill = solidFill)
engine.block.setWidth(overlay, value = PAGE_WIDTH * 0.4F)
engine.block.setHeight(overlay, value = PAGE_HEIGHT * 0.3F)
engine.block.setPositionX(overlay, value = PAGE_WIDTH * 0.55F)
engine.block.setPositionY(overlay, value = PAGE_HEIGHT * 0.65F)
engine.block.appendChild(parent = page, child = overlay)
```
The overlay uses absolute page coordinates for positioning. Set the alpha channel to 1.0 for complete opacity.
### Time-Based Redaction
Redactions can appear only during specific portions of the video. Use a separate redaction layer, then call `setTimeOffset()` and `setDuration()` on that layer so the source video timing stays intact.
```kotlin highlight-android-time-based-redaction
val timedRedaction = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(
block = timedRedaction,
shape = engine.block.createShape(type = ShapeType.Rect),
)
val timedFill = engine.block.createFill(fillType = FillType.Color)
engine.block.setColor(
block = timedFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.05F, g = 0.05F, b = 0.05F, a = 1.0F),
)
engine.block.setFill(block = timedRedaction, fill = timedFill)
engine.block.setWidth(timedRedaction, value = PAGE_WIDTH * 0.35F)
engine.block.setHeight(timedRedaction, value = PAGE_HEIGHT * 0.22F)
engine.block.setPositionX(timedRedaction, value = PAGE_WIDTH * 0.32F)
engine.block.setPositionY(timedRedaction, value = PAGE_HEIGHT * 0.24F)
engine.block.setTimeOffset(timedRedaction, offset = 4 * SEGMENT_DURATION)
engine.block.setDuration(timedRedaction, duration = SEGMENT_DURATION)
engine.block.appendChild(parent = page, child = timedRedaction)
```
The time offset specifies when the overlay appears in seconds from the start of its parent timeline, and the duration controls how long it remains visible. If you need blurred or pixelized partial redaction instead of an opaque block, use the same timing pattern on a bounded duplicate video layer.
### Radial Blur
Radial blur keeps the center region unblurred and increases blur outside that focus area. Use it when the content to protect is around the focus region, not when the sensitive subject is centered. To obscure a centered face, license plate, or document area, use pixelization, uniform blur on a duplicate crop, or a solid overlay.
```kotlin highlight-android-radial-blur
if (engine.block.supportsBlur(radialVideo)) {
val radialBlur = engine.block.createBlur(type = BlurType.Radial)
// Radial blur leaves the radius clear; use it to protect content outside that focus area.
engine.block.setFloat(radialBlur, property = "blur/radial/blurRadius", value = 50F)
engine.block.setFloat(radialBlur, property = "blur/radial/radius", value = 75F)
engine.block.setFloat(radialBlur, property = "blur/radial/gradientRadius", value = 80F)
engine.block.setFloat(radialBlur, property = "blur/radial/x", value = 0.5F)
engine.block.setFloat(radialBlur, property = "blur/radial/y", value = 0.45F)
engine.block.setBlur(block = radialVideo, blurBlock = radialBlur)
engine.block.setBlurEnabled(block = radialVideo, enabled = true)
}
```
Radial blur properties control the focus center (`x`, `y` from 0.0 to 1.0), the unblurred center area (`radius` from 0.0 to 100.0), the blur transition zone (`gradientRadius` from 0.0 to 100.0), and the blur strength outside the focus region (`blurRadius` from 0.0 to 100.0).
## Performance Considerations
Different redaction techniques have different performance impacts:
- **Solid overlays**: Minimal impact, and you can create many without significant overhead
- **Pixelization**: Faster than blur, with larger pixel sizes adding little extra work
- **Blur effects**: Higher intensity and radius values increase rendering time
For complex scenes with multiple redactions, use solid overlays where blur is not necessary, or reduce blur intensity to maintain smooth processing.
## Troubleshooting
### Redaction Not Visible
If your redaction does not appear, verify that:
- The overlay is attached to the page with `appendChild()`
- Blur is enabled with `setBlurEnabled()` after assigning it with `setBlur()`
- Effects are enabled with `setEffectEnabled()` after appending them with `appendEffect()`
- Partial redactions resize and position the duplicate frame before using crop values for source alignment
### Performance Issues
Reduce blur intensity, use pixelization instead of heavy blur, or switch to solid overlays for redactions that do not need the underlying content to remain recognizable.
## Best Practices
- **Preview thoroughly**: Scrub the timeline to verify that all sensitive content is covered.
- **Add safety margins**: Make redaction regions slightly larger than the sensitive area.
- **Test at export resolution**: Higher resolutions may need stronger blur or larger pixel sizes.
- **Archive originals**: Exported redactions are permanent and cannot be reversed.
## API Reference
| Method | Description |
| ------ | ----------- |
| `engine.block.supportsBlur(block=_)` | Check whether a block supports blur |
| `engine.block.createBlur(type=BlurType.Uniform)` | Create a blur block |
| `engine.block.setFloat(block=_, property="blur/uniform/intensity", value=_)` | Set the uniform blur intensity |
| `engine.block.setFloat(block=_, property="blur/radial/blurRadius", value=_)` | Set the radial blur strength outside the focus region |
| `engine.block.setFloat(block=_, property="blur/radial/radius", value=_)` | Set the unblurred center radius for radial blur |
| `engine.block.setFloat(block=_, property="blur/radial/gradientRadius", value=_)` | Set the radial blur transition radius |
| `engine.block.setFloat(block=_, property="blur/radial/x", value=_)` | Set the radial blur center x coordinate |
| `engine.block.setFloat(block=_, property="blur/radial/y", value=_)` | Set the radial blur center y coordinate |
| `engine.block.setBlur(block=_, blurBlock=_)` | Apply a blur block to a design block |
| `engine.block.setBlurEnabled(block=_, enabled=_)` | Enable or disable a block's blur |
| `engine.block.duplicate(block=_, attachToParent=_)` | Duplicate a video block for partial redaction |
| `engine.block.supportsCrop(block=_)` | Check whether a block supports crop properties |
| `engine.block.setCropScaleX(block=_, scaleX=_)` | Scale source video content inside the redaction frame |
| `engine.block.setCropScaleY(block=_, scaleY=_)` | Scale source video content inside the redaction frame |
| `engine.block.setCropTranslationX(block=_, translationX=_)` | Align the source content horizontally inside the redaction frame |
| `engine.block.setCropTranslationY(block=_, translationY=_)` | Align the source content vertically inside the redaction frame |
| `engine.block.supportsEffects(block=_)` | Check whether a block supports effects |
| `engine.block.createEffect(type=EffectType.Pixelize)` | Create an effect block |
| `engine.block.setInt(block=_, property="effect/pixelize/horizontalPixelSize", value=_)` | Set the horizontal pixelization block size |
| `engine.block.setInt(block=_, property="effect/pixelize/verticalPixelSize", value=_)` | Set the vertical pixelization block size |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Add an effect block to a design block |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enable or disable an effect |
| `engine.block.setTimeOffset(block=_, offset=_)` | Set when a block appears on its parent timeline |
| `engine.block.setDuration(block=_, duration=_)` | Set how long a block remains active |
| `engine.block.getDuration(block=_)` | Read a block's active duration |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a graphic block |
| `engine.block.appendChild(parent=_, child=_)` | Add a block to a scene, page, or parent block |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangle shape |
| `engine.block.setShape(block=_, shape=_)` | Apply a shape to a graphic block |
| `engine.block.createFill(fillType=FillType.Color)` | Create a fill block |
| `engine.block.setFill(block=_, fill=_)` | Apply a fill to a block |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Set a video fill source URI |
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set a color property |
| `engine.block.setWidth(block=_, value=_)` | Set a block width |
| `engine.block.setHeight(block=_, value=_)` | Set a block height |
| `engine.block.setPositionX(block=_, value=_)` | Set a block's x position |
| `engine.block.setPositionY(block=_, value=_)` | Set a block's y position |
| `engine.block.isBlurEnabled(block=_)` | Check whether blur is enabled on a block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Check whether an effect block is enabled |
| `engine.block.getTimeOffset(block=_)` | Read when a block appears on its parent timeline |
## Next Steps
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Apply, configure, stack, and manage filters and effects.
- [Create and Edit Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) - Create shape blocks for solid redaction overlays
- [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) - Position and resize video blocks before adding redactions
- [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/) - Export video compositions as MP4 files with configurable encoding options, progress tracking, and resolution control.
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Control timing, duration, and playback for media blocks
- [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) — Documentation for Trim
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Split Video and Audio"
description: "Learn how to split video and audio clips at specific time points in CE.SDK for Android."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/split-464167/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Split](https://img.ly/docs/cesdk/android/edit-video/split-464167/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-video-split/SplitVideoAndAudio.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DemoAssetSource
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.FindAssetsQuery
import ly.img.engine.ShapeType
import ly.img.engine.SplitOptions
import ly.img.engine.addDemoAssetSources
import kotlin.math.abs
data class SplitTiming(
val originalTrimOffsetBefore: Double,
val originalTrimLengthBefore: Double,
val originalTrimOffsetAfter: Double,
val originalTrimLengthAfter: Double,
val newBlockTrimOffset: Double,
val newBlockTrimLength: Double,
)
data class SplitGuideResult(
val basicSegmentDurations: List,
val audioSegmentDurations: List,
val playheadSegmentDurations: List,
val multiTrackSegmentDurations: List,
val splitTiming: SplitTiming,
val audioSplitTiming: SplitTiming,
val remainingSegmentCount: Int,
val validatedSplitCreated: Boolean,
)
suspend fun splitVideoAndAudio(engine: Engine): SplitGuideResult = withContext(Dispatchers.Main) {
runSplitGuide(engine)
}
private suspend fun runSplitGuide(engine: Engine): SplitGuideResult {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 12.0)
val basicClip = createVideoClip(engine, page, name = "Basic split", videoUri = VIDEO_URI)
val basicNewClip = splitAtSpecificTime(engine, basicClip)
checkClose(engine.block.getDuration(basicClip), 5.0, "basic first segment duration")
checkClose(engine.block.getDuration(basicNewClip), 5.0, "basic second segment duration")
val audioUri = loadDemoAudioUri(engine)
val audioClip = createAudioClip(engine, page, name = "Audio split", audioUri = audioUri)
val audioNewClip = splitAtSpecificTime(engine, audioClip)
checkClose(engine.block.getDuration(audioClip), 5.0, "audio first segment duration")
checkClose(engine.block.getDuration(audioNewClip), 5.0, "audio second segment duration")
val optionsClip = createVideoClip(engine, page, name = "Options split", videoUri = VIDEO_URI)
splitWithOptions(engine, optionsClip)
val playheadClip = createVideoClip(
engine = engine,
page = page,
name = "Playhead split",
videoUri = VIDEO_URI,
trackOffset = 1.0,
)
engine.block.setPlaybackTime(page, time = 5.0)
val playheadNewClip = splitAtPlayhead(engine, page, playheadClip)
checkClose(engine.block.getDuration(playheadClip), 4.0, "playhead first segment duration")
checkClose(engine.block.getDuration(playheadNewClip), 6.0, "playhead second segment duration")
val multiTrackPage = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = multiTrackPage)
engine.block.setWidth(multiTrackPage, value = 1280F)
engine.block.setHeight(multiTrackPage, value = 720F)
engine.block.setDuration(multiTrackPage, duration = 12.0)
val multiTrackClipA = createVideoClip(
engine = engine,
page = multiTrackPage,
name = "Track A",
videoUri = VIDEO_URI,
trackOffset = 1.0,
)
val multiTrackClipB = createVideoClip(
engine = engine,
page = multiTrackPage,
name = "Track B",
videoUri = VIDEO_URI,
trackOffset = 2.0,
)
val multiTrackNewSegments = splitClipsAcrossTracks(
engine = engine,
page = multiTrackPage,
timelineTime = 4.0,
)
check(multiTrackNewSegments.size == 2)
val multiTrackSegmentDurations = listOf(
engine.block.getDuration(multiTrackClipA),
engine.block.getDuration(multiTrackNewSegments[0]),
engine.block.getDuration(multiTrackClipB),
engine.block.getDuration(multiTrackNewSegments[1]),
)
multiTrackSegmentDurations.forEachIndexed { index, duration ->
val expected = listOf(3.0, 7.0, 2.0, 8.0)[index]
checkClose(duration, expected, "multi-track segment $index duration")
}
val resultsClip = createVideoClip(engine, page, name = "Results split", videoUri = VIDEO_URI)
val splitTiming = readSplitTimingAfterSplit(engine, resultsClip)
checkClose(splitTiming.originalTrimOffsetAfter, splitTiming.originalTrimOffsetBefore, "original trim offset")
checkClose(splitTiming.originalTrimLengthAfter, 6.0, "original trim length")
checkClose(splitTiming.newBlockTrimOffset, splitTiming.originalTrimOffsetBefore + 6.0, "new trim offset")
checkClose(splitTiming.newBlockTrimLength, 4.0, "new trim length")
val audioResultsClip = createAudioClip(engine, page, name = "Audio results split", audioUri = audioUri)
val audioSplitTiming = readSplitTimingAfterSplit(engine, audioResultsClip)
checkClose(audioSplitTiming.originalTrimLengthAfter, 6.0, "audio original trim length")
checkClose(audioSplitTiming.newBlockTrimOffset, 6.0, "audio new trim offset")
checkClose(audioSplitTiming.newBlockTrimLength, 4.0, "audio new trim length")
val deleteClip = createVideoClip(engine, page, name = "Split and delete", videoUri = VIDEO_URI)
val keptAfterDeletedRange = splitAndDeleteRange(
engine = engine,
clipBlock = deleteClip,
startTime = 2.0,
endTime = 5.0,
)
check(engine.block.isValid(deleteClip))
check(engine.block.isValid(keptAfterDeletedRange))
checkClose(engine.block.getDuration(deleteClip), 2.0, "leading segment duration")
checkClose(engine.block.getDuration(keptAfterDeletedRange), 5.0, "trailing segment duration")
val remainingSegmentCount = listOf(deleteClip, keptAfterDeletedRange)
.count { segment -> engine.block.isValid(segment) }
val validateClip = createVideoClip(engine, page, name = "Validated split", videoUri = VIDEO_URI)
val validatedSplit = splitWithValidation(
engine = engine,
clipBlock = validateClip,
desiredSplitTime = 4.0,
)
val validatedSplitCreated = validatedSplit?.let { splitBlock ->
engine.block.isValid(splitBlock)
} == true
check(validatedSplitCreated)
return SplitGuideResult(
basicSegmentDurations = listOf(
engine.block.getDuration(basicClip),
engine.block.getDuration(basicNewClip),
),
audioSegmentDurations = listOf(
engine.block.getDuration(audioClip),
engine.block.getDuration(audioNewClip),
),
playheadSegmentDurations = listOf(
engine.block.getDuration(playheadClip),
engine.block.getDuration(playheadNewClip),
),
multiTrackSegmentDurations = multiTrackSegmentDurations,
splitTiming = splitTiming,
audioSplitTiming = audioSplitTiming,
remainingSegmentCount = remainingSegmentCount,
validatedSplitCreated = validatedSplitCreated,
)
}
fun splitAtSpecificTime(
engine: Engine,
clipBlock: DesignBlock,
): DesignBlock {
val splitTime = 5.0
return engine.block.split(block = clipBlock, atTime = splitTime)
}
fun splitWithOptions(
engine: Engine,
clipBlock: DesignBlock,
): DesignBlock = engine.block.split(
block = clipBlock,
atTime = 4.0,
options = SplitOptions(
attachToParent = true,
createParentTrackIfNeeded = true,
selectNewBlock = false,
),
)
fun splitAtPlayhead(
engine: Engine,
page: DesignBlock,
clipBlock: DesignBlock,
): DesignBlock {
val playheadTime = engine.block.getPlaybackTime(page)
val clipStartTime = absoluteTimelineOffset(
engine = engine,
timelineRoot = page,
block = clipBlock,
)
val splitTime = playheadTime - clipStartTime
val clipDuration = engine.block.getDuration(clipBlock)
require(splitTime > 0.0 && splitTime < clipDuration) {
"Split time must be inside the clip duration."
}
return engine.block.split(block = clipBlock, atTime = splitTime)
}
fun absoluteTimelineOffset(
engine: Engine,
timelineRoot: DesignBlock,
block: DesignBlock,
): Double {
var timelineOffset = 0.0
var currentBlock: DesignBlock? = block
while (currentBlock != null && currentBlock != timelineRoot) {
if (engine.block.supportsTimeOffset(currentBlock)) {
timelineOffset += engine.block.getTimeOffset(currentBlock)
}
currentBlock = engine.block.getParent(currentBlock)
}
return timelineOffset
}
fun splitClipsAcrossTracks(
engine: Engine,
page: DesignBlock,
timelineTime: Double,
): List {
val splitBlocks = mutableListOf()
engine.block.getChildren(page)
.filter { child -> engine.block.getType(child) == DesignBlockType.Track.key }
.forEach { track ->
engine.block.getChildren(track).forEach { clip ->
val clipStartTime = absoluteTimelineOffset(
engine = engine,
timelineRoot = page,
block = clip,
)
val clipDuration = engine.block.getDuration(clip)
val splitTime = timelineTime - clipStartTime
if (splitTime > 0.0 && splitTime < clipDuration) {
splitBlocks += engine.block.split(
block = clip,
atTime = splitTime,
options = SplitOptions(selectNewBlock = false),
)
}
}
}
return splitBlocks
}
private fun trimmableTargetForClip(
engine: Engine,
clipBlock: DesignBlock,
): DesignBlock {
val clipType = engine.block.getType(clipBlock)
val trimTarget = if (clipType == DesignBlockType.Audio.key) {
clipBlock
} else {
engine.block.getFill(clipBlock)
}
require(engine.block.supportsTrim(trimTarget)) {
"Clip does not expose trim properties."
}
return trimTarget
}
fun readSplitTimingAfterSplit(
engine: Engine,
clipBlock: DesignBlock,
): SplitTiming {
val originalTrimTarget = trimmableTargetForClip(engine, clipBlock)
val originalTrimOffset = engine.block.getTrimOffset(originalTrimTarget)
val originalTrimLength = engine.block.getTrimLength(originalTrimTarget)
val newBlock = engine.block.split(block = clipBlock, atTime = 6.0)
val newBlockTrimTarget = trimmableTargetForClip(engine, newBlock)
return SplitTiming(
originalTrimOffsetBefore = originalTrimOffset,
originalTrimLengthBefore = originalTrimLength,
originalTrimOffsetAfter = engine.block.getTrimOffset(originalTrimTarget),
originalTrimLengthAfter = engine.block.getTrimLength(originalTrimTarget),
newBlockTrimOffset = engine.block.getTrimOffset(newBlockTrimTarget),
newBlockTrimLength = engine.block.getTrimLength(newBlockTrimTarget),
)
}
fun splitAndDeleteRange(
engine: Engine,
clipBlock: DesignBlock,
startTime: Double,
endTime: Double,
): DesignBlock {
val middleSegment = engine.block.split(
block = clipBlock,
atTime = startTime,
options = SplitOptions(selectNewBlock = false),
)
val trailingSegment = engine.block.split(
block = middleSegment,
atTime = endTime - startTime,
options = SplitOptions(selectNewBlock = false),
)
engine.block.destroy(middleSegment)
return trailingSegment
}
fun splitWithValidation(
engine: Engine,
clipBlock: DesignBlock,
desiredSplitTime: Double,
): DesignBlock? {
val blockDuration = engine.block.getDuration(clipBlock)
return if (desiredSplitTime > 0.0 && desiredSplitTime < blockDuration) {
engine.block.split(block = clipBlock, atTime = desiredSplitTime)
} else {
null
}
}
suspend fun createVideoClip(
engine: Engine,
page: DesignBlock,
name: String,
videoUri: String,
trackOffset: Double = 0.0,
): DesignBlock {
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.fillParent(track)
engine.block.setTimeOffset(block = track, offset = trackOffset)
val videoBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(videoBlock, name)
engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(videoBlock, value = 320F)
engine.block.setHeight(videoBlock, value = 180F)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = Uri.parse(videoUri),
)
engine.block.setFill(block = videoBlock, fill = videoFill)
engine.block.appendChild(parent = track, child = videoBlock)
engine.block.forceLoadAVResource(videoFill)
engine.block.setDuration(videoBlock, duration = 10.0)
return videoBlock
}
suspend fun createAudioClip(
engine: Engine,
page: DesignBlock,
name: String,
audioUri: String,
): DesignBlock {
val audioBlock = engine.block.create(DesignBlockType.Audio)
engine.block.setName(audioBlock, name)
engine.block.setUri(
block = audioBlock,
property = "audio/fileURI",
value = Uri.parse(audioUri),
)
engine.block.appendChild(parent = page, child = audioBlock)
engine.block.forceLoadAVResource(audioBlock)
engine.block.setDuration(audioBlock, duration = 10.0)
return audioBlock
}
private suspend fun loadDemoAudioUri(engine: Engine): String {
val audioSourceId = DemoAssetSource.AUDIO.key
if (audioSourceId !in engine.asset.findAllSources()) {
engine.addDemoAssetSources(exclude = DemoAssetSource.values().toSet() - DemoAssetSource.AUDIO)
}
val audioAsset = engine.asset.fetchAsset(
sourceId = audioSourceId,
assetId = "far_from_home",
) ?: engine.asset.findAssets(
sourceId = audioSourceId,
query = FindAssetsQuery(page = 0, perPage = 10),
).assets.first { it.id.endsWith("far_from_home") }
return requireNotNull(audioAsset.meta?.get("uri")) {
"The demo audio asset does not provide a URI."
}
}
private fun checkClose(
actual: Double,
expected: Double,
label: String,
) {
check(abs(actual - expected) < 0.001) {
"$label expected $expected but was $actual"
}
}
private const val VIDEO_URI = "https://img.ly/static/ubq_video_samples/bbb.mp4"
```
Split video and audio clips at specific time points using CE.SDK's timeline UI
and programmatic split API to create independent segments.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-video-split)
Clip splitting divides one block into two at a specified time. The original block ends at the split point, and the new block starts there. Both blocks reference the same source media with independent timing. This differs from trimming, which adjusts a single block's playback range without creating new blocks.
This guide covers how to use the built-in timeline UI for visual splitting and how to split clips programmatically using the Engine API.
## Splitting Clips via the UI
When a user selects a video or audio clip on the CE.SDK editor timeline, CE.SDK shows the Split control in the inspector bar. The user positions the playhead at the desired point and applies the split action there to divide the selected clip.
CE.SDK creates two independent timeline segments from the selected clip. If the playhead is outside the clip or too close to either edge, CE.SDK shows an out-of-range or short-duration error and no split is created.
For a complete interactive timeline surface, see the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/). This guide keeps the implementation focus on the Engine API used by that UI.
## Programmatic Splitting
For automation, batch editing, or custom controls, use `engine.block.split()` with the block you want to divide. The split time is measured in seconds relative to the clip block.
### Basic Splitting at a Specific Time
Split a video or audio block by providing the block ID and the split time in seconds. The method returns the newly created second segment.
```kotlin highlight-android-basic-split
fun splitAtSpecificTime(
engine: Engine,
clipBlock: DesignBlock,
): DesignBlock {
val splitTime = 5.0
return engine.block.split(block = clipBlock, atTime = splitTime)
}
```
The original block becomes the first segment before the split point. The returned block is the segment after the split point.
### Configuring Split Options
Use `SplitOptions` to control how CE.SDK attaches and selects the new segment.
- `attachToParent` (default: `true`) keeps the new block under the same parent as the original.
- `createParentTrackIfNeeded` (default: `false`) creates a track structure when the block needs one, but only when `attachToParent` is `true`.
- `selectNewBlock` (default: `true`) controls whether the returned segment becomes selected.
```kotlin highlight-android-split-options
fun splitWithOptions(
engine: Engine,
clipBlock: DesignBlock,
): DesignBlock = engine.block.split(
block = clipBlock,
atTime = 4.0,
options = SplitOptions(
attachToParent = true,
createParentTrackIfNeeded = true,
selectNewBlock = false,
),
)
```
Set `selectNewBlock` to `false` when you split multiple clips programmatically and do not want each split to change the current selection.
### Splitting at the Current Playhead Position
To split like the built-in inspector bar action, read the page playback time and convert it to a time relative to the clip. Time offsets are relative to each block's parent, so sum the clip and ancestor offsets up to the page before subtracting that start time from the playhead.
```kotlin highlight-android-absolute-timeline-offset
fun absoluteTimelineOffset(
engine: Engine,
timelineRoot: DesignBlock,
block: DesignBlock,
): Double {
var timelineOffset = 0.0
var currentBlock: DesignBlock? = block
while (currentBlock != null && currentBlock != timelineRoot) {
if (engine.block.supportsTimeOffset(currentBlock)) {
timelineOffset += engine.block.getTimeOffset(currentBlock)
}
currentBlock = engine.block.getParent(currentBlock)
}
return timelineOffset
}
```
Use that absolute timeline offset when calculating the split time for the selected clip.
```kotlin highlight-android-split-at-playhead
fun splitAtPlayhead(
engine: Engine,
page: DesignBlock,
clipBlock: DesignBlock,
): DesignBlock {
val playheadTime = engine.block.getPlaybackTime(page)
val clipStartTime = absoluteTimelineOffset(
engine = engine,
timelineRoot = page,
block = clipBlock,
)
val splitTime = playheadTime - clipStartTime
val clipDuration = engine.block.getDuration(clipBlock)
require(splitTime > 0.0 && splitTime < clipDuration) {
"Split time must be inside the clip duration."
}
return engine.block.split(block = clipBlock, atTime = splitTime)
}
```
Validate the calculated time before splitting. The split point must be greater than `0` and lower than the clip duration.
### Splitting Clips Across Multiple Tracks
To split every track at the same timeline position, iterate the page's tracks with `getChildren()`, inspect each clip's time range, and split only clips that contain the target time.
```kotlin highlight-android-split-multiple-tracks
fun splitClipsAcrossTracks(
engine: Engine,
page: DesignBlock,
timelineTime: Double,
): List {
val splitBlocks = mutableListOf()
engine.block.getChildren(page)
.filter { child -> engine.block.getType(child) == DesignBlockType.Track.key }
.forEach { track ->
engine.block.getChildren(track).forEach { clip ->
val clipStartTime = absoluteTimelineOffset(
engine = engine,
timelineRoot = page,
block = clip,
)
val clipDuration = engine.block.getDuration(clip)
val splitTime = timelineTime - clipStartTime
if (splitTime > 0.0 && splitTime < clipDuration) {
splitBlocks += engine.block.split(
block = clip,
atTime = splitTime,
options = SplitOptions(selectNewBlock = false),
)
}
}
}
return splitBlocks
}
```
This leaves clips that do not span the timeline position unchanged.
## Understanding Split Results
After a split operation, CE.SDK updates the timing of the original and returned blocks.
### Trim Properties After Split
For video graphic blocks, read the trim values on the media fill to inspect how the source media range changed. Audio blocks store trim values on the audio block itself, so call the trim APIs with the audio block ID instead of calling `getFill()`.
```kotlin highlight-android-split-results
private fun trimmableTargetForClip(
engine: Engine,
clipBlock: DesignBlock,
): DesignBlock {
val clipType = engine.block.getType(clipBlock)
val trimTarget = if (clipType == DesignBlockType.Audio.key) {
clipBlock
} else {
engine.block.getFill(clipBlock)
}
require(engine.block.supportsTrim(trimTarget)) {
"Clip does not expose trim properties."
}
return trimTarget
}
fun readSplitTimingAfterSplit(
engine: Engine,
clipBlock: DesignBlock,
): SplitTiming {
val originalTrimTarget = trimmableTargetForClip(engine, clipBlock)
val originalTrimOffset = engine.block.getTrimOffset(originalTrimTarget)
val originalTrimLength = engine.block.getTrimLength(originalTrimTarget)
val newBlock = engine.block.split(block = clipBlock, atTime = 6.0)
val newBlockTrimTarget = trimmableTargetForClip(engine, newBlock)
return SplitTiming(
originalTrimOffsetBefore = originalTrimOffset,
originalTrimLengthBefore = originalTrimLength,
originalTrimOffsetAfter = engine.block.getTrimOffset(originalTrimTarget),
originalTrimLengthAfter = engine.block.getTrimLength(originalTrimTarget),
newBlockTrimOffset = engine.block.getTrimOffset(newBlockTrimTarget),
newBlockTrimLength = engine.block.getTrimLength(newBlockTrimTarget),
)
}
```
The original block keeps its trim offset unchanged, but its trim length is reduced to the split point. The new block advances its trim offset by the split time and uses the remaining trim length. Both blocks continue to reference the same source media, so splitting is non-destructive.
### Timeline Positioning
The original block keeps its `getTimeOffset()`. When `attachToParent` is `true`, the returned block is attached next to the original under the same parent. Use `createParentTrackIfNeeded` when CE.SDK should create a track structure for the split result.
## Split and Delete Workflow
Remove a middle section by splitting at both boundaries and deleting the segment between them.
```kotlin highlight-android-split-and-delete
fun splitAndDeleteRange(
engine: Engine,
clipBlock: DesignBlock,
startTime: Double,
endTime: Double,
): DesignBlock {
val middleSegment = engine.block.split(
block = clipBlock,
atTime = startTime,
options = SplitOptions(selectNewBlock = false),
)
val trailingSegment = engine.block.split(
block = middleSegment,
atTime = endTime - startTime,
options = SplitOptions(selectNewBlock = false),
)
engine.block.destroy(middleSegment)
return trailingSegment
}
```
This workflow is useful for removing pauses, mistakes, or unwanted sections while keeping the leading and trailing segments.
## Validating Split Time
Always validate that the split time is within the block duration before calling `split()`.
```kotlin highlight-android-validate-split-time
fun splitWithValidation(
engine: Engine,
clipBlock: DesignBlock,
desiredSplitTime: Double,
): DesignBlock? {
val blockDuration = engine.block.getDuration(clipBlock)
return if (desiredSplitTime > 0.0 && desiredSplitTime < blockDuration) {
engine.block.split(block = clipBlock, atTime = desiredSplitTime)
} else {
null
}
}
```
Splitting at the beginning, end, or outside the duration can fail because CE.SDK cannot create two valid segments from that time.
## Troubleshooting
### Split Returns Unexpected Block
The returned block is the second segment after the split point. The original block remains the first segment.
### Split Time Out of Range
Use `getDuration()` and validate that the split time is greater than `0` and lower than the block duration before calling `split()`.
### Clip Not Splitting
Check that the target supports trimming with `supportsTrim()`. For video fills and audio blocks, load the media resource with `forceLoadAVResource()` before reading duration or trim metadata.
## API Reference
| API | Purpose |
| --- | --- |
| `engine.block.split(block=_, atTime=_, options=_)` | Splits a block and returns the new second segment |
| `engine.block.getTimeOffset(block=_)` | Returns the block's timeline offset relative to its parent |
| `engine.block.getDuration(block=_)` | Returns the playback duration in seconds |
| `engine.block.getPlaybackTime(block=_)` | Returns the current playback time for a page or playable block |
| `engine.block.getChildren(block=_)` | Returns a block's direct children, such as tracks under a page |
| `engine.block.getParent(block=_)` | Returns the block's parent or `null` for a root block |
| `engine.block.getType(block=_)` | Returns the block type key, such as a track type |
| `engine.block.supportsTimeOffset(block=_)` | Checks whether a block exposes a timeline offset |
| `engine.block.getFill(block=_)` | Returns the fill block of a graphic block |
| `engine.block.getTrimOffset(block=_)` | Returns the media trim offset in seconds |
| `engine.block.getTrimLength(block=_)` | Returns the active media trim length in seconds |
| `engine.block.supportsTrim(block=_)` | Checks whether the block supports trim properties |
| `engine.block.forceLoadAVResource(block=_)` | Loads audio or video metadata before duration and trim reads |
| `engine.block.destroy(block=_)` | Removes a block from the scene |
## Next Steps
- [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) - Control playback range without splitting.
- [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Transform Videos"
description: "Learn how Android video transforms use block geometry, crop transforms, groups, animations, and transform permissions."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform-369f28/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/)
---
```kotlin file=@cesdk_android_examples/engine-guides-video-transform/VideoTransform.kt reference-only
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import ly.img.engine.AnimationType
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GlobalScope
import ly.img.engine.PositionMode
import ly.img.engine.ShapeType
import kotlin.math.PI
import kotlin.math.abs
fun transformVideo(
license: String?,
userId: String,
): Job = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example.video-transform")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1280, height = 720)
try {
val sampleVideoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.setDuration(page, duration = 8.0)
val positionedVideo = engine.block.create(DesignBlockType.Graphic)
val positionedVideoFill = engine.block.createFill(FillType.Video)
engine.block.setName(positionedVideo, "Positioned video")
engine.block.setShape(positionedVideo, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(positionedVideo, value = 80F)
engine.block.setPositionY(positionedVideo, value = 80F)
engine.block.setWidth(positionedVideo, value = 300F)
engine.block.setHeight(positionedVideo, value = 170F)
// Android does not expose a typed setter for the video fill URI property.
engine.block.setUri(block = positionedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri)
engine.block.setFill(block = positionedVideo, fill = positionedVideoFill)
engine.block.setContentFillMode(block = positionedVideo, mode = ContentFillMode.COVER)
engine.block.setDuration(block = positionedVideo, duration = 8.0)
engine.block.appendChild(parent = page, child = positionedVideo)
val rotatedVideo = engine.block.create(DesignBlockType.Graphic)
val rotatedVideoFill = engine.block.createFill(FillType.Video)
engine.block.setName(rotatedVideo, "Rotated video")
engine.block.setShape(rotatedVideo, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(rotatedVideo, value = 460F)
engine.block.setPositionY(rotatedVideo, value = 80F)
engine.block.setWidth(rotatedVideo, value = 300F)
engine.block.setHeight(rotatedVideo, value = 170F)
// Android does not expose a typed setter for the video fill URI property.
engine.block.setUri(block = rotatedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri)
engine.block.setFill(block = rotatedVideo, fill = rotatedVideoFill)
engine.block.setContentFillMode(block = rotatedVideo, mode = ContentFillMode.COVER)
engine.block.setDuration(block = rotatedVideo, duration = 8.0)
engine.block.appendChild(parent = page, child = rotatedVideo)
val croppedVideo = engine.block.create(DesignBlockType.Graphic)
val croppedVideoFill = engine.block.createFill(FillType.Video)
engine.block.setName(croppedVideo, "Cropped video")
engine.block.setShape(croppedVideo, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(croppedVideo, value = 80F)
engine.block.setPositionY(croppedVideo, value = 360F)
engine.block.setWidth(croppedVideo, value = 300F)
engine.block.setHeight(croppedVideo, value = 170F)
// Android does not expose a typed setter for the video fill URI property.
engine.block.setUri(block = croppedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri)
engine.block.setFill(block = croppedVideo, fill = croppedVideoFill)
engine.block.setContentFillMode(block = croppedVideo, mode = ContentFillMode.COVER)
engine.block.setDuration(block = croppedVideo, duration = 8.0)
engine.block.appendChild(parent = page, child = croppedVideo)
val lockedVideo = engine.block.create(DesignBlockType.Graphic)
val lockedVideoFill = engine.block.createFill(FillType.Video)
engine.block.setName(lockedVideo, "Locked video")
engine.block.setShape(lockedVideo, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(lockedVideo, value = 460F)
engine.block.setPositionY(lockedVideo, value = 360F)
engine.block.setWidth(lockedVideo, value = 300F)
engine.block.setHeight(lockedVideo, value = 170F)
// Android does not expose a typed setter for the video fill URI property.
engine.block.setUri(block = lockedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri)
engine.block.setFill(block = lockedVideo, fill = lockedVideoFill)
engine.block.setContentFillMode(block = lockedVideo, mode = ContentFillMode.COVER)
engine.block.setDuration(block = lockedVideo, duration = 8.0)
engine.block.appendChild(parent = page, child = lockedVideo)
engine.block.setPositionXMode(positionedVideo, mode = PositionMode.ABSOLUTE)
engine.block.setPositionYMode(positionedVideo, mode = PositionMode.ABSOLUTE)
engine.block.setPositionX(positionedVideo, value = 120F)
engine.block.setPositionY(positionedVideo, value = 104F)
engine.block.setFlipHorizontal(rotatedVideo, flip = true)
engine.block.scale(rotatedVideo, scale = 1.15F, anchorX = 0.5F, anchorY = 0.5F)
engine.block.setRotation(rotatedVideo, radians = (PI / 8.0).toFloat())
engine.block.setWidth(lockedVideo, value = 280F, maintainCrop = true)
engine.block.setHeight(lockedVideo, value = 158F, maintainCrop = true)
if (engine.block.supportsCrop(croppedVideo)) {
engine.block.setContentFillMode(croppedVideo, mode = ContentFillMode.CROP)
engine.block.setCropScaleRatio(croppedVideo, scaleRatio = 1.35F)
// Crop translations are relative to the block frame dimensions.
engine.block.setCropTranslationX(croppedVideo, translationX = -0.12F)
engine.block.setCropTranslationY(croppedVideo, translationY = 0.08F)
engine.block.setCropRotation(croppedVideo, rotation = (PI / 18.0).toFloat())
engine.block.adjustCropToFillFrame(croppedVideo, minScaleRatio = 1.0F)
}
engine.editor.setSettingBoolean("controlGizmo/showMoveHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showResizeHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showScaleHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showRotateHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showCropHandles", true)
engine.editor.setSettingFloat("controlGizmo/blockScaleDownLimit", 12F)
engine.editor.setSettingEnum("touch/rotateAction", "Rotate")
engine.editor.setSettingEnum("touch/pinchAction", "Scale")
if (engine.block.isGroupable(listOf(positionedVideo, croppedVideo))) {
val group = engine.block.group(listOf(positionedVideo, croppedVideo))
engine.block.setPositionX(group, value = 180F)
engine.block.setRotation(group, radians = (PI / 16.0).toFloat())
}
if (engine.block.supportsAnimation(rotatedVideo)) {
val loopAnimation = engine.block.createAnimation(AnimationType.SpinLoop)
engine.block.setLoopAnimation(rotatedVideo, loopAnimation)
engine.block.setDuration(loopAnimation, duration = 2.0)
engine.block.setTimeOffset(rotatedVideo, offset = 1.0)
}
val blockFrameScopes = listOf("layer/move", "layer/rotate", "layer/resize", "layer/flip")
(blockFrameScopes + "layer/crop").forEach { scope ->
engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DEFER)
}
blockFrameScopes.forEach { scope ->
engine.block.setScopeEnabled(lockedVideo, key = scope, enabled = false)
}
engine.block.setScopeEnabled(lockedVideo, key = "layer/crop", enabled = false)
engine.block.setTransformLocked(lockedVideo, locked = true)
val transformsLocked = engine.block.isTransformLocked(lockedVideo)
val moveScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/move")
val moveAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/move")
val cropScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/crop")
val cropAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/crop")
check(abs(engine.block.getRotation(rotatedVideo) - (PI / 8.0).toFloat()) < 0.001F)
check(engine.block.isFlipHorizontal(rotatedVideo))
check(transformsLocked)
check(!moveScopeEnabled)
check(!moveAllowed)
check(!cropScopeEnabled)
check(!cropAllowed)
check(engine.editor.getSettingBoolean("controlGizmo/showRotateHandles"))
check(abs(engine.editor.getSettingFloat("controlGizmo/blockScaleDownLimit") - 12F) < 0.001F)
check(engine.editor.getSettingEnum("touch/pinchAction") == "Scale")
} finally {
engine.stop()
}
}
```
Transform video blocks by moving, rotating, scaling, cropping, grouping, and
locking them in CE.SDK for Android.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-video-transform)
Video transformations affect either the block frame or the media inside that frame. Block-level transforms change the graphic block's position, rotation, flip state, size, and group placement. Content-level crop transforms reframe the video fill without moving the block itself.
| Transform layer | Use it for | Main APIs |
| --- | --- | --- |
| Block | Move, rotate, flip, scale, and resize the video block in the scene. | `setPositionX()`, `setRotation()`, `setFlipHorizontal()`, `scale()`, `setWidth()` |
| Content | Pan, zoom, or rotate the video inside the block frame. | `setCropScaleRatio()`, `setCropTranslationX()`, `setCropRotation()`, `adjustCropToFillFrame()` |
| Permissions | Restrict end-user edits, crop reframing, or block-frame geometry changes. | `setScopeEnabled()`, `setTransformLocked()` |
The Kotlin snippets below assume the engine is running on the main thread and that each named variable refers to a video graphic block in the current scene.
## Apply Block Transforms
Block transforms affect the whole graphic block. Use absolute or percentage position modes for placement, radians for rotation, booleans for flip states, and an anchor point when scaling.
```kotlin highlight-android-block-transforms
engine.block.setPositionXMode(positionedVideo, mode = PositionMode.ABSOLUTE)
engine.block.setPositionYMode(positionedVideo, mode = PositionMode.ABSOLUTE)
engine.block.setPositionX(positionedVideo, value = 120F)
engine.block.setPositionY(positionedVideo, value = 104F)
engine.block.setFlipHorizontal(rotatedVideo, flip = true)
engine.block.scale(rotatedVideo, scale = 1.15F, anchorX = 0.5F, anchorY = 0.5F)
engine.block.setRotation(rotatedVideo, radians = (PI / 8.0).toFloat())
engine.block.setWidth(lockedVideo, value = 280F, maintainCrop = true)
engine.block.setHeight(lockedVideo, value = 158F, maintainCrop = true)
```
Use `maintainCrop = true` when resizing a video block and you want CE.SDK to adjust crop values so the visible content remains framed.
## Adjust the Video Inside the Frame
Crop transforms change how the video fill appears inside the block frame. They do not change the block's own position or dimensions.
```kotlin highlight-android-content-transforms
if (engine.block.supportsCrop(croppedVideo)) {
engine.block.setContentFillMode(croppedVideo, mode = ContentFillMode.CROP)
engine.block.setCropScaleRatio(croppedVideo, scaleRatio = 1.35F)
// Crop translations are relative to the block frame dimensions.
engine.block.setCropTranslationX(croppedVideo, translationX = -0.12F)
engine.block.setCropTranslationY(croppedVideo, translationY = 0.08F)
engine.block.setCropRotation(croppedVideo, rotation = (PI / 18.0).toFloat())
engine.block.adjustCropToFillFrame(croppedVideo, minScaleRatio = 1.0F)
}
```
Crop translations are scaled offsets relative to the block frame, not pixel or design-unit positions. A `translationX` value of `-0.12` shifts the video content left by 12% of the frame width, while a `translationY` value of `0.08` shifts it down by 8% of the frame height. Positive X moves content right, and positive Y moves content down.
Check `supportsCrop()` before applying crop transforms if your code handles mixed block types. `adjustCropToFillFrame()` prevents empty frame areas after crop scale, translation, or rotation changes.
## Configure Transform Controls
The built-in editor UI reads transform settings from the engine. Configure visible handles and touch behavior when your app needs to expose or hide direct manipulation.
```kotlin highlight-android-transform-controls
engine.editor.setSettingBoolean("controlGizmo/showMoveHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showResizeHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showScaleHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showRotateHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showCropHandles", true)
engine.editor.setSettingFloat("controlGizmo/blockScaleDownLimit", 12F)
engine.editor.setSettingEnum("touch/rotateAction", "Rotate")
engine.editor.setSettingEnum("touch/pinchAction", "Scale")
```
These settings affect the built-in editor interaction layer. `controlGizmo/blockScaleDownLimit` uses screen pixels; the value above keeps scaled blocks at least 12 screen pixels wide and high. Programmatic transform calls use the block APIs. Scope checks for those APIs are disabled by default unless `debug/enforceScopesInAPIs` is enabled, while block-frame transform locks are enforced separately by block-frame transform APIs.
## Transform Groups
Group multiple blocks when they should move or rotate together while preserving their relative placement.
```kotlin highlight-android-group-transforms
if (engine.block.isGroupable(listOf(positionedVideo, croppedVideo))) {
val group = engine.block.group(listOf(positionedVideo, croppedVideo))
engine.block.setPositionX(group, value = 180F)
engine.block.setRotation(group, radians = (PI / 16.0).toFloat())
}
```
Call `isGroupable()` before grouping arbitrary selections. A group receives its own block ID, so you can transform the group with the same block APIs used for single blocks.
## Animate and Time Transforms
Video scenes can combine static transforms with block animations and timeline placement. Attach animation blocks to the transformed video block and set the block's time offset when it should appear later in the page timeline.
```kotlin highlight-android-animated-transforms
if (engine.block.supportsAnimation(rotatedVideo)) {
val loopAnimation = engine.block.createAnimation(AnimationType.SpinLoop)
engine.block.setLoopAnimation(rotatedVideo, loopAnimation)
engine.block.setDuration(loopAnimation, duration = 2.0)
engine.block.setTimeOffset(rotatedVideo, offset = 1.0)
}
```
Use the dedicated animation guides when you need keyframe-style motion, easing, or detailed animation property configuration.
## Restrict Transform Changes
For templates, disable individual transform scopes and use the transform lock for block-frame geometry. Block-level scope flags affect editor interactions when the matching global scope is set to `GlobalScope.DEFER`; use `isAllowedByScope()` to validate the effective editor permission.
```kotlin highlight-android-lock-transforms
val blockFrameScopes = listOf("layer/move", "layer/rotate", "layer/resize", "layer/flip")
(blockFrameScopes + "layer/crop").forEach { scope ->
engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DEFER)
}
blockFrameScopes.forEach { scope ->
engine.block.setScopeEnabled(lockedVideo, key = scope, enabled = false)
}
engine.block.setScopeEnabled(lockedVideo, key = "layer/crop", enabled = false)
engine.block.setTransformLocked(lockedVideo, locked = true)
val transformsLocked = engine.block.isTransformLocked(lockedVideo)
val moveScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/move")
val moveAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/move")
val cropScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/crop")
val cropAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/crop")
```
Use individual scopes when one operation should remain available, such as allowing crop but preventing movement. Disable `layer/crop` when crop or content reframing must stay fixed. `setTransformLocked()` protects block-frame geometry such as moving, rotating, flipping, scaling, and resizing; crop setters use the `layer/crop` permission path instead. `isScopeEnabled()` reads only the block-level flag, while `isAllowedByScope()` combines the global and block-level scope state. Android API calls enforce scope checks only when `debug/enforceScopesInAPIs` is enabled.
## Troubleshooting
| Issue | Check |
| --- | --- |
| A block does not move or rotate | For editor interactions, confirm the block is valid and `isAllowedByScope()` returns true for the needed scope. For Block API calls, also check whether `debug/enforceScopesInAPIs` is enabled and `isTransformLocked()` is false. |
| Position values look wrong | Verify whether the block uses `PositionMode.ABSOLUTE` or `PositionMode.PERCENT`. |
| Rotation uses the wrong angle | Pass radians, not degrees. |
| Crop leaves empty frame areas | Call `adjustCropToFillFrame()` after changing crop scale, translation, or rotation. |
| UI handles are missing | Check `controlGizmo/*` settings and ensure the selected block supports the requested operation. |
## API Reference
| API | Purpose |
| --- | --- |
| `engine.block.setPositionX(block=_,value=_)` | Set the block's x position relative to its parent. |
| `engine.block.setPositionY(block=_,value=_)` | Set the block's y position relative to its parent. |
| `engine.block.setPositionXMode(block=_,mode=_)` | Choose absolute or percentage x positioning. |
| `engine.block.setPositionYMode(block=_,mode=_)` | Choose absolute or percentage y positioning. |
| `engine.block.setRotation(block=_,radians=_)` | Rotate the block around its center. |
| `engine.block.setFlipHorizontal(block=_,flip=_)` | Mirror the block horizontally. |
| `engine.block.setFlipVertical(block=_,flip=_)` | Mirror the block vertically. |
| `engine.block.scale(block=_,scale=_,anchorX=_,anchorY=_)` | Scale the block around a normalized anchor point. |
| `engine.block.setWidth(block=_,value=_,maintainCrop=_)` | Resize the block width and optionally preserve crop framing. |
| `engine.block.setHeight(block=_,value=_,maintainCrop=_)` | Resize the block height and optionally preserve crop framing. |
| `engine.block.setDuration(block=_,duration=_)` | Set how long a page, video block, or animation block participates in the video timeline. |
| `engine.block.setContentFillMode(block=_,mode=_)` | Choose `ContentFillMode.CROP`, `ContentFillMode.COVER`, or `ContentFillMode.CONTAIN` for the block's content. |
| `engine.block.getContentFillMode(block=_)` | Read the block's current content fill mode. |
| `engine.block.supportsCrop(block=_)` | Check whether the block supports crop transforms before applying crop values. |
| `engine.block.setCropScaleX(block=_,scaleX=_)` | Scale the video content horizontally inside the frame. |
| `engine.block.setCropScaleY(block=_,scaleY=_)` | Scale the video content vertically inside the frame. |
| `engine.block.setCropScaleRatio(block=_,scaleRatio=_)` | Uniformly scale the video content inside the frame. |
| `engine.block.setCropTranslationX(block=_,translationX=_)` | Set the relative horizontal crop offset; `1.0` equals one frame width, positive values move content right. |
| `engine.block.setCropTranslationY(block=_,translationY=_)` | Set the relative vertical crop offset; `1.0` equals one frame height, positive values move content down. |
| `engine.block.setCropRotation(block=_,rotation=_)` | Rotate the video content inside the block frame. |
| `engine.block.adjustCropToFillFrame(block=_,minScaleRatio=_)` | Adjust crop values so the content fills the block frame. |
| `engine.block.flipCropHorizontal(block=_)` | Flip the cropped content along its horizontal axis. |
| `engine.block.flipCropVertical(block=_)` | Flip the cropped content along its vertical axis. |
| `engine.block.setCropAspectRatioLocked(block=_,locked=_)` | Keep crop handles constrained to the current aspect ratio. |
| `engine.block.resetCrop(block=_)` | Reset manual crop values and return the content fill mode to cover. |
| `engine.block.isGroupable(blocks=_)` | Check whether selected blocks can be grouped. |
| `engine.block.group(blocks=_)` | Create a group that can be transformed as one block. |
| `engine.block.supportsAnimation(block=_)` | Check whether a block can receive animations. |
| `engine.block.createAnimation(type=_)` | Create an animation block such as `AnimationType.SpinLoop`. |
| `engine.block.setInAnimation(block=_,animation=_)` | Attach an entry animation to a block. |
| `engine.block.setLoopAnimation(block=_,animation=_)` | Attach a looping animation to a block. |
| `engine.block.setOutAnimation(block=_,animation=_)` | Attach an exit animation to a block. |
| `engine.block.setTimeOffset(block=_,offset=_)` | Place the block later in its parent timeline, in seconds. |
| `engine.editor.setGlobalScope(key=_,globalScope=_)` | Set a global scope to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER`. |
| `engine.block.setScopeEnabled(block=_,key=_,enabled=_)` | Enable or disable block-level transform scopes such as `"layer/move"`, `"layer/rotate"`, `"layer/flip"`, `"layer/resize"`, and `"layer/crop"`. |
| `engine.block.isScopeEnabled(block=_,key=_)` | Check only the block-level scope flag. |
| `engine.block.isAllowedByScope(block=_,key=_)` | Check the effective permission after global scope and block-level scope state are combined. |
| `engine.editor.setSettingBoolean(keypath="debug/enforceScopesInAPIs",value=_)` | Enable or disable scope checks inside editing APIs; disabled by default. |
| `engine.block.setTransformLocked(block=_,locked=_)` | Lock or unlock block-frame geometry transforms such as movement, rotation, flip, scale, and resize. |
| `engine.block.isTransformLocked(block=_)` | Check whether block-frame geometry transforms are locked for a block. |
| `engine.block.getPositionXMode(block=_)` | Read whether x positioning uses absolute or percentage values. |
| `engine.block.getPositionYMode(block=_)` | Read whether y positioning uses absolute or percentage values. |
| `engine.block.getRotation(block=_)` | Read the block's rotation in radians. |
| `engine.block.isFlipHorizontal(block=_)` | Check whether the block is mirrored horizontally. |
| `engine.block.isFlipVertical(block=_)` | Check whether the block is mirrored vertically. |
| `engine.block.getCropScaleRatio(block=_)` | Read the uniform crop scale ratio. |
| `engine.block.getCropTranslationX(block=_)` | Read the relative horizontal crop offset. |
| `engine.block.getCropTranslationY(block=_)` | Read the relative vertical crop offset. |
| `engine.block.getCropRotation(block=_)` | Read the crop rotation in radians. |
| `engine.editor.setSettingBoolean(keypath="controlGizmo/showMoveHandles",value=_)` | Show or hide the move handles in the editor UI. |
| `engine.editor.setSettingBoolean(keypath="controlGizmo/showResizeHandles",value=_)` | Show or hide the edge resize handles in the editor UI. |
| `engine.editor.setSettingBoolean(keypath="controlGizmo/showScaleHandles",value=_)` | Show or hide the corner scale handles in the editor UI. |
| `engine.editor.setSettingBoolean(keypath="controlGizmo/showRotateHandles",value=_)` | Show or hide the rotation handles in the editor UI. |
| `engine.editor.setSettingBoolean(keypath="controlGizmo/showCropHandles",value=_)` | Show or hide the crop handles in the editor UI. |
| `engine.editor.setSettingFloat(keypath="controlGizmo/blockScaleDownLimit",value=_)` | Set the minimum on-screen block size while users scale blocks in the editor UI. |
| `engine.editor.setSettingEnum(keypath="touch/rotateAction",value=_)` | Choose how rotation touch gestures map to transform behavior. |
| `engine.editor.setSettingEnum(keypath="touch/pinchAction",value=_)` | Choose how a touch gesture maps to transform behavior. |
## Next Steps
- [Move](https://img.ly/docs/cesdk/android/edit-video/transform/move-aa9d89/) - Position video blocks with absolute or percentage coordinates.
- [Rotate](https://img.ly/docs/cesdk/android/edit-video/transform/rotate-eaf662/) - Rotate video blocks with radians.
- [Flip](https://img.ly/docs/cesdk/android/edit-video/transform/flip-a603b0/) - Mirror video blocks horizontally or vertically.
- [Scale](https://img.ly/docs/cesdk/android/edit-video/transform/scale-f75c8a/) - Scale video blocks around an anchor point.
- [Crop](https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/) - Reframe video content inside a block.
- [Resize](https://img.ly/docs/cesdk/android/edit-video/transform/resize-b1ce14/) - Change video block dimensions while managing crop behavior.
---
## Related Pages
- [Move](https://img.ly/docs/cesdk/android/edit-video/transform/move-aa9d89/) - Position a video relative to its parent using either percentage or units
- [Crop Video in Android](https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/) - Cut out specific areas of a video to focus on key content or change aspect ratio
- [Rotate](https://img.ly/docs/cesdk/android/edit-video/transform/rotate-eaf662/) - Documentation for Rotate
- [Resize](https://img.ly/docs/cesdk/android/edit-video/transform/resize-b1ce14/) - Change the size of individual elements or groups.
- [Scale](https://img.ly/docs/cesdk/android/edit-video/transform/scale-f75c8a/) - Scale videos uniformly in your Android app.
- [Flip Videos](https://img.ly/docs/cesdk/android/edit-video/transform/flip-a603b0/) - Flip videos horizontally or vertically.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Crop Video in Android"
description: "Cut out specific areas of a video to focus on key content or change aspect ratio"
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Crop](https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/)
---
Video cropping is essential for Android video editing apps, enabling users to focus on
important content and optimize videos for different platforms. The CreativeEditor SDK
provides both intuitive crop interfaces and powerful Kotlin APIs for video manipulation.
Whether you're targeting TikTok, Instagram, or YouTube formats, this guide covers all
video cropping scenarios for your Android application.
## Interactive video crop tools
The SDK includes specialized video crop controls designed for Android touch interfaces. These components handle real-time video preview, aspect ratio selection, and provide smooth crop adjustments that maintain video quality during editing.

### User interaction workflow
1. **Select the video** you want to crop.
2. **Tap the crop icon** in the editor toolbar.
3. **Adjust the crop area** by dragging the corners or edges or using two-finger gestures.
4. **Use the tools** to modify the crop flip, rotation and angle or to reset the crop.
5. **Close the Sheet** to finalize the crop.
The cropped video appears in your project, but the underlying original video and crop values are preserved even when you rotate or resize the cropped video.
### Enable and configure crop tool
The default UI allows cropping. When you are creating your own UI or custom toolbars you can configure editing behavior. To ensure the crop tool is available in the UI, make sure it's included in your dock configuration or quick actions.
```kotlin
engine.editor.setSettingBoolean("doubleClickToCropEnabled", true)
engine.editor.setSettingBoolean("controlGizmo/showCropHandles", true)
engine.editor.setSettingBoolean("controlGizmo/showCropScaleHandles", true)
```
The cropping handles are only available when a selected block has a fill of type `FillType.Video`. Otherwise setting the edit mode of the `engine.editor` to crop has no effect.
## Programmatic Cropping
Programmatic cropping gives you complete control over video block boundaries, dimensions, and integration with other transformations like rotation or flipping. This is useful for automation, predefined layouts, or server-synced workflows. When you initially create a fill to insert a video into a block, the engine centers the video in the block and crops any dimension that doesn't match. For example: when a block with dimensions of 400.0 × 400.0 is filled with a video that is 600.0 × 500.0, there will be horizontal cropping.
When working with cropping using code, it's important to remember that you are modifying the scale, translation, rotation, etc. of the underlying video. The examples below always adjust the x and y values equally. This is not required, but adjusting them unequally can distort the video, which might be just what you want.
### Reset Crop
When a video is initially placed into a block it will get crop scale and crop translation values. Resetting the crop will return the video to the original values.

This is a block (called `videoBlock` in the example code) with dimensions of 400 × 400 filled with a video that has dimensions of 720 × 1280. The video has slight scaling and translation applied so that it fills the block evenly. At any time, the code can execute the reset crop command to return it to this stage.
```kotlin
engine.block.resetCrop(videoBlock)
```
### Crop Translation
The translation values adjust the placement of the origin point of a video. You can read and change the values. They are not pixel units or centimeters, they are scaled percentages. A video that has its origin point at the origin point of the crop block will have translation value of 0.0 for x and y.

```kotlin
engine.block.setCropTranslationX(videoBlock, 0.25f)
```
This video has had its translation in the x direction set to 0.25. That moved the video one quarter of its width to the right. Setting the value to -0.25 would change the offset of the origin to the left.
These are absolute values. Setting the x value to 0.25 and then setting it to -0.25 does not move the video to an offset of 0.0.
There is a `setCropTranslationY(block: DesignBlock, translationY: Float)` function to adjust the translation of the video in the vertical direction. Negative values move the video up and positive values move the video down.
To read the current crop translation values you can use the convenience getters for the x and y values.
```kotlin
val currentX = engine.block.getCropTranslationX(videoBlock)
val currentY = engine.block.getCropTranslationY(videoBlock)
```
### Crop scale
The scale values adjust the height and width of the underlying video. Values larger than 1.0 will make the video larger while values less than 1.0 make the video smaller. Unless the video also has offsetting translation applied, the center of the video will move.

This video has been scaled by 1.5 in the x and y directions, but the origin point has not been translated. So, the center of the video has moved.
```kotlin
engine.block.setCropScaleX(videoBlock, 1.5f)
engine.block.setCropScaleY(videoBlock, 1.5f)
```
To read the current crop scale values, use the convenience getters for the x and y values.
```kotlin
val currentX = engine.block.getCropScaleX(videoBlock)
val currentY = engine.block.getCropScaleY(videoBlock)
```
### Crop rotate
The same as when rotating blocks, the crop rotation function uses radians. Positive values rotate clockwise and negative values rotate counter clockwise. The video rotates around its center.

```kotlin
import kotlin.math.PI
engine.block.setCropRotation(videoBlock, (PI / 4.0).toFloat())
```
For working with radians, Kotlin has a constant defined for pi. It can be used as `PI` from `kotlin.math.PI`. Because the `setCropRotation` function takes a `Float` for the rotation value, you can use `.toFloat()` to convert the Double to Float.
### Crop to scale ratio
To center crop a video, you can use the scale ratio. This will adjust the x and y scales of the video evenly, and adjust the translation to keep it centered.

This video has been scaled by 2.0 in the x and y directions. Its translation has been adjusted automatically by -0.5 in the x and y directions to keep the video centered.
```kotlin
engine.block.setCropScaleRatio(videoBlock, 2.0f)
```
Using the crop scale ratio function is the same as calling the translation and scale functions, but in one line.
```kotlin
engine.block.setCropScaleX(videoBlock, 2.0f)
engine.block.setCropScaleY(videoBlock, 2.0f)
engine.block.setCropTranslationX(videoBlock, -0.5f)
engine.block.setCropTranslationY(videoBlock, -0.5f)
```
### Chained crops
Crop operations can be chained together. The order of the chaining impacts the final video.

```kotlin
import kotlin.math.PI
engine.block.setCropScaleRatio(videoBlock, 2.0f)
engine.block.setCropRotation(videoBlock, (PI / 3.0).toFloat())
```

```kotlin
import kotlin.math.PI
engine.block.setCropRotation(videoBlock, (PI / 3.0).toFloat())
engine.block.setCropScaleRatio(videoBlock, 2.0f)
```
### Flipping the crop
There are two functions for crop flipping the video. One for horizontal and one for vertical. They each flip the video along its center.

```kotlin
engine.block.flipCropVertical(videoBlock)
engine.block.flipCropHorizontal(videoBlock)
```
The video will be crop flipped every time the function gets called. So calling the function an even number of times will return the video to its original orientation.
### Filling the frame
When the various crop operations cause the background of the crop block to be displayed, such as in the **Crop Translation** example above, the function
```kotlin
engine.block.adjustCropToFillFrame(videoBlock, minScaleRatio = 1.0f)
```
will adjust the translation values and the scale values of the video so that the entire crop block is filled. This is not the same as resetting the crop.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Flip Videos"
description: "Flip videos horizontally or vertically to create mirror effects and symmetrical designs."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform/flip-a603b0/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Flip](https://img.ly/docs/cesdk/android/edit-video/transform/flip-a603b0/)
---
Video flipping in CreativeEditor SDK (CE.SDK) allows you to mirror video content horizontally or vertically. This transformation is useful for creating symmetrical designs, correcting orientation issues, or achieving specific visual effects in your video projects.
You can flip videos both through the built-in user interface and programmatically using the SDK's APIs, providing flexibility for different workflow requirements.
[Launch Web Demo](https://img.ly/showcases/cesdk)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
## Available Flip Operations
CE.SDK supports two types of video flipping:
- **Horizontal Flip**: Mirror the video along its vertical axis, creating a left-right reflection
- **Vertical Flip**: Mirror the video along its horizontal axis, creating a top-bottom reflection
These operations can be applied individually or combined to achieve the desired visual effect.
## Applying Flips
### UI-Based Flipping
You can apply flips directly in the CE.SDK user interface. The editor provides intuitive controls for horizontally and vertically flipping videos, making it easy for users to quickly mirror content without writing code.
### Programmatic Flipping
Developers can also apply flips programmatically, using the SDK's API. This allows for dynamic video adjustments based on application logic, user input, or automated processes.
## Combining with Other Transforms
Video flipping works seamlessly with other transformation operations like rotation, scaling, and cropping. You can chain multiple transformations to create complex visual effects while maintaining video quality.
## Guides
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Move"
description: "Position a video relative to its parent using either percentage or units"
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform/move-aa9d89/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Move](https://img.ly/docs/cesdk/android/edit-video/transform/move-aa9d89/)
---
Video positioning is crucial for creating professional video compositions in Android apps.
The CreativeEditor SDK enables precise video placement through both touch-based dragging
and coordinate-based Kotlin APIs. Perfect for building video collages, picture-in-picture
effects, or complex multi-layer video compositions.
## Video positioning features
- Touch-based video dragging with smooth gestures
- Coordinate-based positioning through Kotlin code
- Multi-video positioning with relationship preservation
- Position constraints for maintaining video layouts
## Video positioning scenarios
Apply video movement for:
- Creating picture-in-picture video layouts
- Building video collages and multi-layer compositions
- Implementing drag-and-drop video editing interfaces
***
## Move videos with the UI
Users can drag and drop elements directly in the editor canvas.
***
## Move a video block programmatically
Video block position is controlled using the `position/x` and `position/y` properties. They can either use absolute or percentage (relative) values. In addition to setting the properties, there are helper functions.
```kotlin
engine.block.setFloat(videoBlock, "position/x", 150f)
engine.block.setFloat(videoBlock, "position/y", 100f)
```
or
```kotlin
engine.block.setPositionX(videoBlock, 150f)
engine.block.setPositionY(videoBlock, 100f)
```
The preceding code moves the video to coordinates (150, 100) on the canvas. The origin point (0, 0) is at the top-left.
```kotlin
import ly.img.engine.PositionMode
engine.block.setPositionXMode(videoBlock, PositionMode.PERCENT)
engine.block.setPositionYMode(videoBlock, PositionMode.PERCENT)
engine.block.setPositionX(videoBlock, 0.5f)
engine.block.setPositionY(videoBlock, 0.5f)
```
The preceding code moves the video to the center of the canvas, regardless of the dimensions of the canvas. As with setting position, you can update or check the mode using `position/x/mode` and `position/y/mode` properties.
```kotlin
val xPosition = engine.block.getPositionX(videoBlock)
val yPosition = engine.block.getPositionY(videoBlock)
```
***
## Move multiple elements together
Group elements before moving to keep them aligned:
```kotlin
val groupId = engine.block.group(listOf(videoBlock, textBlock))
engine.block.setPositionX(groupId, 200f)
```
The preceding code moves the entire group to 200 from the left edge.
***
## Move relative to current position
To nudge a video instead of setting an absolute position:
```kotlin
val xPosition = engine.block.getPositionX(videoBlock)
engine.block.setPositionX(videoBlock, xPosition + 20f)
```
The preceding code moves the video 20 points to the right.
***
## Lock movement (optional)
When building templates, you might want to lock movement to protect the layout:
```kotlin
engine.block.setScopeEnabled(videoBlock, "layer/move", false)
```
You can also disable all transformations for a block by locking, this is regardless of working with a template.
```kotlin
engine.block.setTransformLocked(videoBlock, true)
```
***
## Troubleshooting
| Issue | Solution |
| ------------------------ | ----------------------------------------------------- |
| Video block not moving | Ensure it is not constrained or locked |
| Unexpected position | Check canvas coordinates and alignment settings |
| Grouped items misaligned | Confirm all items share the same reference point |
| Can't move via UI | Ensure the move feature is enabled in the UI settings |
***
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Resize Videos"
description: "Change the dimensions of video elements to fit specific layout requirements."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform/resize-b1ce14/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Resize](https://img.ly/docs/cesdk/android/edit-video/transform/resize-b1ce14/)
---
Video resizing in CreativeEditor SDK (CE.SDK) allows you to change the dimensions of video elements to match specific layout requirements. Unlike scaling, resizing allows independent control of width and height dimensions, making it ideal for fitting videos into predefined spaces or responsive layouts.
You can resize videos both through the built-in user interface and programmatically using the SDK's APIs, providing flexibility for different workflow requirements.
[Launch Web Demo](https://img.ly/showcases/cesdk)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
## Resize Methods
CE.SDK supports several approaches to video resizing:
- **Absolute Dimensions**: Set specific pixel dimensions for precise control
- **Percentage-based Resizing**: Size relative to parent container for responsive designs
- **UI Resize Handles**: Interactive resize controls in the editor interface
- **Aspect Ratio Constraints**: Maintain or ignore aspect ratios during resize operations
## Applying Resizing
### UI-Based Resizing
You can resize videos directly in the CE.SDK user interface using resize handles. Users can drag edge and corner handles to adjust dimensions independently or proportionally, making it easy to fit videos into specific layouts visually.
### Programmatic Resizing
Developers can apply resizing programmatically, using the SDK's API. This allows for precise dimension control, automated layout adjustments, and integration with responsive design systems or template constraints.
## Combining with Other Transforms
Video resizing works seamlessly with other transformation operations like rotation, cropping, and positioning. You can chain multiple transformations to create complex layouts while maintaining video quality and performance.
## Guides
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Rotate"
description: "Documentation for Rotate"
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform/rotate-eaf662/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Rotate](https://img.ly/docs/cesdk/android/edit-video/transform/rotate-eaf662/)
---
Video rotation is critical for Android video apps, especially when dealing with content
from mobile cameras that may be recorded in different orientations. The CreativeEditor
SDK provides smooth video rotation with both touch controls and precise Kotlin APIs,
ensuring your users can easily correct orientation and create dynamic video compositions.
## Video rotation features
- Smooth video rotation with real-time preview
- Orientation correction for mobile-recorded content
- Precise angle control through Kotlin programming
- Multi-video rotation for synchronized editing
### Rotating a Video Using the UI
By default, selecting a block will show handles for resizing and rotating. You can freeform rotate a block by dragging the rotation handle.

### Rotating a Video Using Code
You can rotate a video block using the `setRotation` function. It takes the `id` of the block and a rotation amount in radians.
```kotlin
import kotlin.math.PI
engine.block.setRotation(videoBlock, (PI / 4).toFloat())
```
If you need to convert between radians and degrees, multiply the number in degrees by pi and divide by 180.
```kotlin
val angleInRadians: Float = (angleInDegrees * PI / 180).toFloat()
val angleInDegrees: Float = (angleInRadians * 180 / PI).toFloat()
```
You can discover the current rotation of a block using the `getRotation` function.
```kotlin
val rotationOfVideo = engine.block.getRotation(videoBlock)
```

> **Note:** This rotates the entire block. If you want to rotate a video that is filling
> a block but not the block, explore the crop rotate function.
### Locking Rotation
You can remove the rotation handle from the UI by changing the setting for the engine. This will affect *all* blocks.
```kotlin
engine.editor.setSettingBoolean("controlGizmo/showRotateHandles", false)
```
Though the handle is gone, the user can still use the two finger rotation gesture on a touch device. You can disable that gesture with the following setting.
```kotlin
engine.editor.setSettingBoolean("touch/rotateAction", false)
```
When you want to lock only certain blocks, you can toggle the transform lock property. This will apply to all transformations for the block.
```kotlin
engine.block.setTransformLocked(videoBlock, true)
```
### Rotating As a Group
To rotate multiple elements together, first add them to a `group` and then rotate the group.
```kotlin
import kotlin.math.PI
val groupId = engine.block.group(listOf(videoBlock, textBlock))
engine.block.setRotation(groupId, (PI / 2).toFloat())
```
### Troubleshooting
Troubleshooting
| Issue | Solution |
| ----------------------------------- | ------------------------------------------------------------------------------- |
| Video appears offset after rotation | Make sure the pivot point is centered (default is center). |
| Rotation not applying | Confirm that the video block is inserted and rendered before applying rotation. |
| Rotation handle not visible | Check that interactive UI controls are enabled in the settings. |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Scale"
description: "Scale videos uniformly in your Android app."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/transform/scale-f75c8a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Scale](https://img.ly/docs/cesdk/android/edit-video/transform/scale-f75c8a/)
---
This guide shows how to scale videos using CE.SDK in your Android app. You'll
learn how to scale video blocks proportionally, scale groups, and apply
scaling constraints to protect template structure.
## What you'll learn
- Scale videos programmatically using Kotlin
- Scale proportionally or non-uniformly
- Scale grouped elements
- Apply scale constraints in templates
## When to use
Use scaling to:
- Emphasize or de-emphasize elements
- Fit videos to available space without cropping
- Enable pinch-to-zoom gestures or dynamic layouts
***
## Scale a video uniformly
Scaling uses the `scale(block: DesignBlock, scaleX: Float, scaleY: Float, anchorX: Float = 0f, anchorY: Float = 0f)` function. A scale value of `1.0f` is the original scale. Values larger than `1.0f` increase the scale of the block and values lower than `1.0f` scale the block smaller. A value of `2.0f`, for example makes the block twice as large.
The following code scales the video to 150% of its original size. The origin anchor point remains unchanged, so the video expands down and to the right:
```kotlin
engine.block.scale(videoBlock, 1.5f, 1.5f)
```

By default, the anchor point for the video when scaling is the origin point on the top left. The scale function has optional parameters to move the anchor point in the x and y direction. They can have values between `0.0f` and `1.0f`
The following code scales the video to 150% of its original size. The origin anchor point is 0.5, 0.5, so the video expands from the center:
```kotlin
engine.block.scale(videoBlock, 1.5f, 1.5f, 0.5f, 0.5f)
```

***
## Scale non-uniformly
To stretch or compress only one axis, thus distorting a video, use the crop scale function in combination with the width or height function. How you decide to make the adjustment will have different results. Below are three examples of scaling the original video in the x direction only.

```kotlin
import ly.img.engine.SizeMode
engine.block.setWidthMode(videoBlock, SizeMode.AUTO)
val newWidth = engine.block.getWidth(videoBlock) * 1.5f
engine.block.setWidth(videoBlock, newWidth)
```
The preceding code adjusts the width of the block and allows the engine to adjust the scale of the video to maintain it as a fill.

```kotlin
engine.block.setCropScaleX(videoBlock, 1.5f)
engine.block.setWidthMode(videoBlock, SizeMode.AUTO)
val newWidth = engine.block.getWidth(videoBlock) * 1.5f
engine.block.setWidth(videoBlock, newWidth)
```
The preceding code uses crop scale to scale the video in a single direction and then adjusts the block's width to match the change. The change in width does not take the crop into account and so distorts the video as it's scaling the scaled video.

```kotlin
engine.block.setCropScaleX(videoBlock, 1.5f)
engine.block.setWidthMode(videoBlock, SizeMode.AUTO)
val newWidth = engine.block.getWidth(videoBlock) * 1.5f
engine.block.setWidth(videoBlock, newWidth, true) // maintainCrop = true
```
By setting the `maintainCrop` parameter to true, expanding the width of the video by the scale factor respects the crop scale and the video is less distorted.
***
## Scale multiple elements together
Group blocks to scale them proportionally:
```kotlin
val groupId = engine.block.group(listOf(videoBlock, textBlock))
engine.block.scale(groupId, 0.75f, 0.75f)
```
The preceding code scales the entire group to 75%.
***
## Lock scaling
When working with templates, you can lock a block from scaling by setting its scope. Remember that the global layer has to defer to the blocks using `setGlobalScope`.
```kotlin
engine.block.setScopeEnabled(videoBlock, "layer/resize", false)
```
To prevent users from transforming an element at all:
```kotlin
engine.block.setTransformLocked(videoBlock, true)
```
***
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Trim Video and Audio"
description: "Learn how to trim video and audio clips in CE.SDK for Android using the Video Editor starter kit timeline and Engine APIs."
platform: android
url: "https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Trim](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/)
---
```kotlin file=@cesdk_android_examples/engine-guides-trim/Trim.kt reference-only
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import kotlin.math.abs
data class TrimVideoClipsSummary(
val sourceDuration: Double,
val trimOffset: Double,
val trimLength: Double,
val blockDuration: Double,
val audioSourceDuration: Double,
val audioTrimOffset: Double,
val audioTrimLength: Double,
val looping: Boolean,
)
fun trimVideoClips(
license: String?,
userId: String,
): Job = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.trim.example")
try {
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1280, height = 720)
trimVideoClips(engine)
} finally {
engine.stop()
}
}
suspend fun trimVideoClips(engine: Engine): TrimVideoClipsSummary = withContext(Dispatchers.Main) {
val sourceVideoUri =
Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/" +
"pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
)
engine.scene.createFromVideo(sourceVideoUri)
val page = engine.scene.getPages().first()
val videoBlock = engine.block.findByType(DesignBlockType.Graphic).first()
val videoFill = engine.block.getFill(videoBlock)
engine.block.forceLoadAVResource(block = videoFill)
val sourceDuration = engine.block.getAVResourceTotalDuration(block = videoFill)
check(sourceDuration >= 8.0) {
"The sample video must be at least 8 seconds long."
}
check(engine.block.supportsTrim(block = videoFill)) {
"This video fill does not support trim properties."
}
engine.block.setTrimOffset(block = videoFill, offset = 2.0)
engine.block.setTrimLength(block = videoFill, length = 5.0)
val currentTrimOffset = engine.block.getTrimOffset(block = videoFill)
val currentTrimLength = engine.block.getTrimLength(block = videoFill)
check(abs(currentTrimOffset - 2.0) < 0.001)
check(abs(currentTrimLength - 5.0) < 0.001)
check(engine.block.supportsDuration(block = videoBlock))
engine.block.setLooping(block = videoFill, looping = false)
engine.block.setTrimOffset(block = videoFill, offset = 3.0)
engine.block.setTrimLength(block = videoFill, length = 5.0)
engine.block.setDuration(block = videoBlock, duration = 5.0)
val blockDuration = engine.block.getDuration(block = videoBlock)
check(abs(blockDuration - 5.0) < 0.001)
engine.block.setLooping(block = videoFill, looping = true)
engine.block.setTrimOffset(block = videoFill, offset = 5.0)
engine.block.setTrimLength(block = videoFill, length = 3.0)
engine.block.setDuration(block = videoBlock, duration = 9.0)
val looping = engine.block.isLooping(block = videoFill)
check(looping)
// Supply this from your media pipeline; Android does not expose source frame rate through the Engine API.
val knownFrameRate = 30.0
val startFrame = 60
val frameCount = 150
val frameOffset = startFrame / knownFrameRate
val frameLength = frameCount / knownFrameRate
engine.block.setTrimOffset(block = videoFill, offset = frameOffset)
engine.block.setTrimLength(block = videoFill, length = frameLength)
val trimmableFills =
engine.block
.findByType(DesignBlockType.Graphic)
.map(engine.block::getFill)
.filter { fill -> engine.block.supportsTrim(block = fill) }
val loadedFillDurations =
coroutineScope {
trimmableFills
.map { fill ->
async {
engine.block.forceLoadAVResource(block = fill)
fill to engine.block.getAVResourceTotalDuration(block = fill)
}
}.awaitAll()
}
for ((fill, duration) in loadedFillDurations) {
if (duration >= 4.0) {
engine.block.setTrimOffset(block = fill, offset = 1.0)
engine.block.setTrimLength(block = fill, length = 3.0)
}
}
val audioBlock = engine.block.create(DesignBlockType.Audio)
engine.block.appendChild(parent = page, child = audioBlock)
// Android exposes the audio source URI through the "audio/fileURI" property key.
engine.block.setUri(
block = audioBlock,
property = "audio/fileURI",
value =
Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",
),
)
engine.block.forceLoadAVResource(block = audioBlock)
val audioSourceDuration = engine.block.getAVResourceTotalDuration(block = audioBlock)
check(audioSourceDuration >= 9.0) {
"The sample audio must be at least 9 seconds long."
}
check(engine.block.supportsTrim(block = audioBlock))
engine.block.setTrimOffset(block = audioBlock, offset = 1.0)
engine.block.setTrimLength(block = audioBlock, length = 8.0)
engine.block.setTimeOffset(block = audioBlock, offset = 2.0)
engine.block.setDuration(block = audioBlock, duration = 8.0)
engine.block.setVolume(block = audioBlock, volume = 0.7F)
TrimVideoClipsSummary(
sourceDuration = sourceDuration,
trimOffset = currentTrimOffset,
trimLength = currentTrimLength,
blockDuration = blockDuration,
audioSourceDuration = audioSourceDuration,
audioTrimOffset = engine.block.getTrimOffset(block = audioBlock),
audioTrimLength = engine.block.getTrimLength(block = audioBlock),
looping = looping,
)
}
```
Control which part of a video or audio source plays by setting trim offsets
and trim lengths while keeping the original media file unchanged.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-trim)
Trimming works together with block timing. Fill-level trim values choose the
portion of the source media that plays, while block-level timing controls when
that block appears in the video composition and how long it stays active.
The Android [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) exposes trim
handles through its timeline. This guide explains that starter kit surface and
focuses on the Engine APIs for trimming video fills and audio blocks
programmatically.
## Understanding Trim Concepts
### Fill-Level Trimming
Fill-level trimming controls the source media range. Use
`setTrimOffset()` to choose where playback starts inside the media file and
`setTrimLength()` to choose how much media plays from that point.
A trim offset of `2.0` skips the first two seconds of the source. A trim length
of `5.0` then plays five seconds from that offset, so the visible range runs
from second 2 to second 7 of the original media.
### Block-Level Timing
Block-level timing controls placement in the composition timeline.
`setTimeOffset()` moves a block relative to its parent timeline, while
`setDuration()` controls how long the block remains active.
Use trim values for source-media in and out points. Use time offset when you
need to arrange clips or audio blocks in the composition. For non-looping video
fills, changing the trim length can update connected block durations. Treat
`setDuration()` as timeline timing and set fill trim values explicitly when you
change source-media in or out points.
### Common Use Cases
- **Remove unwanted segments** - Skip intro or outro sections without changing the source file.
- **Extract key moments** - Use a short range from longer video or audio.
- **Sync audio and video** - Trim related media independently while keeping their timeline offsets aligned.
- **Create loops** - Trim a short segment and repeat it through the block duration.
## Trimming Video via UI
### Accessing Trim Controls
Use the Android [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) when you want
the built-in timeline UI. Selecting a video clip in that starter kit reveals
trim handles at the clip edges. These handles represent the source range that
plays inside the clip.
### Using Trim Handles
Drag the left handle to move the trim offset forward or backward. Drag the
right handle to shorten or extend the trim length.
The starter kit timeline updates the selected range immediately, so the visible
clip range matches the media section that will play in the composition. For
custom Android editing surfaces, drive equivalent controls with the Engine trim
APIs shown below.
### Preview During Trimming
Scrub or play the timeline after changing trim values. Playback reflects the
current trim offset and trim length, which lets you verify the chosen range
before exporting.
## Programmatic Video Trimming
### Loading Video Resources
Load the audio or video resource before reading duration or trim metadata.
`forceLoadAVResource()` downloads and parses the media resource so the engine can
return reliable duration values.
```kotlin highlight-android-load-resource
engine.block.forceLoadAVResource(block = videoFill)
val sourceDuration = engine.block.getAVResourceTotalDuration(block = videoFill)
```
Use `getAVResourceTotalDuration()` to validate requested trim ranges before
setting them.
### Checking Trim Support
Check trim support before applying trim operations. Video fills and audio blocks
support trimming, but pages, scenes, and many graphic-only blocks do not.
```kotlin highlight-android-check-support
check(engine.block.supportsTrim(block = videoFill)) {
"This video fill does not support trim properties."
}
```
This check keeps custom editing UI from showing trim controls for unsupported
blocks.
### Trimming Video
Apply the trim offset and trim length to the video fill. The values are seconds
in the media timeline and are scaled by playback rate.
```kotlin highlight-android-apply-trim
engine.block.setTrimOffset(block = videoFill, offset = 2.0)
engine.block.setTrimLength(block = videoFill, length = 5.0)
```
This example skips the first two seconds and plays the following five seconds.
### Getting Current Trim Values
Read trim values when you need to populate UI controls, verify a change, or make
relative adjustments.
```kotlin highlight-android-read-trim-values
val currentTrimOffset = engine.block.getTrimOffset(block = videoFill)
val currentTrimLength = engine.block.getTrimLength(block = videoFill)
check(abs(currentTrimOffset - 2.0) < 0.001)
check(abs(currentTrimLength - 5.0) < 0.001)
```
The getters return seconds, using the same unit as the setter APIs.
## Additional Trimming Techniques
### Trimming Audio Blocks
Audio blocks use the same trim APIs as video fills. After loading the audio
resource, set its trim range, timeline placement, and active duration
separately.
```kotlin highlight-android-trim-audio
// Android exposes the audio source URI through the "audio/fileURI" property key.
engine.block.setUri(
block = audioBlock,
property = "audio/fileURI",
value =
Uri.parse(
"https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",
),
)
engine.block.forceLoadAVResource(block = audioBlock)
val audioSourceDuration = engine.block.getAVResourceTotalDuration(block = audioBlock)
check(audioSourceDuration >= 9.0) {
"The sample audio must be at least 9 seconds long."
}
check(engine.block.supportsTrim(block = audioBlock))
engine.block.setTrimOffset(block = audioBlock, offset = 1.0)
engine.block.setTrimLength(block = audioBlock, length = 8.0)
engine.block.setTimeOffset(block = audioBlock, offset = 2.0)
engine.block.setDuration(block = audioBlock, duration = 8.0)
engine.block.setVolume(block = audioBlock, volume = 0.7F)
```
Use `setTimeOffset()` when the audio should start later in the composition.
Match `setDuration()` to the trim length when the selected audio range should
play once, and use `setVolume()` when the trimmed clip needs a different level.
### Trimming with Block Duration
Trim length and block duration work together, but they are not interchangeable.
For non-looping video fills, call `setTrimLength()` on the fill to choose the
source segment; the Engine updates connected block durations from that trim
length. Use `setDuration()` on the block when you need to control how long the
block stays active in the composition, and read back both values when custom
controls expose them side by side.
```kotlin highlight-android-trim-with-duration
check(engine.block.supportsDuration(block = videoBlock))
engine.block.setLooping(block = videoFill, looping = false)
engine.block.setTrimOffset(block = videoFill, offset = 3.0)
engine.block.setTrimLength(block = videoFill, length = 5.0)
engine.block.setDuration(block = videoBlock, duration = 5.0)
val blockDuration = engine.block.getDuration(block = videoBlock)
check(abs(blockDuration - 5.0) < 0.001)
```
In this example the trim length and block duration are both set to five seconds,
so the trimmed segment plays once. To keep a longer block duration than the trim
length, enable looping before setting the longer duration.
### Trimming with Looping
Enable looping when a trimmed segment should repeat until the block duration is
filled.
```kotlin highlight-android-trim-with-looping
engine.block.setLooping(block = videoFill, looping = true)
engine.block.setTrimOffset(block = videoFill, offset = 5.0)
engine.block.setTrimLength(block = videoFill, length = 3.0)
engine.block.setDuration(block = videoBlock, duration = 9.0)
val looping = engine.block.isLooping(block = videoFill)
check(looping)
```
Here the three-second trimmed segment repeats to fill the nine-second block
duration.
### Frame-Accurate Trimming
When your app works from known frame numbers, convert those frame values to
seconds before setting the trim APIs.
```kotlin highlight-android-frame-accurate-trim
// Supply this from your media pipeline; Android does not expose source frame rate through the Engine API.
val knownFrameRate = 30.0
val startFrame = 60
val frameCount = 150
val frameOffset = startFrame / knownFrameRate
val frameLength = frameCount / knownFrameRate
engine.block.setTrimOffset(block = videoFill, offset = frameOffset)
engine.block.setTrimLength(block = videoFill, length = frameLength)
```
The Android trim APIs accept seconds. Keep the frame rate value tied to the
source media you are trimming.
### Batch Processing Multiple Videos
For repeated trim settings, collect trimmable video fills, start resource-load
coroutines for each fill, and apply the same range to every compatible fill
after the durations are known.
```kotlin highlight-android-batch-trim-videos
val trimmableFills =
engine.block
.findByType(DesignBlockType.Graphic)
.map(engine.block::getFill)
.filter { fill -> engine.block.supportsTrim(block = fill) }
val loadedFillDurations =
coroutineScope {
trimmableFills
.map { fill ->
async {
engine.block.forceLoadAVResource(block = fill)
fill to engine.block.getAVResourceTotalDuration(block = fill)
}
}.awaitAll()
}
for ((fill, duration) in loadedFillDurations) {
if (duration >= 4.0) {
engine.block.setTrimOffset(block = fill, offset = 1.0)
engine.block.setTrimLength(block = fill, length = 3.0)
}
}
```
Always load each fill before reading its duration because source media can have
different lengths.
## Troubleshooting
| Issue | Fix |
| --- | --- |
| Trim values have no visible effect | Await `forceLoadAVResource()` before setting or reading trim properties. |
| Trim starts at the wrong point | Use `setTrimOffset()` for the source-media start point and `setTimeOffset()` for timeline placement. |
| Playback continues longer than expected | Check whether looping is enabled, then read back `getDuration()` and `getTrimLength()` after changing trim or duration controls. |
| Audio and video drift out of sync | Apply coordinated trim offsets and timeline offsets to both media blocks. |
## API Reference
| API | Description |
| --- | --- |
| `engine.block.findByType(type=_)` | Finds blocks of a specific Android `DesignBlockType` |
| `engine.block.getFill(block=_)` | Gets the fill block attached to a graphic block |
| `engine.block.setUri(block=_, property="audio/fileURI", value=_)` | Assigns an audio source URI to an audio block |
| `engine.block.getUri(block=_, property="audio/fileURI")` | Reads the audio source URI assigned to an audio block |
| `engine.block.forceLoadAVResource(block=_)` | Loads audio or video metadata before trim and duration access |
| `engine.block.getAVResourceTotalDuration(block=_)` | Returns the source media duration in seconds |
| `engine.block.supportsTrim(block=_)` | Checks whether a block or fill supports trim properties |
| `engine.block.setTrimOffset(block=_, offset=_)` | Sets the source-media playback start in seconds |
| `engine.block.getTrimOffset(block=_)` | Reads the current trim offset in seconds |
| `engine.block.setTrimLength(block=_, length=_)` | Sets how much source media plays from the trim offset |
| `engine.block.getTrimLength(block=_)` | Reads the current trim length in seconds |
| `engine.block.supportsDuration(block=_)` | Checks whether a block supports playback duration |
| `engine.block.setDuration(block=_, duration=_)` | Sets how long the block is active in the composition |
| `engine.block.getDuration(block=_)` | Reads the block duration in seconds |
| `engine.block.supportsTimeOffset(block=_)` | Checks whether a block supports timeline placement |
| `engine.block.setTimeOffset(block=_, offset=_)` | Sets when the block becomes active in its parent timeline |
| `engine.block.getTimeOffset(block=_)` | Reads when the block becomes active in its parent timeline |
| `engine.block.setLooping(block=_, looping=_)` | Enables or disables looping for the media block or fill |
| `engine.block.isLooping(block=_)` | Reads whether looping is enabled |
| `engine.block.supportsPlaybackControl(block=_)` | Checks whether a block supports playback controls such as volume |
| `engine.block.setVolume(block=_, volume=_)` | Sets audio volume from `0.0F` to `1.0F` |
| `engine.block.getVolume(block=_)` | Reads audio volume from `0.0F` to `1.0F` |
## Next Steps
- [Split Video and Audio](https://img.ly/docs/cesdk/android/edit-video/split-464167/) - Learn how to split video and audio clips at specific time points in CE.SDK, creating two independent segments from a single clip.
- [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Master playback controls, volume, and muting.
- [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Understand the complete timeline editing model.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Engine Interface"
description: "Initialize and manage the Android Engine lifecycle for headless and UI-based integrations."
platform: android
url: "https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Engine](https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/)
---
```kotlin file=@cesdk_android_examples/engine-guides-setup/MyApplication.kt reference-only
import android.app.Application
import ly.img.engine.Engine
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Engine.init(application = this)
}
}
```
```kotlin file=@cesdk_android_examples/engine-guides-setup/MyActivity.kt reference-only
import android.net.Uri
import android.os.Bundle
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.TextureView
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ly.img.engine.Engine
class MyActivity : ComponentActivity() {
private val engine = Engine.getInstance(id = "ly.img.engine.example")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textureView = TextureView(this)
setContentView(textureView)
lifecycleScope.launch {
engine.start(
license = null, // pass null or empty for evaluation mode with watermark
userId = "",
savedStateRegistryOwner = this@MyActivity,
)
bindTextureView(textureView)
loadScene()
}
}
override fun onDestroy() {
engine.stop()
super.onDestroy()
}
private fun bindTextureView(textureView: TextureView) {
engine.bindTextureView(textureView)
}
private suspend fun loadScene() {
// Check whether a scene already exists before loading it again as it might have been restored in engine.start.
engine.scene.get() ?: run {
val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")
engine.scene.load(sceneUri)
}
}
private fun bindSurfaceView() {
val surfaceView = SurfaceView(this)
setContentView(surfaceView)
engine.bindSurfaceView(surfaceView)
}
private fun bindSurfaceHolder(surfaceHolder: SurfaceHolder) {
engine.bindSurfaceHolder(surfaceHolder)
}
private fun bindOffscreen() {
engine.bindOffscreen(width = 100, height = 100)
}
}
```
```kotlin file=@cesdk_android_examples/engine-guides-setup/MyComposable.kt reference-only
import android.net.Uri
import android.view.SurfaceView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.compose.ui.viewinterop.AndroidView
import ly.img.engine.Engine
@Composable
fun MyComposable() {
val engine = remember { Engine.getInstance(id = "ly.img.engine.example") }
val context = LocalContext.current
val surfaceView = remember { SurfaceView(context) }
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
AndroidView(factory = { surfaceView })
LaunchedEffect(Unit) {
engine.start(
license = null, // pass null or empty for evaluation mode with watermark
userId = "",
savedStateRegistryOwner = savedStateRegistryOwner,
)
engine.bindSurfaceView(surfaceView)
engine.scene.get() ?: run {
val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")
engine.scene.load(sceneUri)
}
}
DisposableEffect(Unit) {
onDispose {
engine.stop()
}
}
}
```
The Android Engine is the programmatic entry point behind CE.SDK. You can use it through the prebuilt editor UI, where the editor setup manages initialization, or create and bind an Engine yourself for headless rendering, custom UI, and automation.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-setup)
## Choose the Integration Mode
Most Android integrations fall into one of two modes:
| Mode | Who initializes the Engine? | When to use it |
| --- | --- | --- |
| Prebuilt editor UI | The editor setup initializes the Engine for you. | You use the CE.SDK editor UI or a Starter Kit and access the Engine from editor callbacks or configuration hooks. |
| Headless or custom Engine | Your app initializes and starts the Engine. | You render offscreen, generate assets, validate scenes, build custom UI, or run automation without showing the editor. |
The lifecycle below applies only when you create and start an Engine yourself. If you use the prebuilt editor or a Starter Kit, access the Engine through the editor's callbacks instead because it handles initialization for you.
## Initialize the Engine
Before any Engine instance can start, call `Engine.init(application)` from your `Application.onCreate()` method.
```kotlin highlight-android-application-init
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Engine.init(application = this)
}
}
```
This call stores the application context and prepares the Engine's internal directories. It must run on the main thread and should happen once during app startup.
## Create and Start an Engine
Create an Engine instance with a stable ID, then start it on the main thread with your license and user ID. The sample uses an Activity lifecycle coroutine, but any main-thread coroutine works. Render binding and scene work should run after `engine.start(...)` completes.
```kotlin highlight-android-activity-engine
private val engine = Engine.getInstance(id = "ly.img.engine.example")
```
```kotlin highlight-android-activity-start
lifecycleScope.launch {
engine.start(
license = null, // pass null or empty for evaluation mode with watermark
userId = "",
savedStateRegistryOwner = this@MyActivity,
)
bindTextureView(textureView)
loadScene()
}
```
The `id` controls Engine instance reuse: calling `Engine.getInstance(id=_)` with the same ID returns the same instance. `engine.start(...)` returns `false` if that instance is already running, so a new `savedStateRegistryOwner` is registered only when the Engine starts again. To avoid loading the same scene repeatedly after state restoration, check `engine.scene.get()` after `engine.start(...)` and only load a scene when none was restored.
## Bind a Render Target
After `engine.start(...)` completes, bind the Engine to the render target that matches your integration:
```kotlin highlight-android-activity-bind-texture-view
engine.bindTextureView(textureView)
```
```kotlin highlight-android-activity-bind-surface-view
val surfaceView = SurfaceView(this)
setContentView(surfaceView)
engine.bindSurfaceView(surfaceView)
```
```kotlin highlight-android-activity-bind-surface-holder
engine.bindSurfaceHolder(surfaceHolder)
```
```kotlin highlight-android-activity-bind-offscreen
engine.bindOffscreen(width = 100, height = 100)
```
Use a visible `TextureView` or `SurfaceView` for a custom Android UI. Use `bindSurfaceHolder(surfaceHolder=_)` when your UI already owns a raw `SurfaceHolder`, such as a custom player view or third-party wrapper. Use `bindOffscreen(width=_, height=_)` for headless rendering where no view is displayed.
Only one Engine can be bound to a render target at a time. Binding one Engine unbinds other running UI Engines, so keep background and foreground rendering flows explicit.
## Work With the Scene
After the Engine is started and bound, load or create a scene before calling APIs that need scene content.
When you pass a `SavedStateRegistryOwner` to `engine.start(...)`, the Engine can restore a saved scene after process recreation. Check whether a scene already exists before loading a new one.
```kotlin highlight-android-activity-load-scene
// Check whether a scene already exists before loading it again as it might have been restored in engine.start.
engine.scene.get() ?: run {
val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")
engine.scene.load(sceneUri)
}
```
## Clean Up the Engine
Call `engine.stop()` when the standalone host that owns the `savedStateRegistryOwner` is destroyed. In this sample, that includes Activity recreation so the next Activity can start the named Engine and register its new saved-state owner. App architectures that keep one Engine running across configuration changes should manage restoration outside this snippet instead of passing a new owner to an already-running instance.
```kotlin highlight-android-activity-cleanup
override fun onDestroy() {
engine.stop()
super.onDestroy()
}
```
Use `engine.stop()` for lifecycle cleanup: it unbinds the current render target, releases runtime resources, unregisters saved-state handling, and clears the current scene. Use `engine.unbind()` only when the Engine should keep running while you detach it from one render target and bind it to another.
## Compose Lifecycle
Compose integrations follow the same lifecycle: remember the Engine instance, start it in a `LaunchedEffect`, bind a `SurfaceView`, work with the scene after the start call completes, and stop it from `DisposableEffect.onDispose` when this Composable owns the Engine lifecycle.
```kotlin highlight-android-compose-engine
val engine = remember { Engine.getInstance(id = "ly.img.engine.example") }
```
Set up the `SurfaceView` and saved-state owner before the start coroutine uses them.
```kotlin highlight-android-compose-render-target
val context = LocalContext.current
val surfaceView = remember { SurfaceView(context) }
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
AndroidView(factory = { surfaceView })
```
```kotlin highlight-android-compose-start
LaunchedEffect(Unit) {
engine.start(
license = null, // pass null or empty for evaluation mode with watermark
userId = "",
savedStateRegistryOwner = savedStateRegistryOwner,
)
engine.bindSurfaceView(surfaceView)
engine.scene.get() ?: run {
val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")
engine.scene.load(sceneUri)
}
}
```
```kotlin highlight-android-compose-cleanup
DisposableEffect(Unit) {
onDispose {
engine.stop()
}
}
```
Use this pattern when you build a custom UI around the Engine. If you use the prebuilt editor UI, prefer the editor or Starter Kit lifecycle hooks instead of creating a separate Engine in the Composable.
## Troubleshooting
**Cannot start the Engine before calling Engine.init(applicationContext)**: Call `Engine.init(application)` once from `Application.onCreate()` before calling `Engine.getInstance(...)` or `engine.start(...)`.
**Engine APIs fail from the wrong thread**: Call Engine APIs from the Engine's own dispatcher. Public Engine instances created with `Engine.getInstance(...)` run on the main thread. Export callbacks such as `onPreExport` run on a separate background Engine thread, so configure that Engine inside the provided callback.
**No frames render in a custom render loop**: Ensure the Engine is bound before rendering frames. Headless frame rendering needs `engine.bindOffscreen(width=_, height=_)`; custom UI flows need a visible `SurfaceView` or `TextureView`.
**The export output is empty**: Ensure the scene contains renderable blocks, required resources are reachable, and export options target the block you expect. `engine.block.export(...)` prepares its own background export Engine, so `bindOffscreen(...)` on the main Engine is not the fix for empty export output.
**A scene loads twice after rotation or process recreation**: If you pass a `SavedStateRegistryOwner` to `engine.start(...)`, check `engine.scene.get()` before loading a new scene.
## API Reference
| Method | Purpose |
| --- | --- |
| `Engine.init(application=_)` | Initialize the Engine before any instance starts. |
| `Engine.getInstance(id=_, audioContext=_)` | Get or create a named Engine instance. Pass `AudioContext.NONE` to disable audio playback for the first instance with that ID, or use `AudioContext.AUTO` for the default audio-capable context. |
| `engine.start(license=_, userId=_, savedStateRegistryOwner=_)` | Start the Engine and optionally wire scene state restoration. |
| `engine.bindTextureView(textureView=_)` | Render into a `TextureView`. |
| `engine.bindSurfaceView(surfaceView=_)` | Render into a `SurfaceView`. |
| `engine.bindSurfaceHolder(surfaceHolder=_)` | Render into a raw `SurfaceHolder`. |
| `engine.bindOffscreen(width=_, height=_)` | Render without a visible UI surface. |
| `engine.unbind()` | Detach the current render target. |
| `engine.isEngineRunning()` | Check whether `start()` has completed and `stop()` has not been called. |
| `engine.stop()` | Stop the Engine and release its runtime resources. |
## Next Steps
- [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Use the Engine directly without the prebuilt UI.
- [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) - Process multiple designs.
- [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) - Personalize templates with external data.
- [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Explore export options and formats.
- [Node.js SDK](https://img.ly/docs/cesdk/android/what-is-cesdk-2e7acd/) - Use the server-side Engine package for backend processing.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Thumbnail"
description: "Generate thumbnail preview images from CE.SDK scenes and pages with Android export options."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Create Thumbnail](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-thumbnail/CreateThumbnail.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlock
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
data class ThumbnailSize(
val label: String,
val width: Float,
val height: Float,
)
data class ThumbnailExportResult(
val smallJpeg: ByteBuffer,
val mediumJpeg: ByteBuffer,
val pngPreview: ByteBuffer,
val savedMediumJpeg: File,
val savedPngPreview: File,
)
suspend fun createThumbnail(
engine: Engine,
outputDir: File,
): ThumbnailExportResult {
val page = engine.scene.getCurrentPage()
?: engine.scene.getPages().firstOrNull()
?: error("Load a scene with at least one page before exporting thumbnails.")
val smallSize = ThumbnailSize(label = "small", width = 150F, height = 150F)
val mediumSize = ThumbnailSize(label = "medium", width = 400F, height = 300F)
val smallJpeg = exportJpegThumbnail(engine, page, smallSize)
val mediumJpeg = exportJpegThumbnail(engine, page, mediumSize)
val pngPreview = exportPngThumbnail(engine, page)
val savedMediumJpeg = saveThumbnailToFile(
buffer = mediumJpeg.asReadOnlyBuffer(),
outputFile = File(outputDir, "thumbnail-medium.jpg"),
)
val savedPngPreview = saveThumbnailToFile(
buffer = pngPreview.asReadOnlyBuffer(),
outputFile = File(outputDir, "thumbnail-preview.png"),
)
return ThumbnailExportResult(
smallJpeg = smallJpeg,
mediumJpeg = mediumJpeg,
pngPreview = pngPreview,
savedMediumJpeg = savedMediumJpeg,
savedPngPreview = savedPngPreview,
)
}
suspend fun exportJpegThumbnail(
engine: Engine,
page: DesignBlock,
size: ThumbnailSize,
): ByteBuffer {
val options = ExportOptions(
targetWidth = size.width,
targetHeight = size.height,
jpegQuality = 0.8F,
)
val thumbnail = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(thumbnail.hasRemaining()) { "${size.label} thumbnail is empty" }
return thumbnail
}
suspend fun exportPngThumbnail(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(
targetWidth = 400F,
targetHeight = 300F,
pngCompressionLevel = 6,
)
val thumbnail = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(thumbnail.hasRemaining()) { "PNG thumbnail is empty" }
return thumbnail
}
suspend fun exportThumbnailSet(
engine: Engine,
page: DesignBlock,
): Map {
val sizes = listOf(
ThumbnailSize(label = "small", width = 150F, height = 150F),
ThumbnailSize(label = "medium", width = 400F, height = 300F),
ThumbnailSize(label = "large", width = 800F, height = 600F),
)
return sizes.associate { size ->
size.label to exportJpegThumbnail(engine, page, size)
}
}
suspend fun saveThumbnailToFile(
buffer: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableBuffer = buffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
check(outputFile.length() > 0L) { "Saved thumbnail is empty" }
outputFile
}
```
Generate small preview images from CE.SDK scenes and pages for galleries, file browsers, and design management interfaces.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-thumbnail)
Thumbnails use the same block export API as full-size image exports. Pass a page or scene block to `engine.block.export(...)`, choose an image MIME type, and set target dimensions in `ExportOptions`.
This guide focuses on static image thumbnails. It does not cover audio waveforms or multi-frame scrubber previews.
## Export a Thumbnail
Start from the current page when you want the thumbnail to match the visible canvas. If no page is selected, use the first page in the loaded scene.
```kotlin highlight-android-select-page
val page = engine.scene.getCurrentPage()
?: engine.scene.getPages().firstOrNull()
?: error("Load a scene with at least one page before exporting thumbnails.")
```
Then call `engine.block.export(...)` with both `targetWidth` and `targetHeight`. The Android API returns a `ByteBuffer` that you can decode, cache, write to disk, or upload through your own storage layer.
```kotlin highlight-android-export-thumbnail
suspend fun exportJpegThumbnail(
engine: Engine,
page: DesignBlock,
size: ThumbnailSize,
): ByteBuffer {
val options = ExportOptions(
targetWidth = size.width,
targetHeight = size.height,
jpegQuality = 0.8F,
)
val thumbnail = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(thumbnail.hasRemaining()) { "${size.label} thumbnail is empty" }
return thumbnail
}
```
Both target dimensions should be set together. CE.SDK renders the block large enough to fill the target box while maintaining its aspect ratio.
## Choose Thumbnail Format
Use the MIME type that fits the UI surface where you display the thumbnail:
- **JPEG** - Smaller files for photographic content, controlled with `jpegQuality`.
- **PNG** - Lossless output with transparency support, controlled with `pngCompressionLevel`.
### JPEG Thumbnails
JPEG works well for most gallery and list previews. Values around `0.8F` usually balance image quality and file size for thumbnails.
### PNG Thumbnails
PNG preserves transparency and lossless quality. Increase `pngCompressionLevel` when smaller files matter more than encoding speed.
```kotlin highlight-android-export-png
suspend fun exportPngThumbnail(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(
targetWidth = 400F,
targetHeight = 300F,
pngCompressionLevel = 6,
)
val thumbnail = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
check(thumbnail.hasRemaining()) { "PNG thumbnail is empty" }
return thumbnail
}
```
## Common Thumbnail Sizes
Use target boxes that match the destination UI instead of exporting full-resolution artwork and scaling it later. `targetWidth` and `targetHeight` define the box CE.SDK fills, not guaranteed exact output dimensions. When the source aspect ratio differs, the thumbnail may exceed one axis while preserving the source aspect ratio.
| Size | Target box | Use case |
| ---- | ---------- | -------- |
| Small | 150 x 150 | Dense grids and file browsers |
| Medium | 400 x 300 | Preview cards and detail lists |
| Large | 800 x 600 | Larger preview panels |
## Generate Multiple Sizes
Create responsive thumbnail sets by exporting the same page with different dimensions. This keeps each asset close to the size your UI needs.
```kotlin highlight-android-export-multiple
suspend fun exportThumbnailSet(
engine: Engine,
page: DesignBlock,
): Map {
val sizes = listOf(
ThumbnailSize(label = "small", width = 150F, height = 150F),
ThumbnailSize(label = "medium", width = 400F, height = 300F),
ThumbnailSize(label = "large", width = 800F, height = 600F),
)
return sizes.associate { size ->
size.label to exportJpegThumbnail(engine, page, size)
}
}
```
Batching several sizes in one helper also makes it easier to cache or upload them together in your app code.
## Save a Thumbnail
Exporting creates in-memory image data. To persist the thumbnail on Android, write the returned `ByteBuffer` to a file in your app's cache or files directory.
```kotlin highlight-android-save-file
suspend fun saveThumbnailToFile(
buffer: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableBuffer = buffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
check(outputFile.length() > 0L) { "Saved thumbnail is empty" }
outputFile
}
```
The example writes to a caller-provided file so your app controls storage lifetime, cleanup, and sharing behavior.
## Optimize Thumbnail Quality
Tune format-specific `ExportOptions` fields to balance file size, quality, and export time.
| Format | Option | Range | Default | Notes |
| ------ | ------ | ----- | ------- | ----- |
| JPEG | `jpegQuality` | `(0, 1]` | `0.9F` | Lower values reduce file size and may add visible artifacts. |
| PNG | `pngCompressionLevel` | `0-9` | `5` | Higher values reduce file size but can encode more slowly. |
| Target size | `targetWidth` / `targetHeight` | Positive values | `null` | Set both values to scale thumbnail output. |
For thumbnails, start with JPEG quality around `0.8F` or PNG compression around `6`, then adjust based on your UI and storage constraints.
## Headless and Background Thumbnail Generation
For occasional thumbnails, export from the existing `Engine` instance after the scene has loaded. This works well when a user saves a draft or opens a design detail screen.
For large batches, use a separate headless engine instance, load each saved scene, and call the same export helpers. Keep engine calls on the main thread and move only file I/O or uploads to background dispatchers.
## Thumbnails from Video Blocks
Exporting a paused video page produces a single static image of the current visual state. This can be useful for poster-frame previews, but it is not the same as generating a sequence of scrubber frames.
To control the captured frame, seek the video or page timeline to the desired time, pause playback, and export the page with the thumbnail options shown above.
## Troubleshooting
| Symptom | Likely cause | Solution |
| ------- | ------------ | -------- |
| Thumbnail only shows part of the design | A child block was exported instead of the page | Export the page block to capture the full visible canvas. |
| Thumbnail size looks wrong | One target dimension is missing or zero | Set both `targetWidth` and `targetHeight` to positive values. |
| Export is slow | Target dimensions or PNG compression are too high | Reduce dimensions or use a lower compression level. |
| File size is too large | Quality settings or dimensions are too high | Lower JPEG quality or reduce the target size. |
| Export fails | The scene has not loaded or no page is selected | Load a scene and get the current page before exporting. |
## API Reference
| Method | Description |
| ------ | ----------- |
| `engine.block.export(block=_, mimeType=_, options=_)` | Export a page, scene, group, or block as image data. |
| `engine.scene.getCurrentPage()` | Get the current page block to export as a thumbnail. |
| `engine.scene.getPages()` | Get available page blocks when no current page is selected. |
| `ExportOptions(targetWidth=_, targetHeight=_)` | Scale the exported output to thumbnail dimensions. |
| `ExportOptions(jpegQuality=_)` | Tune JPEG output quality and file size. |
| `ExportOptions(pngCompressionLevel=_)` | Tune PNG compression speed and file size. |
## Next Steps
- To learn more about exporting images and controlling output quality, see [Export designs to image formats](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/).
- Reduce file size or tune quality for thumbnails and previews with [Compress exported images](https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/).
- If you need to generate thumbnails at scale or as part of automated workflows, take a look at [Batch processing designs](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/).
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Export"
description: "Explore export options, supported formats, and configuration features for sharing or rendering output."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/)
---
---
## Related Pages
- [Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Explore export options, supported formats, and configuration features for sharing or rendering output.
- [Export for Social Media](https://img.ly/docs/cesdk/android/export-save-publish/for-social-media-0e8a92/) - Export vertical videos with the correct dimensions, format, and quality settings for Instagram Reels, TikTok, and YouTube Shorts.
- [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/) - Export video compositions as MP4 files with H.264 encoding, progress events, and configurable bitrate and resolution.
- [For Audio Processing](https://img.ly/docs/cesdk/android/guides/export-save-publish/export/audio-68de25/) - Learn how to export audio in WAV or MP4 format from any block type in CE.SDK for Android.
- [To PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - Export designs as PDF documents with high compatibility mode and underlayer support for special media printing.
- [To JPEG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-jpeg-6f88e9/) - Export CE.SDK designs to JPEG format with configurable quality settings for photographs, web images, and social media content.
- [To PNG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/) - Export CE.SDK designs as PNG images with lossless compression, alpha support, and configurable output dimensions.
- [Export to Raw Data](https://img.ly/docs/cesdk/android/export-save-publish/export/to-raw-data-abd7da/) - Export CE.SDK designs to uncompressed RGBA pixel data for custom image processing, GPU uploads, and advanced graphics workflows on Android.
- [Compress Exports for Smaller Files](https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/) - Learn how to reduce file sizes during export from CE.SDK for Android by tuning format-specific compression settings in Kotlin.
- [Export with a Color Mask](https://img.ly/docs/cesdk/android/export-save-publish/export/with-color-mask-4f868f/) - Export CE.SDK design blocks on Android with a color mask to isolate exact opaque color matches in a separate mask image.
- [Pre-Export Validation](https://img.ly/docs/cesdk/android/export-save-publish/pre-export-validation-3a2cba/) - Validate designs before export by detecting layout, visibility, and placeholder issues.
- [Partial Export](https://img.ly/docs/cesdk/android/export-save-publish/export/partial-export-89aaf6/) - Export individual blocks, grouped elements, or specific pages from a CE.SDK scene in Android instead of exporting the whole scene.
- [Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/) - Configure and understand CE.SDK's image and video size limits in Android to balance quality and performance across devices.
- [Create Thumbnail](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/) - Generate thumbnail preview images from CE.SDK scenes and pages with Android export options.
- [Export for Printing](https://img.ly/docs/cesdk/android/export-save-publish/for-printing-bca896/) - Export designs from CE.SDK as print-ready PDFs with professional output options including high compatibility mode, underlayers for special media, and scene DPI configuration.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Compress Exports for Smaller Files"
description: "Learn how to reduce file sizes during export from CE.SDK for Android by tuning format-specific compression settings in Kotlin."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Compress](https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/)
---
Compression's goal is to reduce file sizes during export while maintaining as much visual quality as possible. With the CreativeEditor SDK (CE.SDK) for Android, you can fine-tune compression settings for both images and videos in Kotlin. This allows your app to balance performance, quality, and storage efficiency across all Android devices.
## What You'll Learn
- How to configure compression for PNG and JPEG image exports in Kotlin.
- How to control video file size using bitrate and resolution scaling.
- How to balance file size, quality, and export performance for different use cases.
- How to configure compression programmatically during automation or batch operations.
## When to Use It
Compression tuning is useful whenever:
- Exported media is too large for upload limits
- You need to optimize storage quotas
- You have constrained network bandwidth
Use it when preparing images or videos for any workflow that benefits from:
- Faster load times and smaller files, like:
- Social media
- Web delivery
- Consistent file size and predictable performance, like:
- Batch export
- Automation scenarios
## Understanding Compression Options by Format
Each format supports its own parameters for balancing:
- Speed
- File size
- Quality
You pass these through the `ExportOptions` structure when calling the export functions.
| Format | Parameter | Type | Effect | Default |
| ------- | ---------- | ---- | ------- | -------- |
| PNG | `pngCompressionLevel` | 0–9 | Higher = smaller, slower (lossless) | 5 |
| JPEG | `jpegQuality` | 0.0–1.0 | Lower = smaller, lower quality | 0.9 |
| MP4 | Video bitrate via options | bits/sec | Higher = larger, higher quality | Auto |
## Export Images with Compression
Below is an example that exports a design block as PNG and JPEG while tuning compression options.
```kotlin
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
import java.io.File
fun exportCompressedImages(
context: Context,
license: String,
userId: String
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1080, height = 1920)
// Load a demo scene
val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")
val scene = engine.scene.load(sceneUri = sceneUri)
// Select the first graphic block to export
val blocks = engine.block.findByType(DesignBlockType.Graphic)
if (blocks.isNotEmpty()) {
val block = blocks.first()
// Export PNG with maximum compression (lossless)
val pngOptions = ExportOptions(pngCompressionLevel = 9)
val pngData = engine.block.export(block, mimeType = MimeType.PNG, options = pngOptions)
// Save PNG to file
val pngFile = File(context.filesDir, "compressed.png")
withContext(Dispatchers.IO) {
pngFile.outputStream().channel.use { channel ->
channel.write(pngData)
}
}
// Export JPEG with balanced quality (lossy)
val jpegOptions = ExportOptions(jpegQuality = 0.7f)
val jpegData = engine.block.export(block, mimeType = MimeType.JPEG, options = jpegOptions)
// Save JPEG to file
val jpegFile = File(context.filesDir, "compressed.jpg")
withContext(Dispatchers.IO) {
jpegFile.outputStream().channel.use { channel ->
channel.write(jpegData)
}
}
}
engine.stop()
}
```
Choose a format depending on what matters the most for your output:
- **PNG** is ideal for flat graphics or assets that require **transparency**.
- **JPEG** is best for photographs where slight **compression** artifacts are acceptable.
- Use PNG when you need lossless output or transparency, and JPEG when compact photographic output is more important.
## Combine Compression with Resolution Scaling
You can further reduce file size by downscaling exports:
```kotlin
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
val scaledOptions = ExportOptions(
pngCompressionLevel = 7,
targetWidth = 1080f,
targetHeight = 1080f
)
val scaledData = engine.block.export(block, mimeType = MimeType.PNG, options = scaledOptions)
```
When you specify only one dimension, CE.SDK automatically preserves aspect ratio for consistent results.
## Compress Video Exports
For video exports, you can control compression through various export parameters. The Android SDK uses callback-based video export with progress tracking.
```kotlin
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.MimeType
import java.io.File
fun exportCompressedVideo(
context: Context,
license: String,
userId: String
) = CoroutineScope(Dispatchers.Main).launch {
val engine = Engine.getInstance(id = "ly.img.engine.example")
engine.start(license = license, userId = userId)
engine.bindOffscreen(width = 1280, height = 720)
// Load or create a video scene
val scene = engine.scene.createForVideo()
// ... add video content ...
// Export page as compressed MP4
val page = engine.block.findByType(DesignBlockType.Page).firstOrNull()
if (page != null) {
val videoData = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = { progress ->
println("Rendered ${progress.renderedFrames}/${progress.totalFrames} frames")
println("Encoded ${progress.encodedFrames}/${progress.totalFrames} frames")
}
)
// Save video to file
val videoFile = File(context.filesDir, "compressed_video.mp4")
withContext(Dispatchers.IO) {
videoFile.outputStream().channel.use { channel ->
channel.write(videoData)
}
}
}
engine.stop()
}
```
About the video compression:
- Video bitrate and encoding settings are handled automatically by the engine
- The SDK optimizes compression based on the content and target resolution
- You can control output quality through resolution scaling using `targetWidth` and `targetHeight` in export options
## Performance and Trade-Offs
Higher compression results in smaller files but slower export speeds. For example:
- PNG Level 9 may take twice as long to encode as Level 3–5, though it produces smaller files.
- JPEG exports are faster but can introduce visible compression artifacts.
Video exports are more demanding and depend heavily on device CPU and GPU performance.
You can check available export limits before encoding:
```kotlin
import ly.img.engine.Engine
val maxSize = engine.editor.getMaxExportSize()
println("Max export size: $maxSize pixels")
```
## Real-World Compression Comparison (1080 × 1080)
The following table compares average results across different compression settings for photo-like and graphic-like images.
| Format | Setting | Avg. File Size (KB) | Encode Time (ms) | PSNR (dB)\* | Notes |
| ------- | -------- | ------------------- | ---------------- | ------------ | ------ |
| **PNG** | Level 0 | ~1 450 | ~44 | ∞ (lossless) | Fastest, largest |
| | Level 5 | ~1 260 | ~61 | ∞ | Balanced speed and size |
| | Level 9 | ~1 080 | ~88 | ∞ | Smallest, slowest |
| **JPEG** | Quality 95 | ~640 | ~24 | 43 | Near-lossless appearance |
| | Quality 80 | ~420 | ~20 | 39 | Good default for photos |
| | Quality 60 | ~290 | ~17 | 35 | Some artifacts visible |
| | Quality 40 | ~190 | ~15 | 31 | Heavy compression |
\*PSNR > 40 dB ≈ visually lossless; 30–35 dB shows mild artifacts.
**Key Takeaways**:
- **JPEG** performs well for photographs; use `jpegQuality = 0.8f–0.9f` for web or print, `0.6f` for compact exports.
- **PNG** is essential for transparency and vector-like shapes; higher levels reduce size modestly at the cost of speed.
- Test on realistic assets: complex photos and flat graphics compress differently.
## Practical Presets
These presets provide starting points for common export scenarios.
| Use Case | Format | Typical Settings | Result | Notes |
|-----------|---------|------------------|---------|-------|
| **Web or Social Sharing** | JPEG | `jpegQuality: 0.8f` | ~60–70 % smaller than PNG | Balanced quality and size |
| **UI Graphics / Transparent Assets** | PNG | `pngCompressionLevel: 6–8` | ~25 % smaller than default PNG | Maintains transparency |
| **High-Quality Print or Archival** | PNG | `pngCompressionLevel: 9` | Maximum fidelity | Slower export, large files |
| **Video for Web / Social** | MP4 | Use default settings with resolution scaling | Smooth playback, small file | Adjust resolution as needed |
| **Video for Download / HD** | MP4 | Higher resolution (1920×1080) | Full HD quality | Larger file, slower encode |
**PDF and Print**: PDF exports use vector graphics when possible and aren't compressed by default.
> **Note:** Consider showing users an **estimated file size** before export. It helps them make informed choices about quality vs. performance.
## Automating Compression in Batch Exports
When exporting multiple elements, apply the same compression settings programmatically:
```kotlin
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.MimeType
import java.io.File
suspend fun exportAllGraphics(engine: Engine, context: Context) {
val blocks = engine.block.findByType(DesignBlockType.Graphic)
val options = ExportOptions(jpegQuality = 0.8f)
blocks.forEachIndexed { index, block ->
val data = engine.block.export(block, mimeType = MimeType.JPEG, options = options)
val file = File(context.filesDir, "export_$index.jpg")
withContext(Dispatchers.IO) {
file.outputStream().channel.use { channel ->
channel.write(data)
}
}
}
}
```
This ensures consistent quality and file size across all exported assets.
## Troubleshooting
**❌ File size not reduced**:
- Ensure correct property name such as `jpegQuality` or `pngCompressionLevel`.
- Check that values are floats (e.g., `0.8f` not `0.8`).
**❌ JPEG Quality too low**:
- Increase quality to `0.9f` or use PNG for lossless output.
**❌ Export slow**:
- Check for excessive compression level.
- Lower PNG level to 5–6.
- Use `withContext(Dispatchers.IO)` for file operations.
**❌ Video export issues**:
- Ensure video scene is properly configured.
- Check available memory with `engine.editor.getMaxExportSize()`.
- Monitor progress callback for encoding status.
**❌ Out of memory errors**:
- Reduce target resolution with `targetWidth` and `targetHeight`.
- Export smaller blocks instead of full scene.
- Call `engine.stop()` and restart for fresh memory state.
## Next Steps
Compression is one of the most practical tools for optimizing export workflows. By adjusting the `ExportOptions` structure in Kotlin, you can deliver high-quality results efficiently—whether your users are exporting social media posts, UI assets, or professional-grade print layouts.
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) to learn about all available export formats.
- Apply compression consistently in automated exports using [batch processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/).
- Combine scaling and compression for [thumbnails](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/).
- Learn about [export formats](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and their capabilities.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Options"
description: "Explore export options, supported formats, and configuration features for sharing or rendering output."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/)
---
Choose the right export path for images, documents, raw data, and video.
CE.SDK exports on the device and lets Android integrations tune file size,
quality, dimensions, transparency, and compatibility in the dedicated export
guides.
This overview is a map of the export category. Use the format-specific guides for implementation code and option details.
## What Export Options Control
Export options determine the output format and the trade-offs that matter for your app:
- **Format**: Pick the output type that matches the destination, such as an image preview, a print-ready PDF, raw pixel data, or an MP4 video.
- **Quality and compression**: Balance visual fidelity, encoding time, and file size.
- **Dimensions**: Set a target box that the export fills while preserving the exported block's aspect ratio.
- **Transparency**: Use a format that preserves alpha when the design depends on transparent pixels.
- **Compatibility**: Prefer compatibility-oriented settings for files that must open reliably across viewers and devices.
## Android Export Scope
Current Android bindings expose block export APIs for static files, color masks, and MP4 video. They do not expose an audio export API or audio MIME type enum, so audio export is not presented as an Android-supported export area here.
## Supported Android Formats
Use these Android `MimeType` values when choosing an export format:
| Format | Android `MimeType` | MIME String | Practical Use |
| ------ | ------------------ | ----------- | ------------- |
| PNG | `MimeType.PNG` | `image/png` | Lossless image output with transparency for previews, UI graphics, and designs with sharp edges. |
| JPEG | `MimeType.JPEG` | `image/jpeg` | Compact lossy image output for photos, sharing flows, and backgrounds that do not need alpha. |
| TGA | `MimeType.TGA` | `image/x-tga` | Image pipelines that specifically require TGA files. |
| SVG | `MimeType.SVG` | `image/svg+xml` | Scalable graphics and post-processing workflows, with unsupported visual effects rasterized as needed. |
| PDF | `MimeType.PDF` | `application/pdf` | Document and print workflows, including compatibility-oriented export settings. |
| MP4 | `MimeType.MP4` | `video/mp4` | Timeline-based page export for video sharing or playback. |
| Binary | `MimeType.BINARY` | `application/octet-stream` | Raw RGBA8888 data for custom image processing or graphics pipelines. |
## Programmatic Export Basics
Android exports run locally on the device. Use `engine.block.export(...)` for static image, document, SVG, and raw-data output, passing the block to export, a supported `MimeType`, and optional `ExportOptions`. The API returns a `ByteBuffer` that your app can write to app storage, pass to a share flow, or upload to your backend.
Use `ExportOptions` to configure the options that apply to the selected format:
- `pngCompressionLevel` controls PNG file size and encoding speed without changing image quality.
- `jpegQuality` controls JPEG visual quality and file size.
- `targetWidth` and `targetHeight` set a target box that the export fills while preserving the block's aspect ratio; if the ratios differ, one output dimension can exceed the requested target.
- PDF options such as `exportPdfWithHighCompatibility`, `exportPdfWithUnderlayer`, and the underlayer fields tune print and compatibility workflows.
- `allowTextOverhang` includes glyph overhangs when text would otherwise clip at its frame bounds.
For several blocks, use the list overload of `engine.block.export(...)` so the export worker can process them together. Use `engine.block.exportWithColorMask(...)` when you need a masked image and alpha mask from a specific color, and use `engine.block.exportVideo(...)` for MP4 video exports from page blocks with a time offset, duration, progress callback, and optional `ExportVideoOptions`.
## Single-Page vs Multi-Page Designs
Export the block that matches the result you want. Exporting a page gives you one predictable page-sized file, while exporting a group or individual block limits the output to that hierarchy.
When a scene contains multiple pages, exporting the whole scene includes the pages in their scene layout. That can produce transparent areas between pages when page spacing is part of the scene bounds. For multi-page workflows, export each page individually when you need consistent per-page files or dimensions.
## Device Export Limits
Before requesting large exports, check the current device's reported limits. `engine.editor.getMaxExportSize()` returns the maximum supported export dimension in pixels; both the requested width and height must stay at or below that value. When the limit is unknown, the API returns the maximum signed 32-bit integer value.
`engine.editor.getAvailableMemory()` returns currently available memory in bytes. Treat both values as planning signals, not guarantees: an export can still fail because of memory pressure, asset loading, or the complexity of the rendered content. For large previews, prefer explicit `targetWidth` and `targetHeight` values that stay comfortably below the reported size limit.
## Export from the Editor UI
The CE.SDK editor UI can expose export actions for users. Use the editor configuration guides when your app needs to replace the default export behavior, write the resulting file to app storage, or hand it to an Android share flow.
For a complete editor surface around video workflows, see the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/).
## Next Steps
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) — Configure PDF export from the editor UI and decide how your app stores or shares the generated file.
- [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/) — Export video compositions as MP4 files with configurable encoding options, progress tracking, and resolution control.
- [Compress Exports for Smaller Files](https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/) — Tune compression and quality settings to reduce output size.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Partial Export"
description: "Export individual blocks, grouped elements, or specific pages from a CE.SDK scene in Android instead of exporting the whole scene."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/partial-export-89aaf6/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Partial Export](https://img.ly/docs/cesdk/android/export-save-publish/export/partial-export-89aaf6/)
---
```kotlin file=@cesdk_android_examples/engine-guides-partial-export/PartialExport.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
data class PartialExport(
val individualGraphic: ByteBuffer,
val groupedElements: ByteBuffer,
val selection: ByteBuffer,
val currentPage: ByteBuffer,
val allPages: List,
val resizedPage: ByteBuffer,
val jpegPage: ByteBuffer,
val pdfPage: ByteBuffer,
val maxExportSize: Int,
val availableMemory: Long,
) {
val pngExports: List
get() = listOf(individualGraphic, groupedElements, selection, currentPage, resizedPage) + allPages
}
suspend fun partialExport(engine: Engine): PartialExport {
// Demo scaffolding: the guide snippets operate on an existing scene. This
// source file builds a deterministic two-page scene so the smoke test can
// verify every export path with real renderable blocks.
val scene = engine.scene.create()
val page1 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page1, value = 800F)
engine.block.setHeight(page1, value = 600F)
engine.block.appendChild(parent = scene, child = page1)
val rectangle = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(rectangle, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(rectangle, value = 220F)
engine.block.setHeight(rectangle, value = 220F)
engine.block.setPositionX(rectangle, value = 80F)
engine.block.setPositionY(rectangle, value = 100F)
engine.block.setName(rectangle, "background-rect")
engine.block.setFill(rectangle, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = rectangle,
color = Color.fromHex("#FF3366CC"),
)
engine.block.appendChild(parent = page1, child = rectangle)
val ellipse = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(ellipse, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(ellipse, value = 220F)
engine.block.setHeight(ellipse, value = 220F)
engine.block.setPositionX(ellipse, value = 340F)
engine.block.setPositionY(ellipse, value = 100F)
engine.block.setFill(ellipse, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = ellipse,
color = Color.fromHex("#FFF4C542"),
)
engine.block.appendChild(parent = page1, child = ellipse)
val star = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(star, shape = engine.block.createShape(ShapeType.Star))
engine.block.setWidth(star, value = 220F)
engine.block.setHeight(star, value = 220F)
engine.block.setPositionX(star, value = 210F)
engine.block.setPositionY(star, value = 350F)
engine.block.setFill(star, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = star,
color = Color.fromHex("#FFE54B4B"),
)
engine.block.appendChild(parent = page1, child = star)
val graphicBlocks = engine.block.findByType(DesignBlockType.Graphic)
val namedBlocks = engine.block.findByName("background-rect")
check(graphicBlocks.contains(rectangle))
check(namedBlocks.single() == rectangle)
engine.block.forceLoadResources(listOf(page1, rectangle, ellipse, star))
val firstGraphic = engine.block.findByType(DesignBlockType.Graphic).first()
val pngOptions = ExportOptions(pngCompressionLevel = 5)
val blockData = engine.block.export(
block = firstGraphic,
mimeType = MimeType.PNG,
options = pngOptions,
)
check(blockData.hasRemaining()) { "individual graphic export is empty" }
val groupBlocks = engine.block.findByType(DesignBlockType.Graphic).take(2)
val group = engine.block.group(groupBlocks)
val groupData = engine.block.export(
block = group,
mimeType = MimeType.PNG,
)
check(groupData.hasRemaining()) { "group export is empty" }
// In a real app the user selects blocks in the editor UI and the export
// action reads it. The smoke test sets one block selected here so the
// highlighted findAllSelected() snippet is deterministic offscreen.
engine.block.setSelected(star, selected = true)
val selectedBlocks = engine.block.findAllSelected()
val selectionData = when {
selectedBlocks.size == 1 -> engine.block.export(
block = selectedBlocks.single(),
mimeType = MimeType.PNG,
)
selectedBlocks.size > 1 && engine.block.isGroupable(selectedBlocks) -> {
val selectionGroup = engine.block.group(selectedBlocks)
try {
engine.block.export(
block = selectionGroup,
mimeType = MimeType.PNG,
)
} finally {
engine.block.ungroup(selectionGroup)
selectedBlocks.forEach { block ->
engine.block.setSelected(block, selected = true)
}
}
}
else -> null
}
checkNotNull(selectionData) { "no exportable selection" }
check(selectionData.hasRemaining()) { "selection export is empty" }
val currentPage = engine.scene.getCurrentPage()
val currentPageData = currentPage?.let { page ->
engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
}
checkNotNull(currentPageData) { "scene has no current page" }
check(currentPageData.hasRemaining()) { "current page export is empty" }
val page = engine.scene.getCurrentPage() ?: error("Scene has no current page")
val resizedOptions = ExportOptions(
targetWidth = 1200F,
targetHeight = 900F,
)
val resizedData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = resizedOptions,
)
check(resizedData.hasRemaining()) { "resized page export is empty" }
val jpegPage = engine.scene.getCurrentPage() ?: error("Scene has no current page")
val jpegOptions = ExportOptions(jpegQuality = 0.8F)
val jpegData = engine.block.export(
block = jpegPage,
mimeType = MimeType.JPEG,
options = jpegOptions,
)
check(jpegData.hasRemaining()) { "JPEG export is empty" }
val maxExportSize = engine.editor.getMaxExportSize()
val availableMemory = engine.editor.getAvailableMemory()
val pdfPage = engine.scene.getCurrentPage() ?: error("Scene has no current page")
val pdfOptions = ExportOptions(exportPdfWithHighCompatibility = true)
val pdfData = engine.block.export(
block = pdfPage,
mimeType = MimeType.PDF,
options = pdfOptions,
)
check(pdfData.hasRemaining()) { "PDF export is empty" }
val page2 = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page2, value = 800F)
engine.block.setHeight(page2, value = 600F)
engine.block.appendChild(parent = scene, child = page2)
val page2Graphic = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(page2Graphic, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(page2Graphic, value = 400F)
engine.block.setHeight(page2Graphic, value = 300F)
engine.block.setPositionX(page2Graphic, value = 200F)
engine.block.setPositionY(page2Graphic, value = 150F)
engine.block.setFill(page2Graphic, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = page2Graphic,
color = Color.fromHex("#FF35A66B"),
)
engine.block.appendChild(parent = page2, child = page2Graphic)
val pages = engine.scene.getPages()
val pageData = engine.block.export(
blocks = pages,
mimeType = MimeType.PNG,
)
check(pageData.size == pages.size)
pageData.forEachIndexed { index, data ->
check(data.hasRemaining()) { "page ${index + 1} export is empty" }
}
return PartialExport(
individualGraphic = blockData.copyForVerification(),
groupedElements = groupData.copyForVerification(),
selection = selectionData.copyForVerification(),
currentPage = currentPageData.copyForVerification(),
allPages = pageData.map(ByteBuffer::copyForVerification),
resizedPage = resizedData.copyForVerification(),
jpegPage = jpegData.copyForVerification(),
pdfPage = pdfData.copyForVerification(),
maxExportSize = maxExportSize,
availableMemory = availableMemory,
)
}
private fun ByteBuffer.copyForVerification(): ByteBuffer {
val duplicate = asReadOnlyBuffer()
val bytes = ByteArray(duplicate.remaining())
duplicate.get(bytes)
return ByteBuffer.wrap(bytes).asReadOnlyBuffer()
}
```
Export individual design elements, grouped blocks, or specific pages from your
scene instead of exporting everything at once using CE.SDK's export API.

> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-partial-export)
Partial export gives you fine-grained control over what leaves the scene. Instead of rendering the entire composition, you can target a single graphic, a logical group of blocks, or one page out of a multi-page document. This is the foundation for asset-library generation, "export selection" features, page-by-page previews, and per-element output pipelines.
This guide covers exporting individual blocks, grouped elements, and pages with `engine.block.export(...)`, plus the format and sizing options that shape each output.
## Understanding Block Hierarchy and Export
### How Block Hierarchy Affects Exports
CE.SDK organizes content as a tree: Scene -> Pages -> Groups -> Individual Blocks. When you export a block, the export automatically includes every descendant of that block.
Exporting a page exports every element on that page. Exporting a group exports the group and all of its children. Exporting an individual block, such as a graphic or text block, exports only that block. The level of the hierarchy you target determines the scope of the output: choose the page for a complete layout, the group for a composite asset, or the block itself for a single element.
### Export Behavior
The export pipeline normalizes a few things automatically. If the exported block itself is rotated, it is exported without that rotation so the content appears upright in the file. Any margin set on the block is included in the export bounds. Outside strokes are included for most block types; pages handle strokes differently.
> **Note:** Only blocks that belong to the scene hierarchy can be exported. A block
> created with `engine.block.create(...)` but never appended to a parent already
> attached to the scene cannot be exported until it is added to the tree.
## Exporting Individual Blocks
### Finding Blocks to Export
Before exporting, locate the block. The most common entry points are `findByType(...)`, which returns every block of a given `DesignBlockType`, and `findByName(...)`, which returns blocks you have tagged with `engine.block.setName(...)`. If the caller already holds a `DesignBlock` reference from a creation call or a tap handler, you can pass it directly.
```kotlin highlight-android-find-blocks
val graphicBlocks = engine.block.findByType(DesignBlockType.Graphic)
val namedBlocks = engine.block.findByName("background-rect")
```
`findByType(DesignBlockType.Graphic)` returns every graphic block in the scene regardless of fill content. Filter further by inspecting the block's fill or kind if you need a specific subset.
### Basic Block Export
`engine.block.export(...)` is a suspending call that returns a `ByteBuffer`. Pass the block ID, the desired `MimeType`, and an optional `ExportOptions` instance configured for that format. Your app can write the returned buffer to local storage, upload it, share it, or decode image exports with Android platform APIs.
```kotlin highlight-android-export-individual-block
val firstGraphic = engine.block.findByType(DesignBlockType.Graphic).first()
val pngOptions = ExportOptions(pngCompressionLevel = 5)
val blockData = engine.block.export(
block = firstGraphic,
mimeType = MimeType.PNG,
options = pngOptions,
)
```
Common static partial export choices on Android include `MimeType.PNG`, `MimeType.JPEG`, and `MimeType.PDF`. Android also exposes `MimeType.SVG`, `MimeType.TGA`, and `MimeType.BINARY` for workflows that need those block export targets. PNG is ideal for graphics that need transparency such as UI elements, logos, or illustrations with alpha channels. JPEG produces smaller files for photographs but drops transparency, replacing it with a solid background. PDF preserves vector information for print and document workflows.
## Exporting Grouped Elements
### Creating and Exporting Groups
Groups are useful when several blocks should leave the scene as a single output: a logo with multiple components, a composite illustration, or any layout section that should not be split. `engine.block.group(...)` takes a list of `DesignBlock` values, returns the new group's ID, and you export that ID like any other block.
```kotlin highlight-android-create-and-export-group
val groupBlocks = engine.block.findByType(DesignBlockType.Graphic).take(2)
val group = engine.block.group(groupBlocks)
val groupData = engine.block.export(
block = group,
mimeType = MimeType.PNG,
)
```
When a group is exported, CE.SDK renders all children together into one file. The group's bounding box determines the export dimensions, and the relative positioning of children is preserved exactly as designed.
### Exporting Selected Elements
A common product workflow is to export whatever the user has selected. `engine.block.findAllSelected()` returns the selected block IDs for an editor action; pass those IDs into the export logic, export a single block directly, and group several blocks when `engine.block.isGroupable(...)` allows it.
```kotlin highlight-android-export-selected
val selectedBlocks = engine.block.findAllSelected()
val selectionData = when {
selectedBlocks.size == 1 -> engine.block.export(
block = selectedBlocks.single(),
mimeType = MimeType.PNG,
)
selectedBlocks.size > 1 && engine.block.isGroupable(selectedBlocks) -> {
val selectionGroup = engine.block.group(selectedBlocks)
try {
engine.block.export(
block = selectionGroup,
mimeType = MimeType.PNG,
)
} finally {
engine.block.ungroup(selectionGroup)
selectedBlocks.forEach { block ->
engine.block.setSelected(block, selected = true)
}
}
}
else -> null
}
```
This lets a single "Export Selection" action handle both cases without branching in the UI layer. In an editor app the user provides the selection through the canvas. The Android sample sets a deterministic selection before this snippet so the smoke test can verify the same export logic without depending on interactive editor state.
## Exporting Pages
`engine.scene.getCurrentPage()` returns the active page as a nullable `DesignBlock`. Use a null check before exporting. To produce previews for every page in a multi-page document, walk `engine.scene.getPages()` instead.
```kotlin highlight-android-export-current-page
val currentPage = engine.scene.getCurrentPage()
val currentPageData = currentPage?.let { page ->
engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
}
```
Page exports include the page background, every element on the page, and any page-level effects. The page's own dimensions become the output dimensions unless `targetWidth` and `targetHeight` override them.
For multi-page documents, pass the page list from `engine.scene.getPages()` to the `blocks` overload. CE.SDK exports the batch with one worker and returns the buffers in the same order as the page list, so you can preserve document order while saving previews or thumbnails.
```kotlin highlight-android-export-all-pages
val pages = engine.scene.getPages()
val pageData = engine.block.export(
blocks = pages,
mimeType = MimeType.PNG,
)
```
Use sequential numbering when writing the buffers so files are easy to recombine. PNG suits image previews and per-page thumbnails. If you switch the batch to `MimeType.PDF`, the list overload still returns one PDF buffer per page; use the [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) guide when downstream consumers expect a single multi-page document.
## Export Options and Configuration
### Target Size Control
`ExportOptions(targetWidth = ..., targetHeight = ...)` requests output at a specific size while preserving the block's aspect ratio. The block is rendered large enough to fill the target completely; if the requested size has a different aspect than the block, one axis may extend past the target so proportions stay correct.
```kotlin highlight-android-target-size
val page = engine.scene.getCurrentPage() ?: error("Scene has no current page")
val resizedOptions = ExportOptions(
targetWidth = 1200F,
targetHeight = 900F,
)
val resizedData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = resizedOptions,
)
```
`targetWidth` and `targetHeight` are pixel values regardless of the scene's design unit, which makes them useful for thumbnails, social-media presets, and platform-imposed dimensions.
### Quality and Compression
Each export format reads the options that apply to that encoder. Use them to trade output size against visual fidelity.
```kotlin highlight-android-quality-options
val jpegPage = engine.scene.getCurrentPage() ?: error("Scene has no current page")
val jpegOptions = ExportOptions(jpegQuality = 0.8F)
val jpegData = engine.block.export(
block = jpegPage,
mimeType = MimeType.JPEG,
options = jpegOptions,
)
```
| Field | Range | Behavior |
| --- | --- | --- |
| `pngCompressionLevel` | `0`-`9` (default `5`) | Higher values produce smaller files at the cost of encoding time. PNG is lossless, so quality is unaffected. |
| `jpegQuality` | `(0, 1]` (default `0.9`) | Higher values produce larger, sharper files. Values above `0.9` are visually transparent for most content. |
| `exportPdfWithHighCompatibility` | `true` or `false` (default `true`) | Rasterizes bitmap content and some effects at the scene DPI for broader PDF viewer compatibility. |
### Export Size Limits
Before requesting a very large export, query the device's reported limits. `getMaxExportSize()` returns the maximum dimension in pixels, or `Int.MAX_VALUE` when the limit is unknown. Both width and height of the export must stay below or equal to this value. `getAvailableMemory()` returns free engine memory in bytes; an export that fits within `getMaxExportSize()` may still fail if memory is tight.
```kotlin highlight-android-check-limits
val maxExportSize = engine.editor.getMaxExportSize()
val availableMemory = engine.editor.getAvailableMemory()
```
Use these values to gate user-facing presets, warn on requests that exceed the device limit, or pick a smaller `targetWidth` and `targetHeight` automatically when memory is constrained.
## Export Limitations and Considerations
### Format-Specific Constraints
JPEG drops transparency from the output, replacing transparent pixels with a solid background. Designs that rely on alpha channels should export to PNG instead.
PDF behavior depends on `ExportOptions.exportPdfWithHighCompatibility`. With `true` (the default), bitmap content and effects are rasterized at the scene DPI for broader viewer compatibility. With `false`, PDFs export faster by embedding images directly, but gradients with transparency may not render correctly in Safari or macOS Preview. See the [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) guide for detailed performance guidance.
```kotlin highlight-android-export-pdf
val pdfPage = engine.scene.getCurrentPage() ?: error("Scene has no current page")
val pdfOptions = ExportOptions(exportPdfWithHighCompatibility = true)
val pdfData = engine.block.export(
block = pdfPage,
mimeType = MimeType.PDF,
options = pdfOptions,
)
```
### Performance Considerations
`engine.block.export(...)` runs on a worker engine and is suspendable, so the caller can keep the UI responsive. The operation still takes time proportional to the rendered area. Show progress for large exports, and prefer the list overload when batching because it reuses one worker across the batch.
### Hierarchy Requirements
Only blocks attached to the scene can be exported. Always append blocks to a page, or to a parent that is itself in the tree, before calling `export(...)`. Graphic blocks additionally need both a shape and a fill set; `engine.block.create(DesignBlockType.Graphic)` produces an empty placeholder until `setShape(...)` and `setFill(...)` are applied.
## API Reference
| API | Description |
| --- | --- |
| `engine.block.export(block=_, mimeType=_, options=_)` | Exports a single block as a `ByteBuffer`. |
| `engine.block.export(blocks=_, mimeType=_, options=_)` | Exports several blocks and returns one `ByteBuffer` per ID in input order. |
| `engine.block.findByType(type=DesignBlockType.Graphic)` | Returns every block matching the provided `DesignBlockType`. |
| `engine.block.findByName(name=_)` | Returns blocks tagged with `setName(...)`. |
| `engine.block.findAllSelected()` | Returns the currently selected blocks. |
| `engine.block.setSelected(block=_, selected=_)` | Updates selection state when preparing or restoring an export selection. |
| `engine.block.isGroupable(blocks=_)` | Checks whether a selection can be grouped before export. |
| `engine.block.group(blocks=_)` | Groups multiple blocks under a new parent and returns its ID. |
| `engine.block.ungroup(block=_)` | Removes a temporary group after exporting a multi-block selection. |
| `engine.scene.getCurrentPage()` | Returns the active page as `DesignBlock?`. |
| `engine.scene.getPages()` | Returns every page in scene order. |
| `engine.editor.getMaxExportSize()` | Returns the device's maximum export dimension in pixels. |
| `engine.editor.getAvailableMemory()` | Returns free engine memory in bytes. |
| `ExportOptions(...)` | Configures per-format options such as `pngCompressionLevel`, `jpegQuality`, `targetWidth`, `targetHeight`, and `exportPdfWithHighCompatibility`. |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Fundamentals of exporting from CE.SDK
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) — Multi-page PDF output and print-ready settings
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Size Limits"
description: "Configure and understand CE.SDK's image and video size limits in Android to balance quality and performance across devices."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/)
---
```kotlin file=@cesdk_android_examples/engine-guides-size-limits/SizeLimits.kt reference-only
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import ly.img.engine.EngineException
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.SizeMode
import java.nio.ByteBuffer
import kotlin.math.ceil
import kotlin.math.max
fun observeMaxImageSizeChanges(
engine: Engine,
scope: CoroutineScope,
onMaxImageSizeChanged: (Int) -> Unit,
): Job = scope.launch(Dispatchers.Main) {
engine.editor.onSettingsChanged().collect {
onMaxImageSizeChanged(engine.editor.getSettingInt("maxImageSize"))
}
}
suspend fun sizeLimits(engine: Engine): ByteBuffer = withContext(Dispatchers.Main) {
val scene = engine.scene.create()
engine.scene.setDesignUnit(DesignUnit.PIXEL)
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 800F)
engine.block.setHeight(background, value = 600F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(background, color = Color.fromHex("#FF2457D6"))
engine.block.appendChild(parent = page, child = background)
val originalMaxImageSize = engine.editor.getSettingInt("maxImageSize")
try {
val currentMaxImageSize = engine.editor.getSettingInt("maxImageSize")
// The default value is 4096 pixels.
check(currentMaxImageSize > 0)
// Lower the limit on memory-constrained devices. Apply this before
// loading images so newly loaded textures are downscaled to the new limit.
engine.editor.setSettingInt(keypath = "maxImageSize", value = 2048)
// Or raise it for high-quality workflows on capable devices:
// engine.editor.setSettingInt(keypath = "maxImageSize", value = 8192)
check(engine.editor.getSettingInt("maxImageSize") == 2048)
// The engine reports the maximum export size supported on the current device.
// The value is an upper bound. Exports may still fail for memory or other
// reasons. When the limit is unknown, the engine returns Int.MAX_VALUE.
val maxExportSize = engine.editor.getMaxExportSize()
check(maxExportSize > 0)
// This helper is scoped to this sample's untransformed page. In a more
// general scene, validate against the world-space dimensions your export
// workflow uses, or rely on export error handling.
val plannedExportOptions = ExportOptions(targetWidth = 1600F, targetHeight = 1000F)
val designUnit = engine.scene.getDesignUnit()
val widthMode = engine.block.getWidthMode(page)
val heightMode = engine.block.getHeightMode(page)
require(
designUnit == DesignUnit.PIXEL &&
widthMode == SizeMode.ABSOLUTE &&
heightMode == SizeMode.ABSOLUTE,
)
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val targetWidth = plannedExportOptions.targetWidth ?: pageWidth
val targetHeight = plannedExportOptions.targetHeight ?: pageHeight
val fillScale = max(targetWidth / pageWidth, targetHeight / pageHeight)
val renderedWidth = ceil(pageWidth * fillScale).toInt()
val renderedHeight = ceil(pageHeight * fillScale).toInt()
val withinLimit = renderedWidth <= maxExportSize && renderedHeight <= maxExportSize
check(withinLimit)
// Catch export errors so the app can recover. Common remediations are
// lowering the target box for the retry.
// ExportOptions.targetWidth/targetHeight are pixel values for the box
// that the exported block fills while preserving its aspect ratio.
val initialOptions = ExportOptions(targetWidth = 1600F, targetHeight = 1200F)
val pngData = try {
engine.block.export(block = page, mimeType = MimeType.PNG, options = initialOptions)
} catch (error: EngineException) {
val retryOptions = ExportOptions(targetWidth = 640F, targetHeight = 480F)
engine.block.export(block = page, mimeType = MimeType.PNG, options = retryOptions)
}
check(engine.editor.getSettingInt("maxImageSize") == 2048)
pngData.asReadOnlyBuffer()
} finally {
engine.editor.setSettingInt(keypath = "maxImageSize", value = originalMaxImageSize)
}
}
```
Configure size limits to balance quality and performance in CE.SDK applications.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-size-limits)
CE.SDK processes images and videos on the device, so size limits depend on available memory and rendering hardware. Tuning these limits keeps memory use predictable on smaller devices while still letting capable devices export at high resolution.
This guide covers reading and writing the `maxImageSize` setting, observing setting changes, querying the device's maximum export size, and handling export failures.
## Understanding Size Limits
CE.SDK manages size limits at two stages: **input** (when loading images) and **output** (when exporting). The `maxImageSize` setting controls input resolution and downscales images that exceed the configured limit before they reach the canvas. The default is 4096×4096 pixels, which keeps memory use predictable on a wide range of devices.
Export resolution has no artificial limit. The engine can render up to 16,384×16,384 pixels in theory, but the actual ceiling is determined by the device's rendering hardware and available memory. Use `engine.editor.getMaxExportSize()` to read the device's reported upper bound at runtime.
## Resolution & Duration Limits
## Configuring maxImageSize
Read and modify `maxImageSize` through the Settings API. The setting is an integer in pixels, so use the `Int` accessors on `engine.editor`.
### Reading the Current Setting
To check the value currently in effect:
```kotlin highlight-android-read-setting
val currentMaxImageSize = engine.editor.getSettingInt("maxImageSize")
// The default value is 4096 pixels.
```
The default is `4096`. Read this value at startup to surface it in your UI, or to make runtime decisions about asset loading.
### Setting a New Value
Apply a new limit before loading images so newly loaded textures are downscaled to the new size:
```kotlin highlight-android-write-setting
// Lower the limit on memory-constrained devices. Apply this before
// loading images so newly loaded textures are downscaled to the new limit.
engine.editor.setSettingInt(keypath = "maxImageSize", value = 2048)
// Or raise it for high-quality workflows on capable devices:
// engine.editor.setSettingInt(keypath = "maxImageSize", value = 8192)
```
Images already on the canvas keep their loaded resolution until they are reloaded. Lower values reduce memory pressure on phones and tablets; higher values preserve detail on higher-end devices.
### Observing Settings Changes
Subscribe to settings changes through `engine.editor.onSettingsChanged()`. The Flow emits `Unit` on setting changes, so collect it in an owning coroutine scope and read `maxImageSize` back inside the collector. The helper launches the collection on `Dispatchers.Main` because Android Engine APIs must run on the Engine thread:
```kotlin highlight-android-observe-changes
fun observeMaxImageSizeChanges(
engine: Engine,
scope: CoroutineScope,
onMaxImageSizeChanged: (Int) -> Unit,
): Job = scope.launch(Dispatchers.Main) {
engine.editor.onSettingsChanged().collect {
onMaxImageSizeChanged(engine.editor.getSettingInt("maxImageSize"))
}
}
```
Start this subscription after the engine has started and cancel the returned `Job` when the owning screen or view model no longer needs updates.
## Device Export Capabilities
The maximum export size on the current device is exposed directly:
```kotlin highlight-android-max-export-size
// The engine reports the maximum export size supported on the current device.
// The value is an upper bound. Exports may still fail for memory or other
// reasons. When the limit is unknown, the engine returns Int.MAX_VALUE.
val maxExportSize = engine.editor.getMaxExportSize()
```
`getMaxExportSize()` returns the upper export limit in pixels for both width and height. When the device limit is unknown or not reported, the engine returns `Int.MAX_VALUE` as a sentinel value. Treat that as "no reported limit", not as a guarantee that every export size is safe: memory and GPU constraints still apply, so keep large export presets conservative and handle export failures.
Use the value to:
- Cap export presets to dimensions the device can render
- Warn users when a requested export exceeds the device limit
- Pick a conservative default `maxImageSize` for the device class
You can also pre-validate a planned export against the limit when your app can resolve the exported block's dimensions. Static exports use `ExportOptions.targetWidth` and `targetHeight`; MP4 exports use the matching fields on `ExportVideoOptions`. Those target fields are already pixel dimensions for the box the block fills while preserving its aspect ratio.
The helper below is intentionally scoped to the guide sample's untransformed page: the scene uses `DesignUnit.PIXEL`, both page size modes are `SizeMode.ABSOLUTE`, and no parent transform changes the page scale. In that setup, `getWidth()` and `getHeight()` match the page dimensions used to compute the export aspect ratio. If your app supports non-pixel units, percentage or auto sizing, rotated blocks, or parent-scaled content, do not treat local `getWidth()` / `getHeight()` values as final export dimensions. Instead, validate with the world-space dimensions your app uses for export presets, or skip the pre-check and rely on the export error handling shown below.
When the block and target box have different aspect ratios, the rendered width or height can be larger than the raw target value. Use the fill scale, `max(targetWidth / pageWidth, targetHeight / pageHeight)`, then compare `ceil(pageWidth * scale)` and `ceil(pageHeight * scale)` with the device limit.
```kotlin highlight-android-validate-export
// This helper is scoped to this sample's untransformed page. In a more
// general scene, validate against the world-space dimensions your export
// workflow uses, or rely on export error handling.
val plannedExportOptions = ExportOptions(targetWidth = 1600F, targetHeight = 1000F)
val designUnit = engine.scene.getDesignUnit()
val widthMode = engine.block.getWidthMode(page)
val heightMode = engine.block.getHeightMode(page)
require(
designUnit == DesignUnit.PIXEL &&
widthMode == SizeMode.ABSOLUTE &&
heightMode == SizeMode.ABSOLUTE,
)
val pageWidth = engine.block.getWidth(page)
val pageHeight = engine.block.getHeight(page)
val targetWidth = plannedExportOptions.targetWidth ?: pageWidth
val targetHeight = plannedExportOptions.targetHeight ?: pageHeight
val fillScale = max(targetWidth / pageWidth, targetHeight / pageHeight)
val renderedWidth = ceil(pageWidth * fillScale).toInt()
val renderedHeight = ceil(pageHeight * fillScale).toInt()
val withinLimit = renderedWidth <= maxExportSize && renderedHeight <= maxExportSize
```
## Handling Export Errors
`engine.block.export()` can throw, so wrap it in a `try`/`catch` block and provide a fallback when a static export fails. A practical recovery is to catch `EngineException` and retry with smaller `targetWidth` and `targetHeight` values:
```kotlin highlight-android-handle-export
// Catch export errors so the app can recover. Common remediations are
// lowering the target box for the retry.
// ExportOptions.targetWidth/targetHeight are pixel values for the box
// that the exported block fills while preserving its aspect ratio.
val initialOptions = ExportOptions(targetWidth = 1600F, targetHeight = 1200F)
val pngData = try {
engine.block.export(block = page, mimeType = MimeType.PNG, options = initialOptions)
} catch (error: EngineException) {
val retryOptions = ExportOptions(targetWidth = 640F, targetHeight = 480F)
engine.block.export(block = page, mimeType = MimeType.PNG, options = retryOptions)
}
```
This pattern lets the app keep delivering an export even when the first attempt is too large for the current device or memory pressure is high. Use the same retry strategy for video exports, but call `engine.block.exportVideo()` and pass `ExportVideoOptions(targetWidth=_, targetHeight=_)` instead. Catching `EngineException` keeps coroutine cancellation and VM errors from being converted into retry work. Lowering `maxImageSize` only affects images loaded after the setting changes; if image input size is part of your recovery path, reload or recreate the affected image blocks before retrying.
## Troubleshooting
| Issue | Cause | Solution |
| --- | --- | --- |
| Images appear blurry on the canvas | `maxImageSize` is below the source resolution | Raise `maxImageSize` if the device has the memory headroom |
| Out-of-memory crashes during editing | `maxImageSize` is too high for the device | Lower `maxImageSize`, especially on phones and tablets |
| Export throws unexpectedly | Rendered output dimensions exceed `getMaxExportSize()` | Reduce the target box or pick a smaller export preset |
| Video export fails | Resolution or duration exceeds device capability | Export at 1080p instead of 4K, or shorten the video |
| Inconsistent results across devices | Different rendering hardware | Set a conservative `maxImageSize` such as `4096` and gate larger exports on `getMaxExportSize()` |
## API Reference
| Method | Description |
| --- | --- |
| `engine.editor.getSettingInt(keypath=_)` | Reads an integer setting, such as `maxImageSize` |
| `engine.editor.setSettingInt(keypath=_, value=_)` | Updates an integer setting |
| `engine.editor.onSettingsChanged()` | Returns a Flow that emits when editor settings change |
| `engine.editor.getMaxExportSize()` | Returns the device's maximum export dimension in pixels |
| `engine.block.export(block=_, mimeType=_, options=_)` | Exports a block as static image, binary, SVG, or PDF data |
| `ExportOptions(targetWidth=_, targetHeight=_)` | Configures the target box that static exports fill while preserving aspect ratio |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_)` | Exports a page block as MP4 video data |
| `ExportVideoOptions(targetWidth=_, targetHeight=_)` | Configures the target box that video exports fill while preserving aspect ratio |
| `engine.block.getWidth(block=_)` | Returns the block width in the scene's design unit. Use it for export pre-validation only in a scope where that local value matches the exported block's resolved dimensions, such as an untransformed absolute pixel page |
| `engine.block.getHeight(block=_)` | Returns the block height in the scene's design unit. Use it for export pre-validation only in a scope where that local value matches the exported block's resolved dimensions, such as an untransformed absolute pixel page |
| `engine.block.getWidthMode(block=_)` | Returns the `SizeMode` used for the block width |
| `engine.block.getHeightMode(block=_)` | Returns the `SizeMode` used for the block height |
| `engine.scene.getDesignUnit()` | Reads the scene's design unit |
| `engine.scene.setDesignUnit(designUnit=_)` | Sets the scene's design unit |
## Next Steps
Explore related guides to build complete export workflows:
- [Settings Guide](https://img.ly/docs/cesdk/android/settings-970c98/) - Complete Settings API reference and configuration options
- [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) - Supported image and video formats with capabilities
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Fundamentals of exporting images and videos from CE.SDK
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - PDF export guide with multi-page support and print optimization
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To JPEG"
description: "Export CE.SDK designs to JPEG format with configurable quality settings for photographs, web images, and social media content."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/to-jpeg-6f88e9/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [To JPEG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-jpeg-6f88e9/)
---
```kotlin file=@cesdk_android_examples/engine-guides-export-to-jpeg/ToJpeg.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
data class JpegExport(
val label: String,
val jpegData: ByteBuffer,
)
data class ToJpegResult(
val defaultQuality: JpegExport,
val highQuality: JpegExport,
val targetDimensions: JpegExport,
val savedFile: File,
) {
val allExports: List
get() = listOf(defaultQuality, highQuality, targetDimensions)
}
suspend fun toJpeg(
engine: Engine,
outputDir: File,
): ToJpegResult {
val page = createJpegExportPage(engine)
val defaultQualityData = exportToJpeg(engine, page)
val highQualityData = exportJpegWithQuality(engine, page)
val targetDimensionsData = exportJpegWithTargetDimensions(engine, page)
val savedFile = saveJpegToFile(
buffer = defaultQualityData,
outputFile = File(outputDir, "to-jpeg-page.jpg"),
)
return ToJpegResult(
defaultQuality = JpegExport("default quality", defaultQualityData),
highQuality = JpegExport("high quality", highQualityData),
targetDimensions = JpegExport("target dimensions", targetDimensionsData),
savedFile = savedFile,
)
}
suspend fun createJpegExportPage(engine: Engine): DesignBlock {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, "JPEG export background")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 1280F)
engine.block.setHeight(background, value = 720F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(background, color = Color.fromHex("#FFF4F0EA"))
engine.block.appendChild(parent = page, child = background)
val photoPanel = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(photoPanel, "JPEG export photo panel")
engine.block.setShape(photoPanel, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(photoPanel, value = 820F)
engine.block.setHeight(photoPanel, value = 460F)
engine.block.setPositionX(photoPanel, value = 230F)
engine.block.setPositionY(photoPanel, value = 130F)
engine.block.setFill(photoPanel, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(photoPanel, color = Color.fromHex("#FF255C99"))
engine.block.appendChild(parent = page, child = photoPanel)
val highlight = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(highlight, "JPEG export highlight")
engine.block.setShape(highlight, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(highlight, value = 520F)
engine.block.setHeight(highlight, value = 160F)
engine.block.setPositionX(highlight, value = 380F)
engine.block.setPositionY(highlight, value = 270F)
engine.block.setFill(highlight, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(highlight, color = Color.fromHex("#FFE7B65E"))
engine.block.appendChild(parent = page, child = highlight)
return page
}
suspend fun exportToJpeg(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(jpegQuality = 0.9F)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "JPEG export is empty" }
return jpegData
}
suspend fun exportJpegWithQuality(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(jpegQuality = 1.0F)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "high-quality JPEG export is empty" }
return jpegData
}
suspend fun exportJpegWithTargetDimensions(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(
jpegQuality = 0.85F,
targetWidth = 1920F,
targetHeight = 1080F,
)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "target-dimension JPEG export is empty" }
return jpegData
}
suspend fun saveJpegToFile(
buffer: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableBuffer = buffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
check(outputFile.length() > 0L) { "Saved JPEG export is empty" }
outputFile
}
```
Export CE.SDK designs to JPEG format when file size matters more than transparency.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-to-jpeg)
JPEG uses lossy compression, which makes it a good fit for photographs, social media images, and web delivery. It does not preserve transparency, so transparent areas become opaque in the exported image.
This guide covers exporting a page to JPEG, tuning quality, resizing the output, and writing the returned `ByteBuffer` to a file.
## Export to JPEG
Call `engine.block.export(...)` with `MimeType.JPEG` to render a page, scene, group, or block as JPEG data. Pass `ExportOptions` when you want to set the quality explicitly.
```kotlin highlight-android-export-jpeg
suspend fun exportToJpeg(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(jpegQuality = 0.9F)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "JPEG export is empty" }
return jpegData
}
```
The `jpegQuality` value accepts floats from greater than `0F` to `1F`. Higher values keep more visual detail and produce larger files; the default is `0.9F`.
## Export Options
JPEG export reads these fields from `ExportOptions`:
| Option | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------- |
| `jpegQuality` | `Float` | `0.9F` | Quality from greater than `0F` to `1F` |
| `targetWidth` | `Float?` | `null` | Output width in pixels, used together with height |
| `targetHeight` | `Float?` | `null` | Output height in pixels, used together with width |
### Quality Control
Use `jpegQuality = 1.0F` when you need maximum quality and can accept the larger file. Values around `0.8F` to `0.85F` usually work well for web delivery or social media.
```kotlin highlight-android-quality-control
suspend fun exportJpegWithQuality(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(jpegQuality = 1.0F)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "high-quality JPEG export is empty" }
return jpegData
}
```
Lower values reduce file size but can introduce visible compression artifacts, especially around text and hard edges.
### Target Dimensions
Set `targetWidth` and `targetHeight` together to export at a specific size. CE.SDK renders the block large enough to fill the target size while maintaining the block's aspect ratio.
```kotlin highlight-android-target-dimensions
suspend fun exportJpegWithTargetDimensions(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val options = ExportOptions(
jpegQuality = 0.85F,
targetWidth = 1920F,
targetHeight = 1080F,
)
val jpegData = engine.block.export(
block = page,
mimeType = MimeType.JPEG,
options = options,
)
check(jpegData.hasRemaining()) { "target-dimension JPEG export is empty" }
return jpegData
}
```
If the target aspect ratio differs from the block's aspect ratio, the exported image extends on one axis to preserve proportions.
## Save to File System
Android receives the export as a `ByteBuffer`. Write from a read-only duplicate when the same buffer is also needed for validation, upload, or decoding later in your flow.
```kotlin highlight-android-save-file
suspend fun saveJpegToFile(
buffer: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableBuffer = buffer.asReadOnlyBuffer()
FileOutputStream(outputFile).channel.use { channel ->
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
check(outputFile.length() > 0L) { "Saved JPEG export is empty" }
outputFile
}
```
The sample writes into an app-controlled `File`. Use your app's own storage, upload, or sharing pipeline when the JPEG should leave local storage.
## When to Use JPEG
JPEG works well for:
- Photographs and images with gradual color transitions
- Social media posts and web content
- Exports where small file size matters more than perfect pixel fidelity
> **Note:** Use PNG instead when the design needs transparency, sharp text, crisp vector edges, or lossless output.
## Troubleshooting
**Output looks blurry** - Increase `jpegQuality` toward `1.0F`, or use PNG for graphics with hard edges.
**File size is too large** - Lower `jpegQuality` toward `0.7F` to `0.8F`, or reduce dimensions with `targetWidth` and `targetHeight`.
**Transparent areas look opaque** - JPEG does not support alpha. Export PNG when transparent pixels must stay transparent.
## API Reference
| API | Purpose |
| --- | --- |
| `engine.block.export(block=_, mimeType=_, options=_)` | Export one block as JPEG `ByteBuffer` data |
| `ExportOptions(jpegQuality=_, targetWidth=_, targetHeight=_)` | Configure JPEG quality and output dimensions |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Compare all available export formats
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - Export for print and document workflows
- [Create Thumbnail](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/) - Generate thumbnail preview images by exporting with target dimensions
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To MP4"
description: "Export video compositions as MP4 files with H.264 encoding, progress events, and configurable bitrate and resolution."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/)
---
```kotlin file=@cesdk_android_examples/engine-guides-export-to-mp4/ExportToMp4.kt reference-only
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportVideoOptions
import ly.img.engine.ExportVideoProgress
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
import java.nio.ByteBuffer
import kotlin.coroutines.cancellation.CancellationException
data class Mp4ExportResult(
val outputFile: File,
val progressEvents: List,
val cancelableExportCanceled: Boolean,
val configuredExport: ByteBuffer,
val partialExport: ByteBuffer,
)
suspend fun exportToMp4(engine: Engine): Mp4ExportResult = withContext(Dispatchers.Main) {
val page = createVideoPage(engine)
val pageDuration = engine.block.getDuration(page)
val defaultExport = exportMp4(engine, page, pageDuration)
val progressEvents = mutableListOf()
val progressExport = exportMp4WithProgress(engine, page, pageDuration, progressEvents)
val cancelableJob = startCancelableMp4Export(
scope = this,
engine = engine,
page = page,
pageDuration = pageDuration,
onProgress = {},
)
cancelMp4Export(cancelableJob)
cancelableJob.join()
val resolutionExport = exportMp4WithResolutionOptions(engine, page, pageDuration)
val configuredExport = exportMp4WithBitrateOptions(engine, page, pageDuration)
val partialExport = exportPartialTimeline(engine, page)
check(progressExport.hasRemaining()) { "progress MP4 export is empty" }
check(cancelableJob.isCancelled) { "MP4 export cancellation was not observed" }
check(resolutionExport.hasRemaining()) { "resolution MP4 export is empty" }
Mp4ExportResult(
outputFile = writeMp4ToTempFile(defaultExport),
progressEvents = progressEvents.toList(),
cancelableExportCanceled = cancelableJob.isCancelled,
configuredExport = configuredExport,
partialExport = partialExport,
)
}
suspend fun exportMp4(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
): ByteBuffer {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = {},
)
check(videoBytes.hasRemaining()) { "MP4 export is empty" }
return videoBytes
}
suspend fun exportMp4WithProgress(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
progressEvents: MutableList,
): ByteBuffer {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = { progress ->
progressEvents += progress
Log.i(
"ExportToMp4Guide",
"Encoded ${progress.encodedFrames} of ${progress.totalFrames} frames",
)
},
)
check(videoBytes.hasRemaining()) { "progress MP4 export is empty" }
return videoBytes
}
fun startCancelableMp4Export(
scope: CoroutineScope,
engine: Engine,
page: DesignBlock,
pageDuration: Double,
onProgress: (ExportVideoProgress) -> Unit,
): Job = scope.launch(Dispatchers.Main) {
try {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = onProgress,
)
check(videoBytes.hasRemaining()) { "cancelable MP4 export is empty" }
} catch (exception: CancellationException) {
Log.i("ExportToMp4Guide", "MP4 export canceled")
throw exception
}
}
fun cancelMp4Export(exportJob: Job) {
exportJob.cancel()
}
suspend fun exportMp4WithResolutionOptions(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
): ByteBuffer {
val options = ExportVideoOptions(
targetWidth = 1280F,
targetHeight = 720F,
frameRate = 30F, // Use 30 fps for smooth playback on common mobile targets.
)
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = {},
options = options,
)
check(videoBytes.hasRemaining()) { "resolution MP4 export is empty" }
return videoBytes
}
suspend fun exportMp4WithBitrateOptions(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
): ByteBuffer {
val options = ExportVideoOptions(
videoBitrate = 8_000_000,
audioBitrate = 128_000,
)
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = {},
options = options,
)
check(videoBytes.hasRemaining()) { "configured MP4 export is empty" }
return videoBytes
}
suspend fun exportPartialTimeline(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.25, // Start after the first quarter-second to demonstrate offsets.
duration = 0.5, // Export a short segment so partial exports stay fast.
mimeType = MimeType.MP4,
progressCallback = {},
options = ExportVideoOptions(
frameRate = 12F, // Lower fps keeps this short preview segment lightweight.
),
)
check(videoBytes.hasRemaining()) { "partial MP4 export is empty" }
return videoBytes
}
suspend fun writeMp4ToTempFile(videoBytes: ByteBuffer): File = withContext(Dispatchers.IO) {
val outputFile = File.createTempFile("export-to-mp4-", ".mp4")
val videoData = videoBytes.asReadOnlyBuffer()
outputFile.outputStream().channel.use { channel ->
while (videoData.hasRemaining()) {
channel.write(videoData)
}
}
check(outputFile.length() > 0L) { "MP4 output file is empty" }
outputFile
}
private fun createVideoPage(engine: Engine): DesignBlock {
val scene = engine.scene.createForVideo()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 640F)
engine.block.setHeight(page, value = 360F)
engine.block.setDuration(page, duration = 1.0)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 640F)
engine.block.setHeight(background, value = 360F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = background,
color = Color.fromHex("#FF101827"),
)
engine.block.appendChild(parent = page, child = background)
val accent = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(accent, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(accent, value = 320F)
engine.block.setHeight(accent, value = 180F)
engine.block.setPositionX(accent, value = 160F)
engine.block.setPositionY(accent, value = 90F)
engine.block.setFill(accent, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = accent,
color = Color.fromHex("#FF42C3A7"),
)
engine.block.appendChild(parent = page, child = accent)
return page
}
```
Export video compositions as MP4 files with H.264 encoding, progress events,
and configurable bitrate and resolution.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-to-mp4)
MP4 is the most widely supported video format. CE.SDK renders the page timeline,
encodes frames with H.264, and muxes audio into the MP4 container on a
background export engine.
This guide covers exporting an existing page block to MP4, tracking progress,
canceling an in-progress export, configuring resolution and bitrate, exporting
a timeline segment, and writing the returned `ByteBuffer` to a file.
> **Caution:** H.264 does not support transparency. Transparent areas in your scene render with
> a black background in the exported MP4.
## Export to MP4
Call `engine.block.exportVideo(...)` with a page block, timeline range, and
`MimeType.MP4`. The method returns a `ByteBuffer` containing the encoded video
data.
```kotlin highlight-android-export-video
suspend fun exportMp4(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
): ByteBuffer {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = {},
)
check(videoBytes.hasRemaining()) { "MP4 export is empty" }
return videoBytes
}
```
Pass the page duration when you want to export the full timeline. The export API
currently supports page blocks for video output.
## Tracking Export Progress
The `progressCallback` receives rendered frames, encoded frames, and total
frames. Use the encoded frame count for user-facing progress because encoding is
usually the slower stage.
```kotlin highlight-android-progress
suspend fun exportMp4WithProgress(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
progressEvents: MutableList,
): ByteBuffer {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = { progress ->
progressEvents += progress
Log.i(
"ExportToMp4Guide",
"Encoded ${progress.encodedFrames} of ${progress.totalFrames} frames",
)
},
)
check(videoBytes.hasRemaining()) { "progress MP4 export is empty" }
return videoBytes
}
```
## Cancel an Export
`engine.block.exportVideo(...)` is a suspending Android API, so cancel the
coroutine that owns the export. Keep the returned `Job` in your UI state or
view model and call `cancel()` when the user cancels the operation.
```kotlin highlight-android-cancel
fun startCancelableMp4Export(
scope: CoroutineScope,
engine: Engine,
page: DesignBlock,
pageDuration: Double,
onProgress: (ExportVideoProgress) -> Unit,
): Job = scope.launch(Dispatchers.Main) {
try {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = onProgress,
)
check(videoBytes.hasRemaining()) { "cancelable MP4 export is empty" }
} catch (exception: CancellationException) {
Log.i("ExportToMp4Guide", "MP4 export canceled")
throw exception
}
}
fun cancelMp4Export(exportJob: Job) {
exportJob.cancel()
}
```
Let `CancellationException` propagate after cleanup or logging so structured
concurrency can finish the canceled job correctly. The Android binding stops
the background export engine when the suspending export exits.
## Configure Video Encoding
Pass `ExportVideoOptions` to control the target box, framerate, and bitrate.
### Resolution and Framerate
Set `targetWidth`, `targetHeight`, and `frameRate` when you need a target box
for video output and a predictable playback cadence. CE.SDK renders the page
large enough to fill the target box while preserving its aspect ratio, so the
encoded frame may exceed one requested axis when the page and target box use
different aspect ratios.
```kotlin highlight-android-resolution
suspend fun exportMp4WithResolutionOptions(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
): ByteBuffer {
val options = ExportVideoOptions(
targetWidth = 1280F,
targetHeight = 720F,
frameRate = 30F, // Use 30 fps for smooth playback on common mobile targets.
)
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = {},
options = options,
)
check(videoBytes.hasRemaining()) { "resolution MP4 export is empty" }
return videoBytes
}
```
### Video and Audio Bitrate
Set `videoBitrate` and `audioBitrate` in bits per second to control file size
and compression quality. Use `0` when you want the engine to choose a bitrate
automatically.
```kotlin highlight-android-bitrate
suspend fun exportMp4WithBitrateOptions(
engine: Engine,
page: DesignBlock,
pageDuration: Double,
): ByteBuffer {
val options = ExportVideoOptions(
videoBitrate = 8_000_000,
audioBitrate = 128_000,
)
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = pageDuration,
mimeType = MimeType.MP4,
progressCallback = {},
options = options,
)
check(videoBytes.hasRemaining()) { "configured MP4 export is empty" }
return videoBytes
}
```
### Export Parameters and Options
| Parameter or option | Default | Description |
| --- | --- | --- |
| `mimeType` | Required | Use `MimeType.MP4` for MP4 video output. |
| `timeOffset` | Required | Start time in seconds on the page timeline. |
| `duration` | Required | Length in seconds to export. Pass the page duration for the full timeline. |
| `progressCallback` | Required | Receives `ExportVideoProgress` with rendered, encoded, and total frame counts. |
| `targetWidth` | `null` | Optional target-box width in pixels. Use with `targetHeight`; the final width may be larger when aspect ratios differ. |
| `targetHeight` | `null` | Optional target-box height in pixels. Use with `targetWidth`; the final height may be larger when aspect ratios differ. |
| `frameRate` | `30F` | Target framerate in Hz. |
| `videoBitrate` | `0` | Video bitrate in bits per second. `0` enables automatic selection. |
| `audioBitrate` | `0` | Audio bitrate in bits per second. `0` enables automatic selection. |
| `allowTextOverhang` | `false` | Includes text bounds that account for glyph overhangs. |
## Export a Partial Timeline
Use `timeOffset` and `duration` to export a segment without changing the scene.
Both values use seconds relative to the page timeline.
```kotlin highlight-android-partial
suspend fun exportPartialTimeline(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
val videoBytes = engine.block.exportVideo(
block = page,
timeOffset = 0.25, // Start after the first quarter-second to demonstrate offsets.
duration = 0.5, // Export a short segment so partial exports stay fast.
mimeType = MimeType.MP4,
progressCallback = {},
options = ExportVideoOptions(
frameRate = 12F, // Lower fps keeps this short preview segment lightweight.
),
)
check(videoBytes.hasRemaining()) { "partial MP4 export is empty" }
return videoBytes
}
```
## Handle Export Results
The returned `ByteBuffer` stays in memory. Write it to app storage, upload it to
your backend, or pass it to another Android API without copying it into an
intermediate array.
```kotlin highlight-android-write-file
suspend fun writeMp4ToTempFile(videoBytes: ByteBuffer): File = withContext(Dispatchers.IO) {
val outputFile = File.createTempFile("export-to-mp4-", ".mp4")
val videoData = videoBytes.asReadOnlyBuffer()
outputFile.outputStream().channel.use { channel ->
while (videoData.hasRemaining()) {
channel.write(videoData)
}
}
check(outputFile.length() > 0L) { "MP4 output file is empty" }
outputFile
}
```
Delete temporary files after your app finishes sharing, uploading, or processing
the exported video.
## Troubleshooting
- **Export fails or hangs**: Make sure the scene and media assets are fully
loaded before starting the export. Check `engine.editor.getAvailableMemory()`
before large exports and reduce the requested size or framerate on low-memory
devices.
- **Poor video quality**: Increase `videoBitrate` and keep source media at
least as large as the target size.
- **Slow exports**: Lower `targetWidth`, `targetHeight`, `frameRate`, or
bitrate. Clamp requested dimensions with `engine.editor.getMaxExportSize()` so
the app does not offer unsupported output sizes.
- **Playback issues on devices**: Lower the requested size, framerate, or
bitrate, and test the generated MP4 on the devices your app supports.
## API Reference
| API | Description |
| --- | --- |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Exports a page timeline to a video `ByteBuffer` and reports rendering and encoding progress. |
| `CoroutineScope.launch(context=_)` | Starts a coroutine that can own a long-running export operation. |
| `Job.cancel()` | Requests cancellation of the coroutine running the export. |
| `engine.editor.getAvailableMemory()` | Returns available memory in bytes so apps can avoid starting large exports on low-memory devices. |
| `engine.editor.getMaxExportSize()` | Returns the maximum export dimension in pixels for constraining requested output width and height. |
| `ExportVideoOptions(targetWidth=_, targetHeight=_, frameRate=_, videoBitrate=_, audioBitrate=_, allowTextOverhang=_)` | Configures the MP4 target box, framerate, bitrate, and text overhang behavior. |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Compare all supported export formats
- [Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/) - Understand and configure limits on exported file dimensions or data size.
- [Export Audio](https://img.ly/docs/cesdk/android/guides/export-save-publish/export/audio-68de25/) - Export audio tracks separately
- [Partial Export](https://img.ly/docs/cesdk/android/export-save-publish/export/partial-export-89aaf6/) - Learn how to export specific blocks, groups, and page elements instead of entire scenes using CE.SDK's programmatic export API.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To PDF"
description: "Export designs as PDF documents with high compatibility mode and underlayer support for special media printing."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [To PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/)
---
```kotlin file=@cesdk_android_examples/engine-guides-underlayer/Underlayer.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
import java.nio.ByteBuffer
suspend fun underlayer(engine: Engine): List {
// Demo scaffolding: create renderable content for the export snippets. In
// an app, start from the scene already loaded in the editor.
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Star))
engine.block.setPositionX(block, value = 350F)
engine.block.setPositionY(block, value = 250F)
engine.block.setWidth(block, value = 100F)
engine.block.setHeight(block, value = 100F)
val fill = engine.block.createFill(FillType.Color)
engine.block.setFill(block, fill = fill)
engine.block.setFillSolidColor(block, color = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F))
engine.block.appendChild(parent = page, child = block)
val pdfData = engine.block.export(
block = scene,
mimeType = MimeType.PDF,
)
val defaultPdf = writePdfExport(fileName = "design-pages.pdf", buffer = pdfData)
val highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility = true)
val highCompatibilityData = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = highCompatibilityOptions,
)
val highCompatibilityPdf = writePdfExport(
fileName = "design-high-compatibility.pdf",
buffer = highCompatibilityData,
)
engine.editor.setSpotColor(
name = "UnderlayerWhite",
Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F),
)
val underlayerOptions = ExportOptions(
exportPdfWithHighCompatibility = true,
exportPdfWithUnderlayer = true,
underlayerSpotColorName = "UnderlayerWhite",
underlayerOffset = -2.0F,
)
val underlayerData = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = underlayerOptions,
)
val underlayerPdf = writePdfExport(fileName = "design-with-underlayer.pdf", buffer = underlayerData)
val a4Options = ExportOptions(targetWidth = 2480F, targetHeight = 3508F)
val a4Data = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = a4Options,
)
val a4Pdf = writePdfExport(fileName = "design-a4.pdf", buffer = a4Data)
return listOf(defaultPdf, highCompatibilityPdf, underlayerPdf, a4Pdf)
}
private suspend fun writePdfExport(
fileName: String,
buffer: ByteBuffer,
): File = withContext(Dispatchers.IO) {
val prefix = fileName.substringBeforeLast(".pdf")
val source = buffer.asReadOnlyBuffer()
File.createTempFile(prefix, ".pdf").apply {
outputStream().use { output ->
while (source.hasRemaining()) {
output.channel.write(source)
}
}
check(length() > 0L) { "PDF export was empty." }
}
}
```
Export your designs as PDF documents with high compatibility mode and underlayer support for special media printing.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-underlayer)
PDF provides a document format for sharing and printing designs. CE.SDK exports PDF files that preserve vector graphics, support multi-page scenes, and include options for print compatibility. You can configure high compatibility mode for consistent rendering across PDF viewers, and generate underlayers for printing on transparent or non-white materials.
This guide covers exporting designs to PDF, configuring high compatibility mode, generating underlayers with spot colors, and controlling output dimensions.
## Export to PDF
Call `engine.block.export()` with `MimeType.PDF` to export a block as a PDF document. Pass the scene block from `engine.scene.get()` to include every page in a multi-page PDF, or pass one page from `engine.scene.getCurrentPage()` to export a single page.
```kotlin highlight-android-export-pdf
val pdfData = engine.block.export(
block = scene,
mimeType = MimeType.PDF,
)
val defaultPdf = writePdfExport(fileName = "design-pages.pdf", buffer = pdfData)
```
The Android API returns a `ByteBuffer` containing the PDF data. Write that buffer to app-specific storage, or hand the file to your app's share or upload flow.
```kotlin highlight-android-save-pdf
private suspend fun writePdfExport(
fileName: String,
buffer: ByteBuffer,
): File = withContext(Dispatchers.IO) {
val prefix = fileName.substringBeforeLast(".pdf")
val source = buffer.asReadOnlyBuffer()
File.createTempFile(prefix, ".pdf").apply {
outputStream().use { output ->
while (source.hasRemaining()) {
output.channel.write(source)
}
}
check(length() > 0L) { "PDF export was empty." }
}
}
```
## Configure High Compatibility Mode
Set `exportPdfWithHighCompatibility` on `ExportOptions` to rasterize complex elements like gradients with transparency at the scene's DPI. This produces more consistent output across PDF viewers, but it can increase file size because complex elements are converted to raster images.
```kotlin highlight-android-high-compatibility
val highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility = true)
val highCompatibilityData = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = highCompatibilityOptions,
)
val highCompatibilityPdf = writePdfExport(
fileName = "design-high-compatibility.pdf",
buffer = highCompatibilityData,
)
```
Use high compatibility mode when:
- Designs contain gradients with transparency
- Effects or blend modes render inconsistently across viewers
- Compatibility matters more than keeping every element vector-based
The flag defaults to `true`; set it explicitly when you want the export configuration to be visible in your code.
## Generate Underlayers for Special Media
Underlayers provide a base ink layer, typically white, for printing on transparent or non-white substrates like fabric, glass, or acrylic. The underlayer sits behind your design elements and gives the print opacity on the target material.
> **Note:** Do not flatten the resulting PDF file when you need the underlayer separation.
> Flattening removes the separate underlayer shape behind your design.
### Define the Underlayer Spot Color
Before exporting, define a spot color that represents the underlayer ink. The RGB values provide a preview representation in PDF viewers; the spot color name must match the name expected by your print workflow.
```kotlin highlight-android-spot-color
engine.editor.setSpotColor(
name = "UnderlayerWhite",
Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F),
)
```
### Export with Underlayer Options
Enable `exportPdfWithUnderlayer`, then pass the same spot color name in `underlayerSpotColorName`. Use `underlayerOffset` to adjust the underlayer size in design units. Negative values shrink the underlayer inward, which helps prevent visible edges from print misalignment.
```kotlin highlight-android-underlayer
val underlayerOptions = ExportOptions(
exportPdfWithHighCompatibility = true,
exportPdfWithUnderlayer = true,
underlayerSpotColorName = "UnderlayerWhite",
underlayerOffset = -2.0F,
)
val underlayerData = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = underlayerOptions,
)
val underlayerPdf = writePdfExport(fileName = "design-with-underlayer.pdf", buffer = underlayerData)
```
The underlayer is generated from the contours of visible design elements on the exported page. Elements with transparency produce a proportionally lighter underlayer.
## Export at Target Dimensions
Use `targetWidth` and `targetHeight` on `ExportOptions` to control the exported PDF dimensions in pixels. The block renders large enough to fill the target size while maintaining its aspect ratio.
```kotlin highlight-android-target-size
val a4Options = ExportOptions(targetWidth = 2480F, targetHeight = 3508F)
val a4Data = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = a4Options,
)
val a4Pdf = writePdfExport(fileName = "design-a4.pdf", buffer = a4Data)
```
For print output, calculate the target dimensions from your desired DPI:
- A4 at 300 DPI: 2480 x 3508 pixels
- Letter at 300 DPI: 2550 x 3300 pixels
## PDF Export Options
`mimeType` is the second argument to `engine.block.export()`. The remaining fields below are the PDF-related `ExportOptions` properties used by this guide and the underlayer controls available on Android.
| Option | Description |
| ------ | ----------- |
| `mimeType` | Output format. Pass `MimeType.PDF`. |
| `exportPdfWithHighCompatibility` | Rasterize complex elements at scene DPI for consistent rendering. Defaults to `true`. |
| `exportPdfWithUnderlayer` | Generate an underlayer from design contours. Defaults to `false`. |
| `underlayerSpotColorName` | Spot color name for the underlayer ink. Required when `exportPdfWithUnderlayer` is `true`. |
| `underlayerOffset` | Size adjustment in design units. Negative values shrink the underlayer inward. |
| `underlayerRenderRatio` | Resolution multiplier for the raster pass that extracts the underlayer contour. Higher values can preserve small details at higher memory cost. Defaults to `1.0`. |
| `underlayerMaxError` | Maximum curve-fit error in pixels when vectorizing the underlayer contour. Smaller values fit tighter outlines with more path complexity. Defaults to `2.0`. |
| `targetWidth` | Target output width in pixels. Must be used with `targetHeight`. |
| `targetHeight` | Target output height in pixels. Must be used with `targetWidth`. |
## API Reference
| Method | Description |
| ------ | ----------- |
| `engine.block.export(block=_, mimeType=MimeType.PDF, options=_)` | Export a block as PDF with format and compatibility options. |
| `engine.editor.setSpotColor(name=_, color=Color.fromRGBA(r=_, g=_, b=_, a=_))` | Define or update the RGB approximation of a spot color. |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create the RGB preview color for the underlayer spot color. |
| `engine.scene.get()` | Get the scene block for a multi-page PDF export. |
| `engine.scene.getCurrentPage()` | Get the current page for a single-page PDF export. |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Compare all supported export formats
- [Export for Printing](https://img.ly/docs/cesdk/android/export-save-publish/for-printing-bca896/) — Export designs from CE.SDK as print-ready PDFs with professional output options including high compatibility mode, underlayers for special media, and scene DPI configuration.
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) — Define and use spot colors in designs
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "To PNG"
description: "Export CE.SDK designs as PNG images with lossless compression, alpha support, and configurable output dimensions."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [To PNG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/)
---
```kotlin file=@cesdk_android_examples/engine-guides-export-to-png/ToPng.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
data class ToPngResult(
val pngData: ByteBuffer,
val compressedPngData: ByteBuffer,
val sizedPngData: ByteBuffer,
val savedPngFile: File,
)
suspend fun toPng(
engine: Engine,
outputFile: File,
): ToPngResult {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
val accent = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(accent, "PNG export sample")
engine.block.setShape(accent, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(accent, value = 520F)
engine.block.setHeight(accent, value = 520F)
engine.block.setPositionX(accent, value = 380F)
engine.block.setPositionY(accent, value = 70F)
engine.block.setFill(accent, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = accent,
color = Color.fromRGBA(r = 0.14F, g = 0.34F, b = 0.84F, a = 0.86F),
)
engine.block.appendChild(parent = page, child = accent)
val overlay = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(overlay, "PNG export overlay")
engine.block.setShape(overlay, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(overlay, value = 360F)
engine.block.setHeight(overlay, value = 260F)
engine.block.setPositionX(overlay, value = 500F)
engine.block.setPositionY(overlay, value = 230F)
engine.block.setFill(overlay, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = overlay,
color = Color.fromRGBA(r = 0.96F, g = 0.36F, b = 0.18F, a = 0.78F),
)
engine.block.appendChild(parent = page, child = overlay)
val highlight = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(highlight, "PNG export highlight")
engine.block.setShape(highlight, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(highlight, value = 180F)
engine.block.setHeight(highlight, value = 180F)
engine.block.setPositionX(highlight, value = 418F)
engine.block.setPositionY(highlight, value = 182F)
engine.block.setFill(highlight, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = highlight,
color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.9F),
)
engine.block.appendChild(parent = page, child = highlight)
val pngData = exportToPngImage(
engine = engine,
page = page,
onPreExport = { waitForExportEngineStartup() },
)
val compressedPngData = exportPngWithCompression(
engine = engine,
page = page,
onPreExport = { waitForExportEngineStartup() },
)
val sizedPngData = exportPngWithTargetDimensions(
engine = engine,
page = page,
onPreExport = { waitForExportEngineStartup() },
)
val savedPngFile = savePngExportToFile(pngData, outputFile)
return ToPngResult(
pngData = pngData.asReadOnlyBuffer(),
compressedPngData = compressedPngData.asReadOnlyBuffer(),
sizedPngData = sizedPngData.asReadOnlyBuffer(),
savedPngFile = savedPngFile,
)
}
suspend fun exportToPngImage(
engine: Engine,
page: DesignBlock,
onPreExport: (suspend Engine.() -> Unit)? = null,
): ByteBuffer {
val pngData = if (onPreExport == null) {
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
pngData
} else {
engine.block.export(
block = page,
mimeType = MimeType.PNG,
onPreExport = onPreExport,
)
}
check(pngData.hasRemaining()) { "PNG export is empty" }
return pngData
}
suspend fun exportPngWithCompression(
engine: Engine,
page: DesignBlock,
onPreExport: (suspend Engine.() -> Unit)? = null,
): ByteBuffer {
val pngData = if (onPreExport == null) {
val options = ExportOptions(pngCompressionLevel = 9)
val compressedPngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
compressedPngData
} else {
val options = ExportOptions(pngCompressionLevel = 9)
engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
onPreExport = onPreExport,
)
}
check(pngData.hasRemaining()) { "compressed PNG export is empty" }
return pngData
}
suspend fun exportPngWithTargetDimensions(
engine: Engine,
page: DesignBlock,
onPreExport: (suspend Engine.() -> Unit)? = null,
): ByteBuffer {
val pngData = if (onPreExport == null) {
val options = ExportOptions(
targetWidth = 1920F,
targetHeight = 1080F,
)
val sizedPngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
sizedPngData
} else {
val options = ExportOptions(
targetWidth = 1920F,
targetHeight = 1080F,
)
engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
onPreExport = onPreExport,
)
}
check(pngData.hasRemaining()) { "target-size PNG export is empty" }
return pngData
}
// Source-only smoke-test synchronization for Android export worker startup.
// The rendered snippets hide this hook; the runtime gate uses it so the export
// worker settings observer cannot race its background Engine.stop().
private suspend fun waitForExportEngineStartup() {
yield()
}
suspend fun savePngExportToFile(
pngData: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
FileOutputStream(outputFile).channel.use { channel ->
val readableData = pngData.asReadOnlyBuffer()
while (readableData.hasRemaining()) {
channel.write(readableData)
}
}
check(outputFile.length() > 0L) { "saved PNG file is empty" }
outputFile
}
```
Export CE.SDK designs as PNG images with lossless compression, alpha support, and configurable output dimensions.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-to-png)
PNG (Portable Network Graphics) preserves transparency and uses lossless compression. It works well for graphics, UI elements, icons, logos, and other designs that need crisp edges or alpha channels.
This guide covers exporting a block to PNG, configuring compression, controlling output dimensions, and writing the exported bytes to app storage.
## Export to PNG
Use `engine.block.export(...)` with `MimeType.PNG` to export a page, scene, group, or any other design block. The method returns a `ByteBuffer` containing the encoded PNG data.
```kotlin highlight-android-export-png
val pngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
)
```
Pass the page block when you want to export the current page, or pass another block ID to export a specific element.
## Export Options
PNG export supports options for compression, dimensions, and text rendering. Pass an `ExportOptions` instance when you need behavior other than the defaults.
### Compression Level
Set `pngCompressionLevel` from `0` to `9` to control the file size versus encoding speed trade-off. Higher values usually produce smaller files but take longer to encode. PNG remains lossless, so compression level does not change image quality.
```kotlin highlight-android-compression-level
val options = ExportOptions(pngCompressionLevel = 9)
val compressedPngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
```
- `0` - No compression, fastest encoding
- `5` - Balanced default
- `9` - Maximum compression, slowest encoding
### Target Dimensions
Set `targetWidth` and `targetHeight` together to request a target size. CE.SDK preserves the block's aspect ratio, scales the block until it fills the requested target, and derives the PNG dimensions from that scaled block. If the target aspect ratio differs from the block aspect ratio, one exported dimension can be larger than the requested value.
```kotlin highlight-android-target-dimensions
val options = ExportOptions(
targetWidth = 1920F,
targetHeight = 1080F,
)
val sizedPngData = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = options,
)
```
Leave both values unset when you want to export the block at its native size.
### All PNG Export Options
| Option | Description |
| --- | --- |
| `pngCompressionLevel` | Compression level from `0` to `9`. Higher values produce smaller files but take longer. Defaults to `5`. |
| `targetWidth` | Target output width in pixels. Use it together with `targetHeight`. |
| `targetHeight` | Target output height in pixels. Use it together with `targetWidth`. |
| `allowTextOverhang` | When `true`, text blocks export with glyphs that extend beyond their frame still visible. Defaults to `false`. |
## Save to App Storage
After export, write the returned `ByteBuffer` to a file in your app-specific storage or pass it to your upload pipeline. The sample duplicates the buffer before writing so the original export data remains readable.
```kotlin highlight-android-save-file
suspend fun savePngExportToFile(
pngData: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
FileOutputStream(outputFile).channel.use { channel ->
val readableData = pngData.asReadOnlyBuffer()
while (readableData.hasRemaining()) {
channel.write(readableData)
}
}
check(outputFile.length() > 0L) { "saved PNG file is empty" }
outputFile
}
```
Use an Android app-owned directory such as `context.cacheDir` or `context.filesDir` for local exports, then share or upload the file according to your app's storage policy.
## When to Use PNG
PNG is a good fit for:
- Graphics with sharp edges, text, and UI elements
- Designs that require transparent areas
- Logos, icons, and illustrations where lossless output matters
For photographs or images with smooth gradients, JPEG usually produces smaller files.
## Troubleshooting
| Issue | Fix |
| --- | --- |
| File size is too large | Increase `pngCompressionLevel` toward `9`, or reduce dimensions with `targetWidth` and `targetHeight`. |
| Encoding feels slow | Lower `pngCompressionLevel` toward `0`. The default `5` balances size and encoding speed. |
| Output dimensions are unexpected | Check the block aspect ratio against `targetWidth` and `targetHeight`. CE.SDK preserves the block aspect ratio and can export one dimension larger than the requested target when the ratios differ. |
| Transparent areas appear filled | Check the exported page or block background. PNG preserves alpha only when the source content is transparent. |
## API Reference
| API | Description |
| --- | --- |
| `engine.block.export(block=_, mimeType=MimeType.PNG, options=_)` | Export a block as PNG and return the encoded data as a `ByteBuffer`. |
| `ExportOptions(pngCompressionLevel=_, targetWidth=_, targetHeight=_, allowTextOverhang=_)` | Configure PNG compression, output dimensions, and text-overhang handling. |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Compare all supported export formats
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Export to Raw Data"
description: "Export CE.SDK designs to uncompressed RGBA pixel data for custom image processing, GPU uploads, and advanced graphics workflows on Android."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/to-raw-data-abd7da/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [To Raw Data](https://img.ly/docs/cesdk/android/export-save-publish/export/to-raw-data-abd7da/)
---
Export CE.SDK designs to raw RGBA pixel data when your Android app needs
direct access to bytes for custom image processing, GPU uploads, or advanced
graphics pipelines.
> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-to-raw-data)
```kotlin file=@cesdk_android_examples/engine-guides-export-to-raw-data/ToRawData.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import kotlin.math.ceil
import kotlin.math.max
data class ToRawDataResult(
val width: Int,
val height: Int,
val maxExportSize: Int,
val rawByteCount: Int,
val centerPixel: RgbaPixel,
val rawFile: File,
val thumbnailByteCount: Int,
)
suspend fun toRawData(
engine: Engine,
outputDir: File,
): ToRawDataResult {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 640F)
engine.block.setHeight(page, value = 360F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, "Raw data background")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 640F)
engine.block.setHeight(background, value = 360F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = background,
color = Color.fromHex("#FF101827"),
)
engine.block.appendChild(parent = page, child = background)
// The centered panel gives the smoke test a deterministic raw pixel value.
val panel = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(panel, "Raw data sample panel")
engine.block.setShape(panel, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(panel, value = 360F)
engine.block.setHeight(panel, value = 200F)
engine.block.setPositionX(panel, value = 140F)
engine.block.setPositionY(panel, value = 80F)
engine.block.setFill(panel, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = panel,
color = Color.fromHex("#FFFF0000"),
)
engine.block.appendChild(parent = page, child = panel)
val width = 1920
val height = 1080
val maxExportSize = ensureRawExportFits(engine, page, width, height)
val rawData = exportRawData(
engine = engine,
page = page,
width = width,
height = height,
onPreExport = {
waitForBackgroundExportEngine()
},
)
val centerPixel = readPixel(
rawData = rawData,
width = width,
x = width / 2,
y = height / 2,
)
val rawFile = saveRawDataFile(
rawData = rawData,
outputFile = File(outputDir, "design.rgba"),
)
val thumbnailData = exportRawThumbnail(
engine = engine,
page = page,
onPreExport = {
waitForBackgroundExportEngine()
},
)
return ToRawDataResult(
width = width,
height = height,
maxExportSize = maxExportSize,
rawByteCount = rawData.remaining(),
centerPixel = centerPixel,
rawFile = rawFile,
thumbnailByteCount = thumbnailData.remaining(),
)
}
suspend fun exportRawData(
engine: Engine,
page: DesignBlock,
width: Int,
height: Int,
onPreExport: suspend Engine.() -> Unit = {},
): ByteBuffer {
val options = ExportOptions(
targetWidth = width.toFloat(),
targetHeight = height.toFloat(),
)
val rawData = engine.block.export(
block = page,
mimeType = MimeType.BINARY,
options = options,
onPreExport = onPreExport,
)
check(rawData.remaining() % 4 == 0) {
"Expected complete RGBA pixels, got ${rawData.remaining()} bytes"
}
return rawData
}
data class RgbaPixel(
val red: Int,
val green: Int,
val blue: Int,
val alpha: Int,
)
fun readPixel(
rawData: ByteBuffer,
width: Int,
x: Int,
y: Int,
): RgbaPixel {
check(width > 0) {
"Raw data width must be greater than zero"
}
val bytesPerPixel = 4
val rowByteCount = width * bytesPerPixel
val byteCount = rawData.limit()
check(byteCount % rowByteCount == 0) {
"Raw data buffer does not contain complete rows for width $width"
}
val height = byteCount / rowByteCount
check(x in 0 until width && y in 0 until height) {
"Pixel coordinate ($x, $y) is outside the ${width}x$height raw data buffer"
}
val index = (y * width + x) * 4
val buffer = rawData.asReadOnlyBuffer()
return RgbaPixel(
red = buffer.get(index).toInt() and 0xFF,
green = buffer.get(index + 1).toInt() and 0xFF,
blue = buffer.get(index + 2).toInt() and 0xFF,
alpha = buffer.get(index + 3).toInt() and 0xFF,
)
}
suspend fun saveRawDataFile(
rawData: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableData = rawData.asReadOnlyBuffer()
val expectedByteCount = readableData.remaining().toLong()
FileOutputStream(outputFile).channel.use { channel ->
while (readableData.hasRemaining()) {
channel.write(readableData)
}
}
check(outputFile.length() == expectedByteCount) {
"Saved raw data file has ${outputFile.length()} bytes, expected $expectedByteCount"
}
outputFile
}
suspend fun exportRawThumbnail(
engine: Engine,
page: DesignBlock,
onPreExport: suspend Engine.() -> Unit = {},
): ByteBuffer {
val width = 960
val height = 540
val options = ExportOptions(
targetWidth = width.toFloat(),
targetHeight = height.toFloat(),
)
val rawData = engine.block.export(
block = page,
mimeType = MimeType.BINARY,
options = options,
onPreExport = onPreExport,
)
check(rawData.remaining() % 4 == 0) {
"Expected complete RGBA pixels, got ${rawData.remaining()} bytes"
}
return rawData
}
fun ensureRawExportFits(
engine: Engine,
block: DesignBlock,
width: Int,
height: Int,
): Int {
check(width > 0 && height > 0) {
"Requested raw export size must be greater than zero"
}
val blockWidth = engine.block.getWidth(block)
val blockHeight = engine.block.getHeight(block)
check(blockWidth > 0F && blockHeight > 0F) {
"Raw export block size must be greater than zero"
}
val scale = max(width.toFloat() / blockWidth, height.toFloat() / blockHeight)
val filledWidth = ceil(blockWidth * scale).toInt()
val filledHeight = ceil(blockHeight * scale).toInt()
val maxExportSize = engine.editor.getMaxExportSize()
check(filledWidth <= maxExportSize && filledHeight <= maxExportSize) {
"Requested raw export size ${width}x$height fills to ${filledWidth}x$filledHeight, exceeding the $maxExportSize px limit"
}
return maxExportSize
}
private suspend fun waitForBackgroundExportEngine() {
// Raw data exports finish quickly enough that the Android background export engine
// can otherwise stop before its startup settings stream has registered.
yield()
}
```
This guide covers exporting raw RGBA bytes, reading individual pixels, saving the byte stream, and controlling the output resolution.
## When to Use Raw Data Export
Raw pixel data export gives you direct access to uncompressed RGBA bytes from CE.SDK, with complete control over individual pixels for custom processing.
Reach for raw data when you need pixel-level access for custom algorithms, GPU texture uploads, machine-learning inputs, or intermediate processing. For standard image delivery, use PNG or JPEG instead because those formats are compressed and ready to display, store, or transfer.
## Understanding Raw Data Format
When you export with `MimeType.BINARY`, CE.SDK returns a `ByteBuffer` containing uncompressed RGBA8888 pixel data:
- **4 bytes per pixel** representing Red, Green, Blue, and Alpha channels
- **Values from 0-255** for each channel
- **Row-major order** with pixels arranged left-to-right, top-to-bottom
- **Total size** equal to width × height × 4 bytes
The RGB channels are premultiplied by alpha. For translucent pixels, unpremultiply each color channel before passing the bytes to APIs that expect straight RGBA values.
## How to Export Raw Data
Export a block as raw pixel data by calling `engine.block.export(...)` with `MimeType.BINARY`. Pair the call with `targetWidth` and `targetHeight` on `ExportOptions` when your downstream pipeline needs bounded output dimensions. The optional `onPreExport` hook is available when your app needs to configure the temporary export engine before rendering.
The sample page uses the same 16:9 aspect ratio as its 1920x1080 export target, so the returned raw buffer is exactly `width × height × 4` bytes. For other aspect ratios, CE.SDK preserves the block aspect ratio while scaling enough to fill the requested target size; validate the returned buffer size before using fixed coordinates.
```kotlin highlight-android-export
suspend fun exportRawData(
engine: Engine,
page: DesignBlock,
width: Int,
height: Int,
onPreExport: suspend Engine.() -> Unit = {},
): ByteBuffer {
val options = ExportOptions(
targetWidth = width.toFloat(),
targetHeight = height.toFloat(),
)
val rawData = engine.block.export(
block = page,
mimeType = MimeType.BINARY,
options = options,
onPreExport = onPreExport,
)
check(rawData.remaining() % 4 == 0) {
"Expected complete RGBA pixels, got ${rawData.remaining()} bytes"
}
return rawData
}
```
The returned `ByteBuffer` contains the RGBA bytes directly. The rest of this guide reads and stores that buffer without running it through PNG or JPEG encoding first.
## Download Exported Data
Once you have the raw bytes, you can inspect them directly, write them to app storage, or pass them to your own graphics pipeline.
### Read Individual Pixels
Index into the buffer at `(y * width + x) * 4`. The four bytes at that offset are Red, Green, Blue, and Alpha respectively, which is useful for color sampling, brightness analysis, custom filters, or passing pixels to another native layer.
```kotlin highlight-android-read-pixel
data class RgbaPixel(
val red: Int,
val green: Int,
val blue: Int,
val alpha: Int,
)
fun readPixel(
rawData: ByteBuffer,
width: Int,
x: Int,
y: Int,
): RgbaPixel {
check(width > 0) {
"Raw data width must be greater than zero"
}
val bytesPerPixel = 4
val rowByteCount = width * bytesPerPixel
val byteCount = rawData.limit()
check(byteCount % rowByteCount == 0) {
"Raw data buffer does not contain complete rows for width $width"
}
val height = byteCount / rowByteCount
check(x in 0 until width && y in 0 until height) {
"Pixel coordinate ($x, $y) is outside the ${width}x$height raw data buffer"
}
val index = (y * width + x) * 4
val buffer = rawData.asReadOnlyBuffer()
return RgbaPixel(
red = buffer.get(index).toInt() and 0xFF,
green = buffer.get(index + 1).toInt() and 0xFF,
blue = buffer.get(index + 2).toInt() and 0xFF,
alpha = buffer.get(index + 3).toInt() and 0xFF,
)
}
```
### Write Raw Bytes
Write the `ByteBuffer` to a file when another Android component, native module, or backend upload expects the raw RGBA stream. Use a duplicate or read-only view before writing so the caller can still inspect the original buffer position afterward.
```kotlin highlight-android-save-file
suspend fun saveRawDataFile(
rawData: ByteBuffer,
outputFile: File,
): File = withContext(Dispatchers.IO) {
outputFile.parentFile?.mkdirs()
val readableData = rawData.asReadOnlyBuffer()
val expectedByteCount = readableData.remaining().toLong()
FileOutputStream(outputFile).channel.use { channel ->
while (readableData.hasRemaining()) {
channel.write(readableData)
}
}
check(outputFile.length() == expectedByteCount) {
"Saved raw data file has ${outputFile.length()} bytes, expected $expectedByteCount"
}
outputFile
}
```
## Performance Considerations
Raw RGBA data grows linearly with pixel count: a 1920x1080 export consumes about 8.3 MB, compared with a much smaller compressed PNG or JPEG for many designs. Two engine settings help keep memory usage bounded.
### Reduce the Export Resolution
Pass `targetWidth` and `targetHeight` on `ExportOptions` to reduce the export scale while preserving the block's aspect ratio. Both fields default to `null`, which leaves the block's own size in control.
```kotlin highlight-android-target-size
suspend fun exportRawThumbnail(
engine: Engine,
page: DesignBlock,
onPreExport: suspend Engine.() -> Unit = {},
): ByteBuffer {
val width = 960
val height = 540
val options = ExportOptions(
targetWidth = width.toFloat(),
targetHeight = height.toFloat(),
)
val rawData = engine.block.export(
block = page,
mimeType = MimeType.BINARY,
options = options,
onPreExport = onPreExport,
)
check(rawData.remaining() % 4 == 0) {
"Expected complete RGBA pixels, got ${rawData.remaining()} bytes"
}
return rawData
}
```
### Check Export Size Limits
Before exporting very large blocks, query the maximum supported export dimension. `getMaxExportSize()` returns the side-length cap in pixels for either width or height. When the target and block aspect ratios differ, check the filled output size that CE.SDK renders, not only the requested target box.
```kotlin highlight-android-check-limits
fun ensureRawExportFits(
engine: Engine,
block: DesignBlock,
width: Int,
height: Int,
): Int {
check(width > 0 && height > 0) {
"Requested raw export size must be greater than zero"
}
val blockWidth = engine.block.getWidth(block)
val blockHeight = engine.block.getHeight(block)
check(blockWidth > 0F && blockHeight > 0F) {
"Raw export block size must be greater than zero"
}
val scale = max(width.toFloat() / blockWidth, height.toFloat() / blockHeight)
val filledWidth = ceil(blockWidth * scale).toInt()
val filledHeight = ceil(blockHeight * scale).toInt()
val maxExportSize = engine.editor.getMaxExportSize()
check(filledWidth <= maxExportSize && filledHeight <= maxExportSize) {
"Requested raw export size ${width}x$height fills to ${filledWidth}x$filledHeight, exceeding the $maxExportSize px limit"
}
return maxExportSize
}
```
### When to Use Raw vs. Compressed
- **Use raw data** when you need custom post-processing on CE.SDK exports before delivery
- **Use raw data** for intermediate steps in multi-stage pipelines or GPU uploads that would otherwise require decoding a PNG first
- **Use PNG or JPEG** when the output goes straight to disk, a user, or a network transfer
- **Validate the returned byte count** when another system expects fixed buffer dimensions
## API Reference
| API | Description |
| --- | --- |
| `engine.block.export(block=_, mimeType=MimeType.BINARY, options=_, onPreExport=_)` | Exports a block as raw RGBA data in a `ByteBuffer` |
| `engine.block.getWidth(block=_)` | Returns the block width used to predict filled raw export dimensions |
| `engine.block.getHeight(block=_)` | Returns the block height used to predict filled raw export dimensions |
| `ExportOptions(targetWidth=_, targetHeight=_)` | Controls the rendered output dimensions for the raw buffer |
| `engine.editor.getMaxExportSize()` | Returns the maximum supported export side length in pixels |
| `ByteBuffer.remaining()` | Returns the number of bytes available from the current position to the limit |
| `ByteBuffer.get(index=_)` | Reads one byte at an absolute buffer index without changing the current position |
| `ByteBuffer.limit()` | Returns the upper bound used for absolute index validation |
| `ByteBuffer.asReadOnlyBuffer()` | Creates a duplicate view for reading or writing without mutating the original buffer position |
### Related Types
| Type | Purpose |
| --- | --- |
| `MimeType.BINARY` | Android enum value for `application/octet-stream` raw RGBA export |
| `RgbaPixel` | Example value object that stores one pixel as Red, Green, Blue, and Alpha channel values |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Compare all available export formats
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) — Vector export for print and document workflows
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Export with a Color Mask"
description: "Export CE.SDK design blocks on Android with a color mask to isolate exact opaque color matches in a separate mask image."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/export/with-color-mask-4f868f/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [With a Color Mask](https://img.ly/docs/cesdk/android/export-save-publish/export/with-color-mask-4f868f/)
---
```kotlin file=@cesdk_android_examples/engine-guides-export-with-color-mask/ExportWithColorMask.kt reference-only
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
import java.io.File
import java.nio.ByteBuffer
suspend fun exportWithColorMask(engine: Engine): Pair {
val page = createColorMaskScene(engine)
return exportPageWithColorMask(engine, page)
}
suspend fun exportPageWithColorMask(
engine: Engine,
page: DesignBlock,
): Pair {
val maskColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F)
val options = ExportOptions(
pngCompressionLevel = 9,
targetWidth = 800F,
targetHeight = 600F,
)
val (maskedImage, maskImage) = engine.block.exportWithColorMask(
block = page,
mimeType = MimeType.PNG,
maskColor = maskColor,
options = options,
)
check(maskedImage.hasRemaining()) { "Masked image export is empty" }
check(maskImage.hasRemaining()) { "Mask image export is empty" }
return maskedImage to maskImage
}
suspend fun saveColorMaskFiles(
maskedImage: ByteBuffer,
maskImage: ByteBuffer,
outputDir: File,
): Pair = withContext(Dispatchers.IO) {
outputDir.mkdirs()
val maskedImageFile = File(outputDir, "color-mask-image.png")
val maskImageFile = File(outputDir, "color-mask-mask.png")
fun writeBuffer(
buffer: ByteBuffer,
outputFile: File,
) {
outputFile.outputStream().channel.use { channel ->
val readableBuffer = buffer.asReadOnlyBuffer()
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
}
writeBuffer(maskedImage, maskedImageFile)
writeBuffer(maskImage, maskImageFile)
maskedImageFile to maskImageFile
}
private fun createColorMaskScene(engine: Engine): DesignBlock {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
fun addRectangle(
name: String,
x: Float,
y: Float,
width: Float,
height: Float,
color: RGBAColor,
) {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(block, name = name)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = block, color = color)
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = width)
engine.block.setHeight(block, value = height)
engine.block.appendChild(parent = page, child = block)
}
addRectangle(
name = "Color mask background",
x = 0F,
y = 0F,
width = 800F,
height = 600F,
color = Color.fromHex("#FFF8FAFC"),
)
addRectangle(
name = "Print content",
x = 170F,
y = 145F,
width = 460F,
height = 310F,
color = Color.fromHex("#FF2457D6"),
)
val redMaskColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F)
listOf(
48F to 48F,
704F to 48F,
48F to 504F,
704F to 504F,
).forEachIndexed { index, (x, y) ->
addRectangle(
name = "Registration mark ${index + 1}",
x = x,
y = y,
width = 48F,
height = 48F,
color = redMaskColor,
)
}
return page
}
```
Isolate specific opaque colors from Android image exports and generate a
matching mask image for print and compositing workflows.

> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-with-color-mask)
CE.SDK can render a second export pass for pixels that match a chosen opaque color. Android returns a `Pair`: the first buffer contains the image result, and the second buffer contains the mask image.
## Exporting with Color Masks
Call `engine.block.exportWithColorMask(...)` with the page or block you want to export, the output MIME type, a mask color, and optional `ExportOptions`. This example isolates fully opaque red registration marks from a PNG export.
```kotlin highlight-android-export-with-color-mask
suspend fun exportPageWithColorMask(
engine: Engine,
page: DesignBlock,
): Pair {
val maskColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F)
val options = ExportOptions(
pngCompressionLevel = 9,
targetWidth = 800F,
targetHeight = 600F,
)
val (maskedImage, maskImage) = engine.block.exportWithColorMask(
block = page,
mimeType = MimeType.PNG,
maskColor = maskColor,
options = options,
)
check(maskedImage.hasRemaining()) { "Masked image export is empty" }
check(maskImage.hasRemaining()) { "Mask image export is empty" }
return maskedImage to maskImage
}
```
Use `MimeType.PNG` for lossless image output and predictable mask pixels. The sample also sets `targetWidth` and `targetHeight` so the image result and mask image have predictable pixel dimensions.
> **Note:** Color matching is exact against the rendered RGBA value. Android uses a fully
> opaque mask color, so semi-transparent pixels with the same RGB components,
> anti-aliased edges, gradients, compressed source images, and near matches are
> not included in the mask.
### Specifying Color Values
`exportWithColorMask` takes an opaque `RGBAColor` as its mask color. Use the `Color.fromRGBA(...)` overload that matches the values your app already has:
- Normalized components: `Color.fromRGBA(r = 1F, g = 0F, b = 0F)`
- 8-bit components: `Color.fromRGBA(r = 255, g = 0, b = 0)`
When your scene uses CMYK process colors, create those source fills with `Color.fromCMYK(...)`. The mask color still needs the exact opaque RGB value the rendered export should match.
## Save the Export Files
Write each returned `ByteBuffer` separately on `Dispatchers.IO`. Use a read-only duplicate when writing if the same buffer will also be decoded, uploaded, or inspected later.
```kotlin highlight-android-save-color-mask-files
suspend fun saveColorMaskFiles(
maskedImage: ByteBuffer,
maskImage: ByteBuffer,
outputDir: File,
): Pair = withContext(Dispatchers.IO) {
outputDir.mkdirs()
val maskedImageFile = File(outputDir, "color-mask-image.png")
val maskImageFile = File(outputDir, "color-mask-mask.png")
fun writeBuffer(
buffer: ByteBuffer,
outputFile: File,
) {
outputFile.outputStream().channel.use { channel ->
val readableBuffer = buffer.asReadOnlyBuffer()
while (readableBuffer.hasRemaining()) {
channel.write(readableBuffer)
}
}
}
writeBuffer(maskedImage, maskedImageFile)
writeBuffer(maskImage, maskImageFile)
maskedImageFile to maskImageFile
}
```
The first file is the image result. The second file marks matching pixels with the mask color and non-matching pixels as white, which is useful for print-service checks or external compositing.
## API Reference
| Method | Description |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `engine.block.exportWithColorMask(block=_, mimeType=_, maskColor=_, options=_)` | Exports a block with exact opaque color masking, returning image and mask buffers. |
| `Color.fromRGBA(r=_, g=_, b=_)` | Creates the fully opaque `RGBAColor` passed as the mask color. |
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` | Creates CMYK source fill colors when the design uses process colors. |
| `ExportOptions(pngCompressionLevel=_, targetWidth=_, targetHeight=_)` | Configures PNG compression and output dimensions for the export. |
| `engine.block.export(block=_, mimeType=_, options=_)` | Exports a block without color masking. |
## Next Steps
- [Export Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Explore every supported export format
and the options each one accepts.
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - Produce print-ready PDFs with optional
underlayers for spot-color workflows.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Export for Printing"
description: "Export designs from CE.SDK as print-ready PDFs with professional output options including high compatibility mode, underlayers for special media, and scene DPI configuration."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/for-printing-bca896/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [For Printing](https://img.ly/docs/cesdk/android/export-save-publish/for-printing-bca896/)
---
```kotlin file=@cesdk_android_examples/engine-guides-export-for-printing/ExportForPrinting.kt reference-only
import kotlinx.coroutines.delay
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
suspend fun exportForPrinting(
engine: Engine,
synchronizeHeadlessSmokeExport: Boolean = false,
): Map {
// Demo scaffolding: build a small scene with one renderable graphic so the
// PDF exports below produce a non-empty page. In your app you would start
// from a scene that the editor has already loaded.
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 1131.6F)
engine.block.appendChild(parent = scene, child = page)
val star = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(star, shape = engine.block.createShape(ShapeType.Star))
engine.block.setPositionX(star, value = 250F)
engine.block.setPositionY(star, value = 415.8F)
engine.block.setWidth(star, value = 300F)
engine.block.setHeight(star, value = 300F)
engine.block.setFill(star, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = star,
color = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F),
)
engine.block.appendChild(parent = page, child = star)
// 300 DPI is standard for high-quality print output.
engine.block.setFloat(scene, property = "scene/dpi", value = 300F)
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
val highCompatibilityPdf = exportHighCompatibilityPdf(
engine = engine,
page = page,
)
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
val standardPdf = exportStandardPdf(
engine = engine,
page = page,
)
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
// Define the spot color that represents the underlayer ink before export.
// The RGB values are a preview; print software uses the spot color name.
val underlayerSpotColorName = "UnderlayerWhite"
engine.editor.setSpotColor(
name = underlayerSpotColorName,
color = Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F, a = 1F),
)
// Keep the settings stream synchronized before the headless PDF exports.
engine.editor.getSpotColorRGB(underlayerSpotColorName)
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
val underlayerPdf = exportUnderlayerPdf(
engine = engine,
page = page,
underlayerSpotColorName = underlayerSpotColorName,
)
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
val sizedPdf = exportTargetSizePdf(
engine = engine,
page = page,
)
if (synchronizeHeadlessSmokeExport) synchronizeHeadlessExport()
return mapOf(
"highCompatibility" to highCompatibilityPdf,
"standard" to standardPdf,
"underlayer" to underlayerPdf,
"targetSize" to sizedPdf,
)
}
private suspend fun exportHighCompatibilityPdf(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
// High compatibility mode rasterizes complex elements like gradients with
// transparency at the scene's DPI so they render consistently across PDF
// viewers and print RIPs.
val highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility = true)
val highCompatibilityPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = highCompatibilityOptions,
).also { highCompatibilityPdf ->
check(highCompatibilityPdf.hasRemaining()) { "High compatibility PDF export is empty" }
}
check(highCompatibilityPdf.hasRemaining()) { "High compatibility PDF export is empty" }
return highCompatibilityPdf
}
private suspend fun exportStandardPdf(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
// Disabling high compatibility keeps complex elements as vectors. The export
// is faster and the PDF is smaller, but rendering may differ across viewers.
val standardOptions = ExportOptions(exportPdfWithHighCompatibility = false)
val standardPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = standardOptions,
).also { standardPdf ->
check(standardPdf.hasRemaining()) { "Standard PDF export is empty" }
}
check(standardPdf.hasRemaining()) { "Standard PDF export is empty" }
return standardPdf
}
private suspend fun exportUnderlayerPdf(
engine: Engine,
page: DesignBlock,
underlayerSpotColorName: String,
): ByteBuffer {
// Generate an underlayer from the design contours filled with the spot color.
// A negative `underlayerOffset` shrinks the underlayer inward so misaligned
// print layers do not show visible white edges around design elements.
val underlayerOptions = ExportOptions(
exportPdfWithHighCompatibility = true,
exportPdfWithUnderlayer = true,
underlayerSpotColorName = underlayerSpotColorName,
underlayerOffset = -2F,
)
val underlayerPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = underlayerOptions,
).also { underlayerPdf ->
check(underlayerPdf.hasRemaining()) { "Underlayer PDF export is empty" }
}
check(underlayerPdf.hasRemaining()) { "Underlayer PDF export is empty" }
return underlayerPdf
}
private suspend fun exportTargetSizePdf(
engine: Engine,
page: DesignBlock,
): ByteBuffer {
// `targetWidth` / `targetHeight` are pixel dimensions. Combined with the
// scene DPI set above, they determine the physical print size: 2480 x 3508
// pixels at 300 DPI is A4 (210 x 297 mm).
val sizedOptions = ExportOptions(
targetWidth = 2480F,
targetHeight = 3508F,
exportPdfWithHighCompatibility = true,
)
val sizedPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = sizedOptions,
).also { sizedPdf ->
check(sizedPdf.hasRemaining()) { "Target-size PDF export is empty" }
}
check(sizedPdf.hasRemaining()) { "Target-size PDF export is empty" }
return sizedPdf
}
private suspend fun synchronizeHeadlessExport() {
// The isolated offscreen smoke test tears down the engine immediately after
// export, so give asynchronous export/settings callbacks time to drain.
delay(100)
}
```
Export print-ready PDFs from CE.SDK with options for high compatibility mode,
underlayers for special media like fabric or glass, and configurable output
resolution.
> **Reading time:** 10 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-for-printing)
CE.SDK exports designs as PDFs, but professional print workflows require specific configurations beyond standard export. This guide covers PDF export options for print, including high compatibility mode for complex designs, underlayers for printing on special media, and output resolution settings.
## Default PDF Color Behavior
CE.SDK's Android PDF export writes process colors through the standard PDF export path. Android `ExportOptions` does not expose native DeviceCMYK export, so direct CMYK color values are emitted with DeviceRGB output unless you post-process the PDF.
Named spot colors are different: CE.SDK can keep the named color as a spot separation in the PDF and include a DeviceRGB alternate color for preview and fallback rendering. This is the same mechanism used by underlayers below, where the spot color name identifies the separation and the RGB value is only the alternate preview color.
For ICC-profiled CMYK, PDF/X, or full prepress conversion, use the **Print Ready PDF plugin** in a Node.js or browser pipeline that consumes the PDF emitted from your Android app. The base `engine.block.export()` call provides the print compatibility, spot-color underlayer, and target-size options covered here, while ICC-profiled CMYK conversion is handled by the plugin.
## Setting Up for Print Export
Configure the scene DPI before exporting. The DPI controls how complex elements are rasterized when high compatibility mode is enabled, and 300 DPI is the standard for high-quality print output.
```kotlin highlight-android-dpi
// 300 DPI is standard for high-quality print output.
engine.block.setFloat(scene, property = "scene/dpi", value = 300F)
```
Set the DPI on the scene block, not on the page. Every export from that scene then uses this resolution as the rasterization target.
## PDF Export Options for Print
Export a page as PDF with `engine.block.export()`, passing `MimeType.PDF` and an `ExportOptions` value. The PDF-specific fields on `ExportOptions` control how complex artwork is rendered.
### High Compatibility Mode
`exportPdfWithHighCompatibility` defaults to `true` and rasterizes complex elements like gradients with transparency at the scene's DPI. Enable high compatibility when:
- Designs use gradients with transparency
- Effects or blend modes render inconsistently across PDF viewers
- Maximum compatibility across print RIPs matters more than vector precision
```kotlin highlight-android-high-compatibility
// High compatibility mode rasterizes complex elements like gradients with
// transparency at the scene's DPI so they render consistently across PDF
// viewers and print RIPs.
val highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility = true)
val highCompatibilityPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = highCompatibilityOptions,
).also { highCompatibilityPdf ->
check(highCompatibilityPdf.hasRemaining()) { "High compatibility PDF export is empty" }
}
```
The returned `ByteBuffer` is a PDF blob you can write to disk, upload to a print service, or hand to Android's sharing or storage APIs.
### Standard PDF Export
Disabling high compatibility keeps complex elements as vectors. The export is faster and the resulting PDF is smaller, but rendering may differ across viewers. Use this path when you target modern PDF viewers and prefer file size and speed over universal compatibility.
```kotlin highlight-android-standard-pdf
// Disabling high compatibility keeps complex elements as vectors. The export
// is faster and the PDF is smaller, but rendering may differ across viewers.
val standardOptions = ExportOptions(exportPdfWithHighCompatibility = false)
val standardPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = standardOptions,
).also { standardPdf ->
check(standardPdf.hasRemaining()) { "Standard PDF export is empty" }
}
```
## Underlayers for Special Media
Underlayers provide a base ink layer, typically white, for printing on:
- Transparent or non-white substrates
- DTF (Direct-to-Film) transfers
- Fabric, glass, or dark materials
The underlayer is generated from the design's contours and filled with a named spot color. Print software then renders the underlayer as a separate ink separation that the press lays down before the design colors.
### Define the Underlayer Spot Color
Before exporting with an underlayer, define the spot color that represents the underlayer ink. Use `engine.editor.setSpotColor()` with `Color.fromRGBA()` to create a named spot color with RGB preview values. The RGB triplet is the DeviceRGB alternate preview; print software uses the spot color name for the separation.
```kotlin highlight-android-define-spot-color
// Define the spot color that represents the underlayer ink before export.
// The RGB values are a preview; print software uses the spot color name.
val underlayerSpotColorName = "UnderlayerWhite"
engine.editor.setSpotColor(
name = underlayerSpotColorName,
color = Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F, a = 1F),
)
```
### Export with Underlayer
Set `exportPdfWithUnderlayer = true` and pass the spot color name as `underlayerSpotColorName`. The underlayer is generated from the design's contours and rendered as a fill of that named spot separation.
```kotlin highlight-android-export-with-underlayer
// Generate an underlayer from the design contours filled with the spot color.
// A negative `underlayerOffset` shrinks the underlayer inward so misaligned
// print layers do not show visible white edges around design elements.
val underlayerOptions = ExportOptions(
exportPdfWithHighCompatibility = true,
exportPdfWithUnderlayer = true,
underlayerSpotColorName = underlayerSpotColorName,
underlayerOffset = -2F,
)
val underlayerPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = underlayerOptions,
).also { underlayerPdf ->
check(underlayerPdf.hasRemaining()) { "Underlayer PDF export is empty" }
}
```
### Underlayer Offset
`underlayerOffset` adjusts the underlayer size in design units. Negative values shrink the underlayer inward, which prevents visible white edges when the print layers do not align perfectly. Start with values around `-1.0` to `-3.0` and tune based on your print equipment's alignment tolerance.
## Export with Target Size
Control the exported PDF dimensions with `targetWidth` and `targetHeight`. These values are in pixels and combine with the scene's DPI to determine the physical print size, for example 2480 x 3508 px at 300 DPI equals A4 (210 x 297 mm).
```kotlin highlight-android-target-size
// `targetWidth` / `targetHeight` are pixel dimensions. Combined with the
// scene DPI set above, they determine the physical print size: 2480 x 3508
// pixels at 300 DPI is A4 (210 x 297 mm).
val sizedOptions = ExportOptions(
targetWidth = 2480F,
targetHeight = 3508F,
exportPdfWithHighCompatibility = true,
)
val sizedPdf = engine.block.export(
block = page,
mimeType = MimeType.PDF,
options = sizedOptions,
).also { sizedPdf ->
check(sizedPdf.hasRemaining()) { "Target-size PDF export is empty" }
}
```
When only one of `targetWidth` or `targetHeight` is non-null, the engine scales the other axis to preserve the block's aspect ratio. When both are non-null and the aspect ratios differ, the output fills the target dimensions completely and may exceed one of them on the longer axis.
## CMYK PDFs with ICC Profiles
For CMYK color space and embedded ICC profiles, use the **Print Ready PDF plugin**. The plugin post-processes the PDF emitted by `engine.block.export()` and converts RGB to CMYK with embedded ICC profiles. The plugin is a JavaScript package, so wire it into a Node.js post-processing step or a browser-side pipeline that consumes the PDF produced by your Android app.
See the [Print Ready PDF Plugin](#broken-link-iroalu) for setup and usage.
## Troubleshooting
### PDF Not Opening Correctly in Print Software
Pass `exportPdfWithHighCompatibility = true` so complex elements are rasterized at the scene DPI. Some prepress tools are strict about gradients with transparency and rendering effects that the standard PDF path keeps as vectors.
### Underlayer Not Visible in PDF Viewer
Standard PDF viewers do not display spot color separations. Open the PDF in professional print software or your prepress tool to verify that the underlayer separation is present.
### Colors Look Different After Printing
Direct CMYK process colors use DeviceRGB output in Android's standard PDF export path. Run the exported PDF through the Print Ready PDF plugin with an appropriate ICC profile when accurate CMYK reproduction is required.
### White Edges on Special Media
Increase the negative `underlayerOffset` to shrink the underlayer further from design edges. Try values like `-2.0` or `-3.0` depending on your equipment's alignment tolerance.
## API Reference
| Method/Option | Purpose |
|---|---|
| `engine.block.export(block=_, mimeType=MimeType.PDF, options=_)` | Export a block as a PDF `ByteBuffer`; `MimeType.PDF` selects PDF output. |
| `ExportOptions(exportPdfWithHighCompatibility=_)` | Rasterize bitmap images and gradients at scene DPI; defaults to `true`. |
| `ExportOptions(exportPdfWithUnderlayer=_)` | Generate an underlayer from design contours; defaults to `false`. |
| `ExportOptions(underlayerSpotColorName=_)` | Spot color name for underlayer ink. |
| `ExportOptions(underlayerOffset=_)` | Size adjustment in design units; negative values shrink the underlayer. |
| `ExportOptions(targetWidth=_, targetHeight=_)` | Target dimensions for the exported PDF in pixels. |
| `engine.editor.setSpotColor(name=_, color=_)` | Define a named spot color with an alternate preview color. |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create the DeviceRGB alternate preview value used for the underlayer spot color. |
| `engine.block.setFloat(block=_, property="scene/dpi", value=_)` | Set scene DPI for print resolution. |
## Next Steps
- [Print Ready PDF Plugin](#broken-link-iroalu) - Convert Android PDF exports to CMYK PDFs with ICC profiles in a Node.js pipeline
- [CMYK Colors](https://img.ly/docs/cesdk/android/colors/for-print/cmyk-8a1334/) - Configure CMYK colors
- [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Define and use spot colors
- [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - General PDF export options
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Export for Social Media"
description: "Export vertical videos with the correct dimensions, format, and quality settings for Instagram Reels, TikTok, and YouTube Shorts."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/for-social-media-0e8a92/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [For Social Media](https://img.ly/docs/cesdk/android/export-save-publish/for-social-media-0e8a92/)
---
```kotlin file=@cesdk_android_examples/engine-guides-export-for-social-media/ForSocialMedia.kt reference-only
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.DesignUnit
import ly.img.engine.Engine
import ly.img.engine.ExportVideoOptions
import ly.img.engine.ExportVideoProgress
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.io.File
suspend fun exportForSocialMedia(
engine: Engine,
progressObserver: (ExportVideoProgress) -> Unit = {},
): File = withContext(Dispatchers.Main) {
val scene = engine.scene.createForVideo()
engine.scene.setDesignUnit(DesignUnit.PIXEL)
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 1080F)
engine.block.setHeight(block = page, value = 1920F)
val clip = createSocialMediaVideoClip(
engine = engine,
videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.appendChild(parent = track, child = clip.block)
engine.block.fillParent(track)
engine.block.forceLoadAVResource(block = clip.fill)
// Keep this sample compact by exporting at most one second of source media.
val exportDuration = 1.0.coerceAtMost(engine.block.getAVResourceTotalDuration(block = clip.fill))
check(exportDuration > 0.0) { "The source video must contain playable media." }
engine.block.setDuration(block = clip.block, duration = exportDuration)
engine.block.setDuration(block = page, duration = exportDuration)
val exportOptions = ExportVideoOptions(
targetWidth = 1080F,
targetHeight = 1920F,
// 30 fps is a common default for short-form vertical video.
frameRate = 30F,
// 8 Mbps balances quality and upload size for short-form video.
videoBitrate = 8_000_000,
)
val progressCallback: (ExportVideoProgress) -> Unit = { progress ->
reportExportProgress(progress)
progressObserver(progress)
}
val videoData = engine.block.exportVideo(
block = page,
// Start at the beginning of the page timeline.
timeOffset = 0.0,
duration = engine.block.getDuration(block = page),
mimeType = MimeType.MP4,
progressCallback = progressCallback,
options = exportOptions,
)
check(videoData.remaining() > 0) { "The exported MP4 data must not be empty." }
val outputFile = withContext(Dispatchers.IO) {
val file = File.createTempFile("social-media-export-", ".mp4")
file.outputStream().use { output ->
val buffer = videoData.asReadOnlyBuffer()
while (buffer.hasRemaining()) {
output.channel.write(buffer)
}
}
file
}
check(outputFile.length() > 0L) { "The exported MP4 file must not be empty." }
outputFile
}
private fun reportExportProgress(progress: ExportVideoProgress) {
if (progress.totalFrames > 0) {
val percent = (progress.encodedFrames.toFloat() / progress.totalFrames * 100).toInt()
Log.i(
"SocialMediaExport",
"Encoded $percent% (${progress.encodedFrames}/${progress.totalFrames} frames)",
)
}
}
private data class SocialMediaVideoClip(
val block: DesignBlock,
val fill: DesignBlock,
)
private fun createSocialMediaVideoClip(
engine: Engine,
videoUri: Uri,
): SocialMediaVideoClip {
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = clip, shape = engine.block.createShape(type = ShapeType.Rect))
engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(block = clip, fill = videoFill)
return SocialMediaVideoClip(block = clip, fill = videoFill)
}
```
Export vertical video designs for social media platforms with the correct
dimensions, format, and quality settings.
> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-for-social-media)
Short-form vertical video uses a 9:16 aspect ratio. Instagram Reels, TikTok,
and YouTube Shorts commonly use 1080×1920 pixels for that format. This guide
creates a vertical video scene, exports it as MP4, and tracks frame progress
while the export runs.
## Creating a Scene
Create a video scene and use pixels as the design unit so the page dimensions
match the delivery size. Add a 1080×1920 page, which is the block exported
later.
```kotlin highlight-android-create-scene
val scene = engine.scene.createForVideo()
engine.scene.setDesignUnit(DesignUnit.PIXEL)
val page = engine.block.create(DesignBlockType.Page)
engine.block.appendChild(parent = scene, child = page)
engine.block.setWidth(block = page, value = 1080F)
engine.block.setHeight(block = page, value = 1920F)
```
`engine.scene.createForVideo()` enables timeline behavior for the scene.
`engine.scene.setDesignUnit(DesignUnit.PIXEL)` makes the width and height values
line up with the requested social video resolution.
## Adding a Video
Each video clip is a graphic block with a rectangle shape and a video fill. Use
`ContentFillMode.COVER` when the source video should fill the clip frame,
cropping edges if needed.
```kotlin highlight-android-create-video-clip-helper
private data class SocialMediaVideoClip(
val block: DesignBlock,
val fill: DesignBlock,
)
private fun createSocialMediaVideoClip(
engine: Engine,
videoUri: Uri,
): SocialMediaVideoClip {
val clip = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = clip, shape = engine.block.createShape(type = ShapeType.Rect))
engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER)
val videoFill = engine.block.createFill(FillType.Video)
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(block = clip, fill = videoFill)
return SocialMediaVideoClip(block = clip, fill = videoFill)
}
```
Add the clip to a track and make the track fill the page.
```kotlin highlight-android-add-video
val clip = createSocialMediaVideoClip(
engine = engine,
videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"),
)
val track = engine.block.create(DesignBlockType.Track)
engine.block.appendChild(parent = page, child = track)
engine.block.appendChild(parent = track, child = clip.block)
engine.block.fillParent(track)
```
For groups and tracks, `fillParent(...)` also sizes child graphics against the
nearest non-group, non-track parent, so the unsized clip fills the page frame.
## Loading Media and Setting Duration
Load the media resource before export so duration metadata is available. The
sample exports one second of video and applies the same duration to the clip and
the page.
```kotlin highlight-android-load-media
engine.block.forceLoadAVResource(block = clip.fill)
// Keep this sample compact by exporting at most one second of source media.
val exportDuration = 1.0.coerceAtMost(engine.block.getAVResourceTotalDuration(block = clip.fill))
check(exportDuration > 0.0) { "The source video must contain playable media." }
engine.block.setDuration(block = clip.block, duration = exportDuration)
engine.block.setDuration(block = page, duration = exportDuration)
```
## Configuring Export Options
`ExportVideoOptions` controls the encoded output. For short-form vertical video,
export to 1080×1920 at 30 frames per second with an 8 Mbps video bitrate.
```kotlin highlight-android-export-options
val exportOptions = ExportVideoOptions(
targetWidth = 1080F,
targetHeight = 1920F,
// 30 fps is a common default for short-form vertical video.
frameRate = 30F,
// 8 Mbps balances quality and upload size for short-form video.
videoBitrate = 8_000_000,
)
```
Key video export settings:
- **targetWidth / targetHeight**: Output resolution, 1080×1920 for vertical
video.
- **frameRate**: Target export frame rate in Hz, commonly 30 for social video.
- **videoBitrate**: Video bitrate in bits per second; 8 Mbps balances quality
and upload size for short-form video.
Higher bitrates can improve quality but increase the file size. Set
`videoBitrate` to `0` when you want CE.SDK to choose a bitrate automatically.
## Exporting Videos
Export the page with `engine.block.exportVideo(...)`. Use `MimeType.MP4` for
broad platform compatibility, pass the full page duration, and attach the export
options. The callback reports each progress event before forwarding it to the
caller's observer, so observing progress does not replace the reporting path.
```kotlin highlight-android-export-video
val progressCallback: (ExportVideoProgress) -> Unit = { progress ->
reportExportProgress(progress)
progressObserver(progress)
}
val videoData = engine.block.exportVideo(
block = page,
// Start at the beginning of the page timeline.
timeOffset = 0.0,
duration = engine.block.getDuration(block = page),
mimeType = MimeType.MP4,
progressCallback = progressCallback,
options = exportOptions,
)
check(videoData.remaining() > 0) { "The exported MP4 data must not be empty." }
```
The Android API returns a `ByteBuffer` containing the encoded MP4 data. The
sample verifies that the buffer is non-empty before saving it.
## Tracking Export Progress
Video exports run across multiple engine update iterations. The callback
receives `ExportVideoProgress` with these frame counts:
- **renderedFrames**: Frames rendered by the engine.
- **encodedFrames**: Frames written by the encoder.
- **totalFrames**: Total frames expected for the export.
The reporting function calculates progress from `encodedFrames / totalFrames`
and guards against `totalFrames == 0` before dividing.
```kotlin highlight-android-report-progress
private fun reportExportProgress(progress: ExportVideoProgress) {
if (progress.totalFrames > 0) {
val percent = (progress.encodedFrames.toFloat() / progress.totalFrames * 100).toInt()
Log.i(
"SocialMediaExport",
"Encoded $percent% (${progress.encodedFrames}/${progress.totalFrames} frames)",
)
}
}
```
Use the forwarded observer to update your app's progress UI or collect export
metrics while preserving the same reporting path.
## Saving the Exported Video
Write a read-only view of the returned `ByteBuffer` to an `.mp4` file when your
app needs to hand the result to a share sheet, upload pipeline, or media storage
flow.
```kotlin highlight-android-save-file
val outputFile = withContext(Dispatchers.IO) {
val file = File.createTempFile("social-media-export-", ".mp4")
file.outputStream().use { output ->
val buffer = videoData.asReadOnlyBuffer()
while (buffer.hasRemaining()) {
output.channel.write(buffer)
}
}
file
}
check(outputFile.length() > 0L) { "The exported MP4 file must not be empty." }
```
For uploads, stream the same buffer data to your app's storage client instead of
writing a temporary file first. The sample returns ownership of its temporary
file to the caller. Persist it if it must outlive the current operation, and
delete it after sharing or uploading.
## API Reference
| Android API | Purpose |
| --- | --- |
| `engine.scene.createForVideo()` | Create a scene with timeline support. |
| `engine.scene.setDesignUnit(designUnit=_)` | Set the scene unit used by page dimensions. |
| `engine.block.create(blockType=_)` | Create page, track, and graphic blocks. |
| `engine.block.createShape(type=_)` | Create the rectangular clip shape. |
| `engine.block.setShape(block=_, shape=_)` | Assign the shape to the video graphic. |
| `engine.block.setContentFillMode(block=_, mode=_)` | Control how the video fills the graphic frame. |
| `engine.block.createFill(fillType=_)` | Create the video fill. |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Set the source URI on the video fill. |
| `engine.block.setFill(block=_, fill=_)` | Attach the video fill to the graphic. |
| `engine.block.appendChild(parent=_, child=_)` | Attach scene, page, track, and clip blocks. |
| `engine.block.fillParent(block=_)` | Resize a block to its parent frame. |
| `engine.block.setWidth(block=_, value=_)` | Set the page width. |
| `engine.block.setHeight(block=_, value=_)` | Set the page height. |
| `engine.block.forceLoadAVResource(block=_)` | Load video metadata before duration reads and export. |
| `engine.block.getAVResourceTotalDuration(block=_)` | Read the source media duration in seconds. |
| `engine.block.setDuration(block=_, duration=_)` | Set clip and page playback duration in seconds. |
| `engine.block.getDuration(block=_)` | Read the page duration passed to export. |
| `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Export a page timeline as MP4 bytes. |
### Export Options (Videos)
| Option | Type | Description |
| --- | --- | --- |
| `targetWidth` | `Float?` | Output width in pixels when used with `targetHeight`. |
| `targetHeight` | `Float?` | Output height in pixels when used with `targetWidth`. |
| `frameRate` | `Float` | Target export frame rate in Hz. |
| `videoBitrate` | `Int` | Video bitrate in bits per second, or `0` for automatic selection. |
| `audioBitrate` | `Int` | Audio bitrate in bits per second, or `0` for automatic selection. |
| `allowTextOverhang` | `Boolean` | Include glyph overhang bounds to avoid clipping text during export. |
## Next Steps
- [Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Explore export options, supported formats, and configuration features for sharing or rendering output.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Pre-Export Validation"
description: "Validate designs before export by detecting layout, visibility, and placeholder issues."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/pre-export-validation-3a2cba/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Pre-Export Validation](https://img.ly/docs/cesdk/android/export-save-publish/pre-export-validation-3a2cba/)
---
```kotlin file=@cesdk_android_examples/engine-guides-pre-export-validation/PreExportValidation.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
data class BoundingBox(
val minX: Float,
val minY: Float,
val maxX: Float,
val maxY: Float,
)
enum class ValidationSeverity {
ERROR,
WARNING,
}
enum class ValidationIssueKind {
OUTSIDE_PAGE,
PROTRUDING,
TEXT_OBSCURED,
UNFILLED_PLACEHOLDER,
}
data class ValidationIssue(
val kind: ValidationIssueKind,
val severity: ValidationSeverity,
val block: DesignBlock,
val blockName: String,
val message: String,
)
data class ValidationResult(
val errors: List,
val warnings: List,
)
private fun displayName(
engine: Engine,
block: DesignBlock,
): String {
val name = engine.block.getName(block)
if (name.isNotBlank()) return name
val kind = engine.block.getKind(block).substringAfterLast("/")
return kind.replaceFirstChar { it.uppercaseChar() }
}
private fun boundingBox(
engine: Engine,
block: DesignBlock,
): BoundingBox {
val x = engine.block.getGlobalBoundingBoxX(block)
val y = engine.block.getGlobalBoundingBoxY(block)
val width = engine.block.getGlobalBoundingBoxWidth(block)
val height = engine.block.getGlobalBoundingBoxHeight(block)
return BoundingBox(minX = x, minY = y, maxX = x + width, maxY = y + height)
}
private fun overlapRatio(
first: BoundingBox,
second: BoundingBox,
): Float {
val overlapWidth = maxOf(
0F,
minOf(first.maxX, second.maxX) - maxOf(first.minX, second.minX),
)
val overlapHeight = maxOf(
0F,
minOf(first.maxY, second.maxY) - maxOf(first.minY, second.minY),
)
val firstArea = (first.maxX - first.minX) * (first.maxY - first.minY)
return if (firstArea <= 0F) 0F else (overlapWidth * overlapHeight) / firstArea
}
private fun pageDescendantsInRenderOrder(
engine: Engine,
parent: DesignBlock,
): List = engine.block.getChildren(parent).flatMap { child ->
listOf(child) + pageDescendantsInRenderOrder(engine = engine, parent = child)
}
private fun validationCandidates(
engine: Engine,
page: DesignBlock,
): List = pageDescendantsInRenderOrder(engine = engine, parent = page).filter { block ->
if (!engine.block.isValid(block)) return@filter false
if (!engine.block.isVisible(block)) return@filter false
if (!engine.block.isIncludedInExport(block)) return@filter false
val blockType = engine.block.getType(block)
blockType == DesignBlockType.Text.key || blockType == DesignBlockType.Graphic.key
}
private fun findOutsideBlocks(
engine: Engine,
page: DesignBlock,
): List {
val pageBounds = boundingBox(engine = engine, block = page)
val candidates = validationCandidates(engine = engine, page = page)
return candidates.mapNotNull { block ->
val blockBounds = boundingBox(engine = engine, block = block)
if (overlapRatio(blockBounds, pageBounds) == 0F) {
ValidationIssue(
kind = ValidationIssueKind.OUTSIDE_PAGE,
severity = ValidationSeverity.ERROR,
block = block,
blockName = displayName(engine = engine, block = block),
message = "Element is completely outside the visible page area",
)
} else {
null
}
}
}
private fun findProtrudingBlocks(
engine: Engine,
page: DesignBlock,
): List {
val pageBounds = boundingBox(engine = engine, block = page)
val candidates = validationCandidates(engine = engine, page = page)
return candidates.mapNotNull { block ->
val blockBounds = boundingBox(engine = engine, block = block)
val overlap = overlapRatio(blockBounds, pageBounds)
if (overlap > 0F && overlap < 0.99F) {
ValidationIssue(
kind = ValidationIssueKind.PROTRUDING,
severity = ValidationSeverity.WARNING,
block = block,
blockName = displayName(engine = engine, block = block),
message = "Element extends beyond page boundaries",
)
} else {
null
}
}
}
private fun findObscuredText(
engine: Engine,
page: DesignBlock,
): List {
val blocksInRenderOrder = pageDescendantsInRenderOrder(engine = engine, parent = page)
val textBlocks = blocksInRenderOrder.filter { block ->
engine.block.isValid(block) &&
engine.block.isVisible(block) &&
engine.block.isIncludedInExport(block) &&
engine.block.getType(block) == DesignBlockType.Text.key
}
return textBlocks.mapNotNull { textBlock ->
val textIndex = blocksInRenderOrder.indexOf(textBlock)
if (textIndex == -1) return@mapNotNull null
val textBounds = boundingBox(engine = engine, block = textBlock)
val isObscured = blocksInRenderOrder.drop(textIndex + 1).any { blockAbove ->
canObscureText(engine = engine, block = blockAbove) &&
overlapRatio(textBounds, boundingBox(engine = engine, block = blockAbove)) > 0F
}
if (isObscured) {
ValidationIssue(
kind = ValidationIssueKind.TEXT_OBSCURED,
severity = ValidationSeverity.WARNING,
block = textBlock,
blockName = displayName(engine = engine, block = textBlock),
message = "Text may be partially hidden by overlapping elements",
)
} else {
null
}
}
}
private fun canObscureText(
engine: Engine,
block: DesignBlock,
): Boolean {
if (!engine.block.isValid(block)) return false
val blockType = engine.block.getType(block)
if (blockType == DesignBlockType.Text.key || blockType == DesignBlockType.Group.key) return false
return engine.block.isVisible(block) && engine.block.isIncludedInExport(block)
}
private fun findUnfilledPlaceholders(
engine: Engine,
page: DesignBlock,
): List {
val pageBlocks = pageDescendantsInRenderOrder(engine = engine, parent = page).toSet()
return engine.block.findAllPlaceholders().filter { it in pageBlocks }.mapNotNull { placeholder ->
if (!engine.block.isValid(placeholder)) return@mapNotNull null
if (!engine.block.isPlaceholderEnabled(placeholder)) return@mapNotNull null
if (!isPlaceholderFilled(engine = engine, block = placeholder)) {
ValidationIssue(
kind = ValidationIssueKind.UNFILLED_PLACEHOLDER,
severity = ValidationSeverity.ERROR,
block = placeholder,
blockName = displayName(engine = engine, block = placeholder),
message = "Placeholder has not been filled with content",
)
} else {
null
}
}
}
private fun isPlaceholderFilled(
engine: Engine,
block: DesignBlock,
): Boolean {
if (!engine.block.supportsFill(block)) return false
val fill = engine.block.getFill(block)
if (!engine.block.isValid(fill)) return false
if (engine.block.getType(fill) != FillType.Image.key) return true
return engine.block.getUri(
block = fill,
property = "fill/image/imageFileURI",
).toString().isNotBlank()
}
private fun validateDesign(
engine: Engine,
page: DesignBlock,
): ValidationResult {
val allIssues =
findOutsideBlocks(engine = engine, page = page) +
findProtrudingBlocks(engine = engine, page = page) +
findObscuredText(engine = engine, page = page) +
findUnfilledPlaceholders(engine = engine, page = page)
val result = ValidationResult(
errors = allIssues.filter { it.severity == ValidationSeverity.ERROR },
warnings = allIssues.filter { it.severity == ValidationSeverity.WARNING },
)
result.errors.firstOrNull()?.block?.takeIf(engine.block::isValid)?.let { firstError ->
engine.block.select(firstError)
}
return result
}
fun preExportValidation(engine: Engine): ValidationResult {
// This demo scene gives the smoke test one block for each validation outcome.
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val outsideBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(outsideBlock, name = "Outside Element")
engine.block.setShape(outsideBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = outsideBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = outsideBlock,
color = Color.fromRGBA(r = 0.9F, g = 0.2F, b = 0.2F, a = 1F),
)
engine.block.setWidth(outsideBlock, value = 150F)
engine.block.setHeight(outsideBlock, value = 100F)
engine.block.setPositionX(outsideBlock, value = -200F)
engine.block.setPositionY(outsideBlock, value = 80F)
engine.block.appendChild(parent = page, child = outsideBlock)
val protrudingBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(protrudingBlock, name = "Protruding Element")
engine.block.setShape(protrudingBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = protrudingBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = protrudingBlock,
color = Color.fromRGBA(r = 0.95F, g = 0.65F, b = 0.1F, a = 1F),
)
engine.block.setWidth(protrudingBlock, value = 150F)
engine.block.setHeight(protrudingBlock, value = 100F)
engine.block.setPositionX(protrudingBlock, value = 725F)
engine.block.setPositionY(protrudingBlock, value = 80F)
engine.block.appendChild(parent = page, child = protrudingBlock)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(textBlock, name = "Obscured Text")
engine.block.replaceText(block = textBlock, text = "Hidden")
engine.block.setFloat(block = textBlock, property = "text/fontSize", value = 48F)
engine.block.setPositionX(textBlock, value = 200F)
engine.block.setPositionY(textBlock, value = 250F)
engine.block.setWidth(textBlock, value = 220F)
engine.block.setHeight(textBlock, value = 100F)
engine.block.appendChild(parent = page, child = textBlock)
val groupCompanionBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(groupCompanionBlock, name = "Grouped Companion")
engine.block.setShape(groupCompanionBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = groupCompanionBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = groupCompanionBlock,
color = Color.fromRGBA(r = 0.1F, g = 0.7F, b = 0.45F, a = 1F),
)
engine.block.setPositionX(groupCompanionBlock, value = 450F)
engine.block.setPositionY(groupCompanionBlock, value = 250F)
engine.block.setWidth(groupCompanionBlock, value = 60F)
engine.block.setHeight(groupCompanionBlock, value = 60F)
engine.block.appendChild(parent = page, child = groupCompanionBlock)
if (engine.block.isGroupable(listOf(textBlock, groupCompanionBlock))) {
engine.block.group(listOf(textBlock, groupCompanionBlock))
} else {
error("Expected demo blocks to be groupable.")
}
val clearText = engine.block.create(DesignBlockType.Text)
engine.block.setName(clearText, name = "Clear Text")
engine.block.replaceText(block = clearText, text = "Readable")
engine.block.setFloat(block = clearText, property = "text/fontSize", value = 32F)
engine.block.setPositionX(clearText, value = 300F)
engine.block.setPositionY(clearText, value = 80F)
engine.block.setWidth(clearText, value = 180F)
engine.block.setHeight(clearText, value = 80F)
engine.block.appendChild(parent = page, child = clearText)
val leftFrameBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(leftFrameBlock, name = "Left Frame")
engine.block.setShape(leftFrameBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = leftFrameBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = leftFrameBlock,
color = Color.fromRGBA(r = 0.55F, g = 0.25F, b = 0.7F, a = 1F),
)
engine.block.setPositionX(leftFrameBlock, value = 260F)
engine.block.setPositionY(leftFrameBlock, value = 80F)
engine.block.setWidth(leftFrameBlock, value = 20F)
engine.block.setHeight(leftFrameBlock, value = 80F)
engine.block.appendChild(parent = page, child = leftFrameBlock)
val rightFrameBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(rightFrameBlock, name = "Right Frame")
engine.block.setShape(rightFrameBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = rightFrameBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = rightFrameBlock,
color = Color.fromRGBA(r = 0.55F, g = 0.25F, b = 0.7F, a = 1F),
)
engine.block.setPositionX(rightFrameBlock, value = 500F)
engine.block.setPositionY(rightFrameBlock, value = 80F)
engine.block.setWidth(rightFrameBlock, value = 20F)
engine.block.setHeight(rightFrameBlock, value = 80F)
engine.block.appendChild(parent = page, child = rightFrameBlock)
if (engine.block.isGroupable(listOf(leftFrameBlock, rightFrameBlock))) {
engine.block.group(listOf(leftFrameBlock, rightFrameBlock))
} else {
error("Expected frame blocks to be groupable.")
}
val coveringBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(coveringBlock, name = "Overlapping Shape")
engine.block.setShape(coveringBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = coveringBlock, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = coveringBlock,
color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 0.8F),
)
engine.block.setPositionX(coveringBlock, value = 200F)
engine.block.setPositionY(coveringBlock, value = 250F)
engine.block.setWidth(coveringBlock, value = 220F)
engine.block.setHeight(coveringBlock, value = 100F)
engine.block.appendChild(parent = page, child = coveringBlock)
val hiddenTextBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(hiddenTextBlock, name = "Hidden Text")
engine.block.replaceText(block = hiddenTextBlock, text = "Hidden from export")
engine.block.setFloat(block = hiddenTextBlock, property = "text/fontSize", value = 30F)
engine.block.setPositionX(hiddenTextBlock, value = 80F)
engine.block.setPositionY(hiddenTextBlock, value = 250F)
engine.block.setWidth(hiddenTextBlock, value = 220F)
engine.block.setHeight(hiddenTextBlock, value = 80F)
engine.block.setVisible(block = hiddenTextBlock, visible = false)
engine.block.appendChild(parent = page, child = hiddenTextBlock)
val hiddenTextCover = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(hiddenTextCover, name = "Hidden Text Cover")
engine.block.setShape(hiddenTextCover, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = hiddenTextCover, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = hiddenTextCover,
color = Color.fromRGBA(r = 0.65F, g = 0.25F, b = 0.15F, a = 1F),
)
engine.block.setPositionX(hiddenTextCover, value = 80F)
engine.block.setPositionY(hiddenTextCover, value = 250F)
engine.block.setWidth(hiddenTextCover, value = 220F)
engine.block.setHeight(hiddenTextCover, value = 80F)
engine.block.appendChild(parent = page, child = hiddenTextCover)
val excludedTextBlock = engine.block.create(DesignBlockType.Text)
engine.block.setName(excludedTextBlock, name = "Export Excluded Text")
engine.block.replaceText(block = excludedTextBlock, text = "Excluded from export")
engine.block.setFloat(block = excludedTextBlock, property = "text/fontSize", value = 30F)
engine.block.setPositionX(excludedTextBlock, value = 470F)
engine.block.setPositionY(excludedTextBlock, value = 250F)
engine.block.setWidth(excludedTextBlock, value = 220F)
engine.block.setHeight(excludedTextBlock, value = 80F)
engine.block.setIncludedInExport(block = excludedTextBlock, enabled = false)
engine.block.appendChild(parent = page, child = excludedTextBlock)
val excludedTextCover = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(excludedTextCover, name = "Export Excluded Text Cover")
engine.block.setShape(excludedTextCover, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = excludedTextCover, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = excludedTextCover,
color = Color.fromRGBA(r = 0.15F, g = 0.45F, b = 0.65F, a = 1F),
)
engine.block.setPositionX(excludedTextCover, value = 470F)
engine.block.setPositionY(excludedTextCover, value = 250F)
engine.block.setWidth(excludedTextCover, value = 220F)
engine.block.setHeight(excludedTextCover, value = 80F)
engine.block.appendChild(parent = page, child = excludedTextCover)
val placeholder = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(placeholder, name = "Unfilled Placeholder")
engine.block.setShape(placeholder, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = placeholder, fill = engine.block.createFill(FillType.Image))
engine.block.setPositionX(placeholder, value = 50F)
engine.block.setPositionY(placeholder, value = 400F)
engine.block.setWidth(placeholder, value = 150F)
engine.block.setHeight(placeholder, value = 100F)
engine.block.appendChild(parent = page, child = placeholder)
engine.block.setScopeEnabled(block = placeholder, key = "fill/change", enabled = true)
engine.block.setPlaceholderEnabled(block = placeholder, enabled = true)
if (engine.block.supportsPlaceholderBehavior(placeholder)) {
engine.block.setPlaceholderBehaviorEnabled(block = placeholder, enabled = true)
}
val filledColorPlaceholder = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(filledColorPlaceholder, name = "Filled Color Placeholder")
engine.block.setShape(filledColorPlaceholder, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = filledColorPlaceholder, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(
block = filledColorPlaceholder,
color = Color.fromRGBA(r = 0.25F, g = 0.55F, b = 0.9F, a = 1F),
)
engine.block.setPositionX(filledColorPlaceholder, value = 560F)
engine.block.setPositionY(filledColorPlaceholder, value = 400F)
engine.block.setWidth(filledColorPlaceholder, value = 150F)
engine.block.setHeight(filledColorPlaceholder, value = 100F)
engine.block.appendChild(parent = page, child = filledColorPlaceholder)
engine.block.setScopeEnabled(block = filledColorPlaceholder, key = "fill/change", enabled = true)
engine.block.setPlaceholderEnabled(block = filledColorPlaceholder, enabled = true)
if (engine.block.supportsPlaceholderBehavior(filledColorPlaceholder)) {
engine.block.setPlaceholderBehaviorEnabled(block = filledColorPlaceholder, enabled = true)
}
val secondPage = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(secondPage, value = 800F)
engine.block.setHeight(secondPage, value = 600F)
engine.block.appendChild(parent = scene, child = secondPage)
val otherPagePlaceholder = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(otherPagePlaceholder, name = "Other Page Placeholder")
engine.block.setShape(otherPagePlaceholder, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = otherPagePlaceholder, fill = engine.block.createFill(FillType.Image))
engine.block.setPositionX(otherPagePlaceholder, value = 300F)
engine.block.setPositionY(otherPagePlaceholder, value = 300F)
engine.block.setWidth(otherPagePlaceholder, value = 150F)
engine.block.setHeight(otherPagePlaceholder, value = 100F)
engine.block.appendChild(parent = secondPage, child = otherPagePlaceholder)
engine.block.setScopeEnabled(block = otherPagePlaceholder, key = "fill/change", enabled = true)
engine.block.setPlaceholderEnabled(block = otherPagePlaceholder, enabled = true)
if (engine.block.supportsPlaceholderBehavior(otherPagePlaceholder)) {
engine.block.setPlaceholderBehaviorEnabled(block = otherPagePlaceholder, enabled = true)
}
return validateDesign(engine = engine, page = page)
}
```
Validate a design before export by detecting elements outside the page,
protruding content, obscured text, and unfilled placeholders.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-pre-export-validation)
Pre-export validation catches layout and content issues before export, preventing problems like cropped content, hidden text, and incomplete designs in the final output. The checks shown here run against CE.SDK Engine block APIs, so you can call them before your own export step.
Each check returns `ValidationIssue` values categorized by severity. Errors describe content that should block export; warnings describe content that may still export but needs review.
```kotlin highlight-android-types
data class BoundingBox(
val minX: Float,
val minY: Float,
val maxX: Float,
val maxY: Float,
)
enum class ValidationSeverity {
ERROR,
WARNING,
}
enum class ValidationIssueKind {
OUTSIDE_PAGE,
PROTRUDING,
TEXT_OBSCURED,
UNFILLED_PLACEHOLDER,
}
data class ValidationIssue(
val kind: ValidationIssueKind,
val severity: ValidationSeverity,
val block: DesignBlock,
val blockName: String,
val message: String,
)
data class ValidationResult(
val errors: List,
val warnings: List,
)
private fun displayName(
engine: Engine,
block: DesignBlock,
): String {
val name = engine.block.getName(block)
if (name.isNotBlank()) return name
val kind = engine.block.getKind(block).substringAfterLast("/")
return kind.replaceFirstChar { it.uppercaseChar() }
}
```
## Getting Element Bounds
Every spatial check compares block positions in global coordinates. The `getGlobalBoundingBox*` methods return the axis-aligned box components in the scene's global coordinate space, the overlap helper turns two boxes into a ratio from `0` (fully outside) to `1` (fully inside), and the descendant helper keeps later checks scoped to visible, exportable blocks on the page being exported.
```kotlin highlight-android-get-bounding-box
private fun boundingBox(
engine: Engine,
block: DesignBlock,
): BoundingBox {
val x = engine.block.getGlobalBoundingBoxX(block)
val y = engine.block.getGlobalBoundingBoxY(block)
val width = engine.block.getGlobalBoundingBoxWidth(block)
val height = engine.block.getGlobalBoundingBoxHeight(block)
return BoundingBox(minX = x, minY = y, maxX = x + width, maxY = y + height)
}
private fun overlapRatio(
first: BoundingBox,
second: BoundingBox,
): Float {
val overlapWidth = maxOf(
0F,
minOf(first.maxX, second.maxX) - maxOf(first.minX, second.minX),
)
val overlapHeight = maxOf(
0F,
minOf(first.maxY, second.maxY) - maxOf(first.minY, second.minY),
)
val firstArea = (first.maxX - first.minX) * (first.maxY - first.minY)
return if (firstArea <= 0F) 0F else (overlapWidth * overlapHeight) / firstArea
}
private fun pageDescendantsInRenderOrder(
engine: Engine,
parent: DesignBlock,
): List = engine.block.getChildren(parent).flatMap { child ->
listOf(child) + pageDescendantsInRenderOrder(engine = engine, parent = child)
}
private fun validationCandidates(
engine: Engine,
page: DesignBlock,
): List = pageDescendantsInRenderOrder(engine = engine, parent = page).filter { block ->
if (!engine.block.isValid(block)) return@filter false
if (!engine.block.isVisible(block)) return@filter false
if (!engine.block.isIncludedInExport(block)) return@filter false
val blockType = engine.block.getType(block)
blockType == DesignBlockType.Text.key || blockType == DesignBlockType.Graphic.key
}
```
## Detecting Elements Outside the Page
Elements completely outside the page are missing from the export. Scan visible text and graphic descendants that are included in export, compare each block with the page bounds, and flag blocks whose overlap ratio is zero.
```kotlin highlight-android-find-outside-blocks
private fun findOutsideBlocks(
engine: Engine,
page: DesignBlock,
): List {
val pageBounds = boundingBox(engine = engine, block = page)
val candidates = validationCandidates(engine = engine, page = page)
return candidates.mapNotNull { block ->
val blockBounds = boundingBox(engine = engine, block = block)
if (overlapRatio(blockBounds, pageBounds) == 0F) {
ValidationIssue(
kind = ValidationIssueKind.OUTSIDE_PAGE,
severity = ValidationSeverity.ERROR,
block = block,
blockName = displayName(engine = engine, block = block),
message = "Element is completely outside the visible page area",
)
} else {
null
}
}
}
```
Treat these issues as errors because the exported output does not include the content.
## Detecting Protruding Elements
Elements that extend beyond the page boundaries get cropped on export. Reuse the same block scan and report blocks whose overlap ratio is greater than `0` but less than `1`.
```kotlin highlight-android-find-protruding-blocks
private fun findProtrudingBlocks(
engine: Engine,
page: DesignBlock,
): List {
val pageBounds = boundingBox(engine = engine, block = page)
val candidates = validationCandidates(engine = engine, page = page)
return candidates.mapNotNull { block ->
val blockBounds = boundingBox(engine = engine, block = block)
val overlap = overlapRatio(blockBounds, pageBounds)
if (overlap > 0F && overlap < 0.99F) {
ValidationIssue(
kind = ValidationIssueKind.PROTRUDING,
severity = ValidationSeverity.WARNING,
block = block,
blockName = displayName(engine = engine, block = block),
message = "Element extends beyond page boundaries",
)
} else {
null
}
}
}
```
The sample uses a small tolerance (`< 0.99`) to avoid false positives from sub-pixel rounding. These issues are warnings because the content is still partially visible.
## Finding Obscured Text
Text hidden behind other elements can be unreadable in the final export. Recursively walking `getChildren()` gives a render-order list, so blocks later in the list render above earlier blocks.
```kotlin highlight-android-find-obscured-text
private fun findObscuredText(
engine: Engine,
page: DesignBlock,
): List {
val blocksInRenderOrder = pageDescendantsInRenderOrder(engine = engine, parent = page)
val textBlocks = blocksInRenderOrder.filter { block ->
engine.block.isValid(block) &&
engine.block.isVisible(block) &&
engine.block.isIncludedInExport(block) &&
engine.block.getType(block) == DesignBlockType.Text.key
}
return textBlocks.mapNotNull { textBlock ->
val textIndex = blocksInRenderOrder.indexOf(textBlock)
if (textIndex == -1) return@mapNotNull null
val textBounds = boundingBox(engine = engine, block = textBlock)
val isObscured = blocksInRenderOrder.drop(textIndex + 1).any { blockAbove ->
canObscureText(engine = engine, block = blockAbove) &&
overlapRatio(textBounds, boundingBox(engine = engine, block = blockAbove)) > 0F
}
if (isObscured) {
ValidationIssue(
kind = ValidationIssueKind.TEXT_OBSCURED,
severity = ValidationSeverity.WARNING,
block = textBlock,
blockName = displayName(engine = engine, block = textBlock),
message = "Text may be partially hidden by overlapping elements",
)
} else {
null
}
}
}
private fun canObscureText(
engine: Engine,
block: DesignBlock,
): Boolean {
if (!engine.block.isValid(block)) return false
val blockType = engine.block.getType(block)
if (blockType == DesignBlockType.Text.key || blockType == DesignBlockType.Group.key) return false
return engine.block.isVisible(block) && engine.block.isIncludedInExport(block)
}
```
For each text block, the check looks only at blocks above it and skips text-on-text overlaps. Group containers are skipped because their children are checked individually. When a rendered child block overlaps the text bounds, the text receives a warning.
## Checking Placeholder Content
Placeholders mark areas the user must fill before export. `findAllPlaceholders()` returns configured placeholder blocks, `isPlaceholderEnabled()` confirms that the placeholder function is still enabled, and the helper scopes those blocks to the page being exported.
```kotlin highlight-android-find-unfilled-placeholders
private fun findUnfilledPlaceholders(
engine: Engine,
page: DesignBlock,
): List {
val pageBlocks = pageDescendantsInRenderOrder(engine = engine, parent = page).toSet()
return engine.block.findAllPlaceholders().filter { it in pageBlocks }.mapNotNull { placeholder ->
if (!engine.block.isValid(placeholder)) return@mapNotNull null
if (!engine.block.isPlaceholderEnabled(placeholder)) return@mapNotNull null
if (!isPlaceholderFilled(engine = engine, block = placeholder)) {
ValidationIssue(
kind = ValidationIssueKind.UNFILLED_PLACEHOLDER,
severity = ValidationSeverity.ERROR,
block = placeholder,
blockName = displayName(engine = engine, block = placeholder),
message = "Placeholder has not been filled with content",
)
} else {
null
}
}
}
private fun isPlaceholderFilled(
engine: Engine,
block: DesignBlock,
): Boolean {
if (!engine.block.supportsFill(block)) return false
val fill = engine.block.getFill(block)
if (!engine.block.isValid(fill)) return false
if (engine.block.getType(fill) != FillType.Image.key) return true
return engine.block.getUri(
block = fill,
property = "fill/image/imageFileURI",
).toString().isNotBlank()
}
```
Unfilled placeholders are errors because they indicate incomplete design content. The sample uses placeholder state to find configured placeholders, then inspects the placeholder fill. Image placeholders are considered filled only when their image fill has a non-empty `fill/image/imageFileURI` value; other valid fill types are already filled content.
## Running Validation
Aggregate the four checks into one `ValidationResult`, then block export when `result.errors` is not empty. Selecting the first error block helps users locate the issue before trying again.
```kotlin highlight-android-validate-design
private fun validateDesign(
engine: Engine,
page: DesignBlock,
): ValidationResult {
val allIssues =
findOutsideBlocks(engine = engine, page = page) +
findProtrudingBlocks(engine = engine, page = page) +
findObscuredText(engine = engine, page = page) +
findUnfilledPlaceholders(engine = engine, page = page)
val result = ValidationResult(
errors = allIssues.filter { it.severity == ValidationSeverity.ERROR },
warnings = allIssues.filter { it.severity == ValidationSeverity.WARNING },
)
result.errors.firstOrNull()?.block?.takeIf(engine.block::isValid)?.let { firstError ->
engine.block.select(firstError)
}
return result
}
```
Use warnings for confirmation or logging before you continue with export.
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.block.getGlobalBoundingBoxX(block=_)` | Get the block's global X position |
| `engine.block.getGlobalBoundingBoxY(block=_)` | Get the block's global Y position |
| `engine.block.getGlobalBoundingBoxWidth(block=_)` | Get the block's global width |
| `engine.block.getGlobalBoundingBoxHeight(block=_)` | Get the block's global height |
| `engine.block.getChildren(block=_)` | Get child blocks in rendering order |
| `engine.block.getType(block=_)` | Read the block type string |
| `engine.block.getKind(block=_)` | Read the block kind string |
| `engine.block.getName(block=_)` | Read the block display name |
| `engine.block.isValid(block=_)` | Check whether the block still exists |
| `engine.block.isVisible(block=_)` | Check whether a block is visible |
| `engine.block.isIncludedInExport(block=_)` | Check whether a block is exported |
| `engine.block.findAllPlaceholders()` | Find all placeholder blocks in the scene |
| `engine.block.isPlaceholderEnabled(block=_)` | Check whether a block is still marked as a placeholder |
| `engine.block.supportsFill(block=_)` | Check whether a block can have a fill |
| `engine.block.getFill(block=_)` | Get the block's fill block |
| `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Read the image fill source URI |
| `engine.block.select(block=_)` | Select the first block that needs attention |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Learn about the available export formats and options
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Understand the block hierarchy and positioning model
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Save"
description: "Save design progress locally or to a backend service to allow for later editing or publishing."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/)
---
```kotlin file=@cesdk_android_examples/engine-guides-save-designs/SaveDesigns.kt reference-only
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.CompressionFormat
import ly.img.engine.CompressionLevel
import ly.img.engine.CompressionOptions
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.SaveToStringOptions
import ly.img.engine.ShapeType
import java.io.File
import java.io.FileOutputStream
suspend fun saveDesigns(
engine: Engine,
outputDir: File,
): SaveDesigns {
withContext(Dispatchers.IO) {
outputDir.mkdirs()
}
val scene = engine.scene.create()
engine.block.setName(scene, name = "Spring Campaign")
val page = engine.block.create(DesignBlockType.Page)
engine.block.setName(page, name = "Campaign Cover")
engine.block.setWidth(page, value = 1080F)
engine.block.setHeight(page, value = 1080F)
engine.block.appendChild(parent = scene, child = page)
val background = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(background, name = "Background Panel")
engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(background, value = 1080F)
engine.block.setHeight(background, value = 1080F)
engine.block.setFill(background, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = background, color = Color.fromHex("#FFF4F7FB"))
engine.block.appendChild(parent = page, child = background)
val badge = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(badge, name = "Reusable Badge")
engine.block.setShape(badge, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(badge, value = 330F)
engine.block.setPositionY(badge, value = 390F)
engine.block.setWidth(badge, value = 420F)
engine.block.setHeight(badge, value = 300F)
engine.block.setFill(badge, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = badge, color = Color.fromHex("#FF2156D9"))
engine.block.appendChild(parent = page, child = badge)
val sceneString = engine.scene.saveToString(
scene = scene,
allowedResourceSchemes = listOf("bundle", "file", "http", "https"),
)
check(sceneString.isNotBlank())
val sceneArchive = engine.scene.saveToArchive(scene = scene)
val sceneFile = File(outputDir, "spring-campaign.scene")
val sceneArchiveFile = File(outputDir, "spring-campaign.zip")
withContext(Dispatchers.IO) {
sceneFile.bufferedWriter(Charsets.UTF_8).use { writer ->
writer.write(sceneString)
}
FileOutputStream(sceneArchiveFile).channel.use { channel ->
val archiveBuffer = sceneArchive.asReadOnlyBuffer()
while (archiveBuffer.hasRemaining()) {
channel.write(archiveBuffer)
}
}
}
check(sceneFile.length() > 0L)
check(sceneArchiveFile.length() > 0L)
val compressedSceneString = engine.scene.saveToString(
scene = scene,
options = SaveToStringOptions(
allowedResourceSchemes = listOf("bundle", "file", "http", "https"),
compression = CompressionOptions(
format = CompressionFormat.ZSTD,
level = CompressionLevel.DEFAULT,
),
),
)
check(compressedSceneString.isNotBlank())
val blockString = engine.block.saveToString(blocks = listOf(badge))
val blockArchive = engine.block.saveToArchive(blocks = listOf(badge))
val blockArchiveFile = File(outputDir, "reusable-badge.zip")
withContext(Dispatchers.IO) {
FileOutputStream(blockArchiveFile).channel.use { channel ->
val archiveBuffer = blockArchive.asReadOnlyBuffer()
while (archiveBuffer.hasRemaining()) {
channel.write(archiveBuffer)
}
}
}
check(blockString.isNotBlank())
check(blockArchiveFile.length() > 0L)
val savedScene = withContext(Dispatchers.IO) {
sceneFile.readText(Charsets.UTF_8)
}
val loadedScene = engine.scene.load(
scene = savedScene,
waitForResources = true,
)
val loadedSceneName = engine.block.getName(loadedScene)
check(loadedSceneName == "Spring Campaign")
engine.scene.loadArchive(
archiveUri = Uri.fromFile(sceneArchiveFile),
waitForResources = true,
)
val loadedArchivePageCount = engine.scene.getPages().size
check(loadedArchivePageCount == 1)
val currentPage = engine.scene.getPages().first()
val loadedStringBlocks = engine.block.loadFromString(blockString)
val loadedArchiveBlocks = engine.block.loadFromArchive(Uri.fromFile(blockArchiveFile))
(loadedStringBlocks + loadedArchiveBlocks).forEach { block ->
engine.block.appendChild(parent = currentPage, child = block)
}
val loadedBlockNames = (loadedStringBlocks + loadedArchiveBlocks).map(engine.block::getName)
check(loadedBlockNames == listOf("Reusable Badge", "Reusable Badge"))
return SaveDesigns(
sceneStringLength = sceneString.length,
compressedSceneStringLength = compressedSceneString.length,
sceneFile = sceneFile,
sceneArchiveFile = sceneArchiveFile,
blockStringLength = blockString.length,
blockArchiveFile = blockArchiveFile,
loadedSceneName = loadedSceneName,
loadedArchivePageCount = loadedArchivePageCount,
loadedBlockNames = loadedBlockNames,
)
}
data class SaveDesigns(
val sceneStringLength: Int,
val compressedSceneStringLength: Int,
val sceneFile: File,
val sceneArchiveFile: File,
val blockStringLength: Int,
val blockArchiveFile: File,
val loadedSceneName: String,
val loadedArchivePageCount: Int,
val loadedBlockNames: List,
)
```
Save and serialize designs in CE.SDK for later retrieval, sharing, or storage
using string or archive formats.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-save-designs)
CE.SDK provides two formats for persisting designs. Choose the format based on your storage and portability requirements.
## Save Format Comparison
| Format | Method | Assets | Best For |
| ------ | ------ | ------ | -------- |
| String | `saveToString()` | Referenced by URL | Database storage, cloud sync |
| Archive | `saveToArchive()` | Embedded in ZIP | Offline use, file sharing |
**String format** produces a lightweight serialized string where assets remain as URL references. Use this when asset URLs will remain accessible.
**Archive format** creates a self-contained ZIP with all assets embedded. Use this for portable designs that work offline.
## Save to String
Serialize the current scene to a string suitable for database storage. Android lets you restrict the resource URI schemes that may stay referenced in the saved scene.
```kotlin highlight-android-save-to-string
val sceneString = engine.scene.saveToString(
scene = scene,
allowedResourceSchemes = listOf("bundle", "file", "http", "https"),
)
```
The string contains the complete scene structure but references assets by their original URLs. If a referenced asset uses a scheme outside `allowedResourceSchemes`, the save call fails; upload or rewrite those assets before saving, or include the scheme when it is safe for your app.
## Save to Archive
Create a self-contained ZIP with the scene and all embedded assets.
```kotlin highlight-android-save-to-archive
val sceneArchive = engine.scene.saveToArchive(scene = scene)
```
`saveToArchive()` returns a `ByteBuffer` that includes all pages, elements, and available asset data in a single portable file.
## Write to Disk
Use standard Android file APIs to persist saved designs locally before uploading or syncing them.
```kotlin highlight-android-write-to-disk
val sceneFile = File(outputDir, "spring-campaign.scene")
val sceneArchiveFile = File(outputDir, "spring-campaign.zip")
withContext(Dispatchers.IO) {
sceneFile.bufferedWriter(Charsets.UTF_8).use { writer ->
writer.write(sceneString)
}
FileOutputStream(sceneArchiveFile).channel.use { channel ->
val archiveBuffer = sceneArchive.asReadOnlyBuffer()
while (archiveBuffer.hasRemaining()) {
channel.write(archiveBuffer)
}
}
}
```
Scene strings can be written as UTF-8 text. Archive buffers can be streamed from the returned `ByteBuffer` through file NIO APIs to avoid an additional `ByteArray` copy while writing.
## Compression Options
CE.SDK supports optional compression for saved scene strings to reduce file size.
```kotlin highlight-android-compression
val compressedSceneString = engine.scene.saveToString(
scene = scene,
options = SaveToStringOptions(
allowedResourceSchemes = listOf("bundle", "file", "http", "https"),
compression = CompressionOptions(
format = CompressionFormat.ZSTD,
level = CompressionLevel.DEFAULT,
),
),
)
```
**Compression Formats:**
- `CompressionFormat.NONE` - No compression
- `CompressionFormat.ZSTD` - Zstandard compression
**Compression Levels:**
- `CompressionLevel.FASTEST` - Fastest compression, larger output
- `CompressionLevel.DEFAULT` - Balanced speed and size
- `CompressionLevel.BEST` - Best compression, slower
Use the default Zstandard level for most app sync and project persistence workflows.
## Save Blocks
Save specific blocks when your app needs reusable elements, layout presets, or block hierarchies instead of a full scene.
```kotlin highlight-android-save-blocks
val blockString = engine.block.saveToString(blocks = listOf(badge))
val blockArchive = engine.block.saveToArchive(blocks = listOf(badge))
```
Block strings are lightweight and keep external resources as references. Block archives bundle available resources for portable reuse.
## Load Scene from File
Read a previously saved `.scene` file and restore it with `engine.scene.load()`.
```kotlin highlight-android-load-scene
val savedScene = withContext(Dispatchers.IO) {
sceneFile.readText(Charsets.UTF_8)
}
val loadedScene = engine.scene.load(
scene = savedScene,
waitForResources = true,
)
```
Loading a scene replaces the current scene. Scene files are lightweight but require the original asset URLs to remain accessible.
## Load Archive from File
Use `engine.scene.loadArchive()` with a local file `Uri` to restore a self-contained `.zip` archive.
```kotlin highlight-android-load-archive
engine.scene.loadArchive(
archiveUri = Uri.fromFile(sceneArchiveFile),
waitForResources = true,
)
```
Archives are portable and work offline since all bundled assets are resolved relative to the archive.
## Load Blocks
Use `engine.block.loadFromString()` or `engine.block.loadFromArchive()` to restore saved blocks. Loaded blocks are not attached automatically, so append them to a page or another parent block.
```kotlin highlight-android-load-blocks
val currentPage = engine.scene.getPages().first()
val loadedStringBlocks = engine.block.loadFromString(blockString)
val loadedArchiveBlocks = engine.block.loadFromArchive(Uri.fromFile(blockArchiveFile))
(loadedStringBlocks + loadedArchiveBlocks).forEach { block ->
engine.block.appendChild(parent = currentPage, child = block)
}
```
This keeps project loading separate from reusable component loading.
## Troubleshooting
- **Save fails before a scene exists:** Create or load a scene first and keep the returned scene block ID. Pass that ID to scene save APIs instead of calling save logic before your editor or engine setup has produced a scene.
- **Assets are missing after loading a string or `.scene` file:** String and `.scene` saves keep assets as URI references. Make sure those URLs are still reachable from the Android app, or use an archive save when the design must work offline.
- **String saves fail because of disallowed resource schemes:** The `allowedResourceSchemes` list controls which asset URI schemes may stay referenced. Upload local or temporary resources to a durable URL before saving, or include only the schemes your app can safely resolve later.
- **Archives are larger than string saves:** Archives bundle available assets, which makes them portable and offline-friendly but increases file size. Use string saves for database sync when referenced assets remain available, and archives for file sharing or offline restore.
## API Reference
| Method | Description |
| ------ | ----------- |
| `engine.scene.saveToString(scene=_, allowedResourceSchemes=_)` | Serialize a scene to a string |
| `engine.scene.saveToString(scene=_, options=_)` | Serialize a scene with save options such as compression |
| `engine.scene.saveToArchive(scene=_)` | Save a scene with assets as a ZIP `ByteBuffer` |
| `engine.scene.load(scene=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene from a serialized string |
| `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene from a remote or local scene URI |
| `engine.scene.loadArchive(archiveUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene from a ZIP archive URI |
| `engine.scene.getPages()` | Return the pages in the current scene |
| `engine.block.saveToString(blocks=_, allowedResourceSchemes=_)` | Serialize specific blocks to a string |
| `engine.block.saveToArchive(blocks=_)` | Save specific blocks with assets as a ZIP `ByteBuffer` |
| `engine.block.loadFromString(block=_)` | Load blocks from a serialized string |
| `engine.block.loadFromArchive(archiveUri=_)` | Load blocks from a ZIP archive URI |
| `engine.block.loadFromURL(url=_)` | Load blocks from a `blocks.blocks` URL inside an unzipped block archive |
| `engine.block.appendChild(parent=_, child=_)` | Attach a loaded block to a scene hierarchy |
## Next Steps
- [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Export designs to image, PDF, and video formats
- [Load Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Load scenes from remote URLs and archives
- [Store Custom Metadata](https://img.ly/docs/cesdk/android/export-save-publish/store-custom-metadata-337248/) - Attach metadata like tags or version info to designs
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Store Custom Metadata"
description: "Attach, retrieve, and manage custom key-value metadata on design blocks in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/export-save-publish/store-custom-metadata-337248/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Store Custom Metadata](https://img.ly/docs/cesdk/android/export-save-publish/store-custom-metadata-337248/)
---
```kotlin file=@cesdk_android_examples/engine-guides-store-metadata/StoreMetadata.kt reference-only
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ShapeType
import org.json.JSONObject
data class StoreMetadata(
val externalId: String?,
val metadataEntries: Map,
val generationModel: String,
val hasUploadedByAfterRemoval: Boolean,
val remainingKeys: List,
val persistedExternalId: String,
)
suspend fun storeCustomMetadata(engine: Engine): StoreMetadata {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val trackedBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(trackedBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(trackedBlock, value = 400F)
engine.block.setHeight(trackedBlock, value = 300F)
engine.block.setPositionX(trackedBlock, value = 200F)
engine.block.setPositionY(trackedBlock, value = 150F)
engine.block.appendChild(parent = page, child = trackedBlock)
engine.block.setMetadata(trackedBlock, key = "externalId", value = "asset-12345")
engine.block.setMetadata(trackedBlock, key = "source", value = "user-upload")
engine.block.setMetadata(trackedBlock, key = "uploadedBy", value = "designer@example.com")
val externalId = if (engine.block.hasMetadata(trackedBlock, key = "externalId")) {
engine.block.getMetadata(trackedBlock, key = "externalId")
} else {
null
}
val metadataEntries = engine.block
.findAllMetadata(trackedBlock)
.associateWith { key -> engine.block.getMetadata(trackedBlock, key = key) }
val generationInfo = JSONObject()
.put("source", "internal-generator")
.put("model", "image-model-v1")
.put("appVersion", "2026.1")
engine.block.setMetadata(
trackedBlock,
key = "generationInfo",
value = generationInfo.toString(),
)
val decodedInfo = JSONObject(engine.block.getMetadata(trackedBlock, key = "generationInfo"))
val generationModel = decodedInfo.getString("model")
if (engine.block.hasMetadata(trackedBlock, key = "uploadedBy")) {
engine.block.removeMetadata(trackedBlock, key = "uploadedBy")
}
val hasUploadedByAfterRemoval = engine.block.hasMetadata(trackedBlock, key = "uploadedBy")
val remainingKeys = engine.block.findAllMetadata(trackedBlock)
val savedScene = engine.scene.saveToString(scene = scene)
engine.scene.load(scene = savedScene)
val reloadedBlock = engine.block.findByType(DesignBlockType.Graphic).first()
val persistedExternalId = engine.block.getMetadata(reloadedBlock, key = "externalId")
return StoreMetadata(
externalId = externalId,
metadataEntries = metadataEntries,
generationModel = generationModel,
hasUploadedByAfterRemoval = hasUploadedByAfterRemoval,
remainingKeys = remainingKeys,
persistedExternalId = persistedExternalId,
)
}
```
Attach custom key-value metadata to design blocks for tracking asset origins,
storing application state, or linking blocks to external systems.
> **Reading time:** 5 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/main/engine-guides-store-metadata)
Metadata lets you store string key-value pairs on any design block. The data is invisible to end users, but it is saved with the scene and restored when the scene is loaded again.
This guide covers how to set, retrieve, list, remove, and persist metadata on blocks. The snippets use a block named `trackedBlock`; in your app, pass the block you want to tag.
## Set Metadata
Use `engine.block.setMetadata()` to attach a key-value pair to a block. Both the key and value are strings. If the key already exists, the new value replaces the old one.
```kotlin highlight-android-set-metadata
engine.block.setMetadata(trackedBlock, key = "externalId", value = "asset-12345")
engine.block.setMetadata(trackedBlock, key = "source", value = "user-upload")
engine.block.setMetadata(trackedBlock, key = "uploadedBy", value = "designer@example.com")
```
You can attach multiple metadata entries to the same block. Each entry is independent and can be read, updated, or removed by key.
## Get Metadata
Use `engine.block.hasMetadata()` before `engine.block.getMetadata()` when the key may be absent. `getMetadata()` fails if the key does not exist, so the guard keeps optional reads explicit.
```kotlin highlight-android-get-metadata
val externalId = if (engine.block.hasMetadata(trackedBlock, key = "externalId")) {
engine.block.getMetadata(trackedBlock, key = "externalId")
} else {
null
}
```
This pattern is useful when metadata comes from user-generated templates, imported scenes, or older scene versions.
## List All Metadata Keys
Use `engine.block.findAllMetadata()` to list every metadata key stored on a block. The sample turns those keys into a map so the app can inspect or sync the current metadata state.
```kotlin highlight-android-find-all-metadata
val metadataEntries = engine.block
.findAllMetadata(trackedBlock)
.associateWith { key -> engine.block.getMetadata(trackedBlock, key = key) }
```
For blocks without metadata, `engine.block.findAllMetadata()` returns an empty list.
## Store Structured Data
Metadata values are strings. To store structured data, serialize the object first and parse it again after reading the value.
```kotlin highlight-android-store-structured-data
val generationInfo = JSONObject()
.put("source", "internal-generator")
.put("model", "image-model-v1")
.put("appVersion", "2026.1")
engine.block.setMetadata(
trackedBlock,
key = "generationInfo",
value = generationInfo.toString(),
)
val decodedInfo = JSONObject(engine.block.getMetadata(trackedBlock, key = "generationInfo"))
val generationModel = decodedInfo.getString("model")
```
This works for app-owned configuration, generation parameters, creator details, or any small payload that can be represented as a string.
## Remove Metadata
Use `engine.block.removeMetadata()` to delete a key-value pair. Guard with `engine.block.hasMetadata()` when the key may not be present.
```kotlin highlight-android-remove-metadata
if (engine.block.hasMetadata(trackedBlock, key = "uploadedBy")) {
engine.block.removeMetadata(trackedBlock, key = "uploadedBy")
}
```
After removal, read the metadata state again when your app needs to update UI, sync state, or verify a cleanup step.
```kotlin highlight-android-verify-removal
val hasUploadedByAfterRemoval = engine.block.hasMetadata(trackedBlock, key = "uploadedBy")
val remainingKeys = engine.block.findAllMetadata(trackedBlock)
```
## Metadata Persistence
Metadata is preserved when you save scene data with `saveToString()` or `saveToArchive()`. Reload scene strings with `engine.scene.load()`. For archive data, write the archive to a URI and reload it with `engine.scene.loadArchive()`.
```kotlin highlight-android-metadata-persistence
val savedScene = engine.scene.saveToString(scene = scene)
engine.scene.load(scene = savedScene)
val reloadedBlock = engine.block.findByType(DesignBlockType.Graphic).first()
val persistedExternalId = engine.block.getMetadata(reloadedBlock, key = "externalId")
```
> **Note:** Metadata only travels with scene data. Exporting to final output formats such
> as PNG, JPEG, PDF, or MP4 writes the exported asset, not the editable scene
> structure or its metadata.
## Troubleshooting
### getMetadata Fails
If `getMetadata()` fails, the key is not set on the block. Check with `hasMetadata()` before retrieving optional metadata.
### Metadata Is Missing After Reloading
Confirm that your app saves editable scene data, then reloads it with the matching API. Use `engine.scene.load()` for strings from `engine.scene.saveToString()`. Use `engine.scene.loadArchive()` for archives from `engine.scene.saveToArchive()` after writing the archive data to a URI. Image, video, and PDF exports do not preserve editable block metadata.
### Metadata Values Are Large
Keep metadata values small. For large payloads, store a stable external ID or URL in metadata and keep the full data in your app backend or local storage.
## API Reference
| Method | Description |
|--------|-------------|
| `engine.block.setMetadata(block=_, key=_, value=_)` | Set or replace a metadata value on a block |
| `engine.block.getMetadata(block=_, key=_)` | Read the metadata value for a key |
| `engine.block.hasMetadata(block=_, key=_)` | Check whether a block has a metadata key |
| `engine.block.findAllMetadata(block=_)` | List all metadata keys stored on a block |
| `engine.block.removeMetadata(block=_, key=_)` | Remove a metadata key from a block |
| `engine.scene.saveToString(scene=_)` | Serialize the editable scene data as a string |
| `engine.scene.saveToArchive(scene=_)` | Serialize the editable scene and referenced assets as an archive |
| `engine.scene.load(scene=_)` | Load a scene from a serialized scene string |
| `engine.scene.loadArchive(archiveUri=_)` | Load a scene from a saved archive URI |
| `engine.block.findByType(type=DesignBlockType.Graphic)` | Find blocks of a specific type after loading |
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "File Format Support"
description: "See which image, video, audio, font, and template formats CE.SDK supports for import and export."
platform: android
url: "https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/) > [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/)
---
## Importing Media
### SVG Limitations
## Exporting Media
## Importing Templates
## Font Formats
## Video & Audio Codecs
CE.SDK supports the most widely adopted video and audio codecs to ensure compatibility across platforms:
## Size Limits
### Image Resolution Limits
### Video Resolution & Duration Limits
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Fills"
description: "Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements."
platform: android
url: "https://img.ly/docs/cesdk/android/fills-402ddc/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/)
---
---
## Related Pages
- [Fills](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements.
- [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/) - Learn how to apply solid color fills to design elements using sRGB, CMYK, and Spot Colors in CE.SDK.
- [Gradient Fills](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/) - Learn how to create and apply linear, radial, and conical gradient fills to design elements in CE.SDK.
- [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) - Apply photos, textures, and patterns to design elements using image fills in CE.SDK for Android.
- [Video Fills](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/) - Learn how to apply video content as fills to Android design blocks in CE.SDK.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Color Fills"
description: "Learn how to apply solid color fills to design elements using sRGB, CMYK, and Spot Colors in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/fills/color-7129cd/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) > [Solid Color](https://img.ly/docs/cesdk/android/fills/color-7129cd/)
---
```kotlin file=@cesdk_android_examples/engine-guides-fills-color/FillsColor.kt reference-only
import android.util.Log
import ly.img.engine.CMYKColor
import ly.img.engine.Color
import ly.img.engine.ColorSpace
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.RGBAColor
import ly.img.engine.ShapeType
import ly.img.engine.SpotColor
private const val TAG = "FillsColor"
suspend fun fillsColor(engine: Engine) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block, value = 200F)
engine.block.setHeight(block, value = 150F)
engine.block.setPositionX(block, value = 50F)
engine.block.setPositionY(block, value = 50F)
engine.block.appendChild(parent = page, child = block)
if (!engine.block.supportsFill(block)) {
error("Block does not support fills.")
}
val colorFill = engine.block.createFill(FillType.Color)
check(engine.block.getType(colorFill) == FillType.Color.key)
val discardedFill = engine.block.createFill(FillType.Color)
engine.block.destroy(discardedFill)
val allFillProperties = engine.block.findAllProperties(colorFill)
Log.i(TAG, "Color fill properties: $allFillProperties")
check("fill/color/value" in allFillProperties)
engine.block.setFill(block = block, fill = colorFill)
val currentFill = engine.block.getFill(block)
val fillType = engine.block.getType(currentFill)
Log.i(TAG, "Fill type: $fillType")
check(currentFill == colorFill)
check(fillType == FillType.Color.key)
val red = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F)
engine.block.setColor(colorFill, property = "fill/color/value", value = red)
check(engine.block.getColor(colorFill, property = "fill/color/value") == red)
val currentColor = engine.block.getColor(colorFill, property = "fill/color/value")
when (currentColor) {
is RGBAColor -> Log.i(TAG, "sRGB: r=${currentColor.r}, g=${currentColor.g}, b=${currentColor.b}")
is CMYKColor -> Log.i(TAG, "CMYK: c=${currentColor.c}, m=${currentColor.m}, y=${currentColor.y}")
is SpotColor -> Log.i(TAG, "Spot: name=${currentColor.name}, tint=${currentColor.tint}")
}
check(currentColor == red)
val cmykBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(cmykBlock, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(cmykBlock, value = 150F)
engine.block.setHeight(cmykBlock, value = 150F)
engine.block.setPositionX(cmykBlock, value = 300F)
engine.block.setPositionY(cmykBlock, value = 50F)
engine.block.appendChild(parent = page, child = cmykBlock)
val cmykFill = engine.block.createFill(FillType.Color)
engine.block.setFill(cmykBlock, fill = cmykFill)
val magenta = Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F, tint = 1F)
engine.block.setColor(cmykFill, property = "fill/color/value", value = magenta)
check(engine.block.getColor(cmykFill, property = "fill/color/value") == magenta)
val spotBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(spotBlock, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(spotBlock, value = 150F)
engine.block.setHeight(spotBlock, value = 150F)
engine.block.setPositionX(spotBlock, value = 500F)
engine.block.setPositionY(spotBlock, value = 50F)
engine.block.appendChild(parent = page, child = spotBlock)
val spotFill = engine.block.createFill(FillType.Color)
engine.block.setFill(spotBlock, fill = spotFill)
engine.editor.setSpotColor(
name = "BrandRed",
color = Color.fromRGBA(r = 0.9F, g = 0.1F, b = 0.1F, a = 1F),
)
val brandRed = Color.fromSpotColor(
name = "BrandRed",
tint = 1F,
externalReference = "BrandBook",
)
engine.block.setColor(spotFill, property = "fill/color/value", value = brandRed)
val appliedSpot = engine.block.getColor(spotFill, property = "fill/color/value")
check(appliedSpot is SpotColor)
check(appliedSpot.name == brandRed.name)
check(appliedSpot.tint == brandRed.tint)
check(appliedSpot.externalReference == brandRed.externalReference)
val toggleBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(toggleBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(toggleBlock, value = 150F)
engine.block.setHeight(toggleBlock, value = 100F)
engine.block.setPositionX(toggleBlock, value = 50F)
engine.block.setPositionY(toggleBlock, value = 250F)
engine.block.appendChild(parent = page, child = toggleBlock)
val toggleFill = engine.block.createFill(FillType.Color)
engine.block.setFill(toggleBlock, fill = toggleFill)
engine.block.setColor(
toggleFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 1F, g = 0.5F, b = 0F, a = 1F),
)
engine.block.setFillEnabled(toggleBlock, enabled = true)
val isEnabled = engine.block.isFillEnabled(toggleBlock)
Log.i(TAG, "Fill enabled: $isEnabled")
engine.block.setFillEnabled(toggleBlock, enabled = !isEnabled)
check(isEnabled)
engine.block.setFillEnabled(toggleBlock, enabled = true)
check(engine.block.isFillEnabled(toggleBlock))
val block1 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block1, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block1, value = 100F)
engine.block.setHeight(block1, value = 100F)
engine.block.setPositionX(block1, value = 250F)
engine.block.setPositionY(block1, value = 250F)
engine.block.appendChild(parent = page, child = block1)
val block2 = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block2, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block2, value = 100F)
engine.block.setHeight(block2, value = 100F)
engine.block.setPositionX(block2, value = 370F)
engine.block.setPositionY(block2, value = 250F)
engine.block.appendChild(parent = page, child = block2)
val sharedFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
sharedFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.5F, g = 0F, b = 0.5F, a = 1F),
)
engine.block.setFill(block1, fill = sharedFill)
engine.block.setFill(block2, fill = sharedFill)
engine.block.setColor(
sharedFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0F, g = 0.5F, b = 0.5F, a = 1F),
)
check(engine.block.getFill(block1) == sharedFill)
check(engine.block.getFill(block2) == sharedFill)
val rgbColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F)
val cmykColor = engine.editor.convertColorToColorSpace(
color = rgbColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
Log.i(TAG, "Converted CMYK color: $cmykColor")
val brandBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(brandBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(brandBlock, value = 150F)
engine.block.setHeight(brandBlock, value = 100F)
engine.block.setPositionX(brandBlock, value = 500F)
engine.block.setPositionY(brandBlock, value = 250F)
engine.block.appendChild(parent = page, child = brandBlock)
engine.editor.setSpotColor(
name = "PrimaryBrand",
color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F),
)
val brandFill = engine.block.createFill(FillType.Color)
engine.block.setFill(brandBlock, fill = brandFill)
engine.block.setColor(
brandFill,
property = "fill/color/value",
value = Color.fromSpotColor(name = "PrimaryBrand"),
)
val brandColor = engine.block.getColor(brandFill, property = "fill/color/value")
check(brandColor is SpotColor)
check(brandColor.name == "PrimaryBrand")
val transparentBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(transparentBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(transparentBlock, value = 150F)
engine.block.setHeight(transparentBlock, value = 100F)
engine.block.setPositionX(transparentBlock, value = 50F)
engine.block.setPositionY(transparentBlock, value = 400F)
engine.block.appendChild(parent = page, child = transparentBlock)
val transparentFill = engine.block.createFill(FillType.Color)
engine.block.setFill(transparentBlock, fill = transparentFill)
val transparentGreen = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.2F, a = 0.5F)
engine.block.setColor(transparentFill, property = "fill/color/value", value = transparentGreen)
val transparencyColor = engine.block.getColor(transparentFill, property = "fill/color/value")
check(transparencyColor is RGBAColor)
check(transparencyColor.a == 0.5F)
val printBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(printBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(printBlock, value = 150F)
engine.block.setHeight(printBlock, value = 100F)
engine.block.setPositionX(printBlock, value = 250F)
engine.block.setPositionY(printBlock, value = 400F)
engine.block.appendChild(parent = page, child = printBlock)
val printFill = engine.block.createFill(FillType.Color)
engine.block.setFill(printBlock, fill = printFill)
val printColor = Color.fromCMYK(c = 0F, m = 0.85F, y = 1F, k = 0F, tint = 1F)
engine.block.setColor(printFill, property = "fill/color/value", value = printColor)
check(engine.block.getColor(printFill, property = "fill/color/value") == printColor)
}
```
Apply uniform solid colors to shapes, text, and design blocks with CE.SDK's
color fill system.
> **Reading time:** 15 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-fills-color)

Color fills are one of the fundamental fill types in CE.SDK. They paint a block with one solid color instead of a gradient, image, or video. On Android, create a color fill with `FillType.Color`, set its `fill/color/value` property with a `Color` value, and attach the fill to any block that supports fills.
This guide demonstrates how to create, apply, and modify color fills, work with sRGB, CMYK, and Spot Color values, and manage fill state for design blocks.
## Understanding Color Fills
### What is a Color Fill?
A color fill is a fill object identified by `FillType.Color`, whose engine type is `"//ly.img.ubq/fill/color"`. It contains the `fill/color/value` property that stores the actual color value.
Color fills differ from other fill types available in CE.SDK:
- **Color fills**: Solid, uniform color across the entire block
- **Gradient fills**: Color transitions such as linear, radial, and conical gradients
- **Image fills**: Photo or raster content
- **Video fills**: Animated video content
- **Pixel stream fills**: Live camera or stream content
### Supported Color Spaces
CE.SDK's color fill system supports the same Android `Color` values used by other color properties:
- **sRGB**: Red, green, blue, and alpha values for screen display through `Color.fromRGBA()`, `Color.fromHex()`, `Color.fromColor()`, or `Color.fromResource()`
- **CMYK**: Cyan, magenta, yellow, key, and tint values for print workflows through `Color.fromCMYK()`
- **Spot Color**: Named colors registered with `engine.editor.setSpotColor(name=_, color=_)` and referenced from fills through `Color.fromSpotColor()`
Use sRGB for digital designs, CMYK for print-ready content, and Spot Colors when you need a named brand or production color.
## Checking Color Fill Support
### Verifying Block Compatibility
Before applying a color fill, verify that the target block supports fills. Graphic blocks, shapes, and text blocks typically support fills; scene blocks do not.
```kotlin highlight-android-check-fill-support
if (!engine.block.supportsFill(block)) {
error("Block does not support fills.")
}
```
Branch on `supportsFill()` before calling fill APIs on arbitrary blocks. This avoids runtime errors when a selected block cannot render fill content.
## Creating Color Fills
### Creating a New Color Fill
Create a color fill with `engine.block.createFill(FillType.Color)`:
```kotlin highlight-android-create-fill
val colorFill = engine.block.createFill(FillType.Color)
```
The method returns a fill block handle. The fill exists independently until you attach it to a block with `engine.block.setFill()`. If you create a fill and discard it before assigning it, clean up the unused fill with `engine.block.destroy(block=_)`.
### Default Color Fill Properties
New color fills start with opaque black (`Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F)`) by default. Use `findAllProperties()` to inspect the properties available on a color fill, then set `fill/color/value` explicitly before relying on the rendered color:
```kotlin highlight-android-default-properties
val allFillProperties = engine.block.findAllProperties(colorFill)
Log.i(TAG, "Color fill properties: $allFillProperties")
```
The returned list includes `fill/color/value`, which is the property used with `engine.block.setColor()` and `engine.block.getColor()` throughout this guide.
## Applying Color Fills
### Setting a Fill on a Block
Attach the fill to a supported block with `setFill()`:
```kotlin highlight-android-apply-fill
engine.block.setFill(block = block, fill = colorFill)
```
The block now renders with the fill's current color. Assigning a new fill does not automatically destroy the previous fill, so clean up replaced fills that are no longer needed.
### Getting the Current Fill
Use `getFill()` to retrieve the fill attached to a block, then inspect its type with `getType()`:
```kotlin highlight-android-get-fill
val currentFill = engine.block.getFill(block)
val fillType = engine.block.getType(currentFill)
Log.i(TAG, "Fill type: $fillType")
```
For a color fill, `getType()` returns the engine type behind `FillType.Color`.
## Modifying Color Fill Properties
### Setting RGB Colors
Set the fill color with `engine.block.setColor()`. sRGB values use normalized floats from 0.0 to 1.0, and `a` controls opacity.
```kotlin highlight-android-set-rgb
val red = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F)
engine.block.setColor(colorFill, property = "fill/color/value", value = red)
```
Use `engine.block.setFillSolidColor()` when you only need to set an RGBA fill directly on a block. This shortcut accepts `RGBAColor` only; use `engine.block.setColor()` on `fill/color/value` for `CMYKColor` or `SpotColor` values.
An alpha value of `1.0` is fully opaque, and `0.0` is fully transparent.
### Setting CMYK Colors
For print workflows, create a `CMYKColor` with normalized `c`, `m`, `y`, `k`, and `tint` values:
```kotlin highlight-android-set-cmyk
val magenta = Color.fromCMYK(c = 0F, m = 1F, y = 0F, k = 0F, tint = 1F)
engine.block.setColor(cmykFill, property = "fill/color/value", value = magenta)
```
Tint lets you apply a lighter version of the CMYK color without changing the base components.
### Setting Spot Colors
Register a spot color before you use it in a fill. The registered approximation lets CE.SDK preview the spot color on screen.
```kotlin highlight-android-set-spot
engine.editor.setSpotColor(
name = "BrandRed",
color = Color.fromRGBA(r = 0.9F, g = 0.1F, b = 0.1F, a = 1F),
)
val brandRed = Color.fromSpotColor(
name = "BrandRed",
tint = 1F,
externalReference = "BrandBook",
)
engine.block.setColor(spotFill, property = "fill/color/value", value = brandRed)
```
The `externalReference` value can store the name of an external system or color book. Omit it when you only need the spot color name.
For PDF export workflows, `engine.block.setFillOverprint()` marks a block's spot-color fill as overprint. Process-color fills ignore the overprint flag.
### Getting Current Color Value
Read a color fill value with `getColor()`. The returned `Color` subtype preserves the original color space.
```kotlin highlight-android-get-color
val currentColor = engine.block.getColor(colorFill, property = "fill/color/value")
when (currentColor) {
is RGBAColor -> Log.i(TAG, "sRGB: r=${currentColor.r}, g=${currentColor.g}, b=${currentColor.b}")
is CMYKColor -> Log.i(TAG, "CMYK: c=${currentColor.c}, m=${currentColor.m}, y=${currentColor.y}")
is SpotColor -> Log.i(TAG, "Spot: name=${currentColor.name}, tint=${currentColor.tint}")
}
```
Use `when` on `RGBAColor`, `CMYKColor`, and `SpotColor` when your app needs to branch by color space.
## Enabling and Disabling Color Fills
### Toggle Fill Visibility
Disable a fill without removing it from the block by changing the block's fill-enabled state:
```kotlin highlight-android-toggle-fill
val isEnabled = engine.block.isFillEnabled(toggleBlock)
Log.i(TAG, "Fill enabled: $isEnabled")
engine.block.setFillEnabled(toggleBlock, enabled = !isEnabled)
```
Disabling a fill preserves its properties. This is useful for stroke-only designs or temporary UI states where the fill should be hidden and restored later.
## Additional Techniques
### Sharing Color Fills
You can assign the same fill object to multiple blocks. Changing the shared fill updates every block that references it.
```kotlin highlight-android-share-fill
val sharedFill = engine.block.createFill(FillType.Color)
engine.block.setColor(
sharedFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.5F, g = 0F, b = 0.5F, a = 1F),
)
engine.block.setFill(block1, fill = sharedFill)
engine.block.setFill(block2, fill = sharedFill)
engine.block.setColor(
sharedFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 0F, g = 0.5F, b = 0.5F, a = 1F),
)
```
Shared fills are useful for synchronized brand elements. Keep ownership clear in your app code so you only destroy the shared fill after no block needs it.
### Color Space Conversion
Convert color values to sRGB or CMYK with `convertColorToColorSpace()`:
```kotlin highlight-android-convert-color
val rgbColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F)
val cmykColor = engine.editor.convertColorToColorSpace(
color = rgbColor,
colorSpace = ColorSpace.CMYK,
) as CMYKColor
Log.i(TAG, "Converted CMYK color: $cmykColor")
```
Use conversion when you need a screen preview for print colors or a CMYK value for print output. Spot Color values are valid source colors, but `ColorSpace.SPOT_COLOR` is not a conversion target.
## Common Use Cases
### Brand Color Application
Define a brand color as a Spot Color, then reference that name from a color fill:
```kotlin highlight-android-brand-colors
engine.editor.setSpotColor(
name = "PrimaryBrand",
color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F),
)
val brandFill = engine.block.createFill(FillType.Color)
engine.block.setFill(brandBlock, fill = brandFill)
engine.block.setColor(
brandFill,
property = "fill/color/value",
value = Color.fromSpotColor(name = "PrimaryBrand"),
)
```
This example registers `PrimaryBrand` once and applies it by name. To change that brand color later, call `engine.editor.setSpotColor()` again with the same name; fills referencing that name use the updated definition.
### Transparency Effects
Create semi-transparent overlays by lowering the sRGB alpha value:
```kotlin highlight-android-transparency
val transparentGreen = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.2F, a = 0.5F)
engine.block.setColor(transparentFill, property = "fill/color/value", value = transparentGreen)
```
Use this for overlays, highlights, or layered compositions where content below the block should remain visible.
### Print-Ready Colors
Use CMYK values when a design is intended for print production:
```kotlin highlight-android-print-colors
val printColor = Color.fromCMYK(c = 0F, m = 0.85F, y = 1F, k = 0F, tint = 1F)
engine.block.setColor(printFill, property = "fill/color/value", value = printColor)
```
CMYK values can be assigned to the same `fill/color/value` property as sRGB and Spot Color values.
## Troubleshooting
### Fill Not Visible
If your fill does not appear:
- Check that `engine.block.isFillEnabled(block=_)` returns `true`
- Verify that an sRGB color has an alpha value above `0.0`
- Ensure the block has dimensions greater than `0`
- Confirm the block is part of the scene hierarchy
### Color Looks Different Than Expected
If colors do not match expectations:
- Verify the color space you assign: `RGBAColor`, `CMYKColor`, or `SpotColor`
- Register a spot color before referencing it from `Color.fromSpotColor()`
- Review tint values, which should be between `0.0` and `1.0`
- Convert values with `engine.editor.convertColorToColorSpace()` when comparing screen and print colors
### Memory Leaks
To avoid leaking fill blocks, apply the same block-lifecycle cleanup pattern when your code discards or replaces fill objects:
- Destroy fills that you create but never assign
- Destroy replaced fills when no block still references them
- Keep shared-fill ownership explicit when multiple blocks reuse the same fill
### Cannot Apply Color to Block
If a color fill cannot be applied:
- Verify the block supports fills with `engine.block.supportsFill(block=_)`
- Ensure the block has a compatible shape or text surface
- Check that the fill object is still valid and was not destroyed earlier
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.supportsFill(block=_)` | Check whether a block supports fill APIs. |
| `engine.block.createFill(fillType=FillType.Color)` | Create a new color fill object. |
| `engine.block.findAllProperties(block=_)` | List properties available on a block or fill. |
| `engine.block.setFill(block=_, fill=_)` | Assign a fill object to a block. |
| `engine.block.getFill(block=_)` | Get the fill object currently assigned to a block. |
| `engine.block.getType(block=_)` | Read the engine type of a block or fill. |
| `engine.block.destroy(block=_)` | Destroy an unneeded block or fill object. |
| `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set an `RGBAColor`, `CMYKColor`, or `SpotColor` value on a color fill. |
| `engine.block.getColor(block=_, property="fill/color/value")` | Read the color value from a color fill. |
| `engine.block.setFillSolidColor(block=_, color=_)` | Set an `RGBAColor` fill directly on a block; use `setColor()` for CMYK or Spot Color fills. |
| `engine.block.getFillSolidColor(block=_)` | Read a block's fill color as RGBA. |
| `engine.block.isFillEnabled(block=_)` | Check whether the block's fill is visible. |
| `engine.block.setFillEnabled(block=_, enabled=_)` | Enable or disable fill rendering for a block. |
| `engine.block.setFillOverprint(block=_, overprint=_)` | Mark a spot-color fill as overprint for PDF export. |
| `engine.block.getFillOverprint(block=_)` | Check whether a block's fill is marked for PDF overprint. |
| `engine.editor.setSpotColor(name=_, color=_)` | Define or update a spot color with an sRGB or CMYK approximation. |
| `engine.editor.findAllSpotColors()` | List the names of registered spot colors. |
| `engine.editor.getSpotColorRGB(name=_)` | Read the sRGB representation for a spot color name. |
| `engine.editor.getSpotColorCMYK(name=_)` | Read the CMYK representation for a spot color name. |
| `engine.editor.removeSpotColor(name=_)` | Remove a spot color registration. |
| `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Convert a color value to sRGB or CMYK. |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an `RGBAColor` from normalized float or 0-255 integer components. |
| `Color.fromColor(color=_)` | Create an `RGBAColor` from an Android `@ColorInt`. |
| `Color.fromResource(colorResource=_, context=_)` | Create an `RGBAColor` from an Android `@ColorRes`; `context` is optional. |
| `Color.fromHex(colorString=_)` | Create an `RGBAColor` from a hex string such as `"#FFFFFFFF"`. |
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=1F)` | Create a `CMYKColor` with normalized CMYK components and optional tint. |
| `Color.fromSpotColor(name=_, tint=1F, externalReference=null)` | Create a `SpotColor` reference to a registered spot color name. |
## Related Types
| Type | Description |
| --- | --- |
| `FillType.Color` | Type-safe Android constant for `"//ly.img.ubq/fill/color"`. |
| `RGBAColor` | sRGB color with `r`, `g`, `b`, and `a` components from `0.0` to `1.0`. |
| `CMYKColor` | CMYK color with `c`, `m`, `y`, `k`, and `tint` components from `0.0` to `1.0`. |
| `SpotColor` | Named spot color reference with `name`, `tint`, and optional `externalReference`. |
| `ColorSpace` | Enum cases are `ColorSpace.SRGB`, `ColorSpace.CMYK`, and `ColorSpace.SPOT_COLOR`. Pass only `ColorSpace.SRGB` or `ColorSpace.CMYK` as targets to `convertColorToColorSpace()`. |
## Next Steps
Now that you understand color fills, explore other fill types and color management features:
- [Fills Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Understand the comprehensive fill system and all available fill types
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Learn about color management across fills, strokes, and shadows
- [Blocks Concept](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Understand the block system that design elements are built on
- [Gradient Fills](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/) — Create color transitions with linear, radial, and conical gradients
- [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) — Display photo and raster content in design blocks
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Gradient Fills"
description: "Learn how to create and apply linear, radial, and conical gradient fills to design elements in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/fills/gradient-0ff079/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) > [Gradient](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/)
---
```kotlin file=@cesdk_android_examples/engine-guides-gradient-fills/GradientFills.kt reference-only
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.GradientColorStop
import ly.img.engine.ShapeType
suspend fun gradientFills(engine: Engine): GradientFillsResult {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val linearBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(linearBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(linearBlock, value = 60F)
engine.block.setPositionY(linearBlock, value = 80F)
engine.block.setWidth(linearBlock, value = 200F)
engine.block.setHeight(linearBlock, value = 160F)
engine.block.appendChild(parent = page, child = linearBlock)
val radialBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(radialBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(radialBlock, value = 300F)
engine.block.setPositionY(radialBlock, value = 80F)
engine.block.setWidth(radialBlock, value = 200F)
engine.block.setHeight(radialBlock, value = 160F)
engine.block.appendChild(parent = page, child = radialBlock)
val conicalBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(conicalBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(conicalBlock, value = 540F)
engine.block.setPositionY(conicalBlock, value = 80F)
engine.block.setWidth(conicalBlock, value = 160F)
engine.block.setHeight(conicalBlock, value = 160F)
engine.block.appendChild(parent = page, child = conicalBlock)
val textBlock = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(textBlock, text = "Solid color text")
engine.block.setPositionX(textBlock, value = 60F)
engine.block.setPositionY(textBlock, value = 460F)
engine.block.setWidth(textBlock, value = 280F)
engine.block.appendChild(parent = page, child = textBlock)
val sceneSupportsFill = engine.block.supportsFill(scene)
val pageSupportsFill = engine.block.supportsFill(page)
val graphicSupportsFill = engine.block.supportsFill(linearBlock)
val textSupportsFill = engine.block.supportsFill(textBlock)
check(!sceneSupportsFill)
check(pageSupportsFill)
check(graphicSupportsFill)
check(textSupportsFill)
val pageGradient = engine.block.createFill(FillType.LinearGradient)
val linearGradient = engine.block.createFill(FillType.LinearGradient)
val radialGradient = engine.block.createFill(FillType.RadialGradient)
val conicalGradient = engine.block.createFill(FillType.ConicalGradient)
val previousPageFill = engine.block.getFill(page)
val previousLinearFill = engine.block.getFill(linearBlock)
engine.block.setFill(page, fill = pageGradient)
engine.block.setFill(linearBlock, fill = linearGradient)
// Only destroy previous fills when you know they are not shared by other blocks.
if (engine.block.isValid(previousPageFill)) {
engine.block.destroy(previousPageFill)
}
if (engine.block.isValid(previousLinearFill)) {
engine.block.destroy(previousLinearFill)
}
val pageFillType = engine.block.getType(engine.block.getFill(page))
val currentFill = engine.block.getFill(linearBlock)
val currentFillType = engine.block.getType(currentFill)
check(pageFillType == FillType.LinearGradient.key)
check(currentFill == linearGradient)
check(currentFillType == FillType.LinearGradient.key)
val textSolidColor = Color.fromRGBA(r = 0.1F, g = 0.1F, b = 0.1F, a = 1F)
engine.block.setFillSolidColor(textBlock, color = textSolidColor)
val currentTextColor = engine.block.getFillSolidColor(textBlock)
val rejectedTextGradient = engine.block.createFill(FillType.LinearGradient)
val textRejectsGradientFill = runCatching {
engine.block.setFill(textBlock, fill = rejectedTextGradient)
}.isFailure
if (textRejectsGradientFill) {
engine.block.destroy(rejectedTextGradient)
}
check(currentTextColor == textSolidColor)
check(engine.block.getType(engine.block.getFill(textBlock)) == FillType.Color.key)
check(textRejectsGradientFill)
val linearColorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 1F, g = 0.8F, b = 0.2F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.3F, g = 0.4F, b = 0.7F, a = 1F)),
)
engine.block.setGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
colorStops = linearColorStops,
)
val currentColorStops = engine.block.getGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
)
check(currentColorStops == linearColorStops)
engine.editor.setSpotColor(
name = "BrandPrimary",
color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F),
)
val colorSpaceStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromHex("#3366CC")),
GradientColorStop(stop = 0.25F, color = Color.fromRGBA(r = 255, g = 204, b = 0, a = 255)),
GradientColorStop(
stop = 0.75F,
color = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 1F),
),
GradientColorStop(
stop = 1F,
color = Color.fromSpotColor(name = "BrandPrimary", tint = 1F),
),
)
engine.block.setGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
colorStops = colorSpaceStops,
)
val currentColorSpaceStops = engine.block.getGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
)
check(currentColorSpaceStops == colorSpaceStops)
engine.block.setGradientColorStops(linearGradient, property = "fill/gradient/colors", colorStops = linearColorStops)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/startPointX", value = 0F)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/startPointY", value = 0F)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/endPointX", value = 1F)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/endPointY", value = 1F)
val linearStartX = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/startPointX")
val linearStartY = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/startPointY")
val linearEndX = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/endPointX")
val linearEndY = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/endPointY")
check(linearStartX == 0F)
check(linearStartY == 0F)
check(linearEndX == 1F)
check(linearEndY == 1F)
val previousRadialFill = engine.block.getFill(radialBlock)
engine.block.setFill(radialBlock, fill = radialGradient)
if (engine.block.isValid(previousRadialFill)) {
engine.block.destroy(previousRadialFill)
}
val radialColorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.3F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
)
engine.block.setGradientColorStops(
block = radialGradient,
property = "fill/gradient/colors",
colorStops = radialColorStops,
)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointX", value = 0.5F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointY", value = 0.5F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/radius", value = 0.8F)
val radialCenterX = engine.block.getFloat(radialGradient, property = "fill/gradient/radial/centerPointX")
val radialCenterY = engine.block.getFloat(radialGradient, property = "fill/gradient/radial/centerPointY")
val radialRadius = engine.block.getFloat(radialGradient, property = "fill/gradient/radial/radius")
check(radialCenterX == 0.5F)
check(radialCenterY == 0.5F)
check(radialRadius == 0.8F)
val previousConicalFill = engine.block.getFill(conicalBlock)
engine.block.setFill(conicalBlock, fill = conicalGradient)
if (engine.block.isValid(previousConicalFill)) {
engine.block.destroy(previousConicalFill)
}
val conicalColorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
GradientColorStop(stop = 0.75F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 0F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
)
engine.block.setGradientColorStops(
block = conicalGradient,
property = "fill/gradient/colors",
colorStops = conicalColorStops,
)
engine.block.setFloat(conicalGradient, property = "fill/gradient/conical/centerPointX", value = 0.5F)
engine.block.setFloat(conicalGradient, property = "fill/gradient/conical/centerPointY", value = 0.5F)
val conicalCenterX = engine.block.getFloat(conicalGradient, property = "fill/gradient/conical/centerPointX")
val conicalCenterY = engine.block.getFloat(conicalGradient, property = "fill/gradient/conical/centerPointY")
check(conicalCenterX == 0.5F)
check(conicalCenterY == 0.5F)
val auroraStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.4F, g = 0.1F, b = 0.8F, a = 1F)),
GradientColorStop(stop = 0.3F, color = Color.fromRGBA(r = 0.8F, g = 0.2F, b = 0.6F, a = 1F)),
GradientColorStop(stop = 0.6F, color = Color.fromRGBA(r = 1F, g = 0.5F, b = 0.3F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 1F, g = 0.8F, b = 0.2F, a = 1F)),
)
engine.block.setGradientColorStops(
block = pageGradient,
property = "fill/gradient/colors",
colorStops = auroraStops,
)
check(engine.block.getGradientColorStops(pageGradient, property = "fill/gradient/colors") == auroraStops)
val buttonHighlightStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.3F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointX", value = 0.5F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointY", value = 0.35F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/radius", value = 0.9F)
engine.block.setGradientColorStops(
block = radialGradient,
property = "fill/gradient/colors",
colorStops = buttonHighlightStops,
)
check(engine.block.getGradientColorStops(radialGradient, property = "fill/gradient/colors") == buttonHighlightStops)
val spinnerStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
GradientColorStop(stop = 0.75F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 0F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
)
engine.block.setGradientColorStops(
block = conicalGradient,
property = "fill/gradient/colors",
colorStops = spinnerStops,
)
check(engine.block.getGradientColorStops(conicalGradient, property = "fill/gradient/colors") == spinnerStops)
val transparencyStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.7F)),
)
engine.block.setGradientColorStops(
block = pageGradient,
property = "fill/gradient/colors",
colorStops = transparencyStops,
)
check(engine.block.getGradientColorStops(pageGradient, property = "fill/gradient/colors") == transparencyStops)
val duotoneStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.8F, g = 0.2F, b = 0.9F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.9F, b = 0.8F, a = 1F)),
)
engine.block.setGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
colorStops = duotoneStops,
)
check(engine.block.getGradientColorStops(linearGradient, property = "fill/gradient/colors") == duotoneStops)
engine.block.setGradientColorStops(linearGradient, property = "fill/gradient/colors", colorStops = linearColorStops)
val sharedBlockA = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(sharedBlockA, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(sharedBlockA, value = 120F)
engine.block.setPositionY(sharedBlockA, value = 300F)
engine.block.setWidth(sharedBlockA, value = 160F)
engine.block.setHeight(sharedBlockA, value = 120F)
engine.block.appendChild(parent = page, child = sharedBlockA)
val sharedBlockB = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(sharedBlockB, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(sharedBlockB, value = 340F)
engine.block.setPositionY(sharedBlockB, value = 300F)
engine.block.setWidth(sharedBlockB, value = 160F)
engine.block.setHeight(sharedBlockB, value = 120F)
engine.block.appendChild(parent = page, child = sharedBlockB)
val sharedGradient = engine.block.createFill(FillType.LinearGradient)
val previousSharedFillA = engine.block.getFill(sharedBlockA)
val previousSharedFillB = engine.block.getFill(sharedBlockB)
engine.block.setFill(sharedBlockA, fill = sharedGradient)
engine.block.setFill(sharedBlockB, fill = sharedGradient)
// Only destroy previous fills when you know they are not shared by other blocks.
if (engine.block.isValid(previousSharedFillA)) {
engine.block.destroy(previousSharedFillA)
}
if (engine.block.isValid(previousSharedFillB)) {
engine.block.destroy(previousSharedFillB)
}
val sharedGradientStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 1F, g = 1F, b = 0F, a = 1F)),
)
engine.block.setGradientColorStops(sharedGradient, property = "fill/gradient/colors", colorStops = sharedGradientStops)
check(engine.block.getFill(sharedBlockA) == sharedGradient)
check(engine.block.getFill(sharedBlockB) == sharedGradient)
val duplicateBlock = engine.block.duplicate(linearBlock)
val duplicateGradient = engine.block.getFill(duplicateBlock)
engine.block.setGradientColorStops(
block = duplicateGradient,
property = "fill/gradient/colors",
colorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.8F, g = 0.2F, b = 0.9F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.9F, b = 0.8F, a = 1F)),
),
)
val duplicateFillIsIndependent = duplicateGradient != linearGradient &&
engine.block.getGradientColorStops(linearGradient, property = "fill/gradient/colors") == linearColorStops
check(duplicateFillIsIndependent)
return GradientFillsResult(
sceneSupportsFill = sceneSupportsFill,
pageSupportsFill = pageSupportsFill,
graphicSupportsFill = graphicSupportsFill,
textSupportsFill = textSupportsFill,
textRejectsGradientFill = textRejectsGradientFill,
pageFillType = pageFillType,
linearFillType = currentFillType,
radialFillType = engine.block.getType(radialGradient),
conicalFillType = engine.block.getType(conicalGradient),
linearStops = currentColorStops,
colorSpaceStops = currentColorSpaceStops,
linearStartX = linearStartX,
linearStartY = linearStartY,
linearEndX = linearEndX,
linearEndY = linearEndY,
radialRadius = radialRadius,
conicalCenterX = conicalCenterX,
sharedStops = sharedGradientStops,
duplicateFillIsIndependent = duplicateFillIsIndependent,
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-gradient-fills/GradientFillsResult.kt reference-only
import ly.img.engine.GradientColorStop
data class GradientFillsResult(
val sceneSupportsFill: Boolean,
val pageSupportsFill: Boolean,
val graphicSupportsFill: Boolean,
val textSupportsFill: Boolean,
val textRejectsGradientFill: Boolean,
val pageFillType: String,
val linearFillType: String,
val radialFillType: String,
val conicalFillType: String,
val linearStops: List,
val colorSpaceStops: List,
val linearStartX: Float,
val linearStartY: Float,
val linearEndX: Float,
val linearEndY: Float,
val radialRadius: Float,
val conicalCenterX: Float,
val sharedStops: List,
val duplicateFillIsIndependent: Boolean,
)
```
Create smooth color transitions on design blocks with linear, radial, and
conical gradient fills.

> **Reading time:** 12 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-gradient-fills)
Gradient fills are part of CE.SDK's fill system. They paint supported blocks with color transitions instead of a single solid color, image, or video.
This guide shows the Android Engine APIs for creating gradient fills, applying them to blocks, configuring color stops, and positioning each gradient type.
## Understanding Gradient Fills
### What Is a Gradient Fill?
A gradient fill is a fill block attached to a design block. Android exposes the three gradient fill types as `FillType.LinearGradient`, `FillType.RadialGradient`, and `FillType.ConicalGradient`.
Each gradient stores color stops on `fill/gradient/colors` and type-specific positioning properties on the fill block.
### Gradient Types Comparison
| Type | Android fill type | Best for |
| ------- | -------------------------- | -------------------------------------------------------------------- |
| Linear | `FillType.LinearGradient` | Backgrounds, banners, buttons, and directional color transitions |
| Radial | `FillType.RadialGradient` | Spotlights, button highlights, vignettes, and depth effects |
| Conical | `FillType.ConicalGradient` | Circular progress, color wheels, spinners, and pie-chart style fills |
### Gradient vs Other Fill Types
Use gradient fills when a block needs a transition between multiple colors. Use color fills for uniform color, image fills for raster media, and video fills for animated media.
### Color Stops Explained
Color stops define the colors and positions in the transition. Each `GradientColorStop` has a `color` and a normalized `stop` value from `0.0` to `1.0`.
Use two or more color stops when you want a visible transition. Keep stop positions in ascending order, make each position unique, use values from `0.0` to `1.0`, and adjust each stop's alpha channel when the gradient needs transparency.
## Using the Built-in Gradient UI
The CE.SDK editor UI exposes fill controls through the fill and stroke sheet. The current Android UI lets users switch between no fill, solid fill, and linear gradient fill, then edit two gradient colors and the linear gradient angle.
Radial and conical gradients are available through the Engine APIs shown below. For a complete Android editing surface that includes the fill controls, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/).
## Checking Gradient Fill Support
### Verifying Block Compatibility
Check support before reading or changing fill data. Scenes do not support fills, while pages, graphics, and text blocks report fill support.
```kotlin highlight-android-check-fill-support
val sceneSupportsFill = engine.block.supportsFill(scene)
val pageSupportsFill = engine.block.supportsFill(page)
val graphicSupportsFill = engine.block.supportsFill(linearBlock)
val textSupportsFill = engine.block.supportsFill(textBlock)
```
`supportsFill()` only tells you whether a block has fill support. Scenes do not support fills. Pages and graphic blocks can receive gradient fills, while text blocks are limited to solid color fills.
## Creating Gradient Fills
Create gradient fill blocks with the type-safe Android `FillType` constants.
```kotlin highlight-android-create-gradients
val pageGradient = engine.block.createFill(FillType.LinearGradient)
val linearGradient = engine.block.createFill(FillType.LinearGradient)
val radialGradient = engine.block.createFill(FillType.RadialGradient)
val conicalGradient = engine.block.createFill(FillType.ConicalGradient)
```
A created fill exists independently until you attach it to a block. Destroy unused fills if you create them and then decide not to attach them.
## Applying Gradient Fills
### Setting a Gradient Fill on a Block
Attach a gradient fill to a supported block with `engine.block.setFill()`, then inspect the fill with `getFill()` and `getType()`.
```kotlin highlight-android-apply-gradient
val previousPageFill = engine.block.getFill(page)
val previousLinearFill = engine.block.getFill(linearBlock)
engine.block.setFill(page, fill = pageGradient)
engine.block.setFill(linearBlock, fill = linearGradient)
// Only destroy previous fills when you know they are not shared by other blocks.
if (engine.block.isValid(previousPageFill)) {
engine.block.destroy(previousPageFill)
}
if (engine.block.isValid(previousLinearFill)) {
engine.block.destroy(previousLinearFill)
}
val pageFillType = engine.block.getType(engine.block.getFill(page))
val currentFill = engine.block.getFill(linearBlock)
val currentFillType = engine.block.getType(currentFill)
```
`getFill()` returns another block ID. Pass that fill handle into property APIs such as `setGradientColorStops()`, `setFloat()`, and `getFloat()`.
Use gradient fills for pages, graphics, and other blocks that accept non-solid fill types.
### Text Fill Compatibility
Text blocks support fill APIs for solid colors only. When a fill type is unsupported, `setFill()` throws; the sample uses `runCatching` to guard the call and then destroys the unattached gradient fill. Use `setFillSolidColor()` and `getFillSolidColor()` when styling text with a solid fill color.
```kotlin highlight-android-text-fill-compatibility
val textSolidColor = Color.fromRGBA(r = 0.1F, g = 0.1F, b = 0.1F, a = 1F)
engine.block.setFillSolidColor(textBlock, color = textSolidColor)
val currentTextColor = engine.block.getFillSolidColor(textBlock)
val rejectedTextGradient = engine.block.createFill(FillType.LinearGradient)
val textRejectsGradientFill = runCatching {
engine.block.setFill(textBlock, fill = rejectedTextGradient)
}.isFailure
if (textRejectsGradientFill) {
engine.block.destroy(rejectedTextGradient)
}
```
## Configuring Gradient Color Stops
### Setting Color Stops
Use `GradientColorStop` values with normalized positions. For visible transitions, pass two or more stops. Keep each stop between `0.0` and `1.0`, make stop positions unique, and order them from the start of the gradient to the end. Android color stops can use any CE.SDK `Color` value, including sRGB, CMYK, and Spot Color.
```kotlin highlight-android-color-stops
val linearColorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 1F, g = 0.8F, b = 0.2F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.3F, g = 0.4F, b = 0.7F, a = 1F)),
)
engine.block.setGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
colorStops = linearColorStops,
)
val currentColorStops = engine.block.getGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
)
```
### Getting Color Stops
Read the same `fill/gradient/colors` property to inspect the current color stop list. The setting snippet above includes the matching `getGradientColorStops()` readback after the update.
### Using Different Color Spaces
Gradient stops accept any CE.SDK `Color` value. The snippet below combines Android-native app colors from a hex string and 0-255 channel values with CMYK and Spot Color values for print-oriented stops. Define a spot color before referencing it with `Color.fromSpotColor()`.
```kotlin highlight-android-color-spaces
engine.editor.setSpotColor(
name = "BrandPrimary",
color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F),
)
val colorSpaceStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromHex("#3366CC")),
GradientColorStop(stop = 0.25F, color = Color.fromRGBA(r = 255, g = 204, b = 0, a = 255)),
GradientColorStop(
stop = 0.75F,
color = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 1F),
),
GradientColorStop(
stop = 1F,
color = Color.fromSpotColor(name = "BrandPrimary", tint = 1F),
),
)
engine.block.setGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
colorStops = colorSpaceStops,
)
```
## Positioning Linear Gradients
### Setting Start and End Points
Linear gradients use normalized start and end coordinates relative to the block frame. `(0, 0)` is the top-left corner and `(1, 1)` is the bottom-right corner.
```kotlin highlight-android-linear-position
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/startPointX", value = 0F)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/startPointY", value = 0F)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/endPointX", value = 1F)
engine.block.setFloat(linearGradient, property = "fill/gradient/linear/endPointY", value = 1F)
val linearStartX = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/startPointX")
val linearStartY = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/startPointY")
val linearEndX = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/endPointX")
val linearEndY = engine.block.getFloat(linearGradient, property = "fill/gradient/linear/endPointY")
```
### Common Linear Gradient Directions
| Direction | Start | End |
| ------------------------ | ---------- | ---------- |
| Left to right | `(0, 0.5)` | `(1, 0.5)` |
| Top to bottom | `(0.5, 0)` | `(0.5, 1)` |
| Top-left to bottom-right | `(0, 0)` | `(1, 1)` |
### Getting Current Position
Use `engine.block.getFloat()` with the same position property keys to read start and end points. The start and end point snippet above includes the matching readbacks for each linear gradient coordinate.
## Positioning Radial Gradients
### Setting Center Point and Radius
Radial gradients use a normalized center point and a radius relative to the smaller side of the block frame.
```kotlin highlight-android-radial-gradient
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointX", value = 0.5F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointY", value = 0.5F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/radius", value = 0.8F)
val radialCenterX = engine.block.getFloat(radialGradient, property = "fill/gradient/radial/centerPointX")
val radialCenterY = engine.block.getFloat(radialGradient, property = "fill/gradient/radial/centerPointY")
val radialRadius = engine.block.getFloat(radialGradient, property = "fill/gradient/radial/radius")
```
### Common Radial Patterns
| Pattern | Center | Radius |
| --------------------- | ------------ | ------ |
| Centered circle | `(0.5, 0.5)` | `0.7` |
| Top-left highlight | `(0, 0)` | `1.0` |
| Bottom-right vignette | `(1, 1)` | `1.5` |
## Positioning Conical Gradients
### Setting Center Point
Conical gradients sweep around a normalized center point using the configured color stops. The sweep starts from the positive X direction, which appears as 3 o'clock from that center point, and later stop values advance clockwise around the center.
```kotlin highlight-android-conical-gradient
engine.block.setFloat(conicalGradient, property = "fill/gradient/conical/centerPointX", value = 0.5F)
engine.block.setFloat(conicalGradient, property = "fill/gradient/conical/centerPointY", value = 0.5F)
val conicalCenterX = engine.block.getFloat(conicalGradient, property = "fill/gradient/conical/centerPointX")
val conicalCenterY = engine.block.getFloat(conicalGradient, property = "fill/gradient/conical/centerPointY")
```
There is no separate rotation or angle property for conical gradients.
## Additional Techniques
### Sharing Gradient Fills
Multiple blocks can share the same gradient fill. Changing the shared fill updates every block that uses it.
```kotlin highlight-android-share-gradient
val sharedGradient = engine.block.createFill(FillType.LinearGradient)
val previousSharedFillA = engine.block.getFill(sharedBlockA)
val previousSharedFillB = engine.block.getFill(sharedBlockB)
engine.block.setFill(sharedBlockA, fill = sharedGradient)
engine.block.setFill(sharedBlockB, fill = sharedGradient)
// Only destroy previous fills when you know they are not shared by other blocks.
if (engine.block.isValid(previousSharedFillA)) {
engine.block.destroy(previousSharedFillA)
}
if (engine.block.isValid(previousSharedFillB)) {
engine.block.destroy(previousSharedFillB)
}
val sharedGradientStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 1F, g = 1F, b = 0F, a = 1F)),
)
engine.block.setGradientColorStops(sharedGradient, property = "fill/gradient/colors", colorStops = sharedGradientStops)
```
### Duplicating Gradient Fills
Duplicating a block with its own gradient fill creates an independent fill for the duplicate. Updating the duplicate's fill leaves the original block's fill unchanged.
```kotlin highlight-android-duplicate-gradient
val duplicateBlock = engine.block.duplicate(linearBlock)
val duplicateGradient = engine.block.getFill(duplicateBlock)
engine.block.setGradientColorStops(
block = duplicateGradient,
property = "fill/gradient/colors",
colorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.8F, g = 0.2F, b = 0.9F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.9F, b = 0.8F, a = 1F)),
),
)
```
## Common Use Cases
The following recipes configure gradient fills after they have been attached to supported page or graphic blocks. Use them as compact starting points for common visual effects.
### Modern Page Background (Aurora Effect)
Use multiple linear gradient stops to create a soft page or banner background.
```kotlin highlight-android-use-case-aurora
val auroraStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.4F, g = 0.1F, b = 0.8F, a = 1F)),
GradientColorStop(stop = 0.3F, color = Color.fromRGBA(r = 0.8F, g = 0.2F, b = 0.6F, a = 1F)),
GradientColorStop(stop = 0.6F, color = Color.fromRGBA(r = 1F, g = 0.5F, b = 0.3F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 1F, g = 0.8F, b = 0.2F, a = 1F)),
)
engine.block.setGradientColorStops(
block = pageGradient,
property = "fill/gradient/colors",
colorStops = auroraStops,
)
```
### Button Highlight Effect
Use a radial gradient for a focused highlight or vignette on a graphic block.
```kotlin highlight-android-use-case-button-highlight
val buttonHighlightStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.3F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointX", value = 0.5F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/centerPointY", value = 0.35F)
engine.block.setFloat(radialGradient, property = "fill/gradient/radial/radius", value = 0.9F)
engine.block.setGradientColorStops(
block = radialGradient,
property = "fill/gradient/colors",
colorStops = buttonHighlightStops,
)
```
### Loading Spinner (Conical)
Use a conical gradient with transparent intermediate stops for circular indicators or color-wheel style graphics.
```kotlin highlight-android-use-case-spinner
val spinnerStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
GradientColorStop(stop = 0.75F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 0F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)),
)
engine.block.setGradientColorStops(
block = conicalGradient,
property = "fill/gradient/colors",
colorStops = spinnerStops,
)
```
### Transparency Overlay
Use alpha values on linear gradient stops to fade a block from transparent to opaque.
```kotlin highlight-android-use-case-transparency
val transparencyStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.7F)),
)
engine.block.setGradientColorStops(
block = pageGradient,
property = "fill/gradient/colors",
colorStops = transparencyStops,
)
```
### Duotone Effect
Use two saturated linear gradient stops for a simple duotone treatment.
```kotlin highlight-android-use-case-duotone
val duotoneStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.8F, g = 0.2F, b = 0.9F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0.2F, g = 0.9F, b = 0.8F, a = 1F)),
)
engine.block.setGradientColorStops(
block = linearGradient,
property = "fill/gradient/colors",
colorStops = duotoneStops,
)
```
## Troubleshooting
### Gradient Not Visible
- Confirm the block supports fills with `engine.block.supportsFill(block=_)`.
- Confirm the target block accepts gradient fills. Text blocks support solid color fills only.
- Check that fill rendering is enabled with `engine.block.isFillEnabled(block=_)`.
- Verify the block has non-zero width and height and is attached to the scene.
### Gradient Looks Different Than Expected
- Keep color stop positions in ascending order between `0.0` and `1.0`.
- Check whether you chose the intended fill type: linear, radial, or conical.
- Confirm the position properties use normalized coordinates, not pixels.
### Color Stops Not Updating
Call `engine.block.setGradientColorStops()` on the gradient fill block returned by `createFill()` or `getFill()`, not on the parent page or graphic block. Use the exact `"fill/gradient/colors"` property string when updating the stop list.
### Memory Leaks
`setFill()` does not destroy a block's previous fill automatically. Destroy a previous fill only when you know it is unshared and no longer referenced. Do not destroy a shared fill that remains attached to other blocks.
## API Reference
### Core Methods
| Method | Description |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `engine.block.createFill(fillType=FillType.LinearGradient)` | Create a linear gradient fill block. |
| `engine.block.createFill(fillType=FillType.RadialGradient)` | Create a radial gradient fill block. |
| `engine.block.createFill(fillType=FillType.ConicalGradient)` | Create a conical gradient fill block. |
| `engine.block.supportsFill(block=_)` | Check whether a block has fill support. This does not guarantee every fill type is valid for that block. |
| `engine.block.setFill(block=_, fill=_)` | Assign a compatible fill block to a design block. |
| `engine.block.getFill(block=_)` | Get the fill block attached to a design block. |
| `engine.block.setFillSolidColor(block=_, color=_)` | Set a block's solid fill color with an `RGBAColor`. |
| `engine.block.getFillSolidColor(block=_)` | Read a block's solid fill color. |
| `engine.block.getType(block=_)` | Read the type key for a block or fill. |
| `engine.block.setGradientColorStops(block=_, property="fill/gradient/colors", colorStops=_)` | Set gradient color stops. |
| `engine.block.getGradientColorStops(block=_, property="fill/gradient/colors")` | Read gradient color stops. |
| `engine.block.setFloat(block=_, property="fill/gradient/linear/startPointX", value=_)` | Set the linear gradient start X coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/linear/startPointY", value=_)` | Set the linear gradient start Y coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/linear/endPointX", value=_)` | Set the linear gradient end X coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/linear/endPointY", value=_)` | Set the linear gradient end Y coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/linear/startPointX")` | Read the linear gradient start X coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/linear/startPointY")` | Read the linear gradient start Y coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/linear/endPointX")` | Read the linear gradient end X coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/linear/endPointY")` | Read the linear gradient end Y coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/radial/centerPointX", value=_)` | Set the radial gradient center X coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/radial/centerPointY", value=_)` | Set the radial gradient center Y coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/radial/radius", value=_)` | Set the radial gradient radius. |
| `engine.block.getFloat(block=_, property="fill/gradient/radial/centerPointX")` | Read the radial gradient center X coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/radial/centerPointY")` | Read the radial gradient center Y coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/radial/radius")` | Read the radial gradient radius. |
| `engine.block.setFloat(block=_, property="fill/gradient/conical/centerPointX", value=_)` | Set the conical gradient center X coordinate. |
| `engine.block.setFloat(block=_, property="fill/gradient/conical/centerPointY", value=_)` | Set the conical gradient center Y coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/conical/centerPointX")` | Read the conical gradient center X coordinate. |
| `engine.block.getFloat(block=_, property="fill/gradient/conical/centerPointY")` | Read the conical gradient center Y coordinate. |
| `engine.block.isFillEnabled(block=_)` | Check whether fill rendering is enabled. |
| `engine.block.setFillEnabled(block=_, enabled=_)` | Enable or disable fill rendering. |
| `engine.block.isValid(block=_)` | Check whether a fill handle is valid before destroying it. |
| `engine.block.destroy(block=_)` | Destroy a fill block that is no longer attached or needed. |
| `engine.block.duplicate(block=_, attachToParent=true)` | Duplicate a block. In the sample, a fill owned only by the original is copied for the duplicate, so changing the duplicate fill does not change the original. By default, the duplicate is attached to the same parent; pass `attachToParent=false` to leave it detached. |
| `Color.fromColor(color=_)` | Create an sRGB color stop color from an Android `@ColorInt`. |
| `Color.fromResource(colorResource=_, context=_)` | Create an sRGB color stop color from an Android `@ColorRes` resource. The `context` parameter is optional; omit it to use the application context. |
| `Color.fromHex(colorString=_)` | Create an sRGB color stop color from a hex string. |
| `Color.fromRGBA(r=_, g=_, b=_, a=255)` | Create an sRGB color stop color from 0-255 channel values. |
| `Color.fromRGBA(r=_, g=_, b=_, a=1F)` | Create an sRGB color stop color from 0.0-1.0 channel values. |
| `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=1F)` | Create a CMYK color stop color. |
| `Color.fromSpotColor(name=_, tint=1F, externalReference=null)` | Reference a registered spot color in a gradient stop, optionally with tint and the external source guide name. |
| `engine.editor.setSpotColor(name=_, color=Color.fromRGBA(r=_, g=_, b=_, a=_))` | Register the RGB representation of a spot color before using it in a gradient stop. The alpha channel is ignored. |
| `engine.editor.setSpotColor(name=_, color=Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_))` | Register the CMYK representation of a spot color before using it in a gradient stop. |
### Gradient Properties
| Property | Applies to | Type | Default | Description |
| ------------------------------------ | ----------------------- | ------------------------- | -------------- | ------------------------------------------------------- |
| `fill/gradient/colors` | Linear, radial, conical | `List` | White to black | Normalized gradient color stops in ascending order. |
| `fill/gradient/linear/startPointX` | Linear | `Float` | `0.5` | Horizontal start position. |
| `fill/gradient/linear/startPointY` | Linear | `Float` | `0` | Vertical start position. |
| `fill/gradient/linear/endPointX` | Linear | `Float` | `0.5` | Horizontal end position. |
| `fill/gradient/linear/endPointY` | Linear | `Float` | `1` | Vertical end position. |
| `fill/gradient/radial/centerPointX` | Radial | `Float` | `0` | Horizontal center position. |
| `fill/gradient/radial/centerPointY` | Radial | `Float` | `0` | Vertical center position. |
| `fill/gradient/radial/radius` | Radial | `Float` | `1` | Radius relative to the smaller side of the block frame. |
| `fill/gradient/conical/centerPointX` | Conical | `Float` | `0` | Horizontal sweep center position. |
| `fill/gradient/conical/centerPointY` | Conical | `Float` | `0` | Vertical sweep center position. |
For conical gradients, `stop = 0.0` starts at the positive X, or 3 o'clock, direction from `centerPointX/Y`, and later stop values proceed clockwise. There is no separate rotation or angle property.
### GradientColorStop Type
| Property | Type | Description |
| -------- | ------- | ---------------------------------------- |
| `stop` | `Float` | Normalized position from `0.0` to `1.0`. |
| `color` | `Color` | sRGB, CMYK, or Spot Color value. |
## Next Steps
- [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/) — Learn about solid color fills with RGB, CMYK, and Spot Colors
- [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) — Display photo and raster content in design blocks
- [Fills Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Understand the fill system across color,
gradient, image, and video fills
- [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply colors to design elements programmatically
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Learn how blocks define elements in a scene
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Image Fills"
description: "Apply photos, textures, and patterns to design elements using image fills in CE.SDK for Android."
platform: android
url: "https://img.ly/docs/cesdk/android/fills/image-e9cb5c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) > [Image](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/)
---
```kotlin file=@cesdk_android_examples/engine-guides-fills-image/FillsImage.kt reference-only
import android.net.Uri
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import ly.img.engine.Source
import java.nio.ByteBuffer
import kotlin.math.abs
data class FillsImage(
val sceneSupportsFill: Boolean,
val blockSupportsFill: Boolean,
val blockSupportsContentFillMode: Boolean,
val blockSupportsOpacity: Boolean,
val imageFillType: String,
val imageUri: Uri,
val currentFillMatches: Boolean,
val coverMode: ContentFillMode,
val containMode: ContentFillMode,
val cropMode: ContentFillMode,
val contentFillModeAfterSourceSet: ContentFillMode,
val sourceSetWidths: List,
val opacity: Float,
val exportedImage: ByteBuffer,
)
suspend fun fillsImage(engine: Engine): FillsImage {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 100F)
engine.block.setPositionY(imageBlock, value = 80F)
engine.block.setWidth(imageBlock, value = 360F)
engine.block.setHeight(imageBlock, value = 240F)
engine.block.appendChild(parent = page, child = imageBlock)
val sceneSupportsFill = engine.block.supportsFill(scene)
val blockSupportsFill = engine.block.supportsFill(imageBlock)
val blockSupportsContentFillMode = engine.block.supportsContentFillMode(imageBlock)
val blockSupportsOpacity = engine.block.supportsOpacity(imageBlock)
require(!sceneSupportsFill) { "Scenes do not support fills." }
require(blockSupportsFill) { "Graphic blocks with shapes can render fills." }
require(blockSupportsContentFillMode) { "This block must support content fill modes." }
require(blockSupportsOpacity) { "This block must support opacity." }
val imageFill = engine.block.createFill(FillType.Image)
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = imageBlock, fill = imageFill)
check(engine.block.getType(imageFill) == FillType.Image.key)
val currentFill = engine.block.getFill(imageBlock)
val imageFillType = engine.block.getType(currentFill)
val currentImageUri = engine.block.getUri(
block = currentFill,
property = "fill/image/imageFileURI",
)
check(currentFill == imageFill)
check(imageFillType == FillType.Image.key)
check(currentImageUri == imageUri)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER)
val coverMode = engine.block.getContentFillMode(imageBlock)
check(coverMode == ContentFillMode.COVER)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CONTAIN)
val containMode = engine.block.getContentFillMode(imageBlock)
check(containMode == ContentFillMode.CONTAIN)
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CROP)
val cropMode = engine.block.getContentFillMode(imageBlock)
check(cropMode == ContentFillMode.CROP)
val sourceSet = listOf(
Source(
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"),
width = 512,
height = 341,
),
Source(
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"),
width = 1024,
height = 683,
),
Source(
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"),
width = 2048,
height = 1366,
),
)
engine.block.setSourceSet(
block = imageFill,
property = "fill/image/sourceSet",
sourceSet = sourceSet,
)
// setSourceSet resets crop and content fill mode on the associated block.
// Reapply Cover or Contain after changing responsive image sources.
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CONTAIN)
val contentFillModeAfterSourceSet = engine.block.getContentFillMode(imageBlock)
check(contentFillModeAfterSourceSet == ContentFillMode.CONTAIN)
val currentSourceSet = engine.block.getSourceSet(
block = imageFill,
property = "fill/image/sourceSet",
)
check(currentSourceSet.map(Source::width) == listOf(2048, 1024, 512))
engine.block.setOpacity(block = imageBlock, value = 0.65F)
val opacity = engine.block.getOpacity(imageBlock)
check(abs(opacity - 0.65F) < 0.0001F)
val exportedImage = engine.block.export(imageBlock, mimeType = MimeType.PNG)
return FillsImage(
sceneSupportsFill = sceneSupportsFill,
blockSupportsFill = blockSupportsFill,
blockSupportsContentFillMode = blockSupportsContentFillMode,
blockSupportsOpacity = blockSupportsOpacity,
imageFillType = imageFillType,
imageUri = currentImageUri,
currentFillMatches = currentFill == imageFill,
coverMode = coverMode,
containMode = containMode,
cropMode = cropMode,
contentFillModeAfterSourceSet = contentFillModeAfterSourceSet,
sourceSetWidths = currentSourceSet.map(Source::width),
opacity = opacity,
exportedImage = exportedImage,
)
}
```
Fill graphic blocks with photos and images from URLs, app-owned files, or
responsive source sets using CE.SDK's image fill system.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-fills-image)
Image fills render design blocks with raster or vector image content, supporting common formats such as PNG, JPEG, WebP, and SVG. You can load images from remote URLs or app-owned Android URIs, provide responsive source sets, and choose how the image scales inside its block.
This guide covers how to create and apply image fills programmatically, configure content fill modes, work with responsive source sets, and pass image URIs that Android can resolve.
## Understanding Image Fills
Image fills are one of the fundamental fill types in CE.SDK, identified by the type string `"//ly.img.ubq/fill/image"` or the `FillType.Image` type-safe constant. While color fills produce solid colors and gradient fills produce color transitions, image fills display raster or vector content from image files.
The image fill is a separate fill block from the graphic block that renders it. The URI and source set live on the fill, while sizing, opacity, and content fill mode live on the design block that owns the fill.
## Checking Image Fill Capabilities
Before working with image fills, verify that the target block supports the fill-related capabilities this guide changes. Scenes do not support fills, while graphic blocks with shapes can render image fills. Content fill mode and opacity are block-level settings, so check those capabilities before changing how the image is framed or blended.
```kotlin highlight-android-check-support
val sceneSupportsFill = engine.block.supportsFill(scene)
val blockSupportsFill = engine.block.supportsFill(imageBlock)
val blockSupportsContentFillMode = engine.block.supportsContentFillMode(imageBlock)
val blockSupportsOpacity = engine.block.supportsOpacity(imageBlock)
require(!sceneSupportsFill) { "Scenes do not support fills." }
require(blockSupportsFill) { "Graphic blocks with shapes can render fills." }
require(blockSupportsContentFillMode) { "This block must support content fill modes." }
require(blockSupportsOpacity) { "This block must support opacity." }
```
`supportsFill()` returns `true` when a block can have a fill assigned to it. `supportsContentFillMode()` and `supportsOpacity()` confirm that the same block can change image scaling and opacity. Check these predicates before calling the corresponding APIs on a block you did not create.
## Creating Image Fills
Create an image fill with `createFill(FillType.Image)`, set its `fill/image/imageFileURI` property, and attach it to the graphic block with `setFill()`.
```kotlin highlight-android-create-image-fill
val imageFill = engine.block.createFill(FillType.Image)
val imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg")
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = imageBlock, fill = imageFill)
```
The fill exists independently until you attach it to a block. If you create a fill and do not attach it, destroy it with `engine.block.destroy()` to avoid leaking an unowned fill block. When you replace an existing fill with `setFill()`, the previous fill is not destroyed automatically.
### Getting the Current Fill
Retrieve the fill from a block with `getFill()` and inspect its type with `getType()` to verify that it is an image fill.
```kotlin highlight-android-get-current-fill
val currentFill = engine.block.getFill(imageBlock)
val imageFillType = engine.block.getType(currentFill)
val currentImageUri = engine.block.getUri(
block = currentFill,
property = "fill/image/imageFileURI",
)
```
`getFill()` returns the fill block ID. Use that ID to read image fill properties such as `fill/image/imageFileURI` or `fill/image/sourceSet`.
## Configuring Content Fill Modes
Content fill modes control how the image scales and positions within the containing block. Use `setContentFillMode()` on the design block, not on the fill block.
### Cover Mode
`ContentFillMode.COVER` scales the image until it fills the entire block while maintaining its aspect ratio. Parts of the image may be cropped when the image and block aspect ratios differ.
```kotlin highlight-android-cover-mode
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER)
val coverMode = engine.block.getContentFillMode(imageBlock)
```
Cover mode is useful for backgrounds, hero images, and photo frames where the block should never show empty space.
### Contain Mode
`ContentFillMode.CONTAIN` scales the image so the full image fits inside the block while maintaining its aspect ratio. Empty space can remain when the aspect ratios differ.
```kotlin highlight-android-contain-mode
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CONTAIN)
val containMode = engine.block.getContentFillMode(imageBlock)
```
Contain mode is useful for logos, product images, and other content where preserving the complete image matters more than filling the entire block.
### Crop Mode
`ContentFillMode.CROP` represents a manual crop. Use it when your app manages crop transforms directly or when the editor UI has produced a user-controlled crop.
```kotlin highlight-android-crop-mode
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CROP)
val cropMode = engine.block.getContentFillMode(imageBlock)
```
## Working with Source Sets
Source sets provide multiple resolutions of the same image. The engine selects the most appropriate source for the current drawing size, which keeps previews efficient while preserving quality for high-resolution exports.
### Setting Up a Source Set
A source set is a list of `Source` values. Each source contains a URI and the image dimensions in pixels.
```kotlin highlight-android-source-set
val sourceSet = listOf(
Source(
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"),
width = 512,
height = 341,
),
Source(
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"),
width = 1024,
height = 683,
),
Source(
uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"),
width = 2048,
height = 1366,
),
)
engine.block.setSourceSet(
block = imageFill,
property = "fill/image/sourceSet",
sourceSet = sourceSet,
)
// setSourceSet resets crop and content fill mode on the associated block.
// Reapply Cover or Contain after changing responsive image sources.
engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.CONTAIN)
```
The engine uses the source with the closest size that meets or exceeds the required drawing size. During export, it can use the highest available resolution.
> **Note:** `setSourceSet()` resets crop and content fill mode on the associated block.
> Reapply `ContentFillMode.COVER` or `ContentFillMode.CONTAIN` after changing
> a fill's responsive sources if you need to preserve that mode.
> **Note:** When both `fill/image/sourceSet` and `fill/image/imageFileURI` are set on a
> fill, the source set takes precedence. The single URI remains stored and is
> used again if the source set is cleared.
### Retrieving Source Sets
Read the current source set with `getSourceSet()` when you need to inspect or update the responsive image sources.
```kotlin highlight-android-get-source-set
val currentSourceSet = engine.block.getSourceSet(
block = imageFill,
property = "fill/image/sourceSet",
)
```
The returned list contains the same `Source` fields you provided: `uri`, `width`, and `height`.
## Loading Images from Different Sources
Image fills accept URI values, so Android integrations can use HTTPS URLs and app-owned local or content URIs that the engine can resolve.
### HTTPS URLs
Pass remote image URLs as `Uri` values to `setUri(...)`, as shown in the image fill and source set examples above. Use HTTPS URLs that are reachable by the running app and keep responsive alternatives in `fill/image/sourceSet` when the same image is available in multiple sizes.
### Data URIs and Base64
Android does not currently load image fill resources from `data:` URIs. If your app starts with base64 image data, decode it into an app-owned file or expose it through a `content://` provider, then pass that `Uri` to `setUri(...)`.
### App-Owned Local and Content URIs
For images owned by your Android app, pass a `file://` or `content://` `Uri` to `setUri(...)` in the same way as an HTTPS URL. The URI must remain readable to the engine while the scene is loaded; if your app hands out a `content://` URI, make sure the provider grants access for that URI and that the data is still available when the editor reopens the scene.
## Additional Techniques
### Controlling Opacity
Set opacity on the design block that owns the image fill. The value ranges from `0F` for fully transparent to `1F` for fully opaque.
```kotlin highlight-android-opacity
engine.block.setOpacity(block = imageBlock, value = 0.65F)
val opacity = engine.block.getOpacity(imageBlock)
```
> **Note:** Opacity is a block property, not a fill property. It affects the whole block,
> including strokes, effects, and any other visual properties applied to that
> block. For transparency inside the image itself, use an image format with
> alpha support such as PNG, WebP, or SVG.
## Troubleshooting
### Image Not Visible
- Check `engine.block.supportsFill(block)` before assigning the fill and `engine.block.isFillEnabled(block)` when a fill is already assigned.
- Make sure the graphic block has a shape, non-zero width and height, and is appended to the scene hierarchy.
- Verify that the image fill is attached to the visible design block with `engine.block.setFill(block=_, fill=_)`; setting the URI on an unattached fill does not render it.
### Image Not Loading
- Read the stored URI with `engine.block.getUri(block=fill, property="fill/image/imageFileURI")` and confirm it uses the expected `https`, `file`, or `content` scheme.
- For HTTPS URLs, verify that the URL is reachable from the device or emulator and that the app has network access.
- For base64 image data, write the decoded bytes to app-owned storage or expose them through a content provider instead of passing a `data:` URI directly.
- For local or content URIs, keep the referenced file available and grant URI access through your app's storage or content provider.
- If `fill/image/sourceSet` is set, remember that it takes precedence over `fill/image/imageFileURI`; inspect the source set when the single URI looks correct but another image is used.
### Unowned or Replaced Fill Cleanup
Image fills are design blocks. A fill created with `engine.block.createFill(FillType.Image)` remains unowned until it is attached with `engine.block.setFill(...)`, so destroy unused fills with `engine.block.destroy(block=_)`. When you replace a block's existing fill, store the old fill from `engine.block.getFill(block=_)` first and destroy it after the replacement if your app no longer needs it.
## API Reference
### Core Methods
| API | Description |
| --- | --- |
| `engine.block.supportsFill(block=_)` | Checks whether a design block can have a fill |
| `engine.block.isFillEnabled(block=_)` | Checks whether fill rendering is enabled on a design block |
| `engine.block.supportsContentFillMode(block=_)` | Checks whether content fill mode can be changed on a block |
| `engine.block.supportsOpacity(block=_)` | Checks whether opacity can be changed on a block |
| `engine.block.createFill(fillType=FillType.Image)` | Creates an image fill block |
| `engine.block.setFill(block=_, fill=_)` | Assigns a fill block to a design block |
| `engine.block.getFill(block=_)` | Returns the fill block attached to a design block |
| `engine.block.getType(block=_)` | Reads a block type string such as `"//ly.img.ubq/fill/image"` |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Writes the image URI |
| `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Reads the image URI |
| `engine.block.setSourceSet(block=_, property="fill/image/sourceSet", sourceSet=_)` | Writes responsive image sources |
| `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Reads responsive image sources |
| `engine.block.setContentFillMode(block=_, mode=ContentFillMode.COVER)` | Sets how image content scales inside the block |
| `engine.block.getContentFillMode(block=_)` | Reads the current content fill mode |
| `engine.block.setOpacity(block=_, value=_)` | Sets block opacity from `0F` to `1F` |
| `engine.block.getOpacity(block=_)` | Reads block opacity |
| `engine.block.destroy(block=_)` | Destroys an unused fill block |
### Image Fill Properties
| Property | Type | Description |
| --- | --- | --- |
| `fill/image/imageFileURI` | `Uri` | Single image URI, such as HTTPS, file, or content URI |
| `fill/image/sourceSet` | `List` | Responsive image sources with pixel dimensions |
### Content Fill Properties
| Property | Type | Values | Description |
| --- | --- | --- | --- |
| `contentFill/mode` | `ContentFillMode` | `CROP`, `COVER`, `CONTAIN` | How image content scales or crops inside the block |
### Source
| Property | Type | Description |
| --- | --- | --- |
| `uri` | `Uri` | Image URI |
| `width` | `Int` | Image width in pixels |
| `height` | `Int` | Image height in pixels |
## Next Steps
- [Fills Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Learn how fills attach to design blocks and how fill types are replaced.
- [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) - Use multiple versions of an asset for different resolutions.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Fills"
description: "Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements."
platform: android
url: "https://img.ly/docs/cesdk/android/fills/overview-3895ee/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) > [Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/)
---
Use fills to paint supported design blocks with color, gradient, image, video,
or live pixel content.
Fills define the visual content inside the shape of a supported [design block](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/). They are separate fill blocks attached to an owner block, so the owner controls geometry, layout, selection, and transforms while the fill controls the pixels drawn inside that shape.
The CE.SDK editor UI exposes fill controls for selected blocks when the block and the active editor configuration allow it. The CreativeEngine APIs expose the same model programmatically, including checking whether a block supports fills, reading the assigned fill, replacing it, disabling it, or configuring type-specific properties.
## Fill Types
Choose the fill type that matches the content you want the block to render:
| Fill type | Use it for |
| --- | --- |
| Color | Solid brand colors, simple backgrounds, masks, or placeholder states. |
| Linear gradient | Directional transitions between colors, such as top-to-bottom or left-to-right shading. |
| Radial gradient | Circular or elliptical color transitions, such as highlights and glow effects. |
| Conical gradient | Angular color transitions around a center point, such as color wheels or sweep effects. |
| Image | Photos, textures, rendered thumbnails, and other still media. |
| Video | Moving media inside a design block, including trimmed or looping clips. |
| Pixel stream | Dynamic pixel content supplied by an integration at runtime. |
Color and gradient fills are usually best for graphic shapes, backgrounds, and design accents. Image, video, and pixel stream fills are media fills: they depend on a source and often need content fitting decisions so the media appears correctly inside the block frame.
## Fill Support
Not every design block can have a fill. Shapes, pages, text blocks, and many visual blocks support fills, while scene/root blocks do not. Text blocks support solid color fills only; graphic, page, and media-capable blocks can support broader fill types depending on their block capabilities. Check fill support before applying fill operations to arbitrary blocks.
A block can also have its fill disabled while retaining other styling, such as a stroke. This is useful when a shape should act as an outline, frame, or interaction target without painting its interior.
## Fill Properties
Each fill type has its own properties. A solid color fill stores a color value, gradient fills store color stops and geometry, image fills store image sources, and video fills store video sources plus playback-related data.
These properties usually fall into a few categories:
- Color values define the visible color of solid fills.
- Gradient geometry and color stops define the direction, shape, and transitions of gradient fills.
- Image sources, previews, placeholders, and source sets define how still media loads and adapts.
- Video sources, playback values, trimming, looping, and related media state define how moving media behaves inside a block.
## Content Fill Modes
Media fills need a rule for fitting source content into the block frame. CE.SDK supports three content fill modes:
| Mode | Behavior |
| --- | --- |
| Crop | Uses explicit manual crop framing for the visible media region. |
| Cover | Scales media to cover the whole block frame and may crop edges. |
| Contain | Scales media so the whole source remains visible and may leave empty space. |
Cover is a good default for image-heavy layouts where the frame should always be filled. Contain is better when preserving the complete source matters more than filling every pixel. Crop is best when users or templates already define the exact visible region.
## Ownership and Reuse
A fill attached to one block is part of that block's visual appearance. Duplicating the owner block duplicates an owned fill, so changes to the duplicate can be made independently.
The same fill can also be shared by multiple blocks. Shared fills are useful when several elements must stay visually synchronized, but any change to the shared fill affects every block using it. Use shared fills intentionally, especially when user edits should apply to only one selected block.
## Editor and Engine Workflows
Use the editor UI when users should choose or adjust fills interactively. Use CreativeEngine APIs when your app needs to apply templates, enforce brand defaults, generate scenes, migrate existing content, or update fills from app state.
For a complete Android editing surface, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). The same fill model applies whether users adjust fills in the editor UI or your app updates them through CreativeEngine APIs.
## Next Steps
- [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/) — Work with solid color fills and their properties
- [Gradient Fills](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/) — Apply linear, radial, and conical gradient fills
- [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) — Use images as fills for design blocks
- [Video Fills](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/) — Use videos as fills for design blocks
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Video Fills"
description: "Learn how to apply video content as fills to Android design blocks in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/fills/video-ec7f9f/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) > [Video](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/)
---
```kotlin file=@cesdk_android_examples/engine-guides-fills-video/VideoFills.kt reference-only
import android.net.Uri
import kotlinx.coroutines.flow.toList
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
import ly.img.engine.Source
suspend fun videoFills(engine: Engine): VideoFillsResult {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1280F)
engine.block.setHeight(page, value = 720F)
engine.block.appendChild(parent = scene, child = page)
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block, value = 640F)
engine.block.setHeight(block, value = 360F)
engine.block.appendChild(parent = page, child = block)
check(engine.block.supportsFill(block)) { "Graphic blocks can receive fills." }
val videoFill = engine.block.createFill(FillType.Video)
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(block = block, fill = videoFill)
val currentFill = engine.block.getFill(block)
val currentFillType = engine.block.getType(currentFill)
val currentVideoUri = engine.block.getUri(
block = currentFill,
property = "fill/video/fileURI",
)
check(engine.block.supportsContentFillMode(block)) {
"Graphic blocks can scale video fills inside their frame."
}
engine.block.setContentFillMode(block = block, mode = ContentFillMode.COVER)
val coverMode = engine.block.getContentFillMode(block)
engine.block.setContentFillMode(block = block, mode = ContentFillMode.CONTAIN)
val containMode = engine.block.getContentFillMode(block)
engine.block.setSourceSet(
block = videoFill,
property = "fill/video/sourceSet",
sourceSet = listOf(
Source(
uri = Uri.parse("https://img.ly/static/example-assets/sourceset/1x.mp4"),
width = 720,
height = 1280,
),
Source(
uri = Uri.parse("https://img.ly/static/example-assets/sourceset/2x.mp4"),
width = 1440,
height = 2560,
),
),
)
val responsiveSources = engine.block.getSourceSet(
block = videoFill,
property = "fill/video/sourceSet",
)
// Setting a source set can reset crop state on connected blocks.
engine.block.setContentFillMode(block = block, mode = ContentFillMode.CONTAIN)
val finalContentFillMode = engine.block.getContentFillMode(block)
engine.block.forceLoadAVResource(block = videoFill)
val durationSeconds = engine.block.getAVResourceTotalDuration(videoFill)
check(durationSeconds > 0.0) { "The video fill must contain playable media." }
// Generate thumbnails from the first two seconds of the video fill.
val thumbnailPreviewEndSeconds = durationSeconds.coerceAtMost(2.0)
val thumbnails = engine.block.generateVideoThumbnailSequence(
block = videoFill,
thumbnailHeight = 72,
timeBegin = 0.0,
timeEnd = thumbnailPreviewEndSeconds,
numberOfFrames = 3,
).toList()
val ellipseBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(ellipseBlock, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(ellipseBlock, value = 320F)
engine.block.setHeight(ellipseBlock, value = 320F)
engine.block.setPositionX(ellipseBlock, value = 760F)
engine.block.setPositionY(ellipseBlock, value = 200F)
engine.block.setOpacity(ellipseBlock, value = 0.72F)
engine.block.setFill(block = ellipseBlock, fill = videoFill)
engine.block.appendChild(parent = page, child = ellipseBlock)
val sharedFill = engine.block.getFill(ellipseBlock)
val opacity = engine.block.getOpacity(ellipseBlock)
return VideoFillsResult(
currentFillType = currentFillType,
currentVideoUri = currentVideoUri,
coverMode = coverMode,
containMode = containMode,
finalContentFillMode = finalContentFillMode,
responsiveSourceCount = responsiveSources.size,
durationSeconds = durationSeconds,
thumbnailCount = thumbnails.size,
thumbnailsHavePixels = thumbnails.all { it.imageData.remaining() > 0 },
sharedFillType = engine.block.getType(sharedFill),
opacity = opacity,
)
}
```
```kotlin file=@cesdk_android_examples/engine-guides-fills-video/VideoFillsResult.kt reference-only
import android.net.Uri
import ly.img.engine.ContentFillMode
data class VideoFillsResult(
val currentFillType: String,
val currentVideoUri: Uri,
val coverMode: ContentFillMode,
val containMode: ContentFillMode,
val finalContentFillMode: ContentFillMode,
val responsiveSourceCount: Int,
val durationSeconds: Double,
val thumbnailCount: Int,
val thumbnailsHavePixels: Boolean,
val sharedFillType: String,
val opacity: Float,
)
```
Apply motion content to Android design blocks by filling graphic shapes and
backgrounds with videos through CE.SDK's video fill system.

> **Reading time:** 9 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-fills-video)
Video fills are fill objects you attach to blocks that support fills. Use them
when video should behave like a visual material inside a graphic shape or
background.
Timeline video editing solves a different problem: the media clip is arranged,
trimmed, timed, and exported as part of a video composition. For that workflow,
use the [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) guide.
The [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) exposes video replacement
and crop controls in the CE.SDK editor UI. This guide focuses on the
CreativeEngine APIs you use when creating and configuring video fills
programmatically.
## Understanding Video Fills
### What Is a Video Fill?
A video fill paints a block with video frames. The fill is identified by
`FillType.Video`, which maps to the Engine type `//ly.img.ubq/fill/video`.
The fill stores its media source through `fill/video/fileURI` or
`fill/video/sourceSet`. The block that owns the fill stores shape, size,
opacity, and content fill mode.
### Video Fill vs. Timeline Editing
Use a video fill when you want motion content clipped by an existing block's
shape. This guide uses graphic blocks because CE.SDK text rendering uses solid
color fill behavior.
Use a timeline editing workflow when the video should behave like a clip with
trim, arrangement, playback timing, and export controls. CE.SDK still represents
that media through design blocks, commonly graphic blocks with video fills inside
tracks; the editing workflow determines how users interact with it.
## Checking Video Fill Support
Before applying a video fill, create or select a block that supports fills. The
sample uses a graphic block with a rectangle shape.
```kotlin highlight-android-check-fill-support
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(block, value = 640F)
engine.block.setHeight(block, value = 360F)
engine.block.appendChild(parent = page, child = block)
check(engine.block.supportsFill(block)) { "Graphic blocks can receive fills." }
```
Graphic blocks and pages support fills; scenes do not. Use page fills for
full-page backgrounds, or graphic blocks for shaped video textures. Text blocks
may report fill support, but CreativeEngine text rendering is
solid-fill-oriented, so keep video fill examples on graphic blocks or pages.
## Creating Video Fills
Create the video fill, assign a media URI, and attach the fill to the block. The
backing sample creates a page for isolation; in your app, append the block to the
scene hierarchy you already manage.
```kotlin highlight-android-create-video-fill
val videoFill = engine.block.createFill(FillType.Video)
val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")
engine.block.setUri(
block = videoFill,
property = "fill/video/fileURI",
value = videoUri,
)
engine.block.setFill(block = block, fill = videoFill)
```
The fill can be configured before it is attached. After `setFill()`, the block
uses the video as the visible content inside its shape.
### Getting Current Fill Information
Read the current fill when your UI needs to display the active fill type or show
the assigned media source.
```kotlin highlight-android-read-current-fill
val currentFill = engine.block.getFill(block)
val currentFillType = engine.block.getType(currentFill)
val currentVideoUri = engine.block.getUri(
block = currentFill,
property = "fill/video/fileURI",
)
```
`getType()` returns the Engine type key, while `getUri()` reads the Android
`Uri` stored on the video fill.
## Content Fill Modes
Content fill modes control how the video is scaled inside the block frame.
Check `supportsContentFillMode()` before changing the mode. Android exposes
typed mode constants through `ContentFillMode`.
```kotlin highlight-android-content-fill-modes
check(engine.block.supportsContentFillMode(block)) {
"Graphic blocks can scale video fills inside their frame."
}
engine.block.setContentFillMode(block = block, mode = ContentFillMode.COVER)
val coverMode = engine.block.getContentFillMode(block)
engine.block.setContentFillMode(block = block, mode = ContentFillMode.CONTAIN)
val containMode = engine.block.getContentFillMode(block)
```
- `ContentFillMode.COVER` fills the frame and may crop the video.
- `ContentFillMode.CONTAIN` keeps the whole video visible and may leave empty
space.
- `ContentFillMode.CROP` leaves crop values under explicit user or app control.
Use Cover for backgrounds and video textures. Use Contain when the full source
must remain visible, such as product or presentation content.
## Working With Video Sources
### Single URI
For one video file, set `fill/video/fileURI` with a remote, local, bundled, or
app-provided `Uri`. Remote videos should use a format supported by the target
Android device.
### Source Sets
Source sets provide multiple versions of the same video so CE.SDK can choose the
best source for the current drawing or export size. Apply the source set to the
attached video fill, then read it back from that fill when your UI needs to show
the available sources. If the block already uses a specific content fill mode,
set that mode again after changing the source set.
```kotlin highlight-android-source-set
engine.block.setSourceSet(
block = videoFill,
property = "fill/video/sourceSet",
sourceSet = listOf(
Source(
uri = Uri.parse("https://img.ly/static/example-assets/sourceset/1x.mp4"),
width = 720,
height = 1280,
),
Source(
uri = Uri.parse("https://img.ly/static/example-assets/sourceset/2x.mp4"),
width = 1440,
height = 2560,
),
),
)
val responsiveSources = engine.block.getSourceSet(
block = videoFill,
property = "fill/video/sourceSet",
)
// Setting a source set can reset crop state on connected blocks.
engine.block.setContentFillMode(block = block, mode = ContentFillMode.CONTAIN)
val finalContentFillMode = engine.block.getContentFillMode(block)
```
Use source sets when previews can use a smaller file and exports need a higher
resolution source. The same pattern is covered in more depth in the
[Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) guide.
## Loading Video Resources
Videos load asynchronously. Call the suspend `forceLoadAVResource()` API before
you read the video's duration or generate thumbnails from the fill.
```kotlin highlight-android-load-resource
engine.block.forceLoadAVResource(block = videoFill)
val durationSeconds = engine.block.getAVResourceTotalDuration(videoFill)
check(durationSeconds > 0.0) { "The video fill must contain playable media." }
// Generate thumbnails from the first two seconds of the video fill.
val thumbnailPreviewEndSeconds = durationSeconds.coerceAtMost(2.0)
val thumbnails = engine.block.generateVideoThumbnailSequence(
block = videoFill,
thumbnailHeight = 72,
timeBegin = 0.0,
timeEnd = thumbnailPreviewEndSeconds,
numberOfFrames = 3,
).toList()
```
On Android, thumbnail generation returns a `Flow` of `VideoThumbnailResult`
objects. Each result contains RGBA pixel data for that thumbnail.
## Common Use Cases
### Video in Shapes and Overlays
Video fills are not limited to rectangles. You can attach the same fill to
another shape, then use normal block APIs for opacity, position, and size.
```kotlin highlight-android-shape-opacity-shared-fill
val ellipseBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(ellipseBlock, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setWidth(ellipseBlock, value = 320F)
engine.block.setHeight(ellipseBlock, value = 320F)
engine.block.setPositionX(ellipseBlock, value = 760F)
engine.block.setPositionY(ellipseBlock, value = 200F)
engine.block.setOpacity(ellipseBlock, value = 0.72F)
engine.block.setFill(block = ellipseBlock, fill = videoFill)
engine.block.appendChild(parent = page, child = ellipseBlock)
val sharedFill = engine.block.getFill(ellipseBlock)
val opacity = engine.block.getOpacity(ellipseBlock)
```
Sharing a fill keeps repeated video elements synchronized because the blocks
reference the same fill object.
## Troubleshooting
| Issue | Fix |
| --- | --- |
| Video fill is not visible | Confirm the block supports fills, has a shape, has non-zero dimensions, and is attached to the scene hierarchy. |
| Metadata or thumbnails fail | Await `forceLoadAVResource()` before reading duration or generating thumbnail data. |
| Video appears cropped | Check `getContentFillMode()` and switch between `COVER`, `CONTAIN`, and `CROP` based on your framing needs. |
| Replacing fills leaks memory | Destroy old fills that are no longer attached or reused by any block. |
## API Reference
| API | Description |
| --- | --- |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a block that can host a video fill. |
| `engine.block.createShape(type=_)` | Create the shape that clips the video fill. |
| `engine.block.setShape(block=_, shape=_)` | Attach a shape to the graphic block. |
| `engine.block.setWidth(block=_, value=_)` | Set the block width in design units. |
| `engine.block.setHeight(block=_, value=_)` | Set the block height in design units. |
| `engine.block.setPositionX(block=_, value=_)` | Set the block's horizontal position. |
| `engine.block.setPositionY(block=_, value=_)` | Set the block's vertical position. |
| `engine.block.appendChild(parent=_, child=_)` | Attach a block to the scene hierarchy. |
| `engine.block.supportsFill(block=_)` | Check whether a block can receive a fill. |
| `engine.block.supportsContentFillMode(block=_)` | Check whether a block supports content fill scaling modes. |
| `engine.block.createFill(fillType=FillType.Video)` | Create a video fill object. |
| `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Assign a single video source URI. |
| `engine.block.getUri(block=_, property="fill/video/fileURI")` | Read the assigned video source URI. |
| `engine.block.setFill(block=_, fill=_)` | Attach a fill to a block. |
| `engine.block.getFill(block=_)` | Read the fill attached to a block. |
| `engine.block.getType(block=_)` | Read a block or fill type key. |
| `engine.block.setContentFillMode(block=_, mode=_)` | Set Cover, Contain, or Crop behavior. |
| `engine.block.getContentFillMode(block=_)` | Read the current content fill mode. |
| `engine.block.setSourceSet(block=_, property="fill/video/sourceSet", sourceSet=_)` | Assign responsive video sources. |
| `engine.block.getSourceSet(block=_, property="fill/video/sourceSet")` | Read responsive video sources. |
| `engine.block.forceLoadAVResource(block=_)` | Load video metadata before reading duration or generating thumbnails. |
| `engine.block.getAVResourceTotalDuration(block=_)` | Read the loaded media duration in seconds. |
| `engine.block.generateVideoThumbnailSequence(block=_, thumbnailHeight=_, timeBegin=_, timeEnd=_, numberOfFrames=_)` | Generate video thumbnail frames. |
| `engine.block.setOpacity(block=_, value=_)` | Set block opacity from `0.0F` to `1.0F`. |
| `engine.block.getOpacity(block=_)` | Read the block opacity. |
| `engine.block.destroy(block=_)` | Release a fill that is no longer attached or shared. |
## Next Steps
- [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/) — Fill blocks with solid colors
- [Gradient Fills](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/) — Fill blocks with color transitions
- [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) — Fill blocks with static image content
- [Fills Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Review the broader fill system.
- [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) - Provide responsive video sources.
- [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Understand the block hierarchy.
- [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) - Work with time-based media ranges.
- [Insert Videos](https://img.ly/docs/cesdk/android/insert-media/videos-a5fa03/) - Insert video graphic blocks into Android CE.SDK scenes and configure their source, trim, position, and size.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Filters and Effects"
description: "Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/)
---
---
## Related Pages
- [Filters & Effects Overview](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) - Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying.
- [Supported Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects/support-a666dd/) - Review Android Engine APIs and property tables for CE.SDK filters and effects.
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Apply, configure, stack, and manage filters and effects on Android with the CE.SDK Engine API.
- [Create Custom Filters](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-filters-c796ba/) - Extend CE.SDK with custom LUT filter asset sources for brand-specific color grading and filter collections.
- [Chroma Key (Green Screen) in Android (Kotlin)](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/) - Use CE.SDK's green/blue screen keyer to replace backgrounds, tune edges & spill, and composite subjects over virtual scenes.
- [Blur Effects](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) - Apply blur effects to soften backgrounds or create depth and focus in your designs.
- [Create a Custom LUT Filter](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/) - Create and apply custom LUT filters to achieve consistent, brand-aligned visual styles.
- [Distortion Effects](https://img.ly/docs/cesdk/android/filters-and-effects/distortion-5b5a66/) - Apply distortion effects to warp, shift, and transform design elements for dynamic artistic visuals in CE.SDK.
- [Duotone](https://img.ly/docs/cesdk/android/filters-and-effects/duotone-831fc5/) - Apply duotone effects to images with the CE.SDK Engine, mapping tones to two colors for stylized, brand-consistent visuals.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Apply a Filter or Effect"
description: "Apply, configure, stack, and manage filters and effects on Android with the CE.SDK Engine API."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Apply Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/)
---
```kotlin file=@cesdk_android_examples/engine-guides-filters-and-effects-apply/ApplyFiltersAndEffects.kt reference-only
import ly.img.editor.defaultBaseUri
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
import kotlin.math.abs
private val sampleImageUri = defaultBaseUri.buildUpon()
.appendPath("ly.img.image")
.appendPath("images")
.appendPath("sample_1.jpg")
.build()
private val sampleLutUri = defaultBaseUri.buildUpon()
.appendPath("ly.img.filter.lut")
.appendPath("LUTs")
.appendPath("imgly_lut_ad1920_5_5_128.png")
.build()
.toString()
data class ApplyFiltersAndEffects(
val sceneSupportsEffects: Boolean,
val pageSupportsEffects: Boolean,
val imageSupportsEffects: Boolean,
val orderedStackMatches: Boolean,
val pixelizePropertyCount: Int,
val adjustmentPropertyCount: Int,
val brightness: Float,
val horizontalPixelSize: Int,
val lutUri: String,
val lutIntensity: Float,
val duotoneIntensity: Float,
val combinedStackMatches: Boolean,
val queriedStackSize: Int,
val disabledState: Boolean,
val enabledState: Boolean,
val removed: Boolean,
val batchEffectCount: Int,
val presetSaturation: Float,
val exportedPage: ByteBuffer,
)
suspend fun applyFiltersAndEffects(engine: Engine): ApplyFiltersAndEffects {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1200F)
engine.block.setHeight(page, value = 900F)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = createImageGraphicBlock(engine, page, x = 70F, y = 70F)
val lutBlock = createImageGraphicBlock(engine, page, x = 430F, y = 70F)
val duotoneBlock = createImageGraphicBlock(engine, page, x = 790F, y = 70F)
val combinedBlock = createImageGraphicBlock(engine, page, x = 70F, y = 430F)
val presetBlock = createImageGraphicBlock(engine, page, x = 430F, y = 430F)
val batchBlock = createImageGraphicBlock(engine, page, x = 790F, y = 430F)
val sceneSupportsEffects = engine.block.supportsEffects(scene)
val pageSupportsEffects = engine.block.supportsEffects(page)
val imageSupportsEffects = engine.block.supportsEffects(imageBlock)
require(!sceneSupportsEffects) { "Scene blocks do not render effect stacks." }
require(pageSupportsEffects) { "Pages can render effects." }
require(imageSupportsEffects) { "Image-backed graphic blocks can render effects." }
val pixelizeEffect = engine.block.createEffect(type = EffectType.Pixelize)
val adjustmentsEffect = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.appendEffect(block = imageBlock, effectBlock = pixelizeEffect)
engine.block.insertEffect(block = imageBlock, effectBlock = adjustmentsEffect, index = 0)
val orderedEffects = engine.block.getEffects(imageBlock)
val orderedStackMatches = orderedEffects == listOf(adjustmentsEffect, pixelizeEffect)
check(orderedStackMatches)
val pixelizeProperties = engine.block.findAllProperties(pixelizeEffect)
val adjustmentProperties = engine.block.findAllProperties(adjustmentsEffect)
engine.block.setInt(pixelizeEffect, property = "effect/pixelize/horizontalPixelSize", value = 18)
engine.block.setInt(pixelizeEffect, property = "effect/pixelize/verticalPixelSize", value = 18)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/brightness", value = 0.18F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/contrast", value = 0.22F)
val brightness = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/brightness")
val horizontalPixelSize = engine.block.getInt(pixelizeEffect, property = "effect/pixelize/horizontalPixelSize")
check(pixelizeProperties.contains("effect/pixelize/horizontalPixelSize"))
check(adjustmentProperties.contains("effect/adjustments/brightness"))
check(abs(brightness - 0.18F) < 0.0001F)
check(horizontalPixelSize == 18)
val lutFilter = engine.block.createEffect(type = EffectType.LutFilter)
engine.block.setString(lutFilter, property = "effect/lut_filter/lutFileURI", value = sampleLutUri)
engine.block.setInt(lutFilter, property = "effect/lut_filter/horizontalTileCount", value = 5)
engine.block.setInt(lutFilter, property = "effect/lut_filter/verticalTileCount", value = 5)
engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.85F)
engine.block.appendEffect(block = lutBlock, effectBlock = lutFilter)
val lutUri = engine.block.getString(lutFilter, property = "effect/lut_filter/lutFileURI")
val lutIntensity = engine.block.getFloat(lutFilter, property = "effect/lut_filter/intensity")
check(lutUri == sampleLutUri)
check(abs(lutIntensity - 0.85F) < 0.0001F)
val duotoneFilter = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.setColor(
duotoneFilter,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.08F, g = 0.12F, b = 0.22F, a = 1F),
)
engine.block.setColor(
duotoneFilter,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 0.96F, g = 0.76F, b = 0.34F, a = 1F),
)
engine.block.setFloat(duotoneFilter, property = "effect/duotone_filter/intensity", value = 0.65F)
engine.block.appendEffect(block = duotoneBlock, effectBlock = duotoneFilter)
val duotoneIntensity = engine.block.getFloat(duotoneFilter, property = "effect/duotone_filter/intensity")
check(abs(duotoneIntensity - 0.65F) < 0.0001F)
val combinedAdjustments = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.setFloat(combinedAdjustments, property = "effect/adjustments/saturation", value = -0.35F)
val combinedDuotone = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.setColor(
combinedDuotone,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.1F, g = 0.18F, b = 0.3F, a = 1F),
)
engine.block.setColor(
combinedDuotone,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 0.9F, g = 0.55F, b = 0.22F, a = 1F),
)
engine.block.setFloat(combinedDuotone, property = "effect/duotone_filter/intensity", value = 0.5F)
val combinedPixelize = engine.block.createEffect(type = EffectType.Pixelize)
engine.block.setInt(combinedPixelize, property = "effect/pixelize/horizontalPixelSize", value = 12)
engine.block.setInt(combinedPixelize, property = "effect/pixelize/verticalPixelSize", value = 12)
engine.block.appendEffect(block = combinedBlock, effectBlock = combinedDuotone)
engine.block.appendEffect(block = combinedBlock, effectBlock = combinedPixelize)
engine.block.insertEffect(block = combinedBlock, effectBlock = combinedAdjustments, index = 0)
val combinedEffects = engine.block.getEffects(combinedBlock)
val combinedStackMatches = combinedEffects == listOf(
combinedAdjustments,
combinedDuotone,
combinedPixelize,
)
check(combinedStackMatches)
val currentEffects = engine.block.getEffects(imageBlock)
val currentAdjustmentProperties = engine.block.findAllProperties(adjustmentsEffect)
check(currentEffects == orderedEffects)
check(currentAdjustmentProperties.contains("effect/adjustments/contrast"))
engine.block.setEffectEnabled(effectBlock = pixelizeEffect, enabled = false)
val disabledState = engine.block.isEffectEnabled(pixelizeEffect)
engine.block.setEffectEnabled(effectBlock = pixelizeEffect, enabled = true)
val enabledState = engine.block.isEffectEnabled(pixelizeEffect)
check(!disabledState)
check(enabledState)
val removableIndex = engine.block.getEffects(imageBlock).indexOf(pixelizeEffect)
require(removableIndex >= 0) { "The pixelize effect must be attached before removal." }
engine.block.removeEffect(block = imageBlock, index = removableIndex)
engine.block.destroy(pixelizeEffect)
val removed = engine.block.getEffects(imageBlock).none { effect -> effect == pixelizeEffect }
check(removed)
val batchTargets = listOf(imageBlock, lutBlock, duotoneBlock, batchBlock)
val batchEffects = mutableListOf()
for (targetBlock in batchTargets) {
if (engine.block.supportsEffects(targetBlock)) {
val batchAdjustment = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.setFloat(batchAdjustment, property = "effect/adjustments/brightness", value = 0.08F)
engine.block.appendEffect(block = targetBlock, effectBlock = batchAdjustment)
batchEffects += batchAdjustment
}
}
check(batchEffects.size == batchTargets.size)
val presetValues = mapOf(
"effect/adjustments/brightness" to -0.08F,
"effect/adjustments/contrast" to 0.3F,
"effect/adjustments/saturation" to -0.2F,
)
val presetAdjustment = engine.block.createEffect(type = EffectType.Adjustments)
for ((property, value) in presetValues) {
engine.block.setFloat(presetAdjustment, property = property, value = value)
}
engine.block.appendEffect(block = presetBlock, effectBlock = presetAdjustment)
val presetSaturation = engine.block.getFloat(presetAdjustment, property = "effect/adjustments/saturation")
check(abs(presetSaturation - -0.2F) < 0.0001F)
val exportedPage = engine.block.export(page, mimeType = MimeType.PNG)
return ApplyFiltersAndEffects(
sceneSupportsEffects = sceneSupportsEffects,
pageSupportsEffects = pageSupportsEffects,
imageSupportsEffects = imageSupportsEffects,
orderedStackMatches = orderedStackMatches,
pixelizePropertyCount = pixelizeProperties.size,
adjustmentPropertyCount = adjustmentProperties.size,
brightness = brightness,
horizontalPixelSize = horizontalPixelSize,
lutUri = lutUri,
lutIntensity = lutIntensity,
duotoneIntensity = duotoneIntensity,
combinedStackMatches = combinedStackMatches,
queriedStackSize = currentEffects.size,
disabledState = disabledState,
enabledState = enabledState,
removed = removed,
batchEffectCount = batchEffects.size,
presetSaturation = presetSaturation,
exportedPage = exportedPage,
)
}
private fun createImageGraphicBlock(
engine: Engine,
page: DesignBlock,
x: Float,
y: Float,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(block, value = x)
engine.block.setPositionY(block, value = y)
engine.block.setWidth(block, value = 310F)
engine.block.setHeight(block, value = 310F)
engine.block.appendChild(parent = page, child = block)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = sampleImageUri,
)
engine.block.setFill(block = block, fill = fill)
return block
}
```
Apply color grading, pixelization, duotone, LUT filters, and other visual
treatments to design elements with CE.SDK's effect system.

> **Reading time:** 9 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-filters-and-effects-apply)
CE.SDK uses one effect stack for both filters and effects. **Filters** apply color transformations such as LUT filters and duotone, while **effects** apply visual modifications such as pixelize, vignette, Extrude Blur, and image adjustments. You create both with `createEffect()`, attach them to a compatible block, and then configure their properties with the same typed property APIs used for design blocks.
## Using the Built-in Effects UI
The default Android editor exposes filters, effects, and adjustments through its appearance controls when the current selection supports them. The UI uses the same effect stack APIs shown below: it creates or reuses an effect block, attaches it to the selected design block, writes effect properties, and removes incompatible effects from the same UI group.
> **Note:** The [Photo Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) and [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) provide complete editor surfaces with built-in appearance controls. Use the Engine API below when your app needs automation, presets, batch processing, or custom controls.
## Programmatic Effect Application
Apply, configure, and combine effects directly through the engine's block API.
### Check Effect Support
Not every block can render effects, so check compatibility before creating effect blocks. The sample verifies that scene blocks do not support effects, while pages and image-backed graphic blocks do.
```kotlin highlight-android-check-effect-support
val sceneSupportsEffects = engine.block.supportsEffects(scene)
val pageSupportsEffects = engine.block.supportsEffects(page)
val imageSupportsEffects = engine.block.supportsEffects(imageBlock)
require(!sceneSupportsEffects) { "Scene blocks do not render effect stacks." }
require(pageSupportsEffects) { "Pages can render effects." }
require(imageSupportsEffects) { "Image-backed graphic blocks can render effects." }
```
### Create an Effect
Create effect blocks with the type-safe `EffectType` overload. Creating an effect does not change the image yet; the effect has to be attached to a compatible block.
```kotlin highlight-android-create-effects
val pixelizeEffect = engine.block.createEffect(type = EffectType.Pixelize)
val adjustmentsEffect = engine.block.createEffect(type = EffectType.Adjustments)
```
### Add Effects to a Block
Attach an effect to the end of a block's effect stack with `appendEffect()`, or place it at a specific position with `insertEffect()`. Effects render in stack order, so the inserted adjustments effect runs before the pixelize effect in this example.
```kotlin highlight-android-add-effects
engine.block.appendEffect(block = imageBlock, effectBlock = pixelizeEffect)
engine.block.insertEffect(block = imageBlock, effectBlock = adjustmentsEffect, index = 0)
val orderedEffects = engine.block.getEffects(imageBlock)
```
### Configure Effect Properties
Each effect exposes its own property keys. Use `findAllProperties()` to inspect them, then write values with the typed setter that matches the property type.
```kotlin highlight-android-configure-effect-properties
val pixelizeProperties = engine.block.findAllProperties(pixelizeEffect)
val adjustmentProperties = engine.block.findAllProperties(adjustmentsEffect)
engine.block.setInt(pixelizeEffect, property = "effect/pixelize/horizontalPixelSize", value = 18)
engine.block.setInt(pixelizeEffect, property = "effect/pixelize/verticalPixelSize", value = 18)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/brightness", value = 0.18F)
engine.block.setFloat(adjustmentsEffect, property = "effect/adjustments/contrast", value = 0.22F)
val brightness = engine.block.getFloat(adjustmentsEffect, property = "effect/adjustments/brightness")
val horizontalPixelSize = engine.block.getInt(pixelizeEffect, property = "effect/pixelize/horizontalPixelSize")
```
Common setters for effects are:
| Setter | Use |
| --- | --- |
| `setFloat()` | Intensity, brightness, contrast, saturation, and other continuous values |
| `setInt()` | Discrete values such as pixel size and LUT tile counts |
| `setString()` | URI-like string properties such as LUT file paths |
| `setColor()` | Color properties such as duotone dark and light colors |
### Apply LUT Filters
A LUT filter maps source colors through a lookup-table image. Set the LUT file URI, the horizontal and vertical tile counts baked into that image, and the blend intensity before attaching the filter to the block.
```kotlin highlight-android-apply-lut-filter
val lutFilter = engine.block.createEffect(type = EffectType.LutFilter)
engine.block.setString(lutFilter, property = "effect/lut_filter/lutFileURI", value = sampleLutUri)
engine.block.setInt(lutFilter, property = "effect/lut_filter/horizontalTileCount", value = 5)
engine.block.setInt(lutFilter, property = "effect/lut_filter/verticalTileCount", value = 5)
engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.85F)
engine.block.appendEffect(block = lutBlock, effectBlock = lutFilter)
```
Use the [Create a Custom LUT Filter](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/) guide when you need to build or package your own LUT assets.
### Apply Duotone Filters
A duotone filter maps darker image tones toward one color and lighter tones toward another. Set both colors with `setColor()` and adjust the intensity with a float value.
```kotlin highlight-android-apply-duotone-filter
val duotoneFilter = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.setColor(
duotoneFilter,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.08F, g = 0.12F, b = 0.22F, a = 1F),
)
engine.block.setColor(
duotoneFilter,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 0.96F, g = 0.76F, b = 0.34F, a = 1F),
)
engine.block.setFloat(duotoneFilter, property = "effect/duotone_filter/intensity", value = 0.65F)
engine.block.appendEffect(block = duotoneBlock, effectBlock = duotoneFilter)
```
The duotone intensity value ranges from `-1F` to `1F`: positive values emphasize the light color and negative values emphasize the dark color.
### Combine Multiple Effects
Stack effects to create a layered treatment. This sample inserts adjustments at index `0`, then applies duotone and pixelization after it so color changes happen before the stylized effects.
```kotlin highlight-android-combine-effects
val combinedAdjustments = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.setFloat(combinedAdjustments, property = "effect/adjustments/saturation", value = -0.35F)
val combinedDuotone = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.setColor(
combinedDuotone,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.1F, g = 0.18F, b = 0.3F, a = 1F),
)
engine.block.setColor(
combinedDuotone,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 0.9F, g = 0.55F, b = 0.22F, a = 1F),
)
engine.block.setFloat(combinedDuotone, property = "effect/duotone_filter/intensity", value = 0.5F)
val combinedPixelize = engine.block.createEffect(type = EffectType.Pixelize)
engine.block.setInt(combinedPixelize, property = "effect/pixelize/horizontalPixelSize", value = 12)
engine.block.setInt(combinedPixelize, property = "effect/pixelize/verticalPixelSize", value = 12)
engine.block.appendEffect(block = combinedBlock, effectBlock = combinedDuotone)
engine.block.appendEffect(block = combinedBlock, effectBlock = combinedPixelize)
engine.block.insertEffect(block = combinedBlock, effectBlock = combinedAdjustments, index = 0)
val combinedEffects = engine.block.getEffects(combinedBlock)
```
Use `getEffects()` to verify the order whenever your preset depends on stack position.
## Managing Applied Effects
Inspect, toggle, and remove effects after they are attached to a block.
### Query Applied Effects
Read a block's ordered effect list with `getEffects()`. This is the entry point for custom effect-management UI, validation, and preset synchronization.
```kotlin highlight-android-query-effects
val currentEffects = engine.block.getEffects(imageBlock)
val currentAdjustmentProperties = engine.block.findAllProperties(adjustmentsEffect)
```
### Enable and Disable Effects
Toggle an effect without removing it from the stack. Disabled effects keep their configured properties and render again when you enable them.
```kotlin highlight-android-toggle-effects
engine.block.setEffectEnabled(effectBlock = pixelizeEffect, enabled = false)
val disabledState = engine.block.isEffectEnabled(pixelizeEffect)
engine.block.setEffectEnabled(effectBlock = pixelizeEffect, enabled = true)
val enabledState = engine.block.isEffectEnabled(pixelizeEffect)
```
This pattern is useful for before/after previews and for temporarily reducing work during heavier editing operations.
### Remove Effects
`removeEffect()` detaches the effect at a specific stack index. If you no longer need that effect block, call `destroy()` after removing it.
```kotlin highlight-android-remove-effects
val removableIndex = engine.block.getEffects(imageBlock).indexOf(pixelizeEffect)
require(removableIndex >= 0) { "The pixelize effect must be attached before removal." }
engine.block.removeEffect(block = imageBlock, index = removableIndex)
engine.block.destroy(pixelizeEffect)
val removed = engine.block.getEffects(imageBlock).none { effect -> effect == pixelizeEffect }
```
Effects still attached to a design block are destroyed automatically when that block is destroyed.
## Additional Techniques
Patterns for applying effects at scale and packaging effect values for reuse.
### Batch Processing
When applying the same treatment to multiple blocks, iterate over the targets and check `supportsEffects()` for each one before creating an effect.
```kotlin highlight-android-batch-processing
val batchTargets = listOf(imageBlock, lutBlock, duotoneBlock, batchBlock)
val batchEffects = mutableListOf()
for (targetBlock in batchTargets) {
if (engine.block.supportsEffects(targetBlock)) {
val batchAdjustment = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.setFloat(batchAdjustment, property = "effect/adjustments/brightness", value = 0.08F)
engine.block.appendEffect(block = targetBlock, effectBlock = batchAdjustment)
batchEffects += batchAdjustment
}
}
```
Create a fresh effect instance per target block unless you intentionally want to share the same effect block.
### Reusable Effect Presets
Represent presets as data and write each property to a new effect block. This keeps brand styles, campaign looks, or user favorites consistent across blocks and sessions.
```kotlin highlight-android-reusable-preset
val presetValues = mapOf(
"effect/adjustments/brightness" to -0.08F,
"effect/adjustments/contrast" to 0.3F,
"effect/adjustments/saturation" to -0.2F,
)
val presetAdjustment = engine.block.createEffect(type = EffectType.Adjustments)
for ((property, value) in presetValues) {
engine.block.setFloat(presetAdjustment, property = property, value = value)
}
engine.block.appendEffect(block = presetBlock, effectBlock = presetAdjustment)
```
Effect properties are fixed values, not animation keyframes. To create animated visual changes, animate the block or use CE.SDK's animation APIs instead of mutating effect properties on a timer.
## Performance Considerations
Effects render on the GPU, but every effect in a visible block's stack still adds work:
- Keep stacks short on lower-powered Android devices.
- Prefer adjustments when they can achieve the desired look; LUT filters and blur-style effects are usually heavier.
- Apply effects sparingly to video blocks to keep preview and export responsive.
- Disable effects with `setEffectEnabled()` during intensive editing when the final look does not need to be visible.
Test effect presets on the devices your app supports before shipping them as defaults.
## Troubleshooting
| Issue | Fix |
| --- | --- |
| Effect has no visible result | Make sure the effect is attached with `appendEffect()` or `insertEffect()` and that at least one non-default property is set. |
| Effect cannot be attached | Check `supportsEffects()` on the target block before creating or inserting the effect. |
| LUT filter is missing or neutral | Verify the LUT URI is reachable and that `horizontalTileCount` and `verticalTileCount` match the LUT image. |
| Effect still renders after removal | `removeEffect()` detaches the effect from the stack. Call `destroy()` on unused effect blocks you no longer need. |
| Saved scene reloads without the expected LUT look | Keep external resources such as `effect/lut_filter/lutFileURI` reachable after loading the scene. |
## API Reference
### Methods
| API | Description |
| --- | --- |
| `engine.block.supportsEffects(block=_)` | Checks whether a block can render effects |
| `engine.block.createEffect(type=EffectType.Pixelize)` | Creates a pixelize effect block |
| `engine.block.createEffect(type=EffectType.Adjustments)` | Creates an adjustments effect block |
| `engine.block.createEffect(type=EffectType.LutFilter)` | Creates a LUT filter effect block |
| `engine.block.createEffect(type=EffectType.DuoToneFilter)` | Creates a duotone filter effect block |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Adds an effect to the end of a block's effect stack |
| `engine.block.insertEffect(block=_, effectBlock=_, index=_)` | Inserts an effect at a specific stack index |
| `engine.block.getEffects(block=_)` | Returns the ordered effects attached to a block |
| `engine.block.findAllProperties(block=_)` | Lists the properties available on an effect block |
| `engine.block.setInt(block=_, property="effect/pixelize/horizontalPixelSize", value=_)` | Writes an integer effect property |
| `engine.block.setInt(block=_, property="effect/pixelize/verticalPixelSize", value=_)` | Writes the vertical pixel block size |
| `engine.block.getInt(block=_, property="effect/pixelize/horizontalPixelSize")` | Reads an integer effect property |
| `engine.block.setFloat(block=_, property="effect/adjustments/brightness", value=_)` | Writes a float effect property |
| `engine.block.setFloat(block=_, property="effect/adjustments/contrast", value=_)` | Writes the contrast adjustment |
| `engine.block.setFloat(block=_, property="effect/adjustments/saturation", value=_)` | Writes the saturation adjustment |
| `engine.block.getFloat(block=_, property="effect/adjustments/brightness")` | Reads a float effect property |
| `engine.block.setString(block=_, property="effect/lut_filter/lutFileURI", value=_)` | Writes a string effect property |
| `engine.block.setInt(block=_, property="effect/lut_filter/horizontalTileCount", value=_)` | Writes the LUT tile count per row |
| `engine.block.setInt(block=_, property="effect/lut_filter/verticalTileCount", value=_)` | Writes the LUT tile count per column |
| `engine.block.setFloat(block=_, property="effect/lut_filter/intensity", value=_)` | Writes the LUT blend intensity |
| `engine.block.setColor(block=_, property="effect/duotone_filter/darkColor", value=_)` | Writes a color effect property |
| `engine.block.setColor(block=_, property="effect/duotone_filter/lightColor", value=_)` | Writes the duotone highlight color |
| `engine.block.setFloat(block=_, property="effect/duotone_filter/intensity", value=_)` | Writes the duotone mixing weight |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Creates a color value for color effect properties |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Returns whether an effect block is enabled |
| `engine.block.removeEffect(block=_, index=_)` | Removes the effect at a stack index |
| `engine.block.destroy(block=_)` | Destroys an unused effect block |
### Properties
| Property | Type | Description |
| --- | --- | --- |
| `effect/adjustments/brightness` | Float | Brightness adjustment |
| `effect/adjustments/contrast` | Float | Contrast adjustment |
| `effect/adjustments/saturation` | Float | Saturation adjustment |
| `effect/pixelize/horizontalPixelSize` | Int | Horizontal pixel block size |
| `effect/pixelize/verticalPixelSize` | Int | Vertical pixel block size |
| `effect/lut_filter/lutFileURI` | String | LUT image URI |
| `effect/lut_filter/horizontalTileCount` | Int | Number of LUT tiles per row |
| `effect/lut_filter/verticalTileCount` | Int | Number of LUT tiles per column |
| `effect/lut_filter/intensity` | Float | LUT blend amount from `0F` to `1F` |
| `effect/duotone_filter/darkColor` | Color | Color mapped to shadows |
| `effect/duotone_filter/lightColor` | Color | Color mapped to highlights |
| `effect/duotone_filter/intensity` | Float | Duotone mixing weight from `-1F` to `1F` |
## Next Steps
- [Filters & Effects Overview](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) - Browse every filter and effect CE.SDK provides.
- [Create a Custom LUT Filter](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/) - Apply professional color grading with a LUT file.
- [Blur Effects](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) - Soften backgrounds and create depth with the blur API.
- [Chroma Key (Green Screen)](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/) - Remove a background color from an image or video.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Blur Effects"
description: "Apply blur effects to soften backgrounds or create depth and focus in your designs."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Apply Blur](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/)
---
```kotlin file=@cesdk_android_examples/engine-guides-filters-and-effects-blur/BlurEffects.kt reference-only
import android.net.Uri
import ly.img.engine.BlurType
import ly.img.engine.Color
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.ExportOptions
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
private const val PAGE_WIDTH = 800F
private const val PAGE_HEIGHT = 600F
data class BlurEffects(
val pageSupportsBlur: Boolean,
val imageSupportsBlur: Boolean,
val imageAllowsBlur: Boolean,
val radialBlurType: String,
val radialBlurRadius: Float,
val radialBlurEnabled: Boolean,
val disabledState: Boolean,
val reenabledState: Boolean,
val pageSharedBlur: DesignBlock,
val secondarySharedBlur: DesignBlock,
val pageSharedBlurType: String,
val secondarySharedBlurType: String,
val pageSharedBlurEnabled: Boolean,
val secondarySharedBlurEnabled: Boolean,
val oldBlur: DesignBlock,
val replacementBlur: DesignBlock,
val replacementBlurType: String,
val replacementBlurRadius: Float,
val oldBlurValidAfterDestroy: Boolean,
val previewPng: ByteBuffer,
)
suspend fun blurEffects(engine: Engine): BlurEffects {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = PAGE_WIDTH)
engine.block.setHeight(page, value = PAGE_HEIGHT)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(imageBlock, value = PAGE_WIDTH)
engine.block.setHeight(imageBlock, value = PAGE_HEIGHT)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("file:///android_asset/imgly-assets/ly.img.image/images/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
engine.block.appendChild(parent = page, child = imageBlock)
val pageSupportsBlur = engine.block.supportsBlur(page)
val imageSupportsBlur = engine.block.supportsBlur(imageBlock)
val imageAllowsBlur = engine.block.isAllowedByScope(imageBlock, "appearance/blur")
check(imageSupportsBlur) { "The image block must support blur effects." }
check(imageAllowsBlur) { "The image block must allow blur changes." }
val radialBlur = engine.block.createBlur(type = BlurType.Radial)
engine.block.setFloat(block = radialBlur, property = "blur/radial/blurRadius", value = 40F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/radius", value = 100F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/gradientRadius", value = 80F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/x", value = 0.5F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/y", value = 0.5F)
engine.block.setBlur(block = imageBlock, blurBlock = radialBlur)
engine.block.setBlurEnabled(block = imageBlock, enabled = true)
val appliedBlur = engine.block.getBlur(block = imageBlock)
val radialBlurType = engine.block.getType(block = appliedBlur)
val radialBlurRadius = engine.block.getFloat(
block = appliedBlur,
property = "blur/radial/blurRadius",
)
val radialBlurEnabled = engine.block.isBlurEnabled(block = imageBlock)
engine.block.setBlurEnabled(block = imageBlock, enabled = false)
val disabledState = engine.block.isBlurEnabled(block = imageBlock)
engine.block.setBlurEnabled(block = imageBlock, enabled = true)
val reenabledState = engine.block.isBlurEnabled(block = imageBlock)
val previewPng = engine.block.export(
block = page,
mimeType = MimeType.PNG,
options = ExportOptions(targetWidth = PAGE_WIDTH, targetHeight = PAGE_HEIGHT),
)
val secondaryBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(secondaryBlock, shape = engine.block.createShape(ShapeType.Rect))
val secondaryFill = engine.block.createFill(FillType.Color)
engine.block.setFill(block = secondaryBlock, fill = secondaryFill)
engine.block.setFillSolidColor(
block = secondaryBlock,
color = Color.fromRGBA(r = 0.1F, g = 0.18F, b = 0.28F, a = 1F),
)
engine.block.setWidth(secondaryBlock, value = 160F)
engine.block.setHeight(secondaryBlock, value = 160F)
engine.block.setPositionX(secondaryBlock, value = 560F)
engine.block.setPositionY(secondaryBlock, value = 380F)
engine.block.appendChild(parent = page, child = secondaryBlock)
val sharedBlur = engine.block.createBlur(type = BlurType.Uniform)
engine.block.setFloat(block = sharedBlur, property = "blur/uniform/intensity", value = 0.35F)
engine.block.setBlur(block = secondaryBlock, blurBlock = sharedBlur)
engine.block.setBlur(block = page, blurBlock = sharedBlur)
engine.block.setBlurEnabled(block = secondaryBlock, enabled = true)
engine.block.setBlurEnabled(block = page, enabled = true)
val pageSharedBlur = engine.block.getBlur(block = page)
val secondarySharedBlur = engine.block.getBlur(block = secondaryBlock)
val oldBlur = engine.block.getBlur(block = imageBlock)
val linearBlur = engine.block.createBlur(type = BlurType.Linear)
engine.block.setFloat(block = linearBlur, property = "blur/linear/blurRadius", value = 30F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/x1", value = 0F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/y1", value = 0.5F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/x2", value = 1F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/y2", value = 0.5F)
engine.block.setBlur(block = imageBlock, blurBlock = linearBlur)
engine.block.setBlurEnabled(block = imageBlock, enabled = true)
val replacementBlur = engine.block.getBlur(block = imageBlock)
engine.block.destroy(block = oldBlur)
val oldBlurValidAfterDestroy = engine.block.isValid(block = oldBlur)
return BlurEffects(
pageSupportsBlur = pageSupportsBlur,
imageSupportsBlur = imageSupportsBlur,
imageAllowsBlur = imageAllowsBlur,
radialBlurType = radialBlurType,
radialBlurRadius = radialBlurRadius,
radialBlurEnabled = radialBlurEnabled,
disabledState = disabledState,
reenabledState = reenabledState,
pageSharedBlur = pageSharedBlur,
secondarySharedBlur = secondarySharedBlur,
pageSharedBlurType = engine.block.getType(block = pageSharedBlur),
secondarySharedBlurType = engine.block.getType(block = secondarySharedBlur),
pageSharedBlurEnabled = engine.block.isBlurEnabled(block = page),
secondarySharedBlurEnabled = engine.block.isBlurEnabled(block = secondaryBlock),
oldBlur = oldBlur,
replacementBlur = replacementBlur,
replacementBlurType = engine.block.getType(block = replacementBlur),
replacementBlurRadius = engine.block.getFloat(block = replacementBlur, property = "blur/linear/blurRadius"),
oldBlurValidAfterDestroy = oldBlurValidAfterDestroy,
previewPng = previewPng,
)
}
```
Apply blur effects to design elements with CE.SDK's dedicated blur API for
softening backgrounds, creating depth, and focusing attention.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/release-$UBQ_VERSION$/engine-guides-filters-and-effects-blur)
Unlike stackable effects, blur is attached through its own block APIs. A block supports at most one blur at a time, but the same blur instance can be assigned to multiple compatible blocks.
This guide covers the built-in editor UI and the Android Engine APIs for applying and managing blur.
## Using the Built-in Blur UI
The CE.SDK editor UI exposes blur through the Blur action when the effective `appearance/blur` editing permission allows it. The inspector bar shows the action for selected image or video fills, and the dock opens the same Blur sheet for the current page.
The Blur sheet uses the `ly.img.blur` appearance library. Users choose a blur type, then open the selected blur's property controls to adjust intensity, gradient size, focus size, and control-point values depending on the active blur type.
> **Note:** For a complete editor setup that includes the standard Android editor UI,
> start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/).
> `Dock.Button.rememberBlur()` and `InspectorBar.Button.rememberBlur()` create
> button instances; add or reposition them through the list-builder configuration
> described in the [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) and [Inspector
> Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) guides.
## Programmatic Blur Application
### Check Blur Support
`supportsBlur()` checks whether the block type can carry a blur. This capability check is separate from the effective `appearance/blur` permission required by `setBlur()`. Check both before applying blur; a denied permission causes `setBlur()` to throw an `EngineException` with code `BLOCK.SCOPE_PERMISSION_DENIED`.
```kotlin highlight-android-check-blur-support
val pageSupportsBlur = engine.block.supportsBlur(page)
val imageSupportsBlur = engine.block.supportsBlur(imageBlock)
val imageAllowsBlur = engine.block.isAllowedByScope(imageBlock, "appearance/blur")
check(imageSupportsBlur) { "The image block must support blur effects." }
check(imageAllowsBlur) { "The image block must allow blur changes." }
```
### Create Blur
Create a blur block with the type-safe `BlurType.Uniform`, `BlurType.Linear`, `BlurType.Mirrored`, or `BlurType.Radial` constants.
```kotlin highlight-android-create-blur
val radialBlur = engine.block.createBlur(type = BlurType.Radial)
```
### Configure Blur Parameters
Each blur type stores its adjustable values as float properties on the blur block. This example configures a radial blur with a clear center point and a soft transition around it.
```kotlin highlight-android-configure-blur
engine.block.setFloat(block = radialBlur, property = "blur/radial/blurRadius", value = 40F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/radius", value = 100F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/gradientRadius", value = 80F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/x", value = 0.5F)
engine.block.setFloat(block = radialBlur, property = "blur/radial/y", value = 0.5F)
```
### Apply Blur to a Block
Attach the configured blur to the target block with `setBlur()`, then enable it. The blur renders only while it is enabled on that block.
```kotlin highlight-android-apply-blur
engine.block.setBlur(block = imageBlock, blurBlock = radialBlur)
engine.block.setBlurEnabled(block = imageBlock, enabled = true)
```
## Managing Blur
### Access Existing Blur
Use `getBlur()` to retrieve the blur block currently assigned to a block. You can then read its type and property values with the same block APIs used for other design blocks.
```kotlin highlight-android-read-blur
val appliedBlur = engine.block.getBlur(block = imageBlock)
val radialBlurType = engine.block.getType(block = appliedBlur)
val radialBlurRadius = engine.block.getFloat(
block = appliedBlur,
property = "blur/radial/blurRadius",
)
val radialBlurEnabled = engine.block.isBlurEnabled(block = imageBlock)
```
### Enable or Disable Blur
Toggle blur with `setBlurEnabled()` when you want a before/after state without losing the configured blur parameters.
```kotlin highlight-android-toggle-blur
engine.block.setBlurEnabled(block = imageBlock, enabled = false)
val disabledState = engine.block.isBlurEnabled(block = imageBlock)
engine.block.setBlurEnabled(block = imageBlock, enabled = true)
val reenabledState = engine.block.isBlurEnabled(block = imageBlock)
```
When disabled, the blur remains attached to the block but does not render until it is enabled again.
### Share Blur Across Blocks
A single blur instance can be assigned to more than one compatible block. Changes to that shared blur block affect every block that uses it.
```kotlin highlight-android-share-blur
val sharedBlur = engine.block.createBlur(type = BlurType.Uniform)
engine.block.setFloat(block = sharedBlur, property = "blur/uniform/intensity", value = 0.35F)
engine.block.setBlur(block = secondaryBlock, blurBlock = sharedBlur)
engine.block.setBlur(block = page, blurBlock = sharedBlur)
engine.block.setBlurEnabled(block = secondaryBlock, enabled = true)
engine.block.setBlurEnabled(block = page, enabled = true)
val pageSharedBlur = engine.block.getBlur(block = page)
val secondarySharedBlur = engine.block.getBlur(block = secondaryBlock)
```
### Replace Blur
To change blur type, create a new blur block and assign it with `setBlur()`. Destroy the old blur only when you know no other block should use it.
```kotlin highlight-android-replace-blur
val oldBlur = engine.block.getBlur(block = imageBlock)
val linearBlur = engine.block.createBlur(type = BlurType.Linear)
engine.block.setFloat(block = linearBlur, property = "blur/linear/blurRadius", value = 30F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/x1", value = 0F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/y1", value = 0.5F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/x2", value = 1F)
engine.block.setFloat(block = linearBlur, property = "blur/linear/y2", value = 0.5F)
engine.block.setBlur(block = imageBlock, blurBlock = linearBlur)
engine.block.setBlurEnabled(block = imageBlock, enabled = true)
val replacementBlur = engine.block.getBlur(block = imageBlock)
engine.block.destroy(block = oldBlur)
val oldBlurValidAfterDestroy = engine.block.isValid(block = oldBlur)
```
## Blur Types and Properties
CE.SDK provides four blur types. Coordinates use relative values from `0.0` to `1.0`, where `0,0` is the top-left of the block and `1,1` is the bottom-right.
| Blur Type | Property | Default | Description |
| --- | --- | --- | --- |
| `BlurType.Uniform` | `blur/uniform/intensity` | `0.5` | Uniform blur strength from `0.0` to `1.0` |
| `BlurType.Linear` | `blur/linear/blurRadius` | `30` | Linear blur intensity |
| `BlurType.Linear` | `blur/linear/x1`, `blur/linear/y1` | `0`, `0.5` | Linear blur start point |
| `BlurType.Linear` | `blur/linear/x2`, `blur/linear/y2` | `1`, `0.5` | Linear blur end point |
| `BlurType.Mirrored` | `blur/mirrored/blurRadius` | `30` | Mirrored blur intensity |
| `BlurType.Mirrored` | `blur/mirrored/gradientSize` | `50` | Width of the transition zones |
| `BlurType.Mirrored` | `blur/mirrored/size` | `75` | Width of the clear focus band |
| `BlurType.Mirrored` | `blur/mirrored/x1`, `blur/mirrored/y1`, `blur/mirrored/x2`, `blur/mirrored/y2` | `0`, `0.5`, `1`, `0.5` | Mirrored blur axis points |
| `BlurType.Radial` | `blur/radial/blurRadius` | `30` | Radial blur intensity |
| `BlurType.Radial` | `blur/radial/radius` | `75` | Size of the sharp center |
| `BlurType.Radial` | `blur/radial/gradientRadius` | `50` | Width of the transition band |
| `BlurType.Radial` | `blur/radial/x`, `blur/radial/y` | `0.5`, `0.5` | Radial blur center point |
Use `getFloat()` to read any property after setting it when your app needs to persist or compare blur state.
## Troubleshooting
| Symptom | Cause | Solution |
| --- | --- | --- |
| Blur does not appear | The block does not support blur, or blur is disabled | Check `supportsBlur()` before applying and `isBlurEnabled()` after enabling |
| `setBlur()` throws `EngineException` with `BLOCK.SCOPE_PERMISSION_DENIED` | The effective `appearance/blur` permission is denied | Check `isAllowedByScope()` separately from `supportsBlur()` and only change the scope when your editing rules allow it |
| Setting a property throws `EngineException`, typically with `BLOCK.PROPERTY_NOT_FOUND` | The property path is unknown or does not match the blur type | Use an exact property path for the active `BlurType`; inspect the block's available properties when needed |
| Blur appears off-center | Radial, linear, or mirrored control points are outside the intended area | Keep coordinate properties in the `0.0` to `1.0` range |
| Replacing blur changes other blocks | The old blur block is shared | Destroy an old blur only when no other block should keep using it |
## API Reference
| Method | Description |
| --- | --- |
| `engine.block.supportsBlur(block=_)` | Check whether a block supports blur |
| `engine.block.isAllowedByScope(block=_, key="appearance/blur")` | Check whether the effective blur editing permission allows changes |
| `engine.block.createBlur(type=_)` | Create a blur block from a `BlurType` |
| `engine.block.setFloat(block=_, property="blur/radial/blurRadius", value=_)` | Set the radial blur intensity |
| `engine.block.getFloat(block=_, property="blur/radial/blurRadius")` | Read the radial blur intensity |
| `engine.block.setFloat(block=_, property="blur/radial/radius", value=_)` | Set the radial blur's sharp center size |
| `engine.block.getFloat(block=_, property="blur/radial/radius")` | Read the radial blur's sharp center size |
| `engine.block.setFloat(block=_, property="blur/radial/gradientRadius", value=_)` | Set the radial blur transition width |
| `engine.block.getFloat(block=_, property="blur/radial/gradientRadius")` | Read the radial blur transition width |
| `engine.block.setFloat(block=_, property="blur/radial/x", value=_)` | Set the radial blur center's x-coordinate |
| `engine.block.getFloat(block=_, property="blur/radial/x")` | Read the radial blur center's x-coordinate |
| `engine.block.setFloat(block=_, property="blur/radial/y", value=_)` | Set the radial blur center's y-coordinate |
| `engine.block.getFloat(block=_, property="blur/radial/y")` | Read the radial blur center's y-coordinate |
| `engine.block.setFloat(block=_, property="blur/uniform/intensity", value=_)` | Set the uniform blur intensity |
| `engine.block.getFloat(block=_, property="blur/uniform/intensity")` | Read the uniform blur intensity |
| `engine.block.setFloat(block=_, property="blur/linear/blurRadius", value=_)` | Set the linear blur intensity |
| `engine.block.getFloat(block=_, property="blur/linear/blurRadius")` | Read the linear blur intensity |
| `engine.block.setFloat(block=_, property="blur/linear/x1", value=_)` | Set the linear blur start point's x-coordinate |
| `engine.block.getFloat(block=_, property="blur/linear/x1")` | Read the linear blur start point's x-coordinate |
| `engine.block.setFloat(block=_, property="blur/linear/y1", value=_)` | Set the linear blur start point's y-coordinate |
| `engine.block.getFloat(block=_, property="blur/linear/y1")` | Read the linear blur start point's y-coordinate |
| `engine.block.setFloat(block=_, property="blur/linear/x2", value=_)` | Set the linear blur end point's x-coordinate |
| `engine.block.getFloat(block=_, property="blur/linear/x2")` | Read the linear blur end point's x-coordinate |
| `engine.block.setFloat(block=_, property="blur/linear/y2", value=_)` | Set the linear blur end point's y-coordinate |
| `engine.block.getFloat(block=_, property="blur/linear/y2")` | Read the linear blur end point's y-coordinate |
| `engine.block.setBlur(block=_, blurBlock=_)` | Assign a blur block to a design block |
| `engine.block.getBlur(block=_)` | Retrieve the blur block assigned to a design block |
| `engine.block.setBlurEnabled(block=_, enabled=_)` | Enable or disable blur rendering for a design block |
| `engine.block.isBlurEnabled(block=_)` | Check whether blur rendering is enabled |
| `engine.block.getType(block=_)` | Read the engine type identifier of a blur block |
| `engine.block.destroy(block=_)` | Destroy an unused blur block |
| `engine.block.isValid(block=_)` | Check whether a destroyed blur block is still valid |
## Next Steps
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Stack visual effects such as adjustments, LUT filters, and duotone alongside blur.
- [Filters & Effects Overview](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) - Browse every filter and effect CE.SDK provides.
- [Modify Properties](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Understand block properties and how to modify them.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Chroma Key (Green Screen) in Android (Kotlin)"
description: "Use CE.SDK's green/blue screen keyer to replace backgrounds, tune edges & spill, and composite subjects over virtual scenes."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Apply Chroma Key (Green Screen)](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/)
---
Chroma keying removes a uniform background color (often green or blue) from a video or image so you can composite the foreground over a new scene. In CE.SDK for Android, chroma keying is an **effect** you attach to an image or video block, with parameters for **color selection**, **similarity threshold**, **edge smoothing**, and **spill suppression**. This guide walks you through applying the effect in Kotlin, dialing it in for clean edges, and composing the keyed result with a replacement background.
## What You'll Learn
- How to add the **Green Screen** effect to **image** and **video** blocks.
- How to set the key color (green by default, but any color works).
- How to tune **colorMatch** (similarity), **smoothness** (edge falloff), and **spill** (desaturating color cast).
- How to layer a new background behind the keyed subject.
- How to persist, export, and protect templates that include chroma key.
## When to Use It
Use chroma key when your source contains a uniform backdrop (green, blue, or a solid brand color) and you want to:
- Replace the background with a **virtual set**, branded plate, or blurred depth backdrop.
- Place talent over **slides** or **product footage**.
- Standardize a team's talking‑head videos with consistent backgrounds.
- Composite when using an asset formats such as MP4, H.264 or, JPEG that don't support transparency.
Avoid chroma key if the subject's clothing, props, or lighting contains the same hue as your key color, or if the background is highly textured.
> **Chroma Key vs. Background Removal:** The `effect/green_screen` shader operates on color similarity directly on the GPU. Unlike AI-based background removal, chroma keying provides predictable, real‑time control for studio footage where lighting and backdrop color are controlled.
## Apply the Green Screen Effect In a Prebuilt Editor
Chroma key is one of the standard effects available for images and video clips in the prebuilt editors, such as the Design Editor and the Video Editor. Use it as follows:
1. Select a key image or video clip.
2. Look for the `Effects` button in the inspector and tap it.

Scroll through the effects until you find "Green Screen". Once you tap it, the effect implements immediately.

An options indicator appears for the effect. Tap it to show the options.

Use the sliders and the color wheel, to change the settings for:
- key color
- color match
- smoothness
- spill

The "Tuning the Effect" section below explains each of these in detail.
## Apply the Green Screen Effect In Code
CE.SDK exposes chroma key as the `EffectType.GreenScreen` effect type with the following key properties:
- `effect/green_screen/fromColor` the color to key out (default green).
- `effect/green_screen/colorMatch` similarity threshold \[0…1].
- `effect/green_screen/smoothness` edge falloff \[0…1].
- `effect/green_screen/spill` desaturates remaining color spill \[0…1].
### Key an Image Block
```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.Color
import ly.img.engine.EffectType
import ly.img.engine.Engine
fun applyGreenScreenToImage(
engine: Engine,
imageBlock: Int
) = CoroutineScope(Dispatchers.Main).launch {
// 1) Create the effect and attach it to the block
val keyer = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.appendEffect(imageBlock, effectBlock = keyer)
// 2) Choose the key color (here: pure green); any color works
engine.block.setColor(
keyer,
property = "effect/green_screen/fromColor",
color = Color.fromRGBA(r = 0.0f, g = 1.0f, b = 0.0f, a = 1.0f)
)
// 3) Tune similarity, smoothness, and spill
engine.block.setFloat(keyer, property = "effect/green_screen/colorMatch", value = 0.40f)
engine.block.setFloat(keyer, property = "effect/green_screen/smoothness", value = 0.08f)
engine.block.setFloat(keyer, property = "effect/green_screen/spill", value = 0.15f)
}
```
### Key a Video Block
Video blocks use video fills instead of image fills, but the rest of the workflow is identical.
```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ly.img.engine.Color
import ly.img.engine.EffectType
import ly.img.engine.Engine
fun applyGreenScreenToVideo(
engine: Engine,
videoBlock: Int
) = CoroutineScope(Dispatchers.Main).launch {
val keyer = engine.block.createEffect(type = EffectType.GreenScreen)
engine.block.appendEffect(videoBlock, effectBlock = keyer)
// Blue screen example
engine.block.setColor(
keyer,
property = "effect/green_screen/fromColor",
color = Color.fromRGBA(r = 0.0f, g = 0.25f, b = 1.0f, a = 1.0f)
)
engine.block.setFloat(keyer, property = "effect/green_screen/colorMatch", value = 0.35f)
engine.block.setFloat(keyer, property = "effect/green_screen/smoothness", value = 0.10f)
engine.block.setFloat(keyer, property = "effect/green_screen/spill", value = 0.25f)
}
```
Order matters: if you add other effects, like color adjustments, place the **keyer first** in the stack so later effects operate on the premultiplied result.
### Pick the Key Color from the Image
Hard‑coding `fromColor` works for controlled shoots. In general, sample the background color under the user's tap.
> **Note:** CE.SDK doesn't provide a built-in API to read a pixel at a screen coordinate. In your Android app, map the tap location to the image/video buffer you control and sample the pixel using APIs such as `Bitmap.getPixel()` or `Canvas`. Convert the sampled RGBA to `Color.fromRGBA()` and set `effect/green_screen/fromColor`. If you embed the `DesignEditor`, keep an app-level copy of the media to sample from, since the editor's preview is GPU-rendered.
Tie the sampled color back to the effect:
```kotlin
import ly.img.engine.Color
import ly.img.engine.Engine
fun setKeyColor(engine: Engine, keyer: Int, r: Float, g: Float, b: Float) {
engine.block.setColor(
keyer,
property = "effect/green_screen/fromColor",
color = Color.fromRGBA(r = r, g = g, b = b, a = 1.0f)
)
}
```
For polished UIs, show a zoomed loupe and a live matte preview as the user drags.
### Composite over a Replacement Background
A keyed subject is transparent where the background was, so you **layer a background block beneath** the keyed block.
```kotlin
import ly.img.engine.DesignBlockType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
fun addBackgroundBehind(engine: Engine, page: Int, subject: Int, imageUrl: String) {
val bg = engine.block.create(DesignBlockType.Graphic)
val shape = engine.block.createShape(ShapeType.Rect)
engine.block.setShape(bg, shape = shape)
val fill = engine.block.createFill(FillType.Image)
engine.block.setString(fill, property = "fill/image/imageFileURI", value = imageUrl)
engine.block.setFill(bg, fill = fill)
// Make background full‑bleed on the page
// Place background **behind** subject
engine.block.insertChild(parent = page, child = bg, index = 0)
engine.block.fillParent(bg)
engine.block.sendToBack(bg)
}
```
For video, create a video fill instead of an image fill and align durations in your export.
## Tuning the Effect
The three parameters for tuning chroma key composition are:
- color match
- spill
- smoothness
Knowing what they impact can help decide your strategy when the composition doesn't look correct. The examples below all show how these values can change this chroma key image.

> **Recommended Starting Values:** | Background | colorMatch | smoothness | spill |
> |-------------|-------------|------------|--------|
> | **Green Screen** | 0.35–0.45 | 0.08–0.12 | 0.15–0.25 |
> | **Blue Screen** | 0.30–0.40 | 0.10–0.15 | 0.25–0.35 |
> | **Custom Color** | 0.40–0.50 | 0.08–0.12 | 0.10–0.20 |Tune `colorMatch` first for coverage, then refine edge softness with `smoothness`, and finally correct color tint with `spill`.
### Color Match
Color Match determines how close a pixel's color has to be to the key color to be considered *background*. When the value is low, only exact matches are removed. When the value is high, a larger range of colors similar to the key color get removed.
What to watch for when the value is wrong:
- Too low: you may see patches of the green screen still visible around edges, especially if lighting is uneven or shadows present.
- Too high: you risk keying out part of the subject (hair strands, clothing edges, reflective items) creating holes or transparency because the effect is too aggressive.

The preceding image shows color match values of 0.0, 0.5 and 1.0.
### Smoothness
Smoothness controls how gradually or sharply the transitions occur, how soft the matte edges of the gradients are. A low value produces sharp transition between keyed and un-keyed areas. When the value is high, there is softer transition.
What to watch for when the value is wrong:
- Too low: harsh edges, visible fringes around hair or "hard cutouts" that look unnatural.
- Too high: a halo effect or the subject blends into the background.

The preceding image shows smoothness values of 0.0, 0.5 and 1.0.
### Spill
Spill impacts the unwanted "color spill", when your key color reflects or bleeds onto the subject. This is especially noticeable around edges, hair and, shiny objects.
What to watch for when the value is wrong:
- Too low: you may see green reflection on the subject (especially edges/hair/shoulders) that doesn't get cleaned up, making it look unnatural or floating.
- Too high: the subject's actual color edges are desaturated, making hair or detail look gray, faded or too soft.

The preceding image shows spill values of 0.0, 0.5 and 1.0.
## Lighting & Capture Tips
- Keep your backdrop evenly lit and 1–2 stops brighter than your subject.
- Avoid shadows or wrinkles. Uneven color creates transparency artifacts.
- Separate your subject from the background by at least 1 m to reduce spill.
## Template & Scope Considerations
If you ship templates that include a keyer, you might want to lock down parameters to protect quality:
- Use **Scopes/Permissions** to limit which effect properties the end‑user can change.
- Store platform‑tested defaults (match, smoothness, spill) in the template.
- Provide preset chips like **"Green Screen"**, **"Blue Screen"**, **"Brand Cyan"** to switch `fromColor` quickly.
## Performance and Rendering Pipeline
CE.SDK runs chroma keying directly on the graphics card for smooth, real-time results. Place the keyer near the start of your effect list so that later effects, like color or tone adjustments, apply correctly to the transparent areas. To keep playback fast, avoid heavy effects such as blur or LUTs before the keyer.
## Export Tips
- Prefer **ProRes 4444** (or other alpha‑carrying formats) when exporting an intermediate keyed asset to reuse elsewhere.
- For final composites, export with the background enabled and a standard delivery codec/format.
## Testing Checklist
- Verify background color is uniform and well lit.
- Check for reflective surfaces that might cause spill.
- Test both **720p** and **4K** previews to compare performance.
- Try different wardrobe colors. Avoid those close to the key color.
- Examine edges on hair or fine detail under motion.
- Validate output formats (e.g., MP4 with solid background vs. ProRes with alpha).
## Troubleshooting
**❌ Holes in the matte (background not fully removed)**:
- Increase `colorMatch` slightly. If edges get harsh, bump `smoothness` too.
**❌ Foreground punched out (you lose subject detail)**:
- Lower `colorMatch` until detail returns; then reduce `spill` if the subject appears tinted.
**❌ Green/blue color cast on edges**:
- Raise `spill` (try 0.2–0.4). If it looks gray, back it down.
**❌ Jagged edges**:
- Increase `smoothness` in small steps (0.05–0.15).
- Consider adding a light `effect/blur` **after** the keyer for video.
**❌ Uneven backgrounds / shadows**:
- Sample a darker patch of the backdrop or increase `colorMatch` and compensate with `spill`.
**❌ Nothing turns transparent:**
- Verify the effect is attached to the **right block** and not to the page.
- Check `fromColor` is close to the actual backdrop hue (sample it!).
- Ensure your block type supports effects (graphic, video are supported).
**❌ Performance drops with 4K video**:
- Avoid stacking extra heavy effects **before** the keyer.
- Render proxies or downscale the preview while tuning; export at full res.
**❌ Skin tones look dull**:
- Reduce `spill` and re‑tune `colorMatch`.
**❌ Hair/fur looks crunchy:**
- Raise `smoothness` incrementally (and consider light post‑blur).
## Next Steps
With the core of chroma key compositing mastered, here are some other topics that may be interesting:
- Learn about other [Filters & Effects](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) and try combining the keyer with adjustments for color matching.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create Custom Filters"
description: "Extend CE.SDK with custom LUT filter asset sources for brand-specific color grading and filter collections."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-filters-c796ba/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Create Custom Filters](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-filters-c796ba/)
---
```kotlin file=@cesdk_android_examples/engine-guides-create-custom-filters/CreateCustomFilters.kt reference-only
import android.net.Uri
import ly.img.editor.defaultBaseUri
import ly.img.engine.Asset
import ly.img.engine.AssetContext
import ly.img.engine.AssetSource
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.FindAssetsQuery
import ly.img.engine.FindAssetsResult
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
data class CreateCustomFilters(
val customSourceId: String,
val jsonSourceId: String,
val customFilterCount: Int,
val warmToneFilterCount: Int,
val jsonFilterCount: Int,
val appliedFilterLabel: String,
val appliedLutUri: String,
val appliedThumbnailUri: String,
val jsonThumbnailUri: String,
val horizontalTileCount: Int,
val verticalTileCount: Int,
val intensity: Float,
val exportedImage: ByteBuffer,
)
private const val CUSTOM_FILTER_SOURCE_ID = "my-custom-filters"
private const val JSON_FILTER_SOURCE_ID = "my-json-filters"
suspend fun createCustomFilters(
engine: Engine,
assetBaseUri: Uri = defaultBaseUri,
): CreateCustomFilters {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 50F)
engine.block.setPositionY(imageBlock, value = 50F)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 225F)
engine.block.appendChild(parent = page, child = imageBlock)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
val warmLutUri = assetBaseUri.buildUpon()
.appendPath("ly.img.filter.lut")
.appendPath("LUTs")
.appendPath("imgly_lut_ad1920_5_5_128.png")
.build()
.toString()
val warmThumbnailUri = assetBaseUri.buildUpon()
.appendPath("ly.img.filter")
.appendPath("thumbnails")
.appendPath("imgly_lut_ad1920.jpg")
.build()
.toString()
val monochromeLutUri = assetBaseUri.buildUpon()
.appendPath("ly.img.filter.lut")
.appendPath("LUTs")
.appendPath("imgly_lut_bw_5_5_128.png")
.build()
.toString()
val monochromeThumbnailUri = assetBaseUri.buildUpon()
.appendPath("ly.img.filter")
.appendPath("thumbnails")
.appendPath("imgly_lut_bw.jpg")
.build()
.toString()
val customFilters = listOf(
Asset(
id = "vintage-warm",
context = AssetContext(sourceId = CUSTOM_FILTER_SOURCE_ID),
label = "Vintage Warm",
locale = "en",
tags = listOf("vintage", "warm", "retro"),
groups = listOf("Warm Tones"),
meta = mapOf(
"uri" to warmLutUri,
"thumbUri" to warmThumbnailUri,
"horizontalTileCount" to "5",
"verticalTileCount" to "5",
"blockType" to EffectType.LutFilter.key,
),
),
Asset(
id = "cool-cinema",
context = AssetContext(sourceId = CUSTOM_FILTER_SOURCE_ID),
label = "Cool Cinema",
locale = "en",
tags = listOf("cinema", "cool", "film"),
groups = listOf("Cool Tones"),
meta = mapOf(
"uri" to monochromeLutUri,
"thumbUri" to monochromeThumbnailUri,
"horizontalTileCount" to "5",
"verticalTileCount" to "5",
"blockType" to EffectType.LutFilter.key,
),
),
Asset(
id = "bw-classic",
context = AssetContext(sourceId = CUSTOM_FILTER_SOURCE_ID),
label = "B&W Classic",
locale = "en",
tags = listOf("black and white", "classic", "monochrome"),
groups = listOf("Monochrome"),
meta = mapOf(
"uri" to monochromeLutUri,
"thumbUri" to monochromeThumbnailUri,
"horizontalTileCount" to "5",
"verticalTileCount" to "5",
"blockType" to EffectType.LutFilter.key,
),
),
)
val customSource = CustomFilterAssetSource(
sourceId = CUSTOM_FILTER_SOURCE_ID,
filters = customFilters,
)
if (customSource.sourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = customSource.sourceId)
}
engine.asset.addSource(source = customSource)
if (JSON_FILTER_SOURCE_ID in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = JSON_FILTER_SOURCE_ID)
}
val loadedJsonSourceId = engine.asset.addLocalSourceFromJSON(
contentJSON = """
{
"version": "2.0.0",
"id": "$JSON_FILTER_SOURCE_ID",
"assets": [
{
"id": "sunset-glow",
"label": { "en": "Sunset Glow" },
"tags": { "en": ["warm", "sunset", "golden"] },
"groups": ["Warm Tones"],
"meta": {
"uri": "$warmLutUri",
"thumbUri": "$warmThumbnailUri",
"horizontalTileCount": "5",
"verticalTileCount": "5",
"blockType": "${EffectType.LutFilter.key}"
}
},
{
"id": "ocean-breeze",
"label": { "en": "Ocean Breeze" },
"tags": { "en": ["cool", "blue", "ocean"] },
"groups": ["Cool Tones"],
"meta": {
"uri": "$monochromeLutUri",
"thumbUri": "$monochromeThumbnailUri",
"horizontalTileCount": "5",
"verticalTileCount": "5",
"blockType": "${EffectType.LutFilter.key}"
}
}
]
}
""".trimIndent(),
)
check(loadedJsonSourceId == JSON_FILTER_SOURCE_ID)
val customFilterResults = engine.asset.findAssets(
sourceId = CUSTOM_FILTER_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10),
)
val warmToneFilters = engine.asset.findAssets(
sourceId = CUSTOM_FILTER_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10, groups = listOf("Warm Tones")),
)
val jsonFilterResults = engine.asset.findAssets(
sourceId = JSON_FILTER_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10),
)
check(customFilterResults.total == customFilters.size)
check(warmToneFilters.assets.map { it.id } == listOf("vintage-warm"))
check(jsonFilterResults.total == 2)
val filterAsset = warmToneFilters.assets.first()
val filterMeta = filterAsset.meta ?: error("Filter asset ${filterAsset.id} is missing metadata.")
require(engine.block.supportsEffects(imageBlock)) {
"The selected block must support effects before applying a LUT filter."
}
val lutEffect = engine.block.createEffect(type = EffectType.LutFilter)
engine.block.setString(
block = lutEffect,
property = "effect/lut_filter/lutFileURI",
value = filterMeta["uri"] ?: error("Filter asset ${filterAsset.id} is missing meta.uri."),
)
engine.block.setInt(
block = lutEffect,
property = "effect/lut_filter/horizontalTileCount",
value = filterMeta["horizontalTileCount"]?.toInt()
?: error("Filter asset ${filterAsset.id} is missing meta.horizontalTileCount."),
)
engine.block.setInt(
block = lutEffect,
property = "effect/lut_filter/verticalTileCount",
value = filterMeta["verticalTileCount"]?.toInt()
?: error("Filter asset ${filterAsset.id} is missing meta.verticalTileCount."),
)
engine.block.setFloat(
block = lutEffect,
property = "effect/lut_filter/intensity",
value = 0.85F,
)
engine.block.appendEffect(block = imageBlock, effectBlock = lutEffect)
val appliedLutUri = engine.block.getString(
block = lutEffect,
property = "effect/lut_filter/lutFileURI",
)
val appliedThumbnailUri = filterMeta["thumbUri"]
?: error("Filter asset ${filterAsset.id} is missing meta.thumbUri.")
val jsonThumbnailUri = jsonFilterResults.assets.first().meta?.get("thumbUri")
?: error("JSON filter asset is missing meta.thumbUri.")
val horizontalTileCount = engine.block.getInt(
block = lutEffect,
property = "effect/lut_filter/horizontalTileCount",
)
val verticalTileCount = engine.block.getInt(
block = lutEffect,
property = "effect/lut_filter/verticalTileCount",
)
val intensity = engine.block.getFloat(
block = lutEffect,
property = "effect/lut_filter/intensity",
)
val exportedImage = engine.block.export(block = page, mimeType = MimeType.PNG)
return CreateCustomFilters(
customSourceId = customSource.sourceId,
jsonSourceId = loadedJsonSourceId,
customFilterCount = customFilterResults.total,
warmToneFilterCount = warmToneFilters.total,
jsonFilterCount = jsonFilterResults.total,
appliedFilterLabel = filterAsset.label.orEmpty(),
appliedLutUri = appliedLutUri,
appliedThumbnailUri = appliedThumbnailUri,
jsonThumbnailUri = jsonThumbnailUri,
horizontalTileCount = horizontalTileCount,
verticalTileCount = verticalTileCount,
intensity = intensity,
exportedImage = exportedImage,
)
}
private class CustomFilterAssetSource(
sourceId: String,
private val filters: List,
) : AssetSource(sourceId = sourceId) {
override suspend fun getGroups(): List? = filters.flatMap { it.groups.orEmpty() }.distinct()
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult {
val searchQuery = query.query
val queryGroups = query.groups.orEmpty()
val filteredAssets = filters.filter { asset ->
val matchesQuery =
searchQuery.isNullOrBlank() ||
buildList {
asset.label?.let(::add)
addAll(asset.tags.orEmpty())
}.any { value ->
value.contains(searchQuery, ignoreCase = true)
}
val matchesGroups =
queryGroups.isEmpty() ||
asset.groups.orEmpty().any(queryGroups::contains)
matchesQuery && matchesGroups
}
val startIndex = query.page * query.perPage
val pageAssets = filteredAssets.drop(startIndex).take(query.perPage)
val nextPage =
if (startIndex + pageAssets.size < filteredAssets.size) {
query.page + 1
} else {
-1
}
return FindAssetsResult(
assets = pageAssets,
currentPage = query.page,
nextPage = nextPage,
total = filteredAssets.size,
)
}
}
```
Extend CE.SDK with your own LUT filters by creating and registering custom
filter asset sources for brand-specific color grading.

> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-custom-filters)
CE.SDK provides built-in LUT filters, but many applications need brand-specific color grading or custom filter collections. Custom filter asset sources let you register your own LUT filters, query them like any other asset source, and apply the selected metadata to an image block.
This guide covers how to define filter metadata, create a custom asset source, load filters from JSON configuration, query filter assets, and apply a filter to a block.
## Filter Asset Metadata
LUT filters need these properties in the `meta` object:
- **`uri`** - URL to the LUT image file, usually a PNG.
- **`thumbUri`** - URL to a preview thumbnail image, not the tiled LUT atlas.
- **`horizontalTileCount`** - Number of horizontal tiles in the LUT grid.
- **`verticalTileCount`** - Number of vertical tiles in the LUT grid.
- **`blockType`** - `EffectType.LutFilter.key`, which resolves to `//ly.img.ubq/effect/lut_filter`.
The sample builds separate LUT and thumbnail URLs from the SDK asset base URI, then defines three filter assets with localized labels, search tags, groups, and LUT metadata.
```kotlin highlight-android-filter-metadata
val customFilters = listOf(
Asset(
id = "vintage-warm",
context = AssetContext(sourceId = CUSTOM_FILTER_SOURCE_ID),
label = "Vintage Warm",
locale = "en",
tags = listOf("vintage", "warm", "retro"),
groups = listOf("Warm Tones"),
meta = mapOf(
"uri" to warmLutUri,
"thumbUri" to warmThumbnailUri,
"horizontalTileCount" to "5",
"verticalTileCount" to "5",
"blockType" to EffectType.LutFilter.key,
),
),
Asset(
id = "cool-cinema",
context = AssetContext(sourceId = CUSTOM_FILTER_SOURCE_ID),
label = "Cool Cinema",
locale = "en",
tags = listOf("cinema", "cool", "film"),
groups = listOf("Cool Tones"),
meta = mapOf(
"uri" to monochromeLutUri,
"thumbUri" to monochromeThumbnailUri,
"horizontalTileCount" to "5",
"verticalTileCount" to "5",
"blockType" to EffectType.LutFilter.key,
),
),
Asset(
id = "bw-classic",
context = AssetContext(sourceId = CUSTOM_FILTER_SOURCE_ID),
label = "B&W Classic",
locale = "en",
tags = listOf("black and white", "classic", "monochrome"),
groups = listOf("Monochrome"),
meta = mapOf(
"uri" to monochromeLutUri,
"thumbUri" to monochromeThumbnailUri,
"horizontalTileCount" to "5",
"verticalTileCount" to "5",
"blockType" to EffectType.LutFilter.key,
),
),
)
```
## Create a Custom Filter Source
Create an `AssetSource` when you want full control over filtering, pagination, group handling, or remote lookup behavior. The source returns `FindAssetsResult` values from its `findAssets` implementation.
```kotlin highlight-android-custom-source
private class CustomFilterAssetSource(
sourceId: String,
private val filters: List,
) : AssetSource(sourceId = sourceId) {
override suspend fun getGroups(): List? = filters.flatMap { it.groups.orEmpty() }.distinct()
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult {
val searchQuery = query.query
val queryGroups = query.groups.orEmpty()
val filteredAssets = filters.filter { asset ->
val matchesQuery =
searchQuery.isNullOrBlank() ||
buildList {
asset.label?.let(::add)
addAll(asset.tags.orEmpty())
}.any { value ->
value.contains(searchQuery, ignoreCase = true)
}
val matchesGroups =
queryGroups.isEmpty() ||
asset.groups.orEmpty().any(queryGroups::contains)
matchesQuery && matchesGroups
}
val startIndex = query.page * query.perPage
val pageAssets = filteredAssets.drop(startIndex).take(query.perPage)
val nextPage =
if (startIndex + pageAssets.size < filteredAssets.size) {
query.page + 1
} else {
-1
}
return FindAssetsResult(
assets = pageAssets,
currentPage = query.page,
nextPage = nextPage,
total = filteredAssets.size,
)
}
}
```
Register the source with the engine before you query it. The sample removes an existing source with the same ID first so repeated runs stay deterministic.
```kotlin highlight-android-register-source
val customSource = CustomFilterAssetSource(
sourceId = CUSTOM_FILTER_SOURCE_ID,
filters = customFilters,
)
if (customSource.sourceId in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = customSource.sourceId)
}
engine.asset.addSource(source = customSource)
```
## Load Filters From JSON
For static filter collections, load the same metadata from a JSON string with `addLocalSourceFromJSON`. The JSON format includes a `version`, the source `id`, and an `assets` array containing filter definitions.
```kotlin highlight-android-load-json
if (JSON_FILTER_SOURCE_ID in engine.asset.findAllSources()) {
engine.asset.removeSource(sourceId = JSON_FILTER_SOURCE_ID)
}
val loadedJsonSourceId = engine.asset.addLocalSourceFromJSON(
contentJSON = """
{
"version": "2.0.0",
"id": "$JSON_FILTER_SOURCE_ID",
"assets": [
{
"id": "sunset-glow",
"label": { "en": "Sunset Glow" },
"tags": { "en": ["warm", "sunset", "golden"] },
"groups": ["Warm Tones"],
"meta": {
"uri": "$warmLutUri",
"thumbUri": "$warmThumbnailUri",
"horizontalTileCount": "5",
"verticalTileCount": "5",
"blockType": "${EffectType.LutFilter.key}"
}
},
{
"id": "ocean-breeze",
"label": { "en": "Ocean Breeze" },
"tags": { "en": ["cool", "blue", "ocean"] },
"groups": ["Cool Tones"],
"meta": {
"uri": "$monochromeLutUri",
"thumbUri": "$monochromeThumbnailUri",
"horizontalTileCount": "5",
"verticalTileCount": "5",
"blockType": "${EffectType.LutFilter.key}"
}
}
]
}
""".trimIndent(),
)
```
For hosted filter catalogs, call the `contentUri` overload of `addLocalSourceFromJSON`. Use absolute URLs in the JSON or write paths with the `{{base_url}}/...` placeholder so CE.SDK replaces the placeholder with the catalog's base path.
## Query and Apply Filters
Use `findAssets` to query your custom source, group-filtered results, or the source created from JSON.
```kotlin highlight-android-query-filters
val customFilterResults = engine.asset.findAssets(
sourceId = CUSTOM_FILTER_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10),
)
val warmToneFilters = engine.asset.findAssets(
sourceId = CUSTOM_FILTER_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10, groups = listOf("Warm Tones")),
)
val jsonFilterResults = engine.asset.findAssets(
sourceId = JSON_FILTER_SOURCE_ID,
query = FindAssetsQuery(page = 0, perPage = 10),
)
```
To apply a LUT filter, create `EffectType.LutFilter`, copy the LUT metadata into the effect properties, set the intensity, and append the effect to a block that supports effects.
```kotlin highlight-android-apply-filter
val filterAsset = warmToneFilters.assets.first()
val filterMeta = filterAsset.meta ?: error("Filter asset ${filterAsset.id} is missing metadata.")
require(engine.block.supportsEffects(imageBlock)) {
"The selected block must support effects before applying a LUT filter."
}
val lutEffect = engine.block.createEffect(type = EffectType.LutFilter)
engine.block.setString(
block = lutEffect,
property = "effect/lut_filter/lutFileURI",
value = filterMeta["uri"] ?: error("Filter asset ${filterAsset.id} is missing meta.uri."),
)
engine.block.setInt(
block = lutEffect,
property = "effect/lut_filter/horizontalTileCount",
value = filterMeta["horizontalTileCount"]?.toInt()
?: error("Filter asset ${filterAsset.id} is missing meta.horizontalTileCount."),
)
engine.block.setInt(
block = lutEffect,
property = "effect/lut_filter/verticalTileCount",
value = filterMeta["verticalTileCount"]?.toInt()
?: error("Filter asset ${filterAsset.id} is missing meta.verticalTileCount."),
)
engine.block.setFloat(
block = lutEffect,
property = "effect/lut_filter/intensity",
value = 0.85F,
)
engine.block.appendEffect(block = imageBlock, effectBlock = lutEffect)
```
## Export the Result
After applying the filter, export the affected page or block with the regular block export API.
```kotlin highlight-android-export
val exportedImage = engine.block.export(block = page, mimeType = MimeType.PNG)
```
## Troubleshooting
### Filters Not Found in Query
- Verify that the source is registered before calling `findAssets`.
- Check that the source ID in `findAssets` matches the ID you registered or loaded from JSON.
- Include labels, tags, and groups that match your search and filtering logic.
### LUT Not Rendering Correctly
- Verify that `horizontalTileCount` and `verticalTileCount` match the actual LUT image grid.
- Confirm that the LUT URI is reachable from the app and from export.
- Store LUT images as PNG files to avoid compression artifacts.
### JSON Source Not Loading
- Verify that the JSON includes `version`, `id`, and `assets`.
- Keep all metadata values as strings.
- Ensure each filter asset includes `uri`, `thumbUri`, tile counts, and `blockType`.
## API Reference
| Method | Description |
| --- | --- |
| `AssetSource.findAssets(query=_)` | Return matching filter assets from a custom source. |
| `AssetSource.getGroups()` | Return the available filter groups for group-based queries. |
| `engine.asset.addSource(source=_)` | Register a custom asset source. |
| `engine.asset.addLocalSourceFromJSON(contentJSON=_)` | Create a local asset source from inline JSON. |
| `engine.asset.addLocalSourceFromJSON(contentUri=_)` | Create a local asset source from a JSON URI. |
| `engine.asset.findAssets(sourceId=_, query=_)` | Query assets from a registered source. |
| `engine.asset.findAllSources()` | Return all registered asset source IDs. |
| `engine.asset.removeSource(sourceId=_)` | Remove a registered asset source by ID. |
| `engine.block.supportsEffects(block=_)` | Check whether a block can render an effect stack. |
| `engine.block.createEffect(type=EffectType.LutFilter)` | Create a LUT filter effect block. |
| `engine.block.setString(block=_, property="effect/lut_filter/lutFileURI", value=_)` | Set the LUT image URI. |
| `engine.block.setInt(block=_, property="effect/lut_filter/horizontalTileCount", value=_)` | Set the horizontal LUT tile count. |
| `engine.block.setInt(block=_, property="effect/lut_filter/verticalTileCount", value=_)` | Set the vertical LUT tile count. |
| `engine.block.setFloat(block=_, property="effect/lut_filter/intensity", value=_)` | Set the filter intensity. |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Attach the configured effect to a block. |
| `engine.block.export(block=_, mimeType=MimeType.PNG)` | Export the filtered result. |
### Key Types
| Type | Purpose |
| --- | --- |
| `Asset` | Represents a filter returned from `findAssets`. |
| `FindAssetsQuery` | Defines pagination, search, and group filters for asset queries. |
| `FindAssetsResult` | Contains the returned assets and pagination metadata. |
| `EffectType.LutFilter` | Type-safe Android effect constant for LUT filters. |
## Next Steps
Now that you understand how to create and register custom filter sources, explore related topics:
- [Create a Custom LUT Filter](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/) - Understand LUT image format and create your own color grading filters.
- [Blur Effects](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) - Add blur effects to images and videos.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Create a Custom LUT Filter"
description: "Create and apply custom LUT filters to achieve consistent, brand-aligned visual styles."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Apply Custom LUT Filter](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/)
---
```kotlin file=@cesdk_android_examples/engine-guides-custom-lut-filter/CustomLUTFilter.kt reference-only
import android.net.Uri
import ly.img.engine.DesignBlock
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.ShapeType
data class CustomLUTFilterResult(
val imageBlock: DesignBlock,
val lutFilter: DesignBlock,
val appliedEffects: List,
val effectEnabled: Boolean,
)
suspend fun customLUTFilter(engine: Engine): CustomLUTFilterResult {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 100F)
engine.block.setPositionY(imageBlock, value = 50F)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 300F)
engine.block.appendChild(parent = page, child = imageBlock)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(imageBlock, fill = imageFill)
val lutFilter = engine.block.createEffect(EffectType.LutFilter)
val bundledAssetBaseUri = "file:///android_asset/imgly-assets"
val lutUri = Uri.parse(
"$bundledAssetBaseUri/ly.img.filter.lut/LUTs/imgly_lut_ad1920_5_5_128.png",
)
engine.block.setUri(
lutFilter,
property = "effect/lut_filter/lutFileURI",
value = lutUri,
)
engine.block.setInt(lutFilter, property = "effect/lut_filter/verticalTileCount", value = 5)
engine.block.setInt(lutFilter, property = "effect/lut_filter/horizontalTileCount", value = 5)
engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.9F)
val supportsEffects = engine.block.supportsEffects(imageBlock)
require(supportsEffects) { "The image block must support effects." }
engine.block.appendEffect(block = imageBlock, effectBlock = lutFilter)
val appliedEffects = engine.block.getEffects(imageBlock)
check(appliedEffects == listOf(lutFilter))
engine.block.setEffectEnabled(effectBlock = lutFilter, enabled = false)
val disabledState = engine.block.isEffectEnabled(lutFilter)
engine.block.setEffectEnabled(effectBlock = lutFilter, enabled = true)
val effectEnabled = engine.block.isEffectEnabled(lutFilter)
check(!disabledState)
check(effectEnabled)
return CustomLUTFilterResult(
imageBlock = imageBlock,
lutFilter = lutFilter,
appliedEffects = appliedEffects,
effectEnabled = effectEnabled,
)
}
fun removeCustomLUTFilter(
engine: Engine,
imageBlock: DesignBlock,
lutFilter: DesignBlock,
) {
val lutFilterIndex = engine.block.getEffects(imageBlock).indexOf(lutFilter)
require(lutFilterIndex >= 0) { "The LUT filter must be attached before it can be removed." }
engine.block.removeEffect(block = imageBlock, index = lutFilterIndex)
engine.block.destroy(lutFilter)
}
```
Apply custom LUT (Look-Up Table) filters to image blocks with CE.SDK's
Android Engine API.
> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/main/engine-guides-custom-lut-filter)
LUT filters remap colors through a predefined transformation table, making them useful for repeatable color grading and brand-aligned image treatments. This guide shows how to configure a tiled PNG LUT, apply it to an image-backed graphic block, and manage the effect after it is attached.
## 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 width
- **`verticalTileCount`** - Number of tiles down the image height
CE.SDK supports these tile configurations:
- 5 x 5 tiles with 128px cube size
- 8 x 8 tiles with 512px cube size
Standard `.cube` files must be converted to this tiled PNG format before you use them with `EffectType.LutFilter`.
## Creating LUT PNG Images
### Starting From the Identity LUT
The fastest way to author a custom LUT filter is to edit the identity LUT: a neutral 8 x 8 tiled PNG that produces no color change when applied. Any color adjustments you make to the image are recorded as the filter transformation, and the exported PNG can be used directly with the LUT filter effect.
To author a new filter from the identity LUT:
1. [Download the identity LUT](content-assets/6e3f49/identity.png)
2. Open it in an image editor that applies adjustments to the whole image
3. Apply color adjustments such as curves, levels, hue, saturation, or color balance
4. Export the edited image as PNG and include that file in your app or serve it from a URL
Do not crop, rotate, resize, or otherwise change the geometry of the image. Each pixel in the identity LUT is a specific color sample, so reorganizing pixels breaks the color mapping.
> **WARNING:** Save the edited LUT as PNG. Lossy formats can introduce compression artifacts that produce visible color banding.
### Converting .cube to Tiled PNG
If you already have a `.cube` LUT, convert it to CE.SDK's tiled PNG layout before applying it on Android:
1. Parse the `.cube` file to read the 3D color lookup table data
2. Arrange each blue-channel slice as a tile containing the red-green color plane
3. Export the tile grid as PNG
CE.SDK's built-in LUTs follow the naming pattern `imgly_lut_{name}_{h}_{v}_{cubeSize}.png`, where `h` and `v` are tile counts and `cubeSize` identifies the LUT precision.
### Using CE.SDK's Built-In LUTs
Built-in LUT assets are useful as format-verified references. The filter extension at `ly.img.filter/LUTs` contains tiled PNGs you can inspect to confirm tile counts, cube size, and layout when authoring or converting your own filters.
## Hosting LUT Files
The Android Engine needs a URI it can load. Use an HTTPS URL for remotely hosted LUTs, or package the PNG with your app and pass a URI that resolves to the bundled file.
Make sure the values you pass for `horizontalTileCount` and `verticalTileCount` match the actual PNG grid. Incorrect tile counts usually render as distorted colors.
## Prepare the Scene
The sample starts with a design scene and page. Your app can use an existing scene instead; the important part is that the target block supports effects.
```kotlin highlight-android-prepare-scene
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
```
## Add an Image Block
Create a graphic block with an image fill. The LUT effect is attached to the graphic block, not to the fill block.
```kotlin highlight-android-create-image-block
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 100F)
engine.block.setPositionY(imageBlock, value = 50F)
engine.block.setWidth(imageBlock, value = 300F)
engine.block.setHeight(imageBlock, value = 300F)
engine.block.appendChild(parent = page, child = imageBlock)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(imageBlock, fill = imageFill)
```
## Create the LUT Effect
Create a LUT effect with the type-safe `EffectType.LutFilter` constant.
```kotlin highlight-android-create-effect
val lutFilter = engine.block.createEffect(EffectType.LutFilter)
```
## Configure LUT Properties
Set the LUT image URI and tile counts on the effect block. The sample uses a bundled LUT file with a 5 x 5 tile layout.
```kotlin highlight-android-configure-lut
val bundledAssetBaseUri = "file:///android_asset/imgly-assets"
val lutUri = Uri.parse(
"$bundledAssetBaseUri/ly.img.filter.lut/LUTs/imgly_lut_ad1920_5_5_128.png",
)
engine.block.setUri(
lutFilter,
property = "effect/lut_filter/lutFileURI",
value = lutUri,
)
engine.block.setInt(lutFilter, property = "effect/lut_filter/verticalTileCount", value = 5)
engine.block.setInt(lutFilter, property = "effect/lut_filter/horizontalTileCount", value = 5)
```
## Set Filter Intensity
Control the strength of the color transformation with `effect/lut_filter/intensity`.
```kotlin highlight-android-set-intensity
engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.9F)
```
Values range from `0.0` for no effect to `1.0` for full strength. Intermediate values are useful for subtle color grading.
## Apply the Effect
Check that the target block supports effects, then attach the configured LUT effect with `appendEffect`.
```kotlin highlight-android-apply-effect
val supportsEffects = engine.block.supportsEffects(imageBlock)
require(supportsEffects) { "The image block must support effects." }
engine.block.appendEffect(block = imageBlock, effectBlock = lutFilter)
val appliedEffects = engine.block.getEffects(imageBlock)
```
The effect renders as part of the block's effect stack.
## Toggle the Effect
Disable and enable the LUT effect without removing it from the block.
```kotlin highlight-android-toggle-effect
engine.block.setEffectEnabled(effectBlock = lutFilter, enabled = false)
val disabledState = engine.block.isEffectEnabled(lutFilter)
engine.block.setEffectEnabled(effectBlock = lutFilter, enabled = true)
val effectEnabled = engine.block.isEffectEnabled(lutFilter)
```
This preserves the LUT URI, tile counts, and intensity while temporarily removing the visual transformation.
## Remove the Effect Later
When your app no longer needs the LUT effect, read the block's effect list, remove the effect by index, and destroy the detached effect block.
```kotlin highlight-android-remove-effect
val lutFilterIndex = engine.block.getEffects(imageBlock).indexOf(lutFilter)
require(lutFilterIndex >= 0) { "The LUT filter must be attached before it can be removed." }
engine.block.removeEffect(block = imageBlock, index = lutFilterIndex)
engine.block.destroy(lutFilter)
```
The runnable sample keeps the LUT attached so the final scene still shows the color grade. Use this cleanup path only after you are done with the effect.
## Troubleshooting
### LUT Not Rendering
- Verify the LUT URI is reachable from the Android app
- Confirm the LUT file is a PNG
- Check that the effect is attached to a block with `appendEffect`
- Verify that the target block returns `true` from `supportsEffects`
### Colors Look Wrong
- Match `horizontalTileCount` and `verticalTileCount` to the actual tiled PNG
- Confirm that the LUT was generated for the sRGB color space
- Make sure the image was not resized, cropped, or saved with lossy compression
## API Reference
| Method | Purpose |
| --- | --- |
| `engine.scene.create()` | Create the scene used by the sample. |
| `engine.block.create(blockType=DesignBlockType.Page)` | Create the page block. |
| `engine.block.create(blockType=DesignBlockType.Graphic)` | Create the image-backed graphic block. |
| `engine.block.setWidth(block=_, value=_)` | Set page or block width. |
| `engine.block.setHeight(block=_, value=_)` | Set page or block height. |
| `engine.block.setPositionX(block=_, value=_)` | Set the graphic block's horizontal position. |
| `engine.block.setPositionY(block=_, value=_)` | Set the graphic block's vertical position. |
| `engine.block.appendChild(parent=_, child=_)` | Add a page or graphic block to its parent. |
| `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular shape for the graphic block. |
| `engine.block.setShape(block=_, shape=_)` | Assign the rectangle shape to the graphic block. |
| `engine.block.createFill(fillType=FillType.Image)` | Create an image fill. |
| `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on the image fill. |
| `engine.block.setFill(block=_, fill=_)` | Assign the image fill to the graphic block. |
| `engine.block.createEffect(type=EffectType.LutFilter)` | Create a LUT filter effect block. |
| `engine.block.setUri(block=_, property="effect/lut_filter/lutFileURI", value=_)` | Set the LUT PNG URI. |
| `engine.block.setInt(block=_, property="effect/lut_filter/verticalTileCount", value=_)` | Set the number of vertical LUT tiles. |
| `engine.block.setInt(block=_, property="effect/lut_filter/horizontalTileCount", value=_)` | Set the number of horizontal LUT tiles. |
| `engine.block.setFloat(block=_, property="effect/lut_filter/intensity", value=_)` | Set LUT filter intensity. |
| `engine.block.supportsEffects(block=_)` | Check whether the target block supports effects. |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Add the LUT effect to the target block. |
| `engine.block.getEffects(block=_)` | Read the effects applied to a block. |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enable or disable the LUT effect without removing it. |
| `engine.block.isEffectEnabled(effectBlock=_)` | Check whether the LUT effect is enabled. |
| `engine.block.removeEffect(block=_, index=_)` | Detach an effect from a block by stack index. |
| `engine.block.destroy(block=_)` | Destroy the detached effect block. |
## Next Steps
- [Create Custom Filters](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-filters-c796ba/) - Extend CE.SDK with custom LUT filter asset sources for brand-specific color grading and filter collections.
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Apply, configure, stack, and manage filters and effects with the Engine API.
- [Duotone](https://img.ly/docs/cesdk/android/filters-and-effects/duotone-831fc5/) - Apply duotone effects to images with two-color treatments.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Distortion Effects"
description: "Apply distortion effects to warp, shift, and transform design elements for dynamic artistic visuals in CE.SDK."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/distortion-5b5a66/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Distortion](https://img.ly/docs/cesdk/android/filters-and-effects/distortion-5b5a66/)
---
```kotlin file=@cesdk_android_examples/engine-guides-distortion/Distortion.kt reference-only
import android.net.Uri
import ly.img.engine.ContentFillMode
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
data class Distortion(
val liquidAmount: Float,
val mirrorSide: Int,
val shifterAmount: Float,
val radialPixelRadius: Float,
val tvGlitchDistortion: Float,
val combinedEffectCount: Int,
val disabledState: Boolean,
val removed: Boolean,
val liquidProperties: List,
val previewPng: ByteBuffer,
)
suspend fun distortion(engine: Engine): Distortion {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1180F)
engine.block.setHeight(page, value = 620F)
engine.block.appendChild(parent = scene, child = page)
val imageCells =
listOf(
30F to 30F,
410F to 30F,
790F to 30F,
30F to 320F,
410F to 320F,
790F to 320F,
).map { (x, y) ->
val cell = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(cell, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(cell, value = 360F)
engine.block.setHeight(cell, value = 270F)
engine.block.setPositionX(cell, value = x)
engine.block.setPositionY(cell, value = y)
val fill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = fill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(cell, fill = fill)
engine.block.setContentFillMode(block = cell, mode = ContentFillMode.COVER)
engine.block.appendChild(parent = page, child = cell)
cell
}
val liquidBlock = imageCells[1]
val mirrorBlock = imageCells[2]
val shifterBlock = imageCells[3]
val radialPixelBlock = imageCells[4]
val tvGlitchBlock = imageCells[5]
require(engine.block.supportsEffects(liquidBlock)) {
"Image-backed graphic blocks can render distortion effects."
}
val liquid = engine.block.createEffect(type = EffectType.Liquid)
engine.block.setFloat(liquid, property = "effect/liquid/amount", value = 0.5F)
engine.block.setFloat(liquid, property = "effect/liquid/scale", value = 1.0F)
engine.block.appendEffect(block = liquidBlock, effectBlock = liquid)
val liquidAmount = engine.block.getFloat(liquid, property = "effect/liquid/amount")
check(liquidAmount == 0.5F)
val mirror = engine.block.createEffect(type = EffectType.Mirror)
engine.block.setInt(mirror, property = "effect/mirror/side", value = 0)
engine.block.appendEffect(block = mirrorBlock, effectBlock = mirror)
val mirrorSide = engine.block.getInt(mirror, property = "effect/mirror/side")
check(mirrorSide == 0)
val shifter = engine.block.createEffect(type = EffectType.Shifter)
engine.block.setFloat(shifter, property = "effect/shifter/amount", value = 0.3F)
engine.block.setFloat(shifter, property = "effect/shifter/angle", value = 0.785F)
engine.block.appendEffect(block = shifterBlock, effectBlock = shifter)
val shifterAmount = engine.block.getFloat(shifter, property = "effect/shifter/amount")
check(shifterAmount == 0.3F)
val radialPixel = engine.block.createEffect(type = EffectType.RadialPixel)
engine.block.setFloat(radialPixel, property = "effect/radial_pixel/radius", value = 0.5F)
engine.block.setFloat(radialPixel, property = "effect/radial_pixel/segments", value = 0.5F)
engine.block.appendEffect(block = radialPixelBlock, effectBlock = radialPixel)
val radialPixelRadius = engine.block.getFloat(radialPixel, property = "effect/radial_pixel/radius")
check(radialPixelRadius == 0.5F)
val tvGlitch = engine.block.createEffect(type = EffectType.TvGlitch)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/distortion", value = 0.4F)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/distortion2", value = 0.2F)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/speed", value = 0.5F)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/rollSpeed", value = 0.5F)
engine.block.appendEffect(block = tvGlitchBlock, effectBlock = tvGlitch)
val tvGlitchDistortion = engine.block.getFloat(tvGlitch, property = "effect/tv_glitch/distortion")
check(tvGlitchDistortion == 0.4F)
val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)
val extraShifter = engine.block.createEffect(type = EffectType.Shifter)
engine.block.setFloat(extraShifter, property = "effect/shifter/amount", value = 0.2F)
engine.block.appendEffect(block = liquidBlock, effectBlock = extraShifter)
val combinedEffects = engine.block.getEffects(liquidBlock)
check(combinedEffects == listOf(liquid, extraShifter))
engine.block.setEffectEnabled(effectBlock = extraShifter, enabled = false)
val disabledState = engine.block.isEffectEnabled(extraShifter)
check(!disabledState)
engine.block.removeEffect(block = liquidBlock, index = 1)
engine.block.destroy(extraShifter)
val removed = engine.block.getEffects(liquidBlock) == listOf(liquid)
check(removed)
val liquidProperties = engine.block.findAllProperties(liquid)
check("effect/liquid/amount" in liquidProperties)
return Distortion(
liquidAmount = liquidAmount,
mirrorSide = mirrorSide,
shifterAmount = shifterAmount,
radialPixelRadius = radialPixelRadius,
tvGlitchDistortion = tvGlitchDistortion,
combinedEffectCount = combinedEffects.size,
disabledState = disabledState,
removed = removed,
liquidProperties = liquidProperties,
previewPng = previewPng,
)
}
```
Apply distortion effects to warp, shift, and transform images and videos for dynamic artistic visuals using CE.SDK's effect system.

> **Reading time:** 8 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-distortion)
Distortion effects differ from color filters because they change the geometry and spatial arrangement of pixels rather than only changing color values. CE.SDK provides several distortion effect types: liquid warping, mirror reflections, color channel shifting, radial pixelation, and TV glitch.
This guide covers the built-in Android workflow and how to apply and configure each distortion effect programmatically, combine multiple effects on a single block, and manage the effect stack with the block API. To compare the effects side by side, the example places six copies of the same image in a grid and applies one effect to each cell.
## Using the Built-in Distortion UI
The Android Base Editor exposes distortion effects for supported image and video blocks:
1. Select a supported image or video block on the canvas.
2. Open **Effect** in the inspector bar and browse the available distortion effects.
3. Choose Liquid, Mirror, Shifter, Radial Pixel, or TV Glitch. Select the applied effect again to open its controls, then adjust the values.
4. Preview the result directly on the canvas as you change the controls.
> **Note:** For a complete ready-to-use editor surface, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/).
## Apply Liquid Effect
The liquid effect creates organic, flowing distortions that warp the image as if viewed through water. Verify the block supports effects, create the effect with `EffectType.Liquid`, configure its properties with `setFloat()`, then attach it with `appendEffect()`.
```kotlin highlight-android-liquid-effect
require(engine.block.supportsEffects(liquidBlock)) {
"Image-backed graphic blocks can render distortion effects."
}
val liquid = engine.block.createEffect(type = EffectType.Liquid)
engine.block.setFloat(liquid, property = "effect/liquid/amount", value = 0.5F)
engine.block.setFloat(liquid, property = "effect/liquid/scale", value = 1.0F)
engine.block.appendEffect(block = liquidBlock, effectBlock = liquid)
```
The liquid effect properties:
- `effect/liquid/amount` (`0.0` to `1.0`) - Intensity of the warping.
- `effect/liquid/scale` (`0.0` to `1.0`) - Scale of the liquid pattern.
- `effect/liquid/time` (`0.0` to `1.0`) - Fixed variation and randomness input for the liquid pattern. This property does not animate the effect automatically.
## Apply Mirror Effect
The mirror effect reflects the image along a configurable side, creating symmetrical compositions.
```kotlin highlight-android-mirror-effect
val mirror = engine.block.createEffect(type = EffectType.Mirror)
engine.block.setInt(mirror, property = "effect/mirror/side", value = 0)
engine.block.appendEffect(block = mirrorBlock, effectBlock = mirror)
```
The `effect/mirror/side` property is an integer: `0` (Left), `1` (Right), `2` (Top), or `3` (Bottom). Set it with `setInt()`.
## Apply Shifter Effect
The shifter effect displaces color channels at an angle, creating chromatic aberration commonly seen in glitch art and retro visuals.
```kotlin highlight-android-shifter-effect
val shifter = engine.block.createEffect(type = EffectType.Shifter)
engine.block.setFloat(shifter, property = "effect/shifter/amount", value = 0.3F)
engine.block.setFloat(shifter, property = "effect/shifter/angle", value = 0.785F)
engine.block.appendEffect(block = shifterBlock, effectBlock = shifter)
```
The shifter effect properties:
- `effect/shifter/amount` (`0.0` to `1.0`) - Displacement distance.
- `effect/shifter/angle` - Direction of the shift in radians.
## Apply Radial Pixel Effect
The radial pixel effect pixelates the image in a circular pattern from the center, useful for focus effects or stylized treatments.
```kotlin highlight-android-radial-pixel-effect
val radialPixel = engine.block.createEffect(type = EffectType.RadialPixel)
engine.block.setFloat(radialPixel, property = "effect/radial_pixel/radius", value = 0.5F)
engine.block.setFloat(radialPixel, property = "effect/radial_pixel/segments", value = 0.5F)
engine.block.appendEffect(block = radialPixelBlock, effectBlock = radialPixel)
```
The radial pixel effect properties:
- `effect/radial_pixel/radius` - Radius of each row of pixels, relative to the image.
- `effect/radial_pixel/segments` - Proportional size of a pixel in each row.
## Apply TV Glitch Effect
The TV glitch effect simulates analog television interference with horizontal distortion and rolling effects.
```kotlin highlight-android-tv-glitch-effect
val tvGlitch = engine.block.createEffect(type = EffectType.TvGlitch)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/distortion", value = 0.4F)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/distortion2", value = 0.2F)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/speed", value = 0.5F)
engine.block.setFloat(tvGlitch, property = "effect/tv_glitch/rollSpeed", value = 0.5F)
engine.block.appendEffect(block = tvGlitchBlock, effectBlock = tvGlitch)
```
The TV glitch effect properties:
- `effect/tv_glitch/distortion` - Primary horizontal distortion intensity.
- `effect/tv_glitch/distortion2` - Secondary distortion layer.
- `effect/tv_glitch/speed` - Fixed variance input for the glitch pattern. This property does not animate the effect automatically.
- `effect/tv_glitch/rollSpeed` - Fixed vertical offset for the TV bands.
## Combine Multiple Distortion Effects
Stack multiple distortion effects on a single block. Effects render in the order they appear in the block's effect list, from bottom to top, so the liquid warp is applied first and the shifter then displaces color channels on the already-warped result.
```kotlin highlight-android-combine-effects
val extraShifter = engine.block.createEffect(type = EffectType.Shifter)
engine.block.setFloat(extraShifter, property = "effect/shifter/amount", value = 0.2F)
engine.block.appendEffect(block = liquidBlock, effectBlock = extraShifter)
```
Use `appendEffect()` to add an effect to the end of the list, or `insertEffect()` when an effect must occupy a specific stack index.
## List Applied Effects
Retrieve every effect attached to a block with `getEffects()`. It returns an ordered list of effect block IDs.
```kotlin highlight-android-list-effects
val combinedEffects = engine.block.getEffects(liquidBlock)
```
Use the list when you need to inspect effect order, find an effect before toggling it, or remove an effect by index.
## Enable and Disable Effects
Toggle an effect on and off without removing it using `setEffectEnabled()`, and query its state with `isEffectEnabled()`. A disabled effect stays attached and keeps its parameters, but the engine skips it during rendering.
```kotlin highlight-android-toggle-effect
engine.block.setEffectEnabled(effectBlock = extraShifter, enabled = false)
val disabledState = engine.block.isEffectEnabled(extraShifter)
```
This is useful for before/after comparisons or temporarily reducing rendering cost.
## Remove Effects
Remove an effect from a block by index with `removeEffect()`, then call `destroy()` on the detached effect block when you no longer need it.
```kotlin highlight-android-remove-effect
engine.block.removeEffect(block = liquidBlock, index = 1)
engine.block.destroy(extraShifter)
```
## Discover Effect Properties
Use `findAllProperties()` to discover every property available on an effect. The distortion properties shown here are numeric, so use `setFloat()` and `getFloat()` or `setInt()` and `getInt()` according to each property type.
```kotlin highlight-android-effect-properties
val liquidProperties = engine.block.findAllProperties(liquid)
```
## Troubleshooting
### Effect Not Visible
Confirm the block supports effects with `supportsEffects()` and that the effect
is enabled with `isEffectEnabled()`. Effects apply to graphic blocks with image
or video fills, not to scene blocks.
### Unexpected Results
Verify each parameter against its accepted range:
| Effect | Accepted Values |
| --- | --- |
| Liquid | `amount`, `scale`, and `time`: `0.0`–`1.0` |
| Mirror | `side`: `0`–`3` |
| Shifter | `amount`: `0.0`–`1.0`; `angle`: `0.0`–`6.3` |
| Radial Pixel | `radius`: `0.05`–`1.0`; `segments`: `0.01`–`1.0` |
| TV Glitch | `distortion`: `0.0`–`10.0`; `distortion2`: `0.0`–`5.0`; `speed`: `0.0`–`5.0`; `rollSpeed`: `0.0`–`3.0` |
### Performance
Distortion effects are GPU-intensive. Limit the number of stacked effects on a
single block, especially on mobile devices, and disable effects you are not
actively rendering.
## API Reference
| API | Description |
| --- | --- |
| `engine.block.supportsEffects(block=_)` | Checks whether a design block can render effects |
| `engine.block.createEffect(type=EffectType.Liquid)` | Creates a liquid distortion effect block |
| `engine.block.createEffect(type=EffectType.Mirror)` | Creates a mirror effect block |
| `engine.block.createEffect(type=EffectType.Shifter)` | Creates a shifter effect block |
| `engine.block.createEffect(type=EffectType.RadialPixel)` | Creates a radial pixel effect block |
| `engine.block.createEffect(type=EffectType.TvGlitch)` | Creates a TV glitch effect block |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Adds the effect to the end of a block's effect stack |
| `engine.block.insertEffect(block=_, effectBlock=_, index=_)` | Inserts an effect at a specific stack index |
| `engine.block.getEffects(block=_)` | Returns the ordered effects attached to a block |
| `engine.block.removeEffect(block=_, index=_)` | Removes the effect at the specified stack index |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Returns whether an effect block is enabled |
| `engine.block.findAllProperties(block=_)` | Lists the properties available on an effect block |
| `engine.block.setFloat(block=_, property="effect/liquid/amount", value=_)` | Writes a liquid effect float property |
| `engine.block.getFloat(block=_, property="effect/liquid/amount")` | Reads a liquid effect float property |
| `engine.block.setFloat(block=_, property="effect/liquid/scale", value=_)` | Writes the liquid pattern scale |
| `engine.block.getFloat(block=_, property="effect/liquid/scale")` | Reads the liquid pattern scale |
| `engine.block.setFloat(block=_, property="effect/liquid/time", value=_)` | Writes the liquid pattern variation input |
| `engine.block.getFloat(block=_, property="effect/liquid/time")` | Reads the liquid pattern variation input |
| `engine.block.setInt(block=_, property="effect/mirror/side", value=_)` | Writes the mirror side property |
| `engine.block.getInt(block=_, property="effect/mirror/side")` | Reads the mirror side property |
| `engine.block.setFloat(block=_, property="effect/shifter/amount", value=_)` | Writes a shifter effect float property |
| `engine.block.getFloat(block=_, property="effect/shifter/amount")` | Reads a shifter effect float property |
| `engine.block.setFloat(block=_, property="effect/shifter/angle", value=_)` | Writes the shifter direction in radians |
| `engine.block.getFloat(block=_, property="effect/shifter/angle")` | Reads the shifter direction in radians |
| `engine.block.setFloat(block=_, property="effect/radial_pixel/radius", value=_)` | Writes a radial pixel effect float property |
| `engine.block.getFloat(block=_, property="effect/radial_pixel/radius")` | Reads a radial pixel effect float property |
| `engine.block.setFloat(block=_, property="effect/radial_pixel/segments", value=_)` | Writes the radial pixel segment size |
| `engine.block.getFloat(block=_, property="effect/radial_pixel/segments")` | Reads the radial pixel segment size |
| `engine.block.setFloat(block=_, property="effect/tv_glitch/distortion", value=_)` | Writes a TV glitch effect float property |
| `engine.block.getFloat(block=_, property="effect/tv_glitch/distortion")` | Reads a TV glitch effect float property |
| `engine.block.setFloat(block=_, property="effect/tv_glitch/distortion2", value=_)` | Writes the secondary TV glitch distortion |
| `engine.block.getFloat(block=_, property="effect/tv_glitch/distortion2")` | Reads the secondary TV glitch distortion |
| `engine.block.setFloat(block=_, property="effect/tv_glitch/speed", value=_)` | Writes the TV glitch variance input |
| `engine.block.getFloat(block=_, property="effect/tv_glitch/speed")` | Reads the TV glitch variance input |
| `engine.block.setFloat(block=_, property="effect/tv_glitch/rollSpeed", value=_)` | Writes the TV glitch vertical offset |
| `engine.block.getFloat(block=_, property="effect/tv_glitch/rollSpeed")` | Reads the TV glitch vertical offset |
| `engine.block.destroy(block=_)` | Destroys a detached or unused effect block |
## Available Distortion Effects
| Effect | `EffectType` | Description | Key Properties |
| --- | --- | --- | --- |
| Liquid | `EffectType.Liquid` | Flowing, organic warping | `amount`, `scale`, `time` |
| Mirror | `EffectType.Mirror` | Reflection along a side | `side` (0=Left, 1=Right, 2=Top, 3=Bottom) |
| Shifter | `EffectType.Shifter` | Chromatic aberration | `amount`, `angle` |
| Radial Pixel | `EffectType.RadialPixel` | Circular pixelation | `radius`, `segments` |
| TV Glitch | `EffectType.TvGlitch` | Analog TV interference | `distortion`, `distortion2`, `speed`, `rollSpeed` |
## Next Steps
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Learn the foundational effect APIs.
- [Blur Effects](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) - Apply blur techniques for depth and focus effects.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Duotone"
description: "Apply duotone effects to images with the CE.SDK Engine, mapping tones to two colors for stylized, brand-consistent visuals."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/duotone-831fc5/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Duotone](https://img.ly/docs/cesdk/android/filters-and-effects/duotone-831fc5/)
---
```kotlin file=@cesdk_android_examples/engine-guides-duotone/Duotone.kt reference-only
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.FindAssetsQuery
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
private const val TAG = "DuotoneGuide"
suspend fun duotone(
engine: Engine,
assetBaseUri: Uri,
): DuotoneResult = withContext(Dispatchers.Main) {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 1100F)
engine.block.setHeight(page, value = 380F)
engine.block.appendChild(parent = scene, child = page)
val imageUri = assetBaseUri.buildUpon()
.appendPath("ly.img.image")
.appendPath("images")
.appendPath("sample_1.jpg")
.build()
val presetImage = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(presetImage, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(presetImage, value = 340F)
engine.block.setHeight(presetImage, value = 320F)
engine.block.setPositionX(presetImage, value = 20F)
engine.block.setPositionY(presetImage, value = 30F)
val presetFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = presetFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = presetImage, fill = presetFill)
engine.block.appendChild(parent = page, child = presetImage)
val canApplyEffects = engine.block.supportsEffects(presetImage)
check(canApplyEffects) { "The image block must support effects." }
val filterSourceId = "ly.img.filter"
if (filterSourceId !in engine.asset.findAllSources()) {
val filterContentUri = assetBaseUri.buildUpon()
.appendPath(filterSourceId)
.appendPath("content.json")
.build()
engine.asset.addLocalSourceFromJSON(
contentUri = filterContentUri,
matcher = listOf("ly.img.filter.duotone.*"),
)
}
val presetResult = engine.asset.findAssets(
sourceId = filterSourceId,
query = FindAssetsQuery(
query = null,
page = 0,
groups = listOf("duotone"),
perPage = 10,
),
)
val duotonePresets = presetResult.assets
val presetEffect = engine.block.createEffect(type = EffectType.DuoToneFilter)
val preset = checkNotNull(duotonePresets.firstOrNull()) {
"The filter source did not return any duotone presets."
}
val darkHex = checkNotNull(preset.meta?.get("darkColor")) {
"The selected duotone preset is missing darkColor."
}
val lightHex = checkNotNull(preset.meta?.get("lightColor")) {
"The selected duotone preset is missing lightColor."
}
engine.block.setColor(
block = presetEffect,
property = "effect/duotone_filter/darkColor",
value = Color.fromHex(darkHex),
)
engine.block.setColor(
block = presetEffect,
property = "effect/duotone_filter/lightColor",
value = Color.fromHex(lightHex),
)
engine.block.setFloat(
block = presetEffect,
property = "effect/duotone_filter/intensity",
value = 0.9F,
)
engine.block.appendEffect(block = presetImage, effectBlock = presetEffect)
val customImage = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(customImage, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(customImage, value = 340F)
engine.block.setHeight(customImage, value = 320F)
engine.block.setPositionX(customImage, value = 380F)
engine.block.setPositionY(customImage, value = 30F)
val customFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = customFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = customImage, fill = customFill)
engine.block.appendChild(parent = page, child = customImage)
val customEffect = engine.block.createEffect(type = EffectType.DuoToneFilter)
// Dark color maps to shadows, light color maps to highlights.
engine.block.setColor(
block = customEffect,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.1F, g = 0.15F, b = 0.3F, a = 1.0F),
)
engine.block.setColor(
block = customEffect,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 0.95F, g = 0.9F, b = 0.8F, a = 1.0F),
)
engine.block.setFloat(
block = customEffect,
property = "effect/duotone_filter/intensity",
value = 0.85F,
)
engine.block.appendEffect(block = customImage, effectBlock = customEffect)
val combinedImage = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(combinedImage, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(combinedImage, value = 340F)
engine.block.setHeight(combinedImage, value = 320F)
engine.block.setPositionX(combinedImage, value = 740F)
engine.block.setPositionY(combinedImage, value = 30F)
val combinedFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = combinedFill,
property = "fill/image/imageFileURI",
value = imageUri,
)
engine.block.setFill(block = combinedImage, fill = combinedFill)
engine.block.appendChild(parent = page, child = combinedImage)
// Adjustments run first, then duotone maps the adjusted tones.
val adjustments = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.setFloat(adjustments, property = "effect/adjustments/brightness", value = 0.1F)
engine.block.setFloat(adjustments, property = "effect/adjustments/contrast", value = 0.15F)
engine.block.appendEffect(block = combinedImage, effectBlock = adjustments)
val combinedDuotone = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.setColor(
block = combinedDuotone,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.2F, g = 0.1F, b = 0.3F, a = 1.0F),
)
engine.block.setColor(
block = combinedDuotone,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 1.0F, g = 0.85F, b = 0.7F, a = 1.0F),
)
engine.block.setFloat(
block = combinedDuotone,
property = "effect/duotone_filter/intensity",
value = 0.75F,
)
engine.block.appendEffect(block = combinedImage, effectBlock = combinedDuotone)
// Ensure remote image fills are ready before exporting the preview for smoke validation.
engine.block.forceLoadResources(listOf(presetImage, customImage, combinedImage))
val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)
val presetIntensity = engine.block.getFloat(
block = presetEffect,
property = "effect/duotone_filter/intensity",
)
val combinedEffectsCount = engine.block.getEffects(combinedImage).size
val appliedEffects = engine.block.getEffects(presetImage)
Log.i(TAG, "Image has ${appliedEffects.size} effect(s) applied")
var disabledPresetEffectEnabled = true
var restoredPresetEffectEnabled = false
appliedEffects.firstOrNull()?.let { firstEffect ->
engine.block.setEffectEnabled(effectBlock = firstEffect, enabled = false)
disabledPresetEffectEnabled = engine.block.isEffectEnabled(firstEffect)
Log.i(TAG, "Effect enabled: $disabledPresetEffectEnabled")
engine.block.setEffectEnabled(effectBlock = firstEffect, enabled = true)
restoredPresetEffectEnabled = engine.block.isEffectEnabled(firstEffect)
}
val customEffects = engine.block.getEffects(customImage)
val customEffectsBeforeRemoval = customEffects.size
customEffects.firstOrNull()?.let { effectToRemove ->
// Detach the effect from the block's stack, then destroy it to free resources.
engine.block.removeEffect(block = customImage, index = 0)
engine.block.destroy(effectToRemove)
}
val customEffectsAfterRemoval = engine.block.getEffects(customImage).size
DuotoneResult(
presetCount = duotonePresets.size,
presetEffectsCount = appliedEffects.size,
presetIntensity = presetIntensity,
disabledPresetEffectEnabled = disabledPresetEffectEnabled,
restoredPresetEffectEnabled = restoredPresetEffectEnabled,
customEffectsBeforeRemoval = customEffectsBeforeRemoval,
customEffectsAfterRemoval = customEffectsAfterRemoval,
combinedEffectsCount = combinedEffectsCount,
previewPng = previewPng,
)
}
```
Apply duotone effects to images with the CE.SDK Engine, mapping image tones to two colors for stylized visuals, vintage aesthetics, and brand-consistent treatments.

> **Reading time:** 7 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-duotone)
Duotone is a color effect that maps image brightness to two colors: a dark color for shadows and a light color for highlights. The result is a two-tone image where the original colors are replaced by gradations between your chosen pair. This guide covers applying duotone presets from the asset library, creating custom color combinations, combining duotone with other effects, and managing applied effects.
## Understanding Duotone
Unlike filters that tint or shift colors, duotone remaps the tonal range of an image. The effect reads each pixel's brightness and assigns a color based on where it falls between black and white:
- **Dark tones** (shadows, blacks) adopt the dark color.
- **Light tones** (highlights, whites) adopt the light color.
- **Midtones** blend between the two colors.
This produces a consistent palette regardless of the original colors, which makes duotone effective for:
- **Brand consistency** - Apply your brand colors across diverse imagery.
- **Visual cohesion** - Unify photos from different sources in one design.
- **Vintage aesthetics** - Recreate classic print techniques like cyanotype or sepia.
- **Bold statements** - Create eye-catching visuals for social media or marketing.
An `intensity` value from `-1.0` to `1.0` controls the tonal balance: negative values push the mapping toward the dark color, positive values toward the light color, and `0.0` maps tones evenly between the two. The output is always a two-color image; intensity shifts the balance rather than fading back toward the original colors.
## Using the Built-in Duotone UI
The default Android editor exposes duotone presets through its Filter appearance control when the current selection supports effects. Users can choose a preset and adjust its intensity. The Engine API below applies the same duotone effect programmatically. See [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) for the unified effects system.
> **Note:** The [Photo Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) and [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) provide complete editor surfaces with built-in appearance controls.
## Check Effect Support
Not all blocks accept effects. Verify support with `supportsEffects()` before applying anything. Duotone is most useful on graphic blocks with an image or video fill, since it remaps the tones of visible pixel content.
```kotlin highlight-android-check-support
val canApplyEffects = engine.block.supportsEffects(presetImage)
check(canApplyEffects) { "The image block must support effects." }
```
Applying an effect to a block that does not support effects throws, so gate the call on `supportsEffects()`.
## Applying Duotone Presets
CE.SDK ships a library of built-in duotone presets. Each preset defines a dark/light color pair as hex strings in its metadata.
### Query Built-in Presets
Register the bundled filter source and query it with the Asset API. The sample receives `assetBaseUri` from app configuration so it can point at bundled Android assets, a branch asset server, or the SDK default without embedding a versioned CDN URL. The `matcher` argument limits the registered assets to duotone presets, and the query reads the duotone group from the merged `ly.img.filter` source.
```kotlin highlight-android-query-presets
val filterSourceId = "ly.img.filter"
if (filterSourceId !in engine.asset.findAllSources()) {
val filterContentUri = assetBaseUri.buildUpon()
.appendPath(filterSourceId)
.appendPath("content.json")
.build()
engine.asset.addLocalSourceFromJSON(
contentUri = filterContentUri,
matcher = listOf("ly.img.filter.duotone.*"),
)
}
val presetResult = engine.asset.findAssets(
sourceId = filterSourceId,
query = FindAssetsQuery(
query = null,
page = 0,
groups = listOf("duotone"),
perPage = 10,
),
)
val duotonePresets = presetResult.assets
```
Each preset exposes `darkColor` and `lightColor` in `meta` as hex strings. Convert them to the engine's `Color` type before applying them.
### Create the Effect Block
Create a duotone effect with `createEffect()`, passing the type-safe `EffectType.DuoToneFilter` value. The effect is a standalone block you configure and then attach to an image.
```kotlin highlight-android-create-effect
val presetEffect = engine.block.createEffect(type = EffectType.DuoToneFilter)
```
### Configure Preset Colors
Apply the converted preset colors to the effect with `setColor()`, and set `intensity` with `setFloat()`. `intensity` ranges from `-1.0` to `1.0` (default `0.0`): negative values emphasize the dark color, positive values emphasize the light color, and `0.0` keeps the two balanced.
```kotlin highlight-android-apply-preset
val preset = checkNotNull(duotonePresets.firstOrNull()) {
"The filter source did not return any duotone presets."
}
val darkHex = checkNotNull(preset.meta?.get("darkColor")) {
"The selected duotone preset is missing darkColor."
}
val lightHex = checkNotNull(preset.meta?.get("lightColor")) {
"The selected duotone preset is missing lightColor."
}
engine.block.setColor(
block = presetEffect,
property = "effect/duotone_filter/darkColor",
value = Color.fromHex(darkHex),
)
engine.block.setColor(
block = presetEffect,
property = "effect/duotone_filter/lightColor",
value = Color.fromHex(lightHex),
)
engine.block.setFloat(
block = presetEffect,
property = "effect/duotone_filter/intensity",
value = 0.9F,
)
```
### Append the Effect
Attach the configured effect to the image block. The duotone takes effect as soon as it joins the block's effect stack.
```kotlin highlight-android-append-preset
engine.block.appendEffect(block = presetImage, effectBlock = presetEffect)
```
## Creating Custom Colors
For brand-specific treatments, define your own pair directly with `setColor()`. The dark color maps to shadows and the light color to highlights.
```kotlin highlight-android-custom-colors
val customEffect = engine.block.createEffect(type = EffectType.DuoToneFilter)
// Dark color maps to shadows, light color maps to highlights.
engine.block.setColor(
block = customEffect,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.1F, g = 0.15F, b = 0.3F, a = 1.0F),
)
engine.block.setColor(
block = customEffect,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 0.95F, g = 0.9F, b = 0.8F, a = 1.0F),
)
engine.block.setFloat(
block = customEffect,
property = "effect/duotone_filter/intensity",
value = 0.85F,
)
engine.block.appendEffect(block = customImage, effectBlock = customEffect)
```
### Choosing Effective Color Pairs
The relationship between the dark and light colors determines the final look:
| Color Relationship | Visual Effect | Example Use Case |
| --- | --- | --- |
| **High contrast** | Bold, graphic look | Social media, posters |
| **Low contrast** | Subtle, sophisticated | Editorial, luxury brands |
| **Warm to cool** | Dynamic temperature shift | Lifestyle, fashion |
| **Monochromatic** | Tinted photography style | Vintage aesthetic |
Classic combinations to try:
- **Cyanotype**: deep blue to light cyan
- **Sepia**: dark brown to cream
- **Corporate**: navy to silver
> **Tip:** Start from your brand palette. Use a primary brand color as the light color for highlights, paired with a darker complementary shade for shadows.
## Combining with Other Effects
Effects apply in stack order, so you can pair duotone with adjustments, blur, or other effects. Here, brightness and contrast adjustments run first, then duotone maps the adjusted tones.
```kotlin highlight-android-combine-effects
// Adjustments run first, then duotone maps the adjusted tones.
val adjustments = engine.block.createEffect(type = EffectType.Adjustments)
engine.block.setFloat(adjustments, property = "effect/adjustments/brightness", value = 0.1F)
engine.block.setFloat(adjustments, property = "effect/adjustments/contrast", value = 0.15F)
engine.block.appendEffect(block = combinedImage, effectBlock = adjustments)
val combinedDuotone = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.setColor(
block = combinedDuotone,
property = "effect/duotone_filter/darkColor",
value = Color.fromRGBA(r = 0.2F, g = 0.1F, b = 0.3F, a = 1.0F),
)
engine.block.setColor(
block = combinedDuotone,
property = "effect/duotone_filter/lightColor",
value = Color.fromRGBA(r = 1.0F, g = 0.85F, b = 0.7F, a = 1.0F),
)
engine.block.setFloat(
block = combinedDuotone,
property = "effect/duotone_filter/intensity",
value = 0.75F,
)
engine.block.appendEffect(block = combinedImage, effectBlock = combinedDuotone)
```
Reversing the order - duotone first, then adjustments - produces a different result, because the adjustments would then operate on the duotone output rather than the original image.
## Managing Duotone Effects
Once effects are applied, you can list, toggle, and remove them.
### List Applied Effects
Retrieve the ordered list of effect IDs attached to a block with `getEffects()`.
```kotlin highlight-android-list-effects
val appliedEffects = engine.block.getEffects(presetImage)
Log.i(TAG, "Image has ${appliedEffects.size} effect(s) applied")
```
### Toggle Effect Visibility
Disable an effect without removing it using `setEffectEnabled()`, and query its state with `isEffectEnabled()`. Disabled effects are skipped when the block renders.
```kotlin highlight-android-toggle-effects
appliedEffects.firstOrNull()?.let { firstEffect ->
engine.block.setEffectEnabled(effectBlock = firstEffect, enabled = false)
disabledPresetEffectEnabled = engine.block.isEffectEnabled(firstEffect)
Log.i(TAG, "Effect enabled: $disabledPresetEffectEnabled")
engine.block.setEffectEnabled(effectBlock = firstEffect, enabled = true)
restoredPresetEffectEnabled = engine.block.isEffectEnabled(firstEffect)
}
```
### Remove and Destroy Effects
Detach an effect from a block by its index with `removeEffect()`. Effects are independent blocks that persist after removal, so destroy the detached effect with `destroy()` to free its resources.
```kotlin highlight-android-remove-effect
val customEffects = engine.block.getEffects(customImage)
val customEffectsBeforeRemoval = customEffects.size
customEffects.firstOrNull()?.let { effectToRemove ->
// Detach the effect from the block's stack, then destroy it to free resources.
engine.block.removeEffect(block = customImage, index = 0)
engine.block.destroy(effectToRemove)
}
```
## Troubleshooting
### Duotone Not Visible
Confirm the block supports effects with `supportsEffects()`. Duotone also needs visible image or video content to remap; a solid color fill maps to a single flat tone.
### Colors Look Wrong
`Color.fromRGBA()` Float channels run from `0.0` to `1.0`, not `0` to `255`. Use `Color.fromRGBA(r = 0.5F, g = 0.5F, b = 0.5F, a = 1.0F)` rather than raw byte values.
### Duotone Leans Too Dark or Too Light
The `intensity` property controls the tonal balance, not opacity. If the result leans too dark, raise `intensity` toward `1.0` to emphasize the light color; if it leans too light, lower it toward `-1.0` to emphasize the dark color. `0.0` maps tones evenly.
## API Reference
### Methods
| Method | Description |
| --- | --- |
| `engine.asset.findAllSources()` | Returns registered asset source IDs |
| `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Registers a local asset source from a JSON URI, optionally filtered by asset ID pattern |
| `engine.asset.findAssets(sourceId=_, query=_)` | Queries assets from a registered asset source |
| `engine.block.supportsEffects(block=_)` | Returns whether a block can have effects applied |
| `engine.block.createEffect(type=EffectType.DuoToneFilter)` | Creates a duotone effect block |
| `engine.block.createEffect(type=EffectType.Adjustments)` | Creates an adjustments effect block used before duotone in a stack |
| `Color.fromHex(colorString=_)` | Converts a hex string to a `Color` value |
| `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Creates a `Color` value from normalized Float channels |
| `engine.block.setColor(block=_, property="effect/duotone_filter/darkColor", value=_)` | Sets the duotone shadow color |
| `engine.block.getColor(block=_, property="effect/duotone_filter/darkColor")` | Returns the duotone shadow color |
| `engine.block.setColor(block=_, property="effect/duotone_filter/lightColor", value=_)` | Sets the duotone highlight color |
| `engine.block.getColor(block=_, property="effect/duotone_filter/lightColor")` | Returns the duotone highlight color |
| `engine.block.setFloat(block=_, property="effect/duotone_filter/intensity", value=_)` | Sets the tonal balance from `-1.0` to `1.0` |
| `engine.block.getFloat(block=_, property="effect/duotone_filter/intensity")` | Returns the current duotone tonal balance |
| `engine.block.setFloat(block=_, property="effect/adjustments/brightness", value=_)` | Sets brightness before duotone in an effect stack |
| `engine.block.getFloat(block=_, property="effect/adjustments/brightness")` | Returns the current brightness adjustment |
| `engine.block.setFloat(block=_, property="effect/adjustments/contrast", value=_)` | Sets contrast before duotone in an effect stack |
| `engine.block.getFloat(block=_, property="effect/adjustments/contrast")` | Returns the current contrast adjustment |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Adds an effect to a block's effect stack |
| `engine.block.getEffects(block=_)` | Returns the ordered effect blocks attached to a block |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Returns whether an effect block is enabled |
| `engine.block.removeEffect(block=_, index=_)` | Removes an effect from a block's effect stack by index |
| `engine.block.destroy(block=_)` | Destroys a detached or unused effect block |
### Properties
| Property | Type | Description |
| --- | --- | --- |
| `effect/duotone_filter/darkColor` | Color | Color applied to shadows and dark tones |
| `effect/duotone_filter/lightColor` | Color | Color applied to highlights and light tones |
| `effect/duotone_filter/intensity` | Float | Tonal balance from `-1.0` to `1.0` (default `0.0`); negative emphasizes the dark color, positive emphasizes the light color |
## Next Steps
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Learn about the unified effects system.
- [Blur Effects](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) - Apply blur for depth and focus.
- [Create a Custom LUT Filter](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49/) - Build custom color grading filters.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Filters & Effects Overview"
description: "Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Overview](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/)
---
Enhance images, videos, and graphics with filters and effects such as LUTs,
duotone looks, blur, and chroma keying.
In CreativeEditor SDK (CE.SDK), *filters* and *effects* refer to visual modifications that enhance or transform the appearance of design elements. Filters typically adjust an element’s overall color or tone, while effects add specific visual treatments like blur, sharpness, or distortion.
The CE.SDK editor UI exposes filters and effects for supported blocks, and the CreativeEngine APIs can apply them programmatically. Use this overview to choose between broad color transforms and targeted effects.
[Explore Demos](https://img.ly/showcases/cesdk?tags=android)
[Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/)
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Supported Filters and Effects"
description: "Review Android Engine APIs and property tables for CE.SDK filters and effects."
platform: android
url: "https://img.ly/docs/cesdk/android/filters-and-effects/support-a666dd/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Supported Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects/support-a666dd/)
---
```kotlin file=@cesdk_android_examples/engine-guides-supported-filters-and-effects/SupportedFiltersAndEffects.kt reference-only
import android.net.Uri
import ly.img.engine.Color
import ly.img.engine.DesignBlockType
import ly.img.engine.EffectType
import ly.img.engine.Engine
import ly.img.engine.FillType
import ly.img.engine.MimeType
import ly.img.engine.ShapeType
import java.nio.ByteBuffer
data class SupportedFiltersAndEffects(
val sceneSupportsEffects: Boolean,
val imageBlockSupportsEffects: Boolean,
val appliedEffectCount: Int,
val effectType: String,
val intensity: Float,
val darkColor: Color,
val lightColor: Color,
val previewPng: ByteBuffer,
)
suspend fun supportedFiltersAndEffects(engine: Engine): SupportedFiltersAndEffects {
val scene = engine.scene.create()
val page = engine.block.create(DesignBlockType.Page)
engine.block.setWidth(page, value = 800F)
engine.block.setHeight(page, value = 600F)
engine.block.appendChild(parent = scene, child = page)
val imageBlock = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setPositionX(imageBlock, value = 100F)
engine.block.setPositionY(imageBlock, value = 75F)
engine.block.setWidth(imageBlock, value = 600F)
engine.block.setHeight(imageBlock, value = 450F)
engine.block.appendChild(parent = page, child = imageBlock)
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setUri(
block = imageFill,
property = "fill/image/imageFileURI",
value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"),
)
engine.block.setFill(block = imageBlock, fill = imageFill)
val sceneSupportsEffects = engine.block.supportsEffects(scene)
val imageBlockSupportsEffects = engine.block.supportsEffects(imageBlock)
require(!sceneSupportsEffects) { "Scenes do not support effect stacks." }
require(imageBlockSupportsEffects) { "Image-backed graphic blocks can render effects." }
val duotoneEffect = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.appendEffect(block = imageBlock, effectBlock = duotoneEffect)
val darkColor = Color.fromRGBA(r = 0.02F, g = 0.04F, b = 0.12F, a = 1F)
val lightColor = Color.fromRGBA(r = 0.5F, g = 0.7F, b = 1F, a = 1F)
engine.block.setColor(
block = duotoneEffect,
property = "effect/duotone_filter/darkColor",
value = darkColor,
)
engine.block.setColor(
block = duotoneEffect,
property = "effect/duotone_filter/lightColor",
value = lightColor,
)
engine.block.setFloat(
block = duotoneEffect,
property = "effect/duotone_filter/intensity",
value = 0.8F,
)
val appliedEffects = engine.block.getEffects(imageBlock)
require(appliedEffects == listOf(duotoneEffect)) {
"Expected one duotone effect on the image block."
}
val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)
check(previewPng.hasRemaining()) { "The supported filters and effects preview export is empty." }
return SupportedFiltersAndEffects(
sceneSupportsEffects = sceneSupportsEffects,
imageBlockSupportsEffects = imageBlockSupportsEffects,
appliedEffectCount = appliedEffects.size,
effectType = engine.block.getType(duotoneEffect),
intensity = engine.block.getFloat(duotoneEffect, property = "effect/duotone_filter/intensity"),
darkColor = engine.block.getColor(duotoneEffect, property = "effect/duotone_filter/darkColor"),
lightColor = engine.block.getColor(duotoneEffect, property = "effect/duotone_filter/lightColor"),
previewPng = previewPng.asReadOnlyBuffer(),
)
}
```
Use this reference to find CE.SDK effect types, support checks, and Android
property keys.

> **Reading time:** 6 minutes
>
> **Resources:**
>
> - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-supported-filters-and-effects)
Effects are separate blocks in an ordered effect stack. You create an effect block, append it to a supported design block, then configure that effect through its property keys.
Use this page as a reference for available Android effect types and properties. For a focused walkthrough on adding and managing effects in a scene, see the [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) guide.
## Check Effect Support
Before applying effects to a block, verify whether it supports them using `supportsEffects()`. Not all block types can have effects applied. The snippet below checks a scene and an image-backed graphic block.
```kotlin highlight-android-check-effect-support
val sceneSupportsEffects = engine.block.supportsEffects(scene)
val imageBlockSupportsEffects = engine.block.supportsEffects(imageBlock)
require(!sceneSupportsEffects) { "Scenes do not support effect stacks." }
require(imageBlockSupportsEffects) { "Image-backed graphic blocks can render effects." }
```
Effect support is available for:
- **Graphic blocks** - including image fills, video fills, shapes, and solid colors
- **Page blocks** - effects apply to the page fill or background, not its child blocks
Other block types, such as scenes and groups, return `false`.
## Add an Effect
Create an effect with `createEffect()` using an `EffectType` value, then attach it to a block's effect stack with `appendEffect()`.
```kotlin highlight-android-add-effect
val duotoneEffect = engine.block.createEffect(type = EffectType.DuoToneFilter)
engine.block.appendEffect(block = imageBlock, effectBlock = duotoneEffect)
```
## Configure Effect Properties
Configure effect parameters using typed setter methods. Property paths follow the format `effect/{effect-type}/{property-name}`.
```kotlin highlight-android-configure-effect
val darkColor = Color.fromRGBA(r = 0.02F, g = 0.04F, b = 0.12F, a = 1F)
val lightColor = Color.fromRGBA(r = 0.5F, g = 0.7F, b = 1F, a = 1F)
engine.block.setColor(
block = duotoneEffect,
property = "effect/duotone_filter/darkColor",
value = darkColor,
)
engine.block.setColor(
block = duotoneEffect,
property = "effect/duotone_filter/lightColor",
value = lightColor,
)
engine.block.setFloat(
block = duotoneEffect,
property = "effect/duotone_filter/intensity",
value = 0.8F,
)
```
CE.SDK provides setter methods for different parameter types:
- **`setFloat()`** - For intensity, amount, and decimal values
- **`setInt()`** - For discrete values like pixel sizes
- **`setUri()`** - For file URIs, such as LUT files
- **`setString()`** - For string values, such as LUT filter IDs
- **`setColor()`** - For color values
The property tables list `effect/enabled` for completeness. Use `setEffectEnabled()` and `isEffectEnabled()` to disable, enable, and query an effect block.
## Retrieve Applied Effects
Use `getEffects()` to retrieve all effects applied to a block, in the order they are applied.
```kotlin highlight-android-retrieve-effects
val appliedEffects = engine.block.getEffects(imageBlock)
require(appliedEffects == listOf(duotoneEffect)) {
"Expected one duotone effect on the image block."
}
```
## Effects
The following tables document all available effect types and their configurable properties.
## Adjustments Type
An effect block for basic image adjustments.
This section describes the properties available for the **Adjustments Type** (`//ly.img.ubq/effect/adjustments`) block type.
| Property | Type | Default | Description |
| -------------------------------- | ------- | ------- | ------------------------------------ |
| `effect/adjustments/blacks` | `Float` | `0` | Adjustment of only the blacks. |
| `effect/adjustments/brightness` | `Float` | `0` | Adjustment of the brightness. |
| `effect/adjustments/clarity` | `Float` | `0` | Adjustment of the detail. |
| `effect/adjustments/contrast` | `Float` | `0` | Adjustment of the contrast. |
| `effect/adjustments/exposure` | `Float` | `0` | Adjustment of the exposure. |
| `effect/adjustments/gamma` | `Float` | `0` | Gamma correction, non-linear. |
| `effect/adjustments/highlights` | `Float` | `0` | Adjustment of only the highlights. |
| `effect/adjustments/saturation` | `Float` | `0` | Adjustment of the saturation. |
| `effect/adjustments/shadows` | `Float` | `0` | Adjustment of only the shadows. |
| `effect/adjustments/sharpness` | `Float` | `0` | Adjustment of the sharpness. |
| `effect/adjustments/temperature` | `Float` | `0` | Adjustment of the color temperature. |
| `effect/adjustments/whites` | `Float` | `0` | Adjustment of only the whites. |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
## Cross Cut Type
An effect that distorts the image with horizontal slices.
This section describes the properties available for the **Cross Cut Type** (`//ly.img.ubq/effect/cross_cut`) block type.
| Property | Type | Default | Description |
| ------------------------- | ------- | ------- | ------------------------------ |
| `effect/cross_cut/offset` | `Float` | `0.07` | Horizontal offset per slice. |
| `effect/cross_cut/slices` | `Float` | `5` | Number of horizontal slices. |
| `effect/cross_cut/speedV` | `Float` | `0.5` | Vertical slice position. |
| `effect/cross_cut/time` | `Float` | `1` | Randomness input. |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
## Dot Pattern Type
An effect that displays the image using a dot matrix.
This section describes the properties available for the **Dot Pattern Type** (`//ly.img.ubq/effect/dot_pattern`) block type.
| Property | Type | Default | Description |
| ------------------------- | ------- | ------- | ------------------------------ |
| `effect/dot_pattern/blur` | `Float` | `0.3` | Global blur. |
| `effect/dot_pattern/dots` | `Float` | `30` | Number of dots. |
| `effect/dot_pattern/size` | `Float` | `0.5` | Size of an individual dot. |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
## Duotone Filter Type
An effect that applies a two-tone color mapping.
This section describes the properties available for the **Duotone Filter Type** (`//ly.img.ubq/effect/duotone_filter`) block type.
| Property | Type | Default | Description |
| ---------------------------------- | ------- | --------------------------- | --------------------------------------------------------------------------------- |
| `effect/duotone_filter/darkColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | The darker of the two colors. Negative filter intensities emphasize this color. |
| `effect/duotone_filter/intensity` | `Float` | `0` | The mixing weight of the two colors in the range \[-1, 1]. |
| `effect/duotone_filter/lightColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | The brighter of the two colors. Positive filter intensities emphasize this color. |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
## Extrude Blur Type
An effect that applies a radial extrude blur.
This section describes the properties available for the **Extrude Blur Type** (`//ly.img.ubq/effect/extrude_blur`) block type.
| Property | Type | Default | Description |
| ---------------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/extrude_blur/amount` | `Float` | `0.2` | Blur intensity. |
## Glow Type
An effect that applies an artificial glow.
This section describes the properties available for the **Glow Type** (`//ly.img.ubq/effect/glow`) block type.
| Property | Type | Default | Description |
| ---------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/glow/amount` | `Float` | `0.5` | Glow brightness. |
| `effect/glow/darkness` | `Float` | `0.3` | Glow darkness. |
| `effect/glow/size` | `Float` | `4` | Intensity of the glow. |
## Green Screen Type
An effect that replaces a specific color with transparency.
This section describes the properties available for the **Green Screen Type** (`//ly.img.ubq/effect/green_screen`) block type.
| Property | Type | Default | Description |
| -------------------------------- | ------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/green_screen/colorMatch` | `Float` | `0.4` | Threshold between the source color and the from color. |
| `effect/green_screen/fromColor` | `Color` | `{"r":0,"g":1,"b":0,"a":1}` | The color to be replaced. |
| `effect/green_screen/smoothness` | `Float` | `0.08` | Controls the rate at which the color transition increases when the similarity threshold is exceeded. |
| `effect/green_screen/spill` | `Float` | `0` | Controls the desaturation of the source color to reduce color spill. |
## Half Tone Type
An effect that overlays a halftone pattern.
This section describes the properties available for the **Half Tone Type** (`//ly.img.ubq/effect/half_tone`) block type.
| Property | Type | Default | Description |
| ------------------------ | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/half_tone/angle` | `Float` | `0` | Angle of pattern. |
| `effect/half_tone/scale` | `Float` | `0.5` | Scale of pattern. |
## Linocut Type
An effect that overlays a linocut pattern.
This section describes the properties available for the **Linocut Type** (`//ly.img.ubq/effect/linocut`) block type.
| Property | Type | Default | Description |
| ---------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/linocut/scale` | `Float` | `0.5` | Scale of pattern. |
## Liquid Type
An effect that applies a liquefy distortion.
This section describes the properties available for the **Liquid Type** (`//ly.img.ubq/effect/liquid`) block type.
| Property | Type | Default | Description |
| ---------------------- | ------- | ------- | ------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/liquid/amount` | `Float` | `0.06` | Severity of the applied effect. |
| `effect/liquid/scale` | `Float` | `0.62` | Global scale. |
| `effect/liquid/time` | `Float` | `0.5` | Continuous randomness input. |
## Lut Filter Type
An effect that applies a color lookup table (LUT).
This section describes the properties available for the **Lut Filter Type** (`//ly.img.ubq/effect/lut_filter`) block type.
| Property | Type | Default | Description |
| --------------------------------------- | -------- | ------- | ---------------------------------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/lut_filter/filterId` | `String` | `""` | The unique identifier of the filter. |
| `effect/lut_filter/horizontalTileCount` | `Int` | `5` | The horizontal number of tiles contained in the LUT image. |
| `effect/lut_filter/intensity` | `Float` | `1` | A value in the range of \[0, 1]. Defaults to 1.0. |
| `effect/lut_filter/lutFileURI` | `String` | `""` | The URI to a LUT PNG file. |
| `effect/lut_filter/verticalTileCount` | `Int` | `5` | The vertical number of tiles contained in the LUT image. |
## Mirror Type
An effect that mirrors the image along a central axis.
This section describes the properties available for the **Mirror Type** (`//ly.img.ubq/effect/mirror`) block type.
| Property | Type | Default | Description |
| -------------------- | ------ | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/mirror/side` | `Int` | `1` | Axis to mirror along. |
## Outliner Type
An effect that highlights the outlines in an image.
This section describes the properties available for the **Outliner Type** (`//ly.img.ubq/effect/outliner`) block type.
| Property | Type | Default | Description |
| ----------------------------- | ------- | ------- | -------------------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/outliner/amount` | `Float` | `0.5` | Intensity of edge highlighting. |
| `effect/outliner/passthrough` | `Float` | `0.5` | Visibility of input image in non-edge areas. |
## Pixelize Type
An effect that pixelizes the image.
This section describes the properties available for the **Pixelize Type** (`//ly.img.ubq/effect/pixelize`) block type.
| Property | Type | Default | Description |
| ------------------------------------- | ------ | ------- | ----------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/pixelize/horizontalPixelSize` | `Int` | `20` | The number of pixels on the x-axis. |
| `effect/pixelize/verticalPixelSize` | `Int` | `20` | The number of pixels on the y-axis. |
## Posterize Type
An effect that reduces the number of colors in the image.
This section describes the properties available for the **Posterize Type** (`//ly.img.ubq/effect/posterize`) block type.
| Property | Type | Default | Description |
| ------------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/posterize/levels` | `Float` | `3` | Number of color levels. |
## Radial Pixel Type
An effect that reduces the image into radial pixel rows.
This section describes the properties available for the **Radial Pixel Type** (`//ly.img.ubq/effect/radial_pixel`) block type.
| Property | Type | Default | Description |
| ------------------------------ | ------- | ------- | ------------------------------------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/radial_pixel/radius` | `Float` | `0.1` | Radius of an individual row of pixels, relative to the image. |
| `effect/radial_pixel/segments` | `Float` | `0.01` | Proportional size of a pixel in each row. |
## Recolor Type
An effect that replaces one color with another.
This section describes the properties available for the **Recolor Type** (`//ly.img.ubq/effect/recolor`) block type.
| Property | Type | Default | Description |
| -------------------------------- | ------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/recolor/brightnessMatch` | `Float` | `1` | Affects the weight of brightness when calculating color similarity. |
| `effect/recolor/colorMatch` | `Float` | `0.4` | Threshold between the source color and the from color. |
| `effect/recolor/fromColor` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | The color to be replaced. |
| `effect/recolor/smoothness` | `Float` | `0.08` | Controls the rate at which the color transition increases when the similarity threshold is exceeded. |
| `effect/recolor/toColor` | `Color` | `{"r":0,"g":0,"b":1,"a":1}` | The color to replace with. |
## Sharpie Type
Cartoon-like effect.
This section describes the properties available for the **Sharpie Type** (`//ly.img.ubq/effect/sharpie`) block type.
| Property | Type | Default | Description |
| ---------------- | ------ | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
## Shifter Type
An effect that shifts individual color channels.
This section describes the properties available for the **Shifter Type** (`//ly.img.ubq/effect/shifter`) block type.
| Property | Type | Default | Description |
| ----------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/shifter/amount` | `Float` | `0.05` | Intensity of the shift. |
| `effect/shifter/angle` | `Float` | `0.3` | Shift direction. |
## Tilt Shift Type
An effect that applies a tilt-shift blur.
This section describes the properties available for the **Tilt Shift Type** (`//ly.img.ubq/effect/tilt_shift`) block type.
| Property | Type | Default | Description |
| ---------------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/tilt_shift/amount` | `Float` | `0.016` | Blur intensity. |
| `effect/tilt_shift/position` | `Float` | `0.4` | Horizontal position in image. |
## Tv Glitch Type
An effect that mimics TV banding and distortion.
This section describes the properties available for the **Tv Glitch Type** (`//ly.img.ubq/effect/tv_glitch`) block type.
| Property | Type | Default | Description |
| ------------------------------ | ------- | ------- | ---------------------------------- |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/tv_glitch/distortion` | `Float` | `3` | Rough horizontal distortion. |
| `effect/tv_glitch/distortion2` | `Float` | `1` | Fine horizontal distortion. |
| `effect/tv_glitch/rollSpeed` | `Float` | `1` | Vertical offset. |
| `effect/tv_glitch/speed` | `Float` | `2` | Number of changes per time change. |
## Vignette Type
An effect that adds a vignette (darkened corners).
This section describes the properties available for the **Vignette Type** (`//ly.img.ubq/effect/vignette`) block type.
| Property | Type | Default | Description |
| -------------------------- | ------- | ------- | ------------------------------ |
| `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. |
| `effect/vignette/darkness` | `Float` | `1` | Brightness of vignette. |
| `effect/vignette/offset` | `Float` | `1` | Radial offset. |
## API Reference
| Method | Category | Purpose |
| --------------------------------------------------------------------------------------- | -------- | ---------------------------------------- |
| `engine.block.supportsEffects(block=_)` | Block | Check if a block supports effects |
| `engine.block.createEffect(type=_)` | Block | Create a new effect instance |
| `engine.block.appendEffect(block=_, effectBlock=_)` | Block | Add an effect to a block stack |
| `engine.block.getEffects(block=_)` | Block | Get all effects attached to a block |
| `engine.block.setFloat(block=_, property="effect/duotone_filter/intensity", value=_)` | Block | Set a float effect property |
| `engine.block.getFloat(block=_, property="effect/duotone_filter/intensity")` | Block | Get a float effect property |
| `engine.block.setInt(block=_, property="effect/pixelize/horizontalPixelSize", value=_)` | Block | Set an integer effect property |
| `engine.block.getInt(block=_, property="effect/pixelize/horizontalPixelSize")` | Block | Get an integer effect property |
| `engine.block.setUri(block=_, property="effect/lut_filter/lutFileURI", value=_)` | Block | Set a URI effect property |
| `engine.block.getUri(block=_, property="effect/lut_filter/lutFileURI")` | Block | Get a URI effect property |
| `engine.block.setString(block=_, property="effect/lut_filter/filterId", value=_)` | Block | Set a string effect property |
| `engine.block.getString(block=_, property="effect/lut_filter/filterId")` | Block | Get a string effect property |
| `engine.block.setColor(block=_, property="effect/duotone_filter/darkColor", value=_)` | Block | Set a color effect property |
| `engine.block.getColor(block=_, property="effect/duotone_filter/darkColor")` | Block | Get a color effect property |
| `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Block | Enable or disable an effect block |
| `engine.block.isEffectEnabled(effectBlock=_)` | Block | Check whether an effect block is enabled |
## Next Steps
- [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) - Apply, configure, stack, and
manage filters and effects
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Quickstart"
description: "Get started with CE.SDK by choosing a starter kit"
platform: android
url: "https://img.ly/docs/cesdk/android/get-started/android/quickstart-4h4zji/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) > [Quickstart Android](https://img.ly/docs/cesdk/android/get-started/android/quickstart-4h4zji/)
---
Get started with CE.SDK. Choose a starter kit below to see it in action, then
follow the integration guide.
## Starter Kits
Get started with the one that fits your use case.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Build with AI"
description: "Give your AI coding assistant context about CE.SDK to generate accurate code and get instant answers."
platform: android
url: "https://img.ly/docs/cesdk/android/get-started/build-with-ai-k7m9p2/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Build with AI](https://img.ly/docs/cesdk/android/get-started/build-with-ai-k7m9p2/)
---
Give your AI coding assistant full context about CE.SDK to generate accurate
code and get instant answers. Choose the integration that fits your workflow.
## Choose Your Approach
### Want Everything in One Install?
Install the **CE.SDK Plugin** into Claude Code to get bundled documentation skills, guided code generation, and an autonomous project scaffolder in a single command.
[Install the Plugin](#broken-link-c0d3ag)
### Using an AI-Powered IDE?
Connect your IDE to our **MCP Server** for real-time documentation search. Works with Claude Desktop, Cursor, VS Code Copilot, Windsurf and any MCP-compatible tool.
[Connect MCP Server](https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/)
### Using an AI Coding Assistant?
Install our **Agent Skills** into Claude Code or the Vercel Skills CLI for bundled offline documentation, guided code generation, and autonomous project scaffolding across 10 Web frameworks.
[Install Agent Skills](#broken-link-f7g8h9)
### Need Raw Documentation for AI?
Download our **LLMs.txt** files to manually load CE.SDK documentation into any AI tool. Available as a compact index or full documentation bundle.
[Download LLMs.txt](https://img.ly/docs/cesdk/android/llms-txt-eb9cc5/)
***
Looking to add AI **generation features** — image, video, audio, or text — into the editor for your own users? That's a different journey: see [AI Features](#broken-link-5aa356).
---
## Related Pages
- [MCP Server](https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/) - Connect AI assistants to CE.SDK documentation using the Model Context Protocol (MCP) server.
- [LLMs.txt](https://img.ly/docs/cesdk/android/llms-txt-eb9cc5/) - Our documentation is available in LLMs.txt format
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Clone GitHub Project"
description: "Using CE.SDK with a cloned Android GitHub project"
platform: android
url: "https://img.ly/docs/cesdk/android/get-started/clone-github-project-f9890l/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
---
This guide will walk you through cloning an existing sample project with the CE.SDK editor already set up
## Pre-requisites
- Android Studio installed on your machine
- A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)), use `null` or an empty string to run in evaluation mode with watermark.
## Clone the GitHub Repository
Launch Android Studio and select `File -> New -> Project from version control`
Next, add the following URL:
```
https://github.com/imgly/cesdk-android-examples.git
```

Click "Clone" and wait for the project to be downloaded and set up.
## Run the Project
In the `local.properties` file, add your CE.SDK license key (or leave it empty to run in evaluation mode with watermark)
```
license=MY_LICENSE_GOES_HERE
```
You may have to create this file, if Android Studio did not generate it for you.
Finally, run the app on your device or android emulator.
## Common Errors
Here are some common errors you may encounter through this guide, and how to solve them.
#### Invalid License
|  |  |
| ---------------------------------------------------- | ---------------------------------------------------- |
| | |
**Solution** -> Check whether you have supplied a valid license
#### No Internet

**Solution** -> Check your internet connection
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "Existing Project Setup"
description: "Learn how to integrate the CreativeEditor SDK into your existing Android Jetpack Compose project"
platform: android
url: "https://img.ly/docs/cesdk/android/get-started/existing-project-g0901m/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
---
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/settings.gradle.kts reference-only
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
name = "IMG.LY Artifactory"
url = uri("https://maven.img.ly/maven")
mavenContent {
includeGroup("ly.img")
}
}
}
}
rootProject.name = "My App"
include(":app")
```
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/build.gradle.kts reference-only
plugins {
id("com.android.application")
id("kotlin-android")
}
android {
namespace = "com.example.cesdkapp"
compileSdk = 36
defaultConfig {
applicationId = "com.example.cesdkapp"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
ndk {
abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
}
dependencies {
// This dependency makes main compose and coroutine APIs available in your project
implementation("ly.img:editor:1.79.0")
// Other dependencies here
}
```
This guide shows you how to integrate the CreativeEditor SDK into your existing Android Jetpack Compose project. Follow these steps to learn how to:
- **add** the necessary dependencies.
- **configure** the editor.
- **test** the integration.
## Who Is This Guide For?
This guide is for developers who:
- Have an existing Android Jetpack Compose project
- Want to add CreativeEditor SDK features to their app
- Need to understand common integration patterns
- Want to test available editing capabilities and workflows
## What You'll Achieve
By following this guide, you'll perform the following tasks:
- **Project Integration**: Add CreativeEditor SDK to your existing Android project
- **Editor Implementation**: Implement the editor with proper lifecycle management
- **Testing**: Verify the integration works correctly
- **Customization**: Learn how to customize the editor for your use case
[View Android Examples](https://github.com/imgly/cesdk-android-examples)
[Android Documentation](https://img.ly/docs/cesdk/android)
## Prerequisites
### Development Environment
- Android Studio (latest version)
- Android SDK (API level 24 or higher)
- Kotlin 1.9.10 or higher
- Gradle 8.4 or later
- Jetpack Compose BOM 2023.05.01 or higher
### Platform Requirements
- **Android**: API level 24 (Android 7.0) or higher
- **Supported ABIs**: arm64-v8a, armeabi-v7a, x86\_64, x86
### License
- A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)), use `null` or an empty string to run in evaluation mode with watermark.
## Verify Your Setup
Before starting, verify your Android development environment:
```bash
gradle --version
```
This command checks your Gradle installation and reports any issues to resolve before proceeding.
> **Note:** You can customize the CreativeEditor SDK for Android exclusively through
> native code (Kotlin), as described in the
> [configuration overview section](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/).
## Step 1: Add the CreativeEditor SDK Dependency
Add the CreativeEditor SDK to your project by updating the build configuration:
### 1.1 Add IMG.LY Repository
Update your `settings.gradle.kts` to include the IMG.LY repository:
```kotlin highlight-maven-dependency
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
name = "IMG.LY Artifactory"
url = uri("https://maven.img.ly/maven")
mavenContent {
includeGroup("ly.img")
}
}
}
}
```
### 1.2 Add Editor Dependency
Update your `app/build.gradle.kts` to include the editor dependency:
```kotlin highlight-dependency
dependencies {
// This dependency makes main compose and coroutine APIs available in your project
implementation("ly.img:editor:1.79.0")
// Other dependencies here
}
```
This adds the latest version of the CreativeEditor SDK editor to your `build.gradle.kts` file.
### 1.3 Configure Android Settings
Update your `app/build.gradle.kts` to ensure proper configuration:
```kotlin highlight-build-android
android {
namespace = "com.example.cesdkapp"
compileSdk = 36
defaultConfig {
applicationId = "com.example.cesdkapp"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
ndk {
abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
}
```
### 1.4 Sync Project
After adding the dependency, sync your project to download the CreativeEditor SDK:
In Android Studio, click **Sync Project with Gradle Files** to download and configure all dependencies.
This downloads and installs the CreativeEditor SDK and its dependencies automatically through Gradle.
## Step 2: Implement the Editor Integration
Now let's implement the CreativeEditor SDK editor in your Android Jetpack Compose application:
### 2.1 Create EditorComposable
Create a new file `EditorComposable.kt` in your project:
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/EditorComposable.kt
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import ly.img.editor.Editor
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.engine.DesignBlockType
// Add this composable to your Activity, Fragment, NavHost etc.
@Composable
fun EditorComposable(onClose: (Throwable?) -> Unit) {
val context = LocalContext.current
Editor(
// Get your license from https://img.ly/forms/free-trial
// Keep this null for evaluation mode with watermark.
// Replace it with your license key for production use.
license = null,
configuration = {
EditorConfiguration.remember {
onCreate = {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
}
onError = {
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
}
}
},
onClose = onClose,
)
}
```
In this example, we create a scene with a single square page.
### 2.2 Include EditorComposable in your navigation system
Update your navigation system to use the editor composable. The example below shows how to integrate it in `NavHost`:
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "main",
) {
composable(route = "main") {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Button(
onClick = { navController.navigate("editor") },
) {
Text("Launch Creative Editor SDK")
}
}
}
composable(route = "editor") {
EditorComposable {
navController.popBackStack()
}
}
}
}
}
}
```
### 2.3 Add the Internet Permission
Add the network permission that CE.SDK needs when it loads its default assets from the CDN:
```xml
```
## Step 3: Test Your Editor Integration
Now let's test your CreativeEditor SDK editor integration:
### 3.1 Build and Run
Build your Android project using Gradle:
### 3.2 Test on Android Device
Connect your Android device and run:
```bash
# Build and install the app
./gradlew installDebug
# Or run from Android Studio
# Click the "Run" button (green play icon)
```
### 3.3 Verify Features
After launching your app, verify these features work correctly:
- **App Launch**: The main activity opens without errors
- **Main Screen**: A clean Android Jetpack Compose interface
- **Button Interaction**: The "Launch Creative Editor SDK" button responds to taps
- **Editor Launch**: The CreativeEditor SDK opens without errors
- **Canvas Interaction**: The blank page can be panned and zoomed.
## Step 4: Customize for Your Use Case
> **Note:** Choose your integration approach based on your development needs.* **[Starter Kits](/android/starter-kits-d0ed07/)** - Use one of our pre-built editors (photo, video, design) and configure them to match your needs.
> * **[Custom Editor](/android/guides/starter-kits/custom-editor-3a5c9b/)** - Build your editor from the ground up as flexibly as possible using CE.SDK's powerful APIs.
## Step 5: Troubleshooting
### Common Issues and Solutions
#### Gradle Sync Issues
- **Problem**: Repository not found
- **Solution**: Verify the IMG.LY repository URL in `settings.gradle.kts`
#### Compilation Errors
- **Problem**: Missing dependencies
- **Solution**: Ensure you've installed all Jetpack Compose dependencies.
#### Runtime Errors
- **Problem**: Invalid license
- **Solution**: Verify your license key is correct and valid (or pass `null` for evaluation mode with watermark)
#### Performance Issues
- **Problem**: Slow editor loading
- **Solution**: Check device specifications and memory usage
#### Integration Issues
- **Problem**: Editor not displaying
- **Solution**: Verify the composable is properly called in your activity
## Step 6: Next Steps
### Advanced Features
- Implement custom asset sources
- Add custom filters and effects
- Integrate with your backend services
- Implement user authentication
### Other Integrations
- Explore camera integration
- Add video editing capabilities
- Implement batch processing
- Add cloud storage integration
### Production Considerations
- Optimize for performance
- Implement proper error handling
- Add analytics and monitoring
- Test on different device configurations
## Additional Resources
- [CreativeEditor SDK Documentation](https://img.ly/docs/cesdk/android)
- [Android Examples Repository](https://github.com/imgly/cesdk-android-examples)
- [Jetpack Compose Documentation](https://developer.android.com/jetpack/compose)
- [Android Development Guide](https://developer.android.com/guide)
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "MCP Server"
description: "Connect AI assistants to CE.SDK documentation using the Model Context Protocol (MCP) server."
platform: android
url: "https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
**Navigation:** [Build with AI](https://img.ly/docs/cesdk/android/get-started/build-with-ai-k7m9p2/) > [MCP Server](https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/)
---
The CE.SDK MCP server provides a standardized interface that allows any compatible AI assistant to search and access our documentation. This enables AI tools like Claude, Cursor, and VS Code Copilot to provide more accurate, context-aware help when working with CE.SDK.
## What is MCP?
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to securely connect to external data sources. By connecting your AI tools to our MCP server, you get:
- **Accurate answers**: AI assistants can search and retrieve the latest CE.SDK documentation
- **Context-aware help**: Get platform-specific guidance for your development environment
- **Up-to-date information**: Always access current documentation without relying on training data
## Available Tools
The MCP server exposes two tools:
| Tool | Description |
| -------- | --------------------------------------------- |
| `search` | Search documentation by query string |
| `fetch` | Retrieve the full content of a document by ID |
## Server Endpoint
| URL | Transport |
| ------------------------ | --------------- |
| `https://mcp.img.ly/mcp` | Streamable HTTP |
No authentication is required.
## Setup Instructions
### Claude Code
Add the MCP server with a single command:
```bash
claude mcp add --transport http imgly_docs https://mcp.img.ly/mcp
```
### Claude Desktop
1. Open Claude Desktop and go to **Settings** (click your profile icon)
2. Navigate to **Connectors** in the sidebar
3. Click **Add custom connector**
4. Enter the URL: `https://mcp.img.ly/mcp`
5. Click **Add** to connect
### Cursor
Add the following to your Cursor MCP configuration. You can use either:
- **Project-specific**: `.cursor/mcp.json` in your project root
- **Global**: `~/.cursor/mcp.json`
```json
{
"mcpServers": {
"imgly_docs": {
"url": "https://mcp.img.ly/mcp"
}
}
}
```
### VS Code
Add to your workspace configuration at `.vscode/mcp.json`:
```json
{
"servers": {
"imgly_docs": {
"type": "http",
"url": "https://mcp.img.ly/mcp"
}
}
}
```
### Windsurf
Add the following to your Windsurf MCP configuration at `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"imgly_docs": {
"serverUrl": "https://mcp.img.ly/mcp"
}
}
}
```
### Other Clients
For other MCP-compatible clients, use the endpoint `https://mcp.img.ly/mcp` with HTTP transport. Refer to your client's documentation for the specific configuration format.
## Usage
Once configured, your AI assistant will automatically have access to CE.SDK documentation. You can ask questions like:
- "How do I add a text block in CE.SDK?"
- "Show me how to export a design as PNG"
- "What are the available blend modes?"
The AI will search our documentation and provide answers based on the latest CE.SDK guides and API references.
---
## More Resources
- **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation
- **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs)
- **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples
- **[Support](mailto:support@img.ly)** - Contact IMG.LY support
---
---
title: "New Project Setup"
description: "Learn how to integrate the CreativeEditor SDK into a new Android Activity-based project"
platform: android
url: "https://img.ly/docs/cesdk/android/get-started/new-activity-based-ui-project-d7678j/"
---
> This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt).
---
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/settings.gradle.kts reference-only
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
name = "IMG.LY Artifactory"
url = uri("https://maven.img.ly/maven")
mavenContent {
includeGroup("ly.img")
}
}
}
}
rootProject.name = "My App"
include(":app")
```
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/build.gradle.kts reference-only
plugins {
id("com.android.application")
id("kotlin-android")
}
android {
namespace = "com.example.cesdkapp"
compileSdk = 36
defaultConfig {
applicationId = "com.example.cesdkapp"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
ndk {
abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
}
dependencies {
// This dependency makes main compose and coroutine APIs available in your project
implementation("ly.img:editor:1.79.0")
// Other dependencies here
}
```
This guide shows you how to integrate the CreativeEditor SDK into a **new Android Activity-based project**. Learn how to:
- **create** a new project.
- **add** the necessary dependencies.
- **configure** the editor.
- **test** the integration.
## Who Is This Guide For?
This guide is for developers who:
- Have experience with Android development and Kotlin
- Want to create a new Android Activity-based project with integrated creative editing capabilities
- Need to implement user-friendly editing interfaces
- Want to add professional-grade image editing, design creation, and video editing to their Android apps
- Prefer using traditional Android Views with Activity-based architecture
## What You'll Achieve
By following this guide, you:
- Create a new Android Activity-based project with CreativeEditor SDK integration
- Configure platform-specific requirements for Android
- Implement a functional editor that you can launch from your app
- Test and verify the integration works correctly
[Explore Android Demos](https://img.ly/showcases/cesdk/?tags=android)
[View on GitHub](https://github.com/imgly/cesdk-android-examples)
## Prerequisites
Before you begin, ensure you have the following requirements:
### Development Environment
- **Android Studio**: Latest version (Hedgehog or later)
- **Kotlin**: 1.9.10 or later
- **Gradle**: 8.4 or later
- **Git CLI** for version control
### Platform Requirements
- **Android**: 7.0+ (API level 24+)
- **Minimum SDK**: 24
- **Target SDK**: Latest stable version
### License
- A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)), use `null` or an empty string to run in evaluation mode with watermark.
### Verify Your Setup
Run the following command to verify your Android development environment:
```bash
gradle --version
```
This command checks your Gradle installation and reports any issues to resolve before proceeding.
> **Note:** You can customize the CreativeEditor SDK for Android through **native code
> (Kotlin) only**, as described in the
> [configuration overview section](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/).
## Step 1: Create a New Android Activity-Based Project
First, verify your Android Studio installation and create a new project:
1. **Open Android Studio** and select "New Project"
2. **Choose "Empty Views Activity"** template
3. **Configure your project**:
- Name: `cesdk_android_activity_app`
- Package name: `com.example.cesdkactivityapp`
- Language: **Kotlin**
- Minimum SDK: **API 24 (Android 7.0)**
- Build configuration language: **Kotlin DSL**
### Project Structure
Your new project should have this structure:
```
cesdk_android_activity_app/
├── app/ # Main application module
│ ├── src/main/
│ │ ├── java/ # Kotlin source files
│ │ ├── res/ # Resources
│ │ └── AndroidManifest.xml
│ ├── build.gradle.kts # App-level build configuration
│ └── proguard-rules.pro # ProGuard rules
├── gradle/ # Gradle wrapper
├── build.gradle.kts # Project-level build configuration
├── settings.gradle.kts # Project settings
└── gradle.properties # Gradle properties
```
## Step 2: Add the CreativeEditor SDK Dependency
Add the CreativeEditor SDK to your project by updating the build configuration:
### 2.1 Add IMG.LY Repository
Update your `settings.gradle.kts` to include the IMG.LY repository:
```kotlin highlight-maven-dependency
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
name = "IMG.LY Artifactory"
url = uri("https://maven.img.ly/maven")
mavenContent {
includeGroup("ly.img")
}
}
}
}
```
### 2.2 Add Editor Dependency
Update your `app/build.gradle.kts` to include the editor dependency:
```kotlin highlight-dependency
dependencies {
// This dependency makes main compose and coroutine APIs available in your project
implementation("ly.img:editor:1.79.0")
// Other dependencies here
}
```
This adds the latest version of the CreativeEditor SDK editor to your `build.gradle.kts` file.
### 2.3 Configure Android Settings
Update your `app/build.gradle.kts` to ensure proper configuration:
```kotlin highlight-build-android
android {
namespace = "com.example.cesdkapp"
compileSdk = 36
defaultConfig {
applicationId = "com.example.cesdkapp"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
ndk {
abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
}
```
### 2.4 Sync Project
After adding the dependency, sync your project to download the CreativeEditor SDK:
In Android Studio, click **Sync Project with Gradle Files** to download and configure all dependencies.
This downloads and installs the CreativeEditor SDK and its dependencies automatically through Gradle.
## Step 3: Implement the Editor Integration
Now let's implement the CreativeEditor SDK editor in your Android application:
### 3.1 Create EditorComposable
Create a new file `EditorComposable.kt` in your app module's main source set (typically `app/src/main/java/com/yourpackage/`):
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/EditorComposable.kt
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import ly.img.editor.Editor
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.engine.DesignBlockType
// Add this composable to your Activity, Fragment, NavHost etc.
@Composable
fun EditorComposable(onClose: (Throwable?) -> Unit) {
val context = LocalContext.current
Editor(
// Get your license from https://img.ly/forms/free-trial
// Keep this null for evaluation mode with watermark.
// Replace it with your license key for production use.
license = null,
configuration = {
EditorConfiguration.remember {
onCreate = {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
}
onError = {
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
}
}
},
onClose = onClose,
)
}
```
In this example, we create a scene with a single square page.
### 3.2 Create EditorActivity
Create a new Kotlin file called `EditorActivity.kt` in your app module's main source set (typically `app/src/main/java/com/yourpackage/`):
```kotlin file=@cesdk_android_examples/editor-guides-quickstart/EditorActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
// Launch this activity via intent
class EditorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
EditorComposable { throwable ->
// You can set result here
finish()
}
}
}
}
```
### 3.3 Update MainActivity
Modify your existing `MainActivity.kt` file (located in `app/src/main/java/com/yourpackage/`) to launch the editor:
```kotlin title="MainActivity.kt"
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById