--- title: "Animation" description: "Add motion to designs with entrance, exit, and loop animation presets, timing controls, and programmatic APIs." 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. ![Android create animations result preview showing the guide's animated graphic block and text blocks](./assets/android.hero.webp) > **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. Use this page as the complete end-to-end workflow. The focused guides linked below cover base, text, editing, and animation-type details separately. ## 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 Set the duration on the animation block, not on the target block. Attaching an entrance or exit animation first lets CE.SDK clamp its duration against the target block's visible duration and the opposing animation. ### 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/) — Create and attach entrance, exit, and loop presets to non-text blocks. - [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) — Configure text-specific presets, writing styles, and overlap. - [Edit Animations](https://img.ly/docs/cesdk/android/animation/edit-32c12a/) — Inspect, update, replace, or remove an attached animation. - [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) — Compare available presets and their properties. - [Animation Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) — Review the animation model and core concepts. --- ## Related Pages - [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) - Apply entrance, exit, and loop animation presets with duration, easing, and type-specific properties. - [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 entrance, exit, and loop animation presets with duration, easing, and type-specific properties." 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. ## Troubleshooting ### Animation Not Playing Verify that the target block supports animations, is visible during page playback, and has enough visible time for the animation duration. ### Duration Issues Set the duration on the animation block, not on the target design block. Attaching an entrance or exit animation first lets CE.SDK clamp its duration against the target block's visible duration and the opposing animation. ## 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/) — Compare available object and text animation presets and their properties. - [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) — Build entrance, exit, loop, and text animations in one end-to-end workflow. - [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. - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) — Arrange video tracks, clips, and audio on a time-based canvas. --- ## 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`) - [ly.img:plugin-background-removal](`$\{props.platform.slug}/api-reference/plugin-background-removal/ly.img[58]plugin-background-removal/ly.img.editor.plugin.backgroundRemoval`) - [ly.img:plugin-background-removal-google](`$\{props.platform.slug}/api-reference/plugin-background-removal-google/ly.img[58]plugin-background-removal-google/ly.img.editor.plugin.backgroundRemoval`) - [ly.img:plugin-background-removal-imgly](`$\{props.platform.slug}/api-reference/plugin-background-removal-imgly/ly.img[58]plugin-background-removal-imgly/ly.img.editor.plugin.backgroundRemoval`) - [ly.img:plugin-ai-core](`$\{props.platform.slug}/api-reference/plugin-ai-core/ly.img[58]plugin-ai-core/ly.img.editor.plugin.ai.core.gateway`) - [ly.img:plugin-ai-image-generation](`$\{props.platform.slug}/api-reference/plugin-ai-image-generation/ly.img[58]plugin-ai-image-generation/ly.img.editor.plugin.ai.imageGeneration`) --- ## 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. - [Automate Design Generation](https://img.ly/docs/cesdk/android/automation/design-generation-98a99e/) - Generate personalized designs programmatically from reusable templates with CE.SDK's Android Engine API. - [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: "Automate Design Generation" description: "Generate personalized designs programmatically from reusable templates with CE.SDK's Android Engine API." platform: android url: "https://img.ly/docs/cesdk/android/automation/design-generation-98a99e/" --- > 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/) > [Design Generation](https://img.ly/docs/cesdk/android/automation/design-generation-98a99e/) --- ```kotlin file=@cesdk_android_examples/engine-guides-design-generation/DesignGeneration.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.DesignUnit import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.FontUnit import ly.img.engine.MimeType import ly.img.engine.ShapeType import java.io.File import java.io.FileOutputStream import java.nio.ByteBuffer data class DesignGenerationResult( val referencedVariableKeys: Set, val populatedVariables: Map, val namedImageBlockCount: Int, val imageFillType: String, val storedImageUri: Uri, val exportedPng: ByteBuffer, val outputFile: File, ) data class DesignGenerationFixture( val templateFile: File, val replacementImageUri: Uri, val outputFile: File, ) suspend fun designGeneration( engine: Engine, templateUri: Uri, replacementImageUri: Uri, outputFile: File, ): DesignGenerationResult = withContext(engine.dispatcher) { engine.scene.load( sceneUri = templateUri, overrideEditorConfig = true, waitForResources = true, ) val page = checkNotNull(engine.scene.getPages().singleOrNull()) { "The template must contain exactly one page." } data class DesignRecord( val firstName: String, val lastName: String, val address: String, val city: String, val imageUri: Uri, ) val record = DesignRecord( firstName = "John", lastName = "Doe", address = "123 Main St.", city = "Anytown", imageUri = replacementImageUri, ) val requiredVariableKeys = setOf("first_name", "last_name", "address", "city") val textBlocks = engine.block.findByType(DesignBlockType.Text) check(textBlocks.any { textBlock -> engine.block.referencesAnyVariables(block = textBlock) }) { "Template text blocks must reference at least one variable." } val variableTokenPattern = Regex("""\{\{\s*([^{}]+?)\s*\}\}""") val referencedVariableKeys = textBlocks .flatMap { textBlock -> variableTokenPattern.findAll(engine.block.getString(block = textBlock, property = "text/text")) .map { match -> match.groupValues[1].trim() } .toList() } .toSet() val missingVariableKeys = requiredVariableKeys - referencedVariableKeys check(missingVariableKeys.isEmpty()) { "Template text is missing variable references: ${missingVariableKeys.sorted().joinToString()}" } val imageBlockName = "profile-photo" val imageBlocks = engine.block.findByName(name = imageBlockName) val imageBlock = checkNotNull(imageBlocks.singleOrNull()) { "Template must contain exactly one block named '$imageBlockName'." } val imageFill = engine.block.getFill(block = imageBlock) val imageFillType = engine.block.getType(block = imageFill) check(imageFillType == FillType.Image.key) { "Block '$imageBlockName' must use an image fill." } engine.variable.set(key = "first_name", value = record.firstName) engine.variable.set(key = "last_name", value = record.lastName) engine.variable.set(key = "address", value = record.address) engine.variable.set(key = "city", value = record.city) val imageUriProperty = "fill/image/imageFileURI" engine.block.setUri( block = imageFill, property = imageUriProperty, value = record.imageUri, ) engine.block.resetCrop(block = imageBlock) val storedImageUri = engine.block.getUri(block = imageFill, property = imageUriProperty) engine.block.forceLoadResources(blocks = listOf(page)) val exportedPng = engine.block.export(block = page, mimeType = MimeType.PNG).apply { rewind() } withContext(Dispatchers.IO) { FileOutputStream(outputFile).channel.use { channel -> val readablePng = exportedPng.asReadOnlyBuffer() while (readablePng.hasRemaining()) { channel.write(readablePng) } } } DesignGenerationResult( referencedVariableKeys = referencedVariableKeys, populatedVariables = mapOf( "first_name" to engine.variable.get(key = "first_name"), "last_name" to engine.variable.get(key = "last_name"), "address" to engine.variable.get(key = "address"), "city" to engine.variable.get(key = "city"), ), namedImageBlockCount = imageBlocks.size, imageFillType = imageFillType, storedImageUri = storedImageUri, exportedPng = exportedPng.asReadOnlyBuffer(), outputFile = outputFile, ) } // Deterministic serialized template used by the Android smoke test. suspend fun createDesignGenerationFixture( engine: Engine, directory: File, ): DesignGenerationFixture = withContext(engine.dispatcher) { val previousVariables = engine.variable.findAll().associateWith { key -> engine.variable.get(key = key) } val templateString = try { previousVariables.keys.forEach { key -> engine.variable.remove(key = key) } val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL) val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(block = page, value = 900F) engine.block.setHeight(block = page, value = 600F) engine.block.appendChild(parent = scene, child = page) engine.block.setFillSolidColor( block = page, color = Color.fromHex("#F6F7F9"), ) val accent = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = accent, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(block = accent, value = 0F) engine.block.setPositionY(block = accent, value = 0F) engine.block.setWidth(block = accent, value = 52F) engine.block.setHeight(block = accent, value = 600F) engine.block.setFill(block = accent, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = accent, color = Color.fromHex("#146C5A"), ) engine.block.appendChild(parent = page, child = accent) val name = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = name, text = "{{first_name}} {{last_name}}") engine.block.setTextFontSize(block = name, fontSize = 48F) engine.block.setTextColor(block = name, color = Color.fromHex("#17212B")) engine.block.setPositionX(block = name, value = 96F) engine.block.setPositionY(block = name, value = 104F) engine.block.setWidth(block = name, value = 380F) engine.block.setHeight(block = name, value = 92F) engine.block.appendChild(parent = page, child = name) val address = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = address, text = "{{address}}\n{{city}}") engine.block.setTextFontSize(block = address, fontSize = 28F) engine.block.setTextColor(block = address, color = Color.fromHex("#40505F")) engine.block.setPositionX(block = address, value = 96F) engine.block.setPositionY(block = address, value = 224F) engine.block.setWidth(block = address, value = 350F) engine.block.setHeight(block = address, value = 140F) engine.block.appendChild(parent = page, child = address) val imageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setName(block = imageBlock, name = "profile-photo") engine.block.setShape(block = imageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(block = imageBlock, value = 520F) engine.block.setPositionY(block = imageBlock, value = 72F) engine.block.setWidth(block = imageBlock, value = 308F) engine.block.setHeight(block = imageBlock, value = 456F) engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER) 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) engine.variable.set(key = "first_name", value = "First") engine.variable.set(key = "last_name", value = "Last") engine.variable.set(key = "address", value = "Address") engine.variable.set(key = "city", value = "City") engine.block.forceLoadResources(blocks = listOf(page)) engine.scene.saveToString(scene = scene) } finally { engine.variable.findAll().forEach { key -> engine.variable.remove(key = key) } previousVariables.forEach { (key, value) -> engine.variable.set(key = key, value = value) } } withContext(Dispatchers.IO) { val templateFile = File.createTempFile("design-generation-template-", ".imgly", directory).apply { writeText(templateString) } val outputFile = File.createTempFile("design-generation-output-", ".png", directory) DesignGenerationFixture( templateFile = templateFile, replacementImageUri = Uri.parse( "file:///android_asset/imgly-assets/ly.img.image/images/sample_2.jpg", ), outputFile = outputFile, ) } } ``` Populate a reusable template from application data and export the finished design with CE.SDK's Android Engine API. ![A personalized design showing John Doe and 123 Main St., Anytown beside a kitten photo.](./assets/android.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-design-generation) Each generation job starts from a pristine template and resolves its current block IDs before applying one record. This keeps template state isolated without turning the mobile workflow into a batch-processing service. ## Load a Template Load a bundled or remote `.scene` through Android's `Uri` type. `overrideEditorConfig = true` imports serialized settings and merges the template's variables into the Engine, replacing matching keys without clearing unrelated variables. `waitForResources = true` waits for initial resources before returning. The sample receives an initialized `Engine` and runs Engine calls on `engine.dispatcher`, which uses the Engine's main-thread dispatcher. ```kotlin highlight-android-load-template engine.scene.load( sceneUri = templateUri, overrideEditorConfig = true, waitForResources = true, ) val page = checkNotNull(engine.scene.getPages().singleOrNull()) { "The template must contain exactly one page." } ``` Resolve the export page and all other block IDs after each load. IDs from an earlier scene do not remain valid for a newly loaded template. ## Map and Validate Data Map one native record to the template contract before changing the scene. Read each loaded text block's `text/text` property and extract its `{{token}}` keys; `findAll()` is engine-wide and can still contain variables from a previously loaded scene. ```kotlin highlight-android-map-and-validate-data data class DesignRecord( val firstName: String, val lastName: String, val address: String, val city: String, val imageUri: Uri, ) val record = DesignRecord( firstName = "John", lastName = "Doe", address = "123 Main St.", city = "Anytown", imageUri = replacementImageUri, ) val requiredVariableKeys = setOf("first_name", "last_name", "address", "city") val textBlocks = engine.block.findByType(DesignBlockType.Text) check(textBlocks.any { textBlock -> engine.block.referencesAnyVariables(block = textBlock) }) { "Template text blocks must reference at least one variable." } val variableTokenPattern = Regex("""\{\{\s*([^{}]+?)\s*\}\}""") val referencedVariableKeys = textBlocks .flatMap { textBlock -> variableTokenPattern.findAll(engine.block.getString(block = textBlock, property = "text/text")) .map { match -> match.groupValues[1].trim() } .toList() } .toSet() val missingVariableKeys = requiredVariableKeys - referencedVariableKeys check(missingVariableKeys.isEmpty()) { "Template text is missing variable references: ${missingVariableKeys.sorted().joinToString()}" } val imageBlockName = "profile-photo" val imageBlocks = engine.block.findByName(name = imageBlockName) val imageBlock = checkNotNull(imageBlocks.singleOrNull()) { "Template must contain exactly one block named '$imageBlockName'." } val imageFill = engine.block.getFill(block = imageBlock) val imageFillType = engine.block.getType(block = imageFill) check(imageFillType == FillType.Image.key) { "Block '$imageBlockName' must use an image fill." } ``` The validation confirms every required text reference and that exactly one block named `profile-photo` owns an image fill. Variable names and block names are case-sensitive, so failing before mutation makes a changed or incomplete template easier to diagnose. ## Populate Text Variables Set each string value with the variable API. Text blocks that reference `{{first_name}}`, `{{last_name}}`, `{{address}}`, and `{{city}}` use these values when CE.SDK renders the page. ```kotlin highlight-android-populate-text-variables engine.variable.set(key = "first_name", value = record.firstName) engine.variable.set(key = "last_name", value = record.lastName) engine.variable.set(key = "address", value = record.address) engine.variable.set(key = "city", value = record.city) ``` Use `get()` for preflight checks or readback and `remove()` only when restoring values that your application owns. Do not clear unrelated variables from a shared Engine session. ## Replace a Named Image Reuse the image fill supplied by the template and update its `fill/image/imageFileURI` property with `setUri()`. Resetting the crop reframes replacement images whose dimensions differ from the original. ```kotlin highlight-android-replace-named-image val imageUriProperty = "fill/image/imageFileURI" engine.block.setUri( block = imageFill, property = imageUriProperty, value = record.imageUri, ) engine.block.resetCrop(block = imageBlock) val storedImageUri = engine.block.getUri(block = imageFill, property = imageUriProperty) ``` Creating a new fill here would hide a broken template contract, so the sample rejects missing blocks, duplicate names, and non-image fills instead. ## Export the Design Force the page's changed resources to load before exporting it. The PNG export remains a `ByteBuffer`; the sample rewinds it and streams it to a file on `Dispatchers.IO` without creating an intermediate byte array. ```kotlin highlight-android-export-design engine.block.forceLoadResources(blocks = listOf(page)) val exportedPng = engine.block.export(block = page, mimeType = MimeType.PNG).apply { rewind() } withContext(Dispatchers.IO) { FileOutputStream(outputFile).channel.use { channel -> val readablePng = exportedPng.asReadOnlyBuffer() while (readablePng.hasRemaining()) { channel.write(readablePng) } } } ``` Use `MimeType.JPEG` with a `.jpeg` file or `MimeType.PDF` with a `.pdf` file when those formats better match your delivery workflow. For multiple records, start each job from the original template input and resolve fresh IDs; use the dedicated data merge workflow for orchestration. ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a `.scene` template from an Android `Uri` | | `engine.scene.getPages()` | Resolve pages from the currently loaded scene | | `engine.variable.findAll()` | List engine-wide variable keys; this is not a per-template inventory | | `engine.variable.set(key=_, value=_)` | Set a string variable value | | `engine.variable.get(key=_)` | Read a variable value | | `engine.variable.remove(key=_)` | Remove an application-owned variable value | | `engine.block.findByType(type=DesignBlockType.Text)` | Resolve text blocks with the type-safe overload | | `engine.block.referencesAnyVariables(block=_)` | Confirm that template text references variables | | `engine.block.getString(block=_, property="text/text")` | Read text content to validate its exact variable keys | | `engine.block.findByName(name=_)` | Find blocks by their stable template name | | `engine.block.getFill(block=_)` | Read the fill attached to a block | | `engine.block.getType(block=_)` | Read the fill's type for validation | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the current image URI | | `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Read back the current image URI | | `engine.block.resetCrop(block=_)` | Reframe replaced media to cover its block | | `engine.block.forceLoadResources(blocks=_)` | Wait for changed page resources | | `engine.block.export(block=_, mimeType=MimeType.PNG)` | Export the populated page as a `ByteBuffer` | ## Next Steps - [Use Templates](https://img.ly/docs/cesdk/android/use-templates/overview-ae74e1/) — Prepare and load reusable templates. - [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) — Work with variable-backed text. - [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) — Process structured records and media placeholders. - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) — Configure output formats and quality. --- ## 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/) | | 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) engine.editor.loadCMYKProfile() 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 engine.editor.loadCMYKProfile() 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 = "", ) // A CMYK conversion reads the document CMYK profile, which is a resource. Load it once so the // conversions below do not have to handle COLOR.PROFILE_NOT_LOADED. engine.editor.loadCMYKProfile() 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 = "", ) ``` ## Loading the CMYK Profile In Managed scenes, RGB-to-CMYK conversion uses the document CMYK profile, or the fallback profile when no document profile is available. The synchronous conversion reports `COLOR.PROFILE_NOT_LOADED` only while a profile from a URI loads. Await `loadCMYKProfile()` before converting colors to avoid this loading error: ```kotlin engine.editor.loadCMYKProfile() ``` The loading call can fail if no usable profile is available. RGB-to-CMYK conversion uses a simple formula if the profile cannot load or only converts CMYK to RGB. Legacy scenes always use the simple formula for RGB-to-CMYK conversion. ## 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. Spot colors retain a registered CMYK approximation. In Managed scenes, a spot with only an RGB approximation converts through the CMYK profile with its tint applied. ```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 Alpha controls transparency. Tint controls color strength. Non-black sRGB colors convert to CMYK with `tint = 1.0`, even if the source has alpha. Pure black sRGB uses the source alpha as the CMYK tint. CMYK and spot colors convert to opaque sRGB previews. In Managed scenes, CMYK tint scales the ink values before profile conversion. Legacy scenes blend the converted RGB components toward white. RGB spot approximations blend toward white in both modes. When converting a spot color to CMYK, a registered CMYK approximation retains its tint. In Managed scenes, RGB-only approximations apply tint before profile conversion and return `tint = 1.0`. Legacy scenes convert RGB-only approximations without applying tint. | 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 | Managed scenes scale ink values; Legacy scenes blend RGB toward white. The result is opaque. | | SpotColor tint | sRGB | Tint changes the approximation; the result is opaque. | | SpotColor tint | CMYK | CMYK approximations retain tint. Managed RGB-only approximations apply tint before conversion and return `tint = 1.0`. | 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. Custom color palette in the Android fill and stroke controls ## 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, ) suspend 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}", ) // Converting CMYK to sRGB reads the document CMYK profile, which is a resource. Load it once // first, so the conversion does not have to handle COLOR.PROFILE_NOT_LOADED. engine.editor.loadCMYKProfile() 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 // Converting CMYK to sRGB reads the document CMYK profile, which is a resource. Load it once // first, so the conversion does not have to handle COLOR.PROFILE_NOT_LOADED. engine.editor.loadCMYKProfile() 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, ) ``` ## Setting the Document CMYK Profile CE.SDK previews CMYK colors through an ICC profile. The bundled default profile models a US press condition. For another print condition, for example European coated paper, set the CMYK profile of the document to the profile your printer supplies. The engine picks the CMYK profile in this order: 1. A CMYK image with its own embedded ICC profile uses that profile, for that image only. 2. The CMYK profile of the document, when you set one. 3. The profile at the `fallbackCMYKProfileUri` setting. 4. The bundled default profile, when that setting is empty. The document profile and its conversion settings belong to `engine.scene`. They are saved with the scene and bundled into scene archives. Undo and redo do not change these properties. The `fallbackCMYKProfileUri` editor setting belongs to the current editor environment. Raster exports use the document profile for CMYK-to-RGB conversion. A scene whose `scene/colorConversionMode` is `Legacy` keeps its previous conversion until you set the property to `Managed`. ```kotlin // Load the profile from a URI. The call returns once the profile is in effect. engine.scene.setCMYKProfile(uri = Uri.parse("https://example.com/profiles/PSOcoated_v3.icc")) // Or pass the profile bytes directly, in a direct ByteBuffer. val bytes = context.assets.open("PSOcoated_v3.icc").use { it.readBytes() } val buffer = ByteBuffer.allocateDirect(bytes.size).apply { put(bytes) rewind() } engine.scene.setCMYKProfileFromData(data = buffer) // Read what the document names, and go back to the fallback profile. val info = engine.scene.getCMYKProfileInfo() // CMYKProfileInfo engine.scene.removeCMYKProfile() // Configure the conversion. engine.scene.setColorRenderingIntent(intent = ColorRenderingIntent.PERCEPTUAL) engine.scene.setBlackPointCompensationEnabled(enabled = false) ``` An assignment is atomic. While a profile loads, the previous profile stays in effect. When the profile cannot be loaded, is not a valid ICC profile, or is not a CMYK profile, the call fails and the previous profile stays in effect. `engine.scene.getCMYKProfileInfo()` returns the content hash of the profile the document names and throws when none is set. `engine.scene.removeCMYKProfile()` goes back to the fallback profile. A profile set from bytes is saved as a `buffer://` URI when you save the scene to a string, and only the same engine can read that URI. Save the scene to an archive to keep the profile bytes with the scene. You can also set how colors outside the destination gamut are mapped, with the rendering intent, and whether black point compensation is on. The defaults are relative colorimetric intent with black point compensation. ## 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. | | `engine.scene.setCMYKProfile(uri=_)` | Load a CMYK ICC profile from a URI and make it the document profile | | `engine.scene.setCMYKProfileFromData(data=_)` | Make the CMYK ICC profile in a direct `ByteBuffer` the document profile | | `engine.scene.getCMYKProfileInfo()` | Get the document CMYK profile hash; throws if none is set | | `engine.scene.removeCMYKProfile()` | Remove the document CMYK profile and use the fallback profile | | `engine.scene.setColorRenderingIntent(intent=_)` | Set the rendering intent of CMYK conversion | | `engine.scene.setBlackPointCompensationEnabled(enabled=_)` | Turn black point compensation on or off | | 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. ![Four graphic blocks showing a full-strength brand spot color, a lighter tint, and spot colors applied to a stroke and drop shadow.](./assets/android.hero.webp) > **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 suspend 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") } // Converting CMYK reads the document CMYK profile, which is a resource. Load it once first. engine.editor.loadCMYKProfile() 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. ![sRGB colors example showing a blue block with a red stroke, orange text background, and semi-transparent shadow](./assets/android.hero.webp) > **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 // Converting CMYK reads the document CMYK profile, which is a resource. Load it once first. engine.editor.loadCMYKProfile() 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`) Building an app with CE.SDK requires `compileSdk 34` or higher. Other dependencies may require a higher compile SDK. This build requirement does not change the minimum supported Android version. ## 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. - [Plugin Architecture](https://img.ly/docs/cesdk/android/concepts/plugin-architecture-068fc4/) - Understand how CE.SDK plugins fit together—what a plugin is, how it attaches to the editor or engine, and which extension surfaces it can reach. - [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. - [Exclusion Areas](https://img.ly/docs/cesdk/android/concepts/exclusion-areas-c2b87b/) - Mark a region of a page that content must stay out of, and cut it out of an export. - [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. - [Import and Export](https://img.ly/docs/cesdk/android/concepts/import-export-0ebbd8/) - Understand every way files move in and out of CE.SDK—importing media and design files, loading and saving native scenes, and exporting finished output. - [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. - [Export Counting](https://img.ly/docs/cesdk/android/export-counting-613923/) - Learn which operations count as an export in CE.SDK, when export events are recorded, and what data they contain. - [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-library/basics-f29078/) - Explore how the asset library connects sources, categories, and dock buttons - [Asset Library](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/asset-library-f2c082/) — Customize the asset library panel to show user uploads, remote assets, or categories. - [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()`. ## Next Steps - [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Understand the root container that holds every block. - [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) — Learn how pages group blocks into discrete canvases. - [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) — Subscribe to block creation, update, and deletion. --- ## 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. ![A custom crop-based mode active in the Android editor, dimming the surrounding canvas and overlaying a crop grid on the selected image](./assets/editor-state-android.png) > **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. | | ## COLOR ICC color profiles, document CMYK profiles, and color management. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `COLOR.DOCUMENT_PROFILE_NOT_SET` | The document has no CMYK profile. | Set a CMYK profile before reading its information. | | | `COLOR.IMAGE_DECODE_FAILED` | The CMYK image data could not be decoded. Size: \{byteCount} bytes. | The CMYK or YCCK samples of the image could not be read. The image loads without color management instead, so its colors can be wrong. | | | `COLOR.IMAGE_TRANSFORM_FAILED` | The color profile of the image at \{uri} could not be applied. | The profile could be read, but the engine could not convert colors with it. The image loads without color management, so its colors can be wrong. | | | `COLOR.IMPORTED_IMAGE_COLOR_SPACE_INVALID` | The imported image color space value \{value} is not valid. | Use DeviceGray, DeviceRGB, DeviceCMYK or ICCBased. | | | `COLOR.IMPORTED_IMAGE_COLOR_TRANSFORM_INVALID` | The imported image color transform value \{value} is invalid. | Use 0 or 1, or omit the color transform. | | | `COLOR.IMPORTED_IMAGE_DECODE_INVALID` | The imported image decode ranges are invalid. | Provide two finite numbers per image component, or omit decode for identity. | | | `COLOR.IMPORTED_IMAGE_DEFINITION_NOT_FOUND` | The image fill has no active imported image definition. | Attach a definition to the fill's current image before reading it. | | | `COLOR.IMPORTED_IMAGE_HAS_SOURCE_SET` | The image fill uses a source set. | Remove the source set before attaching a definition of imageFileURI. | | | `COLOR.IMPORTED_IMAGE_NO_IMAGE` | The image fill has no image to describe. | Set fill/image/imageFileURI before attaching its imported image definition. | | | `COLOR.IMPORTED_IMAGE_PROFILE_MISMATCH` | The imported image profile does not match its color space. | Provide profile bytes only for ICCBased, and declare ICCBased only when the effective space is ICCBased. | | | `COLOR.IMPORTED_IMAGE_RECORD_INVALID` | The imported image record is invalid. | Keep the record below 64 KiB and name its format when it is present. | | | `COLOR.PROFILE_ASSIGNMENT_SUPERSEDED` | The color profile at \{uri} was not assigned, because a later change replaced the request. | Another profile assignment, a profile removal or a scene load happened while this profile was loading. The later change is in effect. Set the profile again if you still want it. | | | `COLOR.PROFILE_DATA_SPACE_MISMATCH` | The color profile data describes \{space}, but \{expected} is required here. | Pass the bytes of a profile whose color space matches the one it is used for. The document CMYK profile has to be a CMYK profile. The previous profile stays in use. | | | `COLOR.PROFILE_INVALID` | The ICC profile data could not be read. Size: \{byteCount} bytes. | The bytes are not a valid ICC profile. Check that the file is complete and was not truncated by the download. | | | `COLOR.PROFILE_MISSING` | The color profile at \{uri} could not be loaded. | Check that the URI is reachable and returns valid ICC profile data. A setter keeps the previous profile. A profile named by the scene or the setting is skipped: colors of that space use the next profile the engine can load, and are previewed unmanaged when there is none. | | | `COLOR.PROFILE_NOT_LOADED` | The CMYK color profile is not loaded yet, so the color cannot be converted. | The call started the load. Run \`update()\` and convert again. Converting without the profile would return a different color than the engine renders. | | | `COLOR.PROFILE_SPACE_MISMATCH` | The color profile at \{uri} describes \{space}, but \{expected} is required here. | Point the profile at a file whose color space matches the one it is used for. A document profile and the \`fallbackCMYKProfileUri\` setting both have to name a CMYK profile. A setter keeps the previous profile. A profile named by the scene or the setting is skipped, and colors use the next profile. | | | `COLOR.PROFILE_UNSUPPORTED_SPACE` | The ICC profile uses the color space \{space}, which is not supported. | Use an RGB, CMYK or Gray profile. Other color spaces cannot be opened. | | | `COLOR.RENDERING_INTENT_INVALID` | The rendering intent value \{value} is not valid. | Use 0 (Perceptual), 1 (RelativeColorimetric), 2 (Saturation) or 3 (AbsoluteColorimetric). The previous rendering intent stays in use. | | ## 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. | Video export targets a single page block. Pass a page or call api.scene.getPages() to obtain one. Scenes with multiple pages must be exported one page at a time. | | | `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_CANCELLED` | The export was cancelled by the receiver. | The chunk callback or the progress callback returned false. No further data will be delivered. Start a new export if the result is still needed. | | | `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.PDF_STAGING_WRITE_FAILED` | Failed to write the PDF export to temporary storage. | The document could not be staged on disk, so the export was stopped instead of returning a truncated file. Free up storage space and retry. | | | `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.STREAM_EXPORT_MIME_TYPE_INVALID` | Streamed export only supports "application/pdf". | Pass application/pdf as the mime type or use exportToBuffer for other formats. | | | `ENCODE.STREAM_EXPORT_MISSING_RECEIVER` | Streamed export was started without a data receiver. | Pass a chunk callback to the streamed export. There is nothing to deliver the data to. | | | `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.AV_CONCURRENCY_LIMIT_REACHED` | Codec license concurrency limit reached: \{activeCount} of \{concurrencyLimit} licensed instances are already in use. | Wait for another engine instance to finish its video or audio work and retry, or raise the concurrency limit of your license. | | | `LICENSE.AV_SESSION_ACQUISITION_FAILED` | Could not reserve a video codec license from the license server. | Check network connectivity to the IMG.LY license service and retry the operation. | | | `LICENSE.AV_SESSION_REQUIRES_API_KEY` | Video codec licensing requires unlocking the engine with an API key. | Initialize the engine with the API key from your dashboard instead of an offline license file. | | | `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.CLAIM_INVALID` | The license has a missing or invalid "\{claim}" value. | The license server issued a license that this SDK version cannot read. Contact support@img.ly and include this message. | | | `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.ENGINE_VERSION_OUT_OF_RANGE` | This license covers engine versions \{min} up to (but not including) \{max}; this build is \{current}. | Run an engine within the licensed range, or obtain a license that covers \{current}. An open bound is shown as '\*'. | | | `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.PRERELEASE_NOT_ALLOWED` | This license does not cover prerelease engine builds, and this build is \{current}. | Run a release build of the engine, or obtain a license whose config sets 'allowPrerelease' to true. | | | `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_BUFFER_DATA_MISSING` | Could not add the resource '\{resource}' to the archive. Its buffer holds no data. | A buffer URL names data that only this engine holds, so the archive must carry the bytes. The buffer is empty or was destroyed. Write the data again with setBufferData, or clear the reference on the block, then save. | [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: "Exclusion Areas" description: "Mark a region of a page that content must stay out of, and cut it out of an export." platform: android url: "https://img.ly/docs/cesdk/android/concepts/exclusion-areas-c2b87b/" --- > 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/) > [Exclusion Areas](https://img.ly/docs/cesdk/android/concepts/exclusion-areas-c2b87b/) --- An exclusion area marks a region of a page that content must stay clear of — an envelope window, a book spine, a glue flap, an address panel on a mailer. This guide covers creating an exclusion area, putting the artwork of the obstruction into it, controlling how it looks on the canvas, and cutting it out of an export. An exclusion area is authoring geometry. It is visible while you design and stays out of the exported file, so a region you must respect no longer has to be faked with a locked graphic. ## Creating an Exclusion Area Create an exclusion area with `DesignBlockType.ExclusionArea` and append it to a page: ```kotlin val exclusionArea = engine.block.create(DesignBlockType.ExclusionArea) engine.block.setPositionX(exclusionArea, 20F) engine.block.setPositionY(exclusionArea, 20F) engine.block.setWidth(exclusionArea, 40F) engine.block.setHeight(exclusionArea, 40F) engine.block.appendChild(parent = page, child = exclusionArea) ``` A new exclusion area is a rectangle with a stripe fill (`//ly.img.ubq/fill/stripe`), so it is visible as soon as you create it. The stripes leave their gaps transparent, so the exclusion area marks the region without hiding what is behind it. An exclusion area is a graphic. It takes a shape, a fill, a stroke, effects and a blur, so you can put the artwork of the obstruction into it. Assigning another fill replaces the stripes: ```kotlin val fill = engine.block.createFill(FillType.Image) engine.block.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block = exclusionArea, fill = fill) ``` Because an exclusion area takes any shape a graphic takes, a round window or a die cut outline is an exclusion area with that shape assigned: ```kotlin engine.block.setShape(block = exclusionArea, shape = engine.block.createShape(ShapeType.Ellipse)) ``` ## Appearance The engine washes every exclusion area in `page/exclusionAreaFillColor` and frames it in `page/exclusionAreaFrameColor`. ```kotlin engine.editor.setSettingColor( keypath = "page/exclusionAreaFillColor", value = Color.fromRGBA(r = 0.79F, g = 0.12F, b = 0.4F, a = 0.35F), ) ``` Both are editor settings rather than block properties, so they apply to every exclusion area in the scene and neither reaches an export. A fully transparent `page/exclusionAreaFrameColor` hides the frame, and a fully transparent `page/exclusionAreaFillColor` hides the wash. The stripes are the exclusion area's fill, so neither setting changes them. Reach the stripes of one exclusion area through its fill: ```kotlin val stripes = engine.block.getFill(block = exclusion area) engine.block.setColor( block = stripes, property = "fill/stripe/color", value = Color.fromRGBA(r = 0.79F, g = 0.12F, b = 0.4F, a = 0.25F), ) engine.block.setFloat(block = stripes, property = "fill/stripe/width", value = 4F) engine.block.setFloat(block = stripes, property = "fill/stripe/gap", value = 4F) engine.block.setFloat(block = stripes, property = "fill/stripe/angle", value = 90F) ``` `fill/stripe/width` and `fill/stripe/gap` are in pixels at the resolution of the scene, so they keep their size when you scale the exclusion area or change the design unit, and `fill/stripe/angle` is in degrees, where 0 stands the stripes upright and 90 lays them flat. ## Exclusion Areas and Export An exclusion area is authoring state, so it is left out of an export. Set `includedInExport` to `true` on the exclusion area to put its artwork in the file. The guide colors still stay out: ```kotlin engine.block.setIncludedInExport(block = exclusionArea, enabled = true) ``` The stripes are the exclusion area's fill, so an exclusion area that still has the fill it was created with brings its stripes into the file. Assign the artwork of the obstruction to the exclusion area to export that in place of the stripes. Set `exclusionArea/punchOut` to `true` to cut the exclusion area out of an export instead. The page and everything on it get a hole where the exclusion area is, so a die cut window in the design becomes a window in the exported file: ```kotlin engine.block.setBoolean(block = exclusionArea, property = "exclusionArea/punchOut", value = true) ``` The hole is transparent, which a print workflow reads as an absence of ink rather than as white. Punch-out is off by default, and the canvas keeps showing the content either way. An exclusion area never changes the size of an export. An exclusion area that hangs over the edge of a page cannot grow the exported page, and an exclusion area parented to the scene does not grow an exported scene unless you opt it into the export. ## Selection An exclusion area is the author's to move and resize. An adopter cannot select one, because a block created in the creator role carries no selection scope. Take the exclusion area out of reach in every role with: ```kotlin engine.editor.setSelectionEnabled(block = exclusionArea, enabled = false) ``` ## Holding Content Out An exclusion area marks a region, and on its own it moves nothing. Set `exclusionArea/constrains` to `true` for a region the engine must hold content out of, such as an envelope window or a die cut: ```kotlin engine.block.setBoolean(block = exclusion area, property = "exclusion area/constrains", value = true) ``` The exclusion area is then a wall while the user drags, nudges or resizes a block. The block stops against the exclusion area's own shape and slides along its edge, and the user drags around the exclusion area to reach the other side. An exclusion area shaped as a ring keeps blocks out of the ring and leaves the hole free, which is how a forbidden outer edge of a page is expressed. The exclusion area the block is up against draws a border on the canvas, so the user sees what stopped them. An exclusion area that marks without constraining draws that border too. The engine never moves a block on its own, so a block that already overlaps an exclusion area stays where it is, and a call through the API is never constrained. Ask which blocks overlap an exclusion area with: ```kotlin val offending = engine.block.findAllInExclusionAreas() ``` ## Limitations Arrow keys are not constrained, and a block added through the API is placed wherever it is asked for. An exclusion area snaps by its bounding box while it constrains by its shape, so a curved exclusion area snaps as a rectangle. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.create(DesignBlockType.ExclusionArea)` | Create an exclusion area. | | `engine.block.setShape(block, shape)` | Give the exclusion area a non-rectangular outline. | | `engine.block.setFill(block, fill)` | Put the artwork of the obstruction into the exclusion area. | | `engine.block.getFill(block)` | Reach the stripe fill the exclusion area was created with. | | `engine.block.setIncludedInExport(block, enabled)` | Put the exclusion area's artwork into an export. | | `engine.block.setBoolean(block, "exclusionArea/punchOut", value)` | Cut the exclusion area out of an export. | | `engine.block.setBoolean(block, "exclusionArea/constrains", value)` | Hold blocks out of the exclusion area while the user drags them. | | `engine.block.findAllInExclusionAreas()` | List the blocks that overlap a constraining exclusion area. | | `engine.editor.setSettingColor(keypath, value)` | Set the wash and frame colors for every exclusion area. | | `engine.editor.setSelectionEnabled(block, enabled)` | Take the exclusion area out of reach in every role. | ## Next Steps - [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) — page margins and the safety inset. - [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — block types, shapes and fills. --- ## 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 output image from the example code.](assets/headless-ios-1-161.png) 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: "Import and Export" description: "Understand every way files move in and out of CE.SDK—importing media and design files, loading and saving native scenes, and exporting finished output." platform: android url: "https://img.ly/docs/cesdk/android/concepts/import-export-0ebbd8/" --- > 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/) > [Import and Export](https://img.ly/docs/cesdk/android/concepts/import-export-0ebbd8/) --- Understand every way files move in and out of CE.SDK—importing media and design files, loading and saving native scenes, and exporting finished output. When developers ask whether CE.SDK "supports" a file format, the answer depends on which workflow they mean. Importing an image as content, converting a Photoshop file into an editable design, and rendering a finished PDF are three different operations with different format support. This page maps every way files come in and go out, and links to the guide for each workflow. ![Diagram showing how files move in and out of CE.SDK: media assets, design files converted by importer packages and native design files flow into a scene; saving produces a re-editable scene string or archive, while exporting renders flattened final output](./assets/import-export-flows.svg) Everything in CE.SDK revolves around the *scene*—the block hierarchy that describes a design. Files coming in either add content to a scene or become a scene; files going out either preserve the scene for later editing or render it into final output. ## Three Ways Files Come In ### Media Assets Media assets are content placed *inside* a design: images, videos, audio clips and fonts. Users add them through the asset library, the device's photo library or the camera, and you add them programmatically through the asset and block APIs—to connect your own backend as a source, see [Create a Custom Importer](https://img.ly/docs/cesdk/android/import-media/create-custom-importer-0f7e16/). Media assets don't change the structure of a scene—they fill blocks with content. See the [Import Media](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/) section for upload workflows and the asset library, [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) for the underlying architecture, and [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) for the exact format list. ### Native Design Files CE.SDK persists designs in its own format, which loads back with full editability. Native files come in two variants: - **Scene string** (`.scene`) — A serialized description of the block hierarchy. Assets are referenced by URL, not embedded, so the file is lightweight but depends on those URLs staying reachable. - **Archive** (`.zip`) — A self-contained bundle of the scene file plus every referenced asset. Larger, but portable and usable offline. The `.imgly` extension is used for both variants; the engine detects the format automatically when loading. Load native files with `engine.scene.load()` or `engine.scene.loadArchive()`. See [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/), [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) and [Import from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/). ### Design Files From Other Tools Files created in other design applications—Photoshop, InDesign, PowerPoint or PDF-producing tools—are not media assets and can't be added like an image. Instead, dedicated importer packages *convert* them into native CE.SDK scenes, preserving text, images, vector paths, positioning and colors as editable blocks: | Source format | Package | | --------------------- | ----------------------- | | Photoshop (`.psd`) | `@imgly/psd-importer` | | InDesign (`.idml`) | `@imgly/idml-importer` | | PDF (`.pdf`) | `@imgly/pdf-importer` | | PowerPoint (`.pptx`) | `@imgly/pptx-importer` | The importers are JavaScript packages and don't run inside the Android SDK. The typical mobile workflow converts files on your server with Node.js, then loads the resulting scene or archive on the device. See [From InDesign](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-indesign-ba3988/) and [From Photoshop](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-photoshop-cca6bb/) for this workflow. #### Importer Features and Limitations Each importer translates a subset of its source format, so review converted scenes—especially complex documents—before publishing. Where a feature can't be mapped one-to-one, the importers degrade gracefully. Typical examples: - Linked, non-embedded images aren't resolved and import as placeholders (IDML, PDF). - Mixing multiple fonts or font sizes within a single Photoshop text layer isn't supported, and unavailable fonts are substituted depending on the configured font strategy. - PowerPoint tables, charts, SmartArt, animations and slide masters are skipped. The npm page of each package maintains the up-to-date list of supported features and limitations: [`@imgly/psd-importer`](https://www.npmjs.com/package/@imgly/psd-importer), [`@imgly/idml-importer`](https://www.npmjs.com/package/@imgly/idml-importer), [`@imgly/pdf-importer`](https://www.npmjs.com/package/@imgly/pdf-importer) and [`@imgly/pptx-importer`](https://www.npmjs.com/package/@imgly/pptx-importer). For any format without a ready-made importer, you can build your own converter with the scene and block APIs—see [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) for building a design programmatically. ## Two Ways Files Go Out ### Saving a Design Saving preserves the scene for later editing. `engine.scene.saveToString()` produces a scene string with assets referenced by URL; `engine.scene.saveToArchive()` produces a self-contained archive with assets embedded. Both round-trip losslessly—loading a saved file restores the design exactly, with full editability. A saved file is not viewable output: it only renders inside CE.SDK. To show a preview of a saved design elsewhere, export a thumbnail alongside it. See [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) and [Create Thumbnail](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/). ### Exporting Final Output Exporting renders the scene into a finished file for use outside CE.SDK. Export the whole scene, a single page or any individual block with `engine.block.export()`. Output covers images, vector graphics, print-ready PDF, video and audio, and raw pixel data—[File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) lists the exact formats on Android. Each output has a focused guide: [To PNG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/), [To JPEG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-jpeg-6f88e9/), [To PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) (including [print-ready output](https://img.ly/docs/cesdk/android/export-save-publish/for-printing-bca896/) with underlayers and spot colors), [To MP4](https://img.ly/docs/cesdk/android/export-save-publish/export/to-mp4-c998a8/), [audio](https://img.ly/docs/cesdk/android/export-save-publish/export/audio-68de25/) and [raw RGBA buffers](https://img.ly/docs/cesdk/android/export-save-publish/export/to-raw-data-abd7da/). Exports are flattened: text becomes pixels or paths, and the block structure is gone. An exported file can't be turned back into the original editable design. See [Export Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) for configuration, plus [Partial Export](https://img.ly/docs/cesdk/android/export-save-publish/export/partial-export-89aaf6/), [Pre-Export Validation](https://img.ly/docs/cesdk/android/export-save-publish/pre-export-validation-3a2cba/), [Export for Social Media](https://img.ly/docs/cesdk/android/export-save-publish/for-social-media-0e8a92/) and [Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/). ## Save vs. Export The most common point of confusion is the difference between saving and exporting: | | Save | Export | | -------------- | ---------------------------------------- | --------------------------------------------- | | Purpose | Continue editing later | Use the result outside CE.SDK | | Output | `.scene` string, `.imgly`/`.zip` archive | Rendered files (PNG, PDF, MP4, …)| | Re-editable | Yes, losslessly | No—content is flattened | | Assets | Referenced by URL or embedded | Rendered into the output | | Viewable outside CE.SDK | No | Yes | A typical integration uses both: save the scene to your backend so users can keep editing, and export when they download or publish the result. ## Common Questions - **Can users add a PDF or Photoshop file like an image?** Not as a media asset—the media path accepts images, video and audio only. PDF, PSD, IDML and PPTX files go through the importer packages, which convert them into editable scenes upstream of the mobile app. - **Why isn't my exported PDF editable when I import it back?** Exporting flattens the design. The PDF importer reconstructs an editable scene from any PDF, but it's a conversion, not a lossless round trip. To preserve editability, save the scene alongside the export. - **What's the difference between `.scene`, `.imgly` and `.zip`?** `.scene` is always a scene string, `.zip` is always an archive, and `.imgly` is the recommended extension for either—the engine detects which one it is when loading. - **Does a saved scene contain its images?** A scene string references assets by URL, so the design breaks if those URLs go away. An archive embeds all assets and is fully self-contained. ## Format Support Reference For the authoritative format tables—including codec details, size limits and known limitations—see [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/). ## API Reference | Method | Category | Purpose | | ------------------------------ | -------- | ---------------------------------------------------------------- | | `engine.scene.load()` | Import | Load a scene string, with automatic format detection | | `engine.scene.loadArchive()` | Import | Load a self-contained scene archive | | `engine.scene.saveToString()` | Save | Serialize the scene to a string with assets referenced by URL | | `engine.scene.saveToArchive()` | Save | Bundle the scene and all assets into a self-contained archive | | `engine.block.export()` | Export | Render a block, page or scene to a final output format | ## Next Steps - [Import Media](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/) — Bring images, video, audio and fonts into designs - [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) — Load saved scenes and archives - [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/) — Load templates from URLs, archives and strings - [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) — Persist designs for later editing - [Export Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Configure output formats and quality --- ## 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. ### Safety Inset The safety inset is the inward inset from the page edge that content must stay inside. It is the counterpart of the bleed margin, which extends outward. Print shops cut with a tolerance, so text and logos placed too close to the edge can be trimmed off. Set `page/safetyEnabled` to `true`, then use `page/safetyInset/top`, `page/safetyInset/bottom`, `page/safetyInset/left`, and `page/safetyInset/right` to configure each side. Every inset defaults to `0`, so set a value as well as enabling the area. The safety inset is authoring state, and nothing it draws reaches an export. The guide appears while a drag is in the safety inset or comes near the safety line, and goes away when the drag ends. A move or resize with the arrow keys shows it the same way for 1.6 seconds, without snapping. The engine shades the band between the page edge and the line, draws the line itself, and lets the dragged block snap to it. Set `page/safetyRevealDuringTransform` to `false` to draw the guide on every page all the time instead. Set the guide colors with the `page/safetyFillColor` and `page/safetyFrameColor` settings; a fully transparent `page/safetyFrameColor` hides the line and turns its snapping off. ### Exclusion Area An exclusion area marks a region of a page that content must stay out of: a window on an envelope, a spine on a book cover, a glue flap on a box, an address panel on a mailer. Where the safety inset is one inset per page, a page can carry any number of exclusion areas, each with its own position, size and rotation. Create one with the `//ly.img.ubq/exclusionArea` block type and append it to a page. An exclusion area is a graphic, so it takes a shape, a fill, a stroke and the ordinary transform, and you can put the artwork of the obstruction into it. A new exclusion area is a rectangle with no fill and shows a striped pattern, so it is visible at once. Assign a fill to replace the stripes. An exclusion area is authoring state. The engine washes it in `page/exclusionAreaFillColor` and frames it in `page/exclusionAreaFrameColor`, neither of which reaches an export, and a fully transparent `page/exclusionAreaFrameColor` hides the frame. An exclusion area is left out of an export as well: set `includedInExport` to `true` on the exclusion area to put its artwork in the file. Set `exclusionArea/punchOut` to `true` to cut the exclusion area out of an export instead. The page and everything on it get a hole where the exclusion area is, so a die cut window in the design becomes a window in the file. It is off by default, and the canvas keeps showing the content either way. ### 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: "Plugin Architecture" description: "Understand how CE.SDK plugins fit together—what a plugin is, how it attaches to the editor or engine, and which extension surfaces it can reach." platform: android url: "https://img.ly/docs/cesdk/android/concepts/plugin-architecture-068fc4/" --- > 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/) > [Plugin Architecture](https://img.ly/docs/cesdk/android/concepts/plugin-architecture-068fc4/) --- ```kotlin file=@cesdk_android_examples/editor-guides-plugins-custom-plugin/CustomFeaturePluginEditorSolution.kt reference-only import android.net.Uri import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.editor.BasicConfigurationBuilder import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.EditorScope import ly.img.editor.core.ScopedProperty import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.modify import ly.img.editor.core.component.remember import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.EditorConfigurationBuilder import ly.img.editor.core.configuration.remember import ly.img.editor.core.configuration.then import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Image import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.HorizontalBlockAlignment import ly.img.engine.ShapeType import ly.img.engine.SizeMode import ly.img.engine.VerticalBlockAlignment import java.io.File import java.nio.ByteBuffer import java.util.UUID @Composable fun CustomFeaturePluginEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::CustomFeaturePlugin) { randomImageUri = "https://img.ly/static/ubq_samples/sample_1.jpg".toUri() } }, onClose = onClose, ) } open class CustomFeaturePlugin : EditorConfigurationBuilder() { var randomImageUri: Uri by editorContext.mutableStateOf( key = "com.example.editor.customFeature.randomImageUri", initial = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) private var isLoading: Boolean by editorContext.mutableStateOf( key = BasicConfigurationBuilder.KEY_STATE_SHOW_LOADING, initial = false, ) override var onCreate: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val editorScope = this coroutineScope { launch { parentConfiguration?.onCreate?.invoke(editorScope) // The parent Design configuration hides loading after scene setup. isLoading = true } launch { Log.d(TAG, "CustomFeaturePlugin setup started.") delay(3_000) Log.d(TAG, "CustomFeaturePlugin setup finished.") } } } finally { isLoading = false } } override var onExport: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val scene = requireNotNull(editorContext.engine.scene.get()) { "A scene is required before exporting." } val archive = editorContext.engine.scene.saveToArchive(scene = scene) val file = writeArchiveToTempFile(archive) Toast.makeText(editorContext.activity, "Saved scene archive to ${file.name}", Toast.LENGTH_SHORT).show() Log.d(TAG, "Saved scene archive to ${file.absolutePath}") } finally { isLoading = false } } override var dock: ScopedProperty?>? = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("com.example.component.dock.button.customFeature") } vectorIcon = { IconPack.Image } textString = { "Image" } contentDescription = { "Add image" } onClick = { addImageBlockFromPlugin() } } } } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } override var canvasMenu: ScopedProperty?>? = { null } private fun EditorScope.addImageBlockFromPlugin() { val engine = editorContext.engine val page = requireNotNull(engine.scene.getCurrentPage()) { "A current page is required before adding an image block." } val imageBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(ShapeType.Rect) val fill = engine.block.createFill(FillType.Image) engine.block.setShape(block = imageBlock, shape = shape) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = randomImageUri, ) engine.block.setFill(block = imageBlock, fill = fill) engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER) engine.block.setWidthMode(block = imageBlock, mode = SizeMode.PERCENT) engine.block.setWidth(block = imageBlock, value = 0.5F) engine.block.appendChild(parent = page, child = imageBlock) if (engine.block.isAlignable(listOf(imageBlock))) { engine.block.alignHorizontally(listOf(imageBlock), alignment = HorizontalBlockAlignment.CENTER) engine.block.alignVertically(listOf(imageBlock), alignment = VerticalBlockAlignment.CENTER) } engine.block.setSelected(block = imageBlock, selected = true) } private suspend fun writeArchiveToTempFile(archive: ByteBuffer): File = withContext(Dispatchers.IO) { File.createTempFile("custom-feature-${UUID.randomUUID()}", ".imgly").apply { outputStream().channel.use { channel -> channel.write(archive) } } } private companion object { const val TAG = "CustomFeaturePlugin" } } ``` Understand how CE.SDK's plugin system fits together on Android—what a plugin is, how it chains onto an editor configuration, and which parts of the editor it can extend. ![Diagram of the Android plugin configuration chain: a base configuration is chained into a custom plugin with then(), the plugin overrides callbacks and components and reaches the previous configuration through parentConfiguration, and the result configures the Editor composable](./assets/plugin-chain-android.svg) A plugin is a self-contained unit that packages editor behavior—callbacks, state, UI component configuration—and attaches to an existing editor without rebuilding it. On Android, a plugin is a class inheriting from `EditorConfigurationBuilder` that is chained onto the editor configuration. IMG.LY ships official plugins such as background removal this way, and you can build your own. This guide covers what a plugin is on Android, when to use one instead of other customization mechanisms, how plugins compose through the configuration chain, and where each official plugin is documented. ## What a Plugin Is The editor configuration is built first, and plugins are chained onto it afterwards. Each plugin is one `EditorConfigurationBuilder` subclass with one place where it applies its behavior: the builder properties it overrides. The same composition idea exists on every platform, even though the APIs differ—the Web registers plugin objects with `addPlugin()` and iOS subclasses `EditorConfiguration`. ## Plugin or Not? Plugins are one of several customization mechanisms, and they're commonly conflated. Use this list to place them: - **Inline configuration**: The default. Configure the editor directly in the builder block where it's created—best for one editor surface or a one-off product flow. - **Starter kits**: Copy-to-adapt scaffolds for complete editor experiences. Browse them in the [starter kits overview](https://img.ly/docs/cesdk/android/starterkits-kxg120/). - **Plugins**: When the same behavior must travel between projects or entry points as one unit. Importers and exporters are standalone packages that convert file formats. They are versioned independently of CE.SDK and are not plugins—you use their own APIs instead of chaining them onto the editor configuration. ## How a Plugin Attaches The base configuration comes first, plugins second, app-specific edits last. `EditorConfiguration.remember` creates the base, and `then(::SomePlugin)` chains a plugin onto it. The builder block passed to `then` configures plugin options for this editor entry point: ```kotlin highlight-android-apply-plugin Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::CustomFeaturePlugin) { randomImageUri = "https://img.ly/static/ubq_samples/sample_1.jpg".toUri() } }, onClose = onClose, ) ``` Each plugin in the chain can reach the previous configuration through `parentConfiguration`, so it decides per callback and per component whether to extend the inherited behavior or replace it. ## Extending and Replacing Behavior A plugin that overrides `onCreate` can run work around the inherited setup by invoking `parentConfiguration?.onCreate` in the middle: ```kotlin highlight-android-on-create override var onCreate: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val editorScope = this coroutineScope { launch { parentConfiguration?.onCreate?.invoke(editorScope) // The parent Design configuration hides loading after scene setup. isLoading = true } launch { Log.d(TAG, "CustomFeaturePlugin setup started.") delay(3_000) Log.d(TAG, "CustomFeaturePlugin setup finished.") } } } finally { isLoading = false } } ``` The same choice applies to UI components. This dock override keeps the inherited dock and prepends one button instead of replacing the whole component: ```kotlin highlight-android-dock override var dock: ScopedProperty?>? = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("com.example.component.dock.button.customFeature") } vectorIcon = { IconPack.Image } textString = { "Image" } contentDescription = { "Add image" } onClick = { addImageBlockFromPlugin() } } } } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } ``` ## Plugin Options Plugins expose options as builder properties, so the same plugin can behave differently in different editors. State that participates in Compose recomposition uses `editorContext.mutableStateOf`: ```kotlin highlight-android-plugin-state var randomImageUri: Uri by editorContext.mutableStateOf( key = "com.example.editor.customFeature.randomImageUri", initial = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) private var isLoading: Boolean by editorContext.mutableStateOf( key = BasicConfigurationBuilder.KEY_STATE_SHOW_LOADING, initial = false, ) ``` ## Official Plugins Each official plugin has its own page with installation and options; this table only maps the landscape. | Plugin | What it does | Docs | | --- | --- | --- | | Background Removal | Removes image backgrounds on device | [Remove Background](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) | | AI Generation | Generates images and more in the editor | [AI Image Generation](https://img.ly/docs/cesdk/android/plugins/ai-image-generation-aimg01/) | The [plugins section](https://img.ly/docs/cesdk/android/plugins-693c48/) lists the full set. ## Building Your Own A custom plugin is an `EditorConfigurationBuilder` subclass with a stable identity, options as builder properties, and overridden callbacks or components. Reach for one when the same behavior needs to travel together instead of being pasted into every editor setup. The [Custom Feature Plugin](https://img.ly/docs/cesdk/android/plugins/custom-plugin-ju8y5t/) guide walks through building the plugin shown on this page. ## Troubleshooting - **The editor opens without the expected scene**: A plugin replaced `onCreate` and skipped the base setup. Delegate to `parentConfiguration?.onCreate`, or fully create the scene inside the plugin. - **A dock or inspector item disappears**: The plugin replaced a component instead of extending it. Derive from `parentConfiguration?.dock` when adding controls. - **Plugin options don't update**: The option is captured once in the builder. Store dynamic values with `editorContext.mutableStateOf` so they participate in state. ## Next Steps - [Custom Feature Plugin](https://img.ly/docs/cesdk/android/plugins/custom-plugin-ju8y5t/) - Build your own plugin - [Architecture](https://img.ly/docs/cesdk/android/concepts/architecture-6ea9b2/) - How the CreativeEngine is structured - [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) - Complete editor scaffolds to start from --- ## 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. The same call also loads archives that bundle referenced assets — the engine detects the content automatically. Both scenes and archives use the `.imgly` extension; `.scene` and `.zip` files also load. ### 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. See [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) for details. ### 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 the [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/), [Gradient Fills](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/), [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/), and [Video Fills](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/) guides. ### 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/exclusionArea` — An editor-only region content must stay out of - `//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. See [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/) for details. ### 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 | Request a trial license through [IMG.LY Contact Sales](https://img.ly/forms/contact-sales/). ```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. The `builder` lambda passed to `EditorConfiguration.remember` runs only once. Assign callbacks once and put conditions that depend on changing Compose state inside the callback so it reads the current value when it runs. 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 Binary Data](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/export-save-publish/export/to-png-f87eaf/) - Export CE.SDK designs as PNG images with lossless compression, alpha support, and configurable output dimensions. - [To PDF](https://img.ly/docs/cesdk/android/conversion/to-pdf-eb937f/) - Convert images and multi-page designs into print-ready PDF files with 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. ## Next Steps - [To Base64](https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/) — Convert exported binary data into Base64 strings and data URIs. - [To Binary Data](https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/) — Export design blocks to `ByteBuffer` data for saving, uploading, or sharing. - [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. - [To PDF](https://img.ly/docs/cesdk/android/conversion/to-pdf-eb937f/) — Convert your design or document into a high-quality, print-ready PDF 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: "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.util.Base64 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(engine: Engine): List = withContext(engine.dispatcher) { 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 } 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 - [To PDF](https://img.ly/docs/cesdk/android/conversion/to-pdf-eb937f/) — Convert your design or document into a high-quality, print-ready PDF format. - [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. - [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 Binary Data" 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 Binary Data](https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/) --- ```kotlin file=@cesdk_android_examples/engine-guides-to-blob/ToBlob.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 ToBlobResult( val pngData: ByteBuffer, val jpegData: ByteBuffer, val pageExports: List, val savedPngFile: File, ) suspend fun toBlob( engine: Engine, outputDir: File, ): ToBlobResult = withContext(engine.dispatcher) { 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, ) } 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 = withContext(engine.dispatcher) { val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, ) check(pngData.hasRemaining()) { "PNG export is empty" } pngData } suspend fun exportWithOptions( engine: Engine, page: DesignBlock, ): ByteBuffer = withContext(engine.dispatcher) { 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" } jpegData } suspend fun exportMultipleBlocks(engine: Engine): List = withContext(engine.dispatcher) { 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" } } 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 = withContext(engine.dispatcher) { val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, ) check(pngData.hasRemaining()) { "PNG export is empty" } 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 = withContext(engine.dispatcher) { 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" } 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 = withContext(engine.dispatcher) { 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" } } 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 - [To PDF](https://img.ly/docs/cesdk/android/conversion/to-pdf-eb937f/) — Convert your design or document into a high-quality, print-ready PDF format. - [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 PDF" description: "Convert images and multi-page designs into print-ready PDF files with CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/conversion/to-pdf-eb937f/" --- > 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 PDF](https://img.ly/docs/cesdk/android/conversion/to-pdf-eb937f/) --- ```kotlin file=@cesdk_android_examples/engine-guides-conversion-to-pdf/ConversionToPdf.kt reference-only import android.net.Uri 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.SceneLayout import java.io.File import java.nio.ByteBuffer data class ConversionToPdfResult( val singleImagePdf: File, val multiPagePdf: File, val highCompatibilityPdf: File, val underlayerPdf: File, val configuredPdf: File, val pageCount: Int, ) suspend fun conversionToPdf( engine: Engine, imageUris: List, outputDirectory: File, ): ConversionToPdfResult = withContext(engine.dispatcher) { require(imageUris.isNotEmpty()) { "Provide at least one image URI." } engine.scene.createFromImage(imageUri = imageUris.first()) val singleImagePage = checkNotNull(engine.scene.getCurrentPage()) val singleImageData = engine.block.export( block = singleImagePage, mimeType = MimeType.PDF, ) val singleImagePdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "single-image.pdf", buffer = singleImageData, ) val stackedScene = engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK) val stack = engine.block.findByType(DesignBlockType.Stack).first() imageUris.forEach { imageUri -> val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = stack, child = page) val imageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = imageUri, ) engine.block.setFill(block = page, fill = imageFill) } val multiPageData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, ) val multiPagePdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "multi-page.pdf", buffer = multiPageData, ) engine.block.setFloat(block = stackedScene, property = "scene/dpi", value = 150F) val compatibilityOptions = ExportOptions(exportPdfWithHighCompatibility = true) val highCompatibilityData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, options = compatibilityOptions, ) val highCompatibilityPdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "high-compatibility.pdf", buffer = highCompatibilityData, ) engine.editor.setSpotColor( name = "BrandUnderlay", color = Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F, a = 1F), ) val underlayerOptions = ExportOptions( exportPdfWithHighCompatibility = true, exportPdfWithUnderlayer = true, underlayerSpotColorName = "BrandUnderlay", underlayerOffset = -2F, ) val underlayerData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, options = underlayerOptions, ) val underlayerPdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "with-underlayer.pdf", buffer = underlayerData, ) engine.block.setFloat(block = stackedScene, property = "scene/dpi", value = 300F) val combinedOptions = ExportOptions( targetWidth = 2480F, targetHeight = 3508F, exportPdfWithHighCompatibility = true, exportPdfWithUnderlayer = true, underlayerSpotColorName = "BrandUnderlay", underlayerOffset = -2F, ) val configuredData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, options = combinedOptions, ) val configuredPdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "configured.pdf", buffer = configuredData, ) ConversionToPdfResult( singleImagePdf = singleImagePdf, multiPagePdf = multiPagePdf, highCompatibilityPdf = highCompatibilityPdf, underlayerPdf = underlayerPdf, configuredPdf = configuredPdf, pageCount = engine.scene.getPages().size, ) } suspend fun saveConversionPdf( outputDirectory: File, fileName: String, buffer: ByteBuffer, ): File = withContext(Dispatchers.IO) { check(outputDirectory.isDirectory || outputDirectory.mkdirs()) { "Could not create the PDF output directory." } val source = buffer.asReadOnlyBuffer() File(outputDirectory, fileName).apply { outputStream().channel.use { channel -> while (source.hasRemaining()) { channel.write(source) } } check(length() > 0L) { "PDF export was empty." } } } ``` Convert images and multi-page designs to PDF programmatically. Load one or more image files, build a scene, and export the result without presenting the editor UI. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-conversion-to-pdf) CE.SDK converts single or multiple images to PDF while letting you crop, rotate, or add content before export. You can also configure resolution, compatibility, target dimensions, and an underlayer for specialty printing. This guide covers single-image conversion, combining multiple images into one multi-page PDF, and configuring the resulting document. ## Convert to PDF Programmatically Use `engine.block.export()` with `MimeType.PDF` to export a scene or page. The method returns a `ByteBuffer`; this sample writes a read-only view to an app-managed output directory so the original buffer remains available to other consumers. ```kotlin highlight-android-conversion-to-pdf-save suspend fun saveConversionPdf( outputDirectory: File, fileName: String, buffer: ByteBuffer, ): File = withContext(Dispatchers.IO) { check(outputDirectory.isDirectory || outputDirectory.mkdirs()) { "Could not create the PDF output directory." } val source = buffer.asReadOnlyBuffer() File(outputDirectory, fileName).apply { outputStream().channel.use { channel -> while (source.hasRemaining()) { channel.write(source) } } check(length() > 0L) { "PDF export was empty." } } } ``` ### Convert a Single Image to PDF Load the first input image into a fresh scene with `engine.scene.createFromImage()`, then export the current page. ```kotlin highlight-android-conversion-to-pdf-single-image engine.scene.createFromImage(imageUri = imageUris.first()) val singleImagePage = checkNotNull(engine.scene.getCurrentPage()) val singleImageData = engine.block.export( block = singleImagePage, mimeType = MimeType.PDF, ) val singleImagePdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "single-image.pdf", buffer = singleImageData, ) ``` `engine.scene.createFromImage()` creates a scene with one page whose fill is the loaded image. Exporting that page produces a single-page PDF. You can query and mutate its fill or add blocks before export when the image needs edits. ### Combine Multiple Images into a Single PDF Create a scene with a vertical stack layout, append one page per image, and export the scene to produce a multi-page PDF. ```kotlin highlight-android-conversion-to-pdf-multi-image val stackedScene = engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK) val stack = engine.block.findByType(DesignBlockType.Stack).first() imageUris.forEach { imageUri -> val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = stack, child = page) val imageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = imageUri, ) engine.block.setFill(block = page, fill = imageFill) } val multiPageData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, ) val multiPagePdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "multi-page.pdf", buffer = multiPageData, ) ``` Exporting the scene includes every page owned by the stack. Export an individual page instead when you need a separate PDF for each image. ## Configure PDF Output Settings The PDF fields on `ExportOptions` control how CE.SDK renders and sizes the document. Set them individually or combine them in one export call. ### Adjust DPI for Print Quality The scene's `scene/dpi` property controls the resolution used for bitmap images and rasterized effects. It does not change the page size. ```kotlin highlight-android-conversion-to-pdf-dpi engine.block.setFloat(block = stackedScene, property = "scene/dpi", value = 150F) ``` Higher DPI values can produce sharper output at the cost of larger files. The default is 300 DPI, which suits most print workflows. ### Enable High Compatibility Mode Set `exportPdfWithHighCompatibility` to rasterize complex elements at the scene's DPI for more consistent rendering across PDF viewers. ```kotlin highlight-android-conversion-to-pdf-high-compatibility val compatibilityOptions = ExportOptions(exportPdfWithHighCompatibility = true) val highCompatibilityData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, options = compatibilityOptions, ) val highCompatibilityPdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "high-compatibility.pdf", buffer = highCompatibilityData, ) ``` The option defaults to `true`. Disable it when you need vectors preserved and have verified that your target viewers render the document correctly. ### Add an Underlayer for Specialty Printing Underlayers provide a base ink layer for printing on transparent or non-white materials such as fabric, glass, or acrylic. CE.SDK generates the underlayer from the design contours and places it behind the visible content. > **Caution:** Do not flatten the resulting PDF file. Flattening removes the separate > underlayer shape. First, register the spot color that represents the underlayer ink. Its name must match the name expected by your print provider; the RGB value provides a preview. ```kotlin highlight-android-conversion-to-pdf-spot-color engine.editor.setSpotColor( name = "BrandUnderlay", color = Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F, a = 1F), ) ``` Then enable the underlayer during export. A negative `underlayerOffset` shrinks the shape inward to reduce visible edges from print misalignment. ```kotlin highlight-android-conversion-to-pdf-underlayer val underlayerOptions = ExportOptions( exportPdfWithHighCompatibility = true, exportPdfWithUnderlayer = true, underlayerSpotColorName = "BrandUnderlay", underlayerOffset = -2F, ) val underlayerData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, options = underlayerOptions, ) val underlayerPdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "with-underlayer.pdf", buffer = underlayerData, ) ``` ### Combine All Options You can apply all PDF settings in one `ExportOptions` value. This example resets the scene to 300 DPI, targets A4 dimensions at 300 DPI (2480 × 3508 px), enables high compatibility, and generates an underlayer. ```kotlin highlight-android-conversion-to-pdf-combined engine.block.setFloat(block = stackedScene, property = "scene/dpi", value = 300F) val combinedOptions = ExportOptions( targetWidth = 2480F, targetHeight = 3508F, exportPdfWithHighCompatibility = true, exportPdfWithUnderlayer = true, underlayerSpotColorName = "BrandUnderlay", underlayerOffset = -2F, ) val configuredData = engine.block.export( block = stackedScene, mimeType = MimeType.PDF, options = combinedOptions, ) val configuredPdf = saveConversionPdf( outputDirectory = outputDirectory, fileName = "configured.pdf", buffer = configuredData, ) ``` Set `targetWidth` and `targetHeight` together. CE.SDK scales the scene to cover the requested dimensions while preserving its aspect ratio. ## Troubleshooting | Symptom | Resolution | | --- | --- | | PDF file size is too large | Reduce the scene DPI or disable `exportPdfWithHighCompatibility` when your target viewers support the original content. Set `pdfImageQuality` below `1.0` to encode the images that CE.SDK still has to rasterize as lossy JPEG. | | Gradients or effects differ between viewers | Enable `exportPdfWithHighCompatibility` so CE.SDK rasterizes complex elements at the scene DPI. | | Underlayer is missing from the printed result | Confirm that `underlayerSpotColorName` matches the print provider's configuration and that the PDF was not flattened. | ## PDF Export Options Pass these fields to `ExportOptions` when calling `engine.block.export()`. | Option | Description | | --- | --- | | `exportPdfWithHighCompatibility` | Rasterize complex elements at the scene DPI. Defaults to `true`. | | `pdfImageQuality` | Encoding quality for images that CE.SDK has to rasterize. Values below the default of `1.0` encode them as lossy JPEG. | | `exportPdfWithUnderlayer` | Generate an underlayer from the design contours. Defaults to `false`. | | `underlayerSpotColorName` | Name of the spot color used for the underlayer ink. | | `underlayerOffset` | Size adjustment in design units. Negative values shrink the underlayer inward. | | `targetWidth` | Target output width in pixels. Set together with `targetHeight`. | | `targetHeight` | Target output height in pixels. Set together with `targetWidth`. | ## API Reference | API | Purpose | | --- | --- | | `engine.scene.createFromImage(imageUri=_)` | Create a scene with one page filled by an image. | | `engine.scene.create(sceneLayout=SceneLayout.VERTICAL_STACK)` | Create an empty scene with vertically stacked pages. | | `engine.scene.getCurrentPage()` | Return the current page. | | `engine.scene.getPages()` | Return all pages in the current scene. | | `engine.block.findByType(type=DesignBlockType.Stack)` | Find the stack that owns the scene's pages. | | `engine.block.create(blockType=DesignBlockType.Page)` | Create a page block. | | `engine.block.appendChild(parent=_, child=_)` | Append a page to the stack. | | `engine.block.createFill(fillType=FillType.Image)` | Create an image fill. | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the source URI of an image fill. | | `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Read the source URI of an image fill. | | `engine.block.setFill(block=_, fill=_)` | Apply the image fill to a page. | | `engine.block.getFill(block=_)` | Return the fill applied to a page. | | `engine.block.supportsFill(block=_)` | Check whether a block supports a fill. | | `engine.block.export(block=_, mimeType=MimeType.PDF, options=_)` | Export a page or scene as a PDF `ByteBuffer`. | | `engine.block.setFloat(block=_, property="scene/dpi", value=_)` | Set the scene DPI. | | `engine.block.getFloat(block=_, property="scene/dpi")` | Return the scene DPI. | | `engine.editor.setSpotColor(name=_, color=_)` | Register the spot color used by an underlayer. | | `engine.editor.getSpotColorRGB(name=_)` | Return the preview color for a registered spot color. | | `ExportOptions(targetWidth=_, targetHeight=_, exportPdfWithHighCompatibility=_, exportPdfWithUnderlayer=_, underlayerSpotColorName=_, underlayerOffset=_)` | Configure compatibility, underlayer, and target dimensions. | ## 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/) — Learn about print workflows, DPI, and color management. - [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) — Define and use spot colors in designs. - [Export Size Limits](https://img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695/) — Check device limits before exporting large designs. - [Convert to PNG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/) — Convert designs to PNG for web and screen. - [Conversion Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) — Explore the full conversion 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. - [Fade Audio In and Out](https://img.ly/docs/cesdk/android/create-audio/audio/fade-a4d7e2/) - Learn how to fade audio in and out in CE.SDK for Android, choose an easing curve, read the fade configuration back, and fade the audio of a video fill. - [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. For a shorter URI-to-block introduction, see [Insert Audio](https://img.ly/docs/cesdk/android/insert-media/audio-c10f10/). Continue here for an end-to-end music workflow that also covers asset queries and multiple tracks. ## 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. ## Add Multiple Music Tracks Create another audio block for each additional music track. The example selects a second catalog result, attaches it to the existing page, and configures its timing and volume independently. ```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 **No catalog results:** Confirm that `ly.img.audio` is registered and that its `content.json` URI is reachable from the app. **Selected music 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: "Fade Audio In and Out" description: "Learn how to fade audio in and out in CE.SDK for Android, choose an easing curve, read the fade configuration back, and fade the audio of a video fill." platform: android url: "https://img.ly/docs/cesdk/android/create-audio/audio/fade-a4d7e2/" --- > 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/) > [Fade In and Out](https://img.ly/docs/cesdk/android/create-audio/audio/fade-a4d7e2/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-audio-audio-fade/Fade.kt reference-only import android.net.Uri import android.util.Log import ly.img.engine.AnimationEasingType 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 = "FadeGuide" suspend fun fadeAudio(engine: Engine): FadeResult { 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 = 15.0) val narration = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = narration) engine.block.setDuration(block = narration, duration = 10.0) engine.block.setUri( block = narration, 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 = narration) engine.block.setVolume(block = narration, volume = 0.8F) engine.block.setAudioFadeIn(block = narration, duration = 2.0) engine.block.setAudioFadeOut(block = narration, duration = 3.0) val backgroundMusic = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = backgroundMusic) engine.block.setDuration(block = backgroundMusic, duration = 15.0) engine.block.setUri( block = backgroundMusic, 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 = backgroundMusic) engine.block.setVolume(block = backgroundMusic, volume = 0.3F) engine.block.setAudioFadeIn( block = backgroundMusic, duration = 4.0, easing = AnimationEasingType.EASE_IN_OUT, ) engine.block.setAudioFadeOut( block = backgroundMusic, duration = 4.0, easing = AnimationEasingType.EASE_OUT, ) 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 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) 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) engine.block.forceLoadAVResource(block = videoFill) // The audio of a video clip lives on its video fill, so the fade goes on the fill // and not on the graphic block that owns it. engine.block.setAudioFadeIn(block = videoFill, duration = 1.0) engine.block.setAudioFadeOut( block = videoFill, duration = 1.5, easing = AnimationEasingType.EASE_OUT, ) val fadeInDuration = engine.block.getDouble(block = narration, property = "playback/fadeIn/duration") val fadeInEasing = engine.block.getEnum(block = narration, property = "playback/fadeIn/easing") val fadeOutDuration = engine.block.getDouble(block = narration, property = "playback/fadeOut/duration") val fadeOutEasing = engine.block.getEnum(block = narration, property = "playback/fadeOut/easing") Log.i(TAG, "Fade in: $fadeInDuration s ($fadeInEasing)") Log.i(TAG, "Fade out: $fadeOutDuration s ($fadeOutEasing)") engine.block.setAudioFadeIn(block = narration, duration = 0.0) val removedFadeInDuration = engine.block.getDouble(block = narration, property = "playback/fadeIn/duration") val musicFadeInDuration = engine.block.getDouble(block = backgroundMusic, property = "playback/fadeIn/duration") val musicFadeInEasing = engine.block.getEnum(block = backgroundMusic, property = "playback/fadeIn/easing") val videoFillFadeInDuration = engine.block.getDouble(block = videoFill, property = "playback/fadeIn/duration") val videoFillFadeOutEasing = engine.block.getEnum(block = videoFill, property = "playback/fadeOut/easing") check(abs(fadeInDuration - 2.0) < 0.001) check(fadeInEasing == AnimationEasingType.LINEAR.key) check(abs(fadeOutDuration - 3.0) < 0.001) check(fadeOutEasing == AnimationEasingType.LINEAR.key) check(abs(musicFadeInDuration - 4.0) < 0.001) check(musicFadeInEasing == AnimationEasingType.EASE_IN_OUT.key) check(abs(videoFillFadeInDuration - 1.0) < 0.001) check(videoFillFadeOutEasing == AnimationEasingType.EASE_OUT.key) check(removedFadeInDuration == 0.0) return FadeResult( fadeInDuration = fadeInDuration, fadeInEasing = fadeInEasing, fadeOutDuration = fadeOutDuration, fadeOutEasing = fadeOutEasing, musicFadeInDuration = musicFadeInDuration, musicFadeInEasing = musicFadeInEasing, videoFillFadeInDuration = videoFillFadeInDuration, videoFillFadeOutEasing = videoFillFadeOutEasing, removedFadeInDuration = removedFadeInDuration, ) } ``` Ramp audio up at the start of a clip and down at the end instead of cutting in and out abruptly. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-audio-fade) An audio fade ramps the volume of a clip between silence and its configured volume over a fixed duration. Use `setAudioFadeIn()` for the start of a clip and `setAudioFadeOut()` for the end. Both take a duration in seconds and an optional easing curve. A duration of `0` — the default — means no fade. This guide covers setting fade-in and fade-out durations, choosing an easing curve, reading the configuration back through block properties, removing a fade, and fading the audio of a video fill. ## Understanding Audio Fades ### How the Fade Gain Is Applied The fade produces a gain between `0.0` and `1.0` that is multiplied with the block's volume. A fade-in on a block at 50% volume therefore ramps from silence to 50%, not to 100%. Fades also combine with page volume, muting, and clip transitions. ### Fades Are Anchored to the Timeline The fade-in window starts at the beginning of the block and the fade-out window ends at the end of the block, both measured against the block's timeline duration. Trimming or resizing a clip keeps the fade-in at the audible start and the fade-out at the audible end. ## Setting Up an Audio Clip Create an audio block, load its file, and give it a duration and a volume. The fade ramps towards this volume rather than towards full scale. ```kotlin highlight-android-create-audio val narration = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = narration) engine.block.setDuration(block = narration, duration = 10.0) engine.block.setUri( block = narration, 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 = narration) engine.block.setVolume(block = narration, volume = 0.8F) ``` `forceLoadAVResource()` makes sure CE.SDK has downloaded the file and read its metadata before you configure playback on the block. ## Fading Audio In Set a fade-in with `setAudioFadeIn()`. The audio ramps up from silence over the given number of seconds at the start of the block. ```kotlin highlight-android-fade-in engine.block.setAudioFadeIn(block = narration, duration = 2.0) ``` Durations are in seconds and accept fractional values. Negative durations and `NaN` are clamped to `0`, which leaves the block without a fade-in instead of failing. ## Fading Audio Out Set a fade-out with `setAudioFadeOut()`. The audio ramps down to silence over the last seconds of the block. ```kotlin highlight-android-fade-out engine.block.setAudioFadeOut(block = narration, duration = 3.0) ``` If the fade-out is longer than the clip, the window is clamped to the block, so the clip already starts partly faded out. Shorten the fade or lengthen the clip when that is not what you want. ## Choosing an Easing Curve Both methods take an optional `AnimationEasingType` as their third argument, defaulting to `AnimationEasingType.LINEAR`. `LINEAR` changes the gain at a constant rate. `EASE_IN`, `EASE_OUT`, and `EASE_IN_OUT` bias the ramp towards the start, the end, or both. The full set of curves is the same one used by block animations. ```kotlin highlight-android-fade-easing val backgroundMusic = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = backgroundMusic) engine.block.setDuration(block = backgroundMusic, duration = 15.0) engine.block.setUri( block = backgroundMusic, 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 = backgroundMusic) engine.block.setVolume(block = backgroundMusic, volume = 0.3F) engine.block.setAudioFadeIn( block = backgroundMusic, duration = 4.0, easing = AnimationEasingType.EASE_IN_OUT, ) engine.block.setAudioFadeOut( block = backgroundMusic, duration = 4.0, easing = AnimationEasingType.EASE_OUT, ) ``` An eased fade-in works well for background music that should swell in gradually under a voiceover, while a linear fade is easier to reason about when you need a predictable ramp. ## Fading Video Audio Video audio lives on the video fill, not on the graphic block that displays it. Resolve the fill with `getFill()`, or reuse the fill you created, and set the fade on it — exactly as with `setVolume()`. ```kotlin highlight-android-video-fill-fade 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) engine.block.forceLoadAVResource(block = videoFill) // The audio of a video clip lives on its video fill, so the fade goes on the fill // and not on the graphic block that owns it. engine.block.setAudioFadeIn(block = videoFill, duration = 1.0) engine.block.setAudioFadeOut( block = videoFill, duration = 1.5, easing = AnimationEasingType.EASE_OUT, ) ``` The fade window still follows the graphic block's position and duration on the timeline. Setting a fade on the graphic block itself throws, because only audio blocks and video fills support audio fades. ## Reading and Removing Fades Fades are exposed as block properties, so `getDouble()` and `getEnum()` read the current configuration back to drive UI controls such as sliders and curve pickers. `getEnum()` returns the easing key, for example `Linear` or `EaseInOut`. | Property | Type | | --------------------------- | ------ | | `playback/fadeIn/duration` | Double | | `playback/fadeIn/easing` | Enum | | `playback/fadeOut/duration` | Double | | `playback/fadeOut/easing` | Enum | ```kotlin highlight-android-read-fade val fadeInDuration = engine.block.getDouble(block = narration, property = "playback/fadeIn/duration") val fadeInEasing = engine.block.getEnum(block = narration, property = "playback/fadeIn/easing") val fadeOutDuration = engine.block.getDouble(block = narration, property = "playback/fadeOut/duration") val fadeOutEasing = engine.block.getEnum(block = narration, property = "playback/fadeOut/easing") Log.i(TAG, "Fade in: $fadeInDuration s ($fadeInEasing)") Log.i(TAG, "Fade out: $fadeOutDuration s ($fadeOutEasing)") ``` Setting a duration of `0` removes a fade again. The easing value stays on the block, so restoring a non-zero duration brings back the same curve. ```kotlin highlight-android-remove-fade engine.block.setAudioFadeIn(block = narration, duration = 0.0) val removedFadeInDuration = engine.block.getDouble(block = narration, property = "playback/fadeIn/duration") ``` ## Fades, Trimming, and Splitting Because the windows are anchored to the block's timeline duration, shortening a clip moves the fade-out along with the new end rather than leaving it stranded inside the clip. Splitting a clip keeps only the outer fades: the first half keeps its fade-in and the second half keeps its fade-out. The audio therefore does not dip at the cut. ## Troubleshooting ### Fade Is Not Audible Check that the block is not muted and that its volume is above `0`. The fade gain is multiplied with the volume, so a muted or silent block stays silent through the whole ramp. ### Setting a Fade Fails Fades apply only to audio blocks and video fills. Pass the video fill, not the graphic block that owns it. ### Fade Disappeared After Splitting This is intentional. Each half keeps only the fade at its outer edge, so playback does not dip at the cut. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.block.setAudioFadeIn(block=_, duration=_, easing=_)` | Block | Ramp audio up at the start of the block | | `engine.block.setAudioFadeOut(block=_, duration=_, easing=_)` | Block | Ramp audio down at the end of the block | | `engine.block.getDouble(block=_, property="playback/fadeIn/duration")` | Block | Read a fade duration | | `engine.block.getEnum(block=_, property="playback/fadeIn/easing")` | Block | Read a fade easing curve | | `engine.block.setVolume(block=_, volume=_)` | Block | Set the volume the fade ramps towards | | `engine.block.forceLoadAVResource(block=_)` | Block | Load the audio resource and its metadata | | `engine.block.create(blockType=_)` | Block | Create an audio or graphic block | | `engine.block.appendChild(parent=_, child=_)` | Block | Attach a block to the page or a track | | `engine.block.setDuration(block=_, duration=_)` | Block | Set the timeline duration the fades are anchored to | | `engine.block.setUri(block=_, property=_, value=_)` | Block | Point a block or fill at a media file | | `engine.block.createFill(fillType=_)` | Block | Create the video fill that carries the audio | | `engine.block.setFill(block=_, fill=_)` | Block | Attach the video fill to a graphic block | ## Next Steps - [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Set the level a fade ramps towards. - [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects. - [Apply Transitions](https://img.ly/docs/cesdk/android/create-video/apply-transitions-146026/) — Cross-fade between clips on a track. - [Split Video and Audio](https://img.ly/docs/cesdk/android/edit-video/split-464167/) — Split clips on the timeline. --- ## 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 androidx.compose.ui.Alignment 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.rememberPlayPause import ly.img.editor.core.component.rememberTimecode import ly.img.editor.core.component.rememberToggleExpanded 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 = { rememberVoiceoverTimeline() } dock = { rememberVoiceoverDock() } inspectorBar = { rememberVoiceoverInspectorBar() } } }, onClose = onClose, ) } // The timeline renders tracks alone by default, so the playback controls are declared here. @Composable private fun rememberVoiceoverTimeline() = Timeline.remember { headerListBuilder = { Timeline.HeaderListBuilder.remember { aligned(alignment = Alignment.Start) { add { Timeline.Label.rememberTimecode() } } aligned(alignment = Alignment.CenterHorizontally) { add { Timeline.Button.rememberPlayPause() } } aligned(alignment = Alignment.End) { add { Timeline.Button.rememberToggleExpanded() } } } } } @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 = { rememberVoiceoverTimeline() } 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. ![Voiceover recorder in the Android editor](./assets/record-voiceover-android.png) ## 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/create-composition/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.net.Uri 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.GlobalScope import ly.img.engine.ShapeType import kotlin.math.roundToInt suspend fun collage(engine: Engine) = withContext(engine.dispatcher) { createAndApplyCollage(engine = engine) } 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 layoutBlocksString = createCollagePage(engine = engine, width = 1080F, height = 1080F).let { layoutPage -> try { 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) engine.block.saveToString(blocks = listOf(layoutPage)) } finally { 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 = withContext(engine.dispatcher) { val previousDestroyScope = engine.editor.getGlobalScope("lifecycle/destroy") engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.ALLOW) 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 layoutBlocksString = createCollagePage(engine = engine, width = 1080F, height = 1080F).let { layoutPage -> try { 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) engine.block.saveToString(blocks = listOf(layoutPage)) } finally { 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 = withContext(engine.dispatcher) { val previousDestroyScope = engine.editor.getGlobalScope("lifecycle/destroy") engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.ALLOW) 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-library/basics-f29078/) - Explore how the asset library connects sources, categories, and dock buttons - [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 a group can be grouped again, which is how a group inside a group is made; a block cannot be grouped with a group it sits inside. ## 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/create-composition/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/create-composition/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. The distinction applies to the canvas preview only. Exports always interpret the spacing in design units, whatever the property is set to. Switching the property never converts the number. A spacing of `40` stays `40` and is reinterpreted in the other space, which changes how far apart the pages appear. ## 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 - [Position and Align](https://img.ly/docs/cesdk/android/create-composition/position-and-align-cc6b6a/) — 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. The distinction applies to the canvas preview only. Exports always interpret the spacing in design units, whatever the property is set to. Switching the property never converts the number. A spacing of `40` stays `40` and is reinterpreted in the other space, which changes how far apart the pages appear. ### 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 - [Export 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: "Positioning and Alignment" description: "Precisely position, align, distribute, and snap objects using CE.SDK's layout APIs." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/position-and-align-cc6b6a/" --- > 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/) > [Position and Align](https://img.ly/docs/cesdk/android/create-composition/position-and-align-cc6b6a/) --- ```kotlin file=@cesdk_android_examples/engine-guides-position-and-align/PositionAndAlign.kt reference-only 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.HorizontalBlockAlignment import ly.img.engine.PositionMode import ly.img.engine.RGBAColor import ly.img.engine.ShapeType import ly.img.engine.VerticalBlockAlignment fun positionAndAlign( 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.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) val primaryBlock = createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.18F, g = 0.38F, b = 0.85F), x = 0F, y = 0F, width = 150F, height = 100F, ) engine.block.setPositionX(primaryBlock, value = 50F) engine.block.setPositionY(primaryBlock, value = 50F) val currentX = engine.block.getPositionX(primaryBlock) val currentY = engine.block.getPositionY(primaryBlock) val percentBlock = createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.04F, g = 0.64F, b = 0.43F), x = 0F, y = 0F, width = 150F, height = 100F, ) engine.block.setPositionXMode(percentBlock, mode = PositionMode.PERCENT) engine.block.setPositionYMode(percentBlock, mode = PositionMode.PERCENT) engine.block.setPositionX(percentBlock, value = 0.5F) engine.block.setPositionY(percentBlock, value = 0.5F) val currentXMode = engine.block.getPositionXMode(percentBlock) val currentYMode = engine.block.getPositionYMode(percentBlock) val alignmentBlocks = listOf( 100F to 100F, 250F to 150F, 180F to 250F, 350F to 200F, ).map { (positionX, positionY) -> createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.18F, g = 0.38F, b = 0.85F), x = positionX, y = positionY, width = 100F, height = 80F, ) } if (engine.block.isAlignable(alignmentBlocks)) { // Blocks can be aligned inside their combined bounding box. } if (engine.block.isAlignable(alignmentBlocks)) { engine.block.alignHorizontally(alignmentBlocks, alignment = HorizontalBlockAlignment.LEFT) engine.block.alignVertically(alignmentBlocks, alignment = VerticalBlockAlignment.TOP) } val headline = createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.84F, g = 0.13F, b = 0.47F), x = 500F, y = 300F, ) if (engine.block.isAlignable(listOf(headline))) { engine.block.alignHorizontally(listOf(headline), alignment = HorizontalBlockAlignment.CENTER) engine.block.alignVertically(listOf(headline), alignment = VerticalBlockAlignment.CENTER) } val horizontalDistributionBlocks = listOf(50F, 180F, 400F, 650F).map { positionX -> createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.96F, g = 0.56F, b = 0.18F), x = positionX, y = 430F, width = 100F, height = 80F, ) } val canDistributeHorizontally = engine.block.isDistributable(horizontalDistributionBlocks) if (canDistributeHorizontally) { // Blocks can be distributed with equal spacing. } if (canDistributeHorizontally) { engine.block.distributeHorizontally(horizontalDistributionBlocks) } val verticalDistributionBlocks = listOf(50F, 150F, 350F, 500F).map { positionY -> createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.84F, g = 0.13F, b = 0.47F), x = 600F, y = positionY, width = 100F, height = 80F, ) } if (engine.block.isDistributable(verticalDistributionBlocks)) { engine.block.distributeVertically(verticalDistributionBlocks) } engine.editor.setSettingFloat(keypath = "positionSnappingThreshold", value = 8F) engine.editor.setSettingFloat(keypath = "rotationSnappingThreshold", value = 0.26F) engine.editor.setSettingColor( keypath = "snappingGuideColor", value = Color.fromRGBA(r = 0.18F, g = 0.38F, b = 0.85F), ) engine.editor.setSettingColor( keypath = "rotationSnappingGuideColor", value = Color.fromRGBA(r = 0.96F, g = 0.56F, b = 0.18F), ) engine.stop() } fun createShapeBlock( engine: Engine, parent: DesignBlock, color: RGBAColor, x: Float, y: Float, width: Float = 120F, height: Float = 120F, ): DesignBlock { val block = engine.block.create(DesignBlockType.Graphic) val fill = engine.block.createFill(FillType.Color) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block, fill = fill) engine.block.setFillSolidColor(block, color = color) engine.block.setWidth(block, value = width) engine.block.setHeight(block, value = height) engine.block.setPositionX(block, value = x) engine.block.setPositionY(block, value = y) engine.block.appendChild(parent = parent, child = block) return block } ``` Position, align, and distribute design elements precisely using CE.SDK's layout APIs and snapping system. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-position-and-align) CE.SDK positions blocks relative to their parent container with the origin at the top left. You can set positions using absolute values in design units or as percentages of the parent's dimensions. For multi-element layouts, alignment and distribution APIs arrange blocks without manual spacing calculations. Snapping settings tune the visual guides shown during interactive editing. This guide covers how to set block positions using different modes, align blocks horizontally and vertically, distribute blocks with even spacing, and configure snapping for interactive editing. ## Setup We start with a scene and a page that we use as the parent for every block created in this guide. ```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) ``` The sample uses this helper to create each colored graphic block while keeping the later snippets focused on layout APIs. ```kotlin highlight-android-create-helper fun createShapeBlock( engine: Engine, parent: DesignBlock, color: RGBAColor, x: Float, y: Float, width: Float = 120F, height: Float = 120F, ): DesignBlock { val block = engine.block.create(DesignBlockType.Graphic) val fill = engine.block.createFill(FillType.Color) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block, fill = fill) engine.block.setFillSolidColor(block, color = color) engine.block.setWidth(block, value = width) engine.block.setHeight(block, value = height) engine.block.setPositionX(block, value = x) engine.block.setPositionY(block, value = y) engine.block.appendChild(parent = parent, child = block) return block } ``` ## Coordinate System CE.SDK uses a coordinate system where the origin (`0`, `0`) is at the top-left corner of the parent container. The X axis extends to the right and the Y axis extends downward. All block positions are relative to the block's parent. ## Setting Block Positions ### Absolute Positioning We can position blocks using absolute coordinates in design units. This is useful when you need precise control over element placement. ```kotlin highlight-android-absolute-position val primaryBlock = createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.18F, g = 0.38F, b = 0.85F), x = 0F, y = 0F, width = 150F, height = 100F, ) engine.block.setPositionX(primaryBlock, value = 50F) engine.block.setPositionY(primaryBlock, value = 50F) val currentX = engine.block.getPositionX(primaryBlock) val currentY = engine.block.getPositionY(primaryBlock) ``` `setPositionX()` and `setPositionY()` set the block's position relative to its parent. Use `getPositionX()` and `getPositionY()` to query the current position. ### Percentage-Based Positioning Positions can also be set as percentages of the parent's dimensions. This approach is useful for layouts that should adapt to different container sizes. ```kotlin highlight-android-percent-position val percentBlock = createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.04F, g = 0.64F, b = 0.43F), x = 0F, y = 0F, width = 150F, height = 100F, ) engine.block.setPositionXMode(percentBlock, mode = PositionMode.PERCENT) engine.block.setPositionYMode(percentBlock, mode = PositionMode.PERCENT) engine.block.setPositionX(percentBlock, value = 0.5F) engine.block.setPositionY(percentBlock, value = 0.5F) val currentXMode = engine.block.getPositionXMode(percentBlock) val currentYMode = engine.block.getPositionYMode(percentBlock) ``` Position modes are set with `setPositionXMode()` and `setPositionYMode()`. When set to `PositionMode.PERCENT`, position values represent a fraction of the parent's size (`0.5F` = 50%). Query the current mode with `getPositionXMode()` and `getPositionYMode()`. The third mode, `PositionMode.AUTO`, lets the engine determine the position automatically. ## Aligning Blocks ### Aligning Multiple Blocks Multiple blocks can be aligned within their combined bounding box. This is useful for creating visually organized layouts. ```kotlin highlight-android-check-alignable val alignmentBlocks = listOf( 100F to 100F, 250F to 150F, 180F to 250F, 350F to 200F, ).map { (positionX, positionY) -> createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.18F, g = 0.38F, b = 0.85F), x = positionX, y = positionY, width = 100F, height = 80F, ) } if (engine.block.isAlignable(alignmentBlocks)) { // Blocks can be aligned inside their combined bounding box. } ``` Before aligning, check whether the blocks can be aligned with `isAlignable()`. ```kotlin highlight-android-align-horizontal if (engine.block.isAlignable(alignmentBlocks)) { engine.block.alignHorizontally(alignmentBlocks, alignment = HorizontalBlockAlignment.LEFT) engine.block.alignVertically(alignmentBlocks, alignment = VerticalBlockAlignment.TOP) } ``` `alignHorizontally()` accepts a list of blocks and a `HorizontalBlockAlignment` value: `LEFT`, `RIGHT`, or `CENTER`. Similarly, `alignVertically()` accepts `VerticalBlockAlignment.TOP`, `BOTTOM`, or `CENTER`. ### Aligning a Single Block to Parent When you pass a single block to the alignment methods, it aligns within its parent container rather than a group bounding box. ```kotlin highlight-android-align-single-block val headline = createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.84F, g = 0.13F, b = 0.47F), x = 500F, y = 300F, ) if (engine.block.isAlignable(listOf(headline))) { engine.block.alignHorizontally(listOf(headline), alignment = HorizontalBlockAlignment.CENTER) engine.block.alignVertically(listOf(headline), alignment = VerticalBlockAlignment.CENTER) } ``` This approach is useful for centering elements on a page or positioning them at specific edges of the container. ## Distributing Blocks Distribution spaces blocks evenly within their bounding box. This is useful for consistent spacing in grid layouts or navigation elements. ```kotlin highlight-android-check-distributable val horizontalDistributionBlocks = listOf(50F, 180F, 400F, 650F).map { positionX -> createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.96F, g = 0.56F, b = 0.18F), x = positionX, y = 430F, width = 100F, height = 80F, ) } val canDistributeHorizontally = engine.block.isDistributable(horizontalDistributionBlocks) if (canDistributeHorizontally) { // Blocks can be distributed with equal spacing. } ``` `isDistributable()` verifies that the blocks can be distributed. ```kotlin highlight-android-distribute-horizontal if (canDistributeHorizontally) { engine.block.distributeHorizontally(horizontalDistributionBlocks) } ``` `distributeHorizontally()` arranges blocks so the horizontal space between them is equal. When more than two blocks are distributed, the leftmost and rightmost blocks remain in place while the blocks between them are repositioned. ```kotlin highlight-android-distribute-vertical val verticalDistributionBlocks = listOf(50F, 150F, 350F, 500F).map { positionY -> createShapeBlock( engine = engine, parent = page, color = Color.fromRGBA(r = 0.84F, g = 0.13F, b = 0.47F), x = 600F, y = positionY, width = 100F, height = 80F, ) } if (engine.block.isDistributable(verticalDistributionBlocks)) { engine.block.distributeVertically(verticalDistributionBlocks) } ``` Similarly, `distributeVertically()` distributes blocks with equal vertical spacing. The topmost and bottommost blocks define the vertical bounds while the blocks between them are repositioned. ## Configuring Snapping Snapping provides visual guides when users drag elements in the editor, helping them align blocks precisely. Configure snapping sensitivity and guide appearance with editor settings. ### Setting Snapping Thresholds ```kotlin highlight-android-snapping-threshold engine.editor.setSettingFloat(keypath = "positionSnappingThreshold", value = 8F) engine.editor.setSettingFloat(keypath = "rotationSnappingThreshold", value = 0.26F) ``` `positionSnappingThreshold` controls how close, in pixels, a block must be to a snap target before snapping activates. `rotationSnappingThreshold` controls rotation snapping sensitivity in radians. ### Customizing Snapping Guide Colors ```kotlin highlight-android-snapping-colors engine.editor.setSettingColor( keypath = "snappingGuideColor", value = Color.fromRGBA(r = 0.18F, g = 0.38F, b = 0.85F), ) engine.editor.setSettingColor( keypath = "rotationSnappingGuideColor", value = Color.fromRGBA(r = 0.96F, g = 0.56F, b = 0.18F), ) ``` `snappingGuideColor` controls position snapping lines, and `rotationSnappingGuideColor` controls rotation guides. ## Troubleshooting ### Position Not Updating If a block's position does not change after calling the setter methods: - Verify the block's transform is not locked with `isTransformLocked()` - Check that the block has the `"layer/move"` scope enabled - Ensure you are using the correct `PositionMode` for your values ### Alignment Not Working If `alignHorizontally()` or `alignVertically()` has no effect: - Confirm `isAlignable()` returns `true` for the blocks - Verify all blocks in the list are valid - Check that blocks have the `"layer/move"` scope enabled - Check that no block is transform-locked, either on its own or by a group or frame above it ### Blocks Cannot Be Distributed If `distributeHorizontally()` or `distributeVertically()` does not work: - Verify `isDistributable()` returns `true` - Confirm each block is valid and all blocks share the same parent - Check that no block is transform-locked, either on its own or by a group or frame above it - Do not enforce a three-block minimum in app code; Android can distribute a two-block set when `isDistributable()` returns `true` ### Snapping Guides Do Not Appear If snapping guides are missing during interactive editing: - Verify the relevant snapping threshold is greater than zero - Check that the editor is in a transform mode where blocks can be dragged - Confirm the guide colors have visible alpha values ## API Reference | Method | Description | |--------|-------------| | `engine.block.getPositionX(block=_)` | Get a block's X position | | `engine.block.getPositionY(block=_)` | Get a block's Y position | | `engine.block.setPositionX(block=_, value=_)` | Set a block's X position | | `engine.block.setPositionY(block=_, value=_)` | Set a block's Y position | | `engine.block.getPositionXMode(block=_)` | Get a block's X position mode | | `engine.block.getPositionYMode(block=_)` | Get a block's Y position mode | | `engine.block.setPositionXMode(block=_, mode=_)` | Set a block's X position mode | | `engine.block.setPositionYMode(block=_, mode=_)` | Set a block's Y position mode | | `engine.block.isAlignable(blocks=_)` | Check if blocks can be aligned | | `engine.block.alignHorizontally(blocks=_, alignment=_)` | Align blocks horizontally | | `engine.block.alignVertically(blocks=_, alignment=_)` | Align blocks vertically | | `engine.block.isDistributable(blocks=_)` | Check if blocks can be distributed | | `engine.block.distributeHorizontally(blocks=_)` | Distribute blocks horizontally with even spacing | | `engine.block.distributeVertically(blocks=_)` | Distribute blocks vertically with even spacing | | `engine.editor.setSettingFloat(keypath="positionSnappingThreshold", value=_)` | Set position snapping sensitivity in pixels | | `engine.editor.setSettingFloat(keypath="rotationSnappingThreshold", value=_)` | Set rotation snapping sensitivity in radians | | `engine.editor.setSettingColor(keypath="snappingGuideColor", value=_)` | Set position snapping guide color | | `engine.editor.setSettingColor(keypath="rotationSnappingGuideColor", value=_)` | Set rotation snapping guide color | ## Next Steps Now that you understand positioning and alignment, explore related layout features: - [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control the stacking order of elements - [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Group related elements together - [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: "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/create-composition/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 - [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 a Local Maven Repository for Offline Builds" description: "Download CE.SDK Android artifacts for a specific version and resolve them from a local Maven repository." platform: android url: "https://img.ly/docs/cesdk/android/create-offline-maven-repository-0d42fd/" --- > 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/) > [Offline Maven Repository](https://img.ly/docs/cesdk/android/create-offline-maven-repository-0d42fd/) --- Mirror all IMG.LY Android artifacts for a specific CE.SDK version and configure Gradle to resolve them from a local Maven repository. CE.SDK Android artifacts flowing from the IMG.LY Maven repository into a local offline repository > **Reading time:** 5 minutes > > **Resources:** > > - [Download examples](/docs/cesdk/downloads/download-maven-version.sh) The download script uses the public IMG.LY Maven repository and preserves its directory structure. It does not require Artifactory access or credentials. > **Note:** The script mirrors artifacts published by IMG.LY. A fully offline Android > build also needs local access to Gradle, plugins, and transitive dependencies > from repositories such as Google Maven and Maven Central. ## Requirements Prepare the repository on a machine with: - Network access to `https://maven.img.ly` - Bash - `curl` - `jq` - The CE.SDK version used by your Android project ## Download the Script Use the download action above, or download the script from the command line: ```bash curl --remote-name https://img.ly/docs/cesdk/downloads/download-maven-version.sh chmod +x download-maven-version.sh ``` You can inspect the matching files without downloading them: ```bash ./download-maven-version.sh --dry-run $UBQ_VERSION$ ``` ## Download IMG.LY Artifacts Run the script with the CE.SDK version and the output directory: ```bash ./download-maven-version.sh $UBQ_VERSION$ ./maven-offline ``` The script downloads every file in an exact `$UBQ_VERSION$` version directory and keeps the Maven layout. The generated local repository starts at `maven-offline/maven` and contains paths such as `ly/img/editor-core/$UBQ_VERSION$/`. ## Transfer the Repository Copy the complete `maven-offline` directory to the target environment. Keep its directory structure unchanged. You can place it inside the Android project or at another stable filesystem location available to Gradle. ## Configure Gradle Register the `maven` subdirectory inside `dependencyResolutionManagement.repositories` in your project's settings file. ```text title="settings.gradle.kts" dependencyResolutionManagement { repositories { maven { name = "imgly-offline" url = uri(rootDir.resolve("maven-offline/maven")) mavenContent { includeGroup("ly.img") } } google() mavenCentral() } } ``` ```groovy title="settings.gradle" dependencyResolutionManagement { repositories { maven { name = 'imgly-offline' url = new File(rootDir, 'maven-offline/maven').toURI() mavenContent { includeGroup 'ly.img' } } google() mavenCentral() } } ``` Adjust the filesystem path if the repository is outside the project. `mavenContent` restricts this repository to the `ly.img` group so Gradle continues to resolve other groups from their configured repositories. ## Verify Offline Resolution Run a representative build with Gradle's offline mode enabled: ```bash ./gradlew --offline assembleDebug ``` The build now resolves CE.SDK artifacts from the local repository. If Gradle reports a missing artifact from another group, add that artifact to the appropriate third-party repository mirror or cache. ## Troubleshooting ### No Artifacts Found Confirm that the requested CE.SDK version exists in the IMG.LY Maven repository and exactly matches the version declared in your Gradle dependencies. ### curl or jq Is Missing Install the missing command on the connected preparation machine, then run the script again. Both commands are required to browse the repository and download files safely. ### Gradle Still Requests Network Access The local repository contains IMG.LY artifacts only. Mirror or cache Gradle, Android Gradle Plugin dependencies, and all transitive dependencies from Google Maven, Maven Central, and any other repositories used by 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: "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 Template](https://img.ly/docs/cesdk/android/use-templates/generate-334e15/) - Generate finished designs from templates with the CE.SDK Engine API for Android by loading, populating variables, and exporting to images and PDFs. - [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. - [Automate Design Generation](https://img.ly/docs/cesdk/android/automation/design-generation-98a99e/) - Generate on-brand designs programmatically using templates, variables, and CE.SDK’s headless API. --- ## 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. ![Generated Android form-based editing result showing updated workshop text and a replacement hero image](./assets/android.hero.png) > **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 - [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: "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. ![Image placeholder with an overlay and Replace button in the Android editor](./assets/android.hero.webp) > **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. - [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 - [Automate Design Generation](https://img.ly/docs/cesdk/android/automation/design-generation-98a99e/) — Generate on-brand designs programmatically using templates, variables, and CE.SDK’s headless API. - [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 reusable templates programmatically with the Engine API. - [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-library/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", ".imgly") withContext(Dispatchers.IO) { updatedTemplateArchiveFile.outputStream().use { output -> val archiveBuffer = updatedTemplateArchive.asReadOnlyBuffer() while (archiveBuffer.hasRemaining()) { output.channel.write(archiveBuffer) } } } val archivedTemplateScene = engine.scene.load( sceneUri = 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. ![Android output preview of the refreshed template](./assets/android.hero.webp) > **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.load`. ```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", ".imgly") withContext(Dispatchers.IO) { updatedTemplateArchiveFile.outputStream().use { output -> val archiveBuffer = updatedTemplateArchive.asReadOnlyBuffer() while (archiveBuffer.hasRemaining()) { output.channel.write(archiveBuffer) } } } val archivedTemplateScene = engine.scene.load( sceneUri = 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 URIs; `load(sceneUri = ...)` opens scene and archive files alike. **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.load(scene=_, waitForResources=_)` | Load serialized template scene content before editing it. | | `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a stored scene or archive template from a local, content, or remote URI. | | `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. ![Create Templates From Scratch](./assets/android.hero.webp) > **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. - [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 - [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", ".imgly") 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.load( sceneUri = 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.load(sceneUri = ...)` — the same call that loads scene files, since the engine detects the content automatically. Archives bundle the scene with its assets, which makes them portable and suitable for offline or self-hosted template delivery; they use the `.imgly` extension (the `.zip` extension also works). ```kotlin highlight-android-load-from-archive engine.scene.load( sceneUri = 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 inputs on `engine.scene.load(...)`; `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.load(sceneUri=_, waitForResources=_)` | Load a scene or archive from a URI (content detected automatically) | | `engine.scene.loadArchive(archiveUri=_, waitForResources=_)` | Load a scene archive from a 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 - [From Scene File](https://img.ly/docs/cesdk/android/create-templates/import/from-scene-file-52a01e/) — Load and import design templates from scene files in CE.SDK - [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", ".imgly") 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.load( sceneUri = 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. ![Android preview of the applied business card template imported from a scene file](./assets/android-template-preview.png) > **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", ".imgly") 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.load( sceneUri = 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 scene archive from a URI. | | `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a scene or archive from a local or remote URI (content detected automatically). | | `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. | ## Next Steps - [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) — Build reusable design templates programmatically using CE.SDK's APIs - [Apply a Template](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) — Apply template scenes via API while preserving page dimensions - [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) — Save design progress locally or to a backend service for later editing 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: "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 - [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 - [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. - [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/) - Let users create, import, edit, and style synchronized captions in the CE.SDK Android video editor using the captions dock button, the captions sheet, and style presets. - [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` ## 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.net.Uri 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( 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.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 { previousGlobalScopes.forEach { (scope, globalScope) -> engine.editor.setGlobalScope(key = scope, globalScope = globalScope) } } } ``` 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 ly.img.engine.VideoBitrate 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( // VideoBitrate.Auto derives a bounded bitrate from the resolution/framerate, // consistent across platforms. Pass VideoBitrate.Custom(bitsPerSecond) instead // for an explicit bitrate. videoBitrate = VideoBitrate.Auto, 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. It also sets `VideoBitrate.Auto`, a bounded bitrate derived from the resolution and frame rate that stays consistent across platforms. ```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( // VideoBitrate.Auto derives a bounded bitrate from the resolution/framerate, // consistent across platforms. Pass VideoBitrate.Custom(bitsPerSecond) instead // for an explicit bitrate. videoBitrate = VideoBitrate.Auto, 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. --- ## 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. ![Add Watermark example showing an image with text and logo watermarks](./assets/android.hero.webp) > **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. ![Shape annotations on Android](./assets/annotation-android.png) > **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: "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. ![Background Removal action in the Android Photo Editor dock](./assets/android.hero.png) > **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. ![Replace Colors example showing color replacement and background removal](./assets/android.hero.webp) > **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. - [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 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. ![Crop tool appears when an image is selected](../mobile-assets/crop-tool.png) ### 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. ![An image that has been scale cropped and rotated slightly showing the cropped and original image.](../mobile-assets/ui-crop-workflow.png) 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. ![Image with no additional crop applied shown in crop mode](../mobile-assets/crop-example-1.png) 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. ![Image crop translated one quarter of it's width to the right](../mobile-assets/crop-example-5.png) ```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. ![Image crop scaled by 1.5 with no corresponding translation adjustment](../mobile-assets/crop-example-6.png) 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**. ![Image crop rotated by pi/4 or 45 degrees](../mobile-assets/crop-example-7.png) ```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. ![Image cropped using the scale ratio to remain centered](../mobile-assets/crop-example-2.png) 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. ![Image cropped and rotated](../mobile-assets/crop-example-3.png) ```kotlin import kotlin.math.PI engine.block.setCropScaleRatio(imageBlock, 2.0f) engine.block.setCropRotation(imageBlock, (PI / 3.0).toFloat()) ``` ![Image rotated first and then scaled](../mobile-assets/crop-example-4.png) ```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. ![Image crop flipped vertically](../mobile-assets/crop-example-8.png) ```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. ## Next Steps - Explore other [image transformations](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) such as rotate, resize, scale, and flip. - Customize the [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) or [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) to add or replace the crop button or provide a custom preset toolbar. - Constrain who can crop using scopes and template rules to [lock a template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/). --- ## 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. ![Android resize guide result showing two image blocks resized and grouped on a page](./assets/android.hero.webp) > **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. ![Rotation handle of the control gizmo](../mobile-assets/rotation-handle.png) ### 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) ``` ![Original image and scaled image](../mobile-assets/scale-example-1.png) 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) ``` ![Original image placed over the scaled image, aligned on the center anchor point](../mobile-assets/scale-example-2.png) ## 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. ![Allowing the engine to scale the image as you adjust the width of the block](../mobile-assets/scale-example-3.png) ```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. ![Using crop scale for the horizontal axis and adjusting the width of the block](../mobile-assets/scale-example-4.png) ```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. ![Using crop scale for the horizontal axis and using the maintainCrop property when changing the width](../mobile-assets/scale-example-5.png) ```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. To let users author captions in the editor UI instead — a dock button, the captions sheet, style presets, and a timeline caption lane — see [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/). ## 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 - [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/) — Let users create, import, edit, and style captions in the editor UI - [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: "Edit Captions" description: "Let users create, import, edit, and style synchronized captions in the CE.SDK Android video editor using the captions dock button, the captions sheet, and style presets." platform: android url: "https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/" --- > 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/) > [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/) --- ```kotlin file=@cesdk_android_examples/editor-guides-create-video-edit-captions/EditCaptionsSolution.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.onCreate import ly.img.editor.configuration.video.callback.onLoadAssetSources import ly.img.editor.core.component.Dock import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberAudiosLibrary import ly.img.editor.core.component.rememberCaptionStyle import ly.img.editor.core.component.rememberCaptions import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberEditCaptions import ly.img.editor.core.component.rememberFormatText import ly.img.editor.core.component.rememberSplit import ly.img.editor.core.component.rememberSystemGallery import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost. @Composable fun EditCaptionsSolution( license: String, onClose: (Throwable?) -> Unit, ) { // The Video Editor Starter Kit registers the caption preset source, the Captions dock button and both // caption inspector buttons, so captions are available without any configuration of your own. Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) { onCreate = { // Demo scaffolding — not part of the lesson: open a sample clip so the canvas shows // footage behind the captions sheet. onCreate( createScene = { editorContext.engine.scene.createFromVideo( videoUri = editorContext.baseUri .buildUpon() .appendPath("ly.img.video") .appendPath("videos") .appendPath("pexels-kampus-production-8154913.mp4") .build(), ) }, ) } } }, onClose = onClose, ) } /** * The three registrations captions need, for a video editor configuration that does not already carry them. * * Compiled against the starter kit so the sample builds, but written for a configuration without captions — * adding a button the list already holds would render it twice. */ @Composable private fun CustomVideoCaptionsSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) { onCreate = { onCreate( loadAssetSources = { onLoadAssetSources() editorContext.engine.asset.addLocalSourceFromJSON( contentUri = editorContext.baseUri .buildUpon() .appendPath("ly.img.caption.presets") .appendPath("content.json") .build(), ) }, ) } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberCaptions() } add { Dock.Button.rememberAudiosLibrary() } } } } } inspectorBar = { InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberEditCaptions() } add { InspectorBar.Button.rememberCaptionStyle() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberSplit() } add { InspectorBar.Button.rememberDelete() } } } } } } }, onClose = onClose, ) } ``` Let users add synchronized captions to their videos inside the editor. A dock button opens the captions sheet for creating, importing, and editing captions, two inspector bar buttons expose caption editing and style presets, and every caption appears as a clip on a dedicated timeline lane. ![The Android video editor with the Add Captions sheet open over a video clip](./assets/android.hero.webp) > **Reading time:** 9 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-create-video-edit-captions) Captions are an editor UI feature for video scenes. Three registrations turn the surface on — a dock button, an asset source, and two inspector bar buttons — and the editor supplies the captions sheet, the timeline caption lane, and preset styling. To build captions programmatically instead, see [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/). The [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/)'s `VideoConfigurationBuilder` already carries all three, so a video editor built on it has captions without any configuration of your own. The sections below cover each registration for a configuration that doesn't, and describe the surface those registrations produce. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers editor setup as a whole. ## Understanding Caption Structure Captions use the same block hierarchy as other video content: a page holds a single caption track, and the track holds one caption block per cue. Each caption stores its own text, time offset, and duration, while styling — font, colors, background, position — synchronizes across every caption on the track, so the subtitles always read as one consistent set. You never manage this hierarchy from the UI: the editor creates the caption track with the first caption and removes it with the last one. ## Adding the Captions Dock Button `Dock.Button.rememberCaptions()` is what opens the captions sheet, so a dock without it leaves the feature unreachable. Include it in the dock's list: ```kotlin highlight-android-captions-dock Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberCaptions() } add { Dock.Button.rememberAudiosLibrary() } } } } ``` The button renders a captions icon with a localized "Captions" label and sends `EditorEvent.Sheet.Open(SheetType.Captions())`. It carries no visibility condition of its own — put it in the docks where captions apply. If you are adding captions to an existing list rather than declaring a new one, `existingListBuilder.modify { addLast { … } }` appends it; check the list doesn't already hold it, because a repeated id renders the button twice. ## Registering the Caption Style Presets Caption styling comes from the `ly.img.caption.presets` asset source. The starter kit loads it with its other sources; a configuration that builds its own list has to include it, or the Style button stays hidden and new captions are created unstyled: ```kotlin highlight-android-caption-presets-source onLoadAssetSources() editorContext.engine.asset.addLocalSourceFromJSON( contentUri = editorContext.baseUri .buildUpon() .appendPath("ly.img.caption.presets") .appendPath("content.json") .build(), ) ``` `editorContext.baseUri` points at the asset root the editor is already configured with, so the same code works against the CDN and against self-hosted assets. To ship your own caption styles, see [Update Caption Presets](https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/). ## Adding the Caption Inspector Buttons Selecting a caption — tap its clip on the timeline — shows the inspector bar. Add the two caption buttons to its list: ```kotlin highlight-android-captions-inspector-bar InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberEditCaptions() } add { InspectorBar.Button.rememberCaptionStyle() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberSplit() } add { InspectorBar.Button.rememberDelete() } } } } ``` - `InspectorBar.Button.rememberEditCaptions()` reopens the captions sheet with the selected caption scrolled into view. It hides itself when the caption's `text/edit` scope is denied — a caption is a text block, and that scope governs the whole sheet, so denying it takes caption editing, deleting, retiming, and importing away together. - `InspectorBar.Button.rememberCaptionStyle()` opens the caption style presets. It hides itself while `ly.img.caption.presets` is unregistered. Both buttons appear only while the selected block is a caption. Of the video configuration's own buttons, Duplicate, Layer, and Animations hide themselves for a caption, while Format, Fill & Stroke, Text Background, Split, and Delete stay available. Move as Clip isn't in that list, but it hides for captions too if you add it yourself. The [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) guide lists the full button set and which selections each one renders for. Selecting a caption on the canvas opens the [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) as usual, minus what a caption cannot do: Duplicate, because the copy would land outside the caption track, and the layer moves, because captions always render above the rest of the page. Delete and the remaining entries stay. **Split** applies to captions too, cutting the selected caption at the playhead into two captions that together cover exactly the range the original occupied; the new caption inherits the original's style. The cut snaps to the nearest word gap — the position just after a space — so the trailing caption never starts with one. Text with no space to snap to, such as a single long word or an unspaced script, falls back to the nearest character boundary and is cut mid-word. Because the cut lands on the playhead, the button is enabled only while the playhead sits more than 0.1 seconds inside the caption at both ends and the caption holds at least two characters. Captions are exempt from the timeline's one-second minimum clip duration and never raise its "Please move the selected clip under the playhead." or "Can't split because one of the clips would become too short." messages — a split that can't be made simply does nothing. ## Using the Captions Sheet The sheet's state follows the caption count. With no captions it is titled **Add Captions** and offers the creation actions; as soon as one caption exists it becomes **Edit Captions**, a scrollable list of caption cards. Reopening the sheet later goes straight to the list, and opening it while a caption is selected on the canvas scrolls that row into view. The sheet is sized by its content and grows to full editor height as captions are added and while the keyboard is open. ### Add Captions - **Create Manually** adds a styled three-second caption at the start of the page timeline, moves the playhead to it, and opens it for editing. - **Import File** opens the system document picker to choose an SRT or VTT subtitle file. - **Generate Automatically** appears as the primary action only when the [Auto Captions](https://img.ly/docs/cesdk/android/user-interface/ai-integration/auto-captions-73368c/) plugin is installed — the editor looks for it at runtime and leaves the action out otherwise. It stays disabled while the page has no audio or video to transcribe. While generation runs, the three actions are replaced by a **Generating Captions** state with a **Cancel** button. The title stays **Add Captions**, because nothing is created until the transcription returns, which is what makes **Cancel** leave the scene untouched. Generation belongs to the editor rather than the sheet, so closing the sheet to look at the timeline does not throw the transcription away — reopening finds it still running. These three actions belong to the empty state only. Once captions exist the sheet shows the list, so re-importing over an existing set means deleting the captions first. ### Editing a Caption Tap a caption card to edit it. The whole card responds, not just the text, and one tap places the caret and raises the keyboard — there is no separate select-then-edit step. The same tap rings the card in the accent color, and unless the caption is already the canvas selection it also selects it, pauses playback, and moves the playhead to that caption so the canvas previews it. Entering a caption parks the caret at the end of its text. A card whose swipe actions stand open is the exception: the first tap closes the swipe. Tapping a different card moves editing there in a single tap and saves the caption you were editing. Text reaches the scene when editing ends or just before a structural action runs, never per keystroke, so typing does not fill the undo history. ### The Keyboard Action Bar While you edit a caption, an action bar floats directly above the keyboard: - A red trash button, **Delete caption**, removes the caption and moves editing to the one below it, with the caret at the start of its text. On the last caption editing falls back to the caption above instead, with the caret at the end of its text. Deleting the only caption ends editing and returns the sheet to its **Add Captions** state. - An overflow button, **More**, opens the structural operations: **Merge with previous**, **Split at cursor**, and **Add new below**. **Merge with previous** is omitted on the first caption. - Up and down arrows, **Previous Caption** and **Next Caption**, move editing to the caption above or below and save the one you were editing. They are disabled on the first and last caption respectively. - **Done** ends editing, dismisses the keyboard, and clears the accent ring. It does not close the sheet. Only **Done** and the menu items carry visible text; the trash, the overflow, and the arrows are icons that announce those labels to screen readers. The whole bar is disabled while a caption is being created, imported, or generated. **Split at cursor** moves the text after the caret into a new caption below, which opens for editing with the caret at the cut, and divides the original duration between the two in proportion to the character split. Because entering a caption parks the caret at the end, tapping **Split at cursor** immediately after tapping into a caption appends a new empty caption below instead of dividing one. ### Return and Backspace Return and Backspace are structural inside a caption field. Return never inserts a line break: - Return with the caret inside the text splits the caption there, exactly as **Split at cursor** does — the tail opens for editing with the caret at the cut, so typing carries on where you left off. - Return with the caret at the end of the text, including in an empty caption, adds a new empty caption below and moves editing to it. - Return with the caret at the very start of a caption that already has text does nothing. No line break is typed and no caption is created above. - Backspace with the caret at the very start of a caption that has text merges it into the caption above, joining the two texts with a single space and leaving the caret at the join. On the first caption there is nothing above, so the key does nothing. - Backspace in an empty caption deletes it and continues editing at the end of the caption above. An empty first caption has nothing above it, so it is deleted and editing ends. - Backspace anywhere else in the text, or over a selection, deletes characters as usual. Only a typed Return is structural: it is recognized as one when the change adds exactly one line break immediately behind the caret. A multi-line paste therefore goes in verbatim, and an input method confirming a composition — CJK input, dictation, autocorrect — is handled normally, so a caption can still hold more than one line. A card shows at most three lines of text. Backspace is read from key events only, because a delete at the start of a field is indistinguishable from a caret move in the text a soft keyboard reports. An input method that does not send `KEYCODE_DEL` therefore leaves merge and delete to the action bar and the row's swipe actions, which reach the same operations. A split is refused silently when it would leave either side empty, when the cut would fall inside a single user-perceived character such as an emoji, or when either half would come out shorter than a tenth of a second. That last floor guards every split, not just the inspector button that can grey itself out — a caret split near the end of a short caption is refused rather than nudged, so the pair always tiles exactly the range the original occupied. The caption is left unchanged. ### Swipe Actions Swipe a caption card aside to reveal **Merge with previous**, **Add new below**, and **Delete caption** as icons, with those labels carried as content descriptions. Full swipe is off, so a hurried gesture cannot destroy a caption outright — an action runs only when its icon is tapped, and only one row can stand open at a time. **Merge with previous** is omitted on the first caption. Swipe actions are withdrawn entirely while a caption is being edited or while another caption operation is still running; the keyboard bar is the way in. Screen readers reach the same three operations as custom actions on each card, so they work without the gesture. ### The List Footer At the end of the list, **Add New Caption** appends a caption after the last one and opens it for editing. The red **Delete All Captions** asks first: tapping it dismisses the keyboard — saving what you typed — then raises a **Delete all captions?** dialog reading "Every caption will be removed from the video." **Delete All** removes every caption and returns the sheet to its **Add Captions** state. **Cancel** dismisses the dialog with every caption intact. Each caption operation — create, split, merge, delete, delete all, import, apply a preset — commits a single undo step, so one undo reverts the whole operation. ## Importing Subtitle Files **Import File** opens the Storage Access Framework picker, so no storage permission is involved. The picker's filter is deliberately wide, because stock Android has no `.vtt` MIME mapping and cloud providers report subtitle files as plain text; the format is decided after the pick, from the file extension, then the MIME type, then a `WEBVTT` signature in the file's bytes, defaulting to SubRip. Only SubRip (`.srt`) and WebVTT (`.vtt`) are recognized, and anything else is rejected by the parser rather than at pick time. Caption timing comes from the file's cues. An import replaces the page's caption track wholesale and lands as a single undo step. The imported captions are styled with the default preset rather than the look of the captions they replace — an import brings in a whole new track, so there is no style of its own to carry over. An import cannot be cancelled, so its outcome is binary — every cue imported, or nothing changed. Failures leave the scene exactly as it was and name the cause in a **Couldn't import captions** dialog: | Message | Cause | | --- | --- | | "No captions were found in the file." | The file parsed but held no cues. | | "This file format isn't supported. Import an SRT or VTT file." | The file is neither SubRip nor WebVTT. | | "The file couldn't be read. It appears to be damaged or incomplete." | The bytes are truncated or corrupt. | | "The file couldn't be read. Please try again." | The file could not be opened or copied. | Anything the engine rejects for another reason falls back to the engine's own message under the same title. Separately, a device with no app able to serve the picker shows a "No app available to complete this action." toast instead of opening it. ## The Timeline Caption Lane Each caption appears as its own clip on a dedicated caption lane, showing the caption's text. The lane is the topmost track in the timeline — it starts directly below the ruler, above every other track — and it scrolls with the other tracks instead of staying pinned in place. It appears with the first caption and collapses once the last caption is removed. - Trimming a caption clip changes its timing without moving its neighbors — the edges clamp against the previous and next caption instead of pushing them, so silences elsewhere on the lane survive. - A caption clip can be trimmed down to 0.1 seconds, where every other clip type stops at one second. - Dragging a caption slides it along the lane to a new start time, clamped into the gap its neighbors leave. A drop into a gap too small for the caption is refused rather than shortening it. - Captions stay on their lane: they can't move into other tracks, and no other clip can be dropped onto the lane or into the space above it — so while a scene has captions, dragging a clip to the very top no longer opens a new track. - Selecting a caption clip scrolls the timeline vertically until the lane is visible, so a caption above the visible tracks is revealed rather than silently selected off screen. The timeline opens scrolled to its last track, so the lane usually needs that scroll or a manual one to come into view. - Captions never stretch the timeline's ruler past the footage, and they don't appear in the reorder sheet. ## Styling Captions The **Style** inspector button opens the caption style presets, fed by the `ly.img.caption.presets` asset source. Tapping one applies it to the selected caption and the engine syncs the style to every caption on the track, keeping the caption's own size and placement where it has one. The applied preset stays marked: its asset id is recorded on the caption track, so the grid still shows which one is in use after a scene round-trip, including for tracks authored on another platform. The first caption on an empty track, and every imported set, starts from **Outline** (`ly.img.caption.presets.outline`). Every later caption is added as a blank copy of its neighbor, so it carries that neighbor's style without touching the rest of the track. The other inspector styling tools behave the same way: Format (font, size, alignment, spacing), Fill & Stroke, and Text Background each change every caption on the track, keeping the subtitles consistent. Because a caption's text properties always apply to the whole block, Format works on the caption as a unit — there is no per-selection formatting inside a caption, and list styles are unavailable. Text and timing remain per-caption. Animations are the exception, and the inspector hides them for a caption: the engine animates a caption block but does not sync animations across the track, so choosing one would animate a single subtitle while the rest stayed still. A style preset that carries animations still applies them to every caption, because there the engine writes each sibling itself. Captions render like any other design block, so exported videos include them burned in. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builder=_)` | Creates the editor configuration that hosts the captions surface. | | `Dock.Button.rememberCaptions()` | Dock button that opens the captions sheet. | | `existingListBuilder.modify { addLast { … } }` | Appends the captions button to the configuration's dock list. | | `InspectorBar.remember(builder=_)` | Rebuilds the inspector bar so caption buttons are constructed in its item scope. | | `InspectorBar.Button.rememberEditCaptions()` | Inspector bar button that reopens the captions sheet; visible for caption selections. | | `InspectorBar.Button.rememberCaptionStyle()` | Inspector bar button that opens the caption style presets; visible for caption selections while `ly.img.caption.presets` is registered. | | `existingListBuilder.modify { addFirst { … } }` | Prepends the caption buttons to the configuration's inspector bar list. | | `engine.asset.addLocalSourceFromJSON(contentUri=_)` | Registers the `ly.img.caption.presets` asset source that feeds the style presets. | ## Next Steps - [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) — Create, import, style, and export caption blocks with the Engine APIs - [Auto Captions](https://img.ly/docs/cesdk/android/user-interface/ai-integration/auto-captions-73368c/) — Generate captions from speech with the Auto Captions plugin - [Update Caption Presets](https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/) — Extend the caption style presets with custom styles using content.json updates - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — Configure the dock area to show or hide tools, panels, or quick access actions - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) — Customize the inspector bar for editing properties like position, color, 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: "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, ) ``` ![Force Trim constraints in the timeline](./assets/force-trim-android.png) ## 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.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 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(engine: Engine): JoinAndArrangeVideoResult = 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 = 1920F) engine.block.setHeight(page, value = 1080F) engine.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 = engine, name = "Clip A", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipB = createVideoClip( engine = engine, name = "Clip B", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipC = createVideoClip( engine = engine, name = "Clip C", videoUri = videoUri, width = 1920F, height = 1080F, ) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.setBoolean( block = track, property = "track/automaticallyManageBlockOffsets", value = false, ) engine.block.appendChild(parent = track, child = clipA) engine.block.appendChild(parent = track, child = clipB) engine.block.appendChild(parent = track, child = clipC) engine.block.fillParent(track) val initialTrackChildren = engine.block.getChildren(track) check(initialTrackChildren == listOf(clipA, clipB, clipC)) engine.block.setDuration(clipA, duration = 5.0) engine.block.setDuration(clipB, duration = 5.0) engine.block.setDuration(clipC, duration = 5.0) engine.block.setDuration(track, duration = 15.0) engine.block.setTimeOffset(clipA, offset = 0.0) engine.block.setTimeOffset(clipB, offset = 5.0) engine.block.setTimeOffset(clipC, offset = 10.0) val initialTrackDuration = engine.block.getDuration(track) check(initialTrackDuration == 15.0) val initialClipStates = engine.block.getChildren(track).map { clip -> TrackClipState( name = engine.block.getName(clip), timeOffset = engine.block.getTimeOffset(clip), duration = engine.block.getDuration(clip), ) } engine.block.insertChild(parent = track, child = clipC, index = 0) engine.block.setTimeOffset(clipC, offset = 0.0) engine.block.setTimeOffset(clipA, offset = 5.0) engine.block.setTimeOffset(clipB, offset = 10.0) val reorderedTrackDuration = engine.block.getDuration(track) check(reorderedTrackDuration == 15.0) val reorderedClipStates = engine.block.getChildren(track).map { clip -> TrackClipState( name = engine.block.getName(clip), timeOffset = engine.block.getTimeOffset(clip), duration = engine.block.getDuration(clip), ) } val finalClipOrder = engine.block.getChildren(track).map { clip -> engine.block.getName(clip) } val finalClipOffsets = engine.block.getChildren(track).map { clip -> engine.block.getTimeOffset(clip) } check(finalClipOrder == listOf("Clip C", "Clip A", "Clip B")) check(finalClipOffsets == listOf(0.0, 5.0, 10.0)) val overlayTrack = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = overlayTrack) engine.block.setTimeOffset(overlayTrack, offset = 2.0) val overlayClip = createVideoClip( engine = engine, name = "Overlay Clip", videoUri = videoUri, width = 1920F / 4F, height = 1080F / 4F, ) engine.block.setDuration(overlayClip, duration = 5.0) engine.block.appendChild(parent = overlayTrack, child = overlayClip) engine.block.setPositionX(overlayClip, value = 1920F - 1920F / 4F - 40F) engine.block.setPositionY(overlayClip, value = 1080F - 1080F / 4F - 40F) JoinAndArrangeVideoResult( initialTrackClips = initialClipStates, reorderedTrackClips = reorderedClipStates, pageDuration = engine.block.getDuration(page), mainTrackDuration = reorderedTrackDuration, overlayTrackOffset = engine.block.getTimeOffset(overlayTrack), overlayTrackDuration = engine.block.getDuration(overlayTrack), overlayClipCount = engine.block.getChildren(overlayTrack).size, ) } private suspend fun createVideoClip( engine: Engine, name: String, videoUri: Uri, width: Float, height: Float, ): DesignBlock { val clip = engine.block.create(DesignBlockType.Graphic) var videoFill: DesignBlock? = null try { 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 fill = engine.block.createFill(FillType.Video) videoFill = fill // The Android binding has no typed property helper for video fill URIs yet. engine.block.setUri(block = fill, property = "fill/video/fileURI", value = videoUri) engine.block.setFill(block = clip, fill = fill) engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER) engine.block.forceLoadAVResource(block = fill) return clip } catch (error: Throwable) { runCatching { if (engine.block.isValid(clip)) engine.block.destroy(clip) } videoFill?.let { fill -> runCatching { if (engine.block.isValid(fill)) engine.block.destroy(fill) } } throw error } } ``` 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 = 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 = 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) var videoFill: DesignBlock? = null try { 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 fill = engine.block.createFill(FillType.Video) videoFill = fill // The Android binding has no typed property helper for video fill URIs yet. engine.block.setUri(block = fill, property = "fill/video/fileURI", value = videoUri) engine.block.setFill(block = clip, fill = fill) engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER) engine.block.forceLoadAVResource(block = fill) return clip } catch (error: Throwable) { runCatching { if (engine.block.isValid(clip)) engine.block.destroy(clip) } videoFill?.let { fill -> runCatching { if (engine.block.isValid(fill)) engine.block.destroy(fill) } } throw error } } ``` 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 = engine, name = "Clip A", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipB = createVideoClip( engine = engine, name = "Clip B", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipC = createVideoClip( engine = engine, 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 = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.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 engine.block.appendChild(parent = track, child = clipA) engine.block.appendChild(parent = track, child = clipB) engine.block.appendChild(parent = track, child = clipC) engine.block.fillParent(track) val initialTrackChildren = engine.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 engine.block.setDuration(clipA, duration = 5.0) engine.block.setDuration(clipB, duration = 5.0) engine.block.setDuration(clipC, duration = 5.0) engine.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 engine.block.setTimeOffset(clipA, offset = 0.0) engine.block.setTimeOffset(clipB, offset = 5.0) engine.block.setTimeOffset(clipC, offset = 10.0) val initialTrackDuration = engine.block.getDuration(track) check(initialTrackDuration == 15.0) val initialClipStates = engine.block.getChildren(track).map { clip -> TrackClipState( name = engine.block.getName(clip), timeOffset = engine.block.getTimeOffset(clip), duration = engine.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 engine.block.insertChild(parent = track, child = clipC, index = 0) engine.block.setTimeOffset(clipC, offset = 0.0) engine.block.setTimeOffset(clipA, offset = 5.0) engine.block.setTimeOffset(clipB, offset = 10.0) val reorderedTrackDuration = engine.block.getDuration(track) check(reorderedTrackDuration == 15.0) val reorderedClipStates = engine.block.getChildren(track).map { clip -> TrackClipState( name = engine.block.getName(clip), timeOffset = engine.block.getTimeOffset(clip), duration = engine.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 = engine.block.getChildren(track).map { clip -> engine.block.getName(clip) } val finalClipOffsets = engine.block.getChildren(track).map { clip -> engine.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 = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = overlayTrack) engine.block.setTimeOffset(overlayTrack, offset = 2.0) val overlayClip = createVideoClip( engine = engine, name = "Overlay Clip", videoUri = videoUri, width = 1920F / 4F, height = 1080F / 4F, ) engine.block.setDuration(overlayClip, duration = 5.0) engine.block.appendChild(parent = overlayTrack, child = overlayClip) engine.block.setPositionX(overlayClip, value = 1920F - 1920F / 4F - 40F) engine.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 @file:Suppress("ktlint:standard:filename") import android.net.Uri 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 suspend fun editVideoProgrammatically(engine: Engine): ProgrammaticVideoEditResult = exportProgrammaticVideoEdit(engine) suspend fun exportProgrammaticVideoEdit(engine: Engine): ProgrammaticVideoEditResult = withContext(engine.dispatcher) { buildProgrammaticVideoEdit(engine) } 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 a named mode via the `VideoBitrate` constructor overload: `VideoBitrate.System` (default) and `VideoBitrate.Auto`, which on Android resolve to the same bounded, resolution-aware formula. The raw sentinels `0` and `-1` remain valid. | | `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. A lock applies down the hierarchy, so a child of a locked group cannot be transformed either. Each block keeps its own flag, so unlocking the group leaves a child that was locked on its own still locked. A page is the exception: locking a page holds the page and not what sits on it. `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. ![Crop tools appear when the crop button is tapped in the editor](../mobile-assets/crop-tool.png) ### 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. ![Video with no additional crop applied shown in crop mode](../mobile-assets/crop-example-1.png) 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. ![Video crop translated one quarter of its width to the right](../mobile-assets/crop-example-5.png) ```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. ![Video crop scaled by 1.5 with no corresponding translation adjustment](../mobile-assets/crop-example-6.png) 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. ![Video crop rotated by pi/4 or 45 degrees](../mobile-assets/crop-example-7.png) ```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. ![Video cropped using the scale ratio to remain centered](../mobile-assets/crop-example-2.png) 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. ![Video cropped and rotated](../mobile-assets/crop-example-3.png) ```kotlin import kotlin.math.PI engine.block.setCropScaleRatio(videoBlock, 2.0f) engine.block.setCropRotation(videoBlock, (PI / 3.0).toFloat()) ``` ![Video rotated first and then scaled](../mobile-assets/crop-example-4.png) ```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. ![Image crop flipped vertically](../mobile-assets/crop-example-8.png) ```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. ![Video rotation handle showing video can be rotated](../mobile-assets/rotate-example-1.png) ### 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) ``` ![Video rotated by 45 degrees](../mobile-assets/rotate-example-2.png) > **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) ``` ![Original video and scaled video](../mobile-assets/scale-example-1.png) 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) ``` ![Original video placed over the scaled video, aligned on the center anchor point](../mobile-assets/scale-example-2.png) *** ## 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. ![Allowing the engine to scale the video as you adjust the width of the block](../mobile-assets/scale-example-3.png) ```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. ![Using crop scale for the horizontal axis and adjusting the width of the block](../mobile-assets/scale-example-4.png) ```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. ![Using crop scale for the horizontal axis and using the maintainCrop property when changing the width](../mobile-assets/scale-example-5.png) ```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](#broken-link-n1234a) - 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: "Equal-Distance Snapping" description: "Snap a dragged block to equal spacing with its siblings and show the size of each gap." platform: android url: "https://img.ly/docs/cesdk/android/equal-distance-snapping-e9d15a/" --- > 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). --- Turn on equal-distance snapping so a dragged block lands at an equal distance from its siblings, and a badge shows the size of each gap. > **Reading time:** 4 minutes While the user drags a block, the engine looks for sibling blocks that already sit at an equal distance from each other. When the dragged block comes close to a position that continues or splits that spacing, the engine pulls the block onto it. It then draws a line across each gap, a tick at both ends of the line, and a badge with the size of the gap. The engine draws these indicators, so Android, iOS and Web all show the same thing. There is no Android-only composable to add. ## Turn the Feature On Equal-distance snapping is off by default. Turn it on per engine instance with the `features/equalDistanceSnappingEnabled` setting. ```kotlin engine.editor.setSettingBoolean( keypath = "features/equalDistanceSnappingEnabled", value = true, ) ``` Call this on the main thread, like every other engine call. A good place is the `onCreate` callback of your editor configuration, before the scene loads. ```kotlin fun DesignConfigurationBuilder.onPreCreateScene() { editorContext.engine.editor.setSettingBoolean( keypath = "features/equalDistanceSnappingEnabled", value = true, ) } ``` The setting takes effect on the next drag. You can turn it off again at any time. ## Understand When the Snap Fires The engine measures gaps along one axis and needs the blocks to overlap across that axis. Two blocks in the same row form one horizontal gap. Two blocks in the same column form one vertical gap. The dragged block must overlap each block of the chain across that axis, and each pair of neighbours must overlap each other. Both axes are checked on every drag, so a block can snap on X and on Y at the same time. From one chain of equal gaps, the engine offers three positions: - **After the chain.** The dragged block continues the spacing on the right, or below. - **Before the chain.** The dragged block continues the spacing on the left, or above. - **Inside a single gap.** The dragged block splits one gap into two equal halves. This needs the block to fit in the gap. Only blocks on the same page take part. A block that overlaps its neighbour has no gap with it, so the chain stops there. ## Control the Pull Distance `positionSnappingThreshold` sets how close the drag must come before the block is pulled. Edge and center snapping share the same value. It is in screen pixels, so the pull feels the same at any zoom. ```kotlin engine.editor.setSettingFloat( keypath = "positionSnappingThreshold", value = 4f, ) ``` ## Style the Indicators `snappingGuideColor` sets the color of the measurement line, of the ticks, and of the badge background. `handleFillColor` sets the color of the label inside the badge, so pick a color with a strong contrast against the background. ```kotlin engine.editor.setSettingColor( keypath = "snappingGuideColor", value = RGBAColor(r = 1f, g = 0.004f, b = 0.361f, a = 1f), ) ``` The badge measures its own label, so it always fits the number. Every gap of a chain shows the same number: the spacing the snap holds, in design units, to one decimal. A badge wider than its gap moves above or beside the line. ## Combine With Other Snapping Equal-distance snapping runs first on each axis. When an equal-distance position is in reach, it takes that axis, even when an edge or center guide sits nearer. The engine uses the edge and center guides only when no equal-distance position is in reach. The badge appears only on an axis that equal distance won. While an equal-distance snap holds the block, the engine does not round the position to the pixel grid. Rounding would break the equality that the badges show. --- ## 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 Counting" description: "Learn which operations count as an export in CE.SDK, when export events are recorded, and what data they contain." platform: android url: "https://img.ly/docs/cesdk/android/export-counting-613923/" --- > 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/) > [Export Counting](https://img.ly/docs/cesdk/android/export-counting-613923/) --- Some CE.SDK plans use usage-based pricing that meters exports. This page defines exactly which operations count as an export, when an export is recorded, and which data is collected along with it. For an overview of licensing options, see [Licensing](https://img.ly/docs/cesdk/android/licensing-8aa063/). For a broader look at data collection and privacy, see [Security](https://img.ly/docs/cesdk/android/security-777bfd/). ## What Counts as an Export CE.SDK records one export event per call to an export API. This applies to: - Exporting a block to an image format such as PNG, JPEG, WebP or TGA - Exporting to PDF or SVG - Exporting raw pixel data through the export API - Exporting with a color mask - Exporting a video Each call produces exactly one event, regardless of the content being exported. The engine doesn't distinguish between a "final" export and any other use of the export API. For example, if you call the export API to generate a thumbnail of a design programmatically, that call counts as an export like any other. ## What Doesn't Count The following operations never record an export event: - **Live rendering in the editor.** The canvas preview your users see while editing isn't an export. - **Saving scenes.** Saving a scene to a string or archive—including autosaves and drafts—isn't an export. Only the export APIs count. - **Built-in thumbnail APIs.** The engine's thumbnail generation APIs, such as video and audio thumbnail sequences and the page thumbnails shown in the editor UI, don't go through the export pipeline and aren't counted. - **Audio-only exports.** Exporting audio doesn't currently record an export event. ## When an Export Is Counted The moment an export is recorded differs between still and video exports: - **Images and PDFs** are counted after the export completes successfully. A failed export—for example due to an invalid block or an encoding error—isn't counted. - **Videos** are counted when encoding starts. A video export that fails or is canceled during encoding still counts. An export is counted when the export API call executes—not when a file is downloaded, uploaded or attached to a business event. There is no built-in option to defer counting to a later point such as a checkout. If you want exports to align with a business event, structure your integration so the export API is only called at that point. ## Multi-Page Documents - **PDF:** Exporting multiple pages in a single call produces one PDF file and one export event. The event includes the number of pages exported and the dimensions of the largest page. - **Image formats:** Each page requires its own export call, so exporting a multi-page document as images produces one event per page. ## Multiple Formats Each export call is counted separately. Exporting the same design once as a PDF and once as a PNG produces two export events, one per format. ## Development, Staging and Production CE.SDK doesn't distinguish between environments. A license key can be used across development, staging and production, and exports are counted the same way in all of them. ## Server-Side Exports and Renderer The same counting rules apply on every platform. Exports performed with the Node.js SDK or other server-side integrations are counted under the same definition as client-side exports. The [CE.SDK Renderer](#broken-link-7f3e9a) uses the same export counting and additionally sends periodic heartbeats to track the number of active instances, as described in the [Security](https://img.ly/docs/cesdk/android/security-777bfd/) documentation. ## What Data Is Collected An export event contains only technical metadata—never your content. Image export events include the media type, format, resolution and page count. Video export events include the media type, format, resolution, frame rate and duration. Events are associated with the user ID (if provided by your integration), device ID and session ID described in the [Security](https://img.ly/docs/cesdk/android/security-777bfd/) documentation. The user ID is transmitted exactly as your integration provides it and is used solely to deduplicate users when counting monthly active users. It doesn't need to be a real identifier: if you want to keep your internal user IDs private, pass a hashed or otherwise opaque value instead—deduplication works just as well, as long as the value is unique and stable per user. Export events are only sent when tracking is enabled for your license. Enterprise licenses with offline validation can opt out of tracking entirely; [contact our sales team](https://img.ly/forms/contact-sales) to explore these options. ## Export Counts and Billing Export events are the technical metering primitive. How those counts map to your bill—which tiers apply and how usage is aggregated—is defined by your plan and contract, not by the SDK. If you have questions about how exports are billed under your agreement, [contact our sales team](https://img.ly/forms/contact-sales). --- ## 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 produced by the export API. For multi-frame scrubber previews, page storyboards, and audio waveforms, see [Thumbnail Previews](https://img.ly/docs/cesdk/android/export-save-publish/thumbnail-previews-dcf031/), which streams each frame and sample chunk as it becomes ready. ## 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/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. - [Thumbnail Previews](https://img.ly/docs/cesdk/android/export-save-publish/thumbnail-previews-dcf031/) - Stream timeline filmstrips, page storyboards, and audio waveforms on Android with the CE.SDK thumbnail sequence APIs. - [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: "For Audio Processing" description: "Learn how to export audio in WAV or MP4 format from any block type in CE.SDK for Android." platform: android url: "https://img.ly/docs/cesdk/android/export-save-publish/export/audio-68de25/" --- > 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 Audio Processing](https://img.ly/docs/cesdk/android/export-save-publish/export/audio-68de25/) --- Export audio from pages, video blocks, audio blocks, and tracks to WAV or MP4 format for external processing, transcription, or analysis. The `exportAudio` API allows you to extract audio from any block that contains audio content. This is particularly useful when integrating with external audio processing services like speech-to-text transcription, audio enhancement, or music analysis platforms. Audio can be exported from multiple block types: - **Page blocks** - Export the complete mixed audio timeline - **Video blocks** - Extract audio tracks from videos - **Audio blocks** - Export standalone audio content - **Track blocks** - Export audio from specific timeline tracks ## Export Audio Export audio from any block using the `exportAudio` API: ```kotlin val page = engine.scene.getCurrentPage() val audioData = engine.block.exportAudio( page, MimeType.AUDIO_WAV, 48000, 2 ) Log.d("AudioExport", "Exported ${audioData.size} bytes") ``` ### Export Options Configure your audio export with these parameters: - **`mimeType`** - `MimeType.AUDIO_WAV` (uncompressed) or `MimeType.AUDIO_MP4` (compressed AAC) - **`sampleRate`** - Audio quality in Hz (default: 48000) - **`numberOfChannels`** - 1 for mono or 2 for stereo - **`timeOffset`** - Start time in seconds (default: 0f) - **`duration`** - Length to export in seconds (0f = entire duration) - **`onProgress`** - Callback receiving `(rendered, encoded, total)` for progress tracking ## Find Audio Sources To find blocks with audio in your scene: ```kotlin // Find audio blocks val audioBlocks = engine.block.findByType(BlockType.Audio) // Find video fills with audio val videoFills = engine.block.findByType(BlockType.VideoFill) val videosWithAudio = videoFills.filter { block -> try { engine.block.getAudioInfoFromVideo(block).isNotEmpty() } catch (e: Exception) { false } } ``` ## Working with Multi-Track Video Audio Videos can contain multiple audio tracks (e.g., different languages). CE.SDK provides APIs to inspect and extract specific tracks. ### Check audio track count ```kotlin val videoFillId = engine.block.findByType(BlockType.VideoFill).first() val trackCount = engine.block.getAudioTrackCountFromVideo(videoFillId) Log.d("AudioExport", "Video has $trackCount audio track(s)") ``` ### Get track information ```kotlin val audioTracks = engine.block.getAudioInfoFromVideo(videoFillId) audioTracks.forEachIndexed { index, track -> Log.d("AudioExport", """ Track $index: - Channels: ${track.channels} // 1=mono, 2=stereo - Sample Rate: ${track.sampleRate} Hz - Language: ${track.language ?: "unknown"} - Label: ${track.label ?: "Track $index"} """.trimIndent()) } ``` ### Extract a specific track ```kotlin // Create audio block from track 0 (first track) val audioBlockId = engine.block.createAudioFromVideo(videoFillId, 0) // Export just this track's audio val trackAudioData = engine.block.exportAudio( audioBlockId, MimeType.AUDIO_WAV, 48000, 2 ) ``` ### Extract all tracks ```kotlin // Create audio blocks for all tracks val audioBlockIds = engine.block.createAudiosFromVideo(videoFillId) // Export each track audioBlockIds.forEachIndexed { i, audioBlockId -> val trackData = engine.block.exportAudio(audioBlockId, MimeType.AUDIO_WAV) Log.d("AudioExport", "Track $i: ${trackData.size} bytes") } ``` ## Complete Workflow: Audio to Captions A common workflow is to export audio, send it to a transcription service, and use the returned captions in your scene. ### Step 1: Export Audio ```kotlin val page = engine.scene.getCurrentPage() val audioData = engine.block.exportAudio( page, MimeType.AUDIO_WAV, 48000, 2 ) ``` ### Step 2: Send to Transcription Service Send the audio to a service that returns SubRip (SRT) format captions: Provide `transcriptionEndpoint` and `transcriptionAuthorizationHeader` from your app's configuration. Point the endpoint at an app-owned backend or proxy, and keep third-party service credentials out of the Android client. ```kotlin suspend fun transcribeAudio( audioData: ByteArray, transcriptionEndpoint: String, authorizationHeader: String, ): String = withContext(Dispatchers.IO) { val client = OkHttpClient() val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart( "audio", "audio.wav", audioData.toRequestBody("audio/wav".toMediaType()) ) .addFormDataPart("format", "srt") .build() val request = Request.Builder() .url(transcriptionEndpoint) .addHeader("Authorization", authorizationHeader) .post(requestBody) .build() val response = client.newCall(request).execute() response.body?.string() ?: throw Exception("Empty response") } val srtContent = transcribeAudio( audioData = audioData, transcriptionEndpoint = transcriptionEndpoint, authorizationHeader = transcriptionAuthorizationHeader, ) ``` ### Step 3: Import Captions from SRT Use the built-in API to create caption blocks from the SRT response: ```kotlin import java.io.File // Save SRT to temporary file val tempFile = File.createTempFile("captions", ".srt") tempFile.writeText(srtContent) // Import captions from file URI val uri = tempFile.toURI().toString() val captions = engine.block.createCaptionsFromURI(uri) // Clean up temporary file tempFile.delete() // Add captions to page val page = engine.scene.getCurrentPage() val captionTrack = engine.block.create(DesignBlockType.CaptionTrack) captions.forEach { caption -> engine.block.appendChild(captionTrack, caption) } engine.block.appendChild(page, captionTrack) // Center the first caption as a reference point engine.block.alignHorizontally(listOf(captions[0]), HorizontalBlockAlignment.Center) engine.block.alignVertically(listOf(captions[0]), VerticalBlockAlignment.Center) ``` ### Other Processing Services Audio export also supports these workflows: - **Audio enhancement** - Noise removal, normalization - **Music analysis** - Tempo, key, beat detection - **Language detection** - Identify spoken language - **Speaker diarization** - Identify who spoke when ## Next Steps - [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. - [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) — Add synchronized captions to video scenes and import subtitle files. - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) — Play, pause, seek, and preview audio and video content 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: "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 | | PDF | `pdfImageQuality` | 0.0–1.0 | 1.0 = lossless, lower = lossy JPEG for rasterized images | 1.0 | | 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/). --- ## 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. | For the complete format compatibility matrix, including codecs and size limits, see [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/). ## 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. - `pdfImageQuality` controls the encoding quality of images that CE.SDK has to rasterize during PDF export; values below the default of `1.0` encode them as lossy JPEG. - `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 - [To PNG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/) — Export your designs as PNG images with transparency support and configurable compression for web graphics, UI elements, and content requiring crisp edges. - [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. - [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. - [To Raw Data](https://img.ly/docs/cesdk/android/export-save-publish/export/to-raw-data-abd7da/) — Export designs to uncompressed RGBA pixel data for custom image processing, GPU uploads, and advanced graphics workflows. - [With a Color Mask](https://img.ly/docs/cesdk/android/export-save-publish/export/with-color-mask-4f868f/) — Learn how to export design blocks with color masking to remove specific colors and generate alpha masks for print workflows and compositing. - [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. - [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. - [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: "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. ![Android partial export preview showing the exported page with a rectangle, ellipse, and star.](./assets/android-partial-export-preview.png) > **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 - [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. - [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: "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 - [Partial Export](https://img.ly/docs/cesdk/android/export-save-publish/export/partial-export-89aaf6/) - Export specific blocks, groups, and page elements instead of entire scenes using CE.SDK's programmatic export API. - [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 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/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-export-to-pdf/ExportToPdf.kt reference-only import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope 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.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 exportToPdf(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) // Report per-page progress as the PDF is written. The callback runs once // per page; only PDF exports invoke it. val progressData = engine.block.export( block = scene, mimeType = MimeType.PDF, progressCallback = { progress -> println("Exported ${progress.exportedPages} of ${progress.totalPages} pages") }, ) val progressPdf = writePdfExport(fileName = "design-with-progress.pdf", buffer = progressData) // Cancelling the coroutine that runs the export stops the export itself. // Keep the job in your view model and cancel it from your Cancel button. coroutineScope { val exportJob = launch { try { engine.block.export(block = scene, mimeType = MimeType.PDF) } catch (cancellation: CancellationException) { // A cancelled export produces no data. println("Export cancelled") throw cancellation } } exportJob.cancel() } 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, progressPdf, 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. ![Rendered PDF export preview](./assets/pdf-export-android-preview.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-export-to-pdf) 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, controlling the quality of rasterized images, 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." } } } ``` ## Track Export Progress Large multi-page PDFs take time to write. Pass a `progressCallback` to `engine.block.export()` to report how far the export has advanced. ```kotlin highlight-android-progress // Report per-page progress as the PDF is written. The callback runs once // per page; only PDF exports invoke it. val progressData = engine.block.export( block = scene, mimeType = MimeType.PDF, progressCallback = { progress -> println("Exported ${progress.exportedPages} of ${progress.totalPages} pages") }, ) val progressPdf = writePdfExport(fileName = "design-with-progress.pdf", buffer = progressData) ``` The callback runs once after each page is serialized into the document. It receives an `ExportPdfProgress` with `exportedPages` and `totalPages`. It is PDF-specific: raster exports like PNG or JPEG never invoke it. The callback runs on the engine's thread, so dispatch to the main thread before updating UI. ## Cancel a Running Export `export` is a suspending function and follows coroutine cancellation. Cancel the job that runs it, and the export stops. The call throws a `CancellationException` and returns no data. ```kotlin highlight-android-cancel // Cancelling the coroutine that runs the export stops the export itself. // Keep the job in your view model and cancel it from your Cancel button. coroutineScope { val exportJob = launch { try { engine.block.export(block = scene, mimeType = MimeType.PDF) } catch (cancellation: CancellationException) { // A cancelled export produces no data. println("Export cancelled") throw cancellation } } exportJob.cancel() } ``` Keep the job in your view model rather than discarding it, because cancelling it is the only way to stop the export. For a multi-page PDF the engine stops at the next page boundary, so the pages that are still queued are never rendered. Every other export finishes its current work before the result is dropped, because there is no boundary to stop at. ## 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. ## Control the Size of Rasterized Images When `exportPdfWithHighCompatibility` is `false`, CE.SDK embeds the original data of unmodified JPEG images directly into the PDF. This keeps exports of photo-heavy documents such as photo books fast and small, because the photos are not decoded and encoded again. CE.SDK must rasterize images that it cannot embed directly, for example images with effects or blurs applied, or every bitmap image when high compatibility mode is enabled. By default these images are encoded losslessly. Set `pdfImageQuality` on `ExportOptions` to a value below `1.0` to encode them as lossy JPEG instead, which produces much smaller files. ```kotlin val imageQualityOptions = ExportOptions( exportPdfWithHighCompatibility = false, pdfImageQuality = 0.85F, ) val pdfData = engine.block.export( block = page, mimeType = MimeType.PDF, options = imageQualityOptions, ) ``` Valid values are greater than `0` and at most `1.0`. The default of `1.0` keeps the lossless encoding, in the same way as `webpQuality`. Images that are embedded as their original JPEG data are never encoded again, so this option does not change them. ## 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. ## Add Printer's Marks A print shop needs to see where to cut, and a press operator needs a target to align the plates against. Enable `exportPdfWithCropMarks` for the four pairs of corner lines that mark the cut, and `exportPdfWithRegistrationMarks` for a bullseye at the middle of each page edge. Both default to `false`, so an export that does not ask for marks is unchanged. ```kotlin val printMarkOptions = ExportOptions( exportPdfWithCropMarks = true, exportPdfWithRegistrationMarks = true, printMarkOffset = 3.0F, cropMarkLength = 5.0F, printMarkWidth = 0.25F, ) val pdfData = engine.block.export( block = page, mimeType = MimeType.PDF, options = printMarkOptions, ) ``` Both mark types are painted in the PDF registration colorant `All`, so they appear on every separation, and each is knocked out in white underneath so it stays legible over dark artwork. `printMarkOffset` and `cropMarkLength` are in design units, so the offset is directly comparable with the page bleed and the sheet grows by the sum of the two. `printMarkWidth` is in points, the unit a print shop states a stroke weight in. A negative offset falls back to the 6 pt default, and `0` places a mark on the trim edge. The offset is measured from the trim, so a small offset places a mark inside the bleed, which the knife removes anyway. Marks grow the exported page by the offset plus the equivalent of 15 points on each side, or by the offset plus `cropMarkLength` when that is longer. The artwork does not move, and the TrimBox and BleedBox keep describing the same physical rectangles. A page whose trim is not a rectangle gets no marks and no larger page. ## 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`. | | `pdfImageQuality` | Encoding quality for images that CE.SDK has to rasterize. Values below the default of `1.0` encode them as lossy JPEG. | | `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`. | | `exportPdfWithCropMarks` | Draw the four pairs of corner lines that show a print shop where to cut. Defaults to `false`. | | `exportPdfWithRegistrationMarks` | Draw a bullseye target at the middle of each page edge, which a press operator aligns the plates by. Defaults to `false`. | | `printMarkOffset` | Distance in design units from the trim to the nearest edge of any mark, so it is comparable with the page bleed. Defaults to 6 pt. | | `printMarkWidth` | Stroke weight in points of any mark, the unit a print shop states a weight in. Defaults to `0.25`. | | `cropMarkLength` | Length in design units of one crop mark line, the same unit as `printMarkOffset`. Defaults to 15 pt. | | `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=_, progressCallback=_)` | Export a block as PDF with format and compatibility options, optionally reporting per-page progress through `progressCallback`. | | `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 - [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 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/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [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 - [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. - [With a Color Mask](https://img.ly/docs/cesdk/android/export-save-publish/export/with-color-mask-4f868f/) - Learn how to export design blocks with color masking to remove specific colors and generate alpha masks for print workflows and compositing. - [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: "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 - [To PNG](https://img.ly/docs/cesdk/android/export-save-publish/export/to-png-f87eaf/) — Export your designs as PNG images with transparency support and configurable compression for web graphics, UI elements, and content requiring crisp edges. - [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. - [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) — Vector export for print and document workflows - [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: "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. ![Export with a color mask showing the Android sample image and mask outputs](./assets/android.hero.webp) > **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. - [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: "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. ## Exclusion Areas in a Print Export An exclusion area marks a region of a page that content must stay out of, such as an envelope window or a book spine. An exclusion area is authoring geometry, so it is left out of an export and its guide colors never reach the file. Set `exclusionArea/punchOut` to `true` on an exclusion area to cut it out of the export instead. The page and everything on it get a hole where the exclusion area is, so a die cut window in the design becomes a window in the exported PDF. The hole is transparent, which a print workflow reads as an absence of ink rather than as white. It is off by default. See [Exclusion Areas](https://img.ly/docs/cesdk/android/concepts/exclusion-areas-c2b87b/) for how to create and place an exclusion area. ## 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" } } ``` In this mode CE.SDK embeds unmodified JPEG images with their original data instead of rasterizing them, which strongly reduces export time and file size for photo-heavy documents such as photo books. Print jobs usually keep the default `pdfImageQuality` of `1.0`, which encodes the images that still have to be rasterized losslessly. Lower the value only when a smaller file matters more than print fidelity. ## 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(pdfImageQuality=_)` | Encoding quality for rasterized images; values below `1.0` use lossy JPEG. Defaults to `1.0`. | | `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 - [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. - [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](https://img.ly/docs/cesdk/android/export-save-publish/export/compress-29105e/) — Tune format-specific compression settings to reduce export file sizes. - [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: "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.SaveToArchiveOptions 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.imgly") val sceneArchiveFile = File(outputDir, "spring-campaign-archive.imgly") 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 compressedSceneArchive = engine.scene.saveToArchive( scene = scene, options = SaveToArchiveOptions( compression = CompressionOptions( format = CompressionFormat.ZSTD, level = CompressionLevel.DEFAULT, ), ), ) check(compressedSceneArchive.hasRemaining()) 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.load( sceneUri = 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. Persist saved files of either format with the `.imgly` extension. The `.scene` and `.zip` extensions also load, and the same `engine.scene.load(sceneUri=_)` call opens either kind. ## 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.imgly") val sceneArchiveFile = File(outputDir, "spring-campaign-archive.imgly") 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 Saved scenes are compressed with Zstd by default, which makes them much smaller and speeds up both saving and loading. Pass a format explicitly to change the level, or to turn compression off. ```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.ZSTD` - Zstd compression (default) - `CompressionFormat.NONE` - No compression **Compression Levels:** - `CompressionLevel.FASTEST` - Fastest compression, larger output - `CompressionLevel.DEFAULT` - Balanced speed and size - `CompressionLevel.BEST` - Best compression, slower An archive can compress its scene the same way. Bundled images, video and fonts are stored as they are, because they already are compressed formats. ```kotlin highlight-android-archive-compression val compressedSceneArchive = engine.scene.saveToArchive( scene = scene, options = SaveToArchiveOptions( compression = CompressionOptions( format = CompressionFormat.ZSTD, level = CompressionLevel.DEFAULT, ), ), ) ``` 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.load()` with a local file `Uri` to restore a self-contained archive — the same call that loads scene files, since the engine detects the content automatically. ```kotlin highlight-android-load-archive engine.scene.load( sceneUri = 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 or archive from a local or remote URI (content detected automatically) | | `engine.scene.loadArchive(archiveUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene archive from a 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 - [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: "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/v$UBQ_VERSION$/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 either kind with `engine.scene.load()`: pass the scene string directly, or write the archive to a URI first and pass that. ```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 and reloads it with `engine.scene.load()`: pass strings from `engine.scene.saveToString()` directly, and write archives from `engine.scene.saveToArchive()` to a URI first. 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 archive from a URI | | `engine.block.findByType(type=DesignBlockType.Graphic)` | Find blocks of a specific type after loading | ## Next Steps - [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) — Save editable scenes and blocks as strings or self-contained archives. - [Load Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) — Load scenes from remote URLs, serialized strings, and archives. - [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Understand the block types and hierarchy that custom metadata attaches to. --- ## 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: "Thumbnail Previews" description: "Stream timeline filmstrips, page storyboards, and audio waveforms on Android with the CE.SDK thumbnail sequence APIs." platform: android url: "https://img.ly/docs/cesdk/android/export-save-publish/thumbnail-previews-dcf031/" --- > 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/) > [Thumbnail Previews](https://img.ly/docs/cesdk/android/export-save-publish/thumbnail-previews-dcf031/) --- ```kotlin file=@cesdk_android_examples/engine-guides-thumbnail-previews/ThumbnailPreviews.kt reference-only import android.content.res.Resources import android.graphics.Bitmap import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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.VideoThumbnailResult import kotlin.math.roundToInt /** * The engine renders thumbnails at most this large on each side. Larger requests are * bilinear-upscaled from that render, so they cost more memory without adding detail. */ private const val MAX_THUMBNAIL_RENDER_SIZE = 512 data class Waveform( val left: List, val right: List, val chunkCount: Int, ) data class ThumbnailPreviews( val filmstrip: List, val storyboard: List, val previewFrame: Bitmap, val waveform: Waveform, val framesBeforeCancel: Int, ) suspend fun thumbnailPreviews( engine: Engine, assetBaseUri: Uri, ): ThumbnailPreviews { val videoUri = assetBaseUri.buildUpon() .appendPath("ly.img.video") .appendPath("videos") .appendPath("pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4") .build() val audioUri = assetBaseUri.buildUpon() .appendPath("ly.img.audio") .appendPath("audios") .appendPath("far_from_home.m4a") .build() 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 videoTrack = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = videoTrack) val clip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(clip, value = 1280F) engine.block.setHeight(clip, value = 720F) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(clip, fill = videoFill) engine.block.appendChild(parent = videoTrack, child = clip) val audioTrack = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = audioTrack) val audioClip = engine.block.create(DesignBlockType.Audio) engine.block.setUri( block = audioClip, property = "audio/fileURI", value = audioUri, ) engine.block.appendChild(parent = audioTrack, child = audioClip) engine.block.setDuration(clip, duration = 10.0) engine.block.setDuration(audioClip, duration = 10.0) // Both APIs wait for the resource on their own, but loading it up front avoids a first // request that returns before the media is decodable. engine.block.forceLoadAVResource(videoFill) engine.block.forceLoadAVResource(audioClip) // Video fills are sampled in media time, so measure the source, not the clip. val sourceDuration = engine.block.getAVResourceTotalDuration(videoFill) return ThumbnailPreviews( filmstrip = generateFilmstrip(engine = engine, videoFill = videoFill, sourceDuration = sourceDuration), storyboard = generateStoryboard(engine = engine, page = page), previewFrame = capturePreviewFrame(engine = engine, page = page, time = 4.0), waveform = generateWaveform(engine = engine, block = audioClip, duration = 10.0), framesBeforeCancel = cancelFilmstripEarly(engine = engine, videoFill = videoFill, sourceDuration = sourceDuration), ) } // thumbnailHeight is measured in pixels, not dp, so convert your layout height first. // Clamping to the engine's render cap keeps the request at a size that adds real detail. fun thumbnailHeightPx(dp: Float): Int { val density = Resources.getSystem().displayMetrics.density return (dp * density).roundToInt().coerceIn(1, MAX_THUMBNAIL_RENDER_SIZE) } suspend fun generateFilmstrip( engine: Engine, videoFill: DesignBlock, sourceDuration: Double, numberOfFrames: Int = 8, ): List { // Frames are not guaranteed to arrive in index order, so key them by the reported index. val frames = sortedMapOf() engine.block.generateVideoThumbnailSequence( block = videoFill, thumbnailHeight = thumbnailHeightPx(dp = 48F), timeBegin = 0.0, timeEnd = sourceDuration, numberOfFrames = numberOfFrames, ).collect { result -> frames[result.frameIndex] = result } // Collection has to happen on the main thread, and the collector must not suspend: // the callback channel is bounded, so a slow collector drops frames. Decode after. return withContext(Dispatchers.Default) { frames.values.map(::videoThumbnailToBitmap) } } fun videoThumbnailToBitmap(result: VideoThumbnailResult): Bitmap { val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888) bitmap.copyPixelsFromBuffer(result.imageData) // copyPixelsFromBuffer leaves the buffer position at its limit. Rewind it, or converting // the same result again throws "Buffer not large enough for pixels". result.imageData.rewind() return bitmap } suspend fun generateStoryboard( engine: Engine, page: DesignBlock, numberOfFrames: Int = 6, ): List { val frames = sortedMapOf() engine.block.generateVideoThumbnailSequence( block = page, thumbnailHeight = thumbnailHeightPx(dp = 96F), timeBegin = 0.0, timeEnd = engine.block.getDuration(page), numberOfFrames = numberOfFrames, ).collect { result -> frames[result.frameIndex] = result } return withContext(Dispatchers.Default) { frames.values.map(::videoThumbnailToBitmap) } } suspend fun capturePreviewFrame( engine: Engine, page: DesignBlock, time: Double, ): Bitmap { // A one-frame request over a zero-length range samples exactly `time`. val result = engine.block.generateVideoThumbnailSequence( block = page, thumbnailHeight = MAX_THUMBNAIL_RENDER_SIZE, timeBegin = time, timeEnd = time, numberOfFrames = 1, ).first() return withContext(Dispatchers.Default) { videoThumbnailToBitmap(result) } } suspend fun generateWaveform( engine: Engine, block: DesignBlock, duration: Double, numberOfSamples: Int = 240, samplesPerChunk: Int = 60, numberOfChannels: Int = 2, ): Waveform { val chunks = sortedMapOf>() engine.block.generateAudioThumbnailSequence( block = block, samplesPerChunk = samplesPerChunk, timeBegin = 0.0, timeEnd = duration, numberOfSamples = numberOfSamples, numberOfChannels = numberOfChannels, ).collect { chunk -> // `samples` is a List, not a FloatArray, and every value already sits in 0..1. chunks[chunk.chunkIndex] = chunk.samples } val left = ArrayList(numberOfSamples) val right = ArrayList(numberOfSamples) chunks.values.forEach { samples -> // Stereo chunks interleave left-then-right, and the last chunk may be short. samples.chunked(numberOfChannels).forEach { frame -> left += frame.first() right += frame.last() } } return Waveform(left = left, right = right, chunkCount = chunks.size) } suspend fun cancelFilmstripEarly( engine: Engine, videoFill: DesignBlock, sourceDuration: Double, ): Int = coroutineScope { var received = 0 // In an app this is viewModelScope or lifecycleScope — both dispatch on the main thread, // which is where the Flow requires you to collect. val job = launch { engine.block.generateVideoThumbnailSequence( block = videoFill, thumbnailHeight = thumbnailHeightPx(dp = 48F), timeBegin = 0.0, timeEnd = sourceDuration, numberOfFrames = 120, ).collect { received += 1 } } delay(timeMillis = 250) // There is no cancel method on the API. Cancelling the collecting Job closes the Flow, // which cancels the native request. Do this before requesting a new sequence for the // same block, otherwise the new request waits for the old one to finish. job.cancelAndJoin() received } ``` Build timeline scrubbers, filmstrips, page storyboards, and waveform lanes from a Kotlin `Flow` that emits each preview as soon as the engine renders it. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-thumbnail-previews) CE.SDK exposes two preview APIs on `engine.block`. `generateVideoThumbnailSequence` returns image frames sampled across a time range, and `generateAudioThumbnailSequence` returns a waveform as chunks of float samples. Both return a cold `Flow` that emits as results become ready, so you can paint a scrubber while the rest of it still renders. Both are built for previews, so they render at most 512 px per side. Ask for more and you still get the size you asked for, but the extra pixels are stretched, not sharper. For full-resolution still images, see [Create Thumbnail](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/). ## Size Requests in Pixels `thumbnailHeight` is measured in pixels, not dp. Convert your layout height with the display density before passing it in, otherwise thumbnails come out soft on high-density screens. The width is derived from the aspect ratio and reported back on each result. ```kotlin highlight=highlight-android-density // thumbnailHeight is measured in pixels, not dp, so convert your layout height first. // Clamping to the engine's render cap keeps the request at a size that adds real detail. fun thumbnailHeightPx(dp: Float): Int { val density = Resources.getSystem().displayMetrics.density return (dp * density).roundToInt().coerceIn(1, MAX_THUMBNAIL_RENDER_SIZE) } ``` Clamping to 512 is optional but worth doing: above that, the engine renders at 512 and upscales, so the extra pixels only cost memory. ## Generate a Video Filmstrip The runnable example starts with a video fill and an audio block already attached to a page. Loading them up front is optional because both preview APIs wait for their media, but doing so keeps the first request from waiting on the download. ```kotlin highlight=highlight-android-media // Both APIs wait for the resource on their own, but loading it up front avoids a first // request that returns before the media is decodable. engine.block.forceLoadAVResource(videoFill) engine.block.forceLoadAVResource(audioClip) // Video fills are sampled in media time, so measure the source, not the clip. val sourceDuration = engine.block.getAVResourceTotalDuration(videoFill) ``` `getAVResourceTotalDuration` measures the video source because a video fill is sampled in **media time**, not page time. Pass that duration with the video fill, the thumbnail height, and the number of frames you want. Collect the `Flow` and key each result by its reported `frameIndex` — frames are not guaranteed to arrive in order. ```kotlin highlight=highlight-android-filmstrip suspend fun generateFilmstrip( engine: Engine, videoFill: DesignBlock, sourceDuration: Double, numberOfFrames: Int = 8, ): List { // Frames are not guaranteed to arrive in index order, so key them by the reported index. val frames = sortedMapOf() engine.block.generateVideoThumbnailSequence( block = videoFill, thumbnailHeight = thumbnailHeightPx(dp = 48F), timeBegin = 0.0, timeEnd = sourceDuration, numberOfFrames = numberOfFrames, ).collect { result -> frames[result.frameIndex] = result } // Collection has to happen on the main thread, and the collector must not suspend: // the callback channel is bounded, so a slow collector drops frames. Decode after. return withContext(Dispatchers.Default) { frames.values.map(::videoThumbnailToBitmap) } } ``` Two threading rules make this code the shape it is: - **Collect on the main thread.** The `Flow` calls `require(Looper.myLooper() === looper)` when collection starts, so collecting from `Dispatchers.IO` throws `IllegalArgumentException: Should be invoked on the main thread.` Use `viewModelScope` or `lifecycleScope`. - **Do not suspend inside the collector.** The engine delivers frames through a bounded channel. If the collector is suspended when that buffer fills, frames are dropped, and because the stream ends after exactly `numberOfFrames` emissions, a dropped frame means it never ends at all. Gather the results first, then move the pixel work off the main thread. The `Flow` is cold, so every `collect` starts a new native request, and it completes on its own once `numberOfFrames` results have arrived. Engine failures surface as a thrown `EngineException` from the collector, not as a value in the stream. ### Choosing the block: video fill vs. design block The same method behaves differently depending on what you pass, and this is the single most important thing to get right: | | Video fill | Page or design block | | --- | --- | --- | | Source | Real decoded frames from the media file | The composed scene, re-rendered | | Frame times | Evenly spaced, including the first and last moment of the range | Spread across the range, but never exactly the first or last moment | | Time base | Media time in the source file — trim, time offset, speed, and looping are ignored | Relative to the block's own time offset on the page timeline | | Speed | Slower, because each frame is decoded on its own | Faster, because several frames render together | A design block must be a page or a descendant of a page. A detached block or the scene block fails with `MEDIA.BLOCK_NOT_PAGE_OR_CHILD`. Fills other than video, along with shapes, effects, and animations, fail with `MEDIA.OPERATION_UNSUPPORTED_FOR_BLOCK`. ### Convert frames to Bitmaps `VideoThumbnailResult` carries `width`, `height`, and `imageData`, a fresh direct `ByteBuffer` of 8-bit RGBA per emission. It is safe to retain, but its position is stateful. ```kotlin highlight=highlight-android-bitmap fun videoThumbnailToBitmap(result: VideoThumbnailResult): Bitmap { val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888) bitmap.copyPixelsFromBuffer(result.imageData) // copyPixelsFromBuffer leaves the buffer position at its limit. Rewind it, or converting // the same result again throws "Buffer not large enough for pixels". result.imageData.rewind() return bitmap } ``` `copyPixelsFromBuffer` reads the buffer to its limit and leaves the position there. Without the `rewind()`, converting the same result a second time throws `"Buffer not large enough for pixels"`. No channel swizzling is needed — `Bitmap.Config.ARGB_8888` matches the layout the engine hands you. ## Storyboard a Page Passing a page renders the composed scene at several points along its timeline, which is what you want for a page storyboard or a chapter strip. ```kotlin highlight=highlight-android-storyboard suspend fun generateStoryboard( engine: Engine, page: DesignBlock, numberOfFrames: Int = 6, ): List { val frames = sortedMapOf() engine.block.generateVideoThumbnailSequence( block = page, thumbnailHeight = thumbnailHeightPx(dp = 96F), timeBegin = 0.0, timeEnd = engine.block.getDuration(page), numberOfFrames = numberOfFrames, ).collect { result -> frames[result.frameIndex] = result } return withContext(Dispatchers.Default) { frames.values.map(::videoThumbnailToBitmap) } } ``` Two things change here. Blocks render without their animations, so you see them in their resting state. And video inside the composition comes from a small set of cached frames rather than a fresh decode, so a page storyboard shows video content only approximately. When you need accurate frames from a clip, ask its video fill instead. ## Capture a Single Preview Frame For a poster frame, ask for one frame and give the same value for the start and the end of the range. That gives you exactly the moment you asked for. ```kotlin highlight=highlight-android-single-frame suspend fun capturePreviewFrame( engine: Engine, page: DesignBlock, time: Double, ): Bitmap { // A one-frame request over a zero-length range samples exactly `time`. val result = engine.block.generateVideoThumbnailSequence( block = page, thumbnailHeight = MAX_THUMBNAIL_RENDER_SIZE, timeBegin = time, timeEnd = time, numberOfFrames = 1, ).first() return withContext(Dispatchers.Default) { videoThumbnailToBitmap(result) } } ``` `Flow.first()` collects the single emission and closes the stream. ## Draw an Audio Waveform `generateAudioThumbnailSequence` accepts an audio block or a video fill. A page or a graphic is rejected. `numberOfSamples` is counted **per channel**, and each chunk carries `samplesPerChunk * numberOfChannels` floats, interleaved left-then-right for stereo. ```kotlin highlight=highlight-android-waveform suspend fun generateWaveform( engine: Engine, block: DesignBlock, duration: Double, numberOfSamples: Int = 240, samplesPerChunk: Int = 60, numberOfChannels: Int = 2, ): Waveform { val chunks = sortedMapOf>() engine.block.generateAudioThumbnailSequence( block = block, samplesPerChunk = samplesPerChunk, timeBegin = 0.0, timeEnd = duration, numberOfSamples = numberOfSamples, numberOfChannels = numberOfChannels, ).collect { chunk -> // `samples` is a List, not a FloatArray, and every value already sits in 0..1. chunks[chunk.chunkIndex] = chunk.samples } val left = ArrayList(numberOfSamples) val right = ArrayList(numberOfSamples) chunks.values.forEach { samples -> // Stereo chunks interleave left-then-right, and the last chunk may be short. samples.chunked(numberOfChannels).forEach { frame -> left += frame.first() right += frame.last() } } return Waveform(left = left, right = right, chunkCount = chunks.size) } ``` `samples` is a `List`, not a `FloatArray`. You receive `numberOfSamples / samplesPerChunk` emissions, rounded up, and the last one can be shorter than the rest — so do not assume a fixed size when you split the channels. ### What the numbers mean The engine analyzes the audio for you. Every value is a loudness between `0.0` and `1.0`, never negative, and already smoothed — so these are numbers you can draw straight away, not raw audio data. That keeps the drawing code simple: mirror each value around a horizontal center line to get a bar height. You do not need to find peaks or rescale anything. Very quiet audio reads as `0.0`, and a range past the end of the media returns zeros instead of an error. A WAV file produces its waveform almost immediately. A long MP3 or AAC file arrives over several moments. ## Cancel Generation There is no cancel method on the Android API. You cancel by cancelling the coroutine that is collecting, which closes the `Flow` and cancels the underlying native request. ```kotlin highlight=highlight-android-cancel suspend fun cancelFilmstripEarly( engine: Engine, videoFill: DesignBlock, sourceDuration: Double, ): Int = coroutineScope { var received = 0 // In an app this is viewModelScope or lifecycleScope — both dispatch on the main thread, // which is where the Flow requires you to collect. val job = launch { engine.block.generateVideoThumbnailSequence( block = videoFill, thumbnailHeight = thumbnailHeightPx(dp = 48F), timeBegin = 0.0, timeEnd = sourceDuration, numberOfFrames = 120, ).collect { received += 1 } } delay(timeMillis = 250) // There is no cancel method on the API. Cancelling the collecting Job closes the Flow, // which cancels the native request. Do this before requesting a new sequence for the // same block, otherwise the new request waits for the old one to finish. job.cancelAndJoin() received } ``` In a timeline UI this is the same pattern production uses: keep the collecting `Job` in a field, and `job?.cancel()` before starting the next request for that clip. Always cancel before you request the same block again. A block handles one request at a time, and a second request waits instead of failing. Requests for different blocks also run one after another, so several strips started at once wait for each other. A cancel takes effect a moment later, so one or two frames can still arrive. Nothing arrives to tell you the cancel happened — the frames simply stop. Cancelling twice, cancelling after the request finished, or cancelling a failed request are all safe. How much a cancel actually stops depends on which sequence you started: | Sequence | Cancel before the first result | Cancel mid-flight | | --- | --- | --- | | Video fill | Stops it — nothing is emitted | **No effect.** The remaining frames are still emitted | | Page or design block | Stops it | Stops it | | Audio waveform | Stops it | Stops it | Plan around the first row. Once the engine starts decoding a video fill, a cancel no longer reaches it. Cancelling the `Job` does stop your collector from seeing those frames, so your UI stays correct — but the engine keeps working, and that work delays your next request. Keep video fill strips short, eight frames rather than a hundred, so an abandoned request finishes quickly. ## Performance and Limits | Limit | Detail | | --- | --- | | Size | At most 512 px per side; larger requests are stretched, not sharper | | Per block | One request at a time; a second request waits instead of failing | | Order | Requests run one after another, so several strips started at once wait for each other | | Speed | Video fills are slower than pages, because each frame is decoded on its own | | Playback | Previews keep arriving while the page is playing | Keep strips short and heights small. A scrubber only needs enough frames to fill its width, and asking for more makes it slower. ## Troubleshooting | Symptom | Likely cause | Solution | | --- | --- | --- | | `IllegalArgumentException: Should be invoked on the main thread.` | The `Flow` is collected from a background dispatcher | Collect in `viewModelScope` or `lifecycleScope`; move only the `Bitmap` decode off the main thread | | `"Buffer not large enough for pixels"` | `imageData` was read twice without rewinding | Call `imageData.rewind()` after `copyPixelsFromBuffer` | | Frames appear out of order | Emissions are not ordered by index | Key results by `frameIndex` instead of appending in arrival order | | A new request seems to hang | An earlier request for the same block is still running | Cancel the previous collecting `Job` before requesting again | | The `Flow` never completes | The collector paused and missed frames | Do not suspend inside the collector; do the `Bitmap` work after collecting | | `MEDIA.OPERATION_UNSUPPORTED_FOR_BLOCK` | The block is not a video fill or a design block, for example an image fill, a shape, or an effect | Pass a video fill, an audio block, a page, or a block on a page | | `MEDIA.BLOCK_NOT_PAGE_OR_CHILD` | The block is not on a page yet, or you passed the scene | Pass a page or a block that sits on one | | Waveform is flat | The range is past the end of the audio, or the audio is very quiet | Clamp the range to `getAVResourceTotalDuration` and check the source level | | Thumbnails look soft | You asked for more than 512 px | Request a `thumbnailHeight` of 512 or less | ## API Reference | Method | Description | | --- | --- | | `generateVideoThumbnailSequence(block: DesignBlock, thumbnailHeight: Int, timeBegin: Double, timeEnd: Double, numberOfFrames: Int): Flow` | Stream image frames across a time range from a video fill or a design block. | | `generateAudioThumbnailSequence(block: DesignBlock, samplesPerChunk: Int, timeBegin: Double, timeEnd: Double, numberOfSamples: Int, numberOfChannels: Int): Flow` | Stream waveform chunks from an audio block or a video fill. `numberOfChannels` must be 1 or 2. | | `VideoThumbnailResult(frameIndex: Int, width: Int, height: Int, imageData: ByteBuffer)` | One rendered frame. `imageData` is a direct buffer of 8-bit RGBA. | | `AudioThumbnailResult(chunkIndex: Int, samples: List)` | One chunk of the waveform, interleaved by channel, each value in `0.0..1.0`. | | `engine.block.forceLoadAVResource(block: DesignBlock)` | Load the audio-video resource before requesting previews. | | `engine.block.getAVResourceTotalDuration(block: DesignBlock): Double` | Read the source media duration for a video fill or audio block. | | `engine.block.getDuration(block: DesignBlock): Double` | Read a page or block's duration for the storyboard time range. | ## Next Steps - For static preview images produced by the export API, see [Create Thumbnail](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/). - To build the timeline these previews feed, see [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/). - For more on trimming and positioning the clips you preview, see [Video Fills](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/). --- ## 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 See [Import and Export](https://img.ly/docs/cesdk/android/concepts/import-export-0ebbd8/) for how these import and export workflows fit together. ## 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 - [Overview](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) engine.editor.loadCMYKProfile() 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 applied to shapes using sRGB, CMYK, and Spot Colors](./assets/android.hero.webp) 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 engine.editor.loadCMYKProfile() 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 - [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. ![Android gradient fills preview showing linear, radial, conical, shared, and duplicated gradient blocks](./assets/android.hero.webp) > **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 --- ## 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. ![A square graphic block filled with an aerial coastline photograph using an image fill in Contain mode](./assets/android.hero.webp) > **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: "Overview" 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. | | Stripe | Marking a block without hiding what is behind it, such as a region content must stay clear of. | 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. ![Android video fill preview showing a rectangle and translucent ellipse filled with the same video frame](./assets/android.hero.webp) > **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 - [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. - [Trim Video and Audio](https://img.ly/docs/cesdk/android/edit-video/trim-4f688b/) - Work with time-based media ranges. --- ## 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)](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/) - Apply the green screen effect to images and videos, replacing specific colors with transparency for compositing workflows. - [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. ![The same photo shown in an Android-generated guide output grid under different effects: the original, pixelization, an adjustments effect, a duotone filter, a LUT filter, and a combination of effects](./assets/android.hero.webp) > **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. ![Radial blur applied to an Android Engine scene](./assets/blur-android-preview.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$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. - [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. - [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)" description: "Apply the green screen effect to images and videos, replacing specific colors with transparency for compositing workflows." 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/) --- ```kotlin file=@cesdk_android_examples/engine-guides-filters-and-effects-chroma-key-green-screen/ChromaKeyGreenScreen.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.MimeType import ly.img.engine.ShapeType private const val TAG = "ChromaKeyGuide" suspend fun chromaKeyGreenScreen( engine: Engine, assetBaseUri: Uri, ): ChromaKeyGreenScreenResult = withContext(Dispatchers.Main) { // Demo scaffolding: a scene with one page, plus synthesized green-screen // footage — an astronaut sticker flattened onto a uniform green backdrop, // exported into an engine buffer — so the example has a keyable frame to work with. 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 backdrop = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(backdrop, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block = backdrop, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(block = backdrop, color = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.25F, a = 1F)) engine.block.setWidth(backdrop, value = 800F) engine.block.setHeight(backdrop, value = 600F) engine.block.setPositionX(backdrop, value = 0F) engine.block.setPositionY(backdrop, value = 0F) engine.block.appendChild(parent = page, child = backdrop) val subject = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(subject, shape = engine.block.createShape(ShapeType.Rect)) val subjectFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = subjectFill, property = "fill/image/imageFileURI", value = assetBaseUri.buildUpon() .appendPath("ly.img.sticker") .appendPath("images") .appendPath("3Dstickers") .appendPath("3d_stickers_astronaut.png") .build(), ) engine.block.setFill(block = subject, fill = subjectFill) engine.block.setWidth(subject, value = 360F) engine.block.setHeight(subject, value = 400F) engine.block.setPositionX(subject, value = 220F) engine.block.setPositionY(subject, value = 130F) engine.block.appendChild(parent = page, child = subject) val frameData = engine.block.export(page, mimeType = MimeType.PNG) // Keep the buffer alive while the image fill references it. // Destroy it when the fill is no longer needed. val frameBufferUri = engine.editor.createBuffer() engine.editor.setBufferData(uri = frameBufferUri, offset = 0, data = frameData) engine.block.destroy(backdrop) engine.block.destroy(subject) 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.setUri( block = imageFill, property = "fill/image/imageFileURI", value = frameBufferUri, ) engine.block.setFill(block = imageBlock, fill = imageFill) engine.block.setWidth(imageBlock, value = 600F) engine.block.setHeight(imageBlock, value = 450F) engine.block.setPositionX(imageBlock, value = 100F) engine.block.setPositionY(imageBlock, value = 75F) engine.block.appendChild(parent = page, child = imageBlock) val greenScreenEffect = engine.block.createEffect(type = EffectType.GreenScreen) engine.block.appendEffect(block = imageBlock, effectBlock = greenScreenEffect) engine.block.setColor( greenScreenEffect, property = "effect/green_screen/fromColor", value = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.25F, a = 1F), ) engine.block.setFloat(greenScreenEffect, property = "effect/green_screen/colorMatch", value = 0.26F) engine.block.setFloat(greenScreenEffect, property = "effect/green_screen/smoothness", value = 0.15F) engine.block.setFloat(greenScreenEffect, property = "effect/green_screen/spill", value = 0.4F) val colorMatch = engine.block.getFloat(greenScreenEffect, property = "effect/green_screen/colorMatch") val smoothness = engine.block.getFloat(greenScreenEffect, property = "effect/green_screen/smoothness") val spill = engine.block.getFloat(greenScreenEffect, property = "effect/green_screen/spill") // A full-page background block with a solid color fill for the composite. val backgroundBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(backgroundBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block = backgroundBlock, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(block = backgroundBlock, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F)) engine.block.setWidth(backgroundBlock, value = 800F) engine.block.setHeight(backgroundBlock, value = 600F) engine.block.setPositionX(backgroundBlock, value = 0F) engine.block.setPositionY(backgroundBlock, value = 0F) engine.block.appendChild(parent = page, child = backgroundBlock) engine.block.sendToBack(backgroundBlock) engine.block.bringToFront(imageBlock) val heroPng = engine.block.export(page, mimeType = MimeType.PNG) val isEnabled = engine.block.isEffectEnabled(greenScreenEffect) Log.i(TAG, "Green screen effect enabled: $isEnabled") engine.block.setEffectEnabled(effectBlock = greenScreenEffect, enabled = !isEnabled) val enabledAfterToggle = engine.block.isEffectEnabled(greenScreenEffect) val blockSupportsEffects = engine.block.supportsEffects(imageBlock) Log.i(TAG, "Block supports effects: $blockSupportsEffects") val effects = engine.block.getEffects(imageBlock) Log.i(TAG, "Number of effects: ${effects.size}") val effectIndex = effects.indexOf(greenScreenEffect) if (effectIndex >= 0) { engine.block.removeEffect(block = imageBlock, index = effectIndex) } engine.block.destroy(greenScreenEffect) val removed = engine.block.getEffects(imageBlock).none { effect -> effect == greenScreenEffect } ChromaKeyGreenScreenResult( blockSupportsEffects = blockSupportsEffects, colorMatch = colorMatch, smoothness = smoothness, spill = spill, enabledAfterToggle = enabledAfterToggle, removed = removed, heroPng = heroPng, ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-filters-and-effects-chroma-key-green-screen/ChromaKeyGreenScreenResult.kt reference-only import java.nio.ByteBuffer data class ChromaKeyGreenScreenResult( val blockSupportsEffects: Boolean, val colorMatch: Float, val smoothness: Float, val spill: Float, val enabledAfterToggle: Boolean, val removed: Boolean, val heroPng: ByteBuffer, ) ``` Replace specific colors with transparency using CE.SDK's green screen effect for video compositing and virtual background applications. ![An astronaut subject composited over a solid blue background after the green backdrop of the source frame was keyed out with the green screen effect](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-filters-and-effects-chroma-key-green-screen) While green is the most common key color due to its contrast with skin tones, the effect works with any solid color—blue screens, white backgrounds, or custom colors. CE.SDK processes chroma keying in real-time using GPU-accelerated shaders. This guide covers how to apply the green screen effect programmatically, configure color selection and keying parameters, composite with background layers, and manage effects on blocks. The example applies the effect to a graphic block whose image fill holds a frame of green-screen footage — a subject in front of a uniform green backdrop — and keys out the backdrop. > **Note:** The CE.SDK editor UI lists Green Screen alongside the other effects in its appearance controls, with sheet controls for the key color, color match, smoothness, and spill parameters. 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 these controls built in; use the Engine API in this guide when your app needs automation, presets, batch processing, or custom controls. ## Apply the Green Screen Effect Create a green screen effect instance with `createEffect()` and attach it to a block with `appendEffect()`, which adds the effect to the block's effect stack. The effect immediately processes the target color, making matching pixels transparent. ```kotlin highlight-android-apply-green-screen val greenScreenEffect = engine.block.createEffect(type = EffectType.GreenScreen) engine.block.appendEffect(block = imageBlock, effectBlock = greenScreenEffect) ``` `imageBlock` is the example's graphic block with an image fill; the same calls work on the block types that support effects — graphic blocks and pages. Video content also lives on a graphic block, with a video fill instead of an image fill, so the same workflow applies. ## Configure Color Selection The green screen effect targets a specific color to key out. Set this color using `setColor()` with the `effect/green_screen/fromColor` property. The effect defaults to pure green, and the color's alpha channel is ignored. ```kotlin highlight-android-set-key-color engine.block.setColor( greenScreenEffect, property = "effect/green_screen/fromColor", value = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.25F, a = 1F), ) ``` The example sets the key color to the exact green of its footage's backdrop. For blue screen footage, set the color to blue instead — any solid color works. Match the exact color you want to remove for best results. ## Adjust Color Matching Tolerance The `colorMatch` parameter controls how closely pixels must match the target color to be keyed out. Adjust it with `setFloat()`. ```kotlin highlight-android-color-match engine.block.setFloat(greenScreenEffect, property = "effect/green_screen/colorMatch", value = 0.26F) ``` Higher values (closer to `1.0`) key out a wider range of similar colors, which is useful for footage with uneven lighting or color variations in the background. Lower values create more precise keying for well-lit footage with uniform backgrounds. The parameter ranges from `0.0` to `1.0` and defaults to `0.4`. ## Control Edge Smoothness The `smoothness` parameter controls the transition between opaque and transparent areas. This affects how sharp or soft the edges appear around keyed subjects. ```kotlin highlight-android-smoothness engine.block.setFloat(greenScreenEffect, property = "effect/green_screen/smoothness", value = 0.15F) ``` Higher smoothness values create softer edges that blend naturally with new backgrounds, reducing harsh outlines. Lower values produce sharper edges, which may be preferable for high-contrast composites or when preserving fine detail. ## Remove Color Spill Color spill occurs when the key color reflects onto the foreground subject, creating a green or blue tint on edges. The `spill` parameter desaturates the remaining traces of the key color. ```kotlin highlight-android-spill engine.block.setFloat(greenScreenEffect, property = "effect/green_screen/spill", value = 0.4F) ``` Increase the spill value when you notice the key color appearing on subject edges or reflective surfaces. This is common with shiny hair, glasses, or metallic objects near the screen. Spill removal is off by default (`0.0`). ## Composite with Background Layers After keying, layer the transparent content over backgrounds using block ordering. The example prepares a background block with a solid color fill sized to cover the page; append it to the page, place it behind the keyed image with `sendToBack()`, and keep the keyed image on top with `bringToFront()`. ```kotlin highlight-android-composite-background engine.block.appendChild(parent = page, child = backgroundBlock) engine.block.sendToBack(backgroundBlock) engine.block.bringToFront(imageBlock) ``` The background appears through the transparent areas where the key color was removed. You can use image or video fills instead of solid colors for more dynamic backgrounds. ## Toggle the Effect Check whether an effect is enabled using `isEffectEnabled()`. ```kotlin highlight-android-check-enabled val isEnabled = engine.block.isEffectEnabled(greenScreenEffect) Log.i(TAG, "Green screen effect enabled: $isEnabled") ``` To toggle the effect on or off, use `setEffectEnabled()`. This preserves the effect configuration while temporarily removing its visual impact — here the effect is flipped relative to the state read above. ```kotlin highlight-android-toggle-green-screen engine.block.setEffectEnabled(effectBlock = greenScreenEffect, enabled = !isEnabled) ``` Toggling effects is useful for before/after comparisons or conditional processing without removing and recreating the effect. ## Manage the Effect Beyond toggling, you can query, remove, and clean up effects. Use `supportsEffects()` to check if a block can have effects, `getEffects()` to list all applied effects, `removeEffect()` to detach an effect from a block, and `destroy()` to free the effect's resources. ```kotlin highlight-android-manage-green-screen val blockSupportsEffects = engine.block.supportsEffects(imageBlock) Log.i(TAG, "Block supports effects: $blockSupportsEffects") val effects = engine.block.getEffects(imageBlock) Log.i(TAG, "Number of effects: ${effects.size}") val effectIndex = effects.indexOf(greenScreenEffect) if (effectIndex >= 0) { engine.block.removeEffect(block = imageBlock, index = effectIndex) } engine.block.destroy(greenScreenEffect) ``` When removing an effect, find its position in the list returned by `getEffects()` and pass that index to `removeEffect()`. Removing an effect detaches it from the block but keeps the instance alive — call `destroy()` on the effect to release its resources. ## Troubleshooting ### Keying Results Appear Rough or Incomplete - Increase the `colorMatch` value to capture more color variations - Ensure source footage has even lighting on the screen - Check that the target color accurately matches the screen color ### Edges Have Color Fringing - Increase the `spill` value to remove color cast - Adjust `smoothness` to soften hard edges - Increase `colorMatch` if the fringe consists of leftover key-color pixels that fall just outside the matching threshold ### Transparent Areas Appear in Wrong Places - Decrease `colorMatch` to be more selective about which colors are keyed - Verify the `fromColor` matches only the intended background color - Check that foreground subjects don't contain colors similar to the key color ## API Reference ### Methods | API | Description | | --- | --- | | `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.setColor(block=_, property="effect/green_screen/fromColor", value=_)` | Sets the color to key out | | `engine.block.setFloat(block=_, property="effect/green_screen/colorMatch", value=_)` | Sets the color matching tolerance | | `engine.block.setFloat(block=_, property="effect/green_screen/smoothness", value=_)` | Sets the edge smoothness | | `engine.block.setFloat(block=_, property="effect/green_screen/spill", value=_)` | Sets the spill removal intensity | | `engine.block.getColor(block=_, property="effect/green_screen/fromColor")` | Reads the configured key color | | `engine.block.getFloat(block=_, property="effect/green_screen/colorMatch")` | Reads the color matching tolerance | | `engine.block.getFloat(block=_, property="effect/green_screen/smoothness")` | Reads the edge smoothness | | `engine.block.getFloat(block=_, property="effect/green_screen/spill")` | Reads the spill removal intensity | | `engine.block.isEffectEnabled(effectBlock=_)` | Returns whether an effect block is enabled | | `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect block | | `engine.block.supportsEffects(block=_)` | Checks whether a block can render effects | | `engine.block.getEffects(block=_)` | Returns the ordered effects attached to a block | | `engine.block.appendChild(parent=_, child=_)` | Appends a block as the last child of a parent | | `engine.block.sendToBack(block=_)` | Moves a block behind its siblings | | `engine.block.bringToFront(block=_)` | Moves a block in front of its siblings | | `engine.block.removeEffect(block=_, index=_)` | Removes the effect at a stack index | | `engine.block.destroy(block=_)` | Destroys an unused effect block | | `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Creates a color value for color effect properties | ### Properties | Property | Type | Description | | --- | --- | --- | | `effect/green_screen/fromColor` | Color | The color to replace with transparency; defaults to pure green, alpha is ignored | | `effect/green_screen/colorMatch` | Float | Color matching tolerance (`0.0`–`1.0`, default `0.4`) | | `effect/green_screen/smoothness` | Float | Edge smoothness (`0.0`–`1.0`, default `0.08`) | | `effect/green_screen/spill` | Float | Spill removal intensity (`0.0`–`1.0`, default `0.0`) | ## 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 with the CE.SDK Engine API. - [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. - [Duotone](https://img.ly/docs/cesdk/android/filters-and-effects/duotone-831fc5/) — Apply duotone effects to images, mapping tones to two colors for stylized visuals, vintage aesthetics, or brand-specific 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: "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. ![Android export result with a custom LUT filter applied](./assets/android.hero.png) > **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: - [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 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/v$UBQ_VERSION$/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. For organizing collections of custom filters through asset sources, see [Create Custom Filters](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-filters-c796ba/). ## 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. Identity LUT 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. ![A grid showing the same photo unaltered alongside the liquid, mirror, shifter, radial pixel, and TV glitch distortion effects](./assets/android.hero.webp) > **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. ![The same photo under three duotone treatments: a library preset, a custom navy-to-cream pair, and a combined adjustments-plus-duotone treatment](./assets/android.hero.png) > **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. ![Supported Filters and Effects Android preview showing an image block with a duotone effect](./assets/android.hero.png) > **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: "Agent Skills" description: "Install CE.SDK documentation and code generation skills for AI coding assistants." platform: android url: "https://img.ly/docs/cesdk/android/get-started/agent-skills-f7g8h9/" --- > 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/) > [Agent Skills](https://img.ly/docs/cesdk/android/get-started/agent-skills-f7g8h9/) --- The CE.SDK Agent Skills plugins give AI coding assistants bundled documentation, guided code generation, and project scaffolding for building applications with CreativeEditor SDK across 10 Web frameworks, Swift on Apple platforms, and Kotlin with Jetpack Compose on Android. ## What Are Agent Skills? [Agent Skills](https://agentskills.io) are portable knowledge packs that plug into AI coding assistants. By installing the CE.SDK skills, you get: - **Offline documentation**: All guides, API references, and best practices bundled locally — no external API calls - **Guided code generation**: Build and explain skills that walk through CE.SDK implementation step by step - **Autonomous scaffolding**: The shared build skill creates and verifies complete CE.SDK projects from scratch ## Available Skills | Skill | Description | | ---------------- | ------------------------------------------------------------------------- | | `docs-react` | Look up CE.SDK React reference guides and documentation | | `docs-vue` | Look up CE.SDK Vue.js reference guides and documentation | | `docs-svelte` | Look up CE.SDK Svelte reference guides and documentation | | `docs-sveltekit` | Look up CE.SDK SvelteKit reference guides and documentation | | `docs-angular` | Look up CE.SDK Angular reference guides and documentation | | `docs-nextjs` | Look up CE.SDK Next.js reference guides and documentation | | `docs-nuxtjs` | Look up CE.SDK Nuxt.js reference guides and documentation | | `docs-electron` | Look up CE.SDK Electron reference guides and documentation | | `docs-js` | Look up CE.SDK Vanilla JavaScript reference guides and documentation | | `docs-node` | Look up CE.SDK Node.js reference guides and documentation | | `build` | Implement features and autonomously scaffold complete CE.SDK Web projects | | `explain` | Explain how CE.SDK Web features work — concepts, architecture, workflows | Claude Code additionally receives a thin optional `builder` agent adapter that delegates to the same shared `build` skill. Codex uses the build skill directly. The separate `cesdk-swift` plugin contains three Swift-focused skills: | Skill | Description | | --------- | ---------------------------------------------------------------------------- | | `docs` | Look up first-party Swift guides and module-qualified API digests | | `explain` | Explain CE.SDK Swift concepts and Apple-platform architecture | | `build` | Implement Swift features and scaffold iOS projects from bundled starter kits | `IMGLYEngine` is available on iOS, macOS, and Mac Catalyst. The prebuilt editor, camera, UI modules, and bundled starter kits are iOS-only. When available, the Swift skills prefer live Xcode symbols for the installed SDK and use bundled API digests as a portable fallback. The separate `cesdk-android` plugin contains three Android-focused skills: | Skill | Description | | --------- | ------------------------------------------------------------------------- | | `docs` | Look up Android guides and Kotlin API digests | | `explain` | Explain CE.SDK Android, Jetpack Compose, and engine concepts | | `build` | Implement Kotlin features and scaffold projects from Android starter kits | The Android API reference is distilled from first-party Dokka Markdown into compact per-type digests. The bundled guides cover threading, Compose state, and engine lifecycle behavior. ## Setup Instructions ### Claude Code Plugin Add the marketplace and install the plugin: ```bash # Add the marketplace (one-time setup) claude plugin marketplace add imgly/agent-skills # Install the plugin claude plugin install imgly-sdk@imgly # Install the Swift plugin claude plugin install cesdk-swift@imgly # Install the Android plugin claude plugin install cesdk-android@imgly ``` ### Codex Plugin Add the same marketplace and install the plugin in Codex: ```bash # Add the marketplace (one-time setup) codex plugin marketplace add imgly/agent-skills # Install the Web, Swift, or Android plugin codex plugin add imgly-sdk@imgly codex plugin add cesdk-swift@imgly codex plugin add cesdk-android@imgly ``` ### Installs from before the rename The plugin used to be called `cesdk`. In Claude Code, an existing `cesdk@imgly` install keeps updating through an alias entry for the same plugin. Its skills now show as `/imgly-sdk:build`, `/imgly-sdk:explain`, and `/imgly-sdk:docs-`. New installs use `imgly-sdk@imgly`. Codex cannot update a plugin under its old name, so an existing `cesdk@imgly` install stays on its last version. Replace it: ```bash codex plugin remove cesdk@imgly codex plugin add imgly-sdk@imgly ``` ### Vercel Skills CLI Install using the [Vercel Skills CLI](https://github.com/vercel-labs/skills): ```bash # Install all skills for Claude Code npx skills add imgly/agent-skills -a claude-code # Install a specific skill only npx skills add imgly/agent-skills --skill docs-react -a claude-code # List available skills first npx skills add imgly/agent-skills --list ``` ### Manual Copy For any skills-compatible agent, copy skill folders directly from the [GitHub repository](https://github.com/imgly/agent-skills): ```bash # Clone the repo git clone https://github.com/imgly/agent-skills.git # Copy a specific skill into your agent's skills directory cp -r agent-skills/plugins/cesdk/skills/docs-react /cesdk-docs-react # Or copy the native documentation skills cp -r agent-skills/plugins/cesdk-swift/skills/docs /cesdk-swift-docs cp -r agent-skills/plugins/cesdk-android/skills/docs /cesdk-android-docs ``` ## Keeping Skills Current Every generated skill records its CE.SDK version, generation date, and plugin identifier. When a bundle is over six weeks old, or when you ask about updates, the assistant can compare it with the matching stable, prerelease, or nightly IMG.LY channel. This check is read-only. Before changing anything, the assistant must identify whether the active skill came from Claude Code, Codex, the Skills CLI, a Git checkout, or a manual copy. It reports unknown or ambiguous installations instead of guessing and requests explicit approval for the exact update command and target. ## Prerelease and Nightly Versions By default every install method above tracks the **latest stable** CE.SDK release. If you build against a prerelease or nightly engine build (for example `@cesdk/node-native` on the `next` or `dev` npm dist-tag), you can install the matching skills from a dedicated release channel. The channels mirror the npm dist-tag names: | Channel | Branch | Matches engine dist-tag | Contents | | ---------- | ----------------- | ----------------------- | ----------------------------------------------------- | | Stable | `main` / `latest` | `latest` | Latest stable release (default) | | Prerelease | `next` | `next` | Latest release candidate (e.g. `1.77.0-rc.4`) | | Nightly | `dev` | `dev` | Latest nightly build (e.g. `1.78.0-nightly.20260630`) | Every published version is also available under an exact `v` Git tag. Use the channel-specific instructions below to track a moving channel or pin one published build. > **Note:** The `v` tag for a given nightly only exists if that nightly actually published skills — when a night's generated content is identical to the previous publish, no new tag is created for that date. ### Claude Code Plugin Pin the marketplace to a channel branch or a version tag with the `@` suffix: ```bash # Prerelease (release candidate) channel — latest rc claude plugin marketplace add imgly/agent-skills@next claude plugin install cesdk@imgly # Nightly channel — latest nightly claude plugin marketplace add imgly/agent-skills@dev claude plugin install cesdk@imgly # A specific published version claude plugin marketplace add imgly/agent-skills@v1.78.0-nightly.20260630 claude plugin install cesdk@imgly ``` ### Codex Plugin Add the matching marketplace ref: ```bash # Prerelease channel codex plugin marketplace add imgly/agent-skills@next codex plugin add cesdk@imgly # Nightly channel codex plugin marketplace add imgly/agent-skills@dev codex plugin add cesdk@imgly # A specific published version codex plugin marketplace add imgly/agent-skills@v1.78.0-nightly.20260630 codex plugin add cesdk@imgly ``` ### Vercel Skills CLI Specify a channel branch or version tag with the `#` fragment (or an equivalent `/tree/` URL): ```bash # Nightly channel — latest nightly npx skills add imgly/agent-skills#dev -a claude-code # A specific published version npx skills add imgly/agent-skills#v1.78.0-nightly.20260630 -a claude-code # A single skill from a pinned version (`@` selects the skill) npx skills add imgly/agent-skills#v1.78.0-nightly.20260630@docs-node -a claude-code ``` ### Manual Copy Clone a specific channel branch or an exact version tag: ```bash # Nightly channel — latest nightly git clone -b dev https://github.com/imgly/agent-skills.git # A specific published version git clone -b v1.78.0-nightly.20260630 https://github.com/imgly/agent-skills.git ``` ## Usage Ask naturally and let your assistant select the right skill. For explicit selection, type `/` in Claude Code or `$` in Codex, then select the matching skill from the installed CE.SDK plugin. ### Look up documentation ```text Use the docs-react skill to look up CE.SDK configuration. Use the docs-vue skill for getting started. Use the docs-nextjs skill to explain server-side rendering. ``` ### Build a feature ```text Use the build skill to add text overlays to images. Use the build skill to create a photo editor with filters. ``` ### Explain a concept ```text Use the explain skill to describe how the block hierarchy works. Use the explain skill to describe the export pipeline and output formats. ``` ### Use Swift skills ```text Use the cesdk-swift docs skill to look up IMGLYEngine asset sources. Use the cesdk-swift build skill to create an iOS photo editor. Use the cesdk-swift explain skill to describe custom editor UI on macOS. ``` ### Use Android skills ```text Use the cesdk-android docs skill to look up BlockApi.create. Use the cesdk-android build skill to create a Compose photo editor. Use the cesdk-android explain skill to describe the EditorConfiguration.remember lifecycle. ``` ## How It Works Each documentation skill bundles the complete CE.SDK guides and API references for its framework in a compressed index. Skills read directly from these local files — no external services or MCP servers are required. The build skills include starter kit templates for common use cases like design editors, video editors, and photo editors. They select the matching Web, Swift, or Android project structure and generate code accordingly. --- ## 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](https://img.ly/docs/cesdk/android/get-started/agent-skills-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. - [Agent Skills](https://img.ly/docs/cesdk/android/get-started/agent-skills-f7g8h9/) - Install CE.SDK documentation and code generation skills for AI coding assistants. - [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: "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: "Get Started" description: "Start integrating CE.SDK into your application—from understanding the SDK to running your first editor." platform: android url: "https://img.ly/docs/cesdk/android/get-started/overview-e18f40/" --- > 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/) --- Everything you need to integrate CE.SDK into your application. Learn what the SDK offers, get up and running with starter kits, explore AI-powered workflows, and understand our licensing model. --- ## Related Pages - [Android Creative Editor](https://img.ly/docs/cesdk/android/what-is-cesdk-2e7acd/) - The Android Mobile Design Editor SDK offers a robust and customizable solution for creating and editing visual designs directly within your Android applications. - [Capabilities](https://img.ly/docs/cesdk/android/capabilities-e1906f/) - Explore the full list of CE.SDK capabilities available for your platform, including design, video, image, text, and more. - [Quickstart](https://img.ly/docs/cesdk/android/get-started/android/quickstart-4h4zji/) - Get started with CE.SDK by choosing a starter kit - [Licensing](https://img.ly/docs/cesdk/android/licensing-8aa063/) - Understand CE.SDK’s flexible licensing, trial options, and how keys work across dev, staging, and production. --- ## 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: "Guides" description: "Documentation for Guides" platform: android url: "https://img.ly/docs/cesdk/android/guides-8d8b00/" --- > 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/) --- --- ## Related Pages - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) - Learn how to configure CE.SDK to match your application's functional, visual, and performance requirements. - [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) - Explore all configurable editor settings and learn how to read, update, and observe them via the Settings API. - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) - Configure CE.SDK to load engine and content assets from your own servers instead of the IMG.LY CDN for production deployments. - [Engine Interface](https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/) - Initialize and manage the Android Engine lifecycle for headless and UI-based integrations. - [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) - Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale. - [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) - Use CE.SDK’s customizable, production-ready UI or replace it entirely with your own interface. - [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) - Learn how to load and create scenes, set the zoom level, and configure URI resolvers. - [Insert Media Into Scenes](https://img.ly/docs/cesdk/android/insert-media-a217f5/) - Understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code. - [Import Media](https://img.ly/docs/cesdk/android/import-media-4e3703/) - Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK. - [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. - [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) - Save design progress locally or to a backend service to allow for later editing or publishing. - [Store Custom Metadata](https://img.ly/docs/cesdk/android/export-save-publish/store-custom-metadata-337248/) - Attach, retrieve, and manage custom key-value metadata on design blocks in CE.SDK. - [Edit Image](https://img.ly/docs/cesdk/android/edit-image-c64912/) - Use CE.SDK to crop, transform, annotate, or enhance images with editing tools and programmatic APIs. - [Create Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) - Learn how to create and customize videos in CE.SDK using scenes, assets, and time-based editing. - [Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) - Create audio blocks, extract tracks from video, control playback, generate waveforms, and manage audio timing in CE.SDK for Android. - [Text](https://img.ly/docs/cesdk/android/text-8a993a/) - Add, style, and customize text layers in your design using CE.SDK’s flexible text editing tools. - [Create and Edit Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) - Draw custom vector shapes, combine them with boolean operations, and insert QR codes into your designs. - [Create and Edit Stickers](https://img.ly/docs/cesdk/android/stickers-3d4e5f/) - Create and customize stickers using image fills for icons, logos, emoji, and multi-color graphics. - [Create Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) - Combine and arrange multiple elements to create complex, multi-page, or layered design compositions. - [Create Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) - Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK. - [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) - Manage color usage in your designs, from applying brand palettes to handling print and screen formats. - [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) - Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements. - [Outlines](https://img.ly/docs/cesdk/android/outlines-b7820c/) - Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal. - [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) - Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying. - [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) - Add motion to designs with entrance, exit, and loop animation presets, timing controls, and programmatic APIs. - [Rules](https://img.ly/docs/cesdk/android/rules-1427c0/) - Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs. - [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) - Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools. - [Improve Performance](https://img.ly/docs/cesdk/android/performance-3c12eb/) - Optimize CE.SDK Android integration with source sets, memory monitoring, export tuning, and lifecycle best practices. - [Create a Local Maven Repository for Offline Builds](https://img.ly/docs/cesdk/android/create-offline-maven-repository-0d42fd/) - Download CE.SDK Android artifacts for a specific version and resolve them from a local Maven repository. --- ## 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 Media" description: "Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/import-media-4e3703/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/) - Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK. - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Understand key asset concepts like sources, formats, metadata, and how assets are integrated into designs. - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) - Manage how users browse, preview, and insert media assets into their designs with a customizable asset library. - [Import From Local Source](https://img.ly/docs/cesdk/android/import-media/from-local-source-39b2a9/) - Enable users to upload files from their device for use as design assets in the editor. - [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) - Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely. - [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) - Capture photos or videos directly from a connected camera for immediate use in your design. - [Edit or Remove Assets](https://img.ly/docs/cesdk/android/import-media/edit-or-remove-assets-ce072c/) - Manage Android assets in local asset sources by updating metadata, removing individual assets, or deleting entire sources. - [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) - Provide multiple image and video sources so CE.SDK can choose the right resolution for Android previews and exports. - [Using Default Assets](https://img.ly/docs/cesdk/android/import-media/default-assets-d2763d/) - Load built-in CE.SDK asset sources on Android and apply shapes, stickers, and sample media with the Asset API. - [Retrieve MIME Type](https://img.ly/docs/cesdk/android/import-media/retrieve-mimetype-ed13bf/) - Detect the MIME type of resources loaded in the engine to choose the correct handling, file extension, or upload content type. - [Create a Custom Importer](https://img.ly/docs/cesdk/android/import-media/create-custom-importer-0f7e16/) - Create a custom Android media importer by registering an AssetSource that maps backend media into CE.SDK assets. - [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) - Review the supported image, video, and audio formats for importing assets. - [Size Limits](https://img.ly/docs/cesdk/android/import-media/size-limits-c32275/) - Learn about file size restrictions and how to optimize large assets for use 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: "Asset Library" description: "Manage how users browse, preview, and insert media assets into their designs with a customizable asset library." platform: android url: "https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) --- --- ## Related Pages - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) - Explore the core functionality of the asset library and how users browse, search, and insert media. - [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Adapt the asset library UI and behavior to suit your application's structure and user needs. - [Thumbnails](https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/) - Configure thumbnail images for assets in the Android asset library with thumbUri, audio previewUri, and custom source mapping. - [Refresh Assets](https://img.ly/docs/cesdk/android/import-media/asset-library/refresh-assets-382060/) - Refresh visible Editor asset-library content after an external catalog changes. --- ## 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: "Basics" description: "Explore the core functionality of the asset library and how users browse, search, and insert media." platform: android url: "https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) > [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) --- ```kotlin file=@cesdk_android_examples/editor-guides-import-media-asset-library-basics/AssetLibraryBasicsEditorSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.component.Dock import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberImagesLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.data.AssetSourceType import ly.img.engine.AssetDefinition import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType private const val BRAND_IMAGE_SOURCE_ID = "ly.img.asset.source.brand.images" @Composable fun AssetLibraryBasicsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { val engine = editorContext.engine if (engine.scene.get() == null) { val scene = engine.scene.create() 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) } val assetEngine = editorContext.engine if (BRAND_IMAGE_SOURCE_ID !in assetEngine.asset.findAllSources()) { assetEngine.asset.addLocalSource( sourceId = BRAND_IMAGE_SOURCE_ID, supportedMimeTypes = listOf(MimeType.JPEG.key), ) assetEngine.asset.addAsset( sourceId = BRAND_IMAGE_SOURCE_ID, asset = AssetDefinition( id = "brand-background", label = mapOf("en" to "Brand Background"), tags = mapOf("en" to listOf("brand", "background")), groups = listOf("campaign"), 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", ), ), ) assetEngine.asset.assetSourceContentsChanged(sourceId = BRAND_IMAGE_SOURCE_ID) } } assetLibrary = { remember { val brandSourceType = AssetSourceType(sourceId = BRAND_IMAGE_SOURCE_ID) val brandImagesSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(brandSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = brandSourceType, assetType = AssetType.Image, ), ) val brandImagesCategory = LibraryCategory.Images.copy( content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_images, sections = listOf(brandImagesSection), ), ) AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, AssetLibrary.Tab.TEXT, AssetLibrary.Tab.SHAPES, ), images = brandImagesCategory, ) } } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberImagesLibrary() } } } } } } }, onClose = onClose, ) } ``` Learn how asset sources, asset library categories, and dock buttons work together in the Android editor UI. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-import-media-asset-library-basics) On Android, configure the asset library through the editor's Kotlin `configuration` block. Asset sources live on the Engine, `AssetLibrary` defines the categories and sections shown in sheets, and dock buttons can open those categories for users. ```text User taps dock button | v AssetLibrary category and sections | v Engine asset source ``` This guide covers the basic flow for registering an asset source, surfacing it in an asset library category, and opening that category from the dock. For a complete product surface that already includes the editor UI, start from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) and apply the configuration shown here. All three layers use the same source ID: ```kotlin highlight-android-source-id private const val BRAND_IMAGE_SOURCE_ID = "ly.img.asset.source.brand.images" ``` ## Layer 1: Asset Source Asset sources provide the media data that the library queries. Register a source on the existing editor Engine, then add asset definitions that include the URI, thumbnail URI, MIME type, and block creation metadata. ```kotlin highlight-android-asset-source val assetEngine = editorContext.engine if (BRAND_IMAGE_SOURCE_ID !in assetEngine.asset.findAllSources()) { assetEngine.asset.addLocalSource( sourceId = BRAND_IMAGE_SOURCE_ID, supportedMimeTypes = listOf(MimeType.JPEG.key), ) assetEngine.asset.addAsset( sourceId = BRAND_IMAGE_SOURCE_ID, asset = AssetDefinition( id = "brand-background", label = mapOf("en" to "Brand Background"), tags = mapOf("en" to listOf("brand", "background")), groups = listOf("campaign"), 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", ), ), ) assetEngine.asset.assetSourceContentsChanged(sourceId = BRAND_IMAGE_SOURCE_ID) } ``` The sample uses a local image source for clarity. A remote source follows the same UI wiring, but its `findAssets` implementation fetches assets from your backend. ## Layer 2: Asset Library Category Inside `configuration`, set the `assetLibrary` property with `LibraryCategory` and `LibraryContent` values. A section connects one or more `AssetSourceType` values to the category that the sheet renders. ```kotlin highlight-android-library-category assetLibrary = { remember { val brandSourceType = AssetSourceType(sourceId = BRAND_IMAGE_SOURCE_ID) val brandImagesSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(brandSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = brandSourceType, assetType = AssetType.Image, ), ) val brandImagesCategory = LibraryCategory.Images.copy( content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_images, sections = listOf(brandImagesSection), ), ) AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, AssetLibrary.Tab.TEXT, AssetLibrary.Tab.SHAPES, ), images = brandImagesCategory, ) } } ``` Use `AssetLibrary.getDefault()` when you want to keep the standard editor behavior and adjust the tab order or one category. This sample replaces the Images category content so the sheet only queries the local source that the sample registers. Use your own Android string resources for app-specific category or section titles. ## Layer 3: Dock Button Dock buttons can be used as a source to open asset library sheets. The image button below opens `assetLibrary.images()`, so the custom image section becomes available from the dock. ```kotlin highlight-android-dock-button dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberImagesLibrary() } } } } } ``` You can build a small dock as shown here, or modify an existing dock list in a larger editor configuration. ## How Browsing and Insertion Work When a user opens a library sheet, the UI queries the configured source types through the Engine asset API. Search text is passed to sources through `FindAssetsQuery`, and selecting an asset lets the Engine apply the asset metadata to the scene. The important connection is the shared source ID: - The Engine source is registered with `BRAND_IMAGE_SOURCE_ID`. - The library section wraps that ID in `AssetSourceType`. - The dock button opens the category that contains that section. ## Key Types | Type | Purpose | | ---- | ------- | | `AssetDefinition` | Describes one asset in a local source, including localized labels and `meta` values. | | `AssetSourceType` | Wraps an Engine asset source ID for use in library content. | | `LibraryContent.Section` | Shows a horizontal preview row for one or more source types. | | `LibraryContent.Grid` | Shows the expanded asset grid for one source type. | | `LibraryCategory` | Defines one library category, including title, icons, and content. | | `AssetLibrary` | Groups the categories used by tabs, dock buttons, and replace sheets. | ## API Reference | Method | Purpose | | ------ | ------- | | `editorContext.engine.asset.findAllSources()` | Check which asset source IDs are already registered. | | `editorContext.engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Register a local source for assets added at runtime. | | `editorContext.engine.asset.addAsset(sourceId=_, asset=_)` | Add one asset definition to a local source. | | `editorContext.engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify the editor UI to re-query a source after its contents change. | | `AssetLibrary.getDefault(tabs=_, images=_)` | Create the default library while overriding selected categories. | | `LibraryCategory.Images.copy(content=_)` | Replace the default Images category content with source-backed sections. | | `LibraryContent.Sections(titleRes=_, sections=_)` | Group one or more section rows inside a library category. | | `Dock.remember(builder=_)` | Create a dock component for an editor configuration. | | `Dock.ListBuilder.remember(builder=_)` | Build the ordered list of dock items. | | `Dock.Button.rememberImagesLibrary(builder=_)` | Create a dock button that opens the image library category. | ## Next Steps - [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Adapt the asset library UI and behavior to suit your application's structure and user needs. - [Thumbnails](https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/) - Configure thumbnail images for assets in CE.SDK's asset library with proper sizing, preview URIs for audio, and customized UI display. - [Refresh Assets](https://img.ly/docs/cesdk/android/import-media/asset-library/refresh-assets-382060/) - Trigger asset reloads to ensure the library reflects newly uploaded or updated items. - [Asset Sources](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Create and configure Engine-level asset sources. --- ## 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: "Customize" description: "Adapt the asset library UI and behavior to suit your application's structure and user needs." platform: android url: "https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) > [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-asset-library/AssetLibraryEditorSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary @Composable fun AssetLibraryEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { assetLibrary = { remember { AssetLibrary.getDefault() } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-asset-library/DefaultAssetLibraryEditorSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.dropSection import ly.img.editor.core.library.replaceSection @Composable fun DefaultAssetLibraryEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { val remoteImageAssetSource = remember { RemoteImageAssetSource(assetBaseUri = "") } Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { editorContext.engine.asset.addSource(remoteImageAssetSource) } assetLibrary = { remember { val remoteImageSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ) AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, AssetLibrary.Tab.SHAPES, AssetLibrary.Tab.STICKERS, AssetLibrary.Tab.TEXT, ), images = LibraryCategory.Images .replaceSection(index = 0) { copy(count = 6) } .dropSection(index = 1) .addSection(remoteImageSection), ) } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-asset-library/CustomAssetLibraryEditorSolution.kt reference-only import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import kotlinx.coroutines.delay import ly.img.editor.Editor import ly.img.editor.core.R 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.iconpack.IconPack import ly.img.editor.core.iconpack.LibraryElements import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryCategory.Companion.sourceTypes import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.sheet.SheetType import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.assetBaseUri import ly.img.engine.populateAssetSource @Composable fun CustomAssetLibraryEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { val remoteImageAssetSource = remember { RemoteImageAssetSource(assetBaseUri = demoImageAssetBaseUri) } Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { editorContext.engine.asset.addSource(remoteImageAssetSource) } assetLibrary = { remember { // This category appears as a tab in the asset library sheet. val myAssetsCategory = LibraryCategory( tabTitleRes = R.string.ly_img_editor_asset_library_title_elements, tabSelectedIcon = IconPack.LibraryElements, tabUnselectedIcon = IconPack.LibraryElements, content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_elements, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_stickers, sourceTypes = LibraryContent.Stickers.sourceTypes, assetType = AssetType.Sticker, expandContent = LibraryContent.Stickers, ), LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ), ), ), ) AssetLibrary( tabs = { listOf( myAssetsCategory, LibraryCategory.Images, ) }, images = { val remoteImageSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ) // Replacement sheets can use a different category than the Add tab. LibraryCategory.Images.addSection(remoteImageSection) }, ) } } } }, onClose = onClose, ) } @Composable fun CustomAssetLibraryPreviewSolution( license: String, onClose: (Throwable?) -> Unit, ) { val remoteImageAssetSource = remember { RemoteImageAssetSource(assetBaseUri = demoImageAssetBaseUri) } 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(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) } onLoaded = { val defaultImageSourceId = "ly.img.image" if (defaultImageSourceId !in editorContext.engine.asset.findAllSources()) { @Suppress("DEPRECATION") editorContext.engine.populateAssetSource( id = defaultImageSourceId, jsonUri = Uri.parse("$demoImageAssetBaseUri/$defaultImageSourceId/content.json"), replaceBaseUri = Uri.parse(demoImageAssetBaseUri), ) } editorContext.engine.asset.addSource(remoteImageAssetSource) // Let the editor apply the runtime asset-library configuration before opening the screenshot sheet. delay(500) val previewCategory = LibraryCategory( tabTitleRes = R.string.ly_img_editor_asset_library_title_elements, tabSelectedIcon = IconPack.LibraryElements, tabUnselectedIcon = IconPack.LibraryElements, content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_elements, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ), ), ), ) editorContext.eventHandler.send( EditorEvent.Sheet.Open(SheetType.LibraryAdd(libraryCategory = previewCategory)), ) } assetLibrary = { remember { val previewCategory = LibraryCategory( tabTitleRes = R.string.ly_img_editor_asset_library_title_elements, tabSelectedIcon = IconPack.LibraryElements, tabUnselectedIcon = IconPack.LibraryElements, content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_elements, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ), ), ), ) AssetLibrary( tabs = { listOf( previewCategory, LibraryCategory.Images, ) }, ) } } } }, onClose = onClose, ) } @Suppress("DEPRECATION") private val demoImageAssetBaseUri: String get() = Engine.assetBaseUri.toString() ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-asset-library/RemoteImageAssetSource.kt reference-only import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetSource import ly.img.engine.DesignBlockType 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 class RemoteImageAssetSource( assetBaseUri: String, ) : AssetSource(sourceId = SOURCE_ID) { override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) override suspend fun getGroups(): List? = null override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = FindAssetsResult( assets = imageAssets, currentPage = query.page, nextPage = -1, total = imageAssets.size, ) private val imageBaseUri = assetBaseUri.trimEnd('/') private val imageAssets = listOf( Asset( id = "brand-background", context = AssetContext(sourceId = sourceId), label = "Brand Background", locale = "en", tags = listOf("background", "brand"), meta = mapOf( "uri" to "$imageBaseUri/ly.img.image/images/sample_1.jpg", "thumbUri" to "$imageBaseUri/ly.img.image/thumbnails/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 "720", ), ), Asset( id = "brand-landscape", context = AssetContext(sourceId = sourceId), label = "Brand Landscape", locale = "en", tags = listOf("landscape", "brand"), meta = mapOf( "uri" to "$imageBaseUri/ly.img.image/images/sample_10.jpg", "thumbUri" to "$imageBaseUri/ly.img.image/thumbnails/sample_10.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", ), ), ) private companion object { const val SOURCE_ID = "remote-image-assets" } } ``` Configure the Android asset library so the editor opens the tabs, sections, and replacement sheets that fit your app. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-asset-library) The asset library displays assets from registered asset sources. Sources provide the data, while `AssetLibrary`, `LibraryCategory`, and `LibraryContent` define how the CE.SDK editor UI presents those sources. This guide covers setting the asset library configuration, reordering default tabs, changing category sections, adding a registered custom source, and providing separate categories for add and replace flows. ## Configuration Asset library customization starts in `EditorConfiguration`. Set `assetLibrary` to return either the default library or a custom `AssetLibrary` instance. ```kotlin highlight-android-asset-library-configuration EditorConfiguration.remember { assetLibrary = { remember { AssetLibrary.getDefault() } } } ``` The default configuration uses `AssetLibrary.getDefault()`, which opens the Elements, Images, Text, Shapes, and Stickers tabs. Video and audio tabs are only included when you pass `includeAVResources = true`, while the video and audio category providers remain available for type-specific replacement flows. ## Register a Custom Source Before a custom source can appear in the asset library, implement an `AssetSource` that returns assets. This source receives an asset base URI from the app and returns deterministic image assets, but the same shape applies when your app loads metadata from its own storage service. ```kotlin highlight-android-remote-image-source class RemoteImageAssetSource( assetBaseUri: String, ) : AssetSource(sourceId = SOURCE_ID) { override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) override suspend fun getGroups(): List? = null override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = FindAssetsResult( assets = imageAssets, currentPage = query.page, nextPage = -1, total = imageAssets.size, ) private val imageBaseUri = assetBaseUri.trimEnd('/') private val imageAssets = listOf( Asset( id = "brand-background", context = AssetContext(sourceId = sourceId), label = "Brand Background", locale = "en", tags = listOf("background", "brand"), meta = mapOf( "uri" to "$imageBaseUri/ly.img.image/images/sample_1.jpg", "thumbUri" to "$imageBaseUri/ly.img.image/thumbnails/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 "720", ), ), Asset( id = "brand-landscape", context = AssetContext(sourceId = sourceId), label = "Brand Landscape", locale = "en", tags = listOf("landscape", "brand"), meta = mapOf( "uri" to "$imageBaseUri/ly.img.image/images/sample_10.jpg", "thumbUri" to "$imageBaseUri/ly.img.image/thumbnails/sample_10.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", ), ), ) private companion object { const val SOURCE_ID = "remote-image-assets" } } ``` Register the source with the editor engine after the editor loads. Replace the asset base URI with the root of your app's bundled assets or hosted image storage. ```kotlin highlight-android-custom-asset-source val remoteImageAssetSource = remember { RemoteImageAssetSource(assetBaseUri = "") } ``` ```kotlin highlight-android-register-asset-source onLoaded = { editorContext.engine.asset.addSource(remoteImageAssetSource) } ``` The `AssetSourceType` used in a library section must reference the same source ID as the registered source. ## Modify the Default Library Use `AssetLibrary.getDefault()` when you want to keep the default behavior but adjust tab order or category content. The sample removes the Elements tab, changes the order of the remaining tabs, changes the first image section's preview count, drops the gallery section, and appends the custom source. ```kotlin highlight-android-default-asset-library assetLibrary = { remember { val remoteImageSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ) AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, AssetLibrary.Tab.SHAPES, AssetLibrary.Tab.STICKERS, AssetLibrary.Tab.TEXT, ), images = LibraryCategory.Images .replaceSection(index = 0) { copy(count = 6) } .dropSection(index = 1) .addSection(remoteImageSection), ) } } ``` The `images` category is reused for both the Images tab and image replacement sheets, so changes to this category affect both flows. ## Create a Custom Category For deeper changes, create a `LibraryCategory` with your own title resource, selected and unselected tab icons, and nested `LibraryContent`. Sections can expand into default content or display a registered source directly. ```kotlin highlight-android-custom-category // This category appears as a tab in the asset library sheet. val myAssetsCategory = LibraryCategory( tabTitleRes = R.string.ly_img_editor_asset_library_title_elements, tabSelectedIcon = IconPack.LibraryElements, tabUnselectedIcon = IconPack.LibraryElements, content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_elements, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_stickers, sourceTypes = LibraryContent.Stickers.sourceTypes, assetType = AssetType.Sticker, expandContent = LibraryContent.Stickers, ), LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ), ), ), ) ``` Use Android string resources for tab and section labels. That keeps labels localizable through your app's normal Android resource workflow. ## Customize Add and Replace Flows The `AssetLibrary` constructor lets each entry point return a different category. In this sample, the Add sheet shows a custom Elements category and the default Images category as tabs, while image replacement uses an Images category with the custom source appended. ```kotlin highlight-android-custom-asset-library AssetLibrary( tabs = { listOf( myAssetsCategory, LibraryCategory.Images, ) }, images = { val remoteImageSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_images, sourceTypes = listOf(AssetSourceType(sourceId = remoteImageAssetSource.sourceId)), assetType = AssetType.Image, ) // Replacement sheets can use a different category than the Add tab. LibraryCategory.Images.addSection(remoteImageSection) }, ) ``` The editor chooses replacement categories by selected block type and fill type. For example, image fills use `images`, video fills use `videos`, stickers use `stickers`, and audio blocks use `audios`. ## Troubleshooting **Custom source section is empty:** Confirm that the source was registered with `editorContext.engine.asset.addSource()` and that the section uses a matching `AssetSourceType(sourceId = ...)`. **Tab order did not change:** Check the `tabs` list passed to `AssetLibrary.getDefault()` or the `tabs` lambda passed to `AssetLibrary`. **Section helper throws at runtime:** `addSection()`, `dropSection()`, and `replaceSection()` only work on categories whose `content` is `LibraryContent.Sections`. **Labels are wrong or missing:** Use valid Android string resources for `tabTitleRes`, `LibraryContent.Sections.titleRes`, and `LibraryContent.Section.titleRes`. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `EditorConfiguration.remember(builder=_)` | Configuration | Create an editor configuration and set the asset library. | | `editorContext.engine.asset.addSource(source=_)` | Assets | Register a custom asset source with the engine. | | `AssetSource(sourceId=_)` | Assets | Define a custom source that the engine can query for assets. | | `AssetSource.findAssets(query=_)` | Assets | Return paginated assets for the current search, group, and page query. | | `AssetSource.getGroups()` | Assets | Return available group identifiers for grouped source content, or `null` for ungrouped content. | | `AssetLibrary.getDefault(tabs=_, images=_)` | Asset Library | Keep the default library and override selected categories or tab order. | | `AssetLibrary(tabs=_, images=_)` | Asset Library | Build a custom library with separate providers for add and replace flows. | | `LibraryCategory.addSection(section=_)` | Asset Library | Append a section to a category backed by `LibraryContent.Sections`. | | `LibraryCategory.dropSection(index=_)` | Asset Library | Remove a section from a category backed by `LibraryContent.Sections`. | | `LibraryCategory.replaceSection(index=_, sectionReducer=_)` | Asset Library | Replace one section while keeping the rest of the category. | ## Key Types | Type | Category | Purpose | | --- | --- | --- | | `Asset` | Assets | Describe one asset returned by a source. | | `AssetContext` | Assets | Associate an asset with the source that returned it. | | `FindAssetsQuery` | Assets | Pass pagination, search, tag, locale, and group filters into `findAssets()`. | | `FindAssetsResult` | Assets | Return paginated asset results from `findAssets()`. | | `AssetSourceType` | Assets | Reference a registered asset source from library content. | | `LibraryCategory` | Asset Library | Define one tab or type-specific replacement category. | | `LibraryContent.Sections` | Asset Library | Group multiple preview sections inside a category. | | `LibraryContent.Section` | Asset Library | Display a preview row and optional drill-down content for one or more sources. | | `LibraryContent.Grid` | Asset Library | Display assets from a source in a grid view. | ## Next Steps - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Explore the core functionality of the asset library and how users browse, search, and insert media. - [Thumbnails](https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/) — Configure thumbnail images for assets in CE.SDK's asset library with proper sizing, preview URIs for audio, and customized UI display. - [Refresh Assets](https://img.ly/docs/cesdk/android/import-media/asset-library/refresh-assets-382060/) — Trigger asset reloads to ensure the library reflects newly uploaded or updated items. - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) — Configure CE.SDK to load engine and content assets from your own servers instead of the IMG.LY CDN for production deployments. - [From Your Server](https://img.ly/docs/cesdk/android/import-media/from-remote-source/your-server-b91910/) — Load images, videos, and audio from your backend servers into CE.SDK for integration with CMS, DAM, or custom asset management systems. --- ## 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: "Refresh Assets" description: "Refresh visible Editor asset-library content after an external catalog changes." platform: android url: "https://img.ly/docs/cesdk/android/import-media/asset-library/refresh-assets-382060/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) > [Refresh Assets](https://img.ly/docs/cesdk/android/import-media/asset-library/refresh-assets-382060/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-asset-library-refresh-assets/RefreshAssets.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withTimeout import kotlinx.coroutines.yield import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetSource import ly.img.engine.DesignBlockType 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 private const val EXTERNAL_IMAGE_SOURCE_ID = "my-external-images" @Composable fun RefreshAssetsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { val externalSource = remember { createExternalImageAssetSource() } Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { val engine = editorContext.engine if (externalSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(externalSource) } } assetLibrary = { remember { val externalSourceType = AssetSourceType(sourceId = EXTERNAL_IMAGE_SOURCE_ID) val externalImagesSection = LibraryContent.Section( sourceTypes = listOf(externalSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = externalSourceType, assetType = AssetType.Image, title = "External images", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(externalImagesSection), ) } } } }, onClose = onClose, ) } suspend fun refreshAssets( engine: Engine, awaitEngineEvents: suspend () -> Unit = {}, ): RefreshAssetsResult = coroutineScope { val externalSource = createExternalImageAssetSource() if (externalSource.sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = externalSource.sourceId) } try { engine.asset.addSource(externalSource) // The smoke test pumps the offscreen engine so native asset events are delivered deterministically. awaitEngineEvents() val initialAssets = engine.asset.findAssets( sourceId = externalSource.sourceId, query = FindAssetsQuery(page = 0, perPage = 20), ) check(initialAssets.total == 2) val sourceUpdates = engine.asset.onAssetSourceUpdated() val refreshEvents = async(start = CoroutineStart.UNDISPATCHED) { sourceUpdates .take(1) .toList() } // Give callbackFlow a turn to register the native subscription before the first smoke-test refresh. yield() externalSource.assets = externalSource.assets + listOf( externalImageAsset( id = "winter-lookbook", label = "Winter Lookbook", tags = listOf("lookbook", "winter"), uri = "https://img.ly/static/ubq_samples/sample_3.jpg", ), ) engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId) // Pump the offscreen smoke-test engine after each refresh notification. awaitEngineEvents() val afterUploadAssets = engine.asset.findAssets( sourceId = externalSource.sourceId, query = FindAssetsQuery(page = 0, perPage = 20), ) externalSource.assets = externalSource.assets.map { asset -> if (asset.id == "spring-poster") { asset.copy(label = "Spring Launch Poster") } else { asset } } engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId) // Pump the offscreen smoke-test engine after each refresh notification. awaitEngineEvents() val renamedAssetLabel = engine.asset.findAssets( sourceId = externalSource.sourceId, query = FindAssetsQuery(page = 0, perPage = 20), ).assets.firstOrNull { asset -> asset.id == "spring-poster" }?.label externalSource.assets = externalSource.assets.filterNot { asset -> asset.id == "winter-lookbook" } engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId) // Pump the offscreen smoke-test engine after each refresh notification. awaitEngineEvents() val remainingAssetIds = engine.asset.findAssets( sourceId = externalSource.sourceId, query = FindAssetsQuery(page = 0, perPage = 20), ).assets.map { asset -> asset.id } val refreshEventSourceIds = awaitRefreshEvents(refreshEvents, awaitEngineEvents) RefreshAssetsResult( sourceId = externalSource.sourceId, initialTotal = initialAssets.total, afterUploadTotal = afterUploadAssets.total, renamedAssetLabel = renamedAssetLabel, remainingAssetIds = remainingAssetIds, refreshEventSourceIds = refreshEventSourceIds, ) } finally { if (externalSource.sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = externalSource.sourceId) } } } private fun createExternalImageAssetSource() = ExternalImageAssetSource( initialAssets = listOf( externalImageAsset( id = "spring-poster", label = "Spring Poster", tags = listOf("poster", "spring"), uri = "https://img.ly/static/ubq_samples/sample_1.jpg", ), externalImageAsset( id = "summer-poster", label = "Summer Poster", tags = listOf("poster", "summer"), uri = "https://img.ly/static/ubq_samples/sample_2.jpg", ), ), ) private fun externalImageAsset( id: String, label: String, tags: List, uri: String, ) = Asset( id = id, context = AssetContext(sourceId = EXTERNAL_IMAGE_SOURCE_ID), label = label, locale = "en", tags = tags, groups = listOf("campaign-assets"), meta = mapOf( "uri" to uri, "thumbUri" to uri, "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", ), ) private class ExternalImageAssetSource( initialAssets: List, ) : AssetSource(sourceId = EXTERNAL_IMAGE_SOURCE_ID) { var assets = initialAssets override val supportedMimeTypes = listOf(MimeType.JPEG.key) override suspend fun getGroups(): List? = listOf("campaign-assets") override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = FindAssetsResult( assets = assets, currentPage = query.page, nextPage = -1, total = assets.size, ) } private suspend fun awaitRefreshEvents( refreshEvents: Deferred>, awaitEngineEvents: suspend () -> Unit, ): List { // The offscreen smoke test drives engine updates manually until the native event flow emits a refresh signal. withTimeout(5_000) { while (!refreshEvents.isCompleted) { awaitEngineEvents() yield() } } return refreshEvents.await() } ``` ```kotlin file=@cesdk_android_examples/engine-guides-import-media-asset-library-refresh-assets/RefreshAssetsResult.kt reference-only data class RefreshAssetsResult( val sourceId: String, val initialTotal: Int, val afterUploadTotal: Int, val renamedAssetLabel: String?, val remainingAssetIds: List, val refreshEventSourceIds: List, ) ``` Keep an open Android Editor asset library in sync after your app changes an external asset catalog. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-asset-library-refresh-assets) When an external upload flow, backend job, or sync process changes a custom asset source, update the backing catalog and any cache first. Then call `engine.asset.assetSourceContentsChanged(sourceId=_)` with that source's exact ID. After the engine starts, the Android Editor observes this notification automatically. If an open library category is currently showing a `LibraryContent.Grid` for that source, or `LibraryContent.Sections` containing that source, the Editor clears and re-queries the visible content in place. Users do not need to close and reopen the library. This guide configures a custom `AssetSource` in the Editor, shows provider-neutral external upload, update, and delete examples, and keeps the engine update flow as an optional integration signal. ## When to Use Asset Refresh The Editor's in-place response is automatic after the engine receives a source-change notification. Sending that notification is only necessary for changes that CE.SDK cannot observe directly. **No manual notification** is needed for source operations that already notify the engine, such as CE.SDK-managed upload and delete handlers. **Manual notification** is needed when another system changes the source data: - An external upload component stores a new file and updates your app catalog. - A backend process changes asset labels, tags, thumbnails, or metadata. - A sync message tells the app that another user added or removed assets. - A polling job detects that the source's remote version changed. ## Registering a Custom Asset Source Start with a stable source ID. This is the same value you pass to `assetSourceContentsChanged(sourceId=_)` later. ```kotlin highlight-android-source-id private const val EXTERNAL_IMAGE_SOURCE_ID = "my-external-images" ``` The sample uses a small helper to create image assets with metadata that CE.SDK can read from the source. ```kotlin highlight-android-asset-helper private fun externalImageAsset( id: String, label: String, tags: List, uri: String, ) = Asset( id = id, context = AssetContext(sourceId = EXTERNAL_IMAGE_SOURCE_ID), label = label, locale = "en", tags = tags, groups = listOf("campaign-assets"), meta = mapOf( "uri" to uri, "thumbUri" to uri, "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", ), ) ``` Define a source whose `findAssets(query=_)` reads from your current catalog. The source can implement filtering, paging, and remote loading as needed; this guide keeps the source shape minimal so the refresh calls stay visible. ```kotlin highlight-android-custom-source private class ExternalImageAssetSource( initialAssets: List, ) : AssetSource(sourceId = EXTERNAL_IMAGE_SOURCE_ID) { var assets = initialAssets override val supportedMimeTypes = listOf(MimeType.JPEG.key) override suspend fun getGroups(): List? = listOf("campaign-assets") override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = FindAssetsResult( assets = assets, currentPage = query.page, nextPage = -1, total = assets.size, ) } ``` The sample source keeps its catalog in memory so the refresh behavior is easy to verify. In production, `findAssets(query=_)` usually reads from your database, cache, or app repository. Create the source instance with the initial assets. The engine-only helper used by the smoke test does this directly: ```kotlin highlight-android-create-source val externalSource = createExternalImageAssetSource() ``` The engine-only helper registers that source directly before observing update signals: ```kotlin highlight-android-register-source engine.asset.addSource(externalSource) ``` ## Configure the Source in the Editor Create one source instance, register it in `EditorConfiguration.onLoaded`, and add the same stable source ID to the asset-library configuration. ```kotlin highlight-android-editor-configuration @Composable fun RefreshAssetsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { val externalSource = remember { createExternalImageAssetSource() } Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { val engine = editorContext.engine if (externalSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(externalSource) } } assetLibrary = { remember { val externalSourceType = AssetSourceType(sourceId = EXTERNAL_IMAGE_SOURCE_ID) val externalImagesSection = LibraryContent.Section( sourceTypes = listOf(externalSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = externalSourceType, assetType = AssetType.Image, title = "External images", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(externalImagesSection), ) } } } }, onClose = onClose, ) } ``` `onLoaded` runs with a started engine, so the custom source can be registered through `editorContext.engine`. The `AssetSourceType` used by both the section and its expanded grid has the same `EXTERNAL_IMAGE_SOURCE_ID` as the source itself and as every later refresh call. When the Images category is open at its sections view, a change to this source refreshes those visible sections. When the external-images grid is open, the same notification refreshes that grid in place. ## Refresh Scope The Editor refreshes narrowly to avoid unnecessary source queries: - Only active, open library categories are eligible for immediate refresh. - Only the currently displayed grid or sections are checked. - A grid refreshes only when its source ID exactly matches the emitted ID. - Sections refresh when at least one displayed section references the emitted source ID. - Content showing unrelated source IDs and inactive or closed content is not immediately re-queried. Closed content reads the current source data through its normal query path when the user opens it later. No close-and-reopen workaround is needed for content that is already visible and references the changed source. ## Observe the Engine Signal (Optional) `onAssetSourceUpdated()` returns a `Flow` of updated source IDs. The Editor already collects this flow for its asset library, so applications do not need a second collector to enable UI refresh. Collect it from an app-owned coroutine scope only when other application state also needs to react. Multiple refreshes for the same source can be coalesced before the next engine update. Treat each event as a signal that the source changed rather than as a receipt for one specific call. ```kotlin highlight-android-observe-refresh val sourceUpdates = engine.asset.onAssetSourceUpdated() ``` ## Refreshing After External Uploads The next snippets mutate the sample's in-memory catalog directly. In production, replace those assignments with the upload, database, cache, or sync operation that changes your source data. When an upload completes outside CE.SDK, make the uploaded file and its catalog entry available, update or invalidate the cache used by `findAssets(query=_)`, and then call `assetSourceContentsChanged(sourceId=_)` once for the affected source. ```kotlin highlight-android-refresh-upload externalSource.assets = externalSource.assets + listOf( externalImageAsset( id = "winter-lookbook", label = "Winter Lookbook", tags = listOf("lookbook", "winter"), uri = "https://img.ly/static/ubq_samples/sample_3.jpg", ), ) engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId) ``` If the source is visible in the Editor, the notification causes an immediate in-place query. Calling it before the external write or cache update finishes can therefore reload stale data. ## Refreshing After External Modifications Use the same ordering after changing asset metadata such as labels, tags, thumbnails, or dimensions: update the backing catalog and cache first, then notify the exact source ID. ```kotlin highlight-android-refresh-modification externalSource.assets = externalSource.assets.map { asset -> if (asset.id == "spring-poster") { asset.copy(label = "Spring Launch Poster") } else { asset } } engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId) ``` Batch related metadata changes together when possible, then call `assetSourceContentsChanged(sourceId=_)` once after the batch completes. ## Refreshing After External Deletions After removing an asset from your external catalog and cache, notify CE.SDK so the visible library query removes the stale entry in place. ```kotlin highlight-android-refresh-deletion externalSource.assets = externalSource.assets.filterNot { asset -> asset.id == "winter-lookbook" } engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId) ``` This prevents users from selecting an asset that your backend or app repository no longer serves. ## Integration Patterns Use the refresh call at the boundary where your app knows the external catalog changed: | Pattern | When to Refresh | | --- | --- | | Upload callback | After the uploaded file, catalog entry, and cache update are complete | | Backend sync | After applying the confirmed add, update, or delete to local backing data | | Polling | After a version check detects a change and the local cache is updated | | Batch import | Once after all imported assets and metadata have been written | Avoid refreshing after every item in a large batch. One notification after the batch gives observers a single source update instead of forcing repeated queries. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Visible assets do not update | The notification ID differs from the ID in the displayed `AssetSourceType` | Use the exact same stable source ID for registration, library content, assets, and notification | | No immediate library query occurs | The category is closed or inactive, or its currently displayed content references another source | This is expected; only visible active content referencing the changed source refreshes immediately | | Query still returns old data | The external operation or cache update had not finished when the notification was sent | Update the backing catalog and cache before calling `assetSourceContentsChanged(sourceId=_)` | | Source still returns stale assets | `findAssets(query=_)` serves a cache that was not invalidated | Invalidate or update that cache before sending the notification | | Refresh throws | The source is not registered anymore | Confirm the source exists with `findAllSources()` before refreshing optional sources | ## Key Types | Type | Purpose | | --- | --- | | `AssetSource` | Base class for a custom Android asset source | | `EditorConfiguration` | Registers the source after engine start and configures Editor behavior | | `AssetLibrary` | Defines the categories shown in the Editor asset library | | `AssetSourceType` | Connects displayed library content to an exact engine source ID | | `LibraryContent` | Defines the grid or sections that can be refreshed in place | | `Asset` | Asset result returned from `findAssets(query=_)` | | `FindAssetsQuery` | Search, paging, tag, and group filters for a source query | | `FindAssetsResult` | Paged result returned by a source query | ## API Reference | Method | Description | | --- | --- | | `EditorConfiguration.onLoaded` | Register a custom source after the Editor engine starts | | `engine.asset.addSource(source=_)` | Register a custom asset source | | `AssetSource.findAssets(query=_)` | Return the current assets for a query | | `AssetSource.getGroups()` | Return the groups exposed by the source | | `engine.asset.findAllSources()` | List registered source IDs before optional refresh or cleanup | | `engine.asset.onAssetSourceUpdated()` | Optionally observe source IDs emitted by refresh notifications | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify CE.SDK after a source's backing data and cache have changed | ## Next Steps - [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) — Understand the asset system, custom sources, and source events. - [From Unsplash](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) — See a complete custom source backed by remote data. - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Configure which asset sources appear in the editor 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: "Thumbnails" description: "Configure thumbnail images for assets in the Android asset library with thumbUri, audio previewUri, and custom source mapping." platform: android url: "https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) > [Thumbnails](https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/) --- ```kotlin file=@cesdk_android_examples/engine-guides-asset-library-thumbnails/AssetLibraryThumbnailsSummary.kt reference-only import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetDefinition import ly.img.engine.AssetSource import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.FindAssetsQuery import ly.img.engine.FindAssetsResult import ly.img.engine.ShapeType private const val ASSET_BASE_URI = "https://cdn.img.ly/packages/imgly/cesdk-android/1.83.0/assets" private const val IMAGE_SOURCE_ID = "ly.img.guides.asset-library-thumbnails.images" private const val AUDIO_SOURCE_ID = "ly.img.guides.asset-library-thumbnails.audio" private const val REMOTE_SOURCE_ID = "ly.img.guides.asset-library-thumbnails.remote" data class AssetLibraryThumbnailsSummary( val imageThumbUri: String, val audioThumbUri: String, val audioPreviewUri: String, val remoteAssetCount: Int, val remoteThumbUris: List, ) suspend fun assetLibraryThumbnails(engine: Engine): AssetLibraryThumbnailsSummary { listOf(IMAGE_SOURCE_ID, AUDIO_SOURCE_ID, REMOTE_SOURCE_ID).forEach { sourceId -> if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId) } } val remoteSource = ThumbnailAssetSource() try { engine.asset.addLocalSource( sourceId = IMAGE_SOURCE_ID, supportedMimeTypes = listOf("image/jpeg"), ) val imageAsset = AssetDefinition( id = "sample-landscape", label = mapOf("en" to "Landscape Photo"), tags = mapOf("en" to listOf("landscape", "thumbnail")), meta = mapOf( "uri" to "$ASSET_BASE_URI/ly.img.image/images/sample_1.jpg", "thumbUri" to "$ASSET_BASE_URI/ly.img.image/thumbnails/sample_1.jpg", "mimeType" to "image/jpeg", "kind" to "image", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "width" to "1767", "height" to "1178", ), ) engine.asset.addAsset(sourceId = IMAGE_SOURCE_ID, asset = imageAsset) engine.asset.assetSourceContentsChanged(sourceId = IMAGE_SOURCE_ID) engine.asset.addLocalSource( sourceId = AUDIO_SOURCE_ID, supportedMimeTypes = listOf("audio/x-m4a"), ) val audioAsset = AssetDefinition( id = "short-audio-preview", label = mapOf("en" to "Short Audio Preview"), meta = mapOf( "uri" to "$ASSET_BASE_URI/ly.img.audio/audios/far_from_home.m4a", "thumbUri" to "$ASSET_BASE_URI/ly.img.audio/thumbnails/audio-wave.png", "previewUri" to "$ASSET_BASE_URI/ly.img.audio/audios/far_from_home.m4a", "mimeType" to "audio/x-m4a", "blockType" to DesignBlockType.Audio.key, "duration" to "98.716009", ), ) engine.asset.addAsset(sourceId = AUDIO_SOURCE_ID, asset = audioAsset) engine.asset.assetSourceContentsChanged(sourceId = AUDIO_SOURCE_ID) engine.asset.addSource(remoteSource) val queriedImage = engine.asset.findAssets( sourceId = IMAGE_SOURCE_ID, query = FindAssetsQuery(perPage = 1, page = 0), ).assets.first() val queriedAudio = engine.asset.findAssets( sourceId = AUDIO_SOURCE_ID, query = FindAssetsQuery(perPage = 1, page = 0), ).assets.first() val remoteAssets = engine.asset.findAssets( sourceId = REMOTE_SOURCE_ID, query = FindAssetsQuery(perPage = 10, page = 0), ) return AssetLibraryThumbnailsSummary( imageThumbUri = queriedImage.meta?.get("thumbUri").orEmpty(), audioThumbUri = queriedAudio.meta?.get("thumbUri").orEmpty(), audioPreviewUri = queriedAudio.meta?.get("previewUri").orEmpty(), remoteAssetCount = remoteAssets.total, remoteThumbUris = remoteAssets.assets.mapNotNull { it.meta?.get("thumbUri") }, ) } finally { listOf(IMAGE_SOURCE_ID, AUDIO_SOURCE_ID, REMOTE_SOURCE_ID).forEach { sourceId -> if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId) } } } } private class ThumbnailAssetSource : AssetSource(sourceId = REMOTE_SOURCE_ID) { override val supportedMimeTypes = listOf("image/jpeg") override suspend fun getGroups(): List = listOf("remote") override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult { val pageAssets = remotePhotos.drop(query.page * query.perPage).take(query.perPage) return FindAssetsResult( assets = pageAssets.map { photo -> Asset( id = photo.id, context = AssetContext(sourceId = sourceId), label = photo.label, locale = "en", groups = listOf("remote"), meta = mapOf( "uri" to photo.fullSizeUri, "thumbUri" to photo.thumbnailUri, "mimeType" to "image/jpeg", "kind" to "image", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "width" to photo.width.toString(), "height" to photo.height.toString(), ), ) }, currentPage = query.page, nextPage = if ((query.page + 1) * query.perPage < remotePhotos.size) query.page + 1 else -1, total = remotePhotos.size, ) } private val remotePhotos = listOf( RemotePhoto( id = "remote-landscape", label = "Remote Landscape", fullSizeUri = "$ASSET_BASE_URI/ly.img.image/images/sample_4.jpg", thumbnailUri = "$ASSET_BASE_URI/ly.img.image/thumbnails/sample_4.jpg", width = 1178, height = 1767, ), RemotePhoto( id = "remote-water", label = "Remote Water", fullSizeUri = "$ASSET_BASE_URI/ly.img.image/images/sample_5.jpg", thumbnailUri = "$ASSET_BASE_URI/ly.img.image/thumbnails/sample_5.jpg", width = 1767, height = 1262, ), ) } private data class RemotePhoto( val id: String, val label: String, val fullSizeUri: String, val thumbnailUri: String, val width: Int, val height: Int, ) ``` Configure the thumbnail metadata that the CE.SDK Android asset library uses to show image, video, audio, and remote-source assets. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-asset-library-thumbnails) Thumbnails make asset sources easier to browse before users add content to a scene. On Android, the editor UI reads thumbnail metadata from each asset result; asset source and metadata configuration happen through the Engine Asset API. ## Understanding thumbUri vs previewUri Three URI fields can appear in an asset's `meta` map: | Property | Purpose | Android Behavior | | --- | --- | --- | | `thumbUri` | Image used as the asset card thumbnail | Displayed in the asset library UI; falls back to `uri` when omitted | | `previewUri` | Lightweight preview resource | Used by audio asset cards for playback; image assets can also copy it to image fill preview data | | `uri` | Final asset resource | Used when the asset is applied to the scene | Use `thumbUri` for images, videos, and audio waveforms. Use `previewUri` for audio assets when the full `uri` is too large for quick preview playback. The Android editor UI falls back to `uri` if an audio asset has no `previewUri`. ## Thumbnail Configuration ### Basic Thumbnails Add `thumbUri` to the asset metadata before adding the asset to a local source. Use a thumbnail around 512px wide so the same source looks sharp in grid cards without loading the full-resolution asset. ```kotlin highlight-android-basic-thumbnails engine.asset.addLocalSource( sourceId = IMAGE_SOURCE_ID, supportedMimeTypes = listOf("image/jpeg"), ) val imageAsset = AssetDefinition( id = "sample-landscape", label = mapOf("en" to "Landscape Photo"), tags = mapOf("en" to listOf("landscape", "thumbnail")), meta = mapOf( "uri" to "$ASSET_BASE_URI/ly.img.image/images/sample_1.jpg", "thumbUri" to "$ASSET_BASE_URI/ly.img.image/thumbnails/sample_1.jpg", "mimeType" to "image/jpeg", "kind" to "image", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "width" to "1767", "height" to "1178", ), ) engine.asset.addAsset(sourceId = IMAGE_SOURCE_ID, asset = imageAsset) engine.asset.assetSourceContentsChanged(sourceId = IMAGE_SOURCE_ID) ``` Call `assetSourceContentsChanged` after changing a source that may already be visible so the editor UI can refresh its asset list. ### Preview URIs for Audio Audio assets should include a visual thumbnail and a previewable audio resource. The `thumbUri` points to an image such as a waveform, while `previewUri` points to the short audio clip the Android asset card plays. ```kotlin highlight-android-audio-preview-uri engine.asset.addLocalSource( sourceId = AUDIO_SOURCE_ID, supportedMimeTypes = listOf("audio/x-m4a"), ) val audioAsset = AssetDefinition( id = "short-audio-preview", label = mapOf("en" to "Short Audio Preview"), meta = mapOf( "uri" to "$ASSET_BASE_URI/ly.img.audio/audios/far_from_home.m4a", "thumbUri" to "$ASSET_BASE_URI/ly.img.audio/thumbnails/audio-wave.png", "previewUri" to "$ASSET_BASE_URI/ly.img.audio/audios/far_from_home.m4a", "mimeType" to "audio/x-m4a", "blockType" to DesignBlockType.Audio.key, "duration" to "98.716009", ), ) engine.asset.addAsset(sourceId = AUDIO_SOURCE_ID, asset = audioAsset) engine.asset.assetSourceContentsChanged(sourceId = AUDIO_SOURCE_ID) ``` The `uri` remains the full asset used when the audio is inserted. If the preview and final audio are the same file, keep both fields explicit so the source can later switch to a shorter preview without changing the asset shape. ### Custom Asset Source Thumbnails For remote sources, map your backend response into CE.SDK asset results in `findAssets`. The important part is to return `meta.thumbUri` next to the full `meta.uri` for every visual asset. ```kotlin highlight-android-custom-source-thumbnails override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult { val pageAssets = remotePhotos.drop(query.page * query.perPage).take(query.perPage) return FindAssetsResult( assets = pageAssets.map { photo -> Asset( id = photo.id, context = AssetContext(sourceId = sourceId), label = photo.label, locale = "en", groups = listOf("remote"), meta = mapOf( "uri" to photo.fullSizeUri, "thumbUri" to photo.thumbnailUri, "mimeType" to "image/jpeg", "kind" to "image", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "width" to photo.width.toString(), "height" to photo.height.toString(), ), ) }, currentPage = query.page, nextPage = if ((query.page + 1) * query.perPage < remotePhotos.size) query.page + 1 else -1, total = remotePhotos.size, ) } ``` The sample keeps the remote data local, but the same mapping applies when `findAssets` calls your backend, cache, or storage service. ## Display Customization The Android asset library applies thumbnail layout from the asset type: - Image, video, filter, effect, and similar visual assets render as cropped grid cards. - Audio assets render as rows with a thumbnail, label, and play control. - Text and typeface assets use text-focused rows rather than image grids. Entry-level layout properties such as `gridBackgroundType`, `previewBackgroundType`, `cardBackgroundPreferences`, `gridColumns`, and `gridItemHeight` are not public Android APIs. To change which sources appear in tabs and sheets, configure the [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/); thumbnail rendering itself is handled by the CE.SDK editor UI. ## Best Practices - Use image files for `thumbUri`; a 512px-wide thumbnail is a good default for quality and memory use. - Use JPEG for photos and PNG when the thumbnail needs transparency or waveform-style graphics. - Include `mimeType`, `blockType`, and the relevant type metadata so applying assets does not require MIME sniffing. - Use `previewUri` for audio preview playback; do not use it as an alternate thumbnail format. - Keep thumbnail URLs cacheable and stable, especially for remote asset sources. ## Troubleshooting **Thumbnails do not appear** Verify that `meta.thumbUri` points to an image URL that Android can load. If `thumbUri` is missing, the editor falls back to `meta.uri`, which may be a large file or a non-image media type. **Audio preview does not play** Check that the asset appears in an audio library section, has an audio `mimeType`, and provides either `previewUri` or `uri` for playback. **The card crop is not what you expected** Android chooses thumbnail scaling from the asset type. If you need different sections, tabs, or source grouping, customize the asset library configuration rather than relying on unsupported entry layout fields. ## API Reference | Method | Purpose | | --- | --- | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create a local source that can hold `AssetDefinition` entries with thumbnail metadata. | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add an asset definition, including `meta.thumbUri` and optional `meta.previewUri`, to a local source. | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify CE.SDK that a source changed after assets were added or updated. | | `engine.asset.addSource(source=_)` | Register a custom `AssetSource` that returns thumbnail-ready asset results. | | `AssetSource.findAssets(query=_)` | Map remote or generated data into `FindAssetsResult` pages. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query a source and inspect the returned asset metadata. | ## Next Steps - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Configure the Android asset library structure. - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Understand asset sources and metadata. - [Unsplash Integration](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) - Map remote image results into CE.SDK assets. - [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) - Provide multiple asset resolutions for drawing and export. --- ## 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: "Capture From Camera" description: "Capture photos or videos directly from a connected camera for immediate use in your design." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) --- --- ## Related Pages - [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) - Enable live camera capture in Android apps to shoot and insert photos or videos. - [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) - Set up the visual interface and behavior when capturing with the IMG.LY Camera. - [Record Video](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-video-47819b/) - Display a live Android camera feed inside CE.SDK with PixelStreamFill and apply real-time effects. - [Record Reaction](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-reaction-42e4c5/) - Record a user's reaction while a base video plays in the CE.SDK mobile camera. - [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) - Manage access to recorded videos or reactions for playback or editing. - [Take Photo](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/take-photo-f13803/) - Capture a still photo with the IMGLY Mobile Camera in photo mode and receive the captured image. - [Access Photos](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/photos-190c3a/) - Read still-photo captures returned by the IMG.LY Camera on Android. - [Zoom](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/zoom-2f2b32/) - Learn how pinch zoom works in the Android Mobile Camera. --- ## 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: "Mobile Camera Configuration" description: "Set up the visual interface and behavior when capturing with the IMG.LY Camera." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) --- ```kotlin file=@cesdk_android_examples/camera-guides-configuration/ConfiguredCameraActivity.kt reference-only import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import ly.img.camera.core.CameraConfiguration import ly.img.camera.core.CameraMode import ly.img.camera.core.CameraResult import ly.img.camera.core.CaptureCount import ly.img.camera.core.CaptureMedia import ly.img.camera.core.CaptureType import ly.img.camera.core.EngineConfiguration import ly.img.camera.core.videos import kotlin.time.Duration.Companion.seconds private const val TAG = "ConfiguredCameraActivity" fun createConfiguredCameraInput( license: String?, userId: String?, ) = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = license, // null or an empty string opens the camera in evaluation (watermark) mode userId = userId, ), cameraConfiguration = CameraConfiguration( recordingColor = Color.Blue, maxTotalDuration = 30.seconds, allowExceedingMaxDuration = false, ), cameraMode = CameraMode.Standard(), ) @Composable fun ConfiguredCameraScreen( license: String?, userId: String? = null, ) { val cameraInput = createConfiguredCameraInput( license = license, userId = userId, ) val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> when (result) { null -> Log.i(TAG, "Camera dismissed") is CameraResult.Captures -> { val videoUris = result.captures.videos.flatMap { recording -> recording.videos.map { it.uri } } Log.i(TAG, "Captured ${result.captures.size} item(s); video URIs: $videoUris") } is CameraResult.Reaction -> Log.i(TAG, "Reaction with ${result.reaction.size} recording(s)") else -> Log.i(TAG, "Unhandled result: $result") } } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } class ConfiguredCameraActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ConfiguredCameraScreen( license = null, userId = "", ) } } } // A single-photo session: take one photo and return immediately, skipping the confirm/discard preview. val photoConfiguration = CameraConfiguration( captureType = CaptureType.Photo, captureCount = CaptureCount.Single, showsPhotoPreview = false, ) ``` Apply a `CameraConfiguration` to the IMG.LY Camera to choose what it captures, tune its recording color and limits, pick a camera mode, and localize the strings it shows to the user. ![The configured IMG.LY Camera running on Android.](./assets/camera-configuration-android.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/camera-guides-configuration) This guide builds on the [Integrate Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) guide and customizes the camera from the `ly.img.camera.core` module. You configure it through the three inputs you pass to `CaptureMedia.Input`: `EngineConfiguration` initializes the underlying engine, an optional `CameraConfiguration` controls what the camera captures and how it looks, and `cameraMode` selects the capture mode. ## Engine Configuration `EngineConfiguration` initializes the underlying engine and is required to build the camera input. ```kotlin highlight-android-engine-configuration engineConfiguration = EngineConfiguration( license = license, // null or an empty string opens the camera in evaluation (watermark) mode userId = userId, ), ``` - `license` – the license key you received from IMG.LY. Pass `null` or an empty string to evaluate the camera in watermark mode. - `userId` – an optional unique ID tied to your application's user. This helps us accurately calculate monthly active users (MAU), which is especially useful when one person uses the app on multiple devices with a sign-in feature, ensuring they're counted once. The default value is `null`. ## Camera Configuration Pass an optional `CameraConfiguration` to customize the camera's appearance and recording behavior. Every property has a default value, so you only set the ones you want to change. ```kotlin highlight-android-camera-configuration cameraConfiguration = CameraConfiguration( recordingColor = Color.Blue, maxTotalDuration = 30.seconds, allowExceedingMaxDuration = false, ), ``` - `recordingColor` – the color of the record button while recording, and of the other recording indicators. Defaults to a muted red (`Color(0xFFDE6F62)`). - `maxTotalDuration` – the target duration of the recording. Defaults to `Duration.INFINITE`, which places no limit on the recording length. In `CameraMode.Reaction`, this value is ignored — the recording is capped by the duration of the video being reacted to. - `allowExceedingMaxDuration` – set to `true` to keep recording past `maxTotalDuration`. The segment visualization still uses the target duration, but the limit is not enforced. Defaults to `false`. In `CameraMode.Reaction`, this is ignored and always behaves as if set to `false`. ### Capture Type and Count These properties control what the camera captures, how many captures a session produces, and how photo captures are handled: - `captureType` – `CaptureType.Video` (default) records videos, `CaptureType.Photo` takes still photos, and `CaptureType.Mixed` shows an in-camera toggle so the user can switch between photo and video during the session. - `captureCount` – `CaptureCount.Multi` (default) lets the user stack several captures into one session; `CaptureCount.Single` returns after a single capture. - `photoClipDuration` – the duration stamped on each captured photo when it is later treated as a clip. Defaults to `5.seconds`. - `showsPhotoPreview` – when `false`, photo captures are committed immediately instead of showing a full-screen confirm/discard preview. Defaults to `true`. ```kotlin highlight-android-capture-type val photoConfiguration = CameraConfiguration( captureType = CaptureType.Photo, captureCount = CaptureCount.Single, showsPhotoPreview = false, ) ``` ## Camera Mode The mode is not part of `CameraConfiguration` — it is a separate `cameraMode` argument you pass to `CaptureMedia.Input`. ```kotlin highlight-android-camera-mode cameraMode = CameraMode.Standard(), ``` ### Available Modes - `CameraMode.Standard()` – the regular camera. This is the default. - `CameraMode.Reaction(video, cameraLayoutMode, positionsSwapped)` – records with the camera while a video plays back. - `video` – the `Uri` of the video to react to. - `cameraLayoutMode` – arranges the two feeds, either `CameraLayoutMode.Vertical` (default) or `CameraLayoutMode.Horizontal`. - `positionsSwapped` – when `true`, the camera feed and the reacted-to video swap positions. Defaults to `false`. Not every mode supports every capture type: `Standard` works with any `CaptureType`, while `Reaction` records video only. `CaptureMedia.Input` validates the pairing when you construct it and throws if the mode can't produce the requested capture type, so check `mode.supports(captureType)` first when the two come from user input. ## Launching the Camera Register the camera with `rememberLauncherForActivityResult` using the `CaptureMedia` contract, then call `launch` with your `CaptureMedia.Input`. The contract delivers a `CameraResult?` — `null` when the user dismisses the camera without capturing, otherwise one of: - `CameraResult.Captures(captures)` for any non-reaction session. Each `Capture` is a `Capture.Photo` or a `Capture.Video`; the `List.videos` extension pulls out just the `Recording`s. - `CameraResult.Reaction(video, reaction)` for reaction mode, pairing the reacted-to video with the user's recordings. ```kotlin highlight-android-result val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> when (result) { null -> Log.i(TAG, "Camera dismissed") is CameraResult.Captures -> { val videoUris = result.captures.videos.flatMap { recording -> recording.videos.map { it.uri } } Log.i(TAG, "Captured ${result.captures.size} item(s); video URIs: $videoUris") } is CameraResult.Reaction -> Log.i(TAG, "Reaction with ${result.reaction.size} recording(s)") else -> Log.i(TAG, "Unhandled result: $result") } } ``` ## Required Permissions The camera captures from the device camera and microphone. The `ly.img:camera` module already declares the `CAMERA` and `RECORD_AUDIO` permissions in its manifest, and the camera screen requests them at runtime the first time it needs them — so you don't add anything to your app's manifest or write permission-handling code. `CaptureType.Photo` sessions request only `CAMERA`; `CaptureType.Video` and `CaptureType.Mixed` sessions request both `CAMERA` and `RECORD_AUDIO`. ## Localization The IMG.LY camera ships with English and German on Android, and exposes its strings through Android string resources so you can override existing values or add new languages. All camera keys live in [https://github.com/imgly/cesdk-android/blob/v$UBQ\_VERSION$/sources/camera-core/src/main/res/values/strings.xml](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/camera-core/src/main/res/values/strings.xml) and follow a strict naming convention that makes locating them self-explanatory. For example, `ly_img_camera_timer_option_off` is the timer-off button, and `ly_img_camera_dialog_delete_last_recording_title` is the title of the alert shown when deleting the last recording. ### Replacing Existing Keys To change any existing string, copy its key into your app module's `res/values/strings.xml` and set your value. Your app's resources take precedence over the ones bundled with the SDK, so your value wins. ### Supporting New Languages To support a language the camera doesn't ship with, copy the English `strings.xml` into `res/values-/strings.xml` and translate the values. ## Troubleshooting - **The camera rejects the input:** Check that `cameraMode.supports(cameraConfiguration.captureType)` returns `true`. Reaction mode supports video capture only. - **Recording stops at an unexpected time:** In standard mode, `maxTotalDuration` defines the target and `allowExceedingMaxDuration` determines whether recording may continue. Reaction mode always follows the source video's duration. - **The camera cannot start:** Confirm that the device has an available camera and that the user grants the requested camera and microphone permissions. ## API Reference | API | Purpose | | --- | --- | | `EngineConfiguration(license=_, userId=_)` | Configure the CE.SDK license and optional user identifier used by the camera. | | `CameraConfiguration(recordingColor=_, maxTotalDuration=_, allowExceedingMaxDuration=_)` | Configure the recording indicators and duration behavior. | | `CameraConfiguration(captureType=_, captureCount=_, showsPhotoPreview=_)` | Configure the media type, number of captures, and photo confirmation flow. | | `CaptureMedia.Input(engineConfiguration=_, cameraConfiguration=_, cameraMode=_)` | Combine the engine, camera, and mode settings passed to the camera contract. | | `CameraMode.Standard()` | Create the standard camera mode. | | `cameraMode.supports(captureType=_)` | Check whether a mode supports a capture type before constructing the input. | | `rememberLauncherForActivityResult(contract=_, onResult=_)` | Register the `CaptureMedia` activity-result contract in Compose. | | `cameraLauncher.launch(input=_)` | Open the camera with the configured input. | | `result.captures.videos` | Read video recordings from a standard camera result. | Key result and configuration types include `CameraResult.Captures`, `CameraResult.Reaction`, `CaptureType`, and `CaptureCount`. ## Next Steps - [Integrate Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) — Add the camera to your app and present it. - [Record Reaction](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-reaction-42e4c5/) — Record a reaction while playing back a video. - [Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) — Send the captured media into the video 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: "Integrate Mobile Camera" description: "Enable live camera capture in Android apps to shoot and insert photos or videos." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) --- ```kotlin file=@cesdk_android_examples/camera-guides-quickstart/CameraActivity.kt reference-only import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.compose.material3.Button import androidx.compose.material3.Text import ly.img.camera.core.CameraResult import ly.img.camera.core.Capture import ly.img.camera.core.CaptureMedia import ly.img.camera.core.EngineConfiguration private const val TAG = "CameraActivity" private fun handleCameraResult(result: CameraResult?) { result ?: run { Log.i(TAG, "Camera dismissed") return } when (result) { is CameraResult.Captures -> { result.captures.forEach { capture -> when (capture) { is Capture.Photo -> { Log.i(TAG, "Captured photo: ${capture.uri}") } is Capture.Video -> { val videoUris = capture.recording.videos.map { it.uri } Log.i(TAG, "Recorded video: $videoUris") } } } } else -> { Log.i(TAG, "Unhandled result: $result") } } } class CameraActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = "YOUR_CESDK_LICENSE_KEY", userId = "", ), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handleCameraResult(result) } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } } } ``` Add the CE.SDK mobile camera to an Android app, launch it through the Activity Result API, and handle captured photo or video results. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/camera-guides-quickstart/) The mobile camera is provided by the `ly.img:camera` package. It opens a camera Activity, requests the required runtime permissions, and returns captured media to your app as a `CameraResult`. ## Add the Camera Package Add the IMG.LY Maven repository to `settings.gradle` so Gradle can resolve the camera artifact: ```groovy maven { name "IMG.LY Artifactory" url "https://maven.img.ly/maven" mavenContent { includeGroup("ly.img") } } ``` Then add the camera dependency to your app module: ```groovy implementation "ly.img:camera:$UBQ_VERSION$" ``` Use the same CE.SDK version for the camera package and any other `ly.img` packages in your app. If your project already uses Jetpack Compose dependencies, keep the Compose BOM at `2023.05.01` or newer. ## Meet Android Requirements The mobile camera requires an Android app module with these settings: | Requirement | Value | | --- | --- | | Minimum SDK | `minSdk 24` or higher | | UI toolkit | Jetpack Compose enabled with `buildFeatures { compose true }` | | Kotlin | `1.9.10` or higher | | Supported ABIs | `arm64-v8a`, `armeabi-v7a`, `x86_64`, and `x86` | The camera package contributes `android.permission.CAMERA` and `android.permission.RECORD_AUDIO` through its manifest. At runtime, the camera UI checks and requests the permissions it needs; photo-only sessions request camera access only, while video and mixed sessions also need microphone access. ## Configure the Camera Input Create a `CaptureMedia.Input` before launching the camera. The input carries the `EngineConfiguration` used by the camera's internal engine instance. ```kotlin highlight-android-create-input val cameraInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = "YOUR_CESDK_LICENSE_KEY", userId = "", ), ) ``` Use a CE.SDK license key from your IMG.LY dashboard. You can request a trial license at [img.ly/forms/contact-sales](https://img.ly/forms/contact-sales). The optional `userId` helps count monthly active users accurately when signed-in users work across multiple devices. ## Register the Camera Launcher Register the `CaptureMedia` contract inside your Compose content. The contract returns `null` when the user dismisses the camera and a `CameraResult` when they finish capturing media. ```kotlin highlight-android-register-launcher val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handleCameraResult(result) } ``` ## Launch the Camera Start the camera from your own UI by passing the input to the launcher. ```kotlin highlight-android-launch-camera cameraLauncher.launch(cameraInput) ``` The default `CaptureMedia.Input` opens the standard camera in video mode. To start with photo capture, mixed photo/video capture, or duration limits, pass a `CameraConfiguration` to the input. To launch reaction mode and handle `CameraResult.Reaction`, follow the [Record Reaction](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-reaction-42e4c5/) guide. ## Handle Captured Media Handle `CameraResult.Captures` to receive the ordered capture stack. Each entry is either a `Capture.Photo` with a file URI or a `Capture.Video` with one or more recorded video tracks. ```kotlin highlight-android-handle-result private fun handleCameraResult(result: CameraResult?) { result ?: run { Log.i(TAG, "Camera dismissed") return } when (result) { is CameraResult.Captures -> { result.captures.forEach { capture -> when (capture) { is Capture.Photo -> { Log.i(TAG, "Captured photo: ${capture.uri}") } is Capture.Video -> { val videoUris = capture.recording.videos.map { it.uri } Log.i(TAG, "Recorded video: $videoUris") } } } } else -> { Log.i(TAG, "Unhandled result: $result") } } } ``` Captured files are written to your app's storage. Copy any URI that must survive beyond the current app-owned cache or files lifecycle into your own persistent storage before inserting it into a scene or uploading it. If you integrate the camera into the CE.SDK editor UI, the editor's camera dock button uses the same `CaptureMedia` contract internally and inserts `CameraResult.Captures` into the active scene. Add `ly.img:camera` next to the editor dependency so that button can open the mobile camera. ## Test on Android Devices The Android Emulator can verify that the Activity launches and permission flow appears, but hardware camera behavior, focus, audio capture, and orientation handling should be tested on a physical device. Guard app-specific fallback flows for emulators when your product requires real camera input. ## Key APIs | API | Purpose | | --- | --- | | `CaptureMedia()` | Activity Result contract that opens the CE.SDK mobile camera. | | `CaptureMedia.Input(engineConfiguration=_, cameraConfiguration=_, cameraMode=_)` | Defines the engine setup, optional camera behavior, and optional camera mode for one launch. | | `EngineConfiguration(license=_, userId=_)` | Provides the camera engine license and optional user identifier. | ## Key Types | Type | Purpose | | --- | --- | | `CameraResult.Captures` | Result case for standard photo, video, or mixed capture sessions. | | `Capture.Photo` | A still photo capture with a file URI and clip duration. | | `Capture.Video` | A video capture that wraps one recording. | ## Next Steps - [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) - Set up camera permissions and behavior when capturing within CE.SDK. - [Take Photo](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/take-photo-f13803/) - Learn how to capture a photo with the Mobile Camera. - [Record Video](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-video-47819b/) - Display live camera feeds inside CE.SDK using PixelStreamFill. - [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) - Manage access to recorded videos or reactions. --- ## 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: "Access Photos" description: "Read still-photo captures returned by the IMG.LY Camera on Android." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/photos-190c3a/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Access Photos](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/photos-190c3a/) --- ```kotlin file=@cesdk_android_examples/camera-guides-photos/PhotosCamera.kt reference-only import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import ly.img.camera.core.CameraConfiguration import ly.img.camera.core.CameraResult import ly.img.camera.core.Capture import ly.img.camera.core.CaptureMedia import ly.img.camera.core.CaptureType import ly.img.camera.core.EngineConfiguration private const val TAG = "PhotosCamera" fun photoCameraInput(): CaptureMedia.Input = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, // Use your production license for release builds. userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Photo, ), ) @Composable fun PhotosCameraButton() { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handlePhotoCameraResult(result) } Button(onClick = { cameraLauncher.launch(photoCameraInput()) }) { Text(text = "Open Camera") } } fun handlePhotoCameraResult(result: CameraResult?) { result ?: run { Log.i(TAG, "Camera returned no successful result") return } when (result) { is CameraResult.Captures -> { result.captures.filterIsInstance().forEach { photo -> Log.i(TAG, "Photo URI: ${photo.uri}; clip duration: ${photo.clipDuration}") } } is CameraResult.Reaction -> { Log.i(TAG, "Reaction mode does not return photo captures") } else -> { Log.i(TAG, "Unhandled camera result: $result") } } } fun mixedCameraInput(): CaptureMedia.Input = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Mixed, ), ) fun handleMixedCameraResult(result: CameraResult?) { result ?: return if (result !is CameraResult.Captures) return result.captures.forEach { capture -> when (capture) { is Capture.Photo -> { Log.i(TAG, "Photo URI: ${capture.uri}; clip duration: ${capture.clipDuration}") } is Capture.Video -> { Log.i(TAG, "Video duration: ${capture.recording.duration}") } } } } ``` Read still-photo captures returned by the IMG.LY Camera and understand the lifetime of their app-local files. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/camera-guides-photos) This guide assumes that you already use the standalone camera contract from [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/). It focuses on the result payload from photo and mixed capture sessions, not on camera permissions or general camera UI setup. ## Configure Photo Capture Create a `CaptureMedia.Input` with a `CameraConfiguration` whose `captureType` is `CaptureType.Photo`. The default `CameraMode.Standard` supports photo capture; reaction mode supports video capture only. ```kotlin highlight-android-configure-photo-capture fun photoCameraInput(): CaptureMedia.Input = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, // Use your production license for release builds. userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Photo, ), ) ``` Use that input with `CaptureMedia` through the Activity Result API. The button below calls the configuration helper and forwards the camera result to the photo handler shown in the next section. ```kotlin highlight-android-launch-photo-capture @Composable fun PhotosCameraButton() { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handlePhotoCameraResult(result) } Button(onClick = { cameraLauncher.launch(photoCameraInput()) }) { Text(text = "Open Camera") } } ``` ## Read Photo Results The `CaptureMedia` callback receives `CameraResult?`. A `null` value means the contract did not receive a successful `RESULT_OK` payload. This can represent user cancellation, another result code, or missing result data. A successful standard photo session returns `CameraResult.Captures`. ```kotlin highlight-android-read-photo-results fun handlePhotoCameraResult(result: CameraResult?) { result ?: run { Log.i(TAG, "Camera returned no successful result") return } when (result) { is CameraResult.Captures -> { result.captures.filterIsInstance().forEach { photo -> Log.i(TAG, "Photo URI: ${photo.uri}; clip duration: ${photo.clipDuration}") } } is CameraResult.Reaction -> { Log.i(TAG, "Reaction mode does not return photo captures") } else -> { Log.i(TAG, "Unhandled camera result: $result") } } } ``` `CameraResult.Captures.captures` preserves the order in which the user captured items. Each `Capture.Photo` exposes: - `uri` - the JPEG file written by the camera. - `clipDuration` - the duration assigned to that photo when the capture stack is later treated as editor clips. ## Handle Mixed Capture Use `CaptureType.Mixed` when the same session can return both photos and videos. The result still uses `CameraResult.Captures`, so branch on each `Capture` entry and keep video handling close to your recordings workflow. ```kotlin highlight-android-handle-mixed-captures fun mixedCameraInput(): CaptureMedia.Input = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Mixed, ), ) fun handleMixedCameraResult(result: CameraResult?) { result ?: return if (result !is CameraResult.Captures) return result.captures.forEach { capture -> when (capture) { is Capture.Photo -> { Log.i(TAG, "Photo URI: ${capture.uri}; clip duration: ${capture.clipDuration}") } is Capture.Video -> { Log.i(TAG, "Video duration: ${capture.recording.duration}") } } } } ``` This guide only extracts still photos from the mixed stack. For video and reaction result handling, continue with [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/). ## Understand Photo File Lifetime The Android camera writes each JPEG directly into your application's `Application.filesDir`. A successful result therefore already points to persistent app-internal storage: the file survives the camera session and normal process restarts. Copying it to another path under the same `filesDir` does not make it more durable. The file remains private to your app and is removed if your app deletes it, the user clears app data, or the app is uninstalled. If the photo needs a different lifetime or owner, copy or upload it to a genuinely different destination, such as user-selected document storage, `MediaStore`, or your backend. Perform that I/O outside the Activity Result callback's main-thread work. ## Handle Dismissal and Capture Problems A `null` Activity Result means no successful camera result was returned. It can represent user cancellation, a non-`RESULT_OK` result, or missing result data, so handle it as a no-result outcome without claiming that the user necessarily dismissed the camera. The camera library declares its required permission and presents the Android permission UI. If no camera view appears, confirm that camera access is granted and that the device exposes a compatible camera. If a shutter attempt does not produce a new `Capture.Photo`, keep the previous result state unchanged and let the user retry; do not invent a URI or persist a missing capture. Use Android logs to investigate CameraX image-capture or device-capability failures. ## Result Fields | Field | Purpose | | ----- | ------- | | `CameraResult.Captures.captures` | Ordered list of captures from photo, video, or mixed standard camera sessions. | | `Capture.Photo.uri` | URI of the captured JPEG image. | | `Capture.Photo.clipDuration` | Duration assigned to the photo when CE.SDK treats it as a clip later. | | `Capture.Video.recording` | Video entry returned alongside photos when `CaptureType.Mixed` is enabled. | ## API Reference | API | Purpose | | --- | ------- | | `CaptureMedia()` | Activity Result contract that launches the IMG.LY Camera and returns `CameraResult?`. | | `CaptureMedia.Input(engineConfiguration=_, cameraConfiguration=_)` | Input object passed to the camera contract. | | `EngineConfiguration(license=_, userId=_)` | Basic engine configuration used by the standalone camera. | | `CameraConfiguration(captureType=_)` | Chooses whether the camera captures photos, videos, or both. | | `rememberLauncherForActivityResult(contract=_)` | Registers the camera contract from Compose and receives the result callback. | | `cameraLauncher.launch(_)` | Starts the camera with the configured `CaptureMedia.Input`. | ## Next Steps - [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) - Configure capture type, capture count, preview behavior, and other camera options. - [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) - Read video and reaction recordings returned by the camera. --- ## 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 a user's reaction while a base video plays in the CE.SDK mobile camera." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-reaction-42e4c5/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Record Reaction](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-reaction-42e4c5/) --- ```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) } ``` Launch the CE.SDK mobile camera in Reaction mode and collect the base video plus every recorded reaction clip. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-record-reaction) This guide uses the CE.SDK mobile camera in Reaction mode. For adding the camera to your app and the permissions it requires, see [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/); for the full set of camera options, see [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/). To compose the returned recordings into an editable picture-in-picture video scene, continue with [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) in the video-creation guides. Reaction mode plays a provided video while the front camera and microphone record the user's response. The camera returns the base video separately from one or more reaction recordings — the source clips rather than an automatically composited export — so your app can decide whether to persist the files, place the reaction as a picture-in-picture clip, or process the clips in another workflow. Android exposes `CameraMode.Standard` and `CameraMode.Reaction`; dual camera is not available in the Android camera API. ## Launch Reaction Mode Register `CaptureMedia` with `rememberLauncherForActivityResult`, then launch it with `CameraMode.Reaction`. The `video` parameter is the base video that plays while the user records. ```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) } } ``` The session starts on the front camera so the user's face is in frame right away. Recording is capped at the base video's duration — `CameraConfiguration.maxTotalDuration` is ignored in Reaction mode, so a reaction can never run longer than the video it responds to. Reaction mode supports video capture only. `CaptureMedia.Input` validates the selected `cameraMode` against the configured capture type and rejects unsupported combinations, such as photo capture with `CameraMode.Reaction`. ## Choose the Layout `cameraLayoutMode` takes a `CameraLayoutMode`: `Vertical` stacks the two feeds on top of each other, `Horizontal` places them side by side. The base video plays in the top or left half and the camera preview fills the other half. Pass `positionsSwapped = true` to start the session with the two positions exchanged. ## Handle the Result `CaptureMedia` returns `null` when the user dismisses the camera. A successful Reaction session returns `CameraResult.Reaction`; other result cases belong to Standard or mixed capture flows and can be ignored here. ```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 separates the base video from the reaction recordings: | Value | Meaning | | ----- | ------- | | `CameraResult.Reaction.video` | The base `Video` that was played during recording. | | `CameraResult.Reaction.reaction` | A list of `Recording` segments captured from the front camera and microphone. | | `Recording.videos` | The videos inside each segment. Reaction recordings contain a single video. | | `CameraResult.Reaction.video.uri` | The original base video URI passed to `CameraMode.Reaction`. | | `Recording.videos.first().uri` | The app-local URI for a generated reaction recording file. | | `Video.rect` | The preview rectangle used by the camera layout. | Reaction recording file URIs point to app-local files. Copy those generated files into your own storage location if you need them after the camera session, especially before passing them to a background upload or long-running editor workflow. The base video URI remains the URI your app passed into `CameraMode.Reaction`. See [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) for the general recording result shape. To turn the result into an editable scene, hand the base video and reaction recordings to the editor and compose the picture-in-picture layout there — [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) in the video-creation guides walks through that flow. ## Troubleshooting ### Incomplete Reaction Clips A pause/resume session produces multiple reaction recordings. Process the whole `reaction` list instead of reading only the first entry. ### Audio Echo The base video's audio can be picked up by the microphone. Lower the playback volume in your camera experience or suggest headphones when echo would hurt the recording. ## Related Types | Type | Purpose | | ---- | ------- | | `CameraMode.Reaction` | Configures Reaction mode with a base video, layout mode, and optional swapped positions. | | `CameraResult.Reaction` | Returns the base video and the reaction recordings. | | `Recording` | Describes one captured segment and its duration. | | `Video` | Describes a video URI and preview rectangle. | ## API Reference | API | Purpose | | --- | ------- | | `CaptureMedia()` | Creates the Activity Result contract for the CE.SDK camera. | | `CaptureMedia.Input(engineConfiguration=_, cameraConfiguration=_, cameraMode=_)` | Configures the camera launch request; `cameraConfiguration` carries the capture type validated against the mode. | | `EngineConfiguration(license=_, userId=_)` | Supplies license and user identity information to the camera engine. | | `CameraMode.Reaction(video=_, cameraLayoutMode=_, positionsSwapped=_)` | Starts Reaction mode with the base video and layout settings. | | `rememberLauncherForActivityResult(contract=_, onResult=_)` | Registers the launcher that opens the camera and receives `CameraResult?`. | ## Next Steps - [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) - Compose the base video and reaction clips into an editable picture-in-picture video scene. - [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) - Add the mobile camera to your Android app. - [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) - Configure capture behavior, duration limits, and camera UI options. - [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) - Read and persist captured video recordings. --- ## 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 Video" description: "Display a live Android camera feed inside CE.SDK with PixelStreamFill and apply real-time effects." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-video-47819b/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Record Video](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/record-video-47819b/) --- ```groovy file=@cesdk_android_examples/engine-guides-using-camera/build.gradle reference-only import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { id 'com.android.application' } android { namespace "ly.img.editor.camera" compileSdk 37 defaultConfig { applicationId "ly.img.editor.camera" minSdk 24 targetSdk 37 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 languageVersion = KotlinVersion.KOTLIN_1_9 apiVersion = KotlinVersion.KOTLIN_1_9 } } dependencies { implementation "ly.img:engine-camera:1.83.0" implementation "androidx.camera:camera-core:1.5.0-alpha04" implementation "androidx.camera:camera-camera2:1.5.0-alpha04" implementation "androidx.camera:camera-view:1.5.0-alpha04" implementation "androidx.camera:camera-lifecycle:1.5.0-alpha04" implementation "androidx.camera:camera-video:1.5.0-alpha04" implementation "ly.img:engine:1.83.0" implementation "androidx.activity:activity:1.7.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" } ``` ```kotlin file=@cesdk_android_examples/engine-guides-using-camera/src/main/java/ly/img/editor/camera/UsingCamera.kt reference-only package ly.img.editor.camera import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.util.Log import android.util.Size import android.view.SurfaceView import androidx.activity.ComponentActivity import androidx.camera.core.CameraSelector import androidx.camera.core.MirrorMode import androidx.camera.core.Preview import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.core.resolutionselector.ResolutionStrategy import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.video.FileOutputOptions import androidx.camera.video.Quality import androidx.camera.video.QualitySelector import androidx.camera.video.Recorder import androidx.camera.video.Recording import androidx.camera.video.VideoCapture import androidx.camera.video.VideoRecordEvent import androidx.core.content.ContextCompat import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import ly.img.engine.DesignBlockType import ly.img.engine.EffectType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.camera.setCameraPreview import java.io.File import java.io.IOException private const val TAG = "UsingCamera" suspend fun recordVideoFromCamera( engine: Engine, activity: ComponentActivity, surfaceView: SurfaceView, cameraProvider: ProcessCameraProvider, ) = withContext(Dispatchers.Main.immediate) { val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_FRONT) .build() val previewSize = Size(1920, 1080) val resolutionSelector = ResolutionSelector.Builder() .setResolutionStrategy( ResolutionStrategy( previewSize, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER, ), ) .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY) .build() val preview = Preview.Builder() .setResolutionSelector(resolutionSelector) .build() val qualitySelector = QualitySelector.from(Quality.FHD) val recorder = Recorder.Builder() .setQualitySelector(qualitySelector) .build() val videoCapture = VideoCapture.Builder(recorder) .setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY) .build() cameraProvider.bindToLifecycle(activity, cameraSelector, preview, videoCapture) val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) // Request a 16:9 FHD preview target. CameraX may choose the closest supported size. val cameraWidth = 1920F val cameraHeight = 1080F engine.block.setWidth(block = scene, value = cameraWidth) engine.block.setHeight(block = scene, value = cameraHeight) engine.block.setWidth(block = page, value = cameraWidth) engine.block.setHeight(block = page, value = cameraHeight) val pixelStreamFill = engine.block.createFill(FillType.PixelStream) engine.block.setFill(block = page, fill = pixelStreamFill) engine.setCameraPreview(pixelStreamFill, preview, mirrored = true) engine.block.appendEffect( block = page, effectBlock = engine.block.createEffect(EffectType.HalfTone), ) engine.scene.zoomToBlock( block = page, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) val recordingFile = File(surfaceView.context.filesDir, "temp.mp4") val fileOutputOptions = FileOutputOptions.Builder(recordingFile).build() val finalizeEvent = CompletableDeferred() var recording: Recording? = null var recordingStopped = false val finalizedRecording = try { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { throw SecurityException("Grant RECORD_AUDIO before enabling audio recording.") } recording = videoCapture.output .prepareRecording(activity, fileOutputOptions) .withAudioEnabled() .start(ContextCompat.getMainExecutor(surfaceView.context)) { event: VideoRecordEvent -> if (event !is VideoRecordEvent.Finalize) return@start finalizeEvent.complete(event) } // Stop after five seconds for this sample. Replace this with your app's own recording controls. delay(5000L) recordingStopped = true recording.stop() finalizeEvent.await() } finally { withContext(NonCancellable) { try { recording?.let { if (!recordingStopped) { recordingStopped = true it.stop() } finalizeEvent.await() } } finally { cameraProvider.unbind(preview, videoCapture) } } } if (finalizedRecording.hasError()) { val exception = IllegalStateException( "CameraX recording failed with error ${finalizedRecording.error}", finalizedRecording.cause, ) Log.e(TAG, "CameraX recording failed with error ${finalizedRecording.error}", exception) throw exception } val recordingUri = finalizedRecording.outputResults.outputUri requireNonEmptyRecording(surfaceView.context, recordingUri) val videoFill = engine.block.createFill(FillType.Video) engine.block.setFill(block = page, fill = videoFill) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = recordingUri, ) } private suspend fun requireNonEmptyRecording( context: Context, uri: Uri, ) = withContext(Dispatchers.IO) { val hasData = try { context.contentResolver.openInputStream(uri)?.use { input -> input.read() != -1 } ?: false } catch (exception: IOException) { throw IllegalStateException("CameraX finalized recording is not readable: $uri", exception) } check(hasData) { "CameraX finalized recording is missing or empty: $uri" } } ``` Beyond pre-recorded video, you can pipe a live CameraX feed into the engine with a `PixelStreamFill`. The feed composes with the rest of your scene, so the engine's effects apply to the live preview in real time. When recording finishes, swap the fill for a `VideoFill` to play the captured file back. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-using-camera) On Android, if you want IMG.LY's ready-made camera Activity instead of wiring your own capture pipeline, see [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/). This guide takes the engine-level path. A `PixelStreamFill` accepts raw camera frames, so you can drive it from any camera pipeline. The sample uses the `engine-camera` extension to bridge CameraX preview frames into CE.SDK, applies an effect to the preview, records with CameraX `VideoCapture`, and swaps in a `VideoFill` for playback once recording stops. ## Create a Video Scene with a Camera Fill Create a video scene with a single page, assign a `PixelStreamFill` to the page, and append an effect to confirm that the live preview composes with the engine's rendering. The sample sizes the page to the same 16:9 FHD target used by CameraX so the feed is not stretched. ```kotlin highlight-android-create-preview val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) // Request a 16:9 FHD preview target. CameraX may choose the closest supported size. val cameraWidth = 1920F val cameraHeight = 1080F engine.block.setWidth(block = scene, value = cameraWidth) engine.block.setHeight(block = scene, value = cameraHeight) engine.block.setWidth(block = page, value = cameraWidth) engine.block.setHeight(block = page, value = cameraHeight) val pixelStreamFill = engine.block.createFill(FillType.PixelStream) engine.block.setFill(block = page, fill = pixelStreamFill) engine.setCameraPreview(pixelStreamFill, preview, mirrored = true) engine.block.appendEffect( block = page, effectBlock = engine.block.createEffect(EffectType.HalfTone), ) engine.scene.zoomToBlock( block = page, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) ``` The `engine.setCameraPreview(...)` helper connects a CameraX `Preview` to the pixel stream fill and updates the fill as frames arrive. ## Control Feed Orientation Transforming every frame manually is wasteful. Instead, `PixelStreamFill` exposes a `fill/pixelStream/orientation` property that the GPU applies during rendering. Pass `mirrored = true` to `setCameraPreview(...)` for a front-facing preview; the extension combines that flag with CameraX transformation information. Supported orientation values are: | Value | Effect | | --- | --- | | `Up` | No rotation (default) | | `Down` | 180 degree rotation | | `Left` | 90 degree counter-clockwise | | `Right` | 90 degree clockwise | | `UpMirrored` | Horizontal flip | | `DownMirrored` | 180 degree rotation plus horizontal flip | | `LeftMirrored` | 90 degree counter-clockwise plus horizontal flip | | `RightMirrored` | 90 degree clockwise plus horizontal flip | Override `fill/pixelStream/orientation` with `engine.block.setEnum(...)` only when your app owns a fixed-orientation camera pipeline and intentionally bypasses CameraX orientation updates. ## Access the Camera Add the `engine-camera` extension together with the CameraX modules used for preview and recording. Keep `engine` and `engine-camera` on the same CE.SDK version. ```groovy highlight-android-dependencies implementation "ly.img:engine-camera:1.83.0" implementation "androidx.camera:camera-core:1.5.0-alpha04" implementation "androidx.camera:camera-camera2:1.5.0-alpha04" implementation "androidx.camera:camera-view:1.5.0-alpha04" implementation "androidx.camera:camera-lifecycle:1.5.0-alpha04" implementation "androidx.camera:camera-video:1.5.0-alpha04" implementation "ly.img:engine:1.83.0" implementation "androidx.activity:activity:1.7.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" ``` Build a CameraX front-camera selector, a `Preview` use case, and a `VideoCapture` use case. CameraX may choose the closest supported preview size for the device, while the matching aspect ratio keeps the CE.SDK page aligned with the feed. ```kotlin highlight-android-setup-camera val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_FRONT) .build() val previewSize = Size(1920, 1080) val resolutionSelector = ResolutionSelector.Builder() .setResolutionStrategy( ResolutionStrategy( previewSize, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER, ), ) .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY) .build() val preview = Preview.Builder() .setResolutionSelector(resolutionSelector) .build() val qualitySelector = QualitySelector.from(Quality.FHD) val recorder = Recorder.Builder() .setQualitySelector(qualitySelector) .build() val videoCapture = VideoCapture.Builder(recorder) .setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY) .build() cameraProvider.bindToLifecycle(activity, cameraSelector, preview, videoCapture) ``` The sample uses `MirrorMode.MIRROR_MODE_ON_FRONT_ONLY` so the recorded front camera video matches the natural selfie orientation. ## Update the Fill with Video Frames `setCameraPreview(...)` receives CameraX preview frames and writes them into the `PixelStreamFill`. Because the feed is a regular fill, engine features such as effects apply to the live preview just as they would to any other block. Call the sample's suspending entry point from an Activity- or Fragment-owned coroutine scope so cancellation follows the visible camera surface. The sample switches to `Dispatchers.Main.immediate` before touching CameraX or Engine APIs. ## Stop and Play Back the Recording Start CameraX recording with `VideoCapture.output.prepareRecording(...)`, enable audio, and wait for CameraX to finalize the file before unbinding the camera use cases. ```kotlin highlight-android-record-video val recordingFile = File(surfaceView.context.filesDir, "temp.mp4") val fileOutputOptions = FileOutputOptions.Builder(recordingFile).build() val finalizeEvent = CompletableDeferred() var recording: Recording? = null var recordingStopped = false val finalizedRecording = try { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { throw SecurityException("Grant RECORD_AUDIO before enabling audio recording.") } recording = videoCapture.output .prepareRecording(activity, fileOutputOptions) .withAudioEnabled() .start(ContextCompat.getMainExecutor(surfaceView.context)) { event: VideoRecordEvent -> if (event !is VideoRecordEvent.Finalize) return@start finalizeEvent.complete(event) } // Stop after five seconds for this sample. Replace this with your app's own recording controls. delay(5000L) recordingStopped = true recording.stop() finalizeEvent.await() } finally { withContext(NonCancellable) { try { recording?.let { if (!recordingStopped) { recordingStopped = true it.stop() } finalizeEvent.await() } } finally { cameraProvider.unbind(preview, videoCapture) } } } if (finalizedRecording.hasError()) { val exception = IllegalStateException( "CameraX recording failed with error ${finalizedRecording.error}", finalizedRecording.cause, ) Log.e(TAG, "CameraX recording failed with error ${finalizedRecording.error}", exception) throw exception } val recordingUri = finalizedRecording.outputResults.outputUri requireNonEmptyRecording(surfaceView.context, recordingUri) val videoFill = engine.block.createFill(FillType.Video) engine.block.setFill(block = page, fill = videoFill) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = recordingUri, ) } private suspend fun requireNonEmptyRecording( context: Context, uri: Uri, ) = withContext(Dispatchers.IO) { val hasData = try { context.contentResolver.openInputStream(uri)?.use { input -> input.read() != -1 } ?: false } catch (exception: IOException) { throw IllegalStateException("CameraX finalized recording is not readable: $uri", exception) } check(hasData) { "CameraX finalized recording is missing or empty: $uri" } } ``` When finalization succeeds, use the URI from `Finalize.outputResults.outputUri`. The CameraX main-executor callback only completes `finalizeEvent`; it does not perform file I/O or mutate the Engine. After `finalizeEvent.await()` returns, the sample opens the URI through `ContentResolver` on `Dispatchers.IO` and reads one byte to verify that the finalized artifact is both readable and non-empty. It then returns to the caller's main context, creates a `VideoFill`, assigns it to the page, and points it at the recording. After awaiting the event and unbinding the camera use cases, the sample checks `Finalize.hasError()` and validates the artifact. A CameraX finalize error, an unreadable URI, or an empty artifact throws an exception to the suspending caller, and the `VideoFill` is not assigned. Handle that exception in your UI instead of trying to play the invalid recording. The file lives in your app's local `filesDir`; move it to a genuinely different long-term destination or upload it if the recording needs another durability boundary. ## The Camera Helper The sample keeps the CameraX setup in the same guide file. Drop the helper code into your app alongside the engine code above, or replace it with your own CameraX pipeline as long as it provides a `Preview` for `setCameraPreview(...)` and a finalized file URI for the playback `VideoFill`. ## Permissions Capturing video and audio requires both manifest declarations and runtime permissions on Android. Request camera and microphone access before binding the CameraX use cases. ```xml ``` ## Troubleshooting **No preview appears** — Verify that the Engine is running, is bound to the same `SurfaceView`, and that the app has camera permission before binding the CameraX use cases. **No audio is recorded** — Verify that the app declares and has runtime access to `RECORD_AUDIO` before calling `withAudioEnabled()`. **The feed is mirrored or rotated incorrectly** — Check the `mirrored` argument passed to `setCameraPreview(...)` first, then override `fill/pixelStream/orientation` only when your app owns a fixed camera orientation. **The recording does not play back** — Handle the exception from `finalizeEvent.await()`. CameraX finalize errors, unreadable URIs, and empty artifacts all fail before the sample replaces the preview with a `VideoFill`. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.createForVideo()` | Create a timeline-enabled scene for camera preview and video playback | | `engine.block.create(blockType=DesignBlockType.Page)` | Create the page that displays the camera feed | | `engine.block.appendChild(parent=_, child=_)` | Add the page to the video scene | | `engine.block.setWidth(block=_, value=_)` | Set the scene and page width to match the camera preview target | | `engine.block.setHeight(block=_, value=_)` | Set the scene and page height to match the camera preview target | | `engine.block.createFill(fillType=FillType.PixelStream)` | Create a fill that accepts live pixel data | | `engine.block.setFill(block=_, fill=_)` | Assign the pixel stream or video fill to a block | | `engine.setCameraPreview(pixelStreamFill=_, preview=_, mirrored=_, onFirstFrameAvailable=_)` | Connect a CameraX `Preview` to a `PixelStreamFill` | | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Point the playback `VideoFill` at the recorded file URI | | `engine.block.setEnum(block=_, property="fill/pixelStream/orientation", value=_)` | Set the orientation for mirroring or rotation | | `engine.block.createEffect(type=_)` | Create an effect for real-time processing | | `engine.block.appendEffect(block=_, effectBlock=_)` | Apply an effect to the block | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Fit the page in the viewport | ## Next Steps - [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) - Add the ready-made CE.SDK camera capture flow. - [Create Video](https://img.ly/docs/cesdk/android/create-video-c41a08/) - Work with prerecorded video scenes. - [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) - Apply visual effects to camera or video content. --- ## 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: "Access Recordings" description: "Manage access to recorded videos or reactions for playback or editing." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) --- ```kotlin file=@cesdk_android_examples/camera-guides-recordings/RecordingsCameraActivity.kt reference-only import android.content.Context import android.net.Uri import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.camera.core.CameraResult import ly.img.camera.core.CaptureMedia import ly.img.camera.core.EngineConfiguration import ly.img.camera.core.videos import java.io.File import java.util.UUID private const val TAG = "RecordingsCameraActivity" class RecordingsCameraActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, // pass null or empty for evaluation mode with watermark userId = "", ), ) setContent { CameraLaunchButton(cameraInput = cameraInput) } } } private object RecordingPersistence { // This process-owned scope outlives Compose and Activity instances. // Use WorkManager for copies that must survive process termination. val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) } @Composable private fun CameraLaunchButton(cameraInput: CaptureMedia.Input) { val applicationContext = LocalContext.current.applicationContext val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handleCameraResult( context = applicationContext, result = result, persistenceScope = RecordingPersistence.applicationScope, ) } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } private fun handleCameraResult( context: Context, result: CameraResult?, persistenceScope: CoroutineScope, ) { if (result == null) { Log.i(TAG, "Camera returned no successful result (canceled or failed)") return } when (result) { is CameraResult.Captures -> { for ((recordingIndex, recording) in result.captures.videos.withIndex()) { Log.i(TAG, "Recording $recordingIndex duration: ${recording.duration}") for ((videoIndex, video) in recording.videos.withIndex()) { Log.i(TAG, "Video $videoIndex uri: ${video.uri}, rect: ${video.rect}") persistenceScope.launch { val persistedUri = persistRecordingVideo( context = context, sourceUri = video.uri, fileName = "recording-$recordingIndex-$videoIndex-${UUID.randomUUID()}.mp4", ) if (persistedUri != null) { Log.i(TAG, "Persisted video $videoIndex uri: $persistedUri") } else { Log.i(TAG, "Unable to persist video $videoIndex from ${video.uri}") } } } } } is CameraResult.Reaction -> { Log.i(TAG, "Reacted-to video uri: ${result.video.uri}, rect: ${result.video.rect}") for ((reactionIndex, recording) in result.reaction.withIndex()) { Log.i(TAG, "Reaction $reactionIndex duration: ${recording.duration}") for ((videoIndex, video) in recording.videos.withIndex()) { Log.i(TAG, "Reaction video $videoIndex uri: ${video.uri}, rect: ${video.rect}") persistenceScope.launch { val persistedUri = persistRecordingVideo( context = context, sourceUri = video.uri, fileName = "reaction-$reactionIndex-$videoIndex-${UUID.randomUUID()}.mp4", ) if (persistedUri != null) { Log.i(TAG, "Persisted reaction video $videoIndex uri: $persistedUri") } else { Log.i(TAG, "Unable to persist reaction video $videoIndex from ${video.uri}") } } } } } else -> { Log.i(TAG, "Unhandled camera result") } } } private suspend fun persistRecordingVideo( context: Context, sourceUri: Uri, fileName: String, ): Uri? = withContext(Dispatchers.IO) { val recordingsDir = File(context.filesDir, "camera-recordings").apply { mkdirs() } val destinationFile = File(recordingsDir, fileName) val inputStream = context.contentResolver.openInputStream(sourceUri) ?: return@withContext null inputStream.use { input -> destinationFile.outputStream().use { output -> input.copyTo(output) } } Uri.fromFile(destinationFile) } ``` Read video recordings returned by the IMGLY Mobile Camera so your app can preview, upload, persist, or hand them to an editing workflow. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/camera-guides-recordings) This guide starts after you have integrated the standalone camera with the Android Activity Result APIs. If you still need the camera dependency, permissions, or base launcher setup, complete [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) first. > **Note:** This guide covers the standalone camera. The camera built into the [Video Editor](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) adds captures to the editor session through its upload asset sources. The camera library declares the camera and microphone permissions in its manifest and presents its own permission prompts the first time it opens. The sample uses evaluation mode with `license = null`; pass your production license and user ID through `EngineConfiguration` in your app. The Activity Result callback returns one of three values: | Result | When it appears | | --- | --- | | `CameraResult.Captures` | Standard video, photo, or mixed capture sessions. Use `result.captures.videos` to extract only video recordings. | | `CameraResult.Reaction` | Reaction camera sessions launched with `CameraMode.Reaction`. It includes the source video and the recorded reaction segments. | | `null` | The contract did not receive a successful camera result. This includes cancellation, any non-`RESULT_OK` outcome, and a successful result code without result data. | ## Create the Camera Launcher Create a `CaptureMedia.Input` with your engine configuration. Then register a `CaptureMedia` Activity Result launcher in your UI and launch it with the same input object. ```kotlin highlight-android-create-input val cameraInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, // pass null or empty for evaluation mode with watermark userId = "", ), ) ``` ```kotlin highlight-android-camera-launch-button private object RecordingPersistence { // This process-owned scope outlives Compose and Activity instances. // Use WorkManager for copies that must survive process termination. val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) } @Composable private fun CameraLaunchButton(cameraInput: CaptureMedia.Input) { val applicationContext = LocalContext.current.applicationContext val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handleCameraResult( context = applicationContext, result = result, persistenceScope = RecordingPersistence.applicationScope, ) } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } ``` The callback delegates result processing to `handleCameraResult`, which keeps the launcher setup separate from the recording access code. It passes the application context and a process-owned persistence scope, so an in-progress copy is not canceled when Compose removes the button or destroys its Activity. To produce a `CameraResult.Reaction`, launch the same Activity Result contract with `CameraMode.Reaction` and a base video URI from your app. For scene composition, continue with [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) in the video-creation guides. ## Handle Camera Results Handle `null` first because the contract has no successful result to inspect. `CaptureMedia` also returns `null` for non-`RESULT_OK` outcomes or missing result data, so it cannot distinguish user cancellation from a camera failure. When the user cancels, the camera deletes captures produced during that session. ```kotlin highlight-android-handle-no-result if (result == null) { Log.i(TAG, "Camera returned no successful result (canceled or failed)") return } ``` ## Read Captured Videos For a standard video session, handle `CameraResult.Captures`. The `captures` list can contain photos in photo or mixed sessions, so use `result.captures.videos` when this guide only needs the video recordings. Each `Recording` is one segment from the session: one recording per shutter press. On Android, each recording contains one `Video`. Read `Recording.duration`, then iterate `Recording.videos` to access each returned `Video.uri` and preview `Video.rect`. ```kotlin highlight-android-read-captured-videos is CameraResult.Captures -> { for ((recordingIndex, recording) in result.captures.videos.withIndex()) { Log.i(TAG, "Recording $recordingIndex duration: ${recording.duration}") for ((videoIndex, video) in recording.videos.withIndex()) { Log.i(TAG, "Video $videoIndex uri: ${video.uri}, rect: ${video.rect}") persistenceScope.launch { val persistedUri = persistRecordingVideo( context = context, sourceUri = video.uri, fileName = "recording-$recordingIndex-$videoIndex-${UUID.randomUUID()}.mp4", ) if (persistedUri != null) { Log.i(TAG, "Persisted video $videoIndex uri: $persistedUri") } else { Log.i(TAG, "Unable to persist video $videoIndex from ${video.uri}") } } } } } ``` ## Read Reaction Recordings When the camera is launched in Reaction mode, handle `CameraResult.Reaction`. The `video` property is the source video the user reacted to, and `reaction` contains the recorded reaction segments. The reaction recordings use the same `Recording` and `Video` shape as standard captures, so the duration, URI, and rect handling stays the same. ```kotlin highlight-android-read-reaction-recordings is CameraResult.Reaction -> { Log.i(TAG, "Reacted-to video uri: ${result.video.uri}, rect: ${result.video.rect}") for ((reactionIndex, recording) in result.reaction.withIndex()) { Log.i(TAG, "Reaction $reactionIndex duration: ${recording.duration}") for ((videoIndex, video) in recording.videos.withIndex()) { Log.i(TAG, "Reaction video $videoIndex uri: ${video.uri}, rect: ${video.rect}") persistenceScope.launch { val persistedUri = persistRecordingVideo( context = context, sourceUri = video.uri, fileName = "reaction-$reactionIndex-$videoIndex-${UUID.randomUUID()}.mp4", ) if (persistedUri != null) { Log.i(TAG, "Persisted reaction video $videoIndex uri: $persistedUri") } else { Log.i(TAG, "Unable to persist reaction video $videoIndex from ${video.uri}") } } } } } ``` ## Persist Returned Files Returned `Uri` values point to files produced by the camera workflow in your app's files directory. The camera removes captures when the user cancels the session, and your app owns any long-term persistence after a successful result. Copy or move any file that must survive beyond the immediate handoff into app-managed storage, and do that work from your own storage layer or a background task when recordings may be large. The sample's process-owned scope outlives the Compose and Activity lifecycles, and it captures `applicationContext` instead of an Activity. It does not survive app process termination. Use `WorkManager` or your app's durable job system when a copy must finish after process death. The result handler launches persistence work from that scope and passes each returned `video.uri` to this helper with a UUID-based file name. The helper performs the file copy on `Dispatchers.IO` so large videos do not block the Activity Result callback on the main thread. ```kotlin highlight-android-persist-returned-file private suspend fun persistRecordingVideo( context: Context, sourceUri: Uri, fileName: String, ): Uri? = withContext(Dispatchers.IO) { val recordingsDir = File(context.filesDir, "camera-recordings").apply { mkdirs() } val destinationFile = File(recordingsDir, fileName) val inputStream = context.contentResolver.openInputStream(sourceUri) ?: return@withContext null inputStream.use { input -> destinationFile.outputStream().use { output -> input.copyTo(output) } } Uri.fromFile(destinationFile) } ``` ## API Reference | API | Purpose | | --- | --- | | `CaptureMedia()` | Creates the Activity Result contract that opens the IMGLY Mobile Camera. | | `CaptureMedia.Input(engineConfiguration=_, cameraConfiguration=_, cameraMode=_)` | Configures the standalone camera launch. | | `EngineConfiguration(license=_, userId=_)` | Provides the license and optional user identifier used by the camera engine. | | `CameraMode.Reaction(video=_, cameraLayoutMode=_, positionsSwapped=_)` | Starts the camera in reaction mode with the video the user reacts to. | | `rememberLauncherForActivityResult(contract=_, onResult=_)` | Registers the Activity Result callback that receives `CameraResult?`. | | `ActivityResultLauncher.launch(input=_)` | Starts the standalone camera with a `CaptureMedia.Input`. | | `CameraResult.Captures.captures` | Reads the heterogeneous capture stack returned by standard camera sessions. | | `List.videos` | Extracts `List` from video entries in a capture stack. | | `Recording.duration` | Reads the duration of one recorded segment. | | `Recording.videos` | Reads the `Video` entries that belong to one segment. | | `Video.uri` | Reads the file URI for a returned video. | | `Video.rect` | Reads the video preview rectangle from the camera layout. | | `CameraResult.Reaction.video` | Reads the source video used for a reaction session. | | `CameraResult.Reaction.reaction` | Reads the recorded reaction segments. | ## 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/) - Configure capture behavior and camera modes. - [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) - Turn reaction recordings into an editable video 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: "Take Photo" description: "Capture a still photo with the IMGLY Mobile Camera in photo mode and receive the captured image." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/take-photo-f13803/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Take Photo](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/take-photo-f13803/) --- ```kotlin file=@cesdk_android_examples/camera-guides-take-photo/TakePhotoActivity.kt reference-only import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button import androidx.compose.material3.Text import ly.img.camera.core.CameraConfiguration import ly.img.camera.core.CameraResult import ly.img.camera.core.Capture import ly.img.camera.core.CaptureCount import ly.img.camera.core.CaptureMedia import ly.img.camera.core.CaptureType import ly.img.camera.core.EngineConfiguration import kotlin.time.Duration.Companion.seconds private const val TAG = "TakePhotoActivity" class TakePhotoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val singlePhotoInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, // pass null or empty for evaluation mode with watermark userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Photo, captureCount = CaptureCount.Single, ), ) val multiPhotoInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Photo, captureCount = CaptureCount.Multi, photoClipDuration = 4.seconds, showsPhotoPreview = false, ), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handlePhotoResult(result) } Column { Button( onClick = { cameraLauncher.launch(singlePhotoInput) }, ) { Text(text = "Take Photo") } Button( onClick = { cameraLauncher.launch(multiPhotoInput) }, ) { Text(text = "Take Multiple Photos") } } } } } private fun handlePhotoResult(result: CameraResult?) { result ?: run { Log.i(TAG, "Camera dismissed") return } when (result) { is CameraResult.Captures -> { val photos = result.captures.filterIsInstance() for (photo in photos) { // Copy the file behind photo.uri to app-owned storage here if the image must persist. Log.i(TAG, "Captured photo: ${photo.uri}, clip duration: ${photo.clipDuration}") } } else -> { Log.i(TAG, "Unhandled result") } } } ``` Capture a still photo with the IMGLY Mobile Camera. Configure the camera for photo mode, launch it, and receive the captured photo so your app can edit, upload, or store it. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/camera-guides-take-photo) This guide uses the IMGLY Mobile Camera in photo mode. For adding the camera to your app, see [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/); for the full set of camera options, see [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/). The camera declares the required permissions in its library manifest and presents its own permission prompt the first time it opens; photo mode does not request the microphone. This sample uses evaluation mode with `license = null`; pass your production license and user ID through `EngineConfiguration` in your app. ## Configure Photo Capture Set `captureType = CaptureType.Photo` on `CameraConfiguration` so the camera shoots still images instead of videos. Use `CaptureCount.Single` when the camera should return after one confirmed photo. ```kotlin highlight-android-single-photo-input val singlePhotoInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, // pass null or empty for evaluation mode with watermark userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Photo, captureCount = CaptureCount.Single, ), ) ``` `CaptureMedia.Input` combines the engine configuration with the camera behavior for this launch. The photo is returned through the Activity Result callback as part of a `CameraResult.Captures` value. ### Capture Multiple Photos Use `CaptureCount.Multi` when the user can take several photos before leaving the camera. Set `showsPhotoPreview = false` if every photo should be committed immediately without the confirm or discard preview. ```kotlin highlight-android-multi-photo-input val multiPhotoInput = CaptureMedia.Input( engineConfiguration = EngineConfiguration( license = null, userId = "", ), cameraConfiguration = CameraConfiguration( captureType = CaptureType.Photo, captureCount = CaptureCount.Multi, photoClipDuration = 4.seconds, showsPhotoPreview = false, ), ) ``` `photoClipDuration` sets the duration stamped on each still photo: the photo occupies that much of the camera's recording-segments ring and counts toward `maxTotalDuration`, and the editor uses it as the clip length if the photo is later placed on a timeline. It does not change the JPEG file itself. ## Launch the Camera Register the `CaptureMedia` Activity Result contract and launch the input that matches the flow you want. The callback receives `null` whenever the camera closes without completing the session, including when the user discards photos they already captured. ```kotlin highlight-android-launch-photo-camera val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureMedia()) { result -> handlePhotoResult(result) } Column { Button( onClick = { cameraLauncher.launch(singlePhotoInput) }, ) { Text(text = "Take Photo") } Button( onClick = { cameraLauncher.launch(multiPhotoInput) }, ) { Text(text = "Take Multiple Photos") } } ``` ## Handle Captured Photos Photo sessions return `CameraResult.Captures`. Filter the capture stack for `Capture.Photo` entries, then read each photo's `uri` and `clipDuration`. ```kotlin highlight-android-handle-photo-result private fun handlePhotoResult(result: CameraResult?) { result ?: run { Log.i(TAG, "Camera dismissed") return } when (result) { is CameraResult.Captures -> { val photos = result.captures.filterIsInstance() for (photo in photos) { // Copy the file behind photo.uri to app-owned storage here if the image must persist. Log.i(TAG, "Captured photo: ${photo.uri}, clip duration: ${photo.clipDuration}") } } else -> { Log.i(TAG, "Unhandled result") } } } ``` Each `Capture.Photo.uri` points to a JPEG written to your app's files directory. Copy the file to a permanent location before relying on it outside the immediate camera or editor workflow. See [Access Photos](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/photos-190c3a/) for accessing and persisting captured media. ## API Reference | API | Purpose | | --- | --- | | `CaptureMedia()` | Creates the Activity Result contract that opens the Mobile Camera. | | `CaptureMedia.Input(engineConfiguration=_, cameraConfiguration=_, cameraMode=_)` | Supplies the engine and camera configuration for one camera launch. | | `EngineConfiguration(license=_, userId=_)` | Provides the license and optional user identifier used by the camera engine. | | `CameraConfiguration(captureType=_, captureCount=_, photoClipDuration=_, showsPhotoPreview=_)` | Chooses photo capture, single or multi capture behavior, per-photo clip duration, and photo preview behavior. | | `CameraResult.Captures.captures` | Returns the ordered capture stack from a standard camera session. | | `Capture.Photo.uri` | Returns the URI of the captured JPEG image. | | `Capture.Photo.clipDuration` | Returns the timeline duration assigned to the photo capture. | ## Next Steps - [Integrate Mobile Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863/) - Add the camera dependency and Activity Result setup. - [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) - Configure capture behavior, durations, and camera UI options. - [Access Photos](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/photos-190c3a/) - Handle camera result objects for captured 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: "Zoom" description: "Learn how pinch zoom works in the Android Mobile Camera." platform: android url: "https://img.ly/docs/cesdk/android/import-media/capture-from-camera/zoom-2f2b32/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) > [Zoom](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/zoom-2f2b32/) --- Use camera zoom to frame subjects before recording video or taking photos with the Mobile Camera. Zoom is built into the live camera preview, and users control it with a pinch gesture. The photo or video captured next reflects the applied zoom level. ## How Camera Zoom Works When the camera is open, users pinch directly on the live preview. The camera reads the gesture scale, multiplies it by the current CameraX zoom ratio, and applies the result to the active camera. The final zoom ratio is clamped to the range the camera device supports. CE.SDK uses the minimum and maximum zoom ratios reported by CameraX, so the available range can vary between devices and lenses. ## No Zoom Configuration Camera zoom does not have a public `CameraConfiguration` option on Android. `CameraConfiguration` controls capture behavior such as recording color, maximum duration, capture type, capture count, photo clip duration, and photo preview behavior. Use [Mobile Camera Configuration](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0/) for those capture options. Zoom remains an interactive preview gesture while the camera is active. ## Camera Zoom vs. Canvas Zoom Camera zoom is separate from CreativeEngine scene and viewport zoom. CreativeEngine scene APIs such as `engine.scene.setZoomLevel(...)`, `engine.scene.zoomToBlock(...)`, and zoom auto-fit affect the editor canvas after a scene is loaded. See [Set Zoom Level](https://img.ly/docs/cesdk/android/open-the-editor/set-zoom-level-d31896/) for the canvas zoom APIs. Canvas zoom APIs do not zoom the device camera feed during capture. For camera capture, users zoom the preview with the pinch gesture before they commit a photo or video. ## Troubleshooting **The zoom feels limited** - The zoom range depends on the active camera and the minimum and maximum zoom ratios reported by CameraX. Some cameras may not expose a usable zoom range. **Pinching does nothing** - The gesture works on the live camera preview from an active camera feed. Test on a physical device when you need representative camera zoom behavior. **I need to set zoom in code** - The CE.SDK Mobile Camera does not expose a public programmatic zoom API. Pinch-to-zoom is the only zoom path on the Mobile Camera today. ## 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/) - Configure capture modes, counts, durations, and preview behavior. - [Take Photo](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/take-photo-f13803/) - Learn how to capture a photo with the Mobile Camera. - [Access Recordings](https://img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e/) - Manage access to recorded videos or reactions. --- ## 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: "Asset Concepts" description: "Understand key asset concepts like sources, formats, metadata, and how assets are integrated into designs." platform: android url: "https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) --- Understand the foundational architecture of CE.SDK's asset system and how asset sources organize content across platforms. Asset sources are CE.SDK's content delivery architecture. Instead of hardcoding asset knowledge into the editor, CE.SDK uses a modular system where any content can be provided through a standardized interface. This decouples what assets are available from how they're discovered and applied. ``` PLATFORM-SPECIFIC UI (Android editor) ---------------------------------------------------------------------------- Dock Button / Gallery Entry -> Asset Library -> Assets Grid Configured via: AssetLibrary, LibraryContent, and source IDs ---------------------------------------------------------------------------- | v CROSS-PLATFORM ENGINE (engine.asset API) --------------------------------------------------------------------------- findAssets() addSource() addLocalSource() applyAssetSourceAsset() Custom Sources Local Sources JSON-Based Sources Your API or DB User uploads Built-in asset packs App backend Runtime lists Bundled or hosted JSON --------------------------------------------------------------------------- ``` This guide covers the foundational concepts of asset sources. For implementation details, see the linked guides at the end. ## Asset Source Fundamentals An asset source provides content to the engine through a common interface. Every source has a unique identifier, such as `ly.img.image` or `ly.img.sticker`, and exposes methods for discovering and applying assets. Sources support: - **Query-based discovery** with pagination and filtering - **Optional grouping**, such as sticker groups or preset categories - **Metadata** including credits, licenses, formats, and block hints Sources are content-agnostic. Images, videos, fonts, templates, and custom content all use the same pattern. ## Content Organized as Asset Sources Asset sources handle virtually all reusable creative content: | Category | Examples | | ---------- | ---------------------------------------- | | Media | Images, videos, audio clips | | Graphics | Stickers, shapes, vectors, icons | | Typography | Fonts, typefaces, text presets | | Colors | Color palettes, spot colors | | Effects | Blur types, filters, LUT effects | | Templates | Design templates, page presets | | Custom | User uploads, remote APIs, your own data | Built-in source IDs include `ly.img.image`, `ly.img.video`, `ly.img.audio`, `ly.img.sticker`, `ly.img.typeface`, `ly.img.filter`, `ly.img.effect`, `ly.img.blur`, and more. ## Types of Asset Sources There are three ways to provide assets to CE.SDK. The right choice depends on who owns the data and how often it changes. ### Custom Sources Custom sources connect CE.SDK to your app backend, content management system, digital asset management system, or another data provider. Use a custom source when your app owns search, filtering, authentication, or custom apply behavior. ### Local Sources Local sources are engine-managed sources that your app can add assets to and remove assets from at runtime. They are useful for uploads, temporary collections, recently used assets, and other session-specific lists. ### JSON-Based Sources JSON-based sources load predefined asset definitions from JSON files. Built-in asset packs use this approach, and Android apps can load the same structure from bundled app assets, a self-hosted server, or another URI that the app can access. ## Asset Sources and the Android UI Asset sources are backend providers. They do not render the editor UI by themselves. The connection between sources and what users see happens through editor configuration. On Android, the editor's asset library presents sources to users. You configure which sources appear and how they're grouped through `AssetLibrary`, `LibraryContent`, and source IDs such as `AssetSourceType` values. A section can reference one source, combine multiple sources, or expand a single source into grouped subsections. This separation lets you: - Show multiple sources in one library section - Show the same source in different locations - Add upload sources only where users should import local media - Change the presentation without changing the source implementation ## Cross-Platform Architecture Asset sources use the `engine.asset` API consistently across every platform CE.SDK supports. All platforms support: - Custom source registration - JSON-based asset loading - Local asset management - Group-based organization - Event subscriptions for source changes Code patterns transfer directly between platforms with syntax and UI configuration changes. Android uses Kotlin `AssetSource` implementations, Android `Uri` values, and editor library configuration classes to present registered sources in the UI. ## Asset Structure Each asset contains data that lets CE.SDK display it, search it, and apply it: - **ID** - Unique identifier within the source - **Meta** - URI, thumbnail URI, MIME type, dimensions, and block type hints - **Label** - Localized display name - **Tags** - Localized searchable keywords - **Groups** - Category membership used by grouped library sections - **Context** - Source reference for tracking origin The engine uses metadata hints such as `blockType`, `fillType`, and `shapeType` to choose what block to create when applying an asset. ## Discovery and Application Assets are discovered through queries supporting pagination, text search, tag and group filtering, and sorting. Query results return assets plus paging information so the editor UI can load asset grids incrementally. When an asset is applied, CE.SDK either creates a new block in the active scene or updates an existing compatible block. Sources can customize application behavior or use the engine's default metadata-based behavior. ## Source Lifecycle Events The engine emits events when sources change: added, removed, or contents updated. Subscribe to these events to keep available content synchronized. On Android, call `assetSourceContentsChanged` when the contents of a registered source change so subscribers can refresh without replacing the whole source. ## Troubleshooting - **Confusing sources with UI** - Asset sources are backend providers; they don't render UI. The asset library presents them, and you configure that presentation separately. - **Expecting sources to filter themselves** - Sources return matching assets; UI configuration determines what's displayed to users. - **Mixing source types** - Custom sources, local sources, and JSON sources serve different purposes. Choose based on whether you need dynamic backend connections, runtime asset management, or static asset packs. - **Forgetting source updates** - Notify the engine after source contents change so the UI can refresh. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addSource(source=_)` | Register a custom asset source with discovery and apply callbacks. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create an engine-managed source for dynamic asset add and remove. | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Load a JSON-defined asset source from a URI. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query assets with pagination, search, filtering, and sorting. | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Apply an asset to the active scene, creating a configured block. | ### Events | Method | Description | | --- | --- | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify subscribers that a source's contents changed. | | `engine.asset.onAssetSourceAdded()` | Observe source registration events. | | `engine.asset.onAssetSourceRemoved()` | Observe source removal events. | | `engine.asset.onAssetSourceUpdated()` | Observe source content update events. | ## Next Steps - [From Your Server](https://img.ly/docs/cesdk/android/import-media/from-remote-source/your-server-b91910/) — Load images, videos, and audio from your backend servers into CE.SDK for integration with CMS, DAM, or custom asset management systems. - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) - Follow an end-to-end remote custom asset source example. - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Explore the core functionality of the asset library and how users browse, search, and insert media. - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Surface registered asset sources in the Android editor 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: "Create a Custom Importer" description: "Create a custom Android media importer by registering an AssetSource that maps backend media into CE.SDK assets." platform: android url: "https://img.ly/docs/cesdk/android/import-media/create-custom-importer-0f7e16/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Create a Custom Importer](https://img.ly/docs/cesdk/android/import-media/create-custom-importer-0f7e16/) --- ```kotlin file=@cesdk_android_examples/engine-guides-custom-asset-source/CreateCustomImporter.kt reference-only import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetCredits import ly.img.engine.AssetLicense import ly.img.engine.AssetSource import ly.img.engine.AssetUTM import ly.img.engine.DesignBlockType 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 private const val CUSTOM_IMPORTER_SOURCE_ID = "com.example.media.images" private const val DEMO_MEDIA_BASE_URI = "https://media.example.com" data class CreateCustomImporterResult( val sourceId: String, val assetIds: List, val groups: List, val nextPage: Int, val total: Int, val sourceStillRegistered: Boolean, ) suspend fun createCustomImporter(engine: Engine): CreateCustomImporterResult { val source = BackendImageAssetSource(mediaItems = demoMediaItems()) if (source.sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(source.sourceId) } engine.asset.addSource(source) val groups = engine.asset.getGroups(sourceId = source.sourceId).orEmpty() val result = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery( query = "summer", page = 0, perPage = 2, locale = "en", ), ) engine.asset.assetSourceContentsChanged(sourceId = source.sourceId) return CreateCustomImporterResult( sourceId = source.sourceId, assetIds = result.assets.map { asset -> asset.id }, groups = groups, nextPage = result.nextPage, total = result.total, sourceStillRegistered = source.sourceId in engine.asset.findAllSources(), ) } class BackendImageAssetSource( private val mediaItems: List, ) : AssetSource(sourceId = CUSTOM_IMPORTER_SOURCE_ID) { override val supportedMimeTypes = listOf(MimeType.JPEG.key) override val credits = AssetCredits( name = "App media library", uri = null, ) override val license = AssetLicense( name = "App media license", uri = null, ) override suspend fun getGroups(): List = withContext(Dispatchers.IO) { mediaItems.flatMap { item -> item.groups }.distinct() } override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val terms = query.query.orEmpty() .lowercase() .split(" ") .filter { term -> term.isNotBlank() } val requestedPage = query.page.coerceAtLeast(0) val pageSize = query.perPage.coerceAtLeast(1) val requestedGroups = query.groups.orEmpty() val filteredItems = mediaItems.filter { item -> val matchesQuery = terms.isEmpty() || terms.all { term -> item.label.lowercase().contains(term) || item.tags.any { tag -> tag.lowercase().contains(term) } } val matchesGroup = requestedGroups.isEmpty() || item.groups.any { group -> group in requestedGroups } matchesQuery && matchesGroup } val start = requestedPage * pageSize val pageItems = filteredItems.drop(start).take(pageSize) FindAssetsResult( assets = pageItems.map { item -> item.toAsset(sourceId = sourceId) }, currentPage = requestedPage, nextPage = if (start + pageSize < filteredItems.size) requestedPage + 1 else -1, total = filteredItems.size, ) } private fun BackendMediaItem.toAsset(sourceId: String) = Asset( id = id, context = AssetContext(sourceId = sourceId), label = label, locale = locale, tags = tags, groups = groups, meta = mapOf( "uri" to uri, "thumbUri" to thumbUri, "mimeType" to mimeType, "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to width.toString(), "height" to height.toString(), ), credits = AssetCredits( name = authorName, uri = authorUri?.let(Uri::parse), ), license = AssetLicense( name = licenseName, uri = licenseUri?.let(Uri::parse), ), utm = AssetUTM(source = "app-media", medium = "importer"), ) } private fun demoMediaItems() = listOf( BackendMediaItem( id = "summer-card", label = "Summer card", tags = listOf("summer", "card", "campaign"), groups = listOf("campaigns"), path = "summer-card.jpg", thumbPath = "summer-card-thumb.jpg", width = 1600, height = 1200, authorName = "Design Team", ), BackendMediaItem( id = "summer-banner", label = "Summer banner", tags = listOf("summer", "banner", "product"), groups = listOf("products"), path = "summer-banner.jpg", thumbPath = "summer-banner-thumb.jpg", width = 1920, height = 1080, authorName = "Design Team", ), BackendMediaItem( id = "summer-square", label = "Summer square", tags = listOf("summer", "square", "social"), groups = listOf("campaigns"), path = "summer-square.jpg", thumbPath = "summer-square-thumb.jpg", width = 1200, height = 1200, authorName = "Content Team", ), ) data class BackendMediaItem( val id: String, val label: String, val tags: List, val groups: List, val path: String, val thumbPath: String, val width: Int, val height: Int, val authorName: String, val locale: String = "en", val mimeType: String = MimeType.JPEG.key, val licenseName: String = "App media license", val authorUri: String? = null, val licenseUri: String? = null, ) { val uri = "$DEMO_MEDIA_BASE_URI/$path" val thumbUri = "$DEMO_MEDIA_BASE_URI/thumbnails/$thumbPath" } ``` Create a custom media importer by connecting your app's backend to CE.SDK's asset system with an Android `AssetSource`. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-custom-asset-source) A custom importer is an asset source that translates media from your app into CE.SDK asset results. The source receives search and pagination requests from the Engine, fetches or filters media in your own data layer, and returns `Asset` objects with enough metadata for CE.SDK to preview and apply them. This guide uses a deterministic in-memory media list to keep the sample runnable without network access. In production, replace that list with your API client and return permanent HTTPS URLs for the media and thumbnail fields. ## Define an Asset Source Extend `AssetSource` with a stable `sourceId`. The source also declares the MIME types it can provide and optional source-level credits or license information. ```kotlin highlight-android-source-definition class BackendImageAssetSource( private val mediaItems: List, ) : AssetSource(sourceId = CUSTOM_IMPORTER_SOURCE_ID) { override val supportedMimeTypes = listOf(MimeType.JPEG.key) override val credits = AssetCredits( name = "App media library", uri = null, ) override val license = AssetLicense( name = "App media license", uri = null, ) override suspend fun getGroups(): List = withContext(Dispatchers.IO) { mediaItems.flatMap { item -> item.groups }.distinct() } ``` `findAssets` and `getGroups` are the required callbacks. CE.SDK calls them when the asset library, your app, or any other Engine workflow needs to browse this source. ## Return Paginated Results Implement `findAssets` by translating the incoming `FindAssetsQuery` into your backend query. Return only the requested page and set `nextPage` to `-1` when there are no more results. ```kotlin highlight-android-find-assets override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val terms = query.query.orEmpty() .lowercase() .split(" ") .filter { term -> term.isNotBlank() } val requestedPage = query.page.coerceAtLeast(0) val pageSize = query.perPage.coerceAtLeast(1) val requestedGroups = query.groups.orEmpty() val filteredItems = mediaItems.filter { item -> val matchesQuery = terms.isEmpty() || terms.all { term -> item.label.lowercase().contains(term) || item.tags.any { tag -> tag.lowercase().contains(term) } } val matchesGroup = requestedGroups.isEmpty() || item.groups.any { group -> group in requestedGroups } matchesQuery && matchesGroup } val start = requestedPage * pageSize val pageItems = filteredItems.drop(start).take(pageSize) FindAssetsResult( assets = pageItems.map { item -> item.toAsset(sourceId = sourceId) }, currentPage = requestedPage, nextPage = if (start + pageSize < filteredItems.size) requestedPage + 1 else -1, total = filteredItems.size, ) } ``` Run network or database work off the main dispatcher. The callback itself is `suspend`, so you can call your repository, cache, or HTTP client and then return a `FindAssetsResult`. ## Map Backend Media to Assets Each backend item becomes an `Asset`. The required field is `id`, but media importers should also include labels, tags, groups, thumbnail URLs, content URLs, and type hints. ```kotlin highlight-android-map-asset private fun BackendMediaItem.toAsset(sourceId: String) = Asset( id = id, context = AssetContext(sourceId = sourceId), label = label, locale = locale, tags = tags, groups = groups, meta = mapOf( "uri" to uri, "thumbUri" to thumbUri, "mimeType" to mimeType, "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to width.toString(), "height" to height.toString(), ), credits = AssetCredits( name = authorName, uri = authorUri?.let(Uri::parse), ), license = AssetLicense( name = licenseName, uri = licenseUri?.let(Uri::parse), ), utm = AssetUTM(source = "app-media", medium = "importer"), ) ``` The `meta` map drives the default apply behavior: | Key | Purpose | | --- | --- | | `uri` | Permanent URL for the media file. | | `thumbUri` | Preview image shown by asset browsing UI. | | `mimeType` | MIME type used before CE.SDK loads the asset data. | | `blockType` | Design block type to create when the asset is applied. | | `fillType` | Fill type used for graphic media. | | `shapeType` | Shape type attached to the graphic block. | | `kind` | App-facing category stored on the resulting block. | | `width` / `height` | Original media dimensions when known. | The sample uses `DesignBlockType.Graphic.key`, `FillType.Image.key`, and `ShapeType.Rect.key` so CE.SDK can create an image graphic without guessing from the file. ## Register and Query the Source Register the source once on the Engine before querying it or exposing it through an asset library entry. ```kotlin highlight-android-register-source engine.asset.addSource(source) ``` You can then query the registered source directly. This is the same path CE.SDK uses when UI surfaces browse the source. ```kotlin highlight-android-query-source val groups = engine.asset.getGroups(sourceId = source.sourceId).orEmpty() val result = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery( query = "summer", page = 0, perPage = 2, locale = "en", ), ) ``` When your backend changes outside CE.SDK, notify the Engine so subscribers and UI surfaces can refresh their cached source contents. ```kotlin highlight-android-refresh-source engine.asset.assetSourceContentsChanged(sourceId = source.sourceId) ``` ## API Reference | Method | Purpose | | --- | --- | | `AssetSource(sourceId=_)` | Defines a custom asset source with a stable source identifier. | | `AssetSource.findAssets(query=_)` | Returns one page of assets for a search, group filter, locale, and pagination request. | | `AssetSource.getGroups()` | Lists the groups exposed by the source. | | `engine.asset.addSource(source=_)` | Registers the custom source with the Engine. | | `engine.asset.findAssets(sourceId=_, query=_)` | Queries a registered source directly. | | `engine.asset.getGroups(sourceId=_)` | Reads the groups exposed by a registered source. | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notifies CE.SDK that external source contents changed. | ## Key Types | Type | Purpose | | --- | --- | | `FindAssetsQuery` | Search text, locale, group filters, sort options, page, and page size. | | `FindAssetsResult` | Returned assets plus `currentPage`, `nextPage`, and `total`. | | `Asset` | One result in a source, including labels, tags, groups, metadata, credits, and license data. | | `AssetContext` | Connects an asset result to the source that produced it. | | `DesignBlockType`, `FillType`, `ShapeType` | Type-safe constants for metadata values that drive default insertion. | ## Next Steps - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Understand how sources, assets, metadata, and UI surfaces fit together. - [From Your Server](https://img.ly/docs/cesdk/android/import-media/from-remote-source/your-server-b91910/) — Load images, videos, and audio from your backend servers into CE.SDK for integration with CMS, DAM, or custom asset management systems. - [Thumbnails](https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/) — Configure thumbnail images for assets in CE.SDK's asset library with proper sizing, preview URIs for audio, and customized UI display. - [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Add custom sources to the Android asset library 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: "Using Default Assets" description: "Load built-in CE.SDK asset sources on Android and apply shapes, stickers, and sample media with the Asset API." platform: android url: "https://img.ly/docs/cesdk/android/import-media/default-assets-d2763d/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Using Default Assets](https://img.ly/docs/cesdk/android/import-media/default-assets-d2763d/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-default-assets/DefaultAssets.kt reference-only import android.net.Uri import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.FindAssetsQuery import ly.img.engine.MimeType import ly.img.engine.ShapeType fun defaultAssetsUri(cesdkVersion: String): Uri = Uri.parse( "https://cdn.img.ly/packages/imgly/cesdk-android/$cesdkVersion/assets", ) suspend fun defaultAssets( engine: Engine, assetBaseUri: Uri, ): DefaultAssetsResult { val defaultAssetSourceIds = listOf( "ly.img.sticker", "ly.img.vector.shape", "ly.img.color.palette", "ly.img.filter", "ly.img.effect", "ly.img.blur", "ly.img.typeface", "ly.img.crop.presets", "ly.img.page.presets", "ly.img.text.components", "ly.img.text", "ly.img.text.styles", "ly.img.text.curves", "ly.img.caption.presets", ) val demoAssetSourceIds = listOf( "ly.img.image", "ly.img.video", "ly.img.audio", "ly.img.templates", ) val sourceIdsToReset = defaultAssetSourceIds + demoAssetSourceIds + "my-json-shapes" sourceIdsToReset .filter { sourceId -> sourceId in engine.asset.findAllSources() } .forEach { sourceId -> engine.asset.removeSource(sourceId) } val loadedDefaultSourceIds = defaultAssetSourceIds.map { sourceId -> engine.asset.addLocalSourceFromJSON( contentUri = assetBaseUri.buildUpon() .appendPath(sourceId) .appendPath("content.json") .build(), ) } val loadedDemoSourceIds = demoAssetSourceIds.map { sourceId -> engine.asset.addLocalSourceFromJSON( contentUri = assetBaseUri.buildUpon() .appendPath(sourceId) .appendPath("content.json") .build(), ) } val jsonSourceId = engine.asset.addLocalSourceFromJSON( contentJSON = """ { "version": "1.0.0", "id": "my-json-shapes", "assets": [ { "id": "brand-star", "label": { "en": "Brand Star" }, "meta": { "blockType": "${DesignBlockType.Graphic.key}", "shapeType": "${ShapeType.Star.key}" } } ] } """.trimIndent(), basePath = null, matcher = null, ) val stickerSourceId = "ly.img.sticker" engine.asset.removeSource(sourceId = stickerSourceId) val filteredStickerSourceId = engine.asset.addLocalSourceFromJSON( contentUri = assetBaseUri.buildUpon() .appendPath(stickerSourceId) .appendPath("content.json") .build(), matcher = listOf("ly.img.sticker.emoji.happyface"), ) val filteredStickerCount = engine.asset.findAssets( sourceId = filteredStickerSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ).total 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 assetsToAdd = listOf( "ly.img.vector.shape" to "ly.img.vector.shape.filled.star", filteredStickerSourceId to "ly.img.sticker.emoji.happyface", "ly.img.image" to "ly.img.image.sample_1", ) val blockSize = 140F val spacing = 36F val startX = (800F - (assetsToAdd.size * blockSize + (assetsToAdd.size - 1) * spacing)) / 2F val centerY = (600F - blockSize) / 2F val createdBlocks = assetsToAdd.mapIndexed { index, (sourceId, assetId) -> val asset = checkNotNull( engine.asset.fetchAsset( sourceId = sourceId, assetId = assetId, ), ) { "Missing asset $assetId from $sourceId." } val block = checkNotNull( engine.asset.applyAssetSourceAsset( sourceId = sourceId, asset = asset, ), ) { "Asset $assetId did not create a block." } engine.block.setWidth(block, value = blockSize) engine.block.setHeight(block, value = blockSize) engine.block.setPositionX(block, value = startX + index * (blockSize + spacing)) engine.block.setPositionY(block, value = centerY) block } engine.block.forceLoadResources(blocks = listOf(page) + createdBlocks) val exportedPng = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) check(exportedPng.hasRemaining()) { "Default assets PNG export is empty." } return DefaultAssetsResult( loadedDefaultSourceIds = loadedDefaultSourceIds, loadedDemoSourceIds = loadedDemoSourceIds, jsonSourceId = jsonSourceId, filteredStickerCount = filteredStickerCount, createdBlocks = createdBlocks, exportedPng = exportedPng.asReadOnlyBuffer(), ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-import-media-default-assets/DefaultAssetsResult.kt reference-only import ly.img.engine.DesignBlock import java.nio.ByteBuffer data class DefaultAssetsResult( val loadedDefaultSourceIds: List, val loadedDemoSourceIds: List, val jsonSourceId: String, val filteredStickerCount: Int, val createdBlocks: List, val exportedPng: ByteBuffer, ) ``` Load the built-in CE.SDK asset sources on Android, then fetch and apply assets such as vector shapes, stickers, and sample images from those sources. ![Default assets rendered from an Android Engine export](./assets/android.hero.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-default-assets) CE.SDK ships asset catalogs for core editor content and sample media. Register the catalogs you need with `engine.asset.addLocalSourceFromJSON()`, then use the regular asset APIs to query, fetch, and apply entries to the active scene. > **Production Deployment:** The IMG.LY CDN is for development and prototyping. For production, download the asset bundle and serve it from your own infrastructure. See the [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) guide for the self-hosting flow. ## What Are Default and Demo Assets? IMG.LY provides two groups of JSON-backed asset sources. **Default assets** are core editor components: | Source ID | Description | | --- | --- | | `ly.img.sticker` | Stickers, emojis, and decorations | | `ly.img.vector.shape` | Vector shapes such as stars, arrows, and polygons | | `ly.img.color.palette` | Default color palette | | `ly.img.filter` | Color filters | | `ly.img.effect` | Visual effects | | `ly.img.blur` | Blur effects | | `ly.img.typeface` | Font families | | `ly.img.crop.presets` | Crop presets | | `ly.img.page.presets` | Page size presets | | `ly.img.text.components` | Text component presets | | `ly.img.text`, `ly.img.text.styles`, `ly.img.text.curves` | Text style presets | | `ly.img.caption.presets` | Caption presets | **Demo assets** are sample content for development: | Source ID | Description | | --- | --- | | `ly.img.image` | Sample images | | `ly.img.video` | Sample videos | | `ly.img.audio` | Sample audio tracks | | `ly.img.templates` | Design templates | ## Versioned CDN URLs Build asset URLs from the Android package version so the JSON catalogs match the SDK in your app. Keep the asset base URI app-provided, so you can swap the CDN URL for a bundled or self-hosted URI without changing the loading code. ```kotlin highlight-android-cdn-url fun defaultAssetsUri(cesdkVersion: String): Uri = Uri.parse( "https://cdn.img.ly/packages/imgly/cesdk-android/$cesdkVersion/assets", ) ``` ## Loading Asset Sources List the source IDs you want to register. Load only the sources your app needs; the editor UI or your own asset browser can query them after registration. ```kotlin highlight-android-source-groups val defaultAssetSourceIds = listOf( "ly.img.sticker", "ly.img.vector.shape", "ly.img.color.palette", "ly.img.filter", "ly.img.effect", "ly.img.blur", "ly.img.typeface", "ly.img.crop.presets", "ly.img.page.presets", "ly.img.text.components", "ly.img.text", "ly.img.text.styles", "ly.img.text.curves", "ly.img.caption.presets", ) val demoAssetSourceIds = listOf( "ly.img.image", "ly.img.video", "ly.img.audio", "ly.img.templates", ) ``` ### Loading Default Asset Sources Use `addLocalSourceFromJSON(contentUri=_)` to load each default source from its `content.json` manifest. ```kotlin highlight-android-load-default-assets val loadedDefaultSourceIds = defaultAssetSourceIds.map { sourceId -> engine.asset.addLocalSourceFromJSON( contentUri = assetBaseUri.buildUpon() .appendPath(sourceId) .appendPath("content.json") .build(), ) } ``` ### Loading Demo Asset Sources Demo sources use the same registration API. They are useful for examples, prototypes, and smoke tests, but production apps usually replace them with app-owned media. ```kotlin highlight-android-load-demo-assets val loadedDemoSourceIds = demoAssetSourceIds.map { sourceId -> engine.asset.addLocalSourceFromJSON( contentUri = assetBaseUri.buildUpon() .appendPath(sourceId) .appendPath("content.json") .build(), ) } ``` ## Loading Assets from a JSON String Use `addLocalSourceFromJSON(contentJSON=_, basePath=_, matcher=_)` when your app already has the JSON content in memory. The JSON contains the source ID and an `assets` array with the same metadata keys used by file-backed catalogs. ```kotlin highlight-android-json-string val jsonSourceId = engine.asset.addLocalSourceFromJSON( contentJSON = """ { "version": "1.0.0", "id": "my-json-shapes", "assets": [ { "id": "brand-star", "label": { "en": "Brand Star" }, "meta": { "blockType": "${DesignBlockType.Graphic.key}", "shapeType": "${ShapeType.Star.key}" } } ] } """.trimIndent(), basePath = null, matcher = null, ) ``` ## Filtering Assets with Matcher Pass `matcher` while registering a JSON source to keep only matching asset IDs. The sample reloads the sticker source with one emoji asset, then queries the filtered source. ```kotlin highlight-android-filter-assets val stickerSourceId = "ly.img.sticker" engine.asset.removeSource(sourceId = stickerSourceId) val filteredStickerSourceId = engine.asset.addLocalSourceFromJSON( contentUri = assetBaseUri.buildUpon() .appendPath(stickerSourceId) .appendPath("content.json") .build(), matcher = listOf("ly.img.sticker.emoji.happyface"), ) val filteredStickerCount = engine.asset.findAssets( sourceId = filteredStickerSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ).total ``` Matcher patterns support `*` wildcards, and an asset is included when its ID matches any pattern in the list. ## Creating Blocks from Assets Create a scene and page before applying assets. `applyAssetSourceAsset()` adds the new block to the active scene when the source can materialize the asset. ```kotlin highlight-android-create-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) ``` Fetch the individual assets by ID, apply them through their source, and position the returned blocks on the page. ```kotlin highlight-android-fetch-apply-assets val assetsToAdd = listOf( "ly.img.vector.shape" to "ly.img.vector.shape.filled.star", filteredStickerSourceId to "ly.img.sticker.emoji.happyface", "ly.img.image" to "ly.img.image.sample_1", ) val blockSize = 140F val spacing = 36F val startX = (800F - (assetsToAdd.size * blockSize + (assetsToAdd.size - 1) * spacing)) / 2F val centerY = (600F - blockSize) / 2F val createdBlocks = assetsToAdd.mapIndexed { index, (sourceId, assetId) -> val asset = checkNotNull( engine.asset.fetchAsset( sourceId = sourceId, assetId = assetId, ), ) { "Missing asset $assetId from $sourceId." } val block = checkNotNull( engine.asset.applyAssetSourceAsset( sourceId = sourceId, asset = asset, ), ) { "Asset $assetId did not create a block." } engine.block.setWidth(block, value = blockSize) engine.block.setHeight(block, value = blockSize) engine.block.setPositionX(block, value = startX + index * (blockSize + spacing)) engine.block.setPositionY(block, value = centerY) block } ``` The example applies a star from `ly.img.vector.shape`, a filtered sticker from `ly.img.sticker`, and a sample image from `ly.img.image`. ## Exporting the Result Force resource loading before export so externally referenced media is available to the renderer. ```kotlin highlight-android-export engine.block.forceLoadResources(blocks = listOf(page) + createdBlocks) val exportedPng = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) ``` The exported PNG confirms that the registered sources can produce visible scene content. ## API Reference | Method | Description | | --- | --- | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Register a local asset source from a JSON manifest URI | | `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_, matcher=_)` | Register a local asset source from a JSON string | | `engine.asset.findAssets(sourceId=_, query=_)` | Query assets from a registered source | | `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Fetch one asset by ID | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Apply an asset through its source and return the created block when available | | `engine.asset.removeSource(sourceId=_)` | Remove an existing source before registering it again | | `engine.scene.create()` | Create a scene for the applied assets | | `engine.block.create(blockType=DesignBlockType.Page)` | Create a page block | | `engine.block.appendChild(parent=_, child=_)` | Attach the page to the scene | | `engine.block.setWidth(block=_, value=_)` | Set block width | | `engine.block.setHeight(block=_, value=_)` | Set block height | | `engine.block.setPositionX(block=_, value=_)` | Set the horizontal block position | | `engine.block.setPositionY(block=_, value=_)` | Set the vertical block position | | `engine.block.forceLoadResources(blocks=_)` | Load external resources before export | | `engine.block.export(block=_, mimeType=MimeType.PNG, options=_)` | Export the page as a PNG | ## Next Steps - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) - Self-host assets for production deployments - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Configure the asset library UI and entries - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Explore the core functionality of the asset library and how users browse, search, and insert media. - [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) - Load assets from external URLs --- ## 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 Assets" description: "Manage Android assets in local asset sources by updating metadata, removing individual assets, or deleting entire sources." platform: android url: "https://img.ly/docs/cesdk/android/import-media/edit-or-remove-assets-ce072c/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Edit or Remove Assets](https://img.ly/docs/cesdk/android/import-media/edit-or-remove-assets-ce072c/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-edit-or-remove-assets/EditOrRemoveAssets.kt reference-only import android.net.Uri import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import ly.img.editor.defaultBaseUri import ly.img.engine.AssetDefinition import ly.img.engine.DesignBlockType 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 = "EditOrRemoveAssets" suspend fun editOrRemoveAssets( engine: Engine, assetBaseUri: Uri = defaultBaseUri, ): EditOrRemoveAssetsResult = coroutineScope { val sourceId = "my-local-images" val temporarySourceId = "temporary-session-images" val assetSourceLifecycleScope = this val sourceObserverJob = observeAssetSourceUpdates( engine = engine, sourceId = sourceId, assetSourceLifecycleScope = assetSourceLifecycleScope, ) try { listOf(sourceId, temporarySourceId).forEach { staleSourceId -> if (staleSourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = staleSourceId) } } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key), ) val logoUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_1.jpg") .build() val logoThumbnailUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("thumbnails") .appendPath("sample_1.jpg") .build() val backgroundUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_4.jpg") .build() val backgroundThumbnailUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("thumbnails") .appendPath("sample_4.jpg") .build() val logoAsset = AssetDefinition( id = "brand-logo", label = mapOf("en" to "Brand Logo"), tags = mapOf("en" to listOf("logo", "brand")), groups = listOf("brand"), meta = mapOf( "uri" to logoUri.toString(), "thumbUri" to logoThumbnailUri.toString(), "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", ), ) val backgroundAsset = AssetDefinition( id = "campaign-background", label = mapOf("en" to "Campaign Background"), tags = mapOf("en" to listOf("campaign", "background")), groups = listOf("campaign"), meta = mapOf( "uri" to backgroundUri.toString(), "thumbUri" to backgroundThumbnailUri.toString(), "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 "1440", ), ) engine.asset.addAsset(sourceId = sourceId, asset = logoAsset) engine.asset.addAsset(sourceId = sourceId, asset = backgroundAsset) val queryResult = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery( page = 0, perPage = 100, query = "logo", locale = "en", ), ) val assetToEdit = queryResult.assets.firstOrNull { asset -> asset.id == "brand-logo" } ?: error("Expected brand-logo in $sourceId.") val initialAssetIds = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 100), ).assets.map { asset -> asset.id } val updatedLogoAsset = logoAsset.copy( label = mapOf("en" to "Primary Brand Logo"), tags = mapOf("en" to listOf("logo", "brand", "primary")), ) engine.asset.removeAsset(sourceId = sourceId, assetId = assetToEdit.id) engine.asset.addAsset(sourceId = sourceId, asset = updatedLogoAsset) val updatedAsset = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 10, query = "primary", locale = "en"), ).assets.firstOrNull { asset -> asset.id == updatedLogoAsset.id } ?: error("Expected updated logo asset in $sourceId.") engine.asset.removeAsset( sourceId = sourceId, assetId = "campaign-background", ) val remainingAssetIds = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 100), ).assets.map { asset -> asset.id } engine.asset.addLocalSource( sourceId = temporarySourceId, supportedMimeTypes = listOf(MimeType.JPEG.key), ) // Use this after changing backing data outside addAsset/removeAsset. engine.asset.assetSourceContentsChanged(sourceId = temporarySourceId) engine.asset.removeSource(sourceId = temporarySourceId) val temporarySourceExistsAfterRemoval = temporarySourceId in engine.asset.findAllSources() val removedAssetWasPresent = remainingAssetIds.contains("campaign-background") engine.asset.removeSource(sourceId = sourceId) val managedSourcesCleanedUp = listOf(sourceId, temporarySourceId).none { managedSourceId -> managedSourceId in engine.asset.findAllSources() } EditOrRemoveAssetsResult( queriedAssetId = assetToEdit.id, initialAssetIds = initialAssetIds, updatedLabel = updatedAsset.label, remainingAssetIds = remainingAssetIds, removedAssetWasPresent = removedAssetWasPresent, temporarySourceExistsAfterRemoval = temporarySourceExistsAfterRemoval, managedSourcesCleanedUp = managedSourcesCleanedUp, ) } finally { listOf(sourceId, temporarySourceId).forEach { managedSourceId -> if (managedSourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = managedSourceId) } } sourceObserverJob.cancelAndJoin() } } fun observeAssetSourceUpdates( engine: Engine, sourceId: String, assetSourceLifecycleScope: CoroutineScope, ): Job { return engine.asset.onAssetSourceUpdated() .onEach { updatedSourceId -> if (updatedSourceId == sourceId) { Log.i(TAG, "Asset source updated: $updatedSourceId") } } .launchIn(assetSourceLifecycleScope) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-import-media-edit-or-remove-assets/EditOrRemoveAssetsResult.kt reference-only data class EditOrRemoveAssetsResult( val queriedAssetId: String, val initialAssetIds: List, val updatedLabel: String?, val remainingAssetIds: List, val removedAssetWasPresent: Boolean, val temporarySourceExistsAfterRemoval: Boolean, val managedSourcesCleanedUp: Boolean, ) ``` Manage assets in Android local asset sources by updating metadata, removing individual assets, or deleting entire sources. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-edit-or-remove-assets) Assets in local sources can change after users import or generate media. CE.SDK lets you query those assets, replace their metadata by removing and re-adding them, remove stale entries, and notify subscribers when a source changes. This guide assumes your app already owns a populated local asset source. For the source setup pattern, see [Import Local Asset](https://img.ly/docs/cesdk/android/import-media/from-local-source/local-asset-3f93f2/). Creating upload UI and remote source backends are separate topics. ## Finding Assets in a Source Use `engine.asset.findAssets()` before editing or removing an item. Android query pages are zero-based, and `locale` controls which localized labels and tags are searched. ```kotlin highlight-android-find-assets val queryResult = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery( page = 0, perPage = 100, query = "logo", locale = "en", ), ) val assetToEdit = queryResult.assets.firstOrNull { asset -> asset.id == "brand-logo" } ?: error("Expected brand-logo in $sourceId.") ``` The returned `FindAssetsResult` includes the matching `assets`, the `total` count, the requested `currentPage`, and `nextPage` when another page is available. ## Updating Asset Metadata Android local sources do not expose a direct update method. Remove the existing asset, then add an updated `AssetDefinition` with the same ID. ```kotlin highlight-android-update-metadata val updatedLogoAsset = logoAsset.copy( label = mapOf("en" to "Primary Brand Logo"), tags = mapOf("en" to listOf("logo", "brand", "primary")), ) engine.asset.removeAsset(sourceId = sourceId, assetId = assetToEdit.id) engine.asset.addAsset(sourceId = sourceId, asset = updatedLogoAsset) ``` Reusing the ID keeps your app-level references stable while changing the label, tags, URI, thumbnail, or other metadata stored in the source. ## Removing an Asset From a Source Remove a single entry with `engine.asset.removeAsset()`. This deletes the asset from the source catalog, but it does not delete blocks that were already created from that asset. ```kotlin highlight-android-remove-asset engine.asset.removeAsset( sourceId = sourceId, assetId = "campaign-background", ) ``` Use this for user-driven deletion, cleanup of temporary imports, or removing outdated generated assets from a local collection. ## Notifying After Out-of-Band Changes Calls to `engine.asset.addAsset()` and `engine.asset.removeAsset()` already notify source subscribers. Call `engine.asset.assetSourceContentsChanged()` only after your app changes source backing data outside the local asset API, so asset-library UI and other subscribers know to re-query the source. ```kotlin highlight-android-notify-source // Use this after changing backing data outside addAsset/removeAsset. engine.asset.assetSourceContentsChanged(sourceId = temporarySourceId) ``` When you change several backing files or metadata records outside CE.SDK, call it once after the batch instead of once per item. ## Removing an Entire Asset Source Remove a complete source with `engine.asset.removeSource()`. This removes the source registration and all assets stored in that local source. ```kotlin highlight-android-remove-source engine.asset.removeSource(sourceId = temporarySourceId) ``` Use this when a temporary session ends or when your app deletes an entire user-managed asset category. If you need to create and populate local sources first, follow the source setup in [Import Local Asset](https://img.ly/docs/cesdk/android/import-media/from-local-source/local-asset-3f93f2/). ## Listening to Asset Source Events The asset API exposes `Flow` streams for source additions, removals, and content updates. Collect these flows in the lifecycle scope that owns your asset library UI or background workflow, then cancel the returned `Job` when that owner is disposed. ```kotlin highlight-android-source-events return engine.asset.onAssetSourceUpdated() .onEach { updatedSourceId -> if (updatedSourceId == sourceId) { Log.i(TAG, "Asset source updated: $updatedSourceId") } } .launchIn(assetSourceLifecycleScope) ``` Use the same pattern with `onAssetSourceAdded()` or `onAssetSourceRemoved()` when you need to react to source registration or cleanup events. ## Best Practices - **Query before mutating**: Use `findAssets()` to verify the asset ID before removing or replacing it. - **Notify only for out-of-band changes**: `addAsset()` and `removeAsset()` already notify subscribers; reserve `assetSourceContentsChanged()` for backing data changes made outside those APIs. - **Scope event subscriptions**: Collect source event flows in the owning lifecycle scope and cancel the job when that owner is disposed. - **Use source-specific IDs**: Keep persistent, temporary, and generated sources clearly separated. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Asset not found | The ID, query, locale, tags, or groups do not match the source contents | Query the source with a wider `FindAssetsQuery` before mutating | | Source updates are not visible | Subscribers have not been notified after an external or batched backing-data change | Call `assetSourceContentsChanged(sourceId=_)` after the external mutation completes | | Cannot remove assets | The source is not a local source or the custom source does not implement removal support | Manage assets through a local source or implement removal behavior on the custom `AssetSource` | | Source remains available | Cleanup only removed individual assets | Call `removeSource(sourceId=_)` when the whole catalog should disappear | ## API Reference | Method | Description | | --- | --- | | `engine.asset.addAsset(sourceId=_, asset=_)` | Re-add an updated `AssetDefinition` to a local source | | `engine.asset.findAssets(sourceId=_, query=_)` | Query assets from a source with paging, search, locale, tags, and groups | | `engine.asset.removeAsset(sourceId=_, assetId=_)` | Remove one asset from a local source | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify subscribers after backing data changes outside local asset API calls | | `engine.asset.removeSource(sourceId=_)` | Remove an entire asset source and its local assets | | `engine.asset.onAssetSourceAdded()` | Observe source registration events | | `engine.asset.onAssetSourceUpdated()` | Observe source content update events | | `engine.asset.onAssetSourceRemoved()` | Observe source removal events | ## Next Steps - [Import Local Asset](https://img.ly/docs/cesdk/android/import-media/from-local-source/local-asset-3f93f2/) - Import files directly from the user's device and insert them into the design canvas. - [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) — Enable file picker uploads from end users for use in the editor. - [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Adapt the asset library UI and behavior to suit your application's structure and user needs. --- ## 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: "Review the supported image, video, and audio formats for importing assets." platform: android url: "https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) --- CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio content. --- ## 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 From Local Source" description: "Enable users to upload files from their device for use as design assets in the editor." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-local-source-39b2a9/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Local Source](https://img.ly/docs/cesdk/android/import-media/from-local-source-39b2a9/) --- --- ## Related Pages - [Import Local Asset](https://img.ly/docs/cesdk/android/import-media/from-local-source/local-asset-3f93f2/) - Import files directly from the user's device and insert them into the design canvas. - [From Photo Roll](https://img.ly/docs/cesdk/android/import-media/from-local-source/photo-roll-23820d/) - Enable users to browse and select images and videos from the Android device gallery within the CE.SDK editor. - [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) - Enable file picker uploads from end users for use 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: "Import Local Asset" description: "Import files directly from the user's device and insert them into the design canvas." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-local-source/local-asset-3f93f2/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Local Source](https://img.ly/docs/cesdk/android/import-media/from-local-source-39b2a9/) > [Import Local Asset](https://img.ly/docs/cesdk/android/import-media/from-local-source/local-asset-3f93f2/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-local-asset/ImportLocalAsset.kt reference-only import android.content.Context import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri import ly.img.engine.AssetDefinition 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.File import java.util.UUID private const val LOCAL_ASSET_SOURCE_ID = "ly.img.guide.local-assets" private val LOCAL_ASSET_MIME_TYPES = listOf( "image/*", "video/*", "audio/*", ) data class LocalAssetImportResult( val sourceId: String, val assetId: String, val mimeType: String, val insertedBlock: DesignBlock?, ) suspend fun importLocalAsset( context: Context, engine: Engine, fileUri: Uri, displayName: String, sourceId: String = LOCAL_ASSET_SOURCE_ID, thumbnailUri: Uri? = null, videoDurationSeconds: Double? = null, ): LocalAssetImportResult { if (sourceId !in engine.asset.findAllSources()) { engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = LOCAL_ASSET_MIME_TYPES, ) } val mimeType = engine.editor.getMimeType(uri = fileUri) val supportedMimeTypes = engine.asset.getSourceSupportedMimeTypes(sourceId = sourceId) val acceptsMimeType = supportedMimeTypes.isEmpty() || supportedMimeTypes.any { supportedMimeType -> supportedMimeType == "*/*" || supportedMimeType == mimeType || (supportedMimeType.endsWith("/*") && mimeType.startsWith(supportedMimeType.removeSuffix("*"))) } require(acceptsMimeType) { "Unsupported local asset MIME type: $mimeType" } val asset = createLocalAssetDefinition( context = context, fileUri = fileUri, displayName = displayName, mimeType = mimeType, thumbnailUri = thumbnailUri, videoDurationSeconds = videoDurationSeconds, ) engine.asset.addAsset(sourceId = sourceId, asset = asset) val importedAsset = engine.asset.fetchAsset( sourceId = sourceId, assetId = asset.id, ) ?: error("Could not fetch imported local asset: ${asset.id}") val insertedBlock = engine.asset.defaultApplyAsset(asset = importedAsset) return LocalAssetImportResult( sourceId = sourceId, assetId = asset.id, mimeType = mimeType, insertedBlock = insertedBlock, ) } data class LocalAssetDimensions( val width: Int, val height: Int, ) fun readLocalAssetDimensions( context: Context, fileUri: Uri, mimeType: String, ): LocalAssetDimensions? = when { mimeType.startsWith("image/") -> readImageDimensions(context, fileUri) mimeType.startsWith("video/") -> readVideoDimensions(context, fileUri) else -> null } private fun readImageDimensions( context: Context, fileUri: Uri, ): LocalAssetDimensions { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } when (fileUri.scheme) { "file" -> { val file = File(requireNotNull(fileUri.path) { "File URI has no path: $fileUri" }) file.inputStream().use { input -> BitmapFactory.decodeStream(input, null, options) } } else -> { context.contentResolver.openInputStream(fileUri)?.use { input -> BitmapFactory.decodeStream(input, null, options) } ?: error("Could not open local image: $fileUri") } } return requireDimensions( width = options.outWidth, height = options.outHeight, fileUri = fileUri, ) } private fun readVideoDimensions( context: Context, fileUri: Uri, ): LocalAssetDimensions { val retriever = MediaMetadataRetriever() return try { retriever.setDataSource(context, fileUri) val width = retriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?.toIntOrNull() val height = retriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?.toIntOrNull() val rotation = retriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) ?.toIntOrNull() val dimensions = requireDimensions( width = width, height = height, fileUri = fileUri, ) if (rotation == 90 || rotation == 270) { LocalAssetDimensions(width = dimensions.height, height = dimensions.width) } else { dimensions } } finally { runCatching { retriever.release() } } } private fun requireDimensions( width: Int?, height: Int?, fileUri: Uri, ): LocalAssetDimensions { require(width != null && width > 0 && height != null && height > 0) { "Could not read dimensions for local asset: $fileUri" } return LocalAssetDimensions(width = width, height = height) } fun createLocalAssetDefinition( context: Context, fileUri: Uri, displayName: String, mimeType: String, thumbnailUri: Uri? = null, videoDurationSeconds: Double? = null, dimensions: LocalAssetDimensions? = null, ): AssetDefinition { val metadata = mutableMapOf( "uri" to fileUri.toString(), "mimeType" to mimeType, ) val mediaDimensions = dimensions ?: readLocalAssetDimensions( context = context, fileUri = fileUri, mimeType = mimeType, ) mediaDimensions?.let { metadata["width"] = it.width.toString() metadata["height"] = it.height.toString() } when { mimeType.startsWith("image/") -> { metadata["kind"] = "image" metadata["thumbUri"] = (thumbnailUri ?: fileUri).toString() metadata["blockType"] = DesignBlockType.Graphic.key metadata["fillType"] = FillType.Image.key metadata["shapeType"] = ShapeType.Rect.key } mimeType.startsWith("video/") -> { val duration = requireNotNull(videoDurationSeconds) { "Video assets require duration metadata in seconds." } metadata["kind"] = "video" thumbnailUri?.let { metadata["thumbUri"] = it.toString() } metadata["duration"] = duration.toString() metadata["blockType"] = DesignBlockType.Graphic.key metadata["fillType"] = FillType.Video.key metadata["shapeType"] = ShapeType.Rect.key } mimeType.startsWith("audio/") -> { metadata["kind"] = "audio" metadata["blockType"] = DesignBlockType.Audio.key } else -> error("Unsupported local asset MIME type: $mimeType") } return AssetDefinition( id = UUID.randomUUID().toString(), label = mapOf("en" to displayName), tags = mapOf("en" to listOf("local", "device")), meta = metadata, ) } ``` Register a local asset source, add a file `Uri` from the user's device, and optionally insert the imported asset into the active scene. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-local-asset) Local asset sources are useful when your app already has access to files on the device. The file can come from Android's system document picker, your app's private storage, or a file that your app copied into its sandbox. This guide starts from a `Uri` that your app already resolved. If you receive a `content://` URI from `ActivityResultContracts.OpenDocument` and need access after the current session, call `takePersistableUriPermission` in your app before handing the URI to CE.SDK. ## Register a Local Source Create one stable source ID for the imported files. The example registers the source only once, then stores the supported MIME types on the source so later validation uses the same allow list. ```kotlin highlight-android-register-source if (sourceId !in engine.asset.findAllSources()) { engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = LOCAL_ASSET_MIME_TYPES, ) } ``` Use a dedicated source ID for each logical bucket of app-managed files. For example, keep user documents separate from bundled brand assets when they should appear in different library sections. ## Validate the Picked File Detect the MIME type through the engine and compare it with the source's supported types before adding the asset. This catches unsupported files before the asset library or apply flow tries to use them. ```kotlin highlight-android-validate-file val mimeType = engine.editor.getMimeType(uri = fileUri) val supportedMimeTypes = engine.asset.getSourceSupportedMimeTypes(sourceId = sourceId) val acceptsMimeType = supportedMimeTypes.isEmpty() || supportedMimeTypes.any { supportedMimeType -> supportedMimeType == "*/*" || supportedMimeType == mimeType || (supportedMimeType.endsWith("/*") && mimeType.startsWith(supportedMimeType.removeSuffix("*"))) } require(acceptsMimeType) { "Unsupported local asset MIME type: $mimeType" } ``` For gallery-wide imports, use the Android photo-roll flow instead of this local-source pattern. For files that must move to backend storage before use, run the upload first and register the permanent URI. ## Read Media Dimensions For image and video assets, read the intrinsic dimensions before creating the asset definition. CE.SDK uses this metadata when it creates the visual block, so non-square media keeps its aspect ratio when inserted. ```kotlin highlight-android-read-dimensions data class LocalAssetDimensions( val width: Int, val height: Int, ) fun readLocalAssetDimensions( context: Context, fileUri: Uri, mimeType: String, ): LocalAssetDimensions? = when { mimeType.startsWith("image/") -> readImageDimensions(context, fileUri) mimeType.startsWith("video/") -> readVideoDimensions(context, fileUri) else -> null } private fun readImageDimensions( context: Context, fileUri: Uri, ): LocalAssetDimensions { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } when (fileUri.scheme) { "file" -> { val file = File(requireNotNull(fileUri.path) { "File URI has no path: $fileUri" }) file.inputStream().use { input -> BitmapFactory.decodeStream(input, null, options) } } else -> { context.contentResolver.openInputStream(fileUri)?.use { input -> BitmapFactory.decodeStream(input, null, options) } ?: error("Could not open local image: $fileUri") } } return requireDimensions( width = options.outWidth, height = options.outHeight, fileUri = fileUri, ) } private fun readVideoDimensions( context: Context, fileUri: Uri, ): LocalAssetDimensions { val retriever = MediaMetadataRetriever() return try { retriever.setDataSource(context, fileUri) val width = retriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?.toIntOrNull() val height = retriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?.toIntOrNull() val rotation = retriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) ?.toIntOrNull() val dimensions = requireDimensions( width = width, height = height, fileUri = fileUri, ) if (rotation == 90 || rotation == 270) { LocalAssetDimensions(width = dimensions.height, height = dimensions.width) } else { dimensions } } finally { runCatching { retriever.release() } } } private fun requireDimensions( width: Int?, height: Int?, fileUri: Uri, ): LocalAssetDimensions { require(width != null && width > 0 && height != null && height > 0) { "Could not read dimensions for local asset: $fileUri" } return LocalAssetDimensions(width = width, height = height) } ``` ## Build the Asset Definition An `AssetDefinition` stores the URI and the metadata CE.SDK needs to present and apply the asset. The sample supports images, videos, and audio; image and video assets use typed block and fill constants plus `width` and `height` metadata, while audio assets use the audio block type. For videos, pass the clip duration in seconds so the inserted block matches the local file length. ```kotlin highlight-android-define-asset fun createLocalAssetDefinition( context: Context, fileUri: Uri, displayName: String, mimeType: String, thumbnailUri: Uri? = null, videoDurationSeconds: Double? = null, dimensions: LocalAssetDimensions? = null, ): AssetDefinition { val metadata = mutableMapOf( "uri" to fileUri.toString(), "mimeType" to mimeType, ) val mediaDimensions = dimensions ?: readLocalAssetDimensions( context = context, fileUri = fileUri, mimeType = mimeType, ) mediaDimensions?.let { metadata["width"] = it.width.toString() metadata["height"] = it.height.toString() } when { mimeType.startsWith("image/") -> { metadata["kind"] = "image" metadata["thumbUri"] = (thumbnailUri ?: fileUri).toString() metadata["blockType"] = DesignBlockType.Graphic.key metadata["fillType"] = FillType.Image.key metadata["shapeType"] = ShapeType.Rect.key } mimeType.startsWith("video/") -> { val duration = requireNotNull(videoDurationSeconds) { "Video assets require duration metadata in seconds." } metadata["kind"] = "video" thumbnailUri?.let { metadata["thumbUri"] = it.toString() } metadata["duration"] = duration.toString() metadata["blockType"] = DesignBlockType.Graphic.key metadata["fillType"] = FillType.Video.key metadata["shapeType"] = ShapeType.Rect.key } mimeType.startsWith("audio/") -> { metadata["kind"] = "audio" metadata["blockType"] = DesignBlockType.Audio.key } else -> error("Unsupported local asset MIME type: $mimeType") } return AssetDefinition( id = UUID.randomUUID().toString(), label = mapOf("en" to displayName), tags = mapOf("en" to listOf("local", "device")), meta = metadata, ) } ``` A `uri` is required for these media assets. The sample also stores the detected `mimeType`, avoiding another MIME lookup during insertion. Add intrinsic dimensions for visual assets. Include user-facing labels and tags so asset search can find the item later. For videos shown in a visual grid, pass a generated poster image as `thumbnailUri`. ## Add the Asset Add the definition to the local source. `addAsset` indexes the asset and notifies subscribers, so any asset library or custom UI displaying the source can refresh automatically. ```kotlin highlight-android-add-asset engine.asset.addAsset(sourceId = sourceId, asset = asset) ``` Call `assetSourceContentsChanged` only after an out-of-band change that the engine cannot observe, such as replacing files in a custom source's backing store without calling `addAsset` or `removeAsset`. To expose this source in the CE.SDK editor UI, include an `AssetSourceType(sourceId)` entry in the relevant asset library category. The [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) guide covers how to configure the visible library sections. ## Insert the Asset Immediately If your import button should place the file on the canvas right away, fetch the imported asset by its generated ID and pass it to `defaultApplyAsset`. Skip this step when you only want the file to appear in the asset library for later selection. ```kotlin highlight-android-insert-asset val importedAsset = engine.asset.fetchAsset( sourceId = sourceId, assetId = asset.id, ) ?: error("Could not fetch imported local asset: ${asset.id}") val insertedBlock = engine.asset.defaultApplyAsset(asset = importedAsset) ``` `defaultApplyAsset` returns the new block when the asset type can be inserted into the active scene. Make sure an active scene exists before calling it. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | CE.SDK cannot read a picked file | The app no longer has permission to access its `content://` URI | Persist the read permission returned by `ActivityResultContracts.OpenDocument`, or copy the file into app-managed storage | | An inserted image or video has the wrong aspect ratio | Its `AssetDefinition` is missing valid `width` and `height` metadata | Read the intrinsic dimensions before adding the asset and store both values in `meta` | | A video import fails while building the definition | The sample did not receive the clip duration | Read the duration from the local file and pass it in seconds as `videoDurationSeconds` | | Adding an asset fails with a duplicate-ID error | The source already contains an asset with the generated ID | Generate a unique ID for each definition, or remove the existing asset before adding it again | ## API Reference | Method | Purpose | | --- | --- | | `engine.asset.findAllSources()` | Check whether the local source is already registered | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Register a mutable local asset source | | `engine.editor.getMimeType(uri=_)` | Resolve the MIME type for a local `Uri` | | `engine.asset.getSourceSupportedMimeTypes(sourceId=_)` | Read the MIME allow list stored on the source | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add an `AssetDefinition` to a local source | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify subscribers after an out-of-band source change | | `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Fetch the newly added asset by its generated ID | | `engine.asset.defaultApplyAsset(asset=_)` | Insert the asset into the active scene using CE.SDK's default behavior | ## Key Types | Type | Purpose | | --- | --- | | `AssetDefinition` | Stores the asset ID, localized labels and tags, and metadata such as `uri`, `mimeType`, `width`, `height`, `duration`, `kind`, and block creation hints | | `DesignBlockType` | Supplies typed block keys for graphic and audio assets | | `FillType` | Supplies typed fill keys for image and video assets | | `ShapeType` | Supplies the typed rectangle shape used for visual assets | ## Next Steps - [From Photo Roll](https://img.ly/docs/cesdk/android/import-media/from-local-source/photo-roll-23820d/) — Select and import photos from the device's media library - [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) — Enable file picker uploads from end users for use in the editor - [Edit or Remove Assets](https://img.ly/docs/cesdk/android/import-media/edit-or-remove-assets-ce072c/) — Update metadata or remove assets and local sources - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) — Configure which asset sources appear in the editor UI - [File Format Support](https://img.ly/docs/cesdk/android/import-media/file-format-support-8cdc84/) — Check supported media formats before adding local 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: "From Photo Roll" description: "Enable users to browse and select images and videos from the Android device gallery within the CE.SDK editor." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-local-source/photo-roll-23820d/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Local Source](https://img.ly/docs/cesdk/android/import-media/from-local-source-39b2a9/) > [From Photo Roll](https://img.ly/docs/cesdk/android/import-media/from-local-source/photo-roll-23820d/) --- ```kotlin file=@cesdk_android_examples/editor-guides-import-media-from-local-source-photo-roll/PhotoRollEditorSolution.kt reference-only import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import ly.img.editor.Editor import ly.img.editor.core.UnstableEditorApi import ly.img.editor.core.component.Dock import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberCloseEditor import ly.img.editor.core.component.rememberSystemGallery 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.core.library.AssetLibrary import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.SystemGalleryAssetSource import ly.img.editor.core.library.data.SystemGalleryConfiguration import ly.img.editor.core.library.data.SystemGalleryPermission import ly.img.engine.Engine @Composable fun PhotoRollEditorSolution( license: String, enableFullGalleryAccess: Boolean, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { val galleryAccessConfiguration = if (enableFullGalleryAccess) { EditorConfiguration.remember { onLoaded = { registerGallerySources( engine = editorContext.engine, context = editorContext.activity, ) @OptIn(UnstableEditorApi::class) SystemGalleryPermission.setMode(SystemGalleryConfiguration.Enabled) } } } else { EditorConfiguration.remember { onLoaded = { registerGallerySources( engine = editorContext.engine, context = editorContext.activity, ) @OptIn(UnstableEditorApi::class) SystemGalleryPermission.setMode(SystemGalleryConfiguration.Disabled) } } } galleryAccessConfiguration.then { dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } } } } } assetLibrary = { remember { AssetLibrary.getDefault(includeAVResources = true) } } navigationBar = { NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } } } } } } }, onClose = onClose, ) } fun registerGallerySources( engine: Engine, context: Context, ) { val existingSourceIds = engine.asset.findAllSources().toSet() listOf( AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, AssetSourceType.GalleryAllVisuals, ).forEach { sourceType -> if (sourceType.sourceId !in existingSourceIds) { engine.asset.addSource(SystemGalleryAssetSource(context, sourceType)) } } } ``` Enable Android gallery access through the CE.SDK editor Gallery surface, either with the system photo picker or in-app device gallery browsing. ![Gallery open in the CE.SDK Android editor](./assets/android.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-import-media-from-local-source-photo-roll) We integrate Android gallery access through `SystemGalleryAssetSource` and the editor's Gallery button. `SystemGalleryConfiguration.Disabled` keeps `MediaStore` indexing disabled and uses the system photo picker, while `SystemGalleryConfiguration.Enabled` lets the editor browse images and videos after the app receives full media read access. > **Note:** The [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) includes a complete > editor surface. If you build your own editor configuration, register the same > gallery sources and expose a Gallery entry in your dock. ## Understanding Photo Roll Integration On Android, Photo Roll integration maps to the device Gallery. The editor uses these four pieces together: | Piece | Role | | --- | --- | | `SystemGalleryAssetSource` | Reads selected media and, when enabled, matching `MediaStore` items. | | `AssetSourceType.GalleryImage`, `GalleryVideo`, and `GalleryAllVisuals` | Define which gallery media a library section should show. | | `Dock.Button.rememberSystemGallery` | Opens the editor Gallery surface from the dock. | | `SystemGalleryPermission.setMode(...)` | Chooses the manual picker flow or full gallery indexing. | The Starter Kits use manual picker mode by default. In that mode, the Gallery surface only lists media that the user adds through the system picker or camera. ## Register Gallery Sources Register one `SystemGalleryAssetSource` for every Gallery source type your asset library uses. The manual-picker configuration below runs the registration from `onLoaded`, after the default editor scene loads, and explicitly selects `SystemGalleryConfiguration.Disabled`. ```kotlin highlight-android-register-gallery-sources EditorConfiguration.remember { onLoaded = { registerGallerySources( engine = editorContext.engine, context = editorContext.activity, ) @OptIn(UnstableEditorApi::class) SystemGalleryPermission.setMode(SystemGalleryConfiguration.Disabled) } } ``` The callback invokes this helper to add the image, video, and combined visual sources only when they are not registered already: ```kotlin highlight-android-gallery-source-helper fun registerGallerySources( engine: Engine, context: Context, ) { val existingSourceIds = engine.asset.findAllSources().toSet() listOf( AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, AssetSourceType.GalleryAllVisuals, ).forEach { sourceType -> if (sourceType.sourceId !in existingSourceIds) { engine.asset.addSource(SystemGalleryAssetSource(context, sourceType)) } } } ``` ## Show the Gallery Button Add `Dock.Button.rememberSystemGallery` when your editor configuration replaces or customizes the default dock. The button opens the configured Gallery category from the asset library. ```kotlin highlight-android-show-gallery-button dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } } } } } ``` This focused sample creates a fresh `Dock.ListBuilder.remember`, which defines the complete dock and therefore produces a Gallery-only dock. Add every other required button to the same builder, or modify the inherited list when your configuration extends an existing dock. The sample uses `AssetLibrary.getDefault(includeAVResources = true)` so the Gallery category can show both images and videos. Compose `remember` retains the library instance across configuration recompositions. Use the default image-only asset library when your editor should import photos only. ```kotlin highlight-android-audiovisual-asset-library assetLibrary = { remember { AssetLibrary.getDefault(includeAVResources = true) } } ``` ## Enable Full Gallery Access Call `SystemGalleryPermission.setMode(SystemGalleryConfiguration.Enabled)` when you want CE.SDK to query the device `MediaStore` after the user grants Android media permissions. `SystemGalleryPermission.setMode` is marked with `@UnstableEditorApi`. The sample applies `@OptIn` locally at the `setMode` call inside `onLoaded`, immediately after registering the Gallery asset sources. ```kotlin highlight-android-enable-full-gallery-access EditorConfiguration.remember { onLoaded = { registerGallerySources( engine = editorContext.engine, context = editorContext.activity, ) @OptIn(UnstableEditorApi::class) SystemGalleryPermission.setMode(SystemGalleryConfiguration.Enabled) } } ``` To use manual picker mode in a custom Base Editor configuration, pass `SystemGalleryConfiguration.Disabled` to `SystemGalleryPermission.setMode` instead. The Gallery surface still works, but it only lists media the user adds through the available selection flows. ## Configure Permissions for Full Gallery Access `SystemGalleryConfiguration.Disabled` launches `ActivityResultContracts.PickVisualMedia` and requires no media read permissions. Declare the permissions below only when you enable `MediaStore` indexing with `SystemGalleryConfiguration.Enabled`. For the sample's `GalleryAllVisuals` setup, and for the current built-in Gallery permission helper even when an image-only or video-only Gallery source is opened, Android 13 and later requests both image and video media permissions. The Android 14 request set also includes `READ_MEDIA_VISUAL_USER_SELECTED`, but a fresh limited-access grant does not populate CE.SDK's process-local selected-URI list. The current Gallery source therefore cannot enumerate those OS-selected items from that grant alone. Use full media access for in-editor `MediaStore` browsing, or keep `SystemGalleryConfiguration.Disabled` and use the explicit system picker. Declare the media permissions in your app manifest. Cap the legacy storage permission at API 32 so Android 13 and later use the granular media permissions: ```xml ``` | Permission | When to use it | | --- | --- | | `READ_MEDIA_IMAGES` | Enables image indexing on Android 13 and later. | | `READ_MEDIA_VIDEO` | Enables video indexing on Android 13 and later. | | `READ_MEDIA_VISUAL_USER_SELECTED` | Included in the Android 14 request set; a fresh limited grant alone does not enable Gallery enumeration in the current implementation. | | `READ_EXTERNAL_STORAGE` | Enables gallery indexing on Android 12 and lower; declare it with `android:maxSdkVersion="32"`. | The CE.SDK Android libraries do not add these permissions to your app manifest. When full gallery indexing is enabled, the built-in Gallery UI requests the permission set returned by `SystemGalleryPermission.requiredPermission(...)` for the Gallery source the user opens. > **Caution:** Treat a limited Android 14 grant as unavailable for in-editor browsing unless > the URI was added through CE.SDK's explicit picker or camera flow. Keep the > empty state usable and offer the manual picker as a fallback. ## Persist Media With the Upload Flow The built-in `SystemGalleryAssetSource` path applies selected assets directly to the scene. It does not call `EditorConfiguration.onUpload`, so this Gallery flow does not expose an interception point for copying the selected content URI. When imported media must live in app-owned storage or a backend, use the [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) flow instead. That flow adds files through an `UploadAssetSourceType` and calls `onUpload`, where your app can replace the temporary URI before CE.SDK registers and applies the asset. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builder=_)` | Builds and remembers the editor configuration. | | `EditorConfigurationBuilder.onLoaded` | Runs gallery source registration after the editor scene loads. | | `editorContext.engine` | Provides the editor-owned engine inside configuration callbacks. | | `editorContext.activity` | Provides the Android context used by the gallery asset source. | | `engine.asset.findAllSources()` | Reads registered asset source IDs before adding gallery sources. | | `engine.asset.addSource(source=_)` | Registers a `SystemGalleryAssetSource` with the engine. | | `AssetSourceType.sourceId` | Provides the source ID used to avoid duplicate registrations. | | `SystemGalleryAssetSource(context=_, type=_)` | Creates an asset source for Android device gallery media. | | `Dock.remember(builder=_)` | Creates the dock component. | | `Dock.ListBuilder.remember(builder=_)` | Defines the complete ordered list of dock items. | | `Dock.ListBuilder.add(block=_)` | Adds a dock item to the list. | | `Dock.Button.rememberSystemGallery()` | Adds a dock button that opens the editor Gallery surface. | | `remember(calculation=_)` | Retains the asset library instance across recompositions. | | `AssetLibrary.getDefault(includeAVResources=_)` | Builds the default asset library with image-only or audiovisual Gallery content. | | `SystemGalleryPermission.setMode(configuration=_)` | Enables or disables full `MediaStore` gallery indexing. | | `SystemGalleryPermission.requiredPermission(mimeTypes=_)` | Returns no permissions in manual mode or the Android permissions requested for enabled `MediaStore` access. | Related types: | Type | Role | | --- | --- | | `AssetSourceType.GalleryImage` | Gallery source type for images. | | `AssetSourceType.GalleryVideo` | Gallery source type for videos. | | `AssetSourceType.GalleryAllVisuals` | Gallery source type for combined image and video browsing. | | `SystemGalleryConfiguration.Enabled` | Enables full Gallery asset-source access. | | `SystemGalleryConfiguration.Disabled` | Disables `MediaStore` indexing and uses the manual picker flow. | | `UnstableEditorApi` | Required opt-in for changing gallery permission mode. | ## Troubleshooting - **The Gallery button is missing:** Add `Dock.Button.rememberSystemGallery()` when your dock configuration replaces the default dock items. - **The Gallery surface is empty:** Register the matching `SystemGalleryAssetSource` values and verify the user added matching media through the explicit picker or granted full media access. A fresh Android 14 limited grant alone does not enable enumeration. - **The permission prompt does not appear:** Check that your manifest declares the required media permissions and that full gallery indexing is enabled. - **Imported assets need app-owned persistence:** Use the [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) flow and persist the file inside `onUpload` before CE.SDK registers it. ## Next Steps - [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) - enable file picker uploads from end users for use in the editor. - [Capture From Camera](https://img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388/) - capture photos or videos directly from a connected camera for immediate use in your design. - [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) - connect CE.SDK to external sources like servers or third-party platforms to import assets remotely. --- ## 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: "From User Upload" description: "Enable file picker uploads from end users for use in the editor." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Local Source](https://img.ly/docs/cesdk/android/import-media/from-local-source-39b2a9/) > [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) --- ```kotlin file=@cesdk_android_examples/editor-guides-import-media-from-local-source-user-upload/UserUploadEditorSolution.kt reference-only import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.component.Dock import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberCloseEditor import ly.img.editor.core.component.rememberImagesLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.UploadAssetSourceType import ly.img.engine.AssetDefinition import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.util.UUID @Composable fun UserUploadEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { val engine = editorContext.engine val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(block = page, value = 1080F) engine.block.setHeight(block = page, value = 1080F) engine.block.setFill(block = page, fill = engine.block.createFill(FillType.Color)) engine.block.appendChild(parent = scene, child = page) registerUploadAssetSources(engine) } assetLibrary = { remember { userUploadAssetLibrary() } } onUpload = { assetDefinition, uploadSource -> persistUploadedAsset( context = editorContext.activity, assetDefinition = assetDefinition, uploadSource = uploadSource, ) } navigationBar = { NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } } } } } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberImagesLibrary() } } } horizontalArrangement = { Arrangement.Center } } } } }, onClose = onClose, ) } private fun registerUploadAssetSources(engine: Engine) { val existingSources = engine.asset.findAllSources().toSet() fun addIfMissing( source: UploadAssetSourceType, mimeTypes: List, ) { if (source.sourceId !in existingSources) { engine.asset.addLocalSource( sourceId = source.sourceId, supportedMimeTypes = mimeTypes, ) } } addIfMissing( source = AssetSourceType.ImageUploads, mimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", "image/webp", ), ) } private fun userUploadAssetLibrary(): AssetLibrary { val images = LibraryCategory.Images.copy( content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_images, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_gallery, sourceTypes = listOf(AssetSourceType.GalleryImage), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_gallery, sourceType = AssetSourceType.GalleryImage, assetType = AssetType.Image, ), ), LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_image_uploads, sourceTypes = listOf(AssetSourceType.ImageUploads), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_image_uploads, sourceType = AssetSourceType.ImageUploads, assetType = AssetType.Image, ), ), ), ), ) return AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, ), images = images, ) } private suspend fun persistUploadedAsset( context: Context, assetDefinition: AssetDefinition, uploadSource: UploadAssetSourceType, ): AssetDefinition { val meta = assetDefinition.meta ?: return assetDefinition val sourceUri = meta["uri"]?.toUri() ?: return assetDefinition val persistentUri = copyUriToAppStorage( context = context, sourceUri = sourceUri, uploadSource = uploadSource, fileLabel = "media", ) val updatedMeta = meta.toMutableMap() updatedMeta["uri"] = persistentUri.toString() meta["thumbUri"]?.let { thumbnail -> updatedMeta["thumbUri"] = if (thumbnail == sourceUri.toString()) { persistentUri.toString() } else { copyUriToAppStorage( context = context, sourceUri = thumbnail.toUri(), uploadSource = uploadSource, fileLabel = "thumbnail", ).toString() } } return assetDefinition.copy(meta = updatedMeta) } private suspend fun copyUriToAppStorage( context: Context, sourceUri: Uri, uploadSource: UploadAssetSourceType, fileLabel: String, ): Uri = withContext(Dispatchers.IO) { val directory = File(context.filesDir, "cesdk-user-uploads/${uploadSource.sourceId}") directory.mkdirs() val extension = sourceUri.extension(context, uploadSource) val outputFile = File(directory, "$fileLabel-${UUID.randomUUID()}.$extension") openUriInputStream(context, sourceUri).use { input -> FileOutputStream(outputFile).use { output -> input.copyTo(output) } } Uri.fromFile(outputFile) } private fun openUriInputStream( context: Context, sourceUri: Uri, ) = context.contentResolver.openInputStream(sourceUri) ?: if (sourceUri.scheme == "file") { FileInputStream(requireNotNull(sourceUri.path) { "File URI has no path." }) } else { error("Cannot read uploaded asset URI: $sourceUri") } private fun Uri.extension( context: Context, uploadSource: UploadAssetSourceType, ): String { val detectedExtension = context.contentResolver.getType(this)?.let { mimeType -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } if (!detectedExtension.isNullOrBlank()) return detectedExtension return when (uploadSource.sourceId) { AssetSourceType.VideoUploads.sourceId -> "mp4" AssetSourceType.AudioUploads.sourceId -> "m4a" else -> "jpg" } } ``` Let users pick images from their Android device, add those files to an upload asset source, and replace temporary file URIs with app-owned storage before the assets appear in the editor. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-import-media-from-local-source-user-upload) Android user uploads are handled by upload asset sources in the CE.SDK editor UI. The editor opens the system picker or camera intent, creates an `AssetDefinition`, calls `EditorConfiguration.onUpload`, and then adds the returned asset to the matching local source. This guide focuses on image uploads. Use the same Android APIs with `AssetSourceType.VideoUploads` or `AssetSourceType.AudioUploads` and matching MIME types when your editor exposes video or audio upload sections. ## Register Upload Sources Upload buttons only work for asset sources that are registered in the engine. Create or load your scene in `onCreate`, then register the local upload source that your asset library should display before `onCreate` returns. ```kotlin highlight-android-create-scene-and-sources onCreate = { val engine = editorContext.engine val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(block = page, value = 1080F) engine.block.setHeight(block = page, value = 1080F) engine.block.setFill(block = page, fill = engine.block.createFill(FillType.Color)) engine.block.appendChild(parent = scene, child = page) registerUploadAssetSources(engine) } ``` The sample registers the predefined image upload source ID used by the editor UI. Use MIME types that match the files your app accepts. ```kotlin highlight-android-register-upload-sources private fun registerUploadAssetSources(engine: Engine) { val existingSources = engine.asset.findAllSources().toSet() fun addIfMissing( source: UploadAssetSourceType, mimeTypes: List, ) { if (source.sourceId !in existingSources) { engine.asset.addLocalSource( sourceId = source.sourceId, supportedMimeTypes = mimeTypes, ) } } addIfMissing( source = AssetSourceType.ImageUploads, mimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", "image/webp", ), ) } ``` The picker filters by the upload source type, while the local source stores the accepted asset definitions. ## Show Uploads in the Asset Library The upload UI appears when a library section contains one `UploadAssetSourceType` and `showUpload` stays enabled. This sample keeps the system gallery section in place so the editor can request media permissions, then adds a separate upload section for persisted files. ```kotlin highlight-android-show-upload-sources assetLibrary = { remember { userUploadAssetLibrary() } } ``` The asset-library helper keeps the rest of the editor unchanged but limits the media tabs to the system gallery and user-uploaded assets. ```kotlin highlight-android-upload-asset-library private fun userUploadAssetLibrary(): AssetLibrary { val images = LibraryCategory.Images.copy( content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_images, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_gallery, sourceTypes = listOf(AssetSourceType.GalleryImage), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_gallery, sourceType = AssetSourceType.GalleryImage, assetType = AssetType.Image, ), ), LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_image_uploads, sourceTypes = listOf(AssetSourceType.ImageUploads), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_image_uploads, sourceType = AssetSourceType.ImageUploads, assetType = AssetType.Image, ), ), ), ), ) return AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, ), images = images, ) } ``` For image sources, the button opens a small menu with system picker and camera choices. ## Persist Uploaded Assets When a user selects a file, CE.SDK creates an `AssetDefinition` with metadata such as `meta["uri"]`, `meta["thumbUri"]`, and `meta["kind"]`. Use `onUpload` to copy or upload the file before the asset is added to the source. ```kotlin highlight-android-handle-upload onUpload = { assetDefinition, uploadSource -> persistUploadedAsset( context = editorContext.activity, assetDefinition = assetDefinition, uploadSource = uploadSource, ) } ``` The sample copies the selected URI into app-owned storage and returns an updated definition with the new `meta["uri"]`. In production, replace the copy step with your storage client and return the permanent CDN or backend URL instead. ```kotlin highlight-android-persist-upload private suspend fun persistUploadedAsset( context: Context, assetDefinition: AssetDefinition, uploadSource: UploadAssetSourceType, ): AssetDefinition { val meta = assetDefinition.meta ?: return assetDefinition val sourceUri = meta["uri"]?.toUri() ?: return assetDefinition val persistentUri = copyUriToAppStorage( context = context, sourceUri = sourceUri, uploadSource = uploadSource, fileLabel = "media", ) val updatedMeta = meta.toMutableMap() updatedMeta["uri"] = persistentUri.toString() meta["thumbUri"]?.let { thumbnail -> updatedMeta["thumbUri"] = if (thumbnail == sourceUri.toString()) { persistentUri.toString() } else { copyUriToAppStorage( context = context, sourceUri = thumbnail.toUri(), uploadSource = uploadSource, fileLabel = "thumbnail", ).toString() } } return assetDefinition.copy(meta = updatedMeta) } private suspend fun copyUriToAppStorage( context: Context, sourceUri: Uri, uploadSource: UploadAssetSourceType, fileLabel: String, ): Uri = withContext(Dispatchers.IO) { val directory = File(context.filesDir, "cesdk-user-uploads/${uploadSource.sourceId}") directory.mkdirs() val extension = sourceUri.extension(context, uploadSource) val outputFile = File(directory, "$fileLabel-${UUID.randomUUID()}.$extension") openUriInputStream(context, sourceUri).use { input -> FileOutputStream(outputFile).use { output -> input.copyTo(output) } } Uri.fromFile(outputFile) } private fun openUriInputStream( context: Context, sourceUri: Uri, ) = context.contentResolver.openInputStream(sourceUri) ?: if (sourceUri.scheme == "file") { FileInputStream(requireNotNull(sourceUri.path) { "File URI has no path." }) } else { error("Cannot read uploaded asset URI: $sourceUri") } private fun Uri.extension( context: Context, uploadSource: UploadAssetSourceType, ): String { val detectedExtension = context.contentResolver.getType(this)?.let { mimeType -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } if (!detectedExtension.isNullOrBlank()) return detectedExtension return when (uploadSource.sourceId) { AssetSourceType.VideoUploads.sourceId -> "mp4" AssetSourceType.AudioUploads.sourceId -> "m4a" else -> "jpg" } } ``` If the asset has a separate thumbnail URI, persist that thumbnail as well. If the thumbnail points to the same URI as the media file, reuse the media URL in `meta["thumbUri"]`. ## Android and Web Differences Android does not expose the Web `UploadAssetSources` plugin, `cesdk.actions.register('uploadFile', ...)`, or `cesdk.utils.localUpload()`. Use the Android editor configuration instead: | Android API | What it replaces from Web | | --- | --- | | `AssetSourceType.ImageUploads`, `VideoUploads`, or `AudioUploads` | Upload asset-source plugin IDs | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Adding upload-capable local sources | | `EditorConfiguration.remember { onUpload = { assetDefinition, uploadSource -> ... } }` | Registering an upload handler | | `AssetDefinition.copy(meta=_)` | Returning a changed asset definition | The Android `onUpload` callback does not receive a CE.SDK progress callback. If your storage client reports progress, keep that state in your app UI while the suspending upload callback runs. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember { onCreate = { ... } }` | Creates or loads the scene and registers upload asset sources. | | `editorContext.engine` | Provides the editor-owned engine inside configuration callbacks. | | `engine.scene.create()` | Creates the blank sample scene. | | `engine.block.create(blockType=_)` | Creates the sample page block. | | `engine.block.setWidth(block=_, value=_)` | Sets the sample page width. | | `engine.block.setHeight(block=_, value=_)` | Sets the sample page height. | | `engine.block.createFill(fillType=_)` | Creates the sample page fill. | | `engine.block.setFill(block=_, fill=_)` | Assigns the sample page fill. | | `engine.block.appendChild(parent=_, child=_)` | Adds the sample page to the scene. | | `engine.asset.findAllSources()` | Reads registered source IDs before adding upload sources. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Registers a local asset source that accepts uploaded assets. | | `EditorConfiguration.remember { assetLibrary = { ... } }` | Provides the asset-library UI definition. | | `AssetLibrary.getDefault(tabs=_, images=_)` | Builds the default editor library with a customized upload tab. | | `LibraryContent.Section(sourceTypes=_, assetType=_)` | Defines a section that can show the upload button. | | `LibraryContent.Grid(sourceType=_, assetType=_)` | Shows uploaded assets after opening a section. | | `EditorConfiguration.remember { onUpload = { assetDefinition, uploadSource -> ... } }` | Customizes the asset before it is added to an upload source. | | `assetDefinition.meta` | Reads the generated asset metadata, including file and thumbnail URIs. | | `AssetDefinition.copy(meta=_)` | Returns an updated asset definition from `onUpload`. | Related types: | Type | Role | | --- | --- | | `AssetSourceType.ImageUploads` | Upload source type for images. | | `AssetSourceType.GalleryImage` | System gallery source that can request media permissions. | | `AssetSourceType.VideoUploads` | Optional counterpart for video upload sections. | | `AssetSourceType.AudioUploads` | Optional counterpart for audio upload sections. | | `UploadAssetSourceType` | Describes the upload source that triggered `onUpload`. | ## Troubleshooting - **The upload button is not visible:** Ensure the library section contains one `UploadAssetSourceType`, the same source ID is registered with `engine.asset.addLocalSource(...)`. - **The gallery cannot access media:** Keep a gallery source section available when you rely on editor-managed media permission prompts, or request media permission from your own app UI before showing an upload-only library. - **Assets disappear after app restart:** Persist the file and asset metadata in app storage or a backend, then re-register saved definitions on startup. - **Unsupported files appear in the picker:** Check the source MIME filter and the `supportedMimeTypes` passed to `addLocalSource(...)`. - **Large files block the upload flow:** Do file copying or network transfer work off the main thread and return from `onUpload` only after the permanent URI is ready. ## Next Steps - [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) - connect CE.SDK to server-backed or third-party asset sources. - [Customize the Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - change library tabs, sections, and source ordering for your 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: "Import From Remote Source" description: "Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) --- --- ## Related Pages - [Import Remote Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/remote-asset-484685/) - Load asset definitions from remote JSON files into CE.SDK's Android asset system. - [From Your Server](https://img.ly/docs/cesdk/android/import-media/from-remote-source/your-server-b91910/) - Load images, videos, audio, stickers, and templates from your backend servers into CE.SDK for Android. - [Integrate IMG.LY Premium Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/imgly-premium-assets-eb1688/) - Host IMG.LY premium templates on your infrastructure and integrate them into CE.SDK's asset library for user access. - [Integrate Third-Party APIs](https://img.ly/docs/cesdk/android/import-media/from-remote-source/third-party-a05854/) - Import media from any third-party service into CE.SDK using custom asset sources. - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) - Search and import high-quality stock images from Unsplash directly into CE.SDK. - [Integrate Pexels Stock Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/pexels-90d5df/) - Search and import royalty-free stock photos from Pexels in CE.SDK for Android. - [Integrate Getty Images Stock Photos](https://img.ly/docs/cesdk/android/import-media/from-remote-source/getty-images-a3931c/) - Search and import premium stock photography from Getty Images in CE.SDK for Android through a secure proxy server. - [Versioning of Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/asset-versioning-0a4d58/) - Learn how CE.SDK stores asset URLs in Android scenes and how to keep saved designs working when those URLs change. --- ## 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: "Versioning of Assets" description: "Learn how CE.SDK stores asset URLs in Android scenes and how to keep saved designs working when those URLs change." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/asset-versioning-0a4d58/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [Versioning of Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/asset-versioning-0a4d58/) --- ```kotlin file=@cesdk_android_examples/engine-guides-asset-versioning/AssetVersioning.kt reference-only import android.net.Uri 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 data class AssetVersioning( val storedUri: Uri, val sceneStringLength: Int, val archiveSizeBytes: Int, val updatedUri: Uri, val migratedUris: List, val migratedSourceSetUris: List, val migratedSceneStringLength: Int, val versionedUrlPatterns: List, ) suspend fun assetVersioning(engine: Engine): AssetVersioning { 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 imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg") val imageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = imageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(block = imageBlock, value = 50F) engine.block.setPositionY(block = imageBlock, value = 50F) engine.block.setWidth(block = imageBlock, value = 300F) engine.block.setHeight(block = imageBlock, value = 200F) val imageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = imageUri, ) engine.block.setFill(block = imageBlock, fill = imageFill) engine.block.appendChild(parent = page, child = imageBlock) val secondImageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_3.jpg") val secondImageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = secondImageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(block = secondImageBlock, value = 400F) engine.block.setPositionY(block = secondImageBlock, value = 50F) engine.block.setWidth(block = secondImageBlock, value = 300F) engine.block.setHeight(block = secondImageBlock, value = 200F) val secondImageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = secondImageFill, property = "fill/image/imageFileURI", value = secondImageUri, ) engine.block.setSourceSet( block = secondImageFill, property = "fill/image/sourceSet", 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.setFill(block = secondImageBlock, fill = secondImageFill) engine.block.appendChild(parent = page, child = secondImageBlock) val fill = engine.block.getFill(imageBlock) val storedUri = engine.block.getUri( block = fill, property = "fill/image/imageFileURI", ) check(storedUri == imageUri) val sceneString = engine.scene.saveToString(scene = scene) val archiveBuffer = engine.scene.saveToArchive(scene = scene) val archiveSizeBytes = archiveBuffer.asReadOnlyBuffer().remaining() check(sceneString.isNotEmpty()) check(archiveSizeBytes > 0) val loadedScene = engine.scene.load(scene = sceneString, waitForResources = true) val loadedPage = engine.scene.getPages().first() val loadedImageBlock = engine.block.getChildren(loadedPage).first() val loadedFill = engine.block.getFill(loadedImageBlock) val updatedUri = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg") engine.block.setUri( block = loadedFill, property = "fill/image/imageFileURI", value = updatedUri, ) val currentUri = engine.block.getUri( block = loadedFill, property = "fill/image/imageFileURI", ) check(currentUri == updatedUri) val oldAssetPrefix = "https://img.ly/static/ubq_samples" val newAssetPrefix = "https://cdn.example.com/assets/v2" val migratedUris = mutableListOf() val migratedSourceSetUris = mutableListOf() engine.block.findByType(DesignBlockType.Graphic).forEach { block -> if (!engine.block.supportsFill(block)) return@forEach val blockFill = engine.block.getFill(block) if (engine.block.getType(blockFill) != FillType.Image.key) return@forEach val previousUri = engine.block.getUri( block = blockFill, property = "fill/image/imageFileURI", ) if (previousUri.toString().startsWith(oldAssetPrefix)) { val migratedUri = Uri.parse( previousUri.toString().replace( oldValue = oldAssetPrefix, newValue = newAssetPrefix, ), ) engine.block.setUri( block = blockFill, property = "fill/image/imageFileURI", value = migratedUri, ) migratedUris += migratedUri } var sourceSetChanged = false val migratedSourceSet = engine.block.getSourceSet( block = blockFill, property = "fill/image/sourceSet", ).map { source -> if (!source.uri.toString().startsWith(oldAssetPrefix)) return@map source sourceSetChanged = true val migratedSourceUri = Uri.parse( source.uri.toString().replace( oldValue = oldAssetPrefix, newValue = newAssetPrefix, ), ) migratedSourceSetUris += migratedSourceUri source.copy(uri = migratedSourceUri) } if (sourceSetChanged) { engine.block.setSourceSet( block = blockFill, property = "fill/image/sourceSet", sourceSet = migratedSourceSet, ) } } val migratedSceneString = engine.scene.saveToString(scene = loadedScene) check(migratedUris.isNotEmpty()) check(migratedSceneString.isNotEmpty()) val pathVersionedUrl = "https://cdn.example.com/assets/v2/logo.png" val hashVersionedUrl = "https://cdn.example.com/assets/logo-a1b2c3d4.png" val queryVersionedUrl = "https://cdn.example.com/assets/logo.png?v=2" return AssetVersioning( storedUri = storedUri, sceneStringLength = sceneString.length, archiveSizeBytes = archiveSizeBytes, updatedUri = currentUri, migratedUris = migratedUris, migratedSourceSetUris = migratedSourceSetUris, migratedSceneStringLength = migratedSceneString.length, versionedUrlPatterns = listOf(pathVersionedUrl, hashVersionedUrl, queryVersionedUrl), ) } ``` Manage how CE.SDK stores and resolves asset URLs in saved designs, keeping designs functional when assets are updated or moved. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-asset-versioning) CE.SDK references assets via URLs rather than embedding files directly into designs. When you save a design with `engine.scene.saveToString()`, asset URLs are stored as strings. On load, CE.SDK fetches assets from those URLs. This keeps saved designs small but means URL changes can break existing designs. This guide explains how CE.SDK stores asset references and strategies for managing asset URLs over time. This guide covers how to inspect asset URLs stored in designs, the difference between scene serialization and archive export, how to programmatically update asset URLs, and strategies for versioned URL schemes. ## How Asset URLs Are Stored An asset reference lives on a fill block, not on the graphic block itself. When you add an image to a design, an image fill stores the source URL in its `fill/image/imageFileURI` property. Use `engine.block.getFill()` to reach the fill and `engine.block.getUri()` to inspect the stored URL. Responsive image variants are stored separately in `fill/image/sourceSet`, so include that property when a fill uses source sets. ```kotlin highlight-android-how-urls-stored val fill = engine.block.getFill(imageBlock) val storedUri = engine.block.getUri( block = fill, property = "fill/image/imageFileURI", ) ``` The `fill/image/imageFileURI` property contains exactly what gets written to the saved scene. CE.SDK does not transform or normalize these URLs; they are stored and loaded as-is. ## Scene Serialization vs Archive Export CE.SDK provides two approaches for saving designs, each with different trade-offs for asset handling. ### Saving as a Scene String The `saveToString()` method serializes the scene structure while keeping asset references as URLs. This produces a small `String` that loads quickly, but requires the original assets to remain available at their URLs. Persist the returned value wherever you store designs. ```kotlin highlight-android-save-scene val sceneString = engine.scene.saveToString(scene = scene) ``` Use scene strings when: - Assets are hosted on a stable CDN with reliable URLs. - You want to keep storage costs low. - Designs need to load quickly. - You can guarantee asset availability. ### Saving as an Archive The `saveToArchive()` method bundles the scene together with all referenced assets into a self-contained archive. Android returns the archive as a `ByteBuffer`, which your app can write to storage or upload. Archives work without network access because every accessible asset is embedded. ```kotlin highlight-android-save-archive val archiveBuffer = engine.scene.saveToArchive(scene = scene) ``` Archives are useful when: - Designs need to work offline. - You migrate designs between environments. - You cannot guarantee long-term URL availability. - Portability matters more than file size. | Approach | Method | Assets | File Size | Portability | | --- | --- | --- | --- | --- | | Scene | `saveToString()` | Referenced by URL | Small | Requires URL availability | | Archive | `saveToArchive()` | Embedded in archive | Larger | Self-contained | ## What Happens When URLs Change When a design is loaded and an asset URL returns a 404 or is otherwise unavailable, the affected block appears empty or shows an error state. Other blocks can still load, so a design may appear partially complete. CE.SDK does not provide automatic fallbacks or retries for failed asset loads. A previously fetched copy of the asset may still appear until caches expire, which can temporarily mask a broken URL. ## Updating Asset URLs Programmatically When you need to migrate assets to a new location, load the existing scene, update the URLs, and save the modified scene. Use `engine.block.setUri()` to point a fill at a new asset. ```kotlin highlight-android-update-url val loadedScene = engine.scene.load(scene = sceneString, waitForResources = true) val loadedPage = engine.scene.getPages().first() val loadedImageBlock = engine.block.getChildren(loadedPage).first() val loadedFill = engine.block.getFill(loadedImageBlock) val updatedUri = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg") engine.block.setUri( block = loadedFill, property = "fill/image/imageFileURI", value = updatedUri, ) val currentUri = engine.block.getUri( block = loadedFill, property = "fill/image/imageFileURI", ) ``` This pattern is useful when a known asset moves to a new permanent location. The updated scene string now points at the replacement URL. For batch migrations, scan graphic blocks, keep only image fills, replace the old CDN prefix in both the image file URL and any source-set entries, then save the migrated scene. ```kotlin highlight-android-batch-update val oldAssetPrefix = "https://img.ly/static/ubq_samples" val newAssetPrefix = "https://cdn.example.com/assets/v2" val migratedUris = mutableListOf() val migratedSourceSetUris = mutableListOf() engine.block.findByType(DesignBlockType.Graphic).forEach { block -> if (!engine.block.supportsFill(block)) return@forEach val blockFill = engine.block.getFill(block) if (engine.block.getType(blockFill) != FillType.Image.key) return@forEach val previousUri = engine.block.getUri( block = blockFill, property = "fill/image/imageFileURI", ) if (previousUri.toString().startsWith(oldAssetPrefix)) { val migratedUri = Uri.parse( previousUri.toString().replace( oldValue = oldAssetPrefix, newValue = newAssetPrefix, ), ) engine.block.setUri( block = blockFill, property = "fill/image/imageFileURI", value = migratedUri, ) migratedUris += migratedUri } var sourceSetChanged = false val migratedSourceSet = engine.block.getSourceSet( block = blockFill, property = "fill/image/sourceSet", ).map { source -> if (!source.uri.toString().startsWith(oldAssetPrefix)) return@map source sourceSetChanged = true val migratedSourceUri = Uri.parse( source.uri.toString().replace( oldValue = oldAssetPrefix, newValue = newAssetPrefix, ), ) migratedSourceSetUris += migratedSourceUri source.copy(uri = migratedSourceUri) } if (sourceSetChanged) { engine.block.setSourceSet( block = blockFill, property = "fill/image/sourceSet", sourceSet = migratedSourceSet, ) } } val migratedSceneString = engine.scene.saveToString(scene = loadedScene) ``` Run this kind of migration before decommissioning the old asset host. Keep the old URLs available until every saved design that depends on them has been migrated. ## Strategies for Versioned Asset URLs Designing your URL scheme to support versioning prevents accidental overwrites and makes migrations easier. ```kotlin highlight-android-versioned-urls val pathVersionedUrl = "https://cdn.example.com/assets/v2/logo.png" val hashVersionedUrl = "https://cdn.example.com/assets/logo-a1b2c3d4.png" val queryVersionedUrl = "https://cdn.example.com/assets/logo.png?v=2" ``` ### Path-Based Versioning Put the version in the path, such as `https://cdn.example.com/assets/v2/logo.png`. Old designs can keep using the `v1` path while new designs point to `v2`. ### Hash-Based Filenames Put a content hash in the filename, such as `logo-a1b2c3d4.png`. The URL changes whenever the file changes, which makes the URL safe to cache for a long time. ### Query Parameter Versioning Add the version as a query parameter, such as `logo.png?v=2`. This keeps the path stable, but you must verify that your CDN includes query parameters in its cache key. ## Best Practices When managing asset URLs in production: - **Use immutable URLs**: Content-addressed or versioned paths prevent accidental overwrites. - **Keep old assets available**: Do not delete assets that may be referenced by saved designs. - **Use archives for portability**: Export as an archive when designs need to work offline or across environments. - **Plan CDN migrations carefully**: Update saved designs before decommissioning old URLs. - **Set appropriate cache headers**: Balance performance with freshness requirements. - **Document your URL scheme**: Make the versioning strategy clear for your team. ## Troubleshooting | Issue | Cause | Fix | | --- | --- | --- | | Asset shows an old version | Cached copy of the asset | Use a cache-busting or versioned URL | | Asset does not load | URL changed or deleted | Verify URL accessibility, update the scene | | Design partially loads | Some assets unavailable | Check all asset URLs, consider an archive export | | Archive is large | Archive embeds referenced assets | Optimize media before archiving or use scene strings | ## API Reference | Method | Description | | --- | --- | | `engine.block.getFill(block=_)` | Get the fill block that stores the media URL | | `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Read an image fill URL | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Replace an image fill URL | | `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Read responsive image source URLs | | `engine.block.setSourceSet(block=_, property="fill/image/sourceSet", sourceSet=_)` | Replace responsive image source URLs | | `engine.block.getChildren(block=_)` | Read child blocks from the loaded page | | `engine.block.findByType(type=_)` | Find blocks to inspect or migrate | | `engine.block.supportsFill(block=_)` | Skip blocks that cannot have fills | | `engine.block.getType(block=_)` | Check whether a fill is an image fill | | `engine.scene.saveToString(scene=_)` | Save a scene with URI references | | `engine.scene.load(scene=_, waitForResources=_)` | Load a saved scene string | | `engine.scene.getPages()` | Get pages from the loaded scene | | `engine.scene.saveToArchive(scene=_)` | Save a self-contained archive as a `ByteBuffer` | | `engine.scene.load(sceneUri=_, waitForResources=_)` | Load an archive saved to a URI | ## Next Steps - [Serve Assets From Your Server](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) - Configure self-hosted assets. - [Save Designs](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) - Save and serialize designs. - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Review export options including archives. --- ## 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: "Integrate Getty Images Stock Photos" description: "Search and import premium stock photography from Getty Images in CE.SDK for Android through a secure proxy server." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/getty-images-a3931c/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [From Getty Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/getty-images-a3931c/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-getty-images/GettyImages.kt reference-only import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetCredits import ly.img.engine.AssetLicense import ly.img.engine.AssetSource 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.FindAssetsResult import ly.img.engine.ShapeType import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL suspend fun gettyImages( engine: Engine, proxyBaseUrl: String, proxyResponseProvider: (suspend (Uri) -> JSONObject)? = null, ): GettyImagesImportResult { 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 source = GettyImagesAssetSource( proxyBaseUrl = proxyBaseUrl, proxyResponseProvider = proxyResponseProvider, ) if (source.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(source) } val results = engine.asset.findAssets( sourceId = GettyImagesAssetSource.SOURCE_ID, query = FindAssetsQuery(query = "architecture", page = 0, perPage = 12), ) val firstBlock = results.assets.firstOrNull()?.let { asset -> engine.asset.applyAssetSourceAsset( sourceId = GettyImagesAssetSource.SOURCE_ID, asset = asset, ) } return GettyImagesImportResult(total = results.total, firstBlock = firstBlock) } data class GettyImagesImportResult( val total: Int, val firstBlock: DesignBlock?, ) @Composable fun GettyImagesEditorSolution( license: String, proxyBaseUrl: String, onClose: (Throwable?) -> Unit, ) { val gettyImagesAssetSource = remember(proxyBaseUrl) { GettyImagesAssetSource(proxyBaseUrl = proxyBaseUrl) } Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { val engine = editorContext.engine if (gettyImagesAssetSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(gettyImagesAssetSource) } } assetLibrary = { remember { val gettyImagesSourceType = AssetSourceType(sourceId = gettyImagesAssetSource.sourceId) val gettyImagesSection = LibraryContent.Section( sourceTypes = listOf(gettyImagesSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = gettyImagesSourceType, assetType = AssetType.Image, title = "Getty Images", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(gettyImagesSection), ) } } } }, onClose = onClose, ) } class GettyImagesAssetSource( private val proxyBaseUrl: String, private val proxyResponseProvider: (suspend (Uri) -> JSONObject)? = null, ) : AssetSource(sourceId = SOURCE_ID) { override val supportedMimeTypes = listOf("image/jpeg") override val credits = AssetCredits( name = "Getty Images", uri = Uri.parse("https://www.gettyimages.com/"), ) override val license = AssetLicense( name = "Getty Images Content License Agreement", uri = Uri.parse("https://www.gettyimages.com/eula"), ) override suspend fun getGroups(): List? = null override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val proxyResponse = loadGettyImagesResponse( uri = buildGettyImagesSearchUri(proxyBaseUrl = proxyBaseUrl, query = query), responseProvider = proxyResponseProvider, ) toFindAssetsResult(response = proxyResponse, requestedPage = query.page, perPage = query.perPage) } companion object { const val SOURCE_ID = "ly.img.asset.source.getty.images" } } fun buildGettyImagesSearchUri( proxyBaseUrl: String, query: FindAssetsQuery, ): Uri = Uri.parse(proxyBaseUrl).buildUpon() .appendQueryParameter("phrase", query.query.orEmpty()) .appendQueryParameter("page", (query.page + 1).toString()) .appendQueryParameter("page_size", query.perPage.toString()) .build() suspend fun loadGettyImagesResponse( uri: Uri, responseProvider: (suspend (Uri) -> JSONObject)? = null, ): JSONObject = responseProvider?.invoke(uri) ?: readGettyImagesJson(uri) fun readGettyImagesJson(uri: Uri): JSONObject { val connection = URL(uri.toString()).openConnection() as HttpURLConnection return try { require(connection.responseCode in 200 until 300) { connection.errorStream?.bufferedReader()?.use { it.readText() }.orEmpty() } JSONObject(connection.inputStream.bufferedReader().use { it.readText() }) } finally { connection.disconnect() } } fun toFindAssetsResult( response: JSONObject, requestedPage: Int, perPage: Int, ): FindAssetsResult { val images = response.optJSONArray("images") val total = response.optInt("result_count", -1) val loadedSoFar = (requestedPage + 1) * perPage val hasNextPage = images != null && images.length() > 0 && (total < 0 || loadedSoFar < total) val assets = if (images == null) { emptyList() } else { (0 until images.length()).map { index -> toGettyImagesAsset(images.getJSONObject(index)) } } return FindAssetsResult( assets = assets, currentPage = requestedPage, nextPage = if (hasNextPage) requestedPage + 1 else -1, total = total, ) } fun toGettyImagesAsset(photo: JSONObject): Asset = Asset( id = photo.getString("id"), context = AssetContext(sourceId = GettyImagesAssetSource.SOURCE_ID), label = photo.optString("title").takeIf { it.isNotBlank() }, locale = "en", meta = toGettyImagesImageMeta(photo), ) fun toGettyImagesImageMeta(photo: JSONObject): Map { val sizes = getGettyImagesDisplaySizesByName(photo) val fullUri = sizes["comp"] ?: sizes["preview"] ?: sizes.values.firstOrNull().orEmpty() val thumbUri = sizes["thumb"] ?: sizes["preview"] ?: fullUri val maxDimensions = photo.optJSONObject("max_dimensions") return mapOf( "uri" to fullUri, "thumbUri" to thumbUri, "mimeType" to "image/jpeg", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to maxDimensions?.optInt("width")?.takeIf { it > 0 }?.toString().orEmpty(), "height" to maxDimensions?.optInt("height")?.takeIf { it > 0 }?.toString().orEmpty(), ) } fun getGettyImagesDisplaySizesByName(photo: JSONObject): Map { val displaySizes = photo.optJSONArray("display_sizes") ?: return emptyMap() return (0 until displaySizes.length()) .mapNotNull { index -> val size = displaySizes.optJSONObject(index) ?: return@mapNotNull null val name = size.optString("name").takeIf { it.isNotBlank() } ?: return@mapNotNull null val uri = size.optString("uri").takeIf { it.isNotBlank() } ?: return@mapNotNull null name to uri } .toMap() } ``` Integrate Getty Images stock photography into your Android app by routing search requests through a secure proxy and exposing the results as a CE.SDK asset source. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-getty-images) Getty Images requires authenticated server-side access. The Android app should call your proxy, and the proxy should hold the Getty Images API key and secret, forward search requests to Getty Images, and return Getty Images' raw `searchimages` JSON. This guide covers the Android client side: registering a custom `AssetSource`, parsing your proxy response into CE.SDK assets, applying a Getty Images result programmatically, and exposing the source in the CE.SDK editor asset library. ## Prerequisites Before integrating Getty Images, make sure you have: - Getty Images API credentials from the [Getty Images API Portal](https://developer.gettyimages.com/). - A server-side proxy that stores `GETTY_IMAGES_API_KEY` and `GETTY_IMAGES_API_SECRET` outside the Android app. - An Android-accessible proxy URL, for example from `local.properties`, `BuildConfig`, remote configuration, or your own secrets layer. - A Getty Images license agreement that covers how your users preview, license, and export content. Never ship the Getty Images API key or secret in the Android app. The app should only know the proxy URL. The example expects a thin pass-through proxy: it forwards the query to Getty Images and returns the Getty Images `searchimages` response unchanged. ## Understand the Proxy Requirement The request flow is: **Android app (CE.SDK)** -> **Your proxy server** -> **Getty Images API** The proxy validates app requests, authenticates with Getty Images, forwards CE.SDK's page request to Getty Images, and returns the Getty Images response unchanged. Because CE.SDK uses zero-based pages on Android and Getty Images uses one-based pages, the Android source adds one to `FindAssetsQuery.page` before calling the proxy. The example sends these query parameters to the proxy: | Parameter | Purpose | | --------- | ------- | | `phrase` | Search text from `FindAssetsQuery.query`. | | `page` | One-based Getty Images page number. | | `page_size` | Number of results requested for the page. | Your proxy should forward those values to Getty Images' `searchimages` endpoint and return a response with fields such as `result_count`, `images`, `display_sizes`, and `max_dimensions`. The Android client decodes that Getty Images shape and translates it into CE.SDK assets. ## Create the Asset Source On Android, a custom remote provider is an `AssetSource` subclass. The complete declaration below shows every member that `GettyImagesAssetSource` overrides: source metadata, optional groups, and the proxy-backed search callback. ```kotlin highlight-android-source-definition class GettyImagesAssetSource( private val proxyBaseUrl: String, private val proxyResponseProvider: (suspend (Uri) -> JSONObject)? = null, ) : AssetSource(sourceId = SOURCE_ID) { override val supportedMimeTypes = listOf("image/jpeg") override val credits = AssetCredits( name = "Getty Images", uri = Uri.parse("https://www.gettyimages.com/"), ) override val license = AssetLicense( name = "Getty Images Content License Agreement", uri = Uri.parse("https://www.gettyimages.com/eula"), ) override suspend fun getGroups(): List? = null override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val proxyResponse = loadGettyImagesResponse( uri = buildGettyImagesSearchUri(proxyBaseUrl = proxyBaseUrl, query = query), responseProvider = proxyResponseProvider, ) toFindAssetsResult(response = proxyResponse, requestedPage = query.page, perPage = query.perPage) } companion object { const val SOURCE_ID = "ly.img.asset.source.getty.images" } } ``` - `supportedMimeTypes`, `credits`, and `license` describe the source and apply to all of its results. - `getGroups()` returns `null` because this source does not expose additional group filters. - `findAssets(query)` moves the network request to `Dispatchers.IO`, then translates the Getty Images response into a `FindAssetsResult`. Per-asset `credits` can still name the photographer or contributor when your proxy includes that data. ## Query the Proxy The `findAssets(query)` override calls two focused helpers: one builds the search URI and the other loads the proxy response. Keep network work on `Dispatchers.IO` because the engine invokes asset source callbacks from the engine thread. ### Build the Search Request Build the proxy request from `FindAssetsQuery`. The sample sends `phrase`, `page`, and `page_size`; add any app-specific filters or license controls on the proxy side. ```kotlin highlight-android-build-search-uri fun buildGettyImagesSearchUri( proxyBaseUrl: String, query: FindAssetsQuery, ): Uri = Uri.parse(proxyBaseUrl).buildUpon() .appendQueryParameter("phrase", query.query.orEmpty()) .appendQueryParameter("page", (query.page + 1).toString()) .appendQueryParameter("page_size", query.perPage.toString()) .build() ``` The Android code sends `page + 1` because Getty Images expects one-based pages. Your proxy receives a one-based `page` parameter from this client and should forward it unchanged to Getty Images. ### Load the Proxy Response Load the JSON through your app's networking layer. The sample uses `HttpURLConnection` and also accepts a response provider so the same source can be exercised without live Getty Images credentials. ```kotlin highlight-android-load-proxy-response suspend fun loadGettyImagesResponse( uri: Uri, responseProvider: (suspend (Uri) -> JSONObject)? = null, ): JSONObject = responseProvider?.invoke(uri) ?: readGettyImagesJson(uri) fun readGettyImagesJson(uri: Uri): JSONObject { val connection = URL(uri.toString()).openConnection() as HttpURLConnection return try { require(connection.responseCode in 200 until 300) { connection.errorStream?.bufferedReader()?.use { it.readText() }.orEmpty() } JSONObject(connection.inputStream.bufferedReader().use { it.readText() }) } finally { connection.disconnect() } } ``` `readGettyImagesJson()` accepts successful HTTP status codes, parses the response as JSON, and always disconnects the connection. ## Parse Proxy Responses ### Build the Paginated Result Translate the response-level count and image array into a `FindAssetsResult`. `nextPage` advances until the loaded result count reaches Getty Images' reported total, or until Getty Images returns an empty page. ```kotlin highlight-android-response-pagination fun toFindAssetsResult( response: JSONObject, requestedPage: Int, perPage: Int, ): FindAssetsResult { val images = response.optJSONArray("images") val total = response.optInt("result_count", -1) val loadedSoFar = (requestedPage + 1) * perPage val hasNextPage = images != null && images.length() > 0 && (total < 0 || loadedSoFar < total) val assets = if (images == null) { emptyList() } else { (0 until images.length()).map { index -> toGettyImagesAsset(images.getJSONObject(index)) } } return FindAssetsResult( assets = assets, currentPage = requestedPage, nextPage = if (hasNextPage) requestedPage + 1 else -1, total = total, ) } ``` ### Map Photos to Assets Translate each Getty Images photo into CE.SDK's Android `Asset` model. The `meta` map carries the image URL and default block creation hints that `defaultApplyAsset()` uses when the asset is inserted. ```kotlin highlight-android-response-asset fun toGettyImagesAsset(photo: JSONObject): Asset = Asset( id = photo.getString("id"), context = AssetContext(sourceId = GettyImagesAssetSource.SOURCE_ID), label = photo.optString("title").takeIf { it.isNotBlank() }, locale = "en", meta = toGettyImagesImageMeta(photo), ) fun toGettyImagesImageMeta(photo: JSONObject): Map { val sizes = getGettyImagesDisplaySizesByName(photo) val fullUri = sizes["comp"] ?: sizes["preview"] ?: sizes.values.firstOrNull().orEmpty() val thumbUri = sizes["thumb"] ?: sizes["preview"] ?: fullUri val maxDimensions = photo.optJSONObject("max_dimensions") return mapOf( "uri" to fullUri, "thumbUri" to thumbUri, "mimeType" to "image/jpeg", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to maxDimensions?.optInt("width")?.takeIf { it > 0 }?.toString().orEmpty(), "height" to maxDimensions?.optInt("height")?.takeIf { it > 0 }?.toString().orEmpty(), ) } ``` The sample fills in these image defaults: - `mimeType` defaults to `image/jpeg`. - `blockType` defaults to `DesignBlockType.Graphic.key`. - `fillType` defaults to `FillType.Image.key`. - `shapeType` defaults to `ShapeType.Rect.key`. - `kind` defaults to `image`. - `context.sourceId` is set to the Getty Images source ID so CE.SDK can route apply calls back to the same source. ### Choose Getty Images Display Sizes Index Getty Images' named `display_sizes` once so the asset mapping can select the most appropriate URLs. ```kotlin highlight-android-response-sizes fun getGettyImagesDisplaySizesByName(photo: JSONObject): Map { val displaySizes = photo.optJSONArray("display_sizes") ?: return emptyMap() return (0 until displaySizes.length()) .mapNotNull { index -> val size = displaySizes.optJSONObject(index) ?: return@mapNotNull null val name = size.optString("name").takeIf { it.isNotBlank() } ?: return@mapNotNull null val uri = size.optString("uri").takeIf { it.isNotBlank() } ?: return@mapNotNull null name to uri } .toMap() } ``` The sample prefers `comp` for the imported asset, with `preview` or the first available size as a fallback. It prefers `thumb` for the asset library thumbnail. ## Create a Scene for Applying Assets `engine.asset.applyAssetSourceAsset()` inserts the returned asset into the active scene. The sample creates a minimal scene and page before querying Getty Images; in your app, you can use any active scene that already contains a page. ```kotlin highlight-android-scene-setup 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) ``` ## Query and Apply Images Programmatically Register the source on an existing `Engine`, query it with `engine.asset.findAssets()`, and apply a returned asset with `engine.asset.applyAssetSourceAsset()`. ```kotlin highlight-android-programmatic-register val source = GettyImagesAssetSource( proxyBaseUrl = proxyBaseUrl, proxyResponseProvider = proxyResponseProvider, ) if (source.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(source) } ``` ```kotlin highlight-android-query-and-apply val results = engine.asset.findAssets( sourceId = GettyImagesAssetSource.SOURCE_ID, query = FindAssetsQuery(query = "architecture", page = 0, perPage = 12), ) val firstBlock = results.assets.firstOrNull()?.let { asset -> engine.asset.applyAssetSourceAsset( sourceId = GettyImagesAssetSource.SOURCE_ID, asset = asset, ) } return GettyImagesImportResult(total = results.total, firstBlock = firstBlock) ``` `applyAssetSourceAsset()` returns the created block when CE.SDK can materialize the asset. Check for `null` before positioning or resizing the result. ## Add Getty Images to the Asset Library UI To expose Getty Images in the CE.SDK editor UI, register the same `AssetSource` after the editor has loaded and add a section for that source to the Images asset library. For a complete editor surface, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). ```kotlin highlight-android-editor-source-create val gettyImagesAssetSource = remember(proxyBaseUrl) { GettyImagesAssetSource(proxyBaseUrl = proxyBaseUrl) } ``` Register the source from `onLoaded` so the editor keeps its default scene creation behavior. ```kotlin highlight-android-editor-source-register onLoaded = { val engine = editorContext.engine if (gettyImagesAssetSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(gettyImagesAssetSource) } } ``` The asset library section points at the Getty Images source ID and expands into an image grid. The sample keeps the default asset library tabs and only appends the Getty Images section to `LibraryCategory.Images`. ```kotlin highlight-android-asset-library-ui assetLibrary = { remember { val gettyImagesSourceType = AssetSourceType(sourceId = gettyImagesAssetSource.sourceId) val gettyImagesSection = LibraryContent.Section( sourceTypes = listOf(gettyImagesSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = gettyImagesSourceType, assetType = AssetType.Image, title = "Getty Images", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(gettyImagesSection), ) } } ``` ## Handle Licensing and Attribution Getty Images content is premium content, not royalty-free stock media. Your integration should: - Use Getty Images credentials and licensing terms that match your product. - Keep credential handling and download/licensing actions on the proxy server. - Preserve provider attribution through source-level `credits` and `license`. - Preserve photographer or contributor attribution through per-asset `credits` when your proxy exposes it. - Review Getty Images usage restrictions before enabling export or commercial workflows. ## Troubleshooting - **Authentication errors:** Verify the API key and secret exist on the proxy server and are valid for the Getty Images API. - **Network or proxy failures:** Allow the Android app's requests at the proxy layer and verify the device can reach the proxy URL. - **Empty results:** Confirm the proxy returns Getty Images' `images` array and that each image includes usable `display_sizes`. - **Pagination repeats or skips results:** Check the zero-based CE.SDK page to one-based Getty Images page conversion. - **Images fail to insert:** Confirm the Getty Images response contains a `comp`, `preview`, or other `display_sizes` URL the device can load. ## API Reference | Method | Purpose | | ----------------------------------------------------- | -------------------------------------------------------------- | | `engine.asset.addSource(source=_)` | Register the Getty Images `AssetSource`. | | `engine.asset.findAllSources()` | Check whether the source is already registered. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query Getty Images results through the registered source. | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Insert a returned Getty Images asset into the current scene. | | `AssetSource.findAssets(query=_)` | Implement the proxy-backed search callback for the source. | | `AssetSource.getGroups()` | Return optional source group filters, or `null` when none exist. | | `engine.scene.create()` | Create the active scene used by the apply call. | | `engine.block.create(blockType=_)` | Create the page used by the sample scene. | | `engine.block.setWidth(block=_, value=_)` | Set the sample page width. | | `engine.block.setHeight(block=_, value=_)` | Set the sample page height. | | `engine.block.appendChild(parent=_, child=_)` | Attach the page to the scene. | | `EditorConfiguration.remember(builder=_)` | Configure the editor callbacks and asset library UI. | | `AssetLibrary.getDefault(images=_)` | Keep the default asset library and override the Images content. | | `LibraryCategory.Images.addSection(section=_)` | Add the Getty Images section to the Images asset library. | | `LibraryContent.Section(sourceTypes=_, assetType=_, expandContent=_)` | Define the Getty Images preview section. | | `LibraryContent.Grid(titleRes=_, sourceType=_, assetType=_, title=_)` | Expand the Getty Images section into an image grid. | | `AssetSourceType(sourceId=_)` | Connect the UI section to the Getty Images source ID. | ## Related Types | Type | Purpose | | ------------------------ | ---------------------------------------------------------- | | `AssetSource` | Base class for custom Android asset providers. | | `FindAssetsQuery` | Search text, paging, and filter data from CE.SDK. | | `FindAssetsResult` | Paginated asset response returned to CE.SDK. | | `Asset` | One Getty Images result in CE.SDK's Android asset model. | | `AssetLibrary` | Editor UI asset library configuration. | | `LibraryContent.Section` | One section inside an asset library category. | ## Next Steps - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Configure asset panels and UI - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Explore the core functionality of the asset library and how users browse, search, and insert media. - [Integrate Unsplash Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) - Add another stock image source - [From Pexels](https://img.ly/docs/cesdk/android/import-media/from-remote-source/pexels-90d5df/) — Connect CE.SDK to Pexels API to search, browse, and add royalty-free stock photos directly to designs. - [Import Media Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Learn core import concepts --- ## 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: "Integrate IMG.LY Premium Assets" description: "Host IMG.LY premium templates on your infrastructure and integrate them into CE.SDK's asset library for user access." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/imgly-premium-assets-eb1688/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [From IMG.LY Premium Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/imgly-premium-assets-eb1688/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-imgly-premium-assets/ImglyPremiumAssets.kt reference-only import android.net.Uri import androidx.annotation.StringRes import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.data.AssetSourceType import ly.img.engine.Asset import ly.img.engine.Engine import ly.img.engine.FindAssetsQuery private const val PREMIUM_ASSET_SOURCE_ID = "ly.img.templates.premium" fun imglyPremiumContentUri(premiumAssetsBaseUri: Uri): Uri = premiumAssetsBaseUri.buildUpon() .appendPath(PREMIUM_ASSET_SOURCE_ID) .appendPath("content.json") .build() fun createImglyPremiumAssetSource(engine: Engine) { if (PREMIUM_ASSET_SOURCE_ID in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = PREMIUM_ASSET_SOURCE_ID) } engine.asset.addLocalSource( sourceId = PREMIUM_ASSET_SOURCE_ID, supportedMimeTypes = emptyList(), applyAsset = { asset: Asset -> val archiveUri = asset.meta?.get("uri")?.let(Uri::parse) ?: error("Premium template ${asset.id} is missing meta.uri") engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) null }, ) } suspend fun addImglyPremiumAssetsToSource( engine: Engine, contentUri: Uri, matcher: List? = null, ) { val sourceId = engine.asset.addLocalSourceFromJSON( contentUri = contentUri, matcher = matcher, ) check(sourceId == PREMIUM_ASSET_SOURCE_ID) } suspend fun imglyPremiumAssets( engine: Engine, premiumAssetsBaseUri: Uri, matcher: List? = null, ): String { val contentUri = imglyPremiumContentUri(premiumAssetsBaseUri) createImglyPremiumAssetSource(engine = engine) addImglyPremiumAssetsToSource( engine = engine, contentUri = contentUri, matcher = matcher, ) return PREMIUM_ASSET_SOURCE_ID } fun imglyPremiumAddCategory( @StringRes titleRes: Int, ): LibraryCategory { val premiumSourceType = AssetSourceType(sourceId = PREMIUM_ASSET_SOURCE_ID) val premiumSection = LibraryContent.Section( titleRes = titleRes, sourceTypes = listOf(premiumSourceType), assetType = AssetType.Image, ) return LibraryCategory.Images.copy( tabTitleRes = titleRes, content = LibraryContent.Sections( titleRes = titleRes, sections = listOf(premiumSection), ), ) } suspend fun applyFirstImglyPremiumTemplate( engine: Engine, sourceId: String, ) { val templates = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val firstTemplate = templates.assets.firstOrNull() ?: error("No premium templates are available in $sourceId.") val createdBlock = engine.asset.applyAssetSourceAsset( sourceId = sourceId, asset = firstTemplate, ) check(createdBlock == null) } ``` Host IMG.LY premium templates on your infrastructure and register them as an 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-import-media-imgly-premium-assets) IMG.LY premium templates are delivered as a licensed asset package. Contact [IMG.LY sales](https://img.ly/forms/contact-sales) to obtain the package, then extract it and host its contents on your server or CDN. CE.SDK can query the hosted manifest, show template thumbnails, and load an individual template's `.zip` archive into the scene. This guide covers locating the hosted `content.json`, creating a local source with archive loading support, loading asset definitions from JSON, and applying a template through the Asset API. ## Prerequisites Before integrating premium assets, make sure you have: - The licensed IMG.LY premium asset package, available from [IMG.LY sales](https://img.ly/forms/contact-sales). - A server or CDN that hosts the extracted package contents over HTTPS. - An existing CE.SDK `Engine` instance. - Network access from your Android app to the hosted asset files. ## Premium Asset Package Structure The extracted package keeps `content.json` inside the `ly.img.templates.premium` folder. Individual template `.zip` archives and thumbnails are stored separately below `ly.img.templates.premium/templates//` and `ly.img.templates.premium/thumbnails//`. ```json { "version": "5.0.0", "id": "ly.img.templates.premium", "assets": [ { "id": "ly.img.templates.premium.e-commerce.e-commerce_retro_story_n99", "groups": ["e-commerce"], "label": { "en": "70s Retro Sale" }, "tags": { "en": ["70s", "retro", "fashion", "online-store"] }, "meta": { "uri": "{{base_url}}/ly.img.templates.premium/templates/e-commerce/e-commerce_retro_story_n99.zip", "thumbUri": "{{base_url}}/ly.img.templates.premium/thumbnails/e-commerce/e-commerce_retro_story_n99.jpg", "mimeType": "scene/zip" } } ] } ``` The `{{base_url}}` placeholder points to the parent URL that contains `ly.img.templates.premium`. ## Hosting Premium Assets Extract the licensed package, then upload its contents without flattening or renaming the `ly.img.templates.premium` tree. For example, if the parent asset root is `https://cdn.example.com/assets`, the manifest URL is `https://cdn.example.com/assets/ly.img.templates.premium/content.json`, template archives stay under `https://cdn.example.com/assets/ly.img.templates.premium/templates/...`, and thumbnails stay under `https://cdn.example.com/assets/ly.img.templates.premium/thumbnails/...`. Keep the parent asset root in your app configuration and derive the manifest URI from it. ## Configuring Asset Sources Register premium templates as a local asset source. Local sources are suited for finite collections because CE.SDK manages searching, grouping, and pagination after you add the asset definitions. ### Build the Manifest URI Build the `content.json` URI from the parent asset root. The Android JSON loader uses this URI to load the manifest and resolve `{{base_url}}` placeholders. ```kotlin highlight-android-content-uri private const val PREMIUM_ASSET_SOURCE_ID = "ly.img.templates.premium" fun imglyPremiumContentUri(premiumAssetsBaseUri: Uri): Uri = premiumAssetsBaseUri.buildUpon() .appendPath(PREMIUM_ASSET_SOURCE_ID) .appendPath("content.json") .build() ``` ### Create a Local Source Create the source with `engine.asset.addLocalSource()`. Premium templates are distributed as `.zip` archives, so the `applyAsset` callback reads `meta.uri`, calls `engine.scene.load()`, and returns `null` because applying a template replaces the scene instead of inserting a new design block. ```kotlin highlight-android-create-local-source fun createImglyPremiumAssetSource(engine: Engine) { if (PREMIUM_ASSET_SOURCE_ID in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = PREMIUM_ASSET_SOURCE_ID) } engine.asset.addLocalSource( sourceId = PREMIUM_ASSET_SOURCE_ID, supportedMimeTypes = emptyList(), applyAsset = { asset: Asset -> val archiveUri = asset.meta?.get("uri")?.let(Uri::parse) ?: error("Premium template ${asset.id} is missing meta.uri") engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) null }, ) } ``` Loading the archive replaces the active scene with the selected template and waits for resources to resolve before the callback returns. ### Process and Add Assets Load the manifest with `engine.asset.addLocalSourceFromJSON()`. The Android loader reads the asset definitions, applies `{{base_url}}` replacement, preserves fields such as `meta.mimeType`, and can filter IDs with `matcher`. ```kotlin highlight-android-process-assets suspend fun addImglyPremiumAssetsToSource( engine: Engine, contentUri: Uri, matcher: List? = null, ) { val sourceId = engine.asset.addLocalSourceFromJSON( contentUri = contentUri, matcher = matcher, ) check(sourceId == PREMIUM_ASSET_SOURCE_ID) } ``` ### Register the Source Combine the previous steps into a single setup function. The manifest `id` becomes the asset source ID used by queries, UI configuration, and template application. ```kotlin highlight-android-register-source suspend fun imglyPremiumAssets( engine: Engine, premiumAssetsBaseUri: Uri, matcher: List? = null, ): String { val contentUri = imglyPremiumContentUri(premiumAssetsBaseUri) createImglyPremiumAssetSource(engine = engine) addImglyPremiumAssetsToSource( engine = engine, contentUri = contentUri, matcher = matcher, ) return PREMIUM_ASSET_SOURCE_ID } ``` ## Displaying Templates in the Asset Library The source is queryable as soon as registration completes. To show its thumbnails, create a dedicated category containing a `LibraryContent.Section`, an `AssetSourceType` for the returned source ID, and `AssetType.Image` as the thumbnail presentation type. ```kotlin highlight-android-premium-library-category fun imglyPremiumAddCategory( @StringRes titleRes: Int, ): LibraryCategory { val premiumSourceType = AssetSourceType(sourceId = PREMIUM_ASSET_SOURCE_ID) val premiumSection = LibraryContent.Section( titleRes = titleRes, sourceTypes = listOf(premiumSourceType), assetType = AssetType.Image, ) return LibraryCategory.Images.copy( tabTitleRes = titleRes, content = LibraryContent.Sections( titleRes = titleRes, sections = listOf(premiumSection), ), ) } ``` Pass your app's “Premium Templates” string resource as `titleRes`. Expose this category only from a dedicated add action, such as a dock button that opens `SheetType.LibraryAdd`. Selecting a thumbnail then runs this guide's `applyAsset` callback and replaces the scene. Do not assign this source to an `AssetLibrary` category reused by `SheetType.LibraryReplace`. Replacement sheets call the source's block-targeted apply path, but this source intentionally defines `applyAsset` only and does not define `applyAssetToBlock`. See [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) for the Android category and dock configuration pattern. ## Testing the Integration Query the source to verify that the manifest loaded and assets are available. To test the full path, apply one asset, confirm that no insertable block is returned, and verify that the template archive loads into the scene. ```kotlin highlight-android-query-and-apply suspend fun applyFirstImglyPremiumTemplate( engine: Engine, sourceId: String, ) { val templates = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val firstTemplate = templates.assets.firstOrNull() ?: error("No premium templates are available in $sourceId.") val createdBlock = engine.asset.applyAssetSourceAsset( sourceId = sourceId, asset = firstTemplate, ) check(createdBlock == null) } ``` If your manifest contains groups, pass them through `FindAssetsQuery(groups=_)` to validate the same categories that your asset library UI displays. ## Optimization Use long-lived CDN cache headers for thumbnail `.jpg` files and template `.zip` archives. Keep `content.json` cache duration shorter if you expect to add or remove templates without app updates. For authenticated access, put signed or pre-signed `meta.uri` and `thumbUri` values in the manifest. Their lifetime must cover both browsing thumbnails and applying a template. The URI-based APIs shown here do not accept request headers; bearer-token or custom-header authentication requires app-owned networking and URI resolution plus an authenticated thumbnail image loader, which is outside this guide's flow. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Source query throws `ASSET.SOURCE_NOT_EXISTS` | No source is registered under the supplied ID | Use the `id` from `content.json` and finish registration before calling `findAssets(sourceId=_, query=_)` | | A registered source returns no templates | The manifest contains no assets or the query and `matcher` filter out every asset | Inspect the registered manifest and relax the query groups or `matcher` patterns | | Template fails to load | `meta.uri` does not point to a valid `.zip` archive | Verify that the archive path under `ly.img.templates.premium/templates/...` is reachable and load it with `engine.scene.load(sceneUri=_)` | | Thumbnails are missing | `thumbUri` still contains `{{base_url}}` or points to the wrong folder | Keep thumbnails under `ly.img.templates.premium/thumbnails/...` and load the source with `engine.asset.addLocalSourceFromJSON(contentUri=_)` | | UI does not show the source | The source was registered but not added to the editor's `AssetLibrary` configuration | Add the source ID through `AssetSourceType(sourceId=_)` in the relevant library section | | Hosted files fail on device | The device cannot reach the server or the signed URLs have expired | Verify HTTPS access, authentication, signed URL lifetime, and cache headers from a real device build | ## API Reference | Method | Description | | --- | --- | | `engine.asset.findAllSources()` | Check whether the premium source is already registered | | `engine.asset.removeSource(sourceId=_)` | Remove an existing source before recreating it | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_, applyAsset=_)` | Register a local source with custom archive-loading behavior | | `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a `.zip` scene archive from the selected template asset | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Load template definitions from the hosted premium asset manifest | | `engine.asset.findAssets(sourceId=_, query=_)` | Query registered premium template assets | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Apply a selected asset through the source's `applyAsset` callback | ### Related Types | Type | Purpose | | --- | --- | | `LibraryContent.Section` | Define the premium template thumbnail section. | | `AssetSourceType` | Connect the section to `ly.img.templates.premium`. | | `AssetType.Image` | Render the source assets with the verified image-thumbnail presentation. | | `SheetType.LibraryAdd` | Open the dedicated add-only category without exposing it to replacement sheets. | ## Next Steps - [From Unsplash](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) — Browse and import royalty-free images from Unsplash into the editor. - [Customize](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Adapt the asset library UI and behavior to your application's structure and user needs. - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Explore the core functionality of the asset library and how users browse, search, and insert media. - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) — Understand key asset concepts like sources, formats, metadata, and how assets are integrated into 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: "Integrate Pexels Stock Images" description: "Search and import royalty-free stock photos from Pexels in CE.SDK for Android." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/pexels-90d5df/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [From Pexels](https://img.ly/docs/cesdk/android/import-media/from-remote-source/pexels-90d5df/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-from-pexels/ImportFromPexels.kt reference-only import android.net.Uri import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetCredits import ly.img.engine.AssetLicense import ly.img.engine.AssetSource import ly.img.engine.AssetUTM 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.FindAssetsResult import ly.img.engine.MimeType import ly.img.engine.ShapeType import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL data class PexelsImportResult( val curatedTotal: Int, val searchTotal: Int, val firstBlock: DesignBlock?, ) suspend fun importFromPexels( engine: Engine, apiKey: String = "YOUR_PEXELS_API_KEY", client: PexelsClient = HttpPexelsClient(apiKey = apiKey), ): PexelsImportResult { val scene = engine.scene.create() 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) val source = PexelsAssetSource(client = client) if (source.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(source) } val curated = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val search = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(page = 0, perPage = 10, query = "workspace"), ) val firstBlock = (search.assets.firstOrNull() ?: curated.assets.firstOrNull())?.let { asset -> engine.asset.applyAssetSourceAsset( sourceId = source.sourceId, asset = asset, ) } return PexelsImportResult( curatedTotal = curated.total, searchTotal = search.total, firstBlock = firstBlock, ) } @Composable fun PexelsEditorSolution( license: String, apiKey: String, client: PexelsClient? = null, onClose: (Throwable?) -> Unit, ) { val pexelsClient = remember(apiKey, client) { client ?: HttpPexelsClient(apiKey = apiKey) } val pexelsAssetSource = remember(pexelsClient) { PexelsAssetSource(client = pexelsClient) } Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) { PexelsProviderLink() Box(modifier = Modifier.weight(1F)) { Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { val engine = editorContext.engine if (pexelsAssetSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(pexelsAssetSource) } } assetLibrary = { remember { val pexelsSourceType = AssetSourceType(sourceId = pexelsAssetSource.sourceId) val pexelsSection = LibraryContent.Section( sourceTypes = listOf(pexelsSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = pexelsSourceType, assetType = AssetType.Image, title = "Pexels", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(pexelsSection), ) } } } }, onClose = onClose, ) } } } const val PEXELS_PROVIDER_URI = "https://www.pexels.com/?utm_source=cesdk-android&utm_medium=referral" @Composable fun PexelsProviderLink(onOpenUri: ((Uri) -> Unit)? = null) { val uriHandler = LocalUriHandler.current val providerUri = remember { Uri.parse(PEXELS_PROVIDER_URI) } TextButton( onClick = { onOpenUri?.invoke(providerUri) ?: uriHandler.openUri(providerUri.toString()) }, ) { Text("Photos provided by Pexels") } } interface PexelsClient { suspend fun searchPhotos( query: String, page: Int, perPage: Int, ): PexelsResponse suspend fun curatedPhotos( page: Int, perPage: Int, ): PexelsResponse } const val PEXELS_CONNECT_TIMEOUT_MILLIS = 10_000 const val PEXELS_READ_TIMEOUT_MILLIS = 30_000 data class PexelsHttpRequest( val uri: Uri, val headers: Map, val connectTimeoutMillis: Int = PEXELS_CONNECT_TIMEOUT_MILLIS, val readTimeoutMillis: Int = PEXELS_READ_TIMEOUT_MILLIS, ) data class PexelsHttpResponse( val statusCode: Int, val body: String, ) fun interface PexelsTransport { suspend fun execute(request: PexelsHttpRequest): PexelsHttpResponse } fun configurePexelsHttpConnection( connection: HttpURLConnection, request: PexelsHttpRequest, ) { connection.connectTimeout = request.connectTimeoutMillis connection.readTimeout = request.readTimeoutMillis request.headers.forEach { (key, value) -> connection.setRequestProperty(key, value) } } object UrlConnectionPexelsTransport : PexelsTransport { override suspend fun execute(request: PexelsHttpRequest): PexelsHttpResponse = withContext(Dispatchers.IO) { val connection = URL(request.uri.toString()).openConnection() as HttpURLConnection try { configurePexelsHttpConnection(connection = connection, request = request) val statusCode = connection.responseCode val stream = if (statusCode in 200..299) connection.inputStream else connection.errorStream PexelsHttpResponse( statusCode = statusCode, body = stream?.bufferedReader()?.use { it.readText() }.orEmpty(), ) } finally { connection.disconnect() } } } class HttpPexelsClient( private val apiKey: String, private val transport: PexelsTransport = UrlConnectionPexelsTransport, ) : PexelsClient { override suspend fun searchPhotos( query: String, page: Int, perPage: Int, ): PexelsResponse = requestPexels( apiKey = apiKey, transport = transport, endpoint = "search", queryParameters = mapOf( "query" to query, "page" to page.toString(), "per_page" to perPage.toString(), ), ) override suspend fun curatedPhotos( page: Int, perPage: Int, ): PexelsResponse = requestPexels( apiKey = apiKey, transport = transport, endpoint = "curated", queryParameters = mapOf( "page" to page.toString(), "per_page" to perPage.toString(), ), ) } suspend fun requestPexels( apiKey: String, transport: PexelsTransport, endpoint: String, queryParameters: Map, ): PexelsResponse { val uriBuilder = Uri.parse("https://api.pexels.com/v1") .buildUpon() .appendPath(endpoint) queryParameters.forEach { (key, value) -> uriBuilder.appendQueryParameter(key, value) } val response = transport.execute( PexelsHttpRequest( uri = uriBuilder.build(), headers = mapOf("Authorization" to apiKey), ), ) require(response.statusCode == HttpURLConnection.HTTP_OK) { response.body.ifBlank { "Pexels request failed with HTTP ${response.statusCode}." } } return PexelsResponse.fromJson(JSONObject(response.body)) } data class PexelsResponse( val photos: List, val totalResults: Int?, val hasNextPage: Boolean, ) { companion object { fun fromJson(json: JSONObject): PexelsResponse { val photosJson = json.getJSONArray("photos") val photos = (0 until photosJson.length()).map { index -> PexelsPhoto.fromJson(photosJson.getJSONObject(index)) } return PexelsResponse( photos = photos, totalResults = if (json.has("total_results")) json.getInt("total_results") else null, hasNextPage = photos.isNotEmpty() && getNullableJsonString(json, "next_page")?.isNotBlank() == true, ) } } } data class PexelsPhoto( val id: Int, val width: Int, val height: Int, val url: String, val photographer: String, val photographerUrl: String?, val alt: String?, val src: PexelsPhotoSource, ) { companion object { fun fromJson(json: JSONObject): PexelsPhoto { val src = json.getJSONObject("src") return PexelsPhoto( id = json.getInt("id"), width = json.getInt("width"), height = json.getInt("height"), url = json.getString("url"), photographer = json.getString("photographer"), photographerUrl = getNullableJsonString(json, "photographer_url"), alt = getNullableJsonString(json, "alt"), src = PexelsPhotoSource( original = src.getString("original"), medium = src.getString("medium"), ), ) } } } data class PexelsPhotoSource( val original: String, val medium: String, ) fun getNullableJsonString( json: JSONObject, name: String, ): String? = if (json.has(name) && !json.isNull(name)) json.getString(name) else null class PexelsAssetSource( private val client: PexelsClient, ) : AssetSource(sourceId = SOURCE_ID) { override suspend fun getGroups(): List? = null override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) override val credits = AssetCredits( name = "Pexels", uri = Uri.parse("https://www.pexels.com/"), ) override val license = AssetLicense( name = "Pexels license (free)", uri = Uri.parse("https://www.pexels.com/license/"), ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { findPexelsAssets( client = client, sourceId = sourceId, query = query, ) } companion object { const val SOURCE_ID = "ly.img.asset.source.pexels" } } suspend fun findPexelsAssets( client: PexelsClient, sourceId: String, query: FindAssetsQuery, ): FindAssetsResult { val pexelsPage = query.page + 1 val searchQuery = query.query?.takeIf { it.isNotBlank() } val response = if (searchQuery == null) { client.curatedPhotos(page = pexelsPage, perPage = query.perPage) } else { client.searchPhotos( query = searchQuery, page = pexelsPage, perPage = query.perPage, ) } val assets = response.photos.map { photo -> toPexelsAsset(photo = photo, sourceId = sourceId) } return FindAssetsResult( assets = assets, currentPage = query.page, nextPage = if (response.hasNextPage) query.page + 1 else -1, total = response.totalResults ?: -1, ) } fun getPexelsMimeType(uri: String): String? { val extension = Uri.parse(uri).lastPathSegment?.substringAfterLast('.', missingDelimiterValue = "")?.lowercase() return when (extension) { "jpg", "jpeg" -> MimeType.JPEG.key "png" -> MimeType.PNG.key else -> null } } fun toPexelsAsset( photo: PexelsPhoto, sourceId: String, ) = Asset( id = photo.id.toString(), context = AssetContext(sourceId = sourceId), label = photo.alt?.takeIf { it.isNotBlank() }, locale = "en", meta = buildMap { put("uri", photo.src.original) put("thumbUri", photo.src.medium) getPexelsMimeType(photo.src.original)?.let { put("mimeType", it) } put("blockType", DesignBlockType.Graphic.key) put("fillType", FillType.Image.key) put("shapeType", ShapeType.Rect.key) put("kind", "image") put("width", photo.width.toString()) put("height", photo.height.toString()) }, credits = AssetCredits( name = photo.photographer, uri = Uri.parse(photo.url), ), utm = AssetUTM( source = "cesdk-android", medium = "referral", ), ) ``` Connect CE.SDK to Pexels so Android users can browse, search, and add royalty-free stock photos without leaving the editor. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-from-pexels) Pexels accepts an API key in the `Authorization` request header. In production, the Android app should call your backend proxy, and the proxy should hold the key, forward curated or search requests to Pexels, and return the Pexels JSON response. This guide covers the Android client side: registering a custom `AssetSource`, querying and translating Pexels responses, applying a result programmatically, exposing Pexels in the editor asset library, and preserving provider and photographer attribution. ## Prerequisites Before integrating Pexels, make sure you have: - A Pexels API key from the [Pexels API documentation](https://www.pexels.com/api/documentation/). - For production Android apps, a backend proxy or secrets layer appropriate for your deployment. - An integration that follows Pexels' [API guidelines](https://www.pexels.com/api/documentation/#guidelines), provider-link and photographer-credit guidance, and rate limits. - A plan for displaying Pexels provider and photographer attribution in your app-owned UI. The sample accepts an API key parameter and calls Pexels directly so you can inspect the complete request flow. > **Warning:** The direct request is for a runnable development sample only. Do not ship the Pexels API key in your Android application > binary. Route production requests through a backend proxy and add the `Authorization` header there. ## Protect the API Key in Production The recommended request flow for an Android production app is: **Android app (CE.SDK)** -> **Your backend proxy** -> **Pexels API** The production implementation of `PexelsClient` sends the search text, page, and page size to your proxy. The proxy validates the app request, authenticates to Pexels with the API key, calls either the curated or search endpoint, and returns the Pexels response. Because CE.SDK uses zero-based pages on Android and Pexels uses one-based pages, the source adds one to `FindAssetsQuery.page` before making the provider request. The sample uses these Pexels request values: | Value | Purpose | | ----- | ------- | | `query` | Search text from `FindAssetsQuery.query`; omitted for curated browsing. | | `page` | One-based Pexels page number. | | `per_page` | Number of results requested for the page. | | `Authorization` | Pexels API key. Add this header on the proxy in production. | Keep the Pexels response shape intact when possible. The Android client maps fields such as `photos`, `total_results`, `next_page`, image dimensions and URLs, the public photo page, and photographer information into CE.SDK assets. ## Create the Asset Source On Android, a custom remote provider is an `AssetSource` subclass. Give the Pexels source a stable ID, declare its supported image formats, and provide source-level credits and licensing metadata. ```kotlin highlight-android-source-metadata class PexelsAssetSource( private val client: PexelsClient, ) : AssetSource(sourceId = SOURCE_ID) { override suspend fun getGroups(): List? = null override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) override val credits = AssetCredits( name = "Pexels", uri = Uri.parse("https://www.pexels.com/"), ) override val license = AssetLicense( name = "Pexels license (free)", uri = Uri.parse("https://www.pexels.com/license/"), ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { findPexelsAssets( client = client, sourceId = sourceId, query = query, ) } companion object { const val SOURCE_ID = "ly.img.asset.source.pexels" } } ``` - `supportedMimeTypes`, `credits`, and `license` describe the source and apply to all results. - `getGroups()` returns `null` because this source does not expose another grouping layer. - `findAssets(query)` moves the complete request, decoding, mapping, and pagination path to `Dispatchers.IO`. Per-asset credits identify the photographer and link to the public Pexels photo page. ## Query Pexels The sample client separates endpoint selection from request execution. Keep request and response processing off the engine thread; `PexelsAssetSource.findAssets()` runs the complete path on `Dispatchers.IO`, while the included `HttpURLConnection` transport uses finite timeouts and always disconnects. ### Build the Pexels Request Use `/v1/curated` when the asset query has no search text and `/v1/search` for a non-empty query. Both requests carry the one-based page and page size. ```kotlin highlight-android-api-client-implementation class HttpPexelsClient( private val apiKey: String, private val transport: PexelsTransport = UrlConnectionPexelsTransport, ) : PexelsClient { override suspend fun searchPhotos( query: String, page: Int, perPage: Int, ): PexelsResponse = requestPexels( apiKey = apiKey, transport = transport, endpoint = "search", queryParameters = mapOf( "query" to query, "page" to page.toString(), "per_page" to perPage.toString(), ), ) override suspend fun curatedPhotos( page: Int, perPage: Int, ): PexelsResponse = requestPexels( apiKey = apiKey, transport = transport, endpoint = "curated", queryParameters = mapOf( "page" to page.toString(), "per_page" to perPage.toString(), ), ) } ``` In a production integration, inject a `PexelsClient` implementation that calls your proxy instead of `HttpPexelsClient`. The proxy-backed client sends only your app's request parameters; the proxy chooses or validates the upstream endpoint and adds the Pexels credential. ### Load the Pexels Response Build the URI, add the `Authorization` header, reject unsuccessful responses, and decode the response body. The sample accepts a transport dependency so tests can exercise the same client without live credentials. ```kotlin highlight-android-api-request suspend fun requestPexels( apiKey: String, transport: PexelsTransport, endpoint: String, queryParameters: Map, ): PexelsResponse { val uriBuilder = Uri.parse("https://api.pexels.com/v1") .buildUpon() .appendPath(endpoint) queryParameters.forEach { (key, value) -> uriBuilder.appendQueryParameter(key, value) } val response = transport.execute( PexelsHttpRequest( uri = uriBuilder.build(), headers = mapOf("Authorization" to apiKey), ), ) require(response.statusCode == HttpURLConnection.HTTP_OK) { response.body.ifBlank { "Pexels request failed with HTTP ${response.statusCode}." } } return PexelsResponse.fromJson(JSONObject(response.body)) } ``` The runnable sample sends the key from Android for transparency. In production, omit that provider credential from the app request and let your backend add it when calling Pexels. ## Parse Pexels Responses ### Decode the Response Decode the photo array together with Pexels' documented `total_results` and optional `next_page` fields. ```kotlin highlight-android-response-decoding fun fromJson(json: JSONObject): PexelsResponse { val photosJson = json.getJSONArray("photos") val photos = (0 until photosJson.length()).map { index -> PexelsPhoto.fromJson(photosJson.getJSONObject(index)) } return PexelsResponse( photos = photos, totalResults = if (json.has("total_results")) json.getInt("total_results") else null, hasNextPage = photos.isNotEmpty() && getNullableJsonString(json, "next_page")?.isNotBlank() == true, ) } ``` The sample keeps `totalResults` nullable as defensive handling for an unexpected response that omits the documented field. `hasNextPage` requires both a non-empty photo page and a usable `next_page` value, matching the iOS integration's defensive pagination behavior. ### Build the Paginated Result Translate CE.SDK's zero-based page to Pexels' one-based page, select curated or search behavior from the query text, and return the mapped page as a `FindAssetsResult`. ```kotlin highlight-android-find-assets suspend fun findPexelsAssets( client: PexelsClient, sourceId: String, query: FindAssetsQuery, ): FindAssetsResult { val pexelsPage = query.page + 1 val searchQuery = query.query?.takeIf { it.isNotBlank() } val response = if (searchQuery == null) { client.curatedPhotos(page = pexelsPage, perPage = query.perPage) } else { client.searchPhotos( query = searchQuery, page = pexelsPage, perPage = query.perPage, ) } val assets = response.photos.map { photo -> toPexelsAsset(photo = photo, sourceId = sourceId) } return FindAssetsResult( assets = assets, currentPage = query.page, nextPage = if (response.hasNextPage) query.page + 1 else -1, total = response.totalResults ?: -1, ) } ``` `nextPage` is `-1` when Pexels omits `next_page`. The response's `total_results` becomes `FindAssetsResult.total`; the sample uses `-1` only as a defensive fallback. ### Map Photos to Assets Derive MIME metadata from the full-resolution image URL and omit it when the extension is unknown: ```kotlin highlight-android-mime-type fun getPexelsMimeType(uri: String): String? { val extension = Uri.parse(uri).lastPathSegment?.substringAfterLast('.', missingDelimiterValue = "")?.lowercase() return when (extension) { "jpg", "jpeg" -> MimeType.JPEG.key "png" -> MimeType.PNG.key else -> null } } ``` Map each Pexels photo to CE.SDK's Android `Asset` model: ```kotlin highlight-android-translate fun toPexelsAsset( photo: PexelsPhoto, sourceId: String, ) = Asset( id = photo.id.toString(), context = AssetContext(sourceId = sourceId), label = photo.alt?.takeIf { it.isNotBlank() }, locale = "en", meta = buildMap { put("uri", photo.src.original) put("thumbUri", photo.src.medium) getPexelsMimeType(photo.src.original)?.let { put("mimeType", it) } put("blockType", DesignBlockType.Graphic.key) put("fillType", FillType.Image.key) put("shapeType", ShapeType.Rect.key) put("kind", "image") put("width", photo.width.toString()) put("height", photo.height.toString()) }, credits = AssetCredits( name = photo.photographer, uri = Uri.parse(photo.url), ), utm = AssetUTM( source = "cesdk-android", medium = "referral", ), ) ``` The mapping provides: - `uri` and `thumbUri` from Pexels' original and medium-size image URLs. - Image dimensions and the block, fill, shape, and kind hints used by `defaultApplyAsset()`. - Photographer credits linked to the public Pexels photo page. - `AssetContext.sourceId` so CE.SDK retains source provenance. - Referral metadata through `AssetUTM`. ## Create a Scene for Applying Assets `engine.asset.applyAssetSourceAsset()` inserts the returned asset into the active scene. The sample creates a square page before querying Pexels; in your app, you can use any active scene that already contains a page. ```kotlin highlight-android-scene-setup val scene = engine.scene.create() 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) ``` ## Query and Apply Images Programmatically Create and register the source on an existing `Engine`. Check the registered IDs first so repeated setup does not add the same source twice. ```kotlin highlight-android-programmatic-register val source = PexelsAssetSource(client = client) if (source.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(source) } ``` An empty query loads curated photos. A non-empty query searches Pexels: ```kotlin highlight-android-query-source val curated = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val search = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(page = 0, perPage = 10, query = "workspace"), ) ``` Pass the selected source ID explicitly when applying an asset: ```kotlin highlight-android-apply-asset val firstBlock = (search.assets.firstOrNull() ?: curated.assets.firstOrNull())?.let { asset -> engine.asset.applyAssetSourceAsset( sourceId = source.sourceId, asset = asset, ) } ``` `applyAssetSourceAsset()` returns the created block when CE.SDK can materialize the asset. Check for `null` before positioning or resizing the result. ## Add Pexels to the Asset Library UI To expose Pexels in the CE.SDK editor UI, keep one source instance across recompositions, register it after the editor has loaded, and add a section for that source to the Images asset library. For a complete editor surface, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). Create the source outside the configuration builder so recomposition does not replace it: ```kotlin highlight-android-editor-source-create val pexelsAssetSource = remember(pexelsClient) { PexelsAssetSource(client = pexelsClient) } ``` Register the source from `onLoaded`. The duplicate check keeps repeated setup safe: ```kotlin highlight-android-editor-source-register onLoaded = { val engine = editorContext.engine if (pexelsAssetSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(pexelsAssetSource) } } ``` The asset library section points at the same source ID and expands into a searchable image grid. Starting with `AssetLibrary.getDefault()` preserves the other default library categories: ```kotlin highlight-android-asset-library-ui assetLibrary = { remember { val pexelsSourceType = AssetSourceType(sourceId = pexelsAssetSource.sourceId) val pexelsSection = LibraryContent.Section( sourceTypes = listOf(pexelsSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = pexelsSourceType, assetType = AssetType.Image, title = "Pexels", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(pexelsSection), ) } } ``` An empty query shows curated photos, while entering text in the grid switches the same source to Pexels search. ## Handle Licensing and Attribution The [Pexels license](https://www.pexels.com/license/) does not require attribution, but the Pexels API guidelines require a prominent Pexels link and ask integrations to credit photographers when possible. Preserve both levels of attribution: - Source-level `credits` and `license` identify Pexels and link to its terms. - Per-asset `credits` identify the photographer and link to the public photo page. - `AssetUTM` adds the recommended referral parameters to outgoing credit links. CE.SDK can surface asset credit metadata in asset details. Keep the provider link in app-owned UI so it stays visible without a custom dock or editor configuration builder: ```kotlin highlight-android-provider-link const val PEXELS_PROVIDER_URI = "https://www.pexels.com/?utm_source=cesdk-android&utm_medium=referral" @Composable fun PexelsProviderLink(onOpenUri: ((Uri) -> Unit)? = null) { val uriHandler = LocalUriHandler.current val providerUri = remember { Uri.parse(PEXELS_PROVIDER_URI) } TextButton( onClick = { onOpenUri?.invoke(providerUri) ?: uriHandler.openUri(providerUri.toString()) }, ) { Text("Photos provided by Pexels") } } ``` Review Pexels' current API guidelines and rate limits before shipping. ## Troubleshooting - **Authentication errors:** Confirm the proxy adds the Pexels API key in the `Authorization` header. For the direct sample, confirm the configured development key is present. - **Rate limiting:** Handle HTTP 429 on the proxy, cache results where permitted, and back off before retrying. - **Empty results:** Confirm an empty CE.SDK query calls `/curated`, a non-empty query calls `/search`, and the response includes a `photos` array. - **Repeated or skipped pages:** Confirm CE.SDK's zero-based page is converted to Pexels' one-based page exactly once. - **Images fail to insert:** Verify `src.original` and `src.medium` are reachable from the device and the mapped asset contains the image block metadata shown above. - **Missing provider link:** Keep the app-owned provider action visible whenever Pexels API content is available. - **Missing photographer credit:** Confirm the Pexels response contains `photographer` and `url` and that both are mapped to the asset's `credits`. - **Missing default library content:** Start with `AssetLibrary.getDefault()` and add the Pexels section to its Images category. ## API Reference | Method | Purpose | | ------ | ------- | | `engine.asset.addSource(source=_)` | Register the Pexels `AssetSource`. | | `engine.asset.findAllSources()` | Check whether the source is already registered. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query curated or searched Pexels results. | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Insert a returned Pexels asset through its source. | | `AssetSource.findAssets(query=_)` | Implement Pexels-backed browsing and search. | | `AssetSource.getGroups()` | Return optional source groups, or `null` when none exist. | | `engine.scene.create()` | Create the active scene used by the programmatic apply call. | | `engine.block.create(blockType=_)` | Create the page used by the sample scene. | | `engine.block.setWidth(block=_, value=_)` | Set the sample page width. | | `engine.block.setHeight(block=_, value=_)` | Set the sample page height. | | `engine.block.appendChild(parent=_, child=_)` | Attach the page to the scene. | | `EditorConfiguration.remember(builder=_)` | Configure source registration and asset library UI. | | `AssetLibrary.getDefault(images=_)` | Keep the default asset library and customize Images. | | `LibraryCategory.Images.addSection(section=_)` | Append the Pexels preview to the Images category. | | `LibraryContent.Section(sourceTypes=_, assetType=_, expandContent=_)` | Define the compact Pexels preview. | | `LibraryContent.Grid(titleRes=_, sourceType=_, assetType=_, title=_)` | Define the paginated Pexels image grid. | | `AssetSourceType(sourceId=_)` | Connect library content to the Pexels source. | ## Related Types | Type | Purpose | | ---- | ------- | | `AssetSource` | Base class for custom Android asset providers. | | `FindAssetsQuery` | Search text, paging, and filter data from CE.SDK. | | `FindAssetsResult` | Paginated asset response returned to CE.SDK. | | `Asset` | One mapped Pexels photo in CE.SDK's Android asset model. | | `AssetContext` | Source provenance associated with an asset. | | `AssetCredits` | Provider-level or photographer-level credit metadata. | | `AssetLibrary` | Editor UI asset library configuration. | | `LibraryCategory` | A category such as the default Images library. | | `LibraryContent.Section` | A preview section inside an asset library category. | ## Next Steps - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Configure asset panels and UI - [Asset Library Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) - Understand asset sources - [Integrate Unsplash Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) - Add another stock image source - [From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) - Explore other remote asset integrations - [Import Media Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Learn core import concepts --- ## 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 Remote Assets" description: "Load asset definitions from remote JSON files into CE.SDK's Android asset system." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/remote-asset-484685/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [Import Remote Asset](https://img.ly/docs/cesdk/android/import-media/from-remote-source/remote-asset-484685/) --- ```kotlin file=@cesdk_android_examples/engine-guides-remote-asset/RemoteAsset.kt reference-only package ly.img.editor.showcase import android.net.Uri 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.MimeType data class RemoteAsset( val remoteSourceId: String, val stringSourceId: String, val hostedSourceId: String, val remoteAssetLoaded: Boolean, val stringAssetCount: Int, val appliedBlock: DesignBlock?, val appliedBlockType: String?, val registeredSources: List, val hostedSourceRemoved: Boolean, val invalidJsonRejected: Boolean, ) suspend fun remoteAsset( engine: Engine, assetBaseUri: Uri, ): RemoteAsset { listOf("my.remote.images.uri", "my.remote.images", "my.remote.images.hosted").forEach { sourceId -> if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } } 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 remoteAssetsBaseUri = assetBaseUri val imageManifestUri = remoteAssetsBaseUri.buildUpon() .appendPath("my.remote.images.uri") .appendPath("content.json") .build() val sampleImageUri = remoteAssetsBaseUri.buildUpon() .appendPath("imgly-assets") .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_1.jpg") .build() val imageSourceId = engine.asset.addLocalSourceFromJSON( contentUri = imageManifestUri, matcher = listOf("sample_1"), ) val manifestJSON = """ { "version": "2.0.0", "id": "my.remote.images", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "$sampleImageUri", "thumbUri": "$sampleImageUri", "kind": "image", "fillType": "${FillType.Image.key}", "mimeType": "${MimeType.JPEG.key}", "width": 2500, "height": 1667 } } ] } """.trimIndent() val stringSourceId = engine.asset.addLocalSourceFromJSON( contentJSON = manifestJSON, basePath = null, matcher = null, ) val hostedManifestJSON = """ { "version": "2.0.0", "id": "my.remote.images.hosted", "assets": [ { "id": "hosted_sample_image", "label": { "en": "Hosted Sample Image" }, "meta": { "uri": "{{base_url}}/imgly-assets/ly.img.image/images/sample_1.jpg", "thumbUri": "{{base_url}}/imgly-assets/ly.img.image/images/sample_1.jpg", "kind": "image", "fillType": "${FillType.Image.key}", "mimeType": "${MimeType.JPEG.key}", "width": 2500, "height": 1667 } } ] } """.trimIndent() val hostedSourceId = engine.asset.addLocalSourceFromJSON( contentJSON = hostedManifestJSON, basePath = remoteAssetsBaseUri.toString(), matcher = null, ) val remoteAssets = engine.asset.findAssets( sourceId = imageSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val remoteAsset = engine.asset.fetchAsset( sourceId = imageSourceId, assetId = "sample_1", ) val stringAssets = engine.asset.findAssets( sourceId = stringSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) check(remoteAssets.total > 0) val hostedAssets = engine.asset.findAssets( sourceId = hostedSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val appliedBlock = hostedAssets.assets.firstOrNull()?.let { asset -> engine.asset.defaultApplyAsset(asset = asset) } val registeredSources = engine.asset.findAllSources() engine.asset.removeSource(sourceId = hostedSourceId) val invalidJsonRejected = try { engine.asset.addLocalSourceFromJSON( contentJSON = "{ not valid json }", basePath = null, matcher = null, ) false } catch (_: Exception) { true } return RemoteAsset( remoteSourceId = imageSourceId, stringSourceId = stringSourceId, hostedSourceId = hostedSourceId, remoteAssetLoaded = remoteAsset != null, stringAssetCount = stringAssets.assets.size, appliedBlock = appliedBlock, appliedBlockType = appliedBlock?.let(engine.block::getType), registeredSources = registeredSources, hostedSourceRemoved = hostedSourceId !in engine.asset.findAllSources(), invalidJsonRejected = invalidJsonRejected, ) } ``` Load asset definitions from remote JSON files hosted on a CDN or server into CE.SDK's asset library. ![Remote image asset loaded into CE.SDK on Android](./assets/android.remote-asset.jpg) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-remote-asset) Remote asset loading lets you host asset definitions outside your Android app and register them at runtime. This keeps asset management separate from your app release cycle, while the asset metadata still flows through the standard CE.SDK asset APIs. This guide covers how to load remote asset manifests, load JSON content that you already have in memory, resolve `{{base_url}}` placeholders, query the loaded assets, and apply them to a scene. ## Setup Create a scene and a page before applying image assets. The remote asset APIs only register asset data; applying an image later needs an active scene. ```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) ``` ## JSON Manifest Structure A manifest declares a `version`, an asset source `id`, and an `assets` array. Each asset has an `id`, localized labels, and a `meta` object with the URIs and apply hints that CE.SDK needs when the asset is applied. ```json { "version": "2.0.0", "id": "my.remote.images.uri", "assets": [ { "id": "sample_1", "label": { "en": "Sample Image" }, "meta": { "uri": "{{base_url}}/my.remote.images.uri/images/sample.jpg", "thumbUri": "{{base_url}}/my.remote.images.uri/thumbnails/sample.jpg", "kind": "image", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/jpeg", "width": 2500, "height": 1667 } } ] } ``` The manifest `id` becomes the source ID for later calls. `uri` points to the media file, `thumbUri` points to a preview image, and the media metadata tells CE.SDK how to apply the asset. When you load a manifest from `//content.json`, `{{base_url}}` resolves to the asset root, `/`. Include the source folder in placeholder-based media paths, such as `{{base_url}}/my.remote.images.uri/images/sample.jpg`. ## Loading Assets From a Remote URL Build the manifest and media URIs from the root folder that hosts your asset sources. In production, provide your own HTTPS server or CDN root, for example `Uri.parse("https://cdn.example.com/cesdk-assets")`. ```kotlin highlight-android-remote-uris val remoteAssetsBaseUri = assetBaseUri val imageManifestUri = remoteAssetsBaseUri.buildUpon() .appendPath("my.remote.images.uri") .appendPath("content.json") .build() val sampleImageUri = remoteAssetsBaseUri.buildUpon() .appendPath("imgly-assets") .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_1.jpg") .build() ``` Pass the manifest URI to `engine.asset.addLocalSourceFromJSON()`. The method fetches the JSON, creates a local asset source, and returns the manifest's source ID. Use source IDs your app owns, and remove or reload those sources intentionally when their manifest changes. Android also accepts an optional `matcher` list so you can load only asset IDs that match specific strings or wildcard patterns. ```kotlin highlight-android-load-from-uri val imageSourceId = engine.asset.addLocalSourceFromJSON( contentUri = imageManifestUri, matcher = listOf("sample_1"), ) ``` When the URI-loaded manifest uses `{{base_url}}`, CE.SDK resolves it against the asset root that contains the source folders. For a manifest at `https://cdn.example.com/cesdk-assets/my.remote.images.uri/content.json`, a media path such as `{{base_url}}/my.remote.images.uri/images/sample_1.jpg` resolves to `https://cdn.example.com/cesdk-assets/my.remote.images.uri/images/sample_1.jpg`. ## Loading Assets From a JSON String Use the string overload when your app already has the manifest content, for example from an API response or embedded configuration. If the manifest contains absolute URLs, you can leave `basePath` as `null`. ```kotlin highlight-android-load-from-string val manifestJSON = """ { "version": "2.0.0", "id": "my.remote.images", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "$sampleImageUri", "thumbUri": "$sampleImageUri", "kind": "image", "fillType": "${FillType.Image.key}", "mimeType": "${MimeType.JPEG.key}", "width": 2500, "height": 1667 } } ] } """.trimIndent() val stringSourceId = engine.asset.addLocalSourceFromJSON( contentJSON = manifestJSON, basePath = null, matcher = null, ) ``` ## Customizing the Base Path When a string manifest contains `{{base_url}}` placeholders, pass `basePath` to define the hosting location for relative asset files. ```kotlin highlight-android-base-path val hostedManifestJSON = """ { "version": "2.0.0", "id": "my.remote.images.hosted", "assets": [ { "id": "hosted_sample_image", "label": { "en": "Hosted Sample Image" }, "meta": { "uri": "{{base_url}}/imgly-assets/ly.img.image/images/sample_1.jpg", "thumbUri": "{{base_url}}/imgly-assets/ly.img.image/images/sample_1.jpg", "kind": "image", "fillType": "${FillType.Image.key}", "mimeType": "${MimeType.JPEG.key}", "width": 2500, "height": 1667 } } ] } """.trimIndent() val hostedSourceId = engine.asset.addLocalSourceFromJSON( contentJSON = hostedManifestJSON, basePath = remoteAssetsBaseUri.toString(), matcher = null, ) ``` Use this pattern when your backend returns a manifest separately from the CDN or server path that hosts the referenced media. ## Verifying Loaded Assets Query a source with `engine.asset.findAssets()` to inspect loaded assets, and fetch a known asset ID to confirm the manifest entry is available. Android uses zero-based pages, so the first page is `page = 0`. ```kotlin highlight-android-verify-assets val remoteAssets = engine.asset.findAssets( sourceId = imageSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val remoteAsset = engine.asset.fetchAsset( sourceId = imageSourceId, assetId = "sample_1", ) val stringAssets = engine.asset.findAssets( sourceId = stringSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) ``` ## Applying Remote Assets Use `engine.asset.defaultApplyAsset()` to turn a queried asset into a block in the active scene. CE.SDK reads the asset's `meta` fields and downloads the referenced media when the block needs it. ```kotlin highlight-android-apply-asset val hostedAssets = engine.asset.findAssets( sourceId = hostedSourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val appliedBlock = hostedAssets.assets.firstOrNull()?.let { asset -> engine.asset.defaultApplyAsset(asset = asset) } ``` If you need custom apply behavior, use a custom `AssetSource` instead. JSON-backed local sources are a good fit when the manifest metadata already describes how the asset should become a block. ## Listing Asset Sources Call `engine.asset.findAllSources()` to inspect the currently registered source IDs. ```kotlin highlight-android-list-sources val registeredSources = engine.asset.findAllSources() ``` ## Removing Asset Sources Remove a source when your app no longer needs its assets or before reloading the same manifest ID with updated content. ```kotlin highlight-android-remove-source engine.asset.removeSource(sourceId = hostedSourceId) ``` ## Error Handling Wrap manifest loading in `try`/`catch` so your app can handle network errors, missing files, or invalid JSON. ```kotlin highlight-android-error-handling val invalidJsonRejected = try { engine.asset.addLocalSourceFromJSON( contentJSON = "{ not valid json }", basePath = null, matcher = null, ) false } catch (_: Exception) { true } ``` Common issues include: - **Network failures**: Check connectivity and whether the manifest URL is reachable from the device. - **Invalid JSON**: Confirm the manifest contains a `version`, an `id`, and an `assets` array. - **Base path issues**: Make sure `{{base_url}}` resolves to reachable media and thumbnail URLs. - **Source ID conflicts**: Use source IDs your app owns, and avoid removing built-in sources such as `ly.img.image`. ## API Reference | Method | Description | | --- | --- | | `engine.scene.create()` | Create the scene that receives applied assets. | | `engine.block.create(blockType=DesignBlockType.Page)` | Create a page for the scene. | | `engine.block.setWidth(block=_, value=_)` | Set the page width. | | `engine.block.setHeight(block=_, value=_)` | Set the page height. | | `engine.block.appendChild(parent=_, child=_)` | Add the page to the scene. | | `Uri.buildUpon()` | Build manifest and media URIs from a remote asset base URI. | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Load an asset source from a JSON file URI, optionally filtering asset IDs. | | `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_, matcher=_)` | Load an asset source from JSON content, optionally resolving `{{base_url}}` with `basePath`. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query assets from a loaded source. | | `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Fetch a specific loaded asset by ID. | | `engine.asset.defaultApplyAsset(asset=_)` | Apply an asset to the active scene using its metadata. | | `engine.asset.findAllSources()` | List registered asset source IDs. | | `engine.asset.removeSource(sourceId=_)` | Remove an asset source and its loaded assets. | ## Next Steps - [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) - How asset sources and assets fit together. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) - Host asset files and manifests on your own server or CDN. - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) - See a complete custom `AssetSource` implementation backed by a remote 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: "Integrate Third-Party APIs" description: "Import media from any third-party service into CE.SDK using custom asset sources." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/third-party-a05854/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [From Third-Party](https://img.ly/docs/cesdk/android/import-media/from-remote-source/third-party-a05854/) --- Connect third-party media APIs to CE.SDK through custom asset sources and make their assets available inside your Android app. ## Custom Asset Sources CE.SDK's asset source system lets you connect external media libraries to the editor UI and Engine API. A custom asset source owns how assets are fetched, searched, paginated, previewed, and applied, so it can work with REST APIs, proxy servers, or your own backend. This page is a hub for third-party integrations. The implementation details live in the service-specific guides. ## Available Integration Examples Complete Android integrations are available for Unsplash, Pexels, and Getty Images: - [Unsplash](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) — Browse and import royalty-free photos, with search, pagination, metadata, credits, and license information handled through a custom asset source. - [Pexels](https://img.ly/docs/cesdk/android/import-media/from-remote-source/pexels-90d5df/) — Search, browse, and add royalty-free stock photos directly to designs. - [Getty Images](https://img.ly/docs/cesdk/android/import-media/from-remote-source/getty-images-a3931c/) — Search and import premium stock photography through a secure proxy server. The same custom asset source pattern can support other stock-photo libraries, royalty-free audio or video services, and private media backends. Each integration mainly differs in the API endpoint it calls and how the response maps to CE.SDK assets. ## Common Integration Patterns Most third-party integrations use the same building blocks: - **Search** — Let users query the provider's media catalog. - **Pagination** — Load large result sets page by page. - **Asset preview** — Provide thumbnails, labels, and metadata before import. - **Authentication** — Keep API keys on a trusted backend or proxy server when the provider requires credentials. ## Next Steps - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) — Understand how asset sources organize content and connect to the rest of the asset system. - [From Pexels](https://img.ly/docs/cesdk/android/import-media/from-remote-source/pexels-90d5df/) — Connect CE.SDK to Pexels API to search, browse, and add royalty-free stock photos directly to designs. - [Integrate Getty Images Stock Photos](https://img.ly/docs/cesdk/android/import-media/from-remote-source/getty-images-a3931c/) — Search and import premium stock photography from Getty Images directly into CE.SDK using a secure proxy server. - [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) — Serve multiple resolutions of an asset for performance and quality. --- ## 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: "Integrate Unsplash Stock Images" description: "Search and import high-quality stock images from Unsplash directly into CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [From Unsplash](https://img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0/) --- ```kotlin file=@cesdk_android_examples/engine-guides-custom-asset-source/UnsplashAssetSource.kt reference-only import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.core.R import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType 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.AssetUTM 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.FindAssetsResult import ly.img.engine.MimeType import ly.img.engine.ShapeType import org.json.JSONArray import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder data class CustomAssetSourceSummary( val sourceRegistered: Boolean, val sourceCreditsName: String?, val sourceLicenseName: String?, val localAssetCount: Int, val localAssetId: String?, val queriedRemoteSource: Boolean, val appliedRemoteAsset: Boolean, ) suspend fun customAssetSource( engine: Engine, unsplashBaseUrl: String, utmSource: String, unsplashAssetSource: UnsplashAssetSource? = null, ): CustomAssetSourceSummary { runCatching { engine.asset.removeSource(UnsplashAssetSource.SOURCE_ID) } runCatching { engine.asset.removeSource(LOCAL_SOURCE_ID) } val source = unsplashAssetSource ?: UnsplashAssetSource( engine = engine, baseUrl = unsplashBaseUrl, utmSource = utmSource, ) engine.asset.addSource(source) val queriedRemoteSource = unsplashBaseUrl.isNotBlank() var appliedRemoteAsset = false if (unsplashBaseUrl.isNotBlank()) { val popularResults = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 0, perPage = 10), ) val searchResults = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "landscape", page = 0, perPage = 10), ) val assetToApply = searchResults.assets.firstOrNull() ?: popularResults.assets.firstOrNull() if (assetToApply != null) { appliedRemoteAsset = engine.asset.applyAssetSourceAsset( sourceId = source.sourceId, asset = assetToApply, ) != null } } engine.asset.addLocalSource( sourceId = LOCAL_SOURCE_ID, supportedMimeTypes = listOf(MimeType.JPEG.key), ) val localAsset = AssetDefinition( id = "sample-landscape", label = mapOf("en" to "Sample Landscape"), tags = mapOf("en" to listOf("landscape", "sample")), 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, "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to "1080", "height" to "1080", ), ) engine.asset.addAsset(sourceId = LOCAL_SOURCE_ID, asset = localAsset) engine.asset.assetSourceContentsChanged(sourceId = LOCAL_SOURCE_ID) val localResults = engine.asset.findAssets( sourceId = LOCAL_SOURCE_ID, query = FindAssetsQuery(query = "landscape", page = 0, perPage = 10), ) return CustomAssetSourceSummary( sourceRegistered = source.sourceId in engine.asset.findAllSources(), sourceCreditsName = engine.asset.getCredits(source.sourceId)?.name, sourceLicenseName = engine.asset.getLicense(source.sourceId)?.name, localAssetCount = localResults.assets.size, localAssetId = localResults.assets.firstOrNull()?.id, queriedRemoteSource = queriedRemoteSource, appliedRemoteAsset = appliedRemoteAsset, ) } open class UnsplashAssetSource( private val engine: Engine, private val baseUrl: String, private val utmSource: String, ) : AssetSource(sourceId = "ly.img.asset.source.unsplash") { companion object { const val SOURCE_ID = "ly.img.asset.source.unsplash" } override val supportedMimeTypes = listOf(MimeType.JPEG.key) override suspend fun getGroups(): List? = null override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrBlank()) { query.getPopularList() } else { query.getSearchList() } } private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val requestUrl = "$baseUrl/photos?order_by=popular&page=${page + 1}&per_page=$perPage" val assetsArray = getResponseAsString(requestUrl).let(::JSONArray) val assets = mutableListOf() for (index in 0 until assetsArray.length()) { assets += assetsArray.getJSONObject(index).toAsset() } return FindAssetsResult( assets = assets, currentPage = page, nextPage = if (assets.isEmpty()) -1 else page + 1, total = Int.MAX_VALUE, ) } private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val encodedQuery = URLEncoder.encode(query.orEmpty(), Charsets.UTF_8.name()) val requestUrl = "$baseUrl/search/photos?query=$encodedQuery&page=${page + 1}&per_page=$perPage" val response = getResponseAsString(requestUrl).let(::JSONObject) val assetsArray = response.getJSONArray("results") val assets = mutableListOf() for (index in 0 until assetsArray.length()) { assets += assetsArray.getJSONObject(index).toAsset() } val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = assets, currentPage = page, nextPage = if (page + 1 >= totalPages) -1 else page + 1, total = total, ) } override suspend fun applyAsset(asset: Asset): DesignBlock? { trackDownloadEvent(asset) return engine.asset.defaultApplyAsset(asset) } override suspend fun applyAsset( asset: Asset, block: DesignBlock, ) { trackDownloadEvent(asset) engine.asset.defaultApplyAsset(asset, block) } private suspend fun trackDownloadEvent(asset: Asset) { val downloadLocation = asset.meta ?.get("downloadLocation") ?.takeIf { it.isNotBlank() } ?: return getResponseAsString(proxiedApiUrl(downloadLocation)) } private suspend fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = listOf( optString("description"), optString("alt_description"), ).firstOrNull { it.isNotBlank() }, tags = optJSONArray("tags") ?.let { array -> (0 until array.length()).mapNotNull { index -> array.optJSONObject(index)?.optString("title")?.takeIf { it.isNotBlank() } } } ?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("regular"), "thumbUri" to getJSONObject("urls").getString("thumb"), "downloadLocation" to optJSONObject("links") ?.optString("download_location") .orEmpty(), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = sourceId), credits = AssetCredits( name = optJSONObject("user")?.optString("name")?.takeIf { it.isNotBlank() } ?: "Unknown photographer", uri = optJSONObject("user") ?.optJSONObject("links") ?.optString("html") ?.takeIf { it.isNotBlank() } ?.let(Uri::parse), ), utm = AssetUTM(source = utmSource, medium = "referral"), ) protected open suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } } private fun proxiedApiUrl(url: String): String { val parsedUrl = Uri.parse(url) if (parsedUrl.scheme == null) { val separator = if (url.startsWith("/")) "" else "/" return "$baseUrl$separator$url" } require(parsedUrl.host == "api.unsplash.com") { "Expected an Unsplash API URL but received ${parsedUrl.host}" } val path = parsedUrl.encodedPath.orEmpty() val query = parsedUrl.encodedQuery?.let { "?$it" }.orEmpty() return "$baseUrl$path$query" } } private const val LOCAL_SOURCE_ID = "sample-local-images" fun unsplashAssetLibrary(): AssetLibrary { val unsplashSourceType = AssetSourceType(sourceId = UnsplashAssetSource.SOURCE_ID) val unsplashSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(unsplashSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = unsplashSourceType, assetType = AssetType.Image, ), ) return AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(unsplashSection), ) } ``` Browse Unsplash's library of royalty-free photos from inside the editor by registering a custom asset source. The engine calls your `findAssets` implementation as users search and scroll, so results stream in from the Unsplash API on demand. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-custom-asset-source) CE.SDK lets you plug external image providers — like Unsplash or your own backend — into the engine as custom asset sources. This guide builds an Unsplash source, maps its REST responses to the engine's asset format, handles attribution, surfaces the source in the asset library, and shows the engine-managed local-source alternative. ## Prerequisites - An Unsplash API access key from the [Unsplash Developer portal](https://unsplash.com/developers). - A proxy that forwards requests to the Unsplash API. The sample expects a proxy base URL such as `https://your-api.example.com/unsplash`; do not embed an Unsplash access key in the Android app. - A UTM source value that identifies your app in links back to Unsplash. Follow the [Unsplash API Guidelines](https://help.unsplash.com/en/articles/2511245-unsplash-api-guidelines) for current attribution, hotlinking, and download-tracking requirements. - A running CE.SDK `Engine` with an active scene when you apply returned assets to the canvas. ## Setting Up the Unsplash API Client The Android sample wraps Unsplash REST calls in an `AssetSource` class. The class receives the current `Engine` for apply-time behavior, stores your proxy base URL and app-specific UTM source, declares JPEG support, and leaves groups empty because the guide queries search and popular images directly. ```kotlin highlight-android-source-definition open class UnsplashAssetSource( private val engine: Engine, private val baseUrl: String, private val utmSource: String, ) : AssetSource(sourceId = "ly.img.asset.source.unsplash") { companion object { const val SOURCE_ID = "ly.img.asset.source.unsplash" } override val supportedMimeTypes = listOf(MimeType.JPEG.key) override suspend fun getGroups(): List? = null ``` ## Creating the Unsplash Asset Source Definition Register the source with `engine.asset.addSource(...)`. Every Asset API call then refers to the source by its unique `sourceId`. ```kotlin highlight-android-register-source engine.asset.addSource(source) ``` ## Implementing Search and Discovery The `findAssets` callback is the core of the integration. CE.SDK passes the current search string and pagination values; the source chooses the Unsplash endpoint and returns a `FindAssetsResult`. ```kotlin highlight-android-find-assets override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrBlank()) { query.getPopularList() } else { query.getSearchList() } } ``` The query carries: - `query.query` - the current search term from the asset library search field. - `query.page` - the zero-based page index requested by CE.SDK. The sample adds `1` when calling Unsplash because Unsplash pages start at `1`. - `query.perPage` - the number of assets to return for this request. ### Search with Query When users enter a search term, call Unsplash's `/search/photos` endpoint with the encoded query string and page controls. ```kotlin highlight-android-search-query private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val encodedQuery = URLEncoder.encode(query.orEmpty(), Charsets.UTF_8.name()) val requestUrl = "$baseUrl/search/photos?query=$encodedQuery&page=${page + 1}&per_page=$perPage" val response = getResponseAsString(requestUrl).let(::JSONObject) val assetsArray = response.getJSONArray("results") val assets = mutableListOf() for (index in 0 until assetsArray.length()) { assets += assetsArray.getJSONObject(index).toAsset() } val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = assets, currentPage = page, nextPage = if (page + 1 >= totalPages) -1 else page + 1, total = total, ) } ``` Map the response to `FindAssetsResult`. Use `-1` for `nextPage` when there are no more pages so CE.SDK stops requesting more results. ### Popular Images When no search term is present, call the `/photos` endpoint to return popular images. The endpoint does not return a stable total count, so the sample loads sequential pages until Unsplash returns an empty page. ```kotlin highlight-android-popular-list private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val requestUrl = "$baseUrl/photos?order_by=popular&page=${page + 1}&per_page=$perPage" val assetsArray = getResponseAsString(requestUrl).let(::JSONArray) val assets = mutableListOf() for (index in 0 until assetsArray.length()) { assets += assetsArray.getJSONObject(index).toAsset() } return FindAssetsResult( assets = assets, currentPage = page, nextPage = if (assets.isEmpty()) -1 else page + 1, total = Int.MAX_VALUE, ) } ``` ## Translating Unsplash Data to CE.SDK Format Each Unsplash photo becomes an `Asset`. The `id` is mandatory and must be unique within the source; the metadata tells CE.SDK which block, fill, and shape to create when the asset is applied. ```kotlin highlight-android-translate-asset private suspend fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = listOf( optString("description"), optString("alt_description"), ).firstOrNull { it.isNotBlank() }, tags = optJSONArray("tags") ?.let { array -> (0 until array.length()).mapNotNull { index -> array.optJSONObject(index)?.optString("title")?.takeIf { it.isNotBlank() } } } ?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("regular"), "thumbUri" to getJSONObject("urls").getString("thumb"), "downloadLocation" to optJSONObject("links") ?.optString("download_location") .orEmpty(), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = sourceId), credits = AssetCredits( name = optJSONObject("user")?.optString("name")?.takeIf { it.isNotBlank() } ?: "Unknown photographer", uri = optJSONObject("user") ?.optJSONObject("links") ?.optString("html") ?.takeIf { it.isNotBlank() } ?.let(Uri::parse), ), utm = AssetUTM(source = utmSource, medium = "referral"), ) ``` The important fields are: - `id`, `locale`, `label`, and `tags` identify and describe the asset in the asset library. - `meta["uri"]` is the image URL CE.SDK should display or apply, while `meta["thumbUri"]` is the preview image. - `meta["downloadLocation"]` preserves the Unsplash event endpoint for apply-time tracking. - `meta["blockType"]`, `meta["fillType"]`, and `meta["shapeType"]` use typed Android constants so CE.SDK can create an image graphic without inferring the type from the file. - `context` records the source ID for the returned asset. - `credits` and `utm` carry the photographer attribution and link-tracking metadata. Set the UTM source to the app name you registered with Unsplash. ## Handling Attribution Requirements Unsplash requires photographer attribution. Set per-asset credits from the photo's `user` object, and set source-level credits and license information once on the source. ```kotlin highlight-android-credits-license override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), ) ``` ## Adding Download Tracking Unsplash's API guidelines ask you to call a photo's download endpoint when the image is used. Search and list results should not trigger that endpoint. The sample keeps `links.download_location` in `meta["downloadLocation"]` and calls it only from the custom apply path. ```kotlin highlight-android-download-tracking private suspend fun trackDownloadEvent(asset: Asset) { val downloadLocation = asset.meta ?.get("downloadLocation") ?.takeIf { it.isNotBlank() } ?: return getResponseAsString(proxiedApiUrl(downloadLocation)) } ``` Override `applyAsset(...)` so asset-library selection and programmatic `applyAssetSourceAsset(...)` calls both track the chosen image before CE.SDK applies it to the scene. ```kotlin highlight-android-apply-and-track override suspend fun applyAsset(asset: Asset): DesignBlock? { trackDownloadEvent(asset) return engine.asset.defaultApplyAsset(asset) } override suspend fun applyAsset( asset: Asset, block: DesignBlock, ) { trackDownloadEvent(asset) engine.asset.defaultApplyAsset(asset, block) } ``` ## Requesting Through Your Proxy Search, list, and tracking calls all go through the same small networking helpers. `proxiedApiUrl(...)` keeps relative paths on your proxy, rewrites Unsplash API URLs back through that proxy, and rejects unexpected hosts so the Android app never needs the access key. ```kotlin highlight-android-networking-helpers protected open suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } } private fun proxiedApiUrl(url: String): String { val parsedUrl = Uri.parse(url) if (parsedUrl.scheme == null) { val separator = if (url.startsWith("/")) "" else "/" return "$baseUrl$separator$url" } require(parsedUrl.host == "api.unsplash.com") { "Expected an Unsplash API URL but received ${parsedUrl.host}" } val path = parsedUrl.encodedPath.orEmpty() val query = parsedUrl.encodedQuery?.let { "?$it" }.orEmpty() return "$baseUrl$path$query" } ``` ## Configuring the Asset Library UI Once the source is registered, add an image section that points at `UnsplashAssetSource.SOURCE_ID`. The section also defines the grid that opens when users expand it. Use the returned `AssetLibrary` from your editor configuration's `assetLibrary` callback to show the Unsplash section in the Images tab. ```kotlin highlight-android-asset-library-ui fun unsplashAssetLibrary(): AssetLibrary { val unsplashSourceType = AssetSourceType(sourceId = UnsplashAssetSource.SOURCE_ID) val unsplashSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(unsplashSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = unsplashSourceType, assetType = AssetType.Image, ), ) return AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(unsplashSection), ) } ``` The [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) guide covers broader Android editor UI patterns, including custom tabs and replacing default sections. ## Testing the Integration After your proxy URL is configured and the editor has an active scene, query the source and apply one returned asset to the scene with `engine.asset.applyAssetSourceAsset(...)`. ```kotlin highlight-android-query-and-apply if (unsplashBaseUrl.isNotBlank()) { val popularResults = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 0, perPage = 10), ) val searchResults = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "landscape", page = 0, perPage = 10), ) val assetToApply = searchResults.assets.firstOrNull() ?: popularResults.assets.firstOrNull() if (assetToApply != null) { appliedRemoteAsset = engine.asset.applyAssetSourceAsset( sourceId = source.sourceId, asset = assetToApply, ) != null } } ``` ## Troubleshooting **Rate limiting** - Unsplash enforces hourly request limits. Cache results and avoid re-querying on every keystroke to stay within them. **Authentication failures** - If requests fail with an authentication error, verify your proxy forwards the Unsplash access key and supports the `/photos`, `/search/photos`, and download-tracking paths. **Missing attribution** - Confirm the per-asset `credits` and the source-level `credits` and `license` are populated; the editor uses them to display attribution. **Images not loading** - Check that the mapped `uri` resolves over HTTPS and that the apply-time tracking request succeeds through your proxy. ## Local Asset Sources When you already have a finite set of assets, you can skip implementing a query callback and let the engine manage search and pagination. Create a local source with `addLocalSource(...)` and a unique ID you reference later. ```kotlin highlight-android-add-local-source engine.asset.addLocalSource( sourceId = LOCAL_SOURCE_ID, supportedMimeTypes = listOf(MimeType.JPEG.key), ) ``` Add assets with `addAsset(...)`. The engine stores them and returns matching items from queries, in insertion order. Call `assetSourceContentsChanged(...)` when the source contents change after registration. ```kotlin highlight-android-add-asset-to-source val localAsset = AssetDefinition( id = "sample-landscape", label = mapOf("en" to "Sample Landscape"), tags = mapOf("en" to listOf("landscape", "sample")), 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, "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to "1080", "height" to "1080", ), ) engine.asset.addAsset(sourceId = LOCAL_SOURCE_ID, asset = localAsset) engine.asset.assetSourceContentsChanged(sourceId = LOCAL_SOURCE_ID) ``` ## API Reference | Method | Description | | --- | --- | | `AssetSource(sourceId=_)` | Create a custom source with a stable source ID. | | `AssetSource.findAssets(query=_)` | Return a page of assets for the current query. | | `AssetSource.applyAsset(asset=_)` | Run source-specific apply logic before creating a block. | | `AssetSource.applyAsset(asset=_, block=_)` | Run source-specific apply logic when replacing an existing block. | | `AssetSource.getGroups()` | Return source groups, or `null` when the source has no group filters. | | `engine.asset.addSource(source=_)` | Register a custom source such as Unsplash. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query a registered source for a page of results. | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Add a queried asset to the active scene as a design block. | | `engine.asset.defaultApplyAsset(asset=_)` | Apply an asset with CE.SDK's standard block creation behavior. | | `engine.asset.defaultApplyAsset(asset=_, block=_)` | Apply an asset to an existing block with CE.SDK's standard behavior. | | `AssetSourceType(sourceId=_)` | Reference a registered asset source from Android asset-library UI configuration. | | `LibraryContent.Section(titleRes=_, sourceTypes=_, assetType=_, expandContent=_)` | Create an asset-library section for one or more source types. | | `LibraryContent.Grid(titleRes=_, sourceType=_, assetType=_)` | Configure the expanded grid that displays assets from one source. | | `LibraryCategory.Images.addSection(section=_)` | Add a custom section to the Images category. | | `AssetLibrary.getDefault(images=_)` | Build the default asset library while replacing the Images category configuration. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create an engine-managed local asset source. | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add an asset definition to a local source. | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify CE.SDK that a source's results changed. | ## Next Steps - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Configure asset panels and surface custom sources in the editor UI. - [Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Explore the core functionality of the asset library and how users browse, search, and insert media. - [From IMG.LY Premium Assets](https://img.ly/docs/cesdk/android/import-media/from-remote-source/imgly-premium-assets-eb1688/) — Access a curated set of premium IMG.LY media assets for use in designs. - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) — Learn the core asset and import 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: "From Your Server" description: "Load images, videos, audio, stickers, and templates from your backend servers into CE.SDK for Android." platform: android url: "https://img.ly/docs/cesdk/android/import-media/from-remote-source/your-server-b91910/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) > [From Your Server](https://img.ly/docs/cesdk/android/import-media/from-remote-source/your-server-b91910/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-media-from-remote-source-your-server/YourServer.kt reference-only import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.core.R import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.addSection import ly.img.editor.core.library.data.AssetSourceType import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetSource import ly.img.engine.DesignBlock 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 data class ServerSourceIds( val images: String = "my-server-images", val videos: String = "my-server-videos", val audio: String = "my-server-audio", val stickers: String = "my-server-stickers", val templates: String = "my-server-templates", val brandStickers: String = "my-server-brand-stickers", ) { val all: List get() = listOf(images, videos, audio, stickers, templates, brandStickers) } data class YourServerResult( val sourceIds: ServerSourceIds, val imageResults: FindAssetsResult, val secondImagePage: FindAssetsResult, val taggedImageResults: FindAssetsResult, val videoResults: FindAssetsResult, val audioResults: FindAssetsResult, val stickerResults: FindAssetsResult, val templateResults: FindAssetsResult, val secondTemplatePage: FindAssetsResult, val fetchedTemplateAsset: Asset, val appliedTemplateBlock: DesignBlock?, val staticJsonSourceId: String, val staticJsonResults: FindAssetsResult, val assetLibraryTabs: Int, ) data class ServerAssetPage( val assets: List, val currentPage: Int, val nextPage: Int, val total: Int, ) interface ServerAssetBackend { suspend fun searchAssets( sourceId: String, query: FindAssetsQuery, ): ServerAssetPage suspend fun getAsset( sourceId: String, assetId: String, ): ServerAsset? suspend fun getGroups(sourceId: String): List? } suspend fun yourServer( engine: Engine, assetBaseUri: Uri, ): YourServerResult { val sourceIds = ServerSourceIds() // Replace this factory with your app's API client. val backend: ServerAssetBackend = createAppServerBackend(sourceIds, assetBaseUri) registerServerAssetSources( engine = engine, sourceIds = sourceIds, backend = backend, ) val staticJsonSourceId = loadStaticJsonAssetSource(engine) val imageResults = engine.asset.findAssets( sourceId = sourceIds.images, query = FindAssetsQuery(perPage = 1, page = 0), ) val secondImagePage = engine.asset.findAssets( sourceId = sourceIds.images, query = FindAssetsQuery(perPage = 1, page = 1), ) val taggedImageResults = engine.asset.findAssets( sourceId = sourceIds.images, query = FindAssetsQuery(perPage = 10, page = 0, tags = listOf("sunset")), ) val videoResults = engine.asset.findAssets( sourceId = sourceIds.videos, query = FindAssetsQuery(perPage = 10, page = 0, query = "surf"), ) val audioResults = engine.asset.findAssets( sourceId = sourceIds.audio, query = FindAssetsQuery(perPage = 10, page = 0), ) val stickerResults = engine.asset.findAssets( sourceId = sourceIds.stickers, query = FindAssetsQuery(perPage = 10, page = 0), ) val templateResults = engine.asset.findAssets( sourceId = sourceIds.templates, query = FindAssetsQuery(perPage = 1, page = 0), ) val secondTemplatePage = engine.asset.findAssets( sourceId = sourceIds.templates, query = FindAssetsQuery(perPage = 1, page = 1), ) val fetchedTemplateAsset = engine.asset.fetchAsset( sourceId = sourceIds.templates, assetId = "template-001", ) ?: error("Template asset not found.") val appliedTemplateBlock = engine.asset.applyAssetSourceAsset( sourceId = sourceIds.templates, asset = fetchedTemplateAsset, ) val staticJsonResults = engine.asset.findAssets( sourceId = staticJsonSourceId, query = FindAssetsQuery(perPage = 10, page = 0), ) val assetLibrary = createServerAssetLibrary(sourceIds) return YourServerResult( sourceIds = sourceIds, imageResults = imageResults, secondImagePage = secondImagePage, taggedImageResults = taggedImageResults, videoResults = videoResults, audioResults = audioResults, stickerResults = stickerResults, templateResults = templateResults, secondTemplatePage = secondTemplatePage, fetchedTemplateAsset = fetchedTemplateAsset, appliedTemplateBlock = appliedTemplateBlock, staticJsonSourceId = staticJsonSourceId, staticJsonResults = staticJsonResults, assetLibraryTabs = assetLibrary.tabs().size, ) } private fun createAppServerBackend( sourceIds: ServerSourceIds, assetBaseUri: Uri, ): ServerAssetBackend { // Replace this demo backend with your app's authenticated API client. return DemoServerBackend(sourceIds, assetBaseUri) } fun registerServerAssetSources( engine: Engine, sourceIds: ServerSourceIds, backend: ServerAssetBackend, ) { sourceIds.all .filter(engine.asset.findAllSources()::contains) .forEach(engine.asset::removeSource) val mediaSources = listOf( ServerMediaAssetSource( sourceId = sourceIds.images, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key), backend = backend, ), ServerMediaAssetSource( sourceId = sourceIds.videos, supportedMimeTypes = listOf(MimeType.MP4.key), backend = backend, ), ServerMediaAssetSource( sourceId = sourceIds.audio, supportedMimeTypes = listOf("audio/mpeg", "audio/x-m4a"), backend = backend, ), ServerMediaAssetSource( sourceId = sourceIds.stickers, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key, MimeType.SVG.key), backend = backend, ), ) mediaSources.forEach(engine.asset::addSource) engine.asset.addSource( ServerTemplateAssetSource( engine = engine, sourceId = sourceIds.templates, backend = backend, ), ) } @Composable fun rememberYourServerEditorConfiguration(assetBaseUri: Uri): EditorConfiguration { val sourceIds = remember { ServerSourceIds() } val backend = remember(sourceIds, assetBaseUri) { createAppServerBackend(sourceIds, assetBaseUri) } return EditorConfiguration.remember { onLoaded = { registerServerAssetSources( engine = editorContext.engine, sourceIds = sourceIds, backend = backend, ) loadStaticJsonAssetSource(editorContext.engine) } assetLibrary = { createServerAssetLibrary(sourceIds) } } } suspend fun loadStaticJsonAssetSource(engine: Engine): String = engine.asset.addLocalSourceFromJSON( contentJSON = STATIC_BRAND_STICKERS_JSON, basePath = "https://img.ly/static/ubq_samples", ) private class ServerMediaAssetSource( sourceId: String, override val supportedMimeTypes: List, private val backend: ServerAssetBackend, ) : AssetSource(sourceId = sourceId) { override suspend fun getGroups(): List? = withContext(Dispatchers.IO) { backend.getGroups(sourceId) } override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val page = backend.searchAssets(sourceId = sourceId, query = query) FindAssetsResult( assets = page.assets.map { it.toAsset(sourceId = sourceId) }, currentPage = page.currentPage, nextPage = page.nextPage, total = page.total, ) } override suspend fun fetchAsset( id: String, options: FetchAssetOptions, ): Asset? = withContext(Dispatchers.IO) { backend.getAsset(sourceId = sourceId, assetId = id)?.toAsset(sourceId = sourceId) } } private class ServerTemplateAssetSource( private val engine: Engine, sourceId: String, private val backend: ServerAssetBackend, ) : AssetSource(sourceId = sourceId) { override val supportedMimeTypes: List = listOf("application/ubq-template-string") override suspend fun getGroups(): List? = withContext(Dispatchers.IO) { backend.getGroups(sourceId) } override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val page = backend.searchAssets(sourceId = sourceId, query = query) FindAssetsResult( assets = page.assets.map { it.toAsset(sourceId = sourceId) }, currentPage = page.currentPage, nextPage = page.nextPage, total = page.total, ) } override suspend fun fetchAsset( id: String, options: FetchAssetOptions, ): Asset? = withContext(Dispatchers.IO) { backend.getAsset(sourceId = sourceId, assetId = id)?.toAsset(sourceId = sourceId) } override suspend fun applyAsset(asset: Asset): DesignBlock? { val templateUri = asset.meta?.get("uri")?.let(Uri::parse) ?: return null engine.scene.load(sceneUri = templateUri) return null } } fun createServerAssetLibrary(sourceIds: ServerSourceIds): AssetLibrary { val serverImageSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(AssetSourceType(sourceId = sourceIds.images)), assetType = AssetType.Image, ) val serverVideoSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_videos, sourceTypes = listOf(AssetSourceType(sourceId = sourceIds.videos)), assetType = AssetType.Video, ) val serverAudioSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_audio, sourceTypes = listOf(AssetSourceType(sourceId = sourceIds.audio)), assetType = AssetType.Audio, ) val serverStickerSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_stickers, sourceTypes = listOf( AssetSourceType(sourceId = sourceIds.stickers), AssetSourceType(sourceId = sourceIds.brandStickers), ), assetType = AssetType.Sticker, ) return AssetLibrary.getDefault( includeAVResources = true, images = LibraryCategory.Images.addSection(serverImageSection), videos = LibraryCategory.Video.addSection(serverVideoSection), audios = LibraryCategory.Audio.addSection(serverAudioSection), stickers = LibraryCategory.Stickers.addSection(serverStickerSection), ) } data class ServerAsset( val id: String, val label: String, val uri: String, val thumbUri: String, val mimeType: String, val blockType: DesignBlockType, val fillType: FillType? = null, val shapeType: ShapeType? = null, val kind: String? = null, val width: Int? = null, val height: Int? = null, val duration: Double? = null, val tags: List = emptyList(), val groups: List = emptyList(), ) { fun toAsset(sourceId: String) = Asset( id = id, context = AssetContext(sourceId = sourceId), label = label, locale = "en", tags = tags, groups = groups, meta = buildMap { put("uri", uri) put("thumbUri", thumbUri) put("mimeType", mimeType) put("blockType", blockType.key) fillType?.let { put("fillType", it.key) } shapeType?.let { put("shapeType", it.key) } kind?.let { put("kind", it) } width?.let { put("width", it.toString()) } height?.let { put("height", it.toString()) } duration?.let { put("duration", it.toString()) } }, ) } private fun createServerAssets( sourceIds: ServerSourceIds, assetBaseUri: Uri, ): Map> = mapOf( sourceIds.images to imageServerAssets(), sourceIds.videos to videoServerAssets(), sourceIds.audio to audioServerAssets(assetBaseUri), sourceIds.stickers to stickerServerAssets(), sourceIds.templates to templateServerAssets(assetBaseUri), ) private fun imageServerAssets(): List = listOf( ServerAsset( id = "img-001", label = "Mountain Landscape", uri = "https://img.ly/static/ubq_samples/sample_1.jpg", thumbUri = "https://img.ly/static/ubq_samples/sample_1.jpg", mimeType = MimeType.JPEG.key, blockType = DesignBlockType.Graphic, fillType = FillType.Image, shapeType = ShapeType.Rect, kind = "image", width = 1920, height = 1280, tags = listOf("mountain", "landscape"), groups = listOf("photos"), ), ServerAsset( id = "img-002", label = "Ocean Sunset", uri = "https://img.ly/static/ubq_samples/sample_2.jpg", thumbUri = "https://img.ly/static/ubq_samples/sample_2.jpg", mimeType = MimeType.JPEG.key, blockType = DesignBlockType.Graphic, fillType = FillType.Image, shapeType = ShapeType.Rect, kind = "image", width = 1920, height = 1280, tags = listOf("ocean", "sunset"), groups = listOf("photos"), ), ) private fun videoServerAssets(): List = listOf( ServerAsset( id = "vid-001", label = "Surf Promo", uri = "https://img.ly/static/ubq_video_samples/bbb.mp4", thumbUri = "https://img.ly/static/ubq_samples/sample_1.jpg", mimeType = MimeType.MP4.key, blockType = DesignBlockType.Graphic, fillType = FillType.Video, shapeType = ShapeType.Rect, kind = "video", width = 640, height = 360, duration = 634.694, tags = listOf("surf", "promo"), groups = listOf("videos"), ), ) private fun audioServerAssets(assetBaseUri: Uri): List = listOf( ServerAsset( id = "audio-001", label = "Background Track", uri = assetBaseUri .buildUpon() .appendPath("ly.img.audio") .appendPath("audios") .appendPath("far_from_home.m4a") .build() .toString(), thumbUri = "https://img.ly/static/ubq_samples/sample_2.jpg", mimeType = "audio/x-m4a", blockType = DesignBlockType.Audio, duration = 98.716, tags = listOf("music", "background"), groups = listOf("audio"), ), ) private fun stickerServerAssets(): List = listOf( ServerAsset( id = "sticker-001", label = "Camera Badge", uri = "https://img.ly/static/ubq_samples/imgly_logo.jpg", thumbUri = "https://img.ly/static/ubq_samples/imgly_logo.jpg", mimeType = MimeType.JPEG.key, blockType = DesignBlockType.Graphic, fillType = FillType.Image, shapeType = ShapeType.Rect, kind = "sticker", width = 1980, height = 720, tags = listOf("camera", "badge"), groups = listOf("stickers"), ), ) private fun templateServerAssets(assetBaseUri: Uri): List = listOf( ServerAsset( id = "template-001", label = "Launch Story", uri = assetBaseUri .buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_postcard_1.scene") .build() .toString(), thumbUri = assetBaseUri .buildUpon() .appendPath("ly.img.templates") .appendPath("thumbnails") .appendPath("cesdk_postcard_1.jpg") .build() .toString(), mimeType = "application/ubq-template-string", blockType = DesignBlockType.Scene, kind = "scene", tags = listOf("launch", "story"), groups = listOf("templates"), ), ServerAsset( id = "template-002", label = "Product Promo", uri = assetBaseUri .buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_postcard_2.scene") .build() .toString(), thumbUri = assetBaseUri .buildUpon() .appendPath("ly.img.templates") .appendPath("thumbnails") .appendPath("cesdk_postcard_2.jpg") .build() .toString(), mimeType = "application/ubq-template-string", blockType = DesignBlockType.Scene, kind = "scene", tags = listOf("product", "promo"), groups = listOf("templates"), ), ) private class DemoServerBackend( sourceIds: ServerSourceIds, assetBaseUri: Uri, ) : ServerAssetBackend { private val assetsBySource = createServerAssets(sourceIds, assetBaseUri) override suspend fun searchAssets( sourceId: String, query: FindAssetsQuery, ): ServerAssetPage { val searchWords = query.query ?.split(" ") ?.map(String::trim) ?.filter(String::isNotEmpty) .orEmpty() val requestedTags = query.tags.orEmpty() val requestedGroups = query.groups.orEmpty() val matchingAssets = assetsBySource.getValue(sourceId) .filter { asset -> val matchesQuery = searchWords.isEmpty() || searchWords.all { word -> asset.label.contains(word, ignoreCase = true) || asset.tags.any { it.contains(word, ignoreCase = true) } } val matchesGroups = requestedGroups.isEmpty() || asset.groups.any(requestedGroups::contains) val matchesTags = requestedTags.isEmpty() || requestedTags.all(asset.tags::contains) matchesQuery && matchesGroups && matchesTags } val startIndex = query.page * query.perPage val pageAssets = matchingAssets.drop(startIndex).take(query.perPage) val nextPage = if (startIndex + pageAssets.size < matchingAssets.size) query.page + 1 else -1 return ServerAssetPage( assets = pageAssets, currentPage = query.page, nextPage = nextPage, total = matchingAssets.size, ) } override suspend fun getAsset( sourceId: String, assetId: String, ): ServerAsset? = assetsBySource[sourceId]?.firstOrNull { it.id == assetId } override suspend fun getGroups(sourceId: String): List? = assetsBySource[sourceId] ?.flatMap(ServerAsset::groups) ?.distinct() } private val STATIC_BRAND_STICKERS_JSON = """ { "version": "1.0.0", "id": "my-server-brand-stickers", "assets": [ { "id": "brand-badge", "label": { "en": "Brand Badge" }, "tags": { "en": ["brand", "badge"] }, "groups": ["stickers"], "meta": { "uri": "{{base_url}}/imgly_logo.jpg", "thumbUri": "{{base_url}}/imgly_logo.jpg", "mimeType": "${MimeType.JPEG.key}", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "shapeType": "//ly.img.ubq/shape/rect", "kind": "sticker", "width": 1980, "height": 720 } } ] } """ ``` Load media from your backend into CE.SDK by exposing your catalog as Android asset sources. > **Reading time:** 9 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-from-remote-source-your-server) CE.SDK uses asset sources to discover media and apply selected assets to a scene. Use a custom `AssetSource` for dynamic server-backed libraries such as user uploads, DAM entries, or CMS media. Use JSON asset sources for static catalogs that you can publish as manifest files next to their thumbnails and original assets. This guide shows both patterns and the Android editor configuration handoff that makes media sources visible in the asset library UI. Template sources use a custom `applyAsset()` path because they load scenes rather than media blocks. ## Understanding Asset Source Types CE.SDK supports two practical patterns for loading server assets. **Custom asset sources** work best for dynamic content. Your Android source receives a `FindAssetsQuery`, forwards search, pagination, tags, and groups to your backend client, and returns a `FindAssetsResult`. **JSON asset sources** work best for static content. Host a `content.json` manifest on your CDN or app server, then load it with `engine.asset.addLocalSourceFromJSON(...)`. URLs in the manifest can use `{{base_url}}` so the same file works after moving the asset folder. ## Creating a Custom Asset Source Create an `AssetSource` subclass for each server catalog you want CE.SDK to query. The source ID must stay stable because the Engine API and editor library UI both refer to sources by ID. ```kotlin highlight-android-backend-setup data class ServerSourceIds( val images: String = "my-server-images", val videos: String = "my-server-videos", val audio: String = "my-server-audio", val stickers: String = "my-server-stickers", val templates: String = "my-server-templates", val brandStickers: String = "my-server-brand-stickers", ) { val all: List get() = listOf(images, videos, audio, stickers, templates, brandStickers) } ``` Define the backend contract your app implements. The guide sample uses a local implementation of this contract, but production code should return your API client from the setup function. ```kotlin highlight-android-backend-contract data class ServerAssetPage( val assets: List, val currentPage: Int, val nextPage: Int, val total: Int, ) interface ServerAssetBackend { suspend fun searchAssets( sourceId: String, query: FindAssetsQuery, ): ServerAssetPage suspend fun getAsset( sourceId: String, assetId: String, ): ServerAsset? suspend fun getGroups(sourceId: String): List? } ``` Return your app-owned backend client from the setup function. The sample factory returns a local demo backend so the guide can run without a network service. The runnable sample passes its configured SDK asset base URI to that demo backend so bundled media stays version-aligned. A production backend should return URLs from your own service instead. ```kotlin highlight-android-backend-client private fun createAppServerBackend( sourceIds: ServerSourceIds, assetBaseUri: Uri, ): ServerAssetBackend { // Replace this demo backend with your app's authenticated API client. return DemoServerBackend(sourceIds, assetBaseUri) } ``` ```kotlin highlight-android-create-backend val sourceIds = ServerSourceIds() // Replace this factory with your app's API client. val backend: ServerAssetBackend = createAppServerBackend(sourceIds, assetBaseUri) ``` ```kotlin highlight-android-dynamic-source private class ServerMediaAssetSource( sourceId: String, override val supportedMimeTypes: List, private val backend: ServerAssetBackend, ) : AssetSource(sourceId = sourceId) { override suspend fun getGroups(): List? = withContext(Dispatchers.IO) { backend.getGroups(sourceId) } override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val page = backend.searchAssets(sourceId = sourceId, query = query) FindAssetsResult( assets = page.assets.map { it.toAsset(sourceId = sourceId) }, currentPage = page.currentPage, nextPage = page.nextPage, total = page.total, ) } override suspend fun fetchAsset( id: String, options: FetchAssetOptions, ): Asset? = withContext(Dispatchers.IO) { backend.getAsset(sourceId = sourceId, assetId = id)?.toAsset(sourceId = sourceId) } } ``` `findAssets()` runs asynchronously, so forward the query to your app's backend client from an IO dispatcher. Return `nextPage = -1` when there are no more results. Register the sources before your code or UI queries them: ```kotlin highlight-android-register-sources fun registerServerAssetSources( engine: Engine, sourceIds: ServerSourceIds, backend: ServerAssetBackend, ) { sourceIds.all .filter(engine.asset.findAllSources()::contains) .forEach(engine.asset::removeSource) val mediaSources = listOf( ServerMediaAssetSource( sourceId = sourceIds.images, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key), backend = backend, ), ServerMediaAssetSource( sourceId = sourceIds.videos, supportedMimeTypes = listOf(MimeType.MP4.key), backend = backend, ), ServerMediaAssetSource( sourceId = sourceIds.audio, supportedMimeTypes = listOf("audio/mpeg", "audio/x-m4a"), backend = backend, ), ServerMediaAssetSource( sourceId = sourceIds.stickers, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key, MimeType.SVG.key), backend = backend, ), ) mediaSources.forEach(engine.asset::addSource) engine.asset.addSource( ServerTemplateAssetSource( engine = engine, sourceId = sourceIds.templates, backend = backend, ), ) } ``` The sample registers separate sources for images, videos, audio, stickers, and templates. Media sources can be placed in editor library sections, while template sources are applied through the custom template asset source. ## Serving Different Media Types Each returned `Asset` needs enough metadata for CE.SDK to create the right block when the asset is applied. Use Android's typed keys for block, fill, and shape types, then store their `.key` values in `meta`. ```kotlin highlight-android-media-metadata data class ServerAsset( val id: String, val label: String, val uri: String, val thumbUri: String, val mimeType: String, val blockType: DesignBlockType, val fillType: FillType? = null, val shapeType: ShapeType? = null, val kind: String? = null, val width: Int? = null, val height: Int? = null, val duration: Double? = null, val tags: List = emptyList(), val groups: List = emptyList(), ) { fun toAsset(sourceId: String) = Asset( id = id, context = AssetContext(sourceId = sourceId), label = label, locale = "en", tags = tags, groups = groups, meta = buildMap { put("uri", uri) put("thumbUri", thumbUri) put("mimeType", mimeType) put("blockType", blockType.key) fillType?.let { put("fillType", it.key) } shapeType?.let { put("shapeType", it.key) } kind?.let { put("kind", it) } width?.let { put("width", it.toString()) } height?.let { put("height", it.toString()) } duration?.let { put("duration", it.toString()) } }, ) } ``` ### Image Assets Images use a graphic block with an image fill. Include `width`, `height`, `mimeType`, `blockType`, `fillType`, and `shapeType` so CE.SDK does not need to infer the asset type from the remote file. ```kotlin highlight-android-image-metadata ServerAsset( id = "img-001", label = "Mountain Landscape", uri = "https://img.ly/static/ubq_samples/sample_1.jpg", thumbUri = "https://img.ly/static/ubq_samples/sample_1.jpg", mimeType = MimeType.JPEG.key, blockType = DesignBlockType.Graphic, fillType = FillType.Image, shapeType = ShapeType.Rect, kind = "image", width = 1920, height = 1280, tags = listOf("mountain", "landscape"), groups = listOf("photos"), ), ``` ### Video Assets Videos also use a graphic block, but with `FillType.Video`. Include `duration` when your backend already knows it so the asset library and timeline can show accurate timing without loading the full video first. ```kotlin highlight-android-video-metadata ServerAsset( id = "vid-001", label = "Surf Promo", uri = "https://img.ly/static/ubq_video_samples/bbb.mp4", thumbUri = "https://img.ly/static/ubq_samples/sample_1.jpg", mimeType = MimeType.MP4.key, blockType = DesignBlockType.Graphic, fillType = FillType.Video, shapeType = ShapeType.Rect, kind = "video", width = 640, height = 360, duration = 634.694, tags = listOf("surf", "promo"), groups = listOf("videos"), ), ``` ### Audio Assets Audio assets use `DesignBlockType.Audio`. Include `mimeType` and `duration`; audio sources do not need image fill or shape metadata. ```kotlin highlight-android-audio-metadata ServerAsset( id = "audio-001", label = "Background Track", uri = assetBaseUri .buildUpon() .appendPath("ly.img.audio") .appendPath("audios") .appendPath("far_from_home.m4a") .build() .toString(), thumbUri = "https://img.ly/static/ubq_samples/sample_2.jpg", mimeType = "audio/x-m4a", blockType = DesignBlockType.Audio, duration = 98.716, tags = listOf("music", "background"), groups = listOf("audio"), ), ``` ### Sticker Assets Stickers are image assets with `kind = "sticker"`. They still use a graphic block with an image fill, but the `kind` value lets editor UI treat them as stickers rather than regular images. ```kotlin highlight-android-sticker-metadata ServerAsset( id = "sticker-001", label = "Camera Badge", uri = "https://img.ly/static/ubq_samples/imgly_logo.jpg", thumbUri = "https://img.ly/static/ubq_samples/imgly_logo.jpg", mimeType = MimeType.JPEG.key, blockType = DesignBlockType.Graphic, fillType = FillType.Image, shapeType = ShapeType.Rect, kind = "sticker", width = 1980, height = 720, tags = listOf("camera", "badge"), groups = listOf("stickers"), ), ``` ### Template Assets Templates replace the current scene instead of adding a media block. Provide a custom `applyAsset()` implementation that loads the `.scene` URL from your asset metadata. ```kotlin highlight-android-template-metadata ServerAsset( id = "template-001", label = "Launch Story", uri = assetBaseUri .buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_postcard_1.scene") .build() .toString(), thumbUri = assetBaseUri .buildUpon() .appendPath("ly.img.templates") .appendPath("thumbnails") .appendPath("cesdk_postcard_1.jpg") .build() .toString(), mimeType = "application/ubq-template-string", blockType = DesignBlockType.Scene, kind = "scene", tags = listOf("launch", "story"), groups = listOf("templates"), ), ``` ```kotlin highlight-android-template-source private class ServerTemplateAssetSource( private val engine: Engine, sourceId: String, private val backend: ServerAssetBackend, ) : AssetSource(sourceId = sourceId) { override val supportedMimeTypes: List = listOf("application/ubq-template-string") override suspend fun getGroups(): List? = withContext(Dispatchers.IO) { backend.getGroups(sourceId) } override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { val page = backend.searchAssets(sourceId = sourceId, query = query) FindAssetsResult( assets = page.assets.map { it.toAsset(sourceId = sourceId) }, currentPage = page.currentPage, nextPage = page.nextPage, total = page.total, ) } override suspend fun fetchAsset( id: String, options: FetchAssetOptions, ): Asset? = withContext(Dispatchers.IO) { backend.getAsset(sourceId = sourceId, assetId = id)?.toAsset(sourceId = sourceId) } override suspend fun applyAsset(asset: Asset): DesignBlock? { val templateUri = asset.meta?.get("uri")?.let(Uri::parse) ?: return null engine.scene.load(sceneUri = templateUri) return null } } ``` Use `engine.scene.load(sceneUri = _)` for both scene files and archives; the engine detects the content automatically. ## Loading Assets from JSON For static catalogs, publish a JSON manifest with `id`, `version`, and `assets` fields. The Android API can load a manifest from a JSON string or directly from a hosted URI. ```kotlin highlight-android-json-source suspend fun loadStaticJsonAssetSource(engine: Engine): String = engine.asset.addLocalSourceFromJSON( contentJSON = STATIC_BRAND_STICKERS_JSON, basePath = "https://img.ly/static/ubq_samples", ) ``` The `contentJSON` overload is useful for bundled or generated manifests. Use `engine.asset.addLocalSourceFromJSON(contentUri = _)` when your CDN serves `content.json` next to the assets. ## Displaying Assets in the UI After the Engine knows about your media sources, reference their IDs from the Android asset library configuration. The editor library uses `AssetSourceType(sourceId = _)` inside `LibraryContent.Section` entries for images, videos, audio, and stickers. The sticker section can combine your dynamic sticker source with the static JSON catalog loaded from your backend. ```kotlin highlight-android-editor-library fun createServerAssetLibrary(sourceIds: ServerSourceIds): AssetLibrary { val serverImageSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(AssetSourceType(sourceId = sourceIds.images)), assetType = AssetType.Image, ) val serverVideoSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_videos, sourceTypes = listOf(AssetSourceType(sourceId = sourceIds.videos)), assetType = AssetType.Video, ) val serverAudioSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_audio, sourceTypes = listOf(AssetSourceType(sourceId = sourceIds.audio)), assetType = AssetType.Audio, ) val serverStickerSection = LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_title_stickers, sourceTypes = listOf( AssetSourceType(sourceId = sourceIds.stickers), AssetSourceType(sourceId = sourceIds.brandStickers), ), assetType = AssetType.Sticker, ) return AssetLibrary.getDefault( includeAVResources = true, images = LibraryCategory.Images.addSection(serverImageSection), videos = LibraryCategory.Video.addSection(serverVideoSection), audios = LibraryCategory.Audio.addSection(serverAudioSection), stickers = LibraryCategory.Stickers.addSection(serverStickerSection), ) } ``` Assign the returned `AssetLibrary` to `EditorConfiguration.assetLibrary` in the Base Editor configuration. Register the server sources and load the static JSON catalog in `onLoaded` so the default scene creation path still runs before the library opens. ```kotlin highlight-android-editor-configuration @Composable fun rememberYourServerEditorConfiguration(assetBaseUri: Uri): EditorConfiguration { val sourceIds = remember { ServerSourceIds() } val backend = remember(sourceIds, assetBaseUri) { createAppServerBackend(sourceIds, assetBaseUri) } return EditorConfiguration.remember { onLoaded = { registerServerAssetSources( engine = editorContext.engine, sourceIds = sourceIds, backend = backend, ) loadStaticJsonAssetSource(editorContext.engine) } assetLibrary = { createServerAssetLibrary(sourceIds) } } } ``` Keep templates on the custom `AssetSource.applyAsset()` path shown above unless your app adds its own template browser. If you need a complete editor surface around this configuration, start from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) and add this configuration to your editor setup. ## Server Architecture Considerations Backend configuration determines how reliable your asset source feels in production. - **Network access and CORS**: Android loads asset, thumbnail, and manifest URLs through native networking, so browser CORS headers do not control Android requests. Use reachable HTTPS URLs, valid authentication or signed URLs, and app network security settings that allow your backend. Configure CORS only for web editors that reuse the same backend. - **Authentication**: Send tokens from your backend client inside `findAssets()`, or return short-lived signed URLs in `meta.uri` and `meta.thumbUri`. - **Thumbnails**: Generate thumbnails server-side and return them through `thumbUri`. Keep thumbnails smaller than the original asset to make library grids responsive. - **Pagination**: Return stable totals and a `nextPage` value only while more results exist. Android uses `-1` to mark the end of the result set. - **Caching**: Use long cache lifetimes for immutable thumbnails and shorter lifetimes for dynamic search responses. ## Troubleshooting **Assets do not appear**: Confirm that the source is registered before you query it and that `findAssets()` returns a non-empty `assets` list with `currentPage`, `nextPage`, and `total`. **Search returns the same results**: Forward `FindAssetsQuery.query`, `tags`, `groups`, `page`, and `perPage` to your backend instead of ignoring the query object. **Media applies as the wrong type**: Check `meta.blockType`, `meta.fillType`, `meta.shapeType`, and `meta.mimeType`. Missing metadata forces CE.SDK to infer the type from the remote resource. **Static JSON URLs resolve incorrectly**: Use `{{base_url}}` in the manifest and pass either the hosted manifest URI or an explicit `basePath` when loading JSON from a string. ## API Reference | Method | Purpose | | --- | --- | | `engine.asset.findAllSources()` | List currently registered source IDs before replacing guide-owned sources | | `engine.asset.removeSource(sourceId=_)` | Remove an existing source by ID before registering an updated source | | `engine.asset.addSource(source=_)` | Register a custom Android asset source | | `AssetSource.getGroups()` | Return the groups available for filtering in a custom source | | `AssetSource.findAssets(query=_)` | Return paginated assets for the current search query | | `AssetSource.fetchAsset(id=_, options=_)` | Resolve a single asset by ID when direct fetching is needed | | `AssetSource.applyAsset(asset=_)` | Override how a selected asset is applied, such as loading a template scene | | `engine.asset.findAssets(sourceId=_, query=_)` | Query an asset source programmatically | | `Asset(id=_, context=_, label=_, locale=_, tags=_, groups=_, meta=_)` | Build an asset result with metadata returned from your backend | | `AssetContext(sourceId=_)` | Associate an asset result with the source that returned it | | `FindAssetsResult(assets=_, currentPage=_, nextPage=_, total=_)` | Return a paginated result from `findAssets()` | | `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_, matcher=_)` | Load a static source from a JSON string | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Load a hosted `content.json` manifest | | `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene or archive template asset (content detected automatically) | | `EditorConfiguration.remember(builder=_)` | Create a Base Editor configuration that registers sources and supplies the asset library | | `EditorConfigurationBuilder.onLoaded` | Register server sources after the default scene has loaded | | `EditorConfigurationBuilder.assetLibrary` | Assign the editor asset library built from server source IDs | | `AssetLibrary.getDefault(includeAVResources=_, images=_, videos=_, audios=_, stickers=_)` | Build an Android editor asset library from source-backed categories | | `LibraryContent.Section(titleRes=_, sourceTypes=_, assetType=_)` | Create a library section that points at one or more asset sources | | `AssetSourceType(sourceId=_)` | Reference a registered source from an Android editor library section | | `LibraryCategory.addSection(section=_)` | Add a server-backed section to an existing library category | | Metadata Property | Purpose | | --- | --- | | `meta.uri` | Full media, scene, or archive URL used when the asset is applied | | `meta.thumbUri` | Thumbnail shown in the asset library | | `meta.mimeType` | MIME type used for filtering, previews, and type inference | | `meta.blockType` | Block type to create, such as `DesignBlockType.Graphic.key` | | `meta.fillType` | Fill type for graphic media, such as `FillType.Image.key` or `FillType.Video.key` | | `meta.shapeType` | Shape for graphic assets, commonly `ShapeType.Rect.key` | | `meta.kind` | Asset kind, such as `image`, `video`, `sticker`, or `scene` | | `meta.duration` | Duration for audio and video assets | | `meta.width` / `meta.height` | Intrinsic media dimensions for layout and aspect ratio handling | ## Next Steps - [From User Upload](https://img.ly/docs/cesdk/android/import-media/from-local-source/user-upload-c6c7d9/) - Enable file picker uploads from end users for use in the editor. - [Asset Concepts](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/) - Asset sources and metadata architecture - [Thumbnails](https://img.ly/docs/cesdk/android/import-media/asset-library/thumbnails-c23949/) - Configure thumbnail images for assets in CE.SDK's asset library with proper sizing, preview URIs for audio, and customized UI display. - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - UI customization 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: "Overview" description: "Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/import-media/overview-84bb23/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Overview](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/) --- In CE.SDK, assets are the building blocks of your creative workflow—whether they’re images, videos, audio, fonts, or templates. They power everything from basic image edits to dynamic, template-driven design generation. This guide gives you a high-level understanding of how to bring assets into CE.SDK, where they can come from, and how to decide on the right strategy for your application. Whether you're working with local uploads, remote storage, or third-party sources, this guide will help you navigate your options and build an efficient import pipeline. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## File Type Support CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio content. ## Media Constraints ### 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: "Retrieve MIME Type" description: "Detect the MIME type of resources loaded in the engine to choose the correct handling, file extension, or upload content type." platform: android url: "https://img.ly/docs/cesdk/android/import-media/retrieve-mimetype-ed13bf/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Retrieve Mimetype](https://img.ly/docs/cesdk/android/import-media/retrieve-mimetype-ed13bf/) --- ```kotlin file=@cesdk_android_examples/engine-guides-retrieve-mimetype/RetrieveMimeType.kt reference-only import android.graphics.Bitmap import android.graphics.Color 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.ShapeType import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.nio.ByteBuffer import java.nio.channels.Channels data class ResourceWithMimeType( val uri: Uri, val size: Int, val mimeType: String, ) data class RetrieveMimeTypeResult( val resourcesByType: Map, val imageResourceCount: Int, val uploadedResourceCount: Int, val uploadedFileResourceCount: Int, val uploadedBufferResourceCount: Int, val remainingTransientResourceCount: Int, val externalMimeType: String, ) suspend fun retrieveMimeType(engine: Engine): RetrieveMimeTypeResult { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 320F) engine.block.setHeight(page, value = 180F) engine.block.setDuration(page, duration = 1.0) val bitmap = Bitmap .createBitmap(32, 32, Bitmap.Config.ARGB_8888) .apply { eraseColor(Color.rgb(52, 118, 235)) } val imageBuffer = bitmap.toPngBuffer() val imageBufferUri = engine.editor.createBuffer() try { engine.editor.setBufferData(uri = imageBufferUri, offset = 0, data = imageBuffer) 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.setWidth(imageBlock, value = 160F) engine.block.setHeight(imageBlock, value = 120F) engine.block.setPositionX(imageBlock, value = 80F) engine.block.setPositionY(imageBlock, value = 30F) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = imageBufferUri, ) engine.block.setFill(block = imageBlock, fill = imageFill) engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER) engine.block.appendChild(parent = page, child = imageBlock) val assetImageUri = Uri.parse("file:///android_asset/webkit-P3.png") val assetImageBlock = engine.block.create(DesignBlockType.Graphic) val assetImageFill = engine.block.createFill(FillType.Image) engine.block.setShape(assetImageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(assetImageBlock, value = 80F) engine.block.setHeight(assetImageBlock, value = 80F) engine.block.setPositionX(assetImageBlock, value = 200F) engine.block.setPositionY(assetImageBlock, value = 50F) engine.block.setUri( block = assetImageFill, property = "fill/image/imageFileURI", value = assetImageUri, ) engine.block.setFill(block = assetImageBlock, fill = assetImageFill) engine.block.setContentFillMode(block = assetImageBlock, mode = ContentFillMode.COVER) engine.block.appendChild(parent = page, child = assetImageBlock) engine.block.forceLoadResources(blocks = listOf(scene)) val transientResources = engine.editor.findAllTransientResources() check(transientResources.any { (uri, _) -> uri == imageBufferUri }) check(transientResources.any { (uri, _) -> uri == assetImageUri }) val resourcesWithMimeType = transientResources.map { (uri, size) -> ResourceWithMimeType( uri = uri, size = size, mimeType = engine.editor.getMimeType(uri = uri), ) } val resourcesByType = resourcesWithMimeType .groupingBy { it.mimeType } .eachCount() val imageResources = resourcesWithMimeType.filter { resource -> resource.mimeType.startsWith("image/") } check(imageResources.isNotEmpty()) val uploadedResources = imageResources.associate { resource -> val uploadedResource = File.createTempFile("cesdk-resource-", ".upload") try { FileOutputStream(uploadedResource).channel.use { channel -> engine.editor.getResourceData( uri = resource.uri, chunkSize = 64 * 1024, ) { chunk -> val data = chunk.asReadOnlyBuffer() while (data.hasRemaining()) { channel.write(data) } true } } check(uploadedResource.length() == resource.size.toLong()) val permanentUri = uploadTransientResource( uri = resource.uri, file = uploadedResource, mimeType = resource.mimeType, ) resource.uri to permanentUri } finally { uploadedResource.delete() } } for ((transientUri, permanentUri) in uploadedResources) { engine.editor.relocateResource( currentUri = transientUri, relocatedUri = permanentUri, ) } val remainingTransientResources = engine.editor.findAllTransientResources() check(remainingTransientResources.none { (uri, _) -> uri in uploadedResources.keys }) val externalMimeType = engine.editor.getMimeType( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) return RetrieveMimeTypeResult( resourcesByType = resourcesByType, imageResourceCount = imageResources.size, uploadedResourceCount = uploadedResources.size, uploadedFileResourceCount = uploadedResources.keys.count { it.scheme == "file" }, uploadedBufferResourceCount = uploadedResources.keys.count { it.scheme == "buffer" }, remainingTransientResourceCount = remainingTransientResources.size, externalMimeType = externalMimeType, ) } finally { engine.editor.destroyBuffer(uri = imageBufferUri) } } private fun Bitmap.toPngBuffer(): ByteBuffer { val pngFile = File.createTempFile("cesdk-buffer-image-", ".png") try { FileOutputStream(pngFile).channel.use { channel -> val output = Channels.newOutputStream(channel) check(compress(Bitmap.CompressFormat.PNG, 100, output)) output.flush() } return FileInputStream(pngFile).channel.use { channel -> val size = channel.size() check(size <= Int.MAX_VALUE) { "PNG buffer is too large." } ByteBuffer.allocateDirect(size.toInt()).apply { while (hasRemaining() && channel.read(this) != -1) { // Keep reading until the direct buffer contains the full PNG file. } flip() } } } finally { recycle() pngFile.delete() } } private fun uploadTransientResource( uri: Uri, file: File, mimeType: String, ): Uri { check(file.length() > 0) { "Cannot upload an empty resource." } // Replace this with your app's storage client and use mimeType as the upload content type. val extension = when (mimeType) { "image/jpeg" -> "jpg" "image/png" -> "png" "image/webp" -> "webp" else -> "bin" } val sourceName = uri.lastPathSegment?.takeIf { it.isNotBlank() } ?: "resource" val storageKey = sourceName .substringBeforeLast(delimiter = ".", missingDelimiterValue = sourceName) .ifBlank { "resource" } return Uri.parse("https://your-storage.example/uploads/$storageKey.$extension") } ``` Detect the content type of CE.SDK resources before you upload, filter, or serialize them. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-retrieve-mimetype) CE.SDK resources can be referenced by `https://`, `file://`, `buffer://`, and other resource Uris. Do not infer whether a resource needs relocation from the URI scheme alone. Use `engine.editor.findAllTransientResources()` as the source of truth, then call `engine.editor.getMimeType()` for the returned Uris before deciding how to upload, filter, or persist them. The Android sample creates a transient PNG image buffer, loads a packaged Android asset image, detects their MIME types, streams each resource's data, relocates the resources to permanent Uris, and validates an external image Uri before import. ## Finding Transient Resources Transient resources are resources whose data would be lost during scene export or serialization unless you relocate them first. Use `findAllTransientResources()` to list the resource Uris and their byte sizes. ```kotlin highlight-android-find-transient-resources val transientResources = engine.editor.findAllTransientResources() ``` Each pair contains the resource `Uri` and the size in bytes. Treat this list as the relocation checklist for the current scene, regardless of whether a returned Uri uses `buffer://`, `file://`, or another scheme. ## Retrieving the MIME Type Call `getMimeType()` for each resource Uri to detect the underlying file format. The method is suspendable because the engine may need to fetch or decode the resource before it can identify the type. ```kotlin highlight-android-get-mime-type val resourcesWithMimeType = transientResources.map { (uri, size) -> ResourceWithMimeType( uri = uri, size = size, mimeType = engine.editor.getMimeType(uri = uri), ) } val resourcesByType = resourcesWithMimeType .groupingBy { it.mimeType } .eachCount() ``` Common return values include: - `image/jpeg` for JPEG images - `image/png` for PNG images - `image/webp` for WebP images - `font/ttf` for TrueType fonts - `font/woff2` for WOFF2 fonts ## Filtering Resources by Type Use the MIME type prefix to route resources through the right handling path. For example, image uploads can use image-specific previews and extensions while fonts can go through a separate font-storage pipeline. ```kotlin highlight-android-filter-images val imageResources = resourcesWithMimeType.filter { resource -> resource.mimeType.startsWith("image/") } ``` This keeps resource processing data-driven instead of relying on file names or URI suffixes. ## Reading Resource Data After identifying a transient resource, stream its data with `getResourceData()`. This works for every Uri returned by `findAllTransientResources()`, including `buffer://` resources and `file://` Android asset resources. Use the MIME type as the upload content type or to choose a file extension. ```kotlin highlight-android-read-resource-data val uploadedResources = imageResources.associate { resource -> val uploadedResource = File.createTempFile("cesdk-resource-", ".upload") try { FileOutputStream(uploadedResource).channel.use { channel -> engine.editor.getResourceData( uri = resource.uri, chunkSize = 64 * 1024, ) { chunk -> val data = chunk.asReadOnlyBuffer() while (data.hasRemaining()) { channel.write(data) } true } } check(uploadedResource.length() == resource.size.toLong()) val permanentUri = uploadTransientResource( uri = resource.uri, file = uploadedResource, mimeType = resource.mimeType, ) resource.uri to permanentUri } finally { uploadedResource.delete() } } ``` The sample upload helper stands in for your app's storage client. Replace it with the code that uploads the streamed data and returns a permanent Uri. ```kotlin highlight-android-upload-helper private fun uploadTransientResource( uri: Uri, file: File, mimeType: String, ): Uri { check(file.length() > 0) { "Cannot upload an empty resource." } // Replace this with your app's storage client and use mimeType as the upload content type. val extension = when (mimeType) { "image/jpeg" -> "jpg" "image/png" -> "png" "image/webp" -> "webp" else -> "bin" } val sourceName = uri.lastPathSegment?.takeIf { it.isNotBlank() } ?: "resource" val storageKey = sourceName .substringBeforeLast(delimiter = ".", missingDelimiterValue = sourceName) .ifBlank { "resource" } return Uri.parse("https://your-storage.example/uploads/$storageKey.$extension") } ``` ## Relocating Resources After upload, call `relocateResource()` to update scene references from each transient resource Uri to a permanent Uri. ```kotlin highlight-android-relocate-resources for ((transientUri, permanentUri) in uploadedResources) { engine.editor.relocateResource( currentUri = transientUri, relocatedUri = permanentUri, ) } ``` Relocation lets the current scene keep working while making future serialization independent of resources that would otherwise be omitted. ## Verifying Relocation Call `findAllTransientResources()` again before saving or exporting. Any remaining transient resource still needs to be uploaded or otherwise persisted. ```kotlin highlight-android-verify-relocation val remainingTransientResources = engine.editor.findAllTransientResources() check(remainingTransientResources.none { (uri, _) -> uri in uploadedResources.keys }) ``` When the list no longer contains the relocated Uris, scene serialization can reference permanent resources instead of transient data. ## Validating External Uris `getMimeType()` also accepts external Uris. The engine downloads the resource if needed, so you can check an incoming file before importing or routing it through a processing pipeline. ```kotlin highlight-android-validate-external-uri val externalMimeType = engine.editor.getMimeType( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) ``` Wrap this call with your normal error handling when the Uri comes from user input or a remote service. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.editor.findAllTransientResources()` | Discovery | Return transient resource Uris and sizes for the current scene | | `engine.editor.getMimeType(uri=_)` | Discovery | Detect the MIME type of a resource Uri | | `engine.editor.getResourceData(uri=_, chunkSize=_, onData=_)` | Resource Data | Stream resource data in chunks for any transient resource Uri | | `engine.editor.relocateResource(currentUri=_, relocatedUri=_)` | Resource Management | Update scene references from one resource Uri to another | ## Troubleshooting **No transient resources are found** Verify that the scene is loaded and that media resources have been loaded before calling `findAllTransientResources()`. Some `file://` resources, including Android asset Uris, can still appear in this list, so rely on the API result instead of filtering by scheme. **MIME type detection fails** Confirm that the Uri is valid and that the resource can be fetched. For external Uris, handle network or authorization failures around the suspend call. If the engine can fetch the resource but cannot classify its bytes, `getMimeType()` can return the literal string `"unknown"`; treat that result like a fallback path. **Resource bytes are missing after serialization** Transient data is not included in the exported scene. Stream the data to persistent storage and call `relocateResource()` for each Uri returned by `findAllTransientResources()` before saving a scene that must load in a future session. ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/) — Load self-contained CE.SDK archive files that bundle scene structure with all referenced assets for portable, reliable design imports. - [Export Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Export scenes and work with the resulting resource data. --- ## 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: "Learn about file size restrictions and how to optimize large assets for use in CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/import-media/size-limits-c32275/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Size Limits](https://img.ly/docs/cesdk/android/import-media/size-limits-c32275/) --- CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio, but there are practical limits to consider based on the user's device capabilities. ## 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: "Source Sets" description: "Provide multiple image and video sources so CE.SDK can choose the right resolution for Android previews and exports." platform: android url: "https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/android/import-media-4e3703/) > [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) --- ```kotlin file=@cesdk_android_examples/engine-guides-source-sets/SourceSets.kt reference-only import android.net.Uri import ly.img.engine.AssetDefinition import ly.img.engine.AssetPayload 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 data class SourceSets( val configuredImageSourceWidths: List, val updatedImageSourceWidths: List, val assetSourceWidths: List, val videoSourceWidths: List, val lowQualityVideoPreviewEnabled: Boolean, val exportedImage: ByteBuffer, ) suspend fun sourceSets(engine: Engine): SourceSets { 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.setWidth(imageBlock, value = 300F) engine.block.setHeight(imageBlock, value = 300F) engine.block.setPositionX(imageBlock, value = 50F) engine.block.setPositionY(imageBlock, value = 50F) val imageFill = engine.block.createFill(FillType.Image) engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", 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, ), ), ) engine.block.setFill(block = imageBlock, fill = imageFill) engine.block.appendChild(parent = page, child = imageBlock) val configuredImageSourceSet = engine.block.getSourceSet( block = imageFill, property = "fill/image/sourceSet", ) check(configuredImageSourceSet.map(Source::width) == listOf(1024, 512)) val currentImageSourceSet = engine.block.getSourceSet( block = imageFill, property = "fill/image/sourceSet", ) engine.block.addImageFileUriToSourceSet( block = imageFill, property = "fill/image/sourceSet", uri = "https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg", ) val updatedImageSourceSet = engine.block.getSourceSet( block = imageFill, property = "fill/image/sourceSet", ) check(currentImageSourceSet.map(Source::width) == listOf(1024, 512)) check(updatedImageSourceSet.map(Source::width) == listOf(2048, 1024, 512)) val assetSourceId = "android-guide-source-sets" if (assetSourceId in engine.asset.findAllSources()) { engine.asset.removeSource(assetSourceId) } val assetWithSourceSet = AssetDefinition( id = "multi-resolution-image", label = mapOf("en" to "Multi-resolution image"), meta = mapOf( "kind" to "image", "fillType" to FillType.Image.key, ), payload = AssetPayload( 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.asset.addLocalSource( sourceId = assetSourceId, supportedMimeTypes = listOf(MimeType.JPEG.key), ) engine.asset.addAsset(sourceId = assetSourceId, asset = assetWithSourceSet) val asset = engine.asset.fetchAsset( sourceId = assetSourceId, assetId = assetWithSourceSet.id, ) ?: error("Expected the local source to return the asset.") val assetBlock = engine.asset.defaultApplyAsset(asset) ?: error("Expected the image asset to create a block.") val assetFill = engine.block.getFill(assetBlock) val assetSourceSet = engine.block.getSourceSet( block = assetFill, property = "fill/image/sourceSet", ) check(assetSourceSet.map(Source::width) == listOf(2048, 1024, 512)) val videoBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(videoBlock, value = 400F) engine.block.setHeight(videoBlock, value = 225F) engine.block.setPositionX(videoBlock, value = 50F) engine.block.setPositionY(videoBlock, value = 400F) val videoFill = engine.block.createFill(FillType.Video) 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, ), ), ) engine.block.setFill(block = videoBlock, fill = videoFill) engine.block.appendChild(parent = page, child = videoBlock) engine.block.addVideoFileUriToSourceSet( block = videoFill, property = "fill/video/sourceSet", uri = "https://img.ly/static/example-assets/sourceset/2x.mp4", ) val videoSourceSet = engine.block.getSourceSet( block = videoFill, property = "fill/video/sourceSet", ) check(videoSourceSet.map(Source::width) == listOf(1440, 720)) val previousLowQualityVideoPreview = engine.editor.getSettingBoolean( keypath = "features/forceLowQualityVideoPreview", ) engine.editor.setSettingBoolean( keypath = "features/forceLowQualityVideoPreview", value = true, ) val lowQualityVideoPreviewEnabled = engine.editor.getSettingBoolean( keypath = "features/forceLowQualityVideoPreview", ) engine.editor.setSettingBoolean( keypath = "features/forceLowQualityVideoPreview", value = previousLowQualityVideoPreview, ) val exportedImage = engine.block.export(imageBlock, mimeType = MimeType.PNG) return SourceSets( configuredImageSourceWidths = configuredImageSourceSet.map(Source::width), updatedImageSourceWidths = updatedImageSourceSet.map(Source::width), assetSourceWidths = assetSourceSet.map(Source::width), videoSourceWidths = videoSourceSet.map(Source::width), lowQualityVideoPreviewEnabled = lowQualityVideoPreviewEnabled, exportedImage = exportedImage, ) } ``` Configure source sets for images and videos so CE.SDK can choose an appropriate resolution for editing previews and exports. ![Source Sets result exported from the Android guide](./assets/android.hero.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-source-sets) Source sets let you provide multiple versions of the same media asset at different pixel dimensions. The engine can load a smaller source for preview rendering and still use a higher-resolution source when the target drawing or export size needs it. This guide covers source sets on image fills, source sets inside asset definitions, video source sets, and the video preview quality setting that helps keep editing responsive. ## How Source Set Selection Works When CE.SDK renders a fill, it calculates the current drawing size in screen pixels. If the fill has a source set, the engine selects the source with the closest size that meets or exceeds the drawing size. If every source is smaller than the drawing size, the largest source is used. Without a source set, image fills decode the single image source and clamp the maximum edge length to the `maxImageSize` setting. Source sets give you explicit control over the intermediate resolutions used during editing and export. ## Setting a Source Set on an Image Fill Set image source sets on the image fill block with `engine.block.setSourceSet()`. Each `Source` contains a `Uri`, `width`, and `height`, and the dimensions must match the actual media file. > **Caution:** CE.SDK supports either `fill/image/imageFileURI` for one image or > `fill/image/sourceSet` for multiple image resolutions. Avoid setting both on the > same fill because the engine may choose a source that does not match the single > URI you expected. ```kotlin highlight-android-set-source-set val imageBlock = engine.block.create(DesignBlockType.Graphic) 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 = 50F) engine.block.setPositionY(imageBlock, value = 50F) val imageFill = engine.block.createFill(FillType.Image) engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", 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, ), ), ) engine.block.setFill(block = imageBlock, fill = imageFill) engine.block.appendChild(parent = page, child = imageBlock) ``` ## Querying and Modifying Source Sets Use `engine.block.getSourceSet()` to inspect the sources stored on a fill. When you need to add an image and do not already know its dimensions, call `engine.block.addImageFileUriToSourceSet()` so the engine loads the image and records its width and height. ```kotlin highlight-android-query-source-set val currentImageSourceSet = engine.block.getSourceSet( block = imageFill, property = "fill/image/sourceSet", ) engine.block.addImageFileUriToSourceSet( block = imageFill, property = "fill/image/sourceSet", uri = "https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg", ) val updatedImageSourceSet = engine.block.getSourceSet( block = imageFill, property = "fill/image/sourceSet", ) ``` `setSourceSet()` is still the better choice when your backend already knows the dimensions because it avoids fetching the file just to read metadata. ## Using Source Sets in Asset Definitions Asset definitions can carry the same responsive sources in `AssetPayload.sourceSet`. After you add the asset to a local source and apply it, `engine.asset.defaultApplyAsset()` configures the resulting block's fill with the source set from the asset payload. ```kotlin highlight-android-asset-source-set val assetSourceId = "android-guide-source-sets" if (assetSourceId in engine.asset.findAllSources()) { engine.asset.removeSource(assetSourceId) } val assetWithSourceSet = AssetDefinition( id = "multi-resolution-image", label = mapOf("en" to "Multi-resolution image"), meta = mapOf( "kind" to "image", "fillType" to FillType.Image.key, ), payload = AssetPayload( 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.asset.addLocalSource( sourceId = assetSourceId, supportedMimeTypes = listOf(MimeType.JPEG.key), ) engine.asset.addAsset(sourceId = assetSourceId, asset = assetWithSourceSet) val asset = engine.asset.fetchAsset( sourceId = assetSourceId, assetId = assetWithSourceSet.id, ) ?: error("Expected the local source to return the asset.") val assetBlock = engine.asset.defaultApplyAsset(asset) ?: error("Expected the image asset to create a block.") val assetFill = engine.block.getFill(assetBlock) val assetSourceSet = engine.block.getSourceSet( block = assetFill, property = "fill/image/sourceSet", ) ``` Use this pattern when your asset library stores the available resolutions alongside each asset. The applied block then behaves like a block you configured manually with `setSourceSet()`. ## Video Source Sets Video fills use the `fill/video/sourceSet` property. The source selection rules match image fills: CE.SDK chooses the closest source for the current drawing size and can use the highest-quality source for export. ```kotlin highlight-android-video-source-set val videoBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(videoBlock, value = 400F) engine.block.setHeight(videoBlock, value = 225F) engine.block.setPositionX(videoBlock, value = 50F) engine.block.setPositionY(videoBlock, value = 400F) val videoFill = engine.block.createFill(FillType.Video) 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, ), ), ) engine.block.setFill(block = videoBlock, fill = videoFill) engine.block.appendChild(parent = page, child = videoBlock) engine.block.addVideoFileUriToSourceSet( block = videoFill, property = "fill/video/sourceSet", uri = "https://img.ly/static/example-assets/sourceset/2x.mp4", ) val videoSourceSet = engine.block.getSourceSet( block = videoFill, property = "fill/video/sourceSet", ) ``` Use `engine.block.addVideoFileUriToSourceSet()` when the engine should load a video file to determine its dimensions before adding it to the existing source set. ## Video Preview Quality Settings For editing performance, enable `features/forceLowQualityVideoPreview` to force video previews to use the smallest available source. Export still uses the highest-quality source available for the requested output. ```kotlin highlight-android-video-preview-settings engine.editor.setSettingBoolean( keypath = "features/forceLowQualityVideoPreview", value = true, ) ``` The related `features/matchThumbnailSourceToFill` setting controls whether video thumbnails follow the fill's selected source. When it is disabled, which is the default, thumbnails use the smallest source. ## Automatic Source Sets The engine can make the smaller entries of a source set by itself, so it can draw a small image where the full one is not needed. This is off by default. Set `features/automaticSourceSetsEnabled` to `true` to turn it on. ### Which fills get entries The engine adds entries to an image fill when both of these are true: - You added the fill in this session. A fill from a loaded scene gets none, so a document of many pages costs nothing to open. - The fill has one image and no smaller sizes. An `imageFileURI` on its own counts, and so does a source set with a single entry. A fill that already holds two or more entries gets none. You gave the engine sizes to choose from, so it adds no more. The engine adds entries one time for each fill. Two fills of the same image file share the same entries, so the engine does the work one time. Give a fill a different image and the engine removes the entries it made, because they show the old image. The new image gets none. ### How many entries, and how large The smallest entry the engine makes is **512 pixels** on the long edge. It makes one entry for each time the long edge of the image halves down to that size. The sizes shrink by the same ratio each step, and the last one lands on 512 pixels: | Long edge of the image | Entries the engine adds | | --- | --- | | Under 1024 pixels | none | | 1024 pixels | one, at 512 pixels | | 2048 pixels | two, at 1024 and 512 pixels | | 4096 pixels | three, at 2048, 1024 and 512 pixels | An image under 1024 pixels on its long edge gets nothing, because the single entry it could hold saves too little to pay for the work. No entry is larger than the `maxImageSize` setting, which is 4096 pixels by default. The engine draws no image above that size, so a larger entry would never be selected. Your own image stays the largest source in the set, so an export still uses the full resolution. The engine reads an image with more pixels than `maxImageSize` squared at a smaller size, and makes each entry from the entry before it. ### The format of an entry Each entry is a JPEG for an image without transparency, and a PNG for one with it. A JPEG entry uses quality 90, which you cannot change, so it saves space but is not exact. ### What the work costs The engine takes one step for each engine update, reads the image a few rows at a time, and encodes each entry row by row, so adding an image does not hold a frame. No step runs while you drag, during an export, or during an implicit update. ### Saving a scene that holds generated entries The entries live in memory. Each one states a `buffer://` address, which means nothing once the session that made it is gone. `findAllTransientResources` lists every resource of that kind, the entries among them. What you must do depends on how you save: **An archive needs nothing.** `saveToArchive` carries the bytes of every resource, the entries included, so it holds no `buffer://` address that a later session cannot resolve. **A string needs somewhere to put the bytes.** A saved string can only state an address, and `buffer` is not one of the schemes a save allows, so `saveToString` reports the disallowed scheme and stops. The Android `SaveToStringOptions` has no persistence callback yet, so do it yourself before the save: read the bytes of each entry with `engine.editor.getBufferData`, store them where your application keeps its media, and put the new addresses into the source set with `setSourceSet`. ## Troubleshooting | Problem | Solution | | --- | --- | | Wrong resolution is selected | Check that every `Source.width` and `Source.height` matches the actual file dimensions. | | Preview performance is poor | Add smaller sources to the source set, especially for video fills or large images. | | Export quality is too low | Include a source that is large enough for the target export resolution. | | The asset applies without a source set | Confirm that `AssetPayload.sourceSet` is populated and that the applied asset uses an image or video fill. | ## API Reference | API | Description | | --- | --- | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a graphic block that can display a fill. | | `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular shape for the graphic block. | | `engine.block.setShape(block=_, shape=_)` | Attach the 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 horizontal position. | | `engine.block.setPositionY(block=_, value=_)` | Set the block's vertical position. | | `engine.block.appendChild(parent=_, child=_)` | Add a block to the scene or page hierarchy. | | `engine.block.createFill(fillType=FillType.Image)` | Create an image fill block. | | `engine.block.createFill(fillType=FillType.Video)` | Create a video fill block. | | `engine.block.setFill(block=_, fill=_)` | Attach a fill to a design block. | | `engine.block.getFill(block=_)` | Read the fill attached to a design block. | | `engine.block.setSourceSet(block=_, property="fill/image/sourceSet", sourceSet=_)` | Assign responsive image sources. | | `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Read responsive image sources. | | `engine.block.addImageFileUriToSourceSet(block=_, property="fill/image/sourceSet", uri=_)` | Add an image source after loading its dimensions. | | `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.addVideoFileUriToSourceSet(block=_, property="fill/video/sourceSet", uri=_)` | Add a video source after loading its dimensions. | | `engine.asset.findAllSources()` | List registered asset source IDs. | | `engine.asset.removeSource(sourceId=_)` | Remove a previously registered local asset source. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create a local asset source. | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add an asset definition to a local source. | | `engine.asset.fetchAsset(sourceId=_, assetId=_)` | Fetch the asset result to apply. | | `engine.asset.defaultApplyAsset(asset=_)` | Apply an asset and configure the created block from its payload. | | `engine.editor.setSettingBoolean(keypath="features/forceLowQualityVideoPreview", value=_)` | Force video previews to use the smallest source while editing. | ### Source | Property | Type | Description | | --- | --- | --- | | `uri` | `Uri` | Image or video resource URI. | | `width` | `Int` | Source width in pixels. | | `height` | `Int` | Source height in pixels. | ## Next Steps - [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) - Apply photos and responsive image sources to design blocks. - [Video Fills](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/) - Apply video content and responsive video sources to design blocks. - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Export designs using the best matching source for the target 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: "Insert Media Into Scenes" description: "Understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code." platform: android url: "https://img.ly/docs/cesdk/android/insert-media-a217f5/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/android/insert-media-a217f5/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/insert-media/overview-491658/) - Understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code. - [Insert Images](https://img.ly/docs/cesdk/android/insert-media/images-63848a/) - Add still images to CE.SDK scenes programmatically using Kotlin or using the built-in Android editor UI. Includes positioning, layering, sizing and format considerations. - [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. - [Insert Audio](https://img.ly/docs/cesdk/android/insert-media/audio-c10f10/) - Add audio files to Android video scenes with CE.SDK audio blocks. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/android/insert-media/shapes-or-stickers-20ac68/) - Add vector shapes and sticker assets to CE.SDK scenes on Android with the Engine 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: "Insert Audio" description: "Add audio files to Android video scenes with CE.SDK audio blocks." platform: android url: "https://img.ly/docs/cesdk/android/insert-media/audio-c10f10/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/android/insert-media-a217f5/) > [Insert Audio](https://img.ly/docs/cesdk/android/insert-media/audio-c10f10/) --- ```kotlin file=@cesdk_android_examples/engine-guides-insert-media-audio/InsertMediaAudio.kt reference-only import android.net.Uri import android.util.Log import ly.img.engine.DesignBlockType import ly.img.engine.Engine import kotlin.math.abs private const val TAG = "InsertAudioGuide" data class InsertMediaAudio( val sourceUri: Uri, val sourceDurationSeconds: Double, val timeOffsetSeconds: Double, val durationSeconds: Double, val volume: Float, val muted: Boolean, val unmuted: Boolean, val looping: Boolean, val audioBlockCountBeforeRemoval: Int, val audioBlockCountAfterRemoval: Int, val removedBlockValid: Boolean, ) suspend fun insertMediaAudio( engine: Engine, assetBaseUri: Uri, ): InsertMediaAudio { 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 = 1920F) engine.block.setHeight(block = page, value = 1080F) engine.block.setDuration(block = page, duration = 30.0) val audioBlock = engine.block.create(DesignBlockType.Audio) val audioUri = assetBaseUri .buildUpon() .appendPath("ly.img.audio") .appendPath("audios") .appendPath("far_from_home.m4a") .build() engine.block.setUri(block = audioBlock, property = "audio/fileURI", value = audioUri) engine.block.appendChild(parent = page, child = audioBlock) engine.block.forceLoadAVResource(block = audioBlock) val sourceDuration = engine.block.getAVResourceTotalDuration(block = audioBlock) // This sample uses a 30-second page, so clamp the audio block to that timeline. val playbackDuration = sourceDuration.coerceAtMost(30.0) engine.block.setTimeOffset(block = audioBlock, offset = 0.0) engine.block.setDuration(block = audioBlock, duration = playbackDuration) val timeOffset = engine.block.getTimeOffset(block = audioBlock) val duration = engine.block.getDuration(block = audioBlock) engine.block.setVolume(block = audioBlock, volume = 0.8F) val currentVolume = engine.block.getVolume(block = audioBlock) Log.i(TAG, "Audio volume: ${(currentVolume * 100).toInt()}%") engine.block.setMuted(block = audioBlock, muted = true) val muted = engine.block.isMuted(block = audioBlock) engine.block.setMuted(block = audioBlock, muted = false) val unmuted = !engine.block.isMuted(block = audioBlock) engine.block.setLooping(block = audioBlock, looping = true) val looping = engine.block.isLooping(block = audioBlock) val audioBlocks = engine.block.findByType(DesignBlockType.Audio) audioBlocks.forEach { block -> val uri = engine.block.getUri(block = block, property = "audio/fileURI") val offset = engine.block.getTimeOffset(block = block) val blockDuration = engine.block.getDuration(block = block) val blockVolume = engine.block.getVolume(block = block) Log.i(TAG, "Audio starts at ${offset}s for ${blockDuration}s at ${(blockVolume * 100).toInt()}%: $uri") } engine.block.destroy(block = audioBlock) val removedBlockValid = engine.block.isValid(block = audioBlock) val remainingAudioBlocks = engine.block.findByType(DesignBlockType.Audio) check(abs(timeOffset - 0.0) < 0.001) check(duration > 0.0) check(duration <= 30.0) check(abs(currentVolume - 0.8F) < 0.001F) check(muted) check(unmuted) check(looping) check(audioBlock in audioBlocks) check(!removedBlockValid) check(audioBlock !in remainingAudioBlocks) return InsertMediaAudio( sourceUri = audioUri, sourceDurationSeconds = sourceDuration, timeOffsetSeconds = timeOffset, durationSeconds = duration, volume = currentVolume, muted = muted, unmuted = unmuted, looping = looping, audioBlockCountBeforeRemoval = audioBlocks.size, audioBlockCountAfterRemoval = remainingAudioBlocks.size, removedBlockValid = removedBlockValid, ) } ``` Add audio files to video scenes with CE.SDK audio blocks for background music, sound effects, and voiceovers. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-insert-media-audio) Audio blocks are time-based blocks that play sound alongside the rest of a scene. They have no visual canvas representation; they live on the timeline with their own duration, offset, and volume controls, independent of video fills attached to graphic blocks. This guide covers creating audio blocks, configuring their time-based properties, controlling playback, and managing audio blocks in a scene. For broader audio workflows, see the [Create and Edit Audio overview](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/). For catalog-based music selection and multiple background tracks, continue with [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/). ## Insert an Audio File Create an audio block with `DesignBlockType.Audio`, build or parse a `Uri` for the audio source, then set it with `setUri(block=_, property="audio/fileURI", value=_)` and append the block to a page. Audio blocks must be children of a page to participate in the timeline. ```kotlin highlight-android-create-audio-block val audioBlock = engine.block.create(DesignBlockType.Audio) val audioUri = assetBaseUri .buildUpon() .appendPath("ly.img.audio") .appendPath("audios") .appendPath("far_from_home.m4a") .build() engine.block.setUri(block = audioBlock, property = "audio/fileURI", value = audioUri) engine.block.appendChild(parent = page, child = audioBlock) ``` The sample receives an asset base URI from the app and appends the demo audio path, so the URL stays aligned with the SDK asset package or your self-hosted assets. You can also pass a reachable remote URL or a local URI resolved by your app. Common Android audio sources include M4A, MP3, and WAV files. ## Configuring Time Position Audio blocks have time-based properties that control when and how long they play. Use `setTimeOffset()` for the start position and `setDuration()` for playback length. Call the suspend `forceLoadAVResource()` API first so CE.SDK can read media metadata such as total duration. ```kotlin highlight-android-configure-timeline engine.block.forceLoadAVResource(block = audioBlock) val sourceDuration = engine.block.getAVResourceTotalDuration(block = audioBlock) // This sample uses a 30-second page, so clamp the audio block to that timeline. val playbackDuration = sourceDuration.coerceAtMost(30.0) engine.block.setTimeOffset(block = audioBlock, offset = 0.0) engine.block.setDuration(block = audioBlock, duration = playbackDuration) val timeOffset = engine.block.getTimeOffset(block = audioBlock) val duration = engine.block.getDuration(block = audioBlock) ``` `getAVResourceTotalDuration()` returns the length of the source audio file in seconds. Use it to clamp the playback duration to the available content or to compute timing relative to the file length. ## Adjusting Volume Set the audio level with `setVolume()`. Volume is a `Float` between `0.0` for silent playback and `1.0` for full volume, and it applies to both preview playback and exported video output. ```kotlin highlight-android-adjust-volume engine.block.setVolume(block = audioBlock, volume = 0.8F) val currentVolume = engine.block.getVolume(block = audioBlock) Log.i(TAG, "Audio volume: ${(currentVolume * 100).toInt()}%") ``` Read the current level back with `getVolume()`. For a deeper look at mixing and force-mute behavior, see [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/). ## Muting Audio To silence a block without changing its configured volume, use `setMuted()`. Muting preserves the volume value so you can restore the previous level by setting `muted` back to `false`. ```kotlin highlight-android-mute-audio engine.block.setMuted(block = audioBlock, muted = true) val muted = engine.block.isMuted(block = audioBlock) engine.block.setMuted(block = audioBlock, muted = false) val unmuted = !engine.block.isMuted(block = audioBlock) ``` Read the current state with `isMuted()`. ## Looping Audio Enable continuous playback with `setLooping()`. When looping is enabled, the source repeats until the end of the block's timeline duration is reached. ```kotlin highlight-android-loop-audio engine.block.setLooping(block = audioBlock, looping = true) val looping = engine.block.isLooping(block = audioBlock) ``` Read the current state with `isLooping()`. ## Finding Audio Blocks Use `findByType(DesignBlockType.Audio)` to retrieve every audio block in the scene. This is useful for building audio management interfaces or for batch operations such as adjusting levels across all tracks at once. ```kotlin highlight-android-find-audio-blocks val audioBlocks = engine.block.findByType(DesignBlockType.Audio) audioBlocks.forEach { block -> val uri = engine.block.getUri(block = block, property = "audio/fileURI") val offset = engine.block.getTimeOffset(block = block) val blockDuration = engine.block.getDuration(block = block) val blockVolume = engine.block.getVolume(block = block) Log.i(TAG, "Audio starts at ${offset}s for ${blockDuration}s at ${(blockVolume * 100).toInt()}%: $uri") } ``` For each block, read the source URI with `getUri(block=_, property="audio/fileURI")` and the timeline properties with `getTimeOffset()`, `getDuration()`, and `getVolume()`. ## Removing Audio Call `destroy()` to remove a block from the scene and free its resources. Destroying a block automatically detaches it from its parent and makes the block handle invalid. ```kotlin highlight-android-remove-audio engine.block.destroy(block = audioBlock) val removedBlockValid = engine.block.isValid(block = audioBlock) val remainingAudioBlocks = engine.block.findByType(DesignBlockType.Audio) ``` ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.block.create(blockType=DesignBlockType.Audio)` | Block | Create a new audio block | | `assetBaseUri.buildUpon()` | Android | Build a URI for the audio source | | `engine.block.setUri(block=_, property="audio/fileURI", value=_)` | Block | Set the audio source file | | `engine.block.getUri(block=_, property="audio/fileURI")` | Block | Get the audio source file | | `engine.block.appendChild(parent=_, child=_)` | Block | Add audio to a page | | `engine.block.forceLoadAVResource(block=_)` | Block | Load audio metadata | | `engine.block.getAVResourceTotalDuration(block=_)` | Block | Get total audio file duration in seconds | | `engine.block.setTimeOffset(block=_, offset=_)` | Block | Set timeline start position | | `engine.block.getTimeOffset(block=_)` | Block | Get timeline start position | | `engine.block.setDuration(block=_, duration=_)` | Block | Set playback duration | | `engine.block.getDuration(block=_)` | Block | Get playback duration | | `engine.block.setVolume(block=_, volume=_)` | Block | Set volume from `0.0` to `1.0` | | `engine.block.getVolume(block=_)` | Block | Get current volume | | `engine.block.setMuted(block=_, muted=_)` | Block | Mute or unmute audio | | `engine.block.isMuted(block=_)` | Block | Check if audio is muted | | `engine.block.setLooping(block=_, looping=_)` | Block | Enable or disable looping | | `engine.block.isLooping(block=_)` | Block | Check if looping is enabled | | `engine.block.findByType(type=DesignBlockType.Audio)` | Block | Find all audio blocks | | `engine.block.destroy(block=_)` | Block | Remove an audio block | | `engine.block.isValid(block=_)` | Block | Check whether a block handle is still valid | ## Next Steps - [Adjust Audio Volume](https://img.ly/docs/cesdk/android/create-audio/audio/adjust-volume-7ecc4a/) — Fine-tune audio levels and balance multiple sources - [Loop Audio](https://img.ly/docs/cesdk/android/create-audio/audio/loop-937be7/) — Repeat a source for the duration of its audio block - [Add Sound Effects](https://img.ly/docs/cesdk/android/create-audio/audio/add-sound-effects-9e984e/) — Generate procedural sound effects from PCM-backed audio buffers - [Add Music](https://img.ly/docs/cesdk/android/create-audio/audio/add-music-5b182c/) — Add background music tracks to video projects - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Export scenes with audio to MP4 --- ## 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: "Insert Images" description: "Add still images to CE.SDK scenes programmatically using Kotlin or using the built-in Android editor UI. Includes positioning, layering, sizing and format considerations." platform: android url: "https://img.ly/docs/cesdk/android/insert-media/images-63848a/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/android/insert-media-a217f5/) > [Insert Images](https://img.ly/docs/cesdk/android/insert-media/images-63848a/) --- ```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() } } ``` You can insert images into a scene using CE.SDK, either through the prebuilt UI for Android or programmatically via Kotlin. This gives you the flexibility to build interactive design workflows, enable user-generated content, or automate image placement based on logic or data. > **Note:** CE.SDK supports a wide range of image formats, including:* `.png` > * `.jpeg`, `.jpg` > * `.gif` > * `.webp` > * `.svg` > * `.bmp`See a [full list](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) of supported file types. ## What You'll Learn - Two ways to insert images: - Programmatically by creating a graphic block, applying an image fill, and setting its position/size/rotation/z-index. - With Editor UI using the controls and asset libraries of a prebuilt editor such as the Design Editor or Photo Editor. - Supported image sources such as app assets, file URIs, and remote URLs. - Practical transforms after insertion such as move, scale, rotate and order. ## When to Use It - You're building custom UI or automation flow to add images to compositions. - You want a ready-made editing experience on Android with an image picker and panels. ## Inserting Images Using the UI CE.SDK's UI includes a built-in **image tool** that lets users add images from device sources directly onto the canvas. Once inserted, users can move, resize, crop, rotate, or stack images visually. **Supported image sources:** - Gallery (Photos app) - Storage (Files app) - Camera (device camera) - Image (project asset library) In the Asset Library, a user can add images from the Gallery, the camera or the Files app. You can customize how the image tool appears in the user interface. ## Inserting Images Programmatically For apps with automation, batch workflows, or logic-driven design experiences, you can insert images into a scene using the block API and the graphics engine. Here's how to do it: ```kotlin // 1. Create a graphic block val imageBlock = engine.block.create(DesignBlockType.Graphic) // 2. Create a shape for the image val shape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(imageBlock, shape = shape) // 3. Create an image 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_4.jpg" ) engine.block.setFill(imageBlock, fill = imageFill) // 4. (Optional) Set semantic kind to "image" for clarity engine.block.setKind(imageBlock, kind = "image") // 5. Add image to the scene val page = engine.block.findByType(DesignBlockType.Page).first() engine.block.appendChild(page, child = imageBlock) ``` The `shape` can be any of the supported shapes like `ShapeType.Rect`, `ShapeType.Star`, etc and masks the inserted image. The asset URI in step 3 can either be a remote URL or a local asset URI. For assets from app resources, use a URI: ```kotlin import android.net.Uri val uri = Uri.parse("file:///android_asset/images/poster.jpg") ``` For file assets, use standard Android file handling: ```kotlin import android.net.Uri import java.io.File val file = File(context.filesDir, "uploads/avatar.png") val uri = Uri.fromFile(file) ``` When working with the asset catalog, you can apply an image that's an `AssetResult` either to: - The scene directly - A block ```kotlin val assetList = engine.asset.findAssets(sourceId, queryData) val newAsset = assetList.assets.first() // Creates a new block that contains the image val imageBlock = engine.asset.defaultApplyAsset(newAsset) // Applies the image to a block that already exists engine.asset.defaultApplyAssetToBlock(newAsset, someBlock) ``` ## Image Properties After inserting the image, you can change the block's layout properties using standard methods in the `engine.block` API. ### Positioning Refer to the guide in the Transform Section for [Move](https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/) for more details and other options. ```kotlin // Set X/Y position on the canvas (in absolute units) engine.block.setPositionX(imageBlock, value = 100f) engine.block.setPositionY(imageBlock, value = 200f) ``` ### Scaling Refer to the guide in the Transform Section for [Scale](https://img.ly/docs/cesdk/android/edit-image/transform/scale-ebe367/) for more details and other options. ```kotlin // Uniform scale engine.block.setFloat(imageBlock, property = "transform/scale/x", value = 1.5f) engine.block.setFloat(imageBlock, property = "transform/scale/y", value = 1.5f) // Non-uniform (stretching) engine.block.setFloat(imageBlock, property = "transform/scale/x", value = 2.0f) engine.block.setFloat(imageBlock, property = "transform/scale/y", value = 1.0f) ``` ### Rotation Refer to the guide in the Transform Section for [Rotate](https://img.ly/docs/cesdk/android/edit-image/transform/rotate-5f39c9/) for more details and other options. ```kotlin // Rotate 45 degrees (in radians) import kotlin.math.PI val degrees = 45.0 val radians = (degrees * PI / 180).toFloat() engine.block.setFloat(imageBlock, property = "transform/rotation", value = radians) ``` ### Layering Control stack order using the helper methods to move blocks forward (towards the user) or backwards. You can also pin a block to the front or back of the stack. ```kotlin engine.block.bringToFront(imageBlock) // Move above siblings engine.block.sendToBack(imageBlock) // Move below siblings engine.block.bringForward(imageBlock) // One step forward engine.block.sendBackward(imageBlock) // One step backward engine.block.setAlwaysOnTop(imageBlock, enabled = true) ``` > **Note:** You can also group images and other elements using `engine.block.group()` for easier layer management. ## Insert Into an Existing Block If your template exposes a placeholder block or you are creating an automated workflow, you can **replace an image fill** instead of creating a new block. Locate the block using its `name` property or by its known `id`. When you know the `id` of the target: ```kotlin val fill = engine.block.createFill(FillType.Image) engine.block.setString(fill, property = "fill/image/imageFileURI", value = imageURI) engine.block.setFill(targetBlock, fill = fill) ``` When you're using the `name` property to find the block, you need to search manually: ```kotlin val allBlocks = engine.block.findByType(DesignBlockType.Graphic) val targetBlock = allBlocks.find { block -> engine.block.getString(block, property = "name") == "HeroTile" } ?: return val fill = engine.block.createFill(FillType.Image) engine.block.setString(fill, property = "fill/image/imageFileURI", value = imageURI) engine.block.setFill(targetBlock, fill = fill) ``` > **Note:** When generating templates, assign names so downstream replacement stays straightforward:```kotlin > engine.block.setString(imageBlock, property = "name", value = "HeroImage") > ``` ## Troubleshooting **❌ Nothing appears after insert**: - Verify that the block is attached to the page. - Verify the URI string is correct. - Check the scene's current zoom and camera framing. **❌ Remote images fail**: - Confirm HTTPS and CORS settings. - Verify network permissions in AndroidManifest.xml. - Test the URL in a browser. **❌ Pixelated result**: - Change the block size or use a higher-resolution source image. **❌ Unexpected orientation of image**: - Some formats carry EXIF orientation information. Apply `setRotation` or normalize the asset during import. ## Next Steps Now that you've learned about inserting images into your compositions, here are some topics to explore to deepen your understanding. - Apply more [transformations](https://img.ly/docs/cesdk/android/edit-image/transform-9d189b/) such as crop, or scale. - Create [templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) for automating content creation and formatting. - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) compositions in a variety of 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: "Overview" description: "Understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code." platform: android url: "https://img.ly/docs/cesdk/android/insert-media/overview-491658/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/android/insert-media-a217f5/) > [Overview](https://img.ly/docs/cesdk/android/insert-media/overview-491658/) --- See how inserted media becomes part of a CE.SDK scene on Android: which block types represent each media type, how the editor UI and Engine API place them, and how saved scenes keep references or embedded bytes. Inserting media turns an asset, such as an image, video clip, audio file, shape, or sticker, into a design block in the scene graph. Each block has an ID and belongs to a page or timeline. Visual blocks carry layout and styling properties, while time-based blocks carry timing and playback properties. Both participate in save, reload, and export. Use this overview to build a mental model before moving to the focused Android guides. The Android editor UI gives users a visual insertion surface, while the Engine API lets your app prepare or modify scenes programmatically. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Inserting Media vs. Importing Assets *Importing* registers media with an asset source so it can be searched, browsed, or selected later. Importing alone does not place anything on the canvas. *Inserting* places media into the current scene as a design block. Images, videos, shapes, and stickers usually become `DesignBlockType.Graphic` blocks. Audio becomes a `DesignBlockType.Audio` block that participates in the timeline without a visual layer. The two steps are independent. You can insert media directly from a URI without first showing it in the asset library, and you can import assets for users to browse without inserting them immediately. ## How Media Is Handled in Scenes Inserted media lives in the scene graph. The representation depends on the media type: | Media Type | Android Scene Representation | Notes | | --- | --- | --- | | Images | A `DesignBlockType.Graphic` block with a `FillType.Image` fill | The graphic block controls position, size, rotation, opacity, and stacking. The fill references the image URI. | | Videos | A `DesignBlockType.Graphic` block with a `FillType.Video` fill | The block behaves like visual content on the page or timeline. The fill references the video URI. | | Audio | A `DesignBlockType.Audio` block | Audio has no canvas shape, but it can be arranged on the page timeline and configured with timing and playback properties. | | Shapes | A `DesignBlockType.Graphic` block with a `ShapeType` shape | The shape defines the outline, while the graphic block controls layout and styling. | | Stickers | A `DesignBlockType.Graphic` block, usually with a rect shape and image fill | Stickers often come from an asset source, then behave like other graphic blocks for positioning and ordering. | Each inserted item receives a block ID. Store that ID when your app needs to update, query, duplicate, reorder, or remove the inserted content later. ## Inserting Media ### Insert via the UI The Android editor UI inserts media through asset-library sheets and editor actions. Users select an asset or add action, and the editor creates the right scene block, applies default sizing, and selects the inserted content for further editing. Configure available categories and sources through your editor configuration and asset-library setup. See [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) for details on controlling which media users can browse and insert. For a complete Android editing surface, start from the relevant Starter Kit, such as the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) or [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/). This overview focuses on how inserted content is represented once it is in the scene. ### Insert Programmatically Use the Engine API when your app needs reproducible output, template population, or insertion before the editor opens. Programmatic insertion follows the same high-level sequence for most media: 1. Create the appropriate graphic or audio block. 2. Add the media fill, shape, or source URI that defines its content. 3. Attach the block to a page or timeline and configure its layout, timing, and styling. The focused guides in Next Steps show the exact Android APIs for each media type. ## Referencing Existing Assets Inserted blocks normally point to media by URI. The same URI can back multiple blocks, for example when you place the same logo on several pages. Each inserted block remains independent and keeps its own position, size, rotation, opacity, timing, and effects. When an asset is already available through the asset library, reuse the selected asset result or URI instead of registering duplicate asset entries. Importing controls what users can browse; insertion controls what becomes part of the scene. ## Media Lifecycle Within a Scene Inserted media participates in the regular scene lifecycle: - **Save.** Scene data stores referenced resource URIs. A scene archive includes the accessible media bytes alongside the scene. - **Reload.** Scene data resolves its referenced URIs again. An archive resolves media from its bundled contents. - **Export.** CE.SDK resolves each block's current media and properties when it renders image, PDF, or video output. If a referenced URL or local file no longer resolves, the affected block cannot display or export that media until your app restores the reference or inserts a replacement. Archives reduce that risk by carrying the media bytes with the scene. ## Embedding vs. Linking Media CE.SDK supports two strategies for saved scene media: | Mode | Description | Use Case | | --- | --- | --- | | **Referenced** scene data | The scene stores resource URIs. Media stays where it is hosted or stored. | Smaller saved scenes, shared assets, server-hosted media. | | **Embedded** archives | The archive bundles the scene and accessible media bytes. | Offline editing, portable handoff, scenes that need to reopen without original URLs. | Choose referenced scene data when your app controls stable media hosting. Choose archives when the scene must travel between devices, users, or storage systems without depending on the original media locations. ## Next Steps - [Insert Images](https://img.ly/docs/cesdk/android/insert-media/images-63848a/) — Add image fills to graphic blocks or insert independent image blocks. - [Insert Videos](https://img.ly/docs/cesdk/android/insert-media/videos-a5fa03/) — Add and trim a video block in a scene. - [Insert Audio](https://img.ly/docs/cesdk/android/insert-media/audio-c10f10/) — Add audio files to your video projects programmatically, configure timeline position, volume, and playback properties. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/android/insert-media/shapes-or-stickers-20ac68/) — Add vector shapes and sticker assets to a CE.SDK scene. - [Import Media Overview](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/) — Bring local, remote, or device-sourced assets into CE.SDK so they are available to insert. - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Tailor the categories, sources, and ordering that show up in the asset panel. --- ## 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: "Insert Shapes or Stickers" description: "Add vector shapes and sticker assets to CE.SDK scenes on Android with the Engine API." platform: android url: "https://img.ly/docs/cesdk/android/insert-media/shapes-or-stickers-20ac68/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/android/insert-media-a217f5/) > [Insert Shapes or Stickers](https://img.ly/docs/cesdk/android/insert-media/shapes-or-stickers-20ac68/) --- ```kotlin file=@cesdk_android_examples/engine-guides-insert-media-shapes-or-stickers/InsertMediaShapesOrStickers.kt reference-only import android.net.Uri import android.util.Log import ly.img.editor.defaultBaseUri import ly.img.engine.AssetDefinition import ly.img.engine.Color import ly.img.engine.ContentFillMode import ly.img.engine.DefaultAssetSource 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.populateAssetSource private const val TAG = "ShapesOrStickers" private const val GUIDE_STICKER_SOURCE_ID = "ly.img.guide.sticker" suspend fun insertMediaShapesOrStickers(engine: Engine): InsertMediaShapesOrStickersResult { 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 stickerAssetsBaseUri: Uri = defaultBaseUri val graphicBlock = engine.block.create(DesignBlockType.Graphic) val graphicSupportsShape = engine.block.supportsShape(graphicBlock) Log.i(TAG, "Graphic block supports shapes: $graphicSupportsShape") val textBlock = engine.block.create(DesignBlockType.Text) val textSupportsShape = engine.block.supportsShape(textBlock) Log.i(TAG, "Text block supports shapes: $textSupportsShape") engine.block.destroy(textBlock) engine.block.destroy(graphicBlock) val rectBlock = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block = rectBlock, shape = rectShape) val rectFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = rectFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.2F, g = 0.5F, b = 0.9F, a = 1F), ) engine.block.setFill(block = rectBlock, fill = rectFill) engine.block.setWidth(rectBlock, value = 160F) engine.block.setHeight(rectBlock, value = 140F) engine.block.appendChild(parent = page, child = rectBlock) val roundedBlock = engine.block.create(DesignBlockType.Graphic) val roundedShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block = roundedBlock, shape = roundedShape) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusTL", value = 20F) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusTR", value = 20F) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusBL", value = 20F) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusBR", value = 20F) val roundedFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = roundedFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.9F, g = 0.4F, b = 0.2F, a = 1F), ) engine.block.setFill(block = roundedBlock, fill = roundedFill) engine.block.setWidth(roundedBlock, value = 160F) engine.block.setHeight(roundedBlock, value = 140F) engine.block.appendChild(parent = page, child = roundedBlock) val ellipseBlock = engine.block.create(DesignBlockType.Graphic) val ellipseShape = engine.block.createShape(ShapeType.Ellipse) engine.block.setShape(block = ellipseBlock, shape = ellipseShape) val ellipseFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = ellipseFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.3F, g = 0.8F, b = 0.4F, a = 1F), ) engine.block.setFill(block = ellipseBlock, fill = ellipseFill) engine.block.setWidth(ellipseBlock, value = 160F) engine.block.setHeight(ellipseBlock, value = 140F) engine.block.appendChild(parent = page, child = ellipseBlock) val starBlock = engine.block.create(DesignBlockType.Graphic) val starShape = engine.block.createShape(ShapeType.Star) engine.block.setShape(block = starBlock, shape = starShape) engine.block.setInt(starShape, property = "shape/star/points", value = 5) engine.block.setFloat(starShape, property = "shape/star/innerDiameter", value = 0.4F) val starFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = starFill, property = "fill/color/value", value = Color.fromRGBA(r = 1F, g = 0.8F, b = 0F, a = 1F), ) engine.block.setFill(block = starBlock, fill = starFill) engine.block.setWidth(starBlock, value = 160F) engine.block.setHeight(starBlock, value = 140F) engine.block.appendChild(parent = page, child = starBlock) val polygonBlock = engine.block.create(DesignBlockType.Graphic) val polygonShape = engine.block.createShape(ShapeType.Polygon) engine.block.setShape(block = polygonBlock, shape = polygonShape) engine.block.setInt(polygonShape, property = "shape/polygon/sides", value = 6) val polygonFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = polygonFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.6F, g = 0.2F, b = 0.8F, a = 1F), ) engine.block.setFill(block = polygonBlock, fill = polygonFill) engine.block.setWidth(polygonBlock, value = 160F) engine.block.setHeight(polygonBlock, value = 140F) engine.block.appendChild(parent = page, child = polygonBlock) val lineBlock = engine.block.create(DesignBlockType.Graphic) val lineShape = engine.block.createShape(ShapeType.Line) engine.block.setShape(block = lineBlock, shape = lineShape) engine.block.setStrokeEnabled(lineBlock, enabled = true) engine.block.setStrokeWidth(lineBlock, width = 6F) engine.block.setStrokeColor( block = lineBlock, color = Color.fromRGBA(r = 0.9F, g = 0.2F, b = 0.5F, a = 1F), ) engine.block.setWidth(lineBlock, value = 160F) // Line shapes use block height as the visible stroke thickness. engine.block.setHeight(lineBlock, value = 6F) engine.block.appendChild(parent = page, child = lineBlock) val vectorPathBlock = engine.block.create(DesignBlockType.Graphic) val vectorPathShape = engine.block.createShape(ShapeType.VectorPath) engine.block.setShape(block = vectorPathBlock, shape = vectorPathShape) val trianglePath = "M 50,0 L 100,100 L 0,100 Z" engine.block.setString( block = vectorPathShape, property = "shape/vector_path/path", value = trianglePath, ) val vectorPathFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = vectorPathFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.2F, g = 0.7F, b = 0.7F, a = 1F), ) engine.block.setFill(block = vectorPathBlock, fill = vectorPathFill) engine.block.setWidth(vectorPathBlock, value = 160F) engine.block.setHeight(vectorPathBlock, value = 140F) engine.block.appendChild(parent = page, child = vectorPathBlock) val starProperties = engine.block.findAllProperties(starShape) Log.i(TAG, "Star shape properties: $starProperties") val stickerBlock = engine.block.create(DesignBlockType.Graphic) val stickerShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block = stickerBlock, shape = stickerShape) val stickerFill = engine.block.createFill(FillType.Image) val stickerUri = stickerAssetsBaseUri.buildUpon() .appendPath(DefaultAssetSource.STICKER.key) .appendPath("images") .appendPath("emoji") .appendPath("emoji_happyface.svg") .build() engine.block.setUri( block = stickerFill, property = "fill/image/imageFileURI", value = stickerUri, ) engine.block.setFill(block = stickerBlock, fill = stickerFill) if (engine.block.supportsContentFillMode(stickerBlock)) { engine.block.setContentFillMode(stickerBlock, mode = ContentFillMode.CONTAIN) } engine.block.setKind(stickerBlock, kind = "sticker") engine.block.setWidth(stickerBlock, value = 160F) engine.block.setHeight(stickerBlock, value = 140F) engine.block.appendChild(parent = page, child = stickerBlock) val stickerSourceId = DefaultAssetSource.STICKER.key if (stickerSourceId !in engine.asset.findAllSources()) { engine.populateAssetSource( id = stickerSourceId, jsonUri = stickerAssetsBaseUri.buildUpon() .appendPath(stickerSourceId) .appendPath("content.json") .build(), replaceBaseUri = stickerAssetsBaseUri, ) } var stickerResults = findStickerAssets(engine = engine, sourceId = stickerSourceId) var stickerAssetSourceId = stickerSourceId if (stickerResults.assets.isEmpty()) { try { engine.populateAssetSource( id = stickerSourceId, jsonUri = stickerAssetsBaseUri.buildUpon() .appendPath(stickerSourceId) .appendPath("content.json") .build(), replaceBaseUri = stickerAssetsBaseUri, ) } catch (error: Exception) { Log.w(TAG, "Could not populate sticker source $stickerSourceId.", error) } stickerResults = findStickerAssets(engine = engine, sourceId = stickerSourceId) } if (stickerResults.assets.isEmpty()) { stickerAssetSourceId = GUIDE_STICKER_SOURCE_ID ensureGuideStickerAssetSource(engine = engine, stickerUri = stickerUri) stickerResults = findStickerAssets(engine = engine, sourceId = stickerAssetSourceId) } Log.i(TAG, "Stickers in emoji category: ${stickerResults.total}") val stickerAsset = checkNotNull(stickerResults.assets.firstOrNull()) { "No sticker assets found." } val stickerFromLibrary = checkNotNull( engine.asset.applyAssetSourceAsset(sourceId = stickerAssetSourceId, asset = stickerAsset), ) { "The sticker asset source did not create a block." } if (engine.block.supportsContentFillMode(stickerFromLibrary)) { engine.block.setContentFillMode(stickerFromLibrary, mode = ContentFillMode.CONTAIN) } engine.block.setWidth(stickerFromLibrary, value = 160F) engine.block.setHeight(stickerFromLibrary, value = 140F) val shapeBlocks = listOf( rectBlock, roundedBlock, ellipseBlock, starBlock, polygonBlock, lineBlock, vectorPathBlock, ) val stickerBlocks = listOf(stickerBlock, stickerFromLibrary) val gridBlocks = shapeBlocks + stickerBlocks gridBlocks.forEachIndexed { index, block -> val column = index % 3 val row = index / 3 engine.block.setPositionX(block, value = 130F + column * 190F) engine.block.setPositionY(block, value = 60F + row * 170F) } engine.block.forceLoadResources(blocks = listOf(page) + gridBlocks) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) check(pngData.hasRemaining()) { "shapes and stickers PNG export is empty" } return InsertMediaShapesOrStickersResult( graphicSupportsShape = graphicSupportsShape, textSupportsShape = textSupportsShape, starPoints = engine.block.getInt(starShape, property = "shape/star/points"), starInnerDiameter = engine.block.getFloat(starShape, property = "shape/star/innerDiameter"), polygonSides = engine.block.getInt(polygonShape, property = "shape/polygon/sides"), vectorPath = engine.block.getString(vectorPathShape, property = "shape/vector_path/path"), starPropertyCount = starProperties.size, stickerResultCount = stickerResults.total, stickerBlockCount = stickerBlocks.size, pngData = pngData.asReadOnlyBuffer(), ) } private suspend fun findStickerAssets( engine: Engine, sourceId: String, ) = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery( query = null, page = 0, groups = listOf("emoji"), perPage = 5, ), ) private suspend fun ensureGuideStickerAssetSource( engine: Engine, stickerUri: Uri, ) { if (GUIDE_STICKER_SOURCE_ID !in engine.asset.findAllSources()) { engine.asset.addLocalSource( sourceId = GUIDE_STICKER_SOURCE_ID, supportedMimeTypes = listOf("image/svg+xml"), ) } if (findStickerAssets(engine = engine, sourceId = GUIDE_STICKER_SOURCE_ID).assets.isNotEmpty()) { return } engine.asset.addAsset( sourceId = GUIDE_STICKER_SOURCE_ID, asset = AssetDefinition( id = "guide-emoji-happyface", label = mapOf("en" to "Happy Face"), tags = mapOf("en" to listOf("emoji", "happy", "face")), groups = listOf("emoji"), meta = mapOf( "uri" to stickerUri.toString(), "thumbUri" to stickerUri.toString(), "mimeType" to "image/svg+xml", "kind" to "sticker", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "width" to "512", "height" to "512", ), ), ) engine.asset.assetSourceContentsChanged(sourceId = GUIDE_STICKER_SOURCE_ID) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-insert-media-shapes-or-stickers/InsertMediaShapesOrStickersResult.kt reference-only import java.nio.ByteBuffer data class InsertMediaShapesOrStickersResult( val graphicSupportsShape: Boolean, val textSupportsShape: Boolean, val starPoints: Int, val starInnerDiameter: Float, val polygonSides: Int, val vectorPath: String, val starPropertyCount: Int, val stickerResultCount: Int, val stickerBlockCount: Int, val pngData: ByteBuffer, ) ``` Add vector shapes and sticker assets to a CE.SDK scene on Android. Use the Engine API to create shape blocks, style them with fills or strokes, and materialize stickers from an asset source. ![A grid of rectangles, rounded rectangles, ellipses, stars, polygons, lines, vector paths, and sticker graphics.](./assets/android.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-insert-media-shapes-or-stickers) Shapes are vector graphics created with `engine.block.createShape()` and attached to graphic blocks. CE.SDK supports six Android shape types: `ShapeType.Rect`, `ShapeType.Ellipse`, `ShapeType.Star`, `ShapeType.Polygon`, `ShapeType.Line`, and `ShapeType.VectorPath`. Stickers are graphic blocks that usually use an image fill and the `sticker` block kind. > **Note:** For a complete editing surface that exposes shapes and stickers through the asset library, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). This guide focuses on inserting the same kinds of content with the Android Engine API.For focused deep dives, see [Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/) and [Create Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-stickers-cc46e5/). This guide covers creating each shape type, configuring shape-specific properties, applying fills, inserting stickers by hand, and querying stickers from the asset library. ## Using the Built-in UI ### Shapes Panel The CE.SDK editor UI can expose shape entries through the asset library when your Android editor configuration includes a shape source such as `ly.img.vector.shape`. Users can place a shape on the canvas from the panel, then resize, rotate, position, and style the resulting block with the editor controls. ### Stickers Panel Sticker entries appear in the asset library under their configured categories when your Android editor configuration includes a sticker source such as `ly.img.sticker`. Users can browse the available stickers and add them to the canvas from the panel. Inserted stickers keep their graphic-block behavior, so users can resize, rotate, align, and style them like other design elements. ## Programmatic Shape Creation ### Prepare the Page and Asset Base URI The snippets below create a scene page for the inserted blocks and resolve sticker assets from a base URI. Use the asset base URI configured for your editor, or replace it with a self-hosted asset bundle URI that contains the `ly.img.sticker` source. ```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) val stickerAssetsBaseUri: Uri = defaultBaseUri ``` ### Check Shape Support Before attaching a shape to a block, confirm the block supports shapes with `supportsShape()`. Graphic blocks return `true`; text blocks return `false`. ```kotlin highlight-android-check-shape-support val graphicBlock = engine.block.create(DesignBlockType.Graphic) val graphicSupportsShape = engine.block.supportsShape(graphicBlock) Log.i(TAG, "Graphic block supports shapes: $graphicSupportsShape") val textBlock = engine.block.create(DesignBlockType.Text) val textSupportsShape = engine.block.supportsShape(textBlock) Log.i(TAG, "Text block supports shapes: $textSupportsShape") engine.block.destroy(textBlock) engine.block.destroy(graphicBlock) ``` ### Create Rectangle Create rectangles with `createShape(ShapeType.Rect)` and attach them to a graphic block with `setShape()`. Apply a color fill to make the shape visible. ```kotlin highlight-android-create-rectangle val rectBlock = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block = rectBlock, shape = rectShape) val rectFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = rectFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.2F, g = 0.5F, b = 0.9F, a = 1F), ) engine.block.setFill(block = rectBlock, fill = rectFill) engine.block.setWidth(rectBlock, value = 160F) engine.block.setHeight(rectBlock, value = 140F) engine.block.appendChild(parent = page, child = rectBlock) ``` A graphic block needs both a shape and a fill before it renders as a visible filled shape. ### Create Rounded Rectangle Rectangles support per-corner radii. Set each corner individually with `setFloat()` using `shape/rect/cornerRadiusTL`, `shape/rect/cornerRadiusTR`, `shape/rect/cornerRadiusBL`, and `shape/rect/cornerRadiusBR`. ```kotlin highlight-android-create-rounded-rectangle val roundedBlock = engine.block.create(DesignBlockType.Graphic) val roundedShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block = roundedBlock, shape = roundedShape) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusTL", value = 20F) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusTR", value = 20F) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusBL", value = 20F) engine.block.setFloat(roundedShape, property = "shape/rect/cornerRadiusBR", value = 20F) val roundedFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = roundedFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.9F, g = 0.4F, b = 0.2F, a = 1F), ) engine.block.setFill(block = roundedBlock, fill = roundedFill) engine.block.setWidth(roundedBlock, value = 160F) engine.block.setHeight(roundedBlock, value = 140F) engine.block.appendChild(parent = page, child = roundedBlock) ``` Use the same value for every corner to create a uniformly rounded rectangle, or use different values for asymmetric rounding. ### Create Ellipse Create circles and ovals with `createShape(ShapeType.Ellipse)`. The block's width and height determine the rendered shape: equal values render a circle, and unequal values render an oval. ```kotlin highlight-android-create-ellipse val ellipseBlock = engine.block.create(DesignBlockType.Graphic) val ellipseShape = engine.block.createShape(ShapeType.Ellipse) engine.block.setShape(block = ellipseBlock, shape = ellipseShape) val ellipseFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = ellipseFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.3F, g = 0.8F, b = 0.4F, a = 1F), ) engine.block.setFill(block = ellipseBlock, fill = ellipseFill) engine.block.setWidth(ellipseBlock, value = 160F) engine.block.setHeight(ellipseBlock, value = 140F) engine.block.appendChild(parent = page, child = ellipseBlock) ``` ### Create Star Create stars with `createShape(ShapeType.Star)`. Configure the point count with `shape/star/points` and the inner-to-outer diameter ratio with `shape/star/innerDiameter`. ```kotlin highlight-android-create-star val starBlock = engine.block.create(DesignBlockType.Graphic) val starShape = engine.block.createShape(ShapeType.Star) engine.block.setShape(block = starBlock, shape = starShape) engine.block.setInt(starShape, property = "shape/star/points", value = 5) engine.block.setFloat(starShape, property = "shape/star/innerDiameter", value = 0.4F) val starFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = starFill, property = "fill/color/value", value = Color.fromRGBA(r = 1F, g = 0.8F, b = 0F, a = 1F), ) engine.block.setFill(block = starBlock, fill = starFill) engine.block.setWidth(starBlock, value = 160F) engine.block.setHeight(starBlock, value = 140F) engine.block.appendChild(parent = page, child = starBlock) ``` Lower `innerDiameter` values produce thinner points, while higher values move the inner vertices closer to the outer radius. ### Create Polygon Create regular polygons with `createShape(ShapeType.Polygon)`. Set the number of sides with `shape/polygon/sides` to render triangles (`3`), pentagons (`5`), hexagons (`6`), and beyond. ```kotlin highlight-android-create-polygon val polygonBlock = engine.block.create(DesignBlockType.Graphic) val polygonShape = engine.block.createShape(ShapeType.Polygon) engine.block.setShape(block = polygonBlock, shape = polygonShape) engine.block.setInt(polygonShape, property = "shape/polygon/sides", value = 6) val polygonFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = polygonFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.6F, g = 0.2F, b = 0.8F, a = 1F), ) engine.block.setFill(block = polygonBlock, fill = polygonFill) engine.block.setWidth(polygonBlock, value = 160F) engine.block.setHeight(polygonBlock, value = 140F) engine.block.appendChild(parent = page, child = polygonBlock) ``` ### Create Line Create lines with `createShape(ShapeType.Line)`. Lines render through their stroke, so enable the stroke, set the color with `setStrokeColor()`, and set the thickness with `setStrokeWidth()`. ```kotlin highlight-android-create-line val lineBlock = engine.block.create(DesignBlockType.Graphic) val lineShape = engine.block.createShape(ShapeType.Line) engine.block.setShape(block = lineBlock, shape = lineShape) engine.block.setStrokeEnabled(lineBlock, enabled = true) engine.block.setStrokeWidth(lineBlock, width = 6F) engine.block.setStrokeColor( block = lineBlock, color = Color.fromRGBA(r = 0.9F, g = 0.2F, b = 0.5F, a = 1F), ) engine.block.setWidth(lineBlock, value = 160F) // Line shapes use block height as the visible stroke thickness. engine.block.setHeight(lineBlock, value = 6F) engine.block.appendChild(parent = page, child = lineBlock) ``` The sample sets the block height to the same value as the stroke width so the line's bounding box matches the visible stroke. ### Create Vector Path Create custom shapes with `createShape(ShapeType.VectorPath)`. Set the SVG path with `setString()` using the `shape/vector_path/path` property. Path coordinates scale proportionally with the block's width and height. ```kotlin highlight-android-create-vector-path val vectorPathBlock = engine.block.create(DesignBlockType.Graphic) val vectorPathShape = engine.block.createShape(ShapeType.VectorPath) engine.block.setShape(block = vectorPathBlock, shape = vectorPathShape) val trianglePath = "M 50,0 L 100,100 L 0,100 Z" engine.block.setString( block = vectorPathShape, property = "shape/vector_path/path", value = trianglePath, ) val vectorPathFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = vectorPathFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.2F, g = 0.7F, b = 0.7F, a = 1F), ) engine.block.setFill(block = vectorPathBlock, fill = vectorPathFill) engine.block.setWidth(vectorPathBlock, value = 160F) engine.block.setHeight(vectorPathBlock, value = 140F) engine.block.appendChild(parent = page, child = vectorPathBlock) ``` The example draws a triangle by moving to the top center, then to the bottom-right and bottom-left corners. CE.SDK accepts the SVG path subset supported by its parser; validate complex paths before shipping them. ### Discover Shape Properties Use `findAllProperties()` to list every configurable property for a given shape. This helps when exploring an unfamiliar shape type or building generic editing tools. ```kotlin highlight-android-discover-shape-properties val starProperties = engine.block.findAllProperties(starShape) Log.i(TAG, "Star shape properties: $starProperties") ``` Each shape type exposes its own set of properties: - **Rectangle**: `shape/rect/cornerRadiusTL`, `shape/rect/cornerRadiusTR`, `shape/rect/cornerRadiusBL`, `shape/rect/cornerRadiusBR` - **Star**: `shape/star/points`, `shape/star/innerDiameter`, `shape/star/cornerRadius` - **Polygon**: `shape/polygon/sides`, `shape/polygon/cornerRadius` - **Vector Path**: `shape/vector_path/path`, `shape/vector_path/width`, `shape/vector_path/height`, `shape/vector_path/cornerRadius`, `shape/vector_path/fillRule` ## Programmatic Sticker Insertion ### Insert a Sticker A sticker is a graphic block with a rect shape and an image fill. Set the fill's `fill/image/imageFileURI` property to the sticker URI, then tag the block as a sticker with `setKind()` so the editor categorizes it correctly. ```kotlin highlight-android-sticker-manual-construction val stickerBlock = engine.block.create(DesignBlockType.Graphic) val stickerShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block = stickerBlock, shape = stickerShape) val stickerFill = engine.block.createFill(FillType.Image) val stickerUri = stickerAssetsBaseUri.buildUpon() .appendPath(DefaultAssetSource.STICKER.key) .appendPath("images") .appendPath("emoji") .appendPath("emoji_happyface.svg") .build() engine.block.setUri( block = stickerFill, property = "fill/image/imageFileURI", value = stickerUri, ) engine.block.setFill(block = stickerBlock, fill = stickerFill) if (engine.block.supportsContentFillMode(stickerBlock)) { engine.block.setContentFillMode(stickerBlock, mode = ContentFillMode.CONTAIN) } engine.block.setKind(stickerBlock, kind = "sticker") engine.block.setWidth(stickerBlock, value = 160F) engine.block.setHeight(stickerBlock, value = 140F) engine.block.appendChild(parent = page, child = stickerBlock) ``` `setContentFillMode()` with `ContentFillMode.CONTAIN` preserves the sticker's aspect ratio inside the block bounds. Guard the call with `supportsContentFillMode()` because not every block configuration exposes content fill modes. ### Query Stickers From the Asset Library Populate the sticker asset source with the `ly.img.sticker` manifest when it is not already registered. Once the source is available, browse the catalog with `findAssets()`. ```kotlin highlight-android-query-stickers val stickerSourceId = DefaultAssetSource.STICKER.key if (stickerSourceId !in engine.asset.findAllSources()) { engine.populateAssetSource( id = stickerSourceId, jsonUri = stickerAssetsBaseUri.buildUpon() .appendPath(stickerSourceId) .appendPath("content.json") .build(), replaceBaseUri = stickerAssetsBaseUri, ) } var stickerResults = findStickerAssets(engine = engine, sourceId = stickerSourceId) var stickerAssetSourceId = stickerSourceId if (stickerResults.assets.isEmpty()) { try { engine.populateAssetSource( id = stickerSourceId, jsonUri = stickerAssetsBaseUri.buildUpon() .appendPath(stickerSourceId) .appendPath("content.json") .build(), replaceBaseUri = stickerAssetsBaseUri, ) } catch (error: Exception) { Log.w(TAG, "Could not populate sticker source $stickerSourceId.", error) } stickerResults = findStickerAssets(engine = engine, sourceId = stickerSourceId) } if (stickerResults.assets.isEmpty()) { stickerAssetSourceId = GUIDE_STICKER_SOURCE_ID ensureGuideStickerAssetSource(engine = engine, stickerUri = stickerUri) stickerResults = findStickerAssets(engine = engine, sourceId = stickerAssetSourceId) } Log.i(TAG, "Stickers in emoji category: ${stickerResults.total}") ``` The `query` parameter accepts a fuzzy search string; `groups` narrows the result to a single category. Omit `query` to remove the fuzzy-search filter for the selected group, or omit `groups` to retrieve stickers from the full source. For vector-based shape assets, use `"ly.img.vector.shape"` as the source ID. ### Apply a Sticker From the Library `applyAssetSourceAsset()` returns a nullable block. A non-null result is already attached to the current scene and includes metadata such as shape, image fill, kind, and dimensions. Check for `null` before resizing or positioning the block because an asset source can decline to materialize a specific asset. ```kotlin highlight-android-apply-sticker val stickerAsset = checkNotNull(stickerResults.assets.firstOrNull()) { "No sticker assets found." } val stickerFromLibrary = checkNotNull( engine.asset.applyAssetSourceAsset(sourceId = stickerAssetSourceId, asset = stickerAsset), ) { "The sticker asset source did not create a block." } if (engine.block.supportsContentFillMode(stickerFromLibrary)) { engine.block.setContentFillMode(stickerFromLibrary, mode = ContentFillMode.CONTAIN) } engine.block.setWidth(stickerFromLibrary, value = 160F) engine.block.setHeight(stickerFromLibrary, value = 140F) ``` This is the recommended path when the sticker URL comes from the asset library. Manual construction is useful when your app already knows the sticker URI. ## Troubleshooting ### Shape Not Visible If a shape does not appear after creation: - **Verify a fill is applied.** Shapes without fills are invisible. Create a fill with `createFill()` and apply it with `setFill()`. - **Check the block is added to the page.** Call `appendChild()` to attach the block to the scene hierarchy. - **Ensure dimensions are set.** Call `setWidth()` and `setHeight()` to give the shape a size. ### Line Not Visible Lines render through their stroke, not a fill. Enable the stroke with `setStrokeEnabled()` and set a stroke width before expecting the line to appear. ### Sticker Appears Cropped Set `setContentFillMode()` to `ContentFillMode.CONTAIN` to preserve the sticker's aspect ratio inside the block bounds. Check `supportsContentFillMode()` before calling it. ### Invalid Shape Type Use the Android `ShapeType` values instead of deprecated string-based shape creation. Valid values are `ShapeType.Rect`, `ShapeType.Ellipse`, `ShapeType.Star`, `ShapeType.Polygon`, `ShapeType.Line`, and `ShapeType.VectorPath`. ### No Assets Found Confirm that the expected source ID appears in `findAllSources()` and that its `content.json` URI is reachable. If you self-host the bundle, pass the corresponding base URI when populating the source. ### Asset Does Not Create a Block Treat the return value of `applyAssetSourceAsset()` as nullable. A `null` result means the source did not create a scene block for the selected asset. ## API Reference | Method | Description | | --- | --- | | `engine.scene.create()` | Create a scene that owns the page and inserted blocks | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a graphic block to host a shape or sticker | | `engine.block.createShape(type=ShapeType.Rect)` | Create a shape of the specified `ShapeType` | | `engine.block.supportsShape(block=_)` | Check whether a block supports shapes | | `engine.block.setShape(block=_, shape=_)` | Attach a shape to a graphic block | | `engine.block.findAllProperties(block=_)` | List every configurable property of a block | | `engine.block.setInt(block=_, property="shape/star/points", value=_)` | Set an integer property, such as star points or polygon sides | | `engine.block.setFloat(block=_, property="shape/star/innerDiameter", value=_)` | Set a float property, such as corner radius or star inner diameter | | `engine.block.setString(block=_, property="shape/vector_path/path", value=_)` | Set a string property, such as an SVG vector path | | `engine.block.createFill(fillType=FillType.Color)` | Create a fill of the specified `FillType` | | `engine.block.setFill(block=_, fill=_)` | Apply a fill to a block | | `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set a color property on a fill | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set a URI property on an image fill | | `engine.block.setKind(block=_, kind=_)` | Set a block's kind for categorization | | `engine.block.setContentFillMode(block=_, mode=ContentFillMode.CONTAIN)` | Set how image content fits within the block bounds | | `engine.block.supportsContentFillMode(block=_)` | Check whether the block supports content fill modes | | `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable a block's stroke | | `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke thickness | | `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color | | `engine.block.setWidth(block=_, value=_)` | Set the block width | | `engine.block.setHeight(block=_, value=_)` | Set the block height | | `engine.block.appendChild(parent=_, child=_)` | Attach a block to a parent | | `engine.block.destroy(block=_)` | Destroy a temporary or no longer needed block | | `engine.populateAssetSource(id=_, jsonUri=_, replaceBaseUri=_)` | Populate a default asset source from a `content.json` URI | | `engine.asset.findAllSources()` | List registered asset source IDs before populating a source | | `engine.asset.findAssets(sourceId=_, query=_)` | Query assets from a registered source | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Materialize a nullable block from an asset result | ## Next Steps - [Asset Library Basics](https://img.ly/docs/cesdk/android/import-media/asset-library/basics-f29078/) — Understand sources, groups, and asset metadata - [Customize Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Control which sources and categories users can browse - [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) - Work with colors, fills, and gradients - [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) - Apply visual effects to design elements - [Position and Align](https://img.ly/docs/cesdk/android/create-composition/position-and-align-cc6b6a/) - Position elements precisely on the canvas --- ## 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: "Insert Videos" description: "Insert video graphic blocks into Android CE.SDK scenes and configure their source, trim, position, and size." platform: android url: "https://img.ly/docs/cesdk/android/insert-media/videos-a5fa03/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/android/insert-media-a217f5/) > [Insert Videos](https://img.ly/docs/cesdk/android/insert-media/videos-a5fa03/) --- ```kotlin file=@cesdk_android_examples/engine-guides-insert-media-videos/InsertMediaVideos.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 kotlin.math.abs suspend fun insertMediaVideos(engine: Engine): InsertMediaVideosResult { 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 videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4") val videoBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = videoBlock, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(block = videoBlock, fill = videoFill) engine.block.appendChild(parent = page, child = videoBlock) engine.block.setWidth(block = videoBlock, value = 720F) engine.block.setHeight(block = videoBlock, value = 405F) engine.block.setPositionX(block = videoBlock, value = 280F) engine.block.setPositionY(block = videoBlock, value = 150F) engine.block.setContentFillMode(block = videoBlock, mode = ContentFillMode.COVER) engine.block.forceLoadAVResource(block = videoFill) val sourceDuration = engine.block.getAVResourceTotalDuration(block = videoFill) val trimOffset = 2.0 require(sourceDuration > trimOffset) { "The source video must be longer than the trim offset." } // Keep the demo clip short while clamping to the loaded source duration. val trimLength = (sourceDuration - trimOffset).coerceAtMost(5.0).coerceAtLeast(0.1) engine.block.setTrimOffset(block = videoFill, offset = trimOffset) engine.block.setTrimLength(block = videoFill, length = trimLength) engine.block.setDuration(block = videoBlock, duration = trimLength) val fillType = engine.block.getType(videoFill) val actualTrimOffset = engine.block.getTrimOffset(videoFill) val actualTrimLength = engine.block.getTrimLength(videoFill) val blockDuration = engine.block.getDuration(videoBlock) val videoBlocks = engine.block.findByType(DesignBlockType.Graphic) .filter { block -> runCatching { val fill = engine.block.getFill(block) engine.block.getType(fill) == FillType.Video.key }.getOrDefault(false) } val currentFill = engine.block.getFill(videoBlocks.first()) val currentVideoUri = engine.block.getUri( block = currentFill, property = "fill/video/fileURI", ) check(currentVideoUri == videoUri) check(fillType == FillType.Video.key) check(abs(actualTrimOffset - trimOffset) < 0.001) check(abs(actualTrimLength - trimLength) < 0.001) check(abs(blockDuration - trimLength) < 0.001) val exportedImage = engine.block.export(block = page, mimeType = MimeType.PNG) engine.block.destroy(block = videoBlocks.first()) val remainingVideoBlocks = engine.block.findByType(DesignBlockType.Graphic) .filter { block -> runCatching { val fill = engine.block.getFill(block) engine.block.getType(fill) == FillType.Video.key }.getOrDefault(false) } check(exportedImage.hasRemaining()) { "The exported video preview is empty." } return InsertMediaVideosResult( videoUri = currentVideoUri, fillType = fillType, sourceDuration = sourceDuration, trimOffset = actualTrimOffset, trimLength = actualTrimLength, blockDuration = blockDuration, videoBlockCount = videoBlocks.size, remainingVideoBlockCount = remainingVideoBlocks.size, exportedImage = exportedImage.asReadOnlyBuffer(), ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-insert-media-videos/InsertMediaVideosResult.kt reference-only import android.net.Uri import java.nio.ByteBuffer data class InsertMediaVideosResult( val videoUri: Uri, val fillType: String, val sourceDuration: Double, val trimOffset: Double, val trimLength: Double, val blockDuration: Double, val videoBlockCount: Int, val remainingVideoBlockCount: Int, val exportedImage: ByteBuffer, ) ``` Add videos to CE.SDK scenes on Android by creating a graphic block, attaching a video fill, and configuring its source, trim range, size, and position. ![Android insert videos preview showing the exported scene with a video block on the page](./assets/android.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-insert-media-videos) Videos in CE.SDK are graphic blocks with a video fill. The graphic block defines the scene placement and shape; the video fill stores the source URI and trim metadata. For a complete timeline editor UI, use the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/). This guide focuses on CreativeEngine APIs for inserting and configuring video blocks programmatically. ## Creating a Video Block Create a graphic block with `create()`, attach a rectangular shape with `setShape()`, then create a video fill with `createFill()`. Set the source with `setUri()` using the `fill/video/fileURI` property and append the block to the page. ```kotlin highlight-android-create-video-block val videoBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = videoBlock, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(block = videoBlock, fill = videoFill) engine.block.appendChild(parent = page, child = videoBlock) ``` Use a remote, local, bundled, or app-provided `Uri` as the source. MP4 with H.264 encoding is the safest default across Android devices; other codecs depend on the target device's media support. ## Positioning and Sizing Set width and height on the graphic block, then move it with `setPositionX()` and `setPositionY()`. Use a content fill mode when you need predictable framing inside the block's rectangle. ```kotlin highlight-android-position-and-size engine.block.setWidth(block = videoBlock, value = 720F) engine.block.setHeight(block = videoBlock, value = 405F) engine.block.setPositionX(block = videoBlock, value = 280F) engine.block.setPositionY(block = videoBlock, value = 150F) engine.block.setContentFillMode(block = videoBlock, mode = ContentFillMode.COVER) ``` All values use the scene's design unit. Positive X moves the frame right, and positive Y moves it down from the page's top-left origin. ## Configuring Trim Load the video fill before reading duration or applying trim values. Trim is set on the fill, while the graphic block's duration controls how long the block stays active on the timeline in a video scene. ```kotlin highlight-android-configure-trim engine.block.forceLoadAVResource(block = videoFill) val sourceDuration = engine.block.getAVResourceTotalDuration(block = videoFill) val trimOffset = 2.0 require(sourceDuration > trimOffset) { "The source video must be longer than the trim offset." } // Keep the demo clip short while clamping to the loaded source duration. val trimLength = (sourceDuration - trimOffset).coerceAtMost(5.0).coerceAtLeast(0.1) engine.block.setTrimOffset(block = videoFill, offset = trimOffset) engine.block.setTrimLength(block = videoFill, length = trimLength) engine.block.setDuration(block = videoBlock, duration = trimLength) ``` `setTrimOffset()` skips into the source media, and `setTrimLength()` controls how much of that source plays from the offset. Matching `setDuration()` to the trim length keeps the timeline block visible for the same time span as the trimmed content. ## Finding Video Blocks Find graphic blocks and filter them by the type of their attached fill. This is useful when your app needs to inspect existing scene content or build a custom media list. ```kotlin highlight-android-find-video-blocks val videoBlocks = engine.block.findByType(DesignBlockType.Graphic) .filter { block -> runCatching { val fill = engine.block.getFill(block) engine.block.getType(fill) == FillType.Video.key }.getOrDefault(false) } val currentFill = engine.block.getFill(videoBlocks.first()) val currentVideoUri = engine.block.getUri( block = currentFill, property = "fill/video/fileURI", ) ``` `getType()` returns the Engine type key for the fill. Compare it with `FillType.Video.key`, then read `fill/video/fileURI` from the fill with `getUri()`. ## Removing Videos Destroy the graphic block when you want to remove the inserted video from the scene. The block is detached from its parent page and the attached video fill is released with it. ```kotlin highlight-android-remove-video engine.block.destroy(block = videoBlocks.first()) val remainingVideoBlocks = engine.block.findByType(DesignBlockType.Graphic) .filter { block -> runCatching { val fill = engine.block.getFill(block) engine.block.getType(fill) == FillType.Video.key }.getOrDefault(false) } ``` ## Troubleshooting ### Video Not Visible - Verify that the `Uri` is reachable from the app, or resolve it with your app's URI resolver before assigning `fill/video/fileURI`. - Use a device-supported video format. MP4 with H.264 is the safest Android default, but codec support still depends on the target device. - Make sure the graphic block is appended to a page and has non-zero width and height. ### Trim Metadata Not Available - Call `forceLoadAVResource()` on the video fill before reading duration or clamping trim values. - Apply `setTrimOffset()` and `setTrimLength()` to the video fill, not the graphic block. - Keep `trimOffset + trimLength` within `getAVResourceTotalDuration()`, then set the graphic block duration to the clip length when the timeline should match the trim. ## API Reference | API | Description | | --- | --- | | `engine.scene.createForVideo()` | Create a video scene where block durations control timeline playback and video export. | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a graphic 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.createFill(fillType=FillType.Video)` | Create a video fill object. | | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Assign a video source URI. | | `engine.block.setFill(block=_, fill=_)` | Attach a fill to a block. | | `engine.block.appendChild(parent=_, child=_)` | Attach a block to the scene hierarchy. | | `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 horizontal position. | | `engine.block.setPositionY(block=_, value=_)` | Set the vertical position. | | `engine.block.setContentFillMode(block=_, mode=_)` | Set Cover, Contain, or Crop behavior. | | `engine.block.forceLoadAVResource(block=_)` | Load video metadata before reading duration or trimming. | | `engine.block.getAVResourceTotalDuration(block=_)` | Read the loaded source duration in seconds. | | `engine.block.setTrimOffset(block=_, offset=_)` | Set where playback starts within the source video. | | `engine.block.setTrimLength(block=_, length=_)` | Set how much source video plays from the trim offset. | | `engine.block.setDuration(block=_, duration=_)` | Set how long the block is active on the timeline. | | `engine.block.findByType(type=DesignBlockType.Graphic)` | Find graphic blocks in the current scene. | | `engine.block.getFill(block=_)` | Read the fill attached to a graphic block. | | `engine.block.getType(block=_)` | Read a block or fill type key. | | `engine.block.getUri(block=_, property="fill/video/fileURI")` | Read the assigned video source URI. | | `engine.block.destroy(block=_)` | Remove a block from the scene. | ## Next Steps - [Video Fills](https://img.ly/docs/cesdk/android/fills/video-ec7f9f/) - Configure video fill properties beyond trim and source URL - [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) - Choose the right guide for creating and editing a video project - [Apply a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/apply-2764e4/) — Apply, stack, and manage effects on a video block - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Render scenes with video to MP4 or image 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: "Key Capabilities" description: "Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control." platform: android url: "https://img.ly/docs/cesdk/android/key-capabilities-dbb5b1/" --- > 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 Capabilities](https://img.ly/docs/cesdk/android/key-capabilities-dbb5b1/) --- This guide gives you a high-level look at what CreativeEditor SDK (CE.SDK) can do—and how deeply it can integrate into your workflows. Whether you’re building a design editor into your product, enabling automation, or scaling personalized content creation, CE.SDK provides a flexible and future-ready foundation. [Explore Demos](https://img.ly/showcases/cesdk/?tags=android) It’s designed for developers, product teams, and technical decision-makers evaluating how CE.SDK fits their use case. - 100% client-side processing - Custom-built rendering engine for consistent cross-platform performance - Flexible enough for both low-code and fully custom implementations --- ## 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: "Key Concepts" description: "Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control." platform: android url: "https://img.ly/docs/cesdk/android/key-concepts-21a270/" --- > 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](https://img.ly/docs/cesdk/android/key-concepts-21a270/) --- CE.SDK is built on two distinct technical layers that work together seamlessly: - **User Interface** — Pre-built editors optimized for different use cases - **Engine Interface** — Core rendering and processing engine ![The different layers CE.SDK is made of, see description below.](layers.png) This intentional separation gives you powerful advantages: 1. **Cross-platform consistency** – The engine is cross-compiled to native web, iOS, Android, and Node.js, ensuring identical output everywhere 2. **Custom UI** – Build your own UI for simpler tools and workflows 3. **Headless automation** – Run the engine independently for automations and batch processing, both client-side and server-side ## Creative Engine The Creative Engine powers all core editing operations. It handles rendering, processing, and manipulation across images, layouts, text, video, audio, and vectors. **What the Engine Does:** - Maintains the scene file (your structured content) - Renders the canvas in real-time - Handles block positioning and resizing - Applies filters and effects to images - Manages text editing and typography - Controls templates with role-based permissions - Displays smart guides and snap lines Every engine capability is exposed through a comprehensive API, letting you build custom UIs, workflows, and automations. ## Headless / Engine only Use the engine without any UI for powerful automation scenarios: **Client-side automation** Perfect for in-browser batch operations and dynamic content generation without server dependencies. **Server-side automation with Node.js** Use the [Node.JS SDK](https://img.ly/docs/cesdk/android/what-is-cesdk-2e7acd/) for following scenarios: - **High-resolution processing** – Edit on the client with preview quality, then render server-side with full-resolution assets - **Bulk generation** – Create a large volume of design variations for variable data printing - **Non-blocking workflows** – Let users continue designing while exports process in the background **Server-side export with the CE.SDK Renderer** When exporting complex graphics and videos, the [CE.SDK Renderer](#broken-link-7f3e9a) can make use of GPU acceleration and video codecs on Linux server environments. **Plugin development** When building CE.SDK plugins, you get direct API access to manipulate canvas elements programmatically. ## User Interface Components CE.SDK includes pre-built UI configurations optimized for different use cases: - **Photo editing** — Advanced image editing tools and filters - **Video editing** — Timeline-based video editing and effects - **Design editing** — Layout and graphic design tools (similar to Canva) - **2D product design** — Apparel, postcards, and custom product templates More configurations are coming based on customer needs. ## UI Customization While UI configurations provide a solid foundation, you maintain control over the user experience: - Apply **custom color schemes** and branding to match your product - Add **custom asset libraries** with your own fonts, images, graphics, videos, and audio The plugin architecture lets you add custom buttons and panels throughout the interface, ensuring the editor feels native to your product. --- ## 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: "Licensing" description: "Understand CE.SDK’s flexible licensing, trial options, and how keys work across dev, staging, and production." platform: android url: "https://img.ly/docs/cesdk/android/licensing-8aa063/" --- > 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/) > [Licensing](https://img.ly/docs/cesdk/android/licensing-8aa063/) --- Thanks for your interest in CreativeEditor SDK (CE.SDK). We offer flexible commercial licensing options to support teams and projects of all sizes. Whether you're building a new product or scaling an existing one, our goal is to provide the best creative editing experience—backed by a licensing model that aligns with your needs. Get in touch with us through our [contact sales form](https://img.ly/forms/contact-sales). ## Commercial Licensing CE.SDK is offered through a subscription-based commercial model. This allows us to: - Deliver ongoing updates and performance improvements - Ensure compatibility with new browsers and devices - Provide dedicated technical support - Build long-term partnerships with our customers ## How Licensing Works CE.SDK licenses are tied to a single commercial product instance, verified by the hostname for web apps and bundle/app ID for mobile apps. Licensing typically uses remote validation and includes lightweight event tracking. It’s possible to disable tracking or use offline-compatible options. To explore these options, [contact our sales team](https://img.ly/forms/contact-sales). For details on which operations count as an export, see [Export Counting](https://img.ly/docs/cesdk/android/export-counting-613923/). ## Trial License Key Trial licenses are available for evaluation and testing and are valid for **30 days**. They provide full access to CE.SDK’s features so you can explore its capabilities in your environment. If you need more time to evaluate, [contact our sales team](https://img.ly/forms/contact-sales). ## Evaluation Mode CE.SDK is a commercial product and a license is required for production use. Without a license, CE.SDK runs in evaluation mode, which watermarks exports. Every API, save, archive and export works, with no expiry, no domain lock and no feature gate. Evaluation mode watermarks the canvas and every export, keeps usage tracking on, and prints a banner to the console. Thumbnails are not watermarked. ## Testing and Production Paid license keys can be used across development, staging, and production environments. Multiple domains or app identifiers can be added to support this setup. --- ## 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: "LLMs.txt" description: "Our documentation is available in LLMs.txt format" platform: android url: "https://img.ly/docs/cesdk/android/llms-txt-eb9cc5/" --- > 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/) > [LLMs.txt](https://img.ly/docs/cesdk/android/llms-txt-eb9cc5/) --- > **Note:** You can also connect your AI assistant directly to our documentation using our > [MCP Server](https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/). This enables real-time search and > retrieval without downloading large files. Our documentation is now available in LLMs.txt format, optimized for AI reasoning engines. To better support platform-specific development, we've created separate documentation files for each platform. For developers, this means you can now access documentation tailored to your specific platform, whether it's iOS, Android, Web, or any other supported platform. This approach allows for a more focused and efficient use of AI tools in your development workflow. [Download](getFullUrl\(`/$\{props.platform.slug}/llms-full.txt`\)) These documentation files are substantial in size, with token counts exceeding the context windows of many AI models. This guide explains how to download and effectively use these platform-specific documentation files with AI tools to accelerate your development process. ## What are LLMs.txt files? LLMs.txt is an emerging standard for making documentation AI-friendly. Unlike traditional documentation formats, LLMs.txt: - Presents content in a clean, markdown-based format - Eliminates extraneous HTML, CSS, and JavaScript - Optimizes content for AI context windows - Provides a comprehensive view of documentation in a single file By using our platform-specific LLMs.txt files, you'll ensure that AI tools have the most relevant and complete context for helping with your development tasks. ## Markdown Content Negotiation Our documentation pages also serve clean markdown directly when requested with the `Accept: text/markdown` HTTP header. AI agents and tools that support content negotiation can fetch any documentation page and receive a markdown response instead of HTML — no separate download required. ```bash curl -H "Accept: text/markdown" https://img.ly/docs/cesdk/react/get-started/overview/ ``` This means AI tools like web-browsing agents can access individual pages in a format optimized for their context windows without needing the full LLMs.txt bundle. ## Handling Large Documentation Files Due to the size of our documentation files (upward of 500 000 tokens) most AI tools will face context window limitations. Standard models typically have context windows ranging from 8,000 to 200,000 tokens, making it challenging to process our complete documentation in a single session. ### Using Large Documentation Files To work with our complete documentation files, use an AI model with a large context window. Many current models support 200,000+ tokens, and some support over 1 million tokens. Check your model's context window limits when loading the full documentation file. --- ## 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: "Open the Editor" description: "Learn how to load and create scenes, set the zoom level, and configure URI resolvers." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor-23a1db/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/open-the-editor/overview-99444b/) - Learn how to load and create scenes, set the zoom level, and configure URI resolvers. - [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Load existing design scenes into the editor to resume or modify previous work. - [Start With Blank Canvas](https://img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05/) - Create a new scene from scratch with the CE.SDK Engine for Android, then size the page and add your first design element. - [Create From Image](https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/) - Open the editor using an image as the base design, with tools ready for immediate editing. - [Create From Video](https://img.ly/docs/cesdk/android/open-the-editor/from-video-86beb0/) - Load a video file into the editor to start editing frame-based or time-based video content. - [Create From Template](https://img.ly/docs/cesdk/android/open-the-editor/from-template-46c096/) - Start the editor with a pre-designed template for faster editing and consistent output. - [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) - Load saved CE.SDK scenes, self-contained archives, or source media with the Android Engine API. - [Set Zoom Level](https://img.ly/docs/cesdk/android/open-the-editor/set-zoom-level-d31896/) - Control the canvas zoom level with the CE.SDK Engine - set and read zoom, frame blocks, follow content with auto-fit, clamp the camera, and observe zoom changes. - [URI Resolver](https://img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624/) - Intercept and transform asset URIs before CE.SDK loads them for authentication, redirects, and custom resolution logic. --- ## 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: "Start With Blank Canvas" description: "Create a new scene from scratch with the CE.SDK Engine for Android, then size the page and add your first design element." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Start With Blank Canvas](https://img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05/) --- ```kotlin file=@cesdk_android_examples/engine-guides-open-the-editor-blank-canvas/BlankCanvas.kt reference-only package ly.img.editor.showcase 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.SceneLayout import ly.img.engine.ShapeType import ly.img.engine.ZoomAutoFitAxis suspend fun blankCanvas(engine: Engine): BlankCanvasResult { val scene = engine.scene.create(sceneLayout = SceneLayout.FREE) 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 pageFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = page, fill = pageFill) engine.block.setFillSolidColor( block = page, color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.96F, a = 1F), ) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Star)) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block = block, fill = fill) engine.block.setFillSolidColor( block = block, color = Color.fromRGBA(r = 0.27F, g = 0.52F, b = 0.96F, a = 1F), ) engine.block.setWidth(block = block, value = 300F) engine.block.setHeight(block = block, value = 300F) engine.block.setPositionX(block = block, value = 250F) engine.block.setPositionY(block = block, value = 150F) engine.block.appendChild(parent = page, child = block) engine.scene.enableZoomAutoFit( block = page, axis = ZoomAutoFitAxis.BOTH, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) val previewPng = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) return BlankCanvasResult( page = page, block = block, previewPng = previewPng, ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-open-the-editor-blank-canvas/BlankCanvasResult.kt reference-only package ly.img.editor.showcase import ly.img.engine.DesignBlock import java.nio.ByteBuffer data class BlankCanvasResult( val page: DesignBlock, val block: DesignBlock, val previewPng: ByteBuffer, ) ``` Create a new scene from scratch to build designs with complete control over canvas dimensions and initial content. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-open-the-editor-blank-canvas) Starting from a blank canvas lets you build new designs without pre-existing content. `engine.scene.create()` creates an empty scene with its own camera, ready for pages and blocks. This differs from loading a template or an image, which start with existing content. See [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for more on scene hierarchy. > **Other Ways to Create Scenes:** You can also start with existing content:* [Create From Image](https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/) — Start with an image as the base > * [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) — Resume editing a previously saved design ## Create an Empty Scene Call `engine.scene.create(sceneLayout=_)` to create a new design scene with a camera attached. The scene itself has no dimensions, so you set the canvas size on each page. ```kotlin highlight-android-create-scene val scene = engine.scene.create(sceneLayout = SceneLayout.FREE) ``` The `sceneLayout` parameter controls how pages are arranged. Use `SceneLayout.FREE` for independent positioning, `SceneLayout.VERTICAL_STACK` or `SceneLayout.HORIZONTAL_STACK` for aligned layouts, and `SceneLayout.DEPTH_STACK` for layered compositions. ## Configure Page Size Create a page with `engine.block.create(blockType=_)`, set its dimensions with `setWidth()` and `setHeight()` in design units, then parent it to the scene with `appendChild()`. ```kotlin highlight-android-configure-page 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) ``` Width and height are separate values rather than a single size object. Repeat these calls to add as many pages as your design needs. ## Set a Background Color Give the page a solid RGBA background by assigning it a color fill, then set the visible fill color with `setFillSolidColor()`. ```kotlin highlight-android-background val pageFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = page, fill = pageFill) engine.block.setFillSolidColor( block = page, color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.96F, a = 1F), ) ``` RGBA color components use values from `0` to `1`. ## Add Your First Block Create a graphic block, assign it a shape and a fill so it has a visual representation, size and position it, then append it to the page. A graphic block needs both a shape and a fill to render. ```kotlin highlight-android-add-block val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Star)) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block = block, fill = fill) engine.block.setFillSolidColor( block = block, color = Color.fromRGBA(r = 0.27F, g = 0.52F, b = 0.96F, a = 1F), ) engine.block.setWidth(block = block, value = 300F) engine.block.setHeight(block = block, value = 300F) engine.block.setPositionX(block = block, value = 250F) engine.block.setPositionY(block = block, value = 150F) engine.block.appendChild(parent = page, child = block) ``` `createShape(type=_)` accepts shapes such as `ShapeType.Star`, `ShapeType.Rect`, and `ShapeType.Ellipse`. `createFill(fillType=_)` accepts fills such as `FillType.Color`, `FillType.Image`, and gradient fill types. ## Enable Auto-Fit Zoom For interactive editing, enable auto-fit zoom so the page stays framed when the viewport resizes. ```kotlin highlight-android-zoom engine.scene.enableZoomAutoFit( block = page, axis = ZoomAutoFitAxis.BOTH, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) ``` `enableZoomAutoFit()` continuously adjusts the zoom level to fit a block. Use `ZoomAutoFitAxis.HORIZONTAL` to fit the width, `ZoomAutoFitAxis.VERTICAL` to fit the height, or `ZoomAutoFitAxis.BOTH` to fit both. The padding parameters add space around the content. Only one block per scene can use auto-fit at a time, and it has no effect while the editor UI controls the zoom level. For a one-time adjustment, use `zoomToBlock()`, and call `disableZoomAutoFit()` to stop the continuous fit. ## API Reference | Method | Description | | --- | --- | | `engine.scene.create(sceneLayout=_)` | Create a new empty scene with a camera | | `engine.block.create(blockType=_)` | Create a block such as `DesignBlockType.Page` or `DesignBlockType.Graphic` | | `engine.block.setWidth(block=_, value=_)` / `engine.block.setHeight(block=_, value=_)` | Set a block's dimensions in design units | | `engine.block.setPositionX(block=_, value=_)` / `engine.block.setPositionY(block=_, value=_)` | Position a block on its parent | | `engine.block.appendChild(parent=_, child=_)` | Add a block as a child of another block | | `engine.block.createFill(fillType=_)` / `engine.block.setFill(block=_, fill=_)` | Create and assign a fill | | `engine.block.setFillSolidColor(block=_, color=_)` | Set the RGBA color value on a solid color fill | | `engine.block.createShape(type=_)` / `engine.block.setShape(block=_, shape=_)` | Create and assign a shape | | `engine.scene.enableZoomAutoFit(block=_, axis=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Continuously fit a block in the viewport | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Frame a block once | | `engine.scene.disableZoomAutoFit(block=_)` | Stop auto-fit zoom | ## Next Steps - [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) — Persist your design to a file or backend service - [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Learn about scene hierarchy and block relationships - [Create From Image](https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/) — Start with an existing image instead of a blank canvas --- ## 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 Image" description: "Open the editor using an image as the base design, with tools ready for immediate editing." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Create From Image](https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-scene-from-image-url/CreateSceneFromImageURL.kt reference-only import android.net.Uri import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.SceneLayout suspend fun createSceneFromImageURL(engine: Engine) { val imageRemoteUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg") engine.scene.createFromImage(imageRemoteUri) val page = engine.block.findByType(DesignBlockType.Page).first() val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) check(pageWidth > 0F) check(pageHeight > 0F) val pageFill = engine.block.getFill(page) val imageFillType = engine.block.getType(pageFill) check(imageFillType == FillType.Image.key) val configuredScene = engine.scene.createFromImage( imageUri = imageRemoteUri, dpi = 300F, pixelScaleFactor = 1F, sceneLayout = SceneLayout.FREE, ) check(configuredScene >= 0) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-create-scene-from-image-blob/CreateSceneFromImageBlob.kt reference-only 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 java.io.ByteArrayOutputStream import java.io.File import java.net.URL suspend fun createSceneFromImageBlob(engine: Engine) { val blobUrl = URL("https://img.ly/static/ubq_samples/sample_4.jpg") val imageBytes = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() blobUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } check(imageBytes.isNotEmpty()) val blobFile = withContext(Dispatchers.IO) { File.createTempFile("cesdk-image-", ".jpg").apply { outputStream().use { it.write(imageBytes) } } } val blobUri = Uri.fromFile(blobFile) check(blobFile.length() > 0L) engine.scene.createFromImage(blobUri) val page = engine.block.findByType(DesignBlockType.Page).first() val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) check(pageWidth > 0F) check(pageHeight > 0F) val pageFill = engine.block.getFill(page) val imageFillType = engine.block.getType(pageFill) check(imageFillType == FillType.Image.key) } ``` Create an editable scene from an image with the Android Engine API. The engine builds a single-page scene sized to the image and configured in pixel design units, ready for immediate editing. ![A photograph loaded as an editable Android Engine scene, with the page sized to and filled by the source image.](./assets/android.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-scene-from-image-url) `engine.scene.createFromImage()` loads the image, creates a scene whose page matches the image dimensions, and adds the image directly as the page fill. Use this when an image file is the starting point for an editing workflow where users enhance, annotate, or transform the image. ## Create a Scene From an Image URL Pass a remote or local image `Uri` to `createFromImage()`. The suspend function returns after the image is loaded and the scene is ready for editing. ```kotlin highlight-android-create-from-url val imageRemoteUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg") engine.scene.createFromImage(imageRemoteUri) ``` The scene's page dimensions match the image, and the scene uses pixel design units. ### Inspect the Page Fill The image becomes the page's fill rather than a separate image block. Locate the page with `findByType()` and read its dimensions when you need to position additional blocks relative to the image. ```kotlin highlight-android-work-with-scene val page = engine.block.findByType(DesignBlockType.Page).first() val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) ``` Read the page fill and compare its type with `FillType.Image` to confirm that the image is attached to the page. ```kotlin highlight-android-inspect-page-fill val pageFill = engine.block.getFill(page) val imageFillType = engine.block.getType(pageFill) ``` ## Create a Scene From Image Bytes When the image arrives as raw bytes from a file picker, network response, or other app storage, write those bytes to a file that remains available to the engine. The complete [image-bytes example](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-scene-from-image-blob) fetches the bytes from a URL to stand in for data your app already holds. ```kotlin highlight-android-fetch-image-bytes val blobUrl = URL("https://img.ly/static/ubq_samples/sample_4.jpg") val imageBytes = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() blobUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } ``` Write the bytes to a temporary image file and create a `Uri` for that file. ```kotlin highlight-android-write-image-file val blobFile = withContext(Dispatchers.IO) { File.createTempFile("cesdk-image-", ".jpg").apply { outputStream().use { it.write(imageBytes) } } } val blobUri = Uri.fromFile(blobFile) ``` Use the file `Uri` as the image source for `createFromImage()`. ```kotlin highlight-android-create-from-file engine.scene.createFromImage(blobUri) ``` As with a remote URL, the created page matches the source image dimensions and uses an image fill. ```kotlin highlight-android-work-with-blob-scene val page = engine.block.findByType(DesignBlockType.Page).first() val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) ``` ```kotlin highlight-android-inspect-blob-page-fill val pageFill = engine.block.getFill(page) val imageFillType = engine.block.getType(pageFill) ``` ## Configure Scene Parameters `createFromImage()` accepts optional parameters that control how the image maps to scene dimensions and how pages are arranged. ```kotlin highlight-android-configure-scene val configuredScene = engine.scene.createFromImage( imageUri = imageRemoteUri, dpi = 300F, pixelScaleFactor = 1F, sceneLayout = SceneLayout.FREE, ) ``` | Parameter | Default | Description | | --- | --- | --- | | `dpi` | `300F` | Dots per inch of the scene, used when converting between pixels and physical units. | | `pixelScaleFactor` | `1F` | Scale factor applied to the final export resolution. | | `sceneLayout` | `SceneLayout.FREE` | Page arrangement: `FREE`, `HORIZONTAL_STACK`, `VERTICAL_STACK`, or `DEPTH_STACK`. | To later save your scene, see [Saving Scenes](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/). ## API Reference | Method | Description | | --- | --- | | `engine.scene.createFromImage(imageUri=_)` | Create a scene whose single page is sized to the image and filled with it | | `engine.scene.createFromImage(imageUri=_, dpi=_, pixelScaleFactor=_, sceneLayout=_)` | Create an image scene with explicit DPI, scale, and layout parameters | | `engine.block.findByType(type=DesignBlockType.Page)` | Find page blocks in the current scene | | `engine.block.getWidth(block=_)` | Read the page width | | `engine.block.getHeight(block=_)` | Read the page height | | `engine.block.getFill(block=_)` | Get the fill block attached to a page | | `engine.block.getType(block=_)` | Read a block type string, including the fill type | | `Uri.parse(_)` | Create a `Uri` from a remote or local string | | `Uri.fromFile(_)` | Create a `Uri` from a local file | | `URL.openStream()` | Read image bytes from a remote URL | | `File.createTempFile(prefix=_, suffix=_)` | Create a temporary file for image bytes | | `engine.scene.saveToString(scene=_)` | Save the scene as a string | | `engine.scene.saveToArchive(scene=_)` | Save the scene with embedded assets | ## Next Steps - [Saving Scenes](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) - Persist the edited scene to a string or an archive. - [Load Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Open the editor from a previously saved scene file. - [Create From Video](https://img.ly/docs/cesdk/android/open-the-editor/from-video-86beb0/) - Start the editor from a video instead of an image. - [Blank Canvas](https://img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05/) - Launch the editor with an empty canvas. --- ## 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 Template" description: "Start the editor with a pre-designed template for faster editing and consistent output." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/from-template-46c096/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Create From Template](https://img.ly/docs/cesdk/android/open-the-editor/from-template-46c096/) --- ```kotlin file=@cesdk_android_examples/engine-guides-from-template/FromTemplate.kt reference-only import android.net.Uri import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.MimeType import ly.img.engine.Typeface import java.nio.ByteBuffer data class FromTemplate( val pageCount: Int, val textBlockCount: Int, val customizedText: String?, val previewPngData: ByteBuffer, ) suspend fun fromTemplate(engine: Engine): FromTemplate { val templateUri = Uri.parse( "https://cdn.img.ly/packages/imgly/cesdk-android/1.83.0/assets/ly.img.templates/templates/cesdk_business_card_1.scene", ) val scene = engine.scene.load(sceneUri = templateUri, waitForResources = true) check(scene == engine.scene.get()) val templateString = engine.scene.saveToString(scene = scene) engine.scene.load(scene = templateString, waitForResources = true) val replacementTemplateUri = Uri.parse( "https://cdn.img.ly/packages/imgly/cesdk-android/1.83.0/assets/ly.img.templates/templates/cesdk_business_card_1.scene", ) engine.scene.applyTemplate(templateUri = replacementTemplateUri) val firstTextBlock = engine.block.findByType(DesignBlockType.Text).firstOrNull() if (firstTextBlock != null) { engine.block.replaceText(block = firstTextBlock, text = "Your Company") } val page = engine.block.findByType(DesignBlockType.Page).first() val exportTypeface = Typeface( name = "Fira Sans", fonts = listOf( Font( uri = Uri.parse("file:///android_asset/imgly-assets/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ) val exportFont = exportTypeface.fonts.first() engine.block.findByType(DesignBlockType.Text).forEach { textBlock -> engine.block.setFont(block = textBlock, fontFileUri = exportFont.uri, typeface = exportTypeface) } engine.block.forceLoadResources(blocks = listOf(page)) val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) val previewPngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions( targetWidth = 1200F, targetHeight = 1200F * pageHeight / pageWidth, ), ) return FromTemplate( pageCount = engine.block.findByType(DesignBlockType.Page).size, textBlockCount = engine.block.findByType(DesignBlockType.Text).size, customizedText = firstTextBlock?.let { engine.block.getString(block = it, property = "text/text") }, previewPngData = previewPngData, ) } ``` Load pre-designed templates to give users a professional starting point instead of a blank canvas. ![Customized template preview](./assets/from-template-android.png) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-from-template) Templates provide consistent layouts and styling that users can customize for their own needs. CE.SDK loads Android templates from remote or local scene URIs, from serialized strings, and applies template content to an existing scene while preserving its page dimensions. ## Load a Template from URL The most common approach is loading a template from a `.scene` file URI. Pass the URI to `engine.scene.load(sceneUri=_)`, and the engine replaces the current scene with the loaded template. ```kotlin highlight-android-load-from-url val templateUri = Uri.parse( "https://cdn.img.ly/packages/imgly/cesdk-android/1.83.0/assets/ly.img.templates/templates/cesdk_business_card_1.scene", ) val scene = engine.scene.load(sceneUri = templateUri, waitForResources = true) ``` The scene file references assets by URI, so those assets must stay reachable. Use `waitForResources=true` when later code depends on the template's resources being loaded before it continues. ## Load a Template from String When a template is stored as serialized scene data, load it with `engine.scene.load(scene=_)`. The string usually comes from a previous `engine.scene.saveToString(scene=_)` call and can be stored in your database or app storage. ```kotlin highlight-android-load-from-string val templateString = engine.scene.saveToString(scene = scene) engine.scene.load(scene = templateString, waitForResources = true) ``` This path is useful for restoring saved user designs or serving templates through your backend. ## Apply a Template to an Existing Scene To populate an existing scene with template content while keeping its current page dimensions, use `engine.scene.applyTemplate(templateUri=_)`. Android also exposes `engine.scene.applyTemplate(template=_)` when the template is already available as a serialized string. ```kotlin highlight-android-apply-template val replacementTemplateUri = Uri.parse( "https://cdn.img.ly/packages/imgly/cesdk-android/1.83.0/assets/ly.img.templates/templates/cesdk_business_card_1.scene", ) engine.scene.applyTemplate(templateUri = replacementTemplateUri) ``` Use this when the canvas size is already set for a fixed output format and you want to drop in template content without changing those dimensions. ## Modify Template Content After loading or applying a template, customize its blocks with the block APIs. Find the elements you want to change and update them. ```kotlin highlight-android-modify-content val firstTextBlock = engine.block.findByType(DesignBlockType.Text).firstOrNull() if (firstTextBlock != null) { engine.block.replaceText(block = firstTextBlock, text = "Your Company") } ``` Common modifications include: - **Replacing text**: `engine.block.replaceText(block=_, text=_)` swaps text content. - **Swapping images**: set `fill/image/imageFileURI` on a graphic block's image fill with `engine.block.setUri(block=_, property=_, value=_)` — see [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/). - **Adjusting colors**: set `fill/color/value` on a block's fill — see [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/). ## Troubleshooting **Template fails to load** - Verify the URI is reachable and returns a valid `.scene` file. - Ensure the template format is compatible with your CE.SDK version. - For remote URLs, confirm your app has network access and the server allows Android clients to download the file. **Assets not displaying after load** - Scene files store asset references as URIs; ensure those URIs remain reachable. - Use an archive (`.imgly` or `.zip`) for a self-contained template with bundled assets, and load that archive with the same `engine.scene.load(sceneUri=_)` call. - Configure a [URI resolver](https://img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624/) if assets are hosted on a different server. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a scene or archive from a remote or local URI (content detected automatically). | | `engine.scene.load(scene=_, waitForResources=_)` | Load a scene from a serialized string. | | `engine.scene.applyTemplate(templateUri=_)` | Apply a template scene URI to the current scene. | | `engine.scene.applyTemplate(template=_)` | Apply a serialized string template to the current scene. | | `engine.scene.saveToString(scene=_)` | Serialize the current scene to a string. | | `engine.block.findByType(type=_)` | Find all blocks of a given type. | | `engine.block.replaceText(block=_, text=_)` | Replace text content in a text block. | | `engine.block.setString(block=_, property=_, value=_)` | Set a string property. | | `engine.block.setUri(block=_, property=_, value=_)` | Set a URI property, such as an image fill URI. | | `engine.block.setColor(block=_, property=_, value=_)` | Set a color property, such as a fill color. | ## Next Steps - [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) — Load saved scenes from various sources - [Save a Design](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) — Save your customized template - [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) — Load previously saved scenes, self-contained archives, or create editable scenes from 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 From Video" description: "Load a video file into the editor to start editing frame-based or time-based video content." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/from-video-86beb0/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Create From Video](https://img.ly/docs/cesdk/android/open-the-editor/from-video-86beb0/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-scene-from-video-url/CreateSceneFromVideoURL.kt reference-only import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine private const val SAMPLE_VIDEO_URI = "https://img.ly/static/ubq_video_samples/bbb.mp4" suspend fun createSceneFromVideoURL(engine: Engine): DesignBlock = withContext(Dispatchers.Main) { val videoRemoteUri = Uri.parse(SAMPLE_VIDEO_URI) val scene = engine.scene.createFromVideo(videoUri = videoRemoteUri) // Find the automatically added graphic block in the scene that contains the video fill. val block = engine.block.findByType(type = DesignBlockType.Graphic).first() engine.block.setOpacity(block = block, value = 0.5F) val page = engine.scene.getPages().first() val duration = engine.block.getDuration(block = page) if (engine.block.supportsPlaybackTime(block = page) && duration >= 1.0) { engine.block.setPlaybackTime(block = page, time = 1.0) engine.block.setPlaying(block = page, enabled = true) engine.block.setPlaying(block = page, enabled = false) } scene } ``` Open CE.SDK with a video as the starting point for editing. The scene's page dimensions match the video resolution and the scene is set up for time-based content. > **Reading time:** 3 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-scene-from-video-url) Starting from an existing video lets you build editors for customizing video content — trimmers, overlay editors, or upload-and-edit flows. Create a scene from a single video with `engine.scene.createFromVideo()`. This guide covers creating a scene from a video and reaching the video block to adjust its properties. ## Create a Scene From a Video URL Pass a video `Uri` to `engine.scene.createFromVideo()`. The URI can point to a local file, a content URI, or a remote resource that your Android app can access. The call loads the video and returns a handle to the new scene. ```kotlin highlight-android-create-from-video val videoRemoteUri = Uri.parse(SAMPLE_VIDEO_URI) val scene = engine.scene.createFromVideo(videoUri = videoRemoteUri) ``` When you start from a video, the scene's page dimensions match the resource, the scene uses pixel design units, and it is set up for time-based editing. ## Work With the Video Block CE.SDK places the video inside a graphic block that carries a video fill. Retrieve it with `engine.block.findByType(DesignBlockType.Graphic)`, which returns every graphic block in the scene. A scene created from a single video contains one graphic block, so the first result is the video block. From there, modify the block like any other element — for example, change its opacity with `engine.block.setOpacity()`. ```kotlin highlight-android-work-with-video-block // Find the automatically added graphic block in the scene that contains the video fill. val block = engine.block.findByType(type = DesignBlockType.Graphic).first() engine.block.setOpacity(block = block, value = 0.5F) ``` See [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for the full Block API. The page also exposes playback state, so editor UIs can connect the same scene to trimming, seeking, and playback controls in the dedicated video guides. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.createFromVideo(videoUri=_)` | Create a scene from a video URI, matching the scene dimensions to the video | | `engine.block.findByType(type=_)` | Find all blocks of a `DesignBlockType` | | `engine.block.setOpacity(block=_, value=_)` | Set a block's opacity | ## Next Steps - [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Edit blocks, layout, and properties in the scene - [Saving Scenes](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) — Persist your scene and reload it later - [Insert Videos](https://img.ly/docs/cesdk/android/insert-media/videos-a5fa03/) — Add and configure additional video blocks programmatically - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) — Trim, seek, and control video and audio playback --- ## 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 a Design" description: "Load saved CE.SDK scenes, self-contained archives, or source media with the Android Engine API." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-design/ImportDesign.kt reference-only 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 importDesign( engine: Engine, assetBaseUri: Uri, ): ImportDesignResult { val sceneUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_postcard_1.scene") .build() val imageUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates.premium") .appendPath("thumbnails") .appendPath("e-commerce") .appendPath("e-commerce_modern_square_n1.jpg") .build() val videoUri = assetBaseUri.buildUpon() .appendPath("ly.img.video") .appendPath("videos") .appendPath("pexels-tony-schnagl-5528015.mp4") .build() val remoteScene = engine.scene.load(sceneUri = sceneUri, waitForResources = true) val sceneString = engine.scene.saveToString(scene = remoteScene) val stringScene = engine.scene.load(scene = sceneString, waitForResources = true) val archiveBytes = engine.scene.saveToArchive(scene = stringScene) val archiveFile = withContext(Dispatchers.IO) { val file = File.createTempFile("import-design-", ".imgly") val archiveBuffer = archiveBytes.asReadOnlyBuffer() file.outputStream().channel.use { channel -> while (archiveBuffer.hasRemaining()) { channel.write(archiveBuffer) } } file } val archiveScene = engine.scene.load( sceneUri = Uri.fromFile(archiveFile), waitForResources = true, ) val imageScene = engine.scene.createFromImage(imageUri = imageUri) val previewPngData = engine.block.export( block = engine.scene.getPages().first(), mimeType = MimeType.PNG, ) val videoScene = engine.scene.createFromVideo(videoUri = videoUri) engine.scene.load(sceneUri = sceneUri, waitForResources = true) val textBlock = engine.block.findByType(DesignBlockType.Text).first() engine.block.setOpacity(block = textBlock, value = 0.85F) return ImportDesignResult( remoteScene = remoteScene, stringScene = stringScene, archiveScene = archiveScene, imageScene = imageScene, videoScene = videoScene, textOpacity = engine.block.getOpacity(textBlock), pageCount = engine.scene.getPages().size, previewPngData = previewPngData, ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-import-design/ImportDesignResult.kt reference-only import ly.img.engine.DesignBlock import java.nio.ByteBuffer data class ImportDesignResult( val remoteScene: DesignBlock, val stringScene: DesignBlock, val archiveScene: DesignBlock, val imageScene: DesignBlock, val videoScene: DesignBlock, val textOpacity: Float, val pageCount: Int, val previewPngData: ByteBuffer, ) ``` Open existing designs in CE.SDK for Android by loading saved scenes, restoring archives, or creating editable scenes from images and videos. ![Image import rendered from the Android Engine sample](./assets/android.import-design.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-design) CE.SDK supports several ways to open a design beyond starting from a blank canvas. Each load or create call replaces the active scene, so the imported design becomes the scene your app edits and exports. The sample receives your app's configured asset base URI and appends one saved scene, one image, and one video resource: ```kotlin highlight-android-source-uris val sceneUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_postcard_1.scene") .build() val imageUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates.premium") .appendPath("thumbnails") .appendPath("e-commerce") .appendPath("e-commerce_modern_square_n1.jpg") .build() val videoUri = assetBaseUri.buildUpon() .appendPath("ly.img.video") .appendPath("videos") .appendPath("pexels-tony-schnagl-5528015.mp4") .build() ``` ## Understanding Import Methods CE.SDK provides three Android import approaches, each suited to a different source: - **Scene files** store design structure, layout, and properties while referencing assets such as images and fonts by URI. They are lightweight, but those asset URIs must stay reachable. - **Archives** bundle the scene file with accessible referenced assets and use relative references. They are larger, but self-contained and portable. For details on creating archives, see [Import Design from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/). - **Media-based scenes** build an editable design directly from a source image or video. Scene files and archives both use the `.imgly` extension — `.scene` and `.zip` files also load — and the same `engine.scene.load(sceneUri=_)` call opens either kind. ## Import from Other Design Tools Photoshop (`.psd`) and InDesign (`.idml`) importers run in browser and Node.js environments, not on device in the Android SDK. Convert Photoshop files with the [Node.js PSD importer](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-photoshop-cca6bb/) and InDesign files with the [Node.js IDML importer](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-indesign-ba3988/), then load the resulting CE.SDK scene or archive on Android with `engine.scene.load()`. ## Load Saved CE.SDK Scenes Load a previously saved scene to resume editing. The Android Engine API accepts a `Uri`, a serialized scene string, or an archive URI. ### From a URI Use `engine.scene.load(sceneUri=_)` for a remote HTTPS scene or a local file URI. This fits cloud-based editing where your app receives the scene location from your backend. ```kotlin highlight-android-load-from-uri val remoteScene = engine.scene.load(sceneUri = sceneUri, waitForResources = true) ``` The engine fetches the scene and replaces the current scene. Passing `waitForResources=true` waits for referenced assets before the sample continues to render, edit, or export the design. ### From a String Use `engine.scene.load(scene=_)` when your app already has the serialized scene content in memory, such as a value from a database or the result of `engine.scene.saveToString()`. Pass `waitForResources=true` when the next step depends on images, fonts, or other referenced resources being ready. ```kotlin highlight-android-load-from-string val sceneString = engine.scene.saveToString(scene = remoteScene) val stringScene = engine.scene.load(scene = sceneString, waitForResources = true) ``` This approach works well for custom storage systems that return scene data as text. ### From an Archive Use `engine.scene.load(sceneUri=_)` for self-contained scene archives — the same call that loads scene files, since the engine detects the content automatically. Archive creation and temporary-file setup are sample scaffolding; the import call loads the archive URI back into the engine. ```kotlin highlight-android-load-from-archive val archiveScene = engine.scene.load( sceneUri = Uri.fromFile(archiveFile), waitForResources = true, ) ``` Archives are useful when original asset URIs might move, expire, or become unavailable. Passing `waitForResources=true` keeps the loaded archive resource-ready for immediate editing or export. ## Create Scenes from Media Build an editable scene directly from a source image or video instead of loading a saved CE.SDK design. ### From an Image Use `engine.scene.createFromImage(imageUri=_)` to create a single-page design scene around an image. ```kotlin highlight-android-create-from-image val imageScene = engine.scene.createFromImage(imageUri = imageUri) ``` The scene is ready for editing, so you can add text, shapes, effects, and other design elements on top of the image. ### From a Video Use `engine.scene.createFromVideo(videoUri=_)` to create a video scene with the video as the page content. ```kotlin highlight-android-create-from-video val videoScene = engine.scene.createFromVideo(videoUri = videoUri) ``` The scene is set up for timeline-based editing with the source video as the initial content. ## Choosing the Right Import Method Pick the method that matches your source and portability requirements: - **Resuming saved work?** Use `engine.scene.load(sceneUri=_)` or `engine.scene.load(scene=_)` for scenes you previously saved. - **Assets might be unavailable?** Use `engine.scene.load(sceneUri=_)` with an archive for a self-contained scene with bundled assets. - **Coming from design tools?** Convert PSD or IDML files outside Android first, then load the converted scene or archive. - **Starting from media?** Use `engine.scene.createFromImage(imageUri=_)` or `engine.scene.createFromVideo(videoUri=_)`. ## Asset Availability Considerations When you load a scene file rather than an archive, the referenced assets must stay reachable at their original URIs. Scene files store those references, so an image saved at a remote HTTPS URL must still be served there when the scene loads. Archives avoid this by bundling assets inside the ZIP and using relative references. Use archives for portable files, offline handoff, or imported designs whose original assets should not be fetched again. ## Working with Loaded Scenes After importing, the design becomes the active scene. Query and modify its blocks immediately with the block APIs. ```kotlin highlight-android-modify-loaded-scene engine.scene.load(sceneUri = sceneUri, waitForResources = true) val textBlock = engine.block.findByType(DesignBlockType.Text).first() engine.block.setOpacity(block = textBlock, value = 0.85F) ``` ## Troubleshooting **Scene loads with missing images or fonts** - Confirm every asset URI referenced in the scene is still reachable. - Use an archive instead of a scene file when assets might move or become unavailable. **Archive fails to load** - Ensure the archive was created with `engine.scene.saveToArchive()`. - Confirm the archive URI is reachable and the file was not truncated during transfer. **Image or video fails to load** - Confirm the media URI is reachable and returns the expected file. - Confirm the media is in a supported image or video format. **Converted PSD or IDML content is incomplete** - Check the conversion logs from the browser or Node.js importer that produced the scene or archive. - Load the converted archive on Android only after conversion succeeds. ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.load(sceneUri=_, waitForResources=true)` | Load a scene or archive from a remote or local URI (content detected automatically) and wait for referenced resources | | `engine.scene.load(scene=_, waitForResources=true)` | Load a scene from serialized scene content and wait for referenced resources | | `engine.scene.loadArchive(archiveUri=_, waitForResources=true)` | Load a scene archive from a URI | | `engine.scene.saveToString(scene=_)` | Serialize a scene to a string for later loading | | `engine.scene.saveToArchive(scene=_)` | Save a scene and its assets as an archive (persist it with the `.imgly` extension) | | `engine.scene.createFromImage(imageUri=_)` | Create an editable design scene from an image URI | | `engine.scene.createFromVideo(videoUri=_)` | Create a video scene from a video URI | | `engine.block.export(block=_, mimeType=MimeType.PNG)` | Export the imported scene preview used by the guide asset | | `engine.block.findByType(type=DesignBlockType.Text)` | Find text blocks in the active scene | | `engine.block.setOpacity(block=_, value=_)` | Change block opacity after loading a scene | --- ## Related Pages - [From InDesign](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-indesign-ba3988/) - Load a CE.SDK archive converted from an Adobe InDesign IDML file into an Android app. - [From Photoshop](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-photoshop-cca6bb/) - Load a CE.SDK archive converted from an Adobe Photoshop PSD file into an Android app. - [Import Design from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/) - Load self-contained CE.SDK archive files that bundle scene structure with all referenced assets for portable, reliable design imports. --- ## 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 Design from Archive" description: "Load self-contained CE.SDK archive files that bundle scene structure with all referenced assets for portable, reliable design imports." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) > [From Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/) --- ```kotlin file=@cesdk_android_examples/engine-guides-import-design-from-archive/ImportDesignFromArchive.kt reference-only import android.net.Uri 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.FillType import ly.img.engine.ShapeType import ly.img.engine.SizeMode import java.io.File import java.io.FileOutputStream import java.nio.ByteBuffer data class ImportDesignFromArchiveResult( val archiveFile: File, val archiveByteCount: Int, val loadedScene: DesignBlock, val updatedTextBlock: DesignBlock, ) suspend fun importDesignFromArchive( engine: Engine, outputDir: File, ): ImportDesignFromArchiveResult { val sourceScene = createArchiveSourceScene(engine) val archive = createArchiveForTransfer(engine, sourceScene) val archiveFile = writeArchiveToFile( archive = archive, archiveFile = File(outputDir, "portable-design.imgly"), ) val loadedScene = loadArchiveFromUri( engine = engine, archiveUri = Uri.fromFile(archiveFile), ) val updatedTextBlock = modifyLoadedArchive(engine) return ImportDesignFromArchiveResult( archiveFile = archiveFile, archiveByteCount = archive.remaining(), loadedScene = loadedScene, updatedTextBlock = updatedTextBlock, ) } suspend fun createArchiveForTransfer( engine: Engine, scene: DesignBlock, ): ByteBuffer { val archive = engine.scene.saveToArchive(scene = scene) check(archive.hasRemaining()) { "Archive is empty" } return archive.asReadOnlyBuffer() } suspend fun writeArchiveToFile( archive: ByteBuffer, archiveFile: File, ): File = withContext(Dispatchers.IO) { archiveFile.parentFile?.mkdirs() val readableArchive = archive.asReadOnlyBuffer() FileOutputStream(archiveFile).channel.use { channel -> while (readableArchive.hasRemaining()) { channel.write(readableArchive) } } check(archiveFile.length() > 0L) { "Saved archive is empty" } archiveFile } suspend fun loadArchiveFromUri( engine: Engine, archiveUri: Uri, ): DesignBlock = engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) fun modifyLoadedArchive(engine: Engine): DesignBlock { val textBlock = engine.block.findByType(DesignBlockType.Text).first() engine.block.replaceText( block = textBlock, text = "Archived design loaded", ) return textBlock } private fun createArchiveSourceScene(engine: Engine): DesignBlock { 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 background = engine.block.create(DesignBlockType.Graphic) 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(background, color = Color.fromHex("#FFF6F8FB")) engine.block.appendChild(parent = page, child = background) val textBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = textBlock) engine.block.replaceText(textBlock, text = "Portable archive") engine.block.setWidthMode(textBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(textBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(textBlock, fontSize = 64F) engine.block.setPositionX(textBlock, value = 96F) engine.block.setPositionY(textBlock, value = 96F) return scene } ``` Import archived CE.SDK scenes that bundle the design structure together with all fonts, images, and assets in a single portable file. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-import-design-from-archive) Scene files reference assets by URL, so they can break when those URLs become unavailable. Archives solve this by packaging the scene together with every accessible font, image, video, and audio resource into one file — saved with the `.imgly` extension (the `.zip` extension also works). Use archives when a design must be moved between environments, shared with another user, or stored long term. ## Understanding CE.SDK Archives A CE.SDK archive is a ZIP container created with `engine.scene.saveToArchive(scene=_)` — save it with the `.imgly` extension. It contains the serialized scene and the resources referenced by that scene. During saving, CE.SDK rewrites those resource references to archive-local paths so the imported design does not depend on the original asset URLs. Archives typically contain a serialized scene entry plus resource folders derived from the bundled MIME types: ```text archive.zip |-- scene.scene |-- images/ |-- fonts/ |-- videos/ `-- audios/ ``` The exact filenames are managed by CE.SDK. Your app should treat the archive as an opaque file and load it through the scene API instead of editing the ZIP contents directly. ## Create an Archive for Loading The runnable sample creates an archive first so the Android loading path can be tested end to end. In production, this archive usually comes from your backend, app storage, or a user-selected document. ```kotlin highlight-android-create-archive suspend fun createArchiveForTransfer( engine: Engine, scene: DesignBlock, ): ByteBuffer { val archive = engine.scene.saveToArchive(scene = scene) check(archive.hasRemaining()) { "Archive is empty" } return archive.asReadOnlyBuffer() } ``` `saveToArchive(scene=_)` returns a `ByteBuffer`. The buffer contains the ZIP archive bytes and can be written to app storage, uploaded, or passed to your own persistence layer. ## Store the Archive as a File Android scene archive loading accepts a `Uri`, so persist downloaded or generated archive bytes to an app-controlled file when you need a local import path. This is also a reliable pattern after receiving a document from the Android Storage Access Framework. ```kotlin highlight-android-write-archive-file suspend fun writeArchiveToFile( archive: ByteBuffer, archiveFile: File, ): File = withContext(Dispatchers.IO) { archiveFile.parentFile?.mkdirs() val readableArchive = archive.asReadOnlyBuffer() FileOutputStream(archiveFile).channel.use { channel -> while (readableArchive.hasRemaining()) { channel.write(readableArchive) } } check(archiveFile.length() > 0L) { "Saved archive is empty" } archiveFile } ``` Use your own storage location and retention policy. The sample writes to a temporary file so it can immediately load the same archive back into the engine. ## Load the Archive Pass the archive location to `engine.scene.load(sceneUri=_)` — the same call that loads scene files, since the engine detects the content automatically. The URI can point to an app file, a user-selected document that your app can still read, or a remote `https://` archive. ```kotlin highlight-android-load-archive suspend fun loadArchiveFromUri( engine: Engine, archiveUri: Uri, ): DesignBlock = engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) ``` Loading is asynchronous and replaces the active scene. The sample sets `waitForResources=true` so the coroutine resumes after bundled resources are ready. ## Modify the Loaded Scene After the archive loads, the scene is editable like any other CE.SDK scene. Locate blocks with the block API and update them normally. ```kotlin highlight-android-modify-loaded-archive fun modifyLoadedArchive(engine: Engine): DesignBlock { val textBlock = engine.block.findByType(DesignBlockType.Text).first() engine.block.replaceText( block = textBlock, text = "Archived design loaded", ) return textBlock } ``` Bundled assets resolve from inside the archive, so text, image, video, and audio resources remain available even if the original URLs are gone. ## Archives vs Scene Files CE.SDK offers two save formats that handle assets differently: - **Scene files** store the design structure and reference assets by their original URLs. Use them when those URLs remain reachable and you want the smallest file. - **Archives** bundle the scene and accessible referenced assets into one package. Use them when portability, offline loading, or long-term reliability matter more than file size. Both kinds are saved with the `.imgly` extension; `.scene` and `.zip` files also load. Load either with the same `engine.scene.load(sceneUri=_)` call — the engine detects the content automatically. See [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) for the scene-file path. ## Asset Availability and Portability A scene file only loads correctly while every referenced asset stays at its original URL. Moving assets, expiring access tokens, or losing network access can break it. Archives remove that dependency by bundling the assets inside the ZIP, which makes a design environment-independent, offline-capable, and easier to share without coordinating asset access. ## Troubleshooting **The archive fails to load** - Confirm it was created with `engine.scene.saveToArchive(scene=_)`, because CE.SDK archives use an internal structure that an arbitrary ZIP file does not have. Check that the file is not corrupted and that the URI remains readable for the duration of the load. **Assets are missing after loading** - Verify the archive was not edited by hand, which can break relative references. Also confirm that the asset formats and codecs are supported on the target Android device. **Large archives take time** - Loading is asynchronous because CE.SDK reads and extracts the archive before the scene is ready. Show a progress indicator in your app when archive files are large. ## API Reference | API | Purpose | | --- | --- | | `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene file or archive from a URI (content detected automatically) | | `engine.scene.saveToArchive(scene=_)` | Create an archive as a `ByteBuffer` with the scene and accessible referenced assets | | `engine.scene.loadArchive(archiveUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene archive from a URI | | `engine.block.findByType(type=_)` | Find blocks by type in the loaded scene | | `engine.block.replaceText(block=_, text=_)` | Replace all text inside a loaded text block | ## Next Steps - [Save a Scene](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) - Create archives and scene files from the current design. - [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Load a scene file when assets are referenced by URL. --- ## 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: "From InDesign" description: "Load a CE.SDK archive converted from an Adobe InDesign IDML file into an Android app." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-indesign-ba3988/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) > [From InDesign](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-indesign-ba3988/) --- ```kotlin file=@cesdk_android_examples/engine-guides-open-the-editor-import-design-from-indesign/ImportFromInDesign.kt reference-only import android.net.Uri import ly.img.engine.DesignBlock import ly.img.engine.Engine suspend fun importFromInDesign( engine: Engine, archiveUri: Uri, ): DesignBlock { val scene = engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) check(engine.scene.getPages().isNotEmpty()) { "The converted InDesign archive contains no pages." } engine.scene.zoomToBlock( block = scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) return scene } ``` Load a CE.SDK archive converted from an Adobe InDesign IDML file into your Android app, then continue editing the imported design. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-open-the-editor-import-design-from-indesign) Android does not parse IDML files. Convert the IDML into a self-contained `.imgly` archive in a browser or Node.js environment, then deliver that archive to the app through your backend, app storage, or a user-selected document. For server-side conversion, use the [Node.js importer](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-indesign-ba3988/). ## Conversion Prerequisite > **Commercial Runtime License:** For commercial use, your CE.SDK license must cover the runtime performing > conversion: Web for browser conversion or Node.js for server conversion. An > Android-only license does not cover either conversion runtime. ## Load the Converted Archive Convert the IDML once with `@imgly/idml-importer` in a browser or Node.js environment, save the resulting `.imgly` archive, and make its `Uri` available to Android. The archive bundles the converted scene with its embedded assets, so `load()` can replace the active scene without Android reading the original IDML file. ```kotlin highlight-android-load-converted-archive val scene = engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) ``` Pass a local file URI, a readable Android content URI, or a remote HTTPS URI. `waitForResources=true` resumes the coroutine after the archive's bundled resources are ready for editing or export. ## Verify the Import Confirm that the loaded scene contains pages before showing it in your app. An empty page list means the converted archive has no usable design pages. ```kotlin highlight-android-verify-import check(engine.scene.getPages().isNotEmpty()) { "The converted InDesign archive contains no pages." } ``` ## Fit the Scene to the Viewport Frame the loaded scene after the archive is ready. The padding values are screen pixels around the focused scene. ```kotlin highlight-android-fit-viewport engine.scene.zoomToBlock( block = scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) ``` ## What Gets Imported The conversion can preserve element grouping, positioning, rotation, transparency, text with bold and italic styling, shapes, solid and gradient fills, strokes, and embedded images. Android receives the converted scene in the archive and can edit it like any other CE.SDK scene. ## Limitations These conversion limits apply before the archive reaches Android: - **Linked images** become placeholders. Embed images in InDesign before exporting to IDML. - **Text flow** between multiple frames is not supported and may appear duplicated. - **Image fitting** can differ when images are shrunk inside their frames. - **Embedded PDF or Adobe Illustrator content** needs the embedded importer during conversion; otherwise it becomes a placeholder. - **Unavailable fonts** use fallbacks selected during conversion. - **Complex text formatting** beyond bold and italic may not be preserved. These are the highlights only—the [`@imgly/idml-importer`](https://www.npmjs.com/package/@imgly/idml-importer) page on npm maintains the complete, up-to-date list of supported features and limitations. ## API Reference | Method | Description | | --- | --- | | `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load the converted archive and its bundled assets from a URI. | | `engine.scene.getPages()` | Return the pages in the active imported scene. | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Fit the imported scene in the viewport with screen-pixel padding. | ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/) — Load CE.SDK archives from storage, document providers, or remote locations. - [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/) — Load templates from scene files, archives, and serialized strings. - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Export the imported design to images, documents, and other 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: "From Photoshop" description: "Load a CE.SDK archive converted from an Adobe Photoshop PSD file into an Android app." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-photoshop-cca6bb/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/) > [From Photoshop](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-photoshop-cca6bb/) --- ```kotlin file=@cesdk_android_examples/engine-guides-open-the-editor-import-design-from-photoshop/ImportFromPhotoshop.kt reference-only import android.net.Uri import ly.img.engine.DesignBlock import ly.img.engine.Engine suspend fun importFromPhotoshop( engine: Engine, archiveUri: Uri, ): DesignBlock { val scene = engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) check(engine.scene.getPages().isNotEmpty()) { "The converted Photoshop archive contains no pages." } engine.scene.zoomToBlock( block = scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) return scene } ``` Load a CE.SDK archive converted from an Adobe Photoshop PSD file into your Android app, then continue editing the imported design. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-open-the-editor-import-design-from-photoshop) Android does not parse PSD files. Convert the PSD into a self-contained `.imgly` archive in a browser or Node.js environment, then deliver that archive to the app through your backend, app storage, or a user-selected document. For server-side conversion, use the [Node.js importer](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-photoshop-cca6bb/). ## Conversion Prerequisite > **Commercial Runtime License:** For commercial use, your CE.SDK license must cover the runtime performing > conversion: Web for browser conversion or Node.js for server conversion. An > Android-only license does not cover either conversion runtime. ## Load the Converted Archive Convert the PSD once with `@imgly/psd-importer` in a browser or Node.js environment, save the resulting `.imgly` archive, and make its `Uri` available to Android. The archive bundles the converted scene with its embedded assets, so `load()` can replace the active scene without Android reading the original PSD file. ```kotlin highlight-android-load-converted-archive val scene = engine.scene.load( sceneUri = archiveUri, waitForResources = true, ) ``` Pass a local file URI, a readable Android content URI, or a remote HTTPS URI. `waitForResources=true` resumes the coroutine after the archive's bundled resources are ready for editing or export. ## Verify the Import Confirm that the loaded scene contains pages before showing it in your app. An empty page list means the converted archive has no usable design pages. ```kotlin highlight-android-verify-import check(engine.scene.getPages().isNotEmpty()) { "The converted Photoshop archive contains no pages." } ``` ## Fit the Scene to the Viewport Frame the loaded scene after the archive is ready. The padding values are screen pixels around the focused scene. ```kotlin highlight-android-fit-viewport engine.scene.zoomToBlock( block = scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) ``` ## What Gets Imported The conversion can preserve layer grouping, positioning, rotation, transparency, text with font-family and bold or italic styling, shapes, solid color fills, strokes, and embedded images. Android receives the converted scene in the archive and can edit it like any other CE.SDK scene. ## Limitations These conversion limits apply before the archive reaches Android: - **Gradient fills** are not supported; only solid color fills are converted. - **Image cropping** is not preserved, so images import at their full bounds. - **Text** in one layer cannot mix multiple font sizes or families, and text justification is not supported. - **Groups** have limited support, especially single-member groups. - **Unavailable fonts** use fallbacks selected during conversion. - **Some blend modes** are not supported, including PassThrough, Dissolve, Linear Burn, and Subtract. These are the highlights only—the [`@imgly/psd-importer`](https://www.npmjs.com/package/@imgly/psd-importer) page on npm maintains the complete, up-to-date list of supported features and limitations. ## API Reference | Method | Description | | --- | --- | | `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load the converted archive and its bundled assets from a URI. | | `engine.scene.getPages()` | Return the pages in the active imported scene. | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Fit the imported scene in the viewport with screen-pixel padding. | ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/) — Load CE.SDK archives from storage, document providers, or remote locations. - [Import Templates](https://img.ly/docs/cesdk/android/create-templates/import-e50084/) — Load templates from scene files, archives, and serialized strings. - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Export the imported design to images, documents, and other 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: "Load a Scene" description: "Load existing design scenes into the editor to resume or modify previous work." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) --- ```kotlin file=@cesdk_android_examples/engine-guides-load-scene-from-remote/LoadSceneFromRemote.kt reference-only import android.net.Uri import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine suspend fun loadSceneFromRemote(engine: Engine): DesignBlock { val sceneUri = Uri.parse( "file:///android_asset/imgly-assets/ly.img.templates/templates/cesdk_postcard_1.scene", ) val scene = engine.scene.load(sceneUri = sceneUri, waitForResources = true) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(block = text, enabled = true) return scene } ``` ```kotlin file=@cesdk_android_examples/engine-guides-load-scene-from-string/LoadSceneFromString.kt reference-only import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine suspend fun loadSceneFromString( engine: Engine, context: Context, ): DesignBlock { val sceneString = withContext(Dispatchers.IO) { context.assets.open("imgly-assets/ly.img.templates/templates/cesdk_postcard_1.scene") .bufferedReader(Charsets.UTF_8) .use { it.readText() } } val scene = engine.scene.load(scene = sceneString, waitForResources = true) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(block = text, enabled = true) return scene } ``` ```kotlin file=@cesdk_android_examples/engine-guides-load-scene-from-blob/LoadSceneFromBlob.kt reference-only import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine suspend fun loadSceneFromBlob( engine: Engine, context: Context, ): DesignBlock { val sceneBytes = withContext(Dispatchers.IO) { context.assets.open("imgly-assets/ly.img.templates/templates/cesdk_postcard_1.scene") .use { it.readBytes() } } val sceneString = sceneBytes.toString(Charsets.UTF_8) val scene = engine.scene.load(scene = sceneString, waitForResources = true) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(block = text, enabled = true) return scene } ``` Load previously saved scenes to resume editing or modify existing designs. ![Loaded postcard scene on Android](./assets/load-scene-android.png) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/find/v$UBQ_VERSION$?q=engine-guides-load-scene) Scene files contain layout, block properties, and asset references, but not the referenced assets themselves. When you load a scene, keep the image, font, audio, and video URLs from that scene accessible to the Android app. This guide covers loading scenes from a URL, a stored string, and byte data, then modifying the loaded scene through the CreativeEngine API. The visible result is the loaded postcard scene with the first text block's drop shadow enabled. ## Load a Scene from URL Use `engine.scene.load(sceneUri = ...)` when your backend or CDN hosts a scene file — an `.imgly` or `.scene` file. The engine replaces the currently active scene with the loaded scene. ```kotlin highlight-android-load-from-url val sceneUri = Uri.parse( "file:///android_asset/imgly-assets/ly.img.templates/templates/cesdk_postcard_1.scene", ) val scene = engine.scene.load(sceneUri = sceneUri, waitForResources = true) ``` The sample passes `waitForResources = true` because it modifies the scene after loading. This waits for referenced resources to finish loading before the suspend call returns. For a hosted scene, replace the bundled `file:///android_asset/...` URI with your HTTPS scene URI. ## Load a Scene from String Use `engine.scene.load(scene = ...)` when you stored the serialized scene contents in a database or local storage. The string should match the output from `engine.scene.saveToString(...)`. ```kotlin highlight-android-load-from-string val sceneString = withContext(Dispatchers.IO) { context.assets.open("imgly-assets/ly.img.templates/templates/cesdk_postcard_1.scene") .bufferedReader(Charsets.UTF_8) .use { it.readText() } } val scene = engine.scene.load(scene = sceneString, waitForResources = true) ``` The sample reads a bundled Android asset to stand in for stored scene contents. Keep the scene string unchanged while storing it; truncation, character-set conversion, or extra wrapping data makes the scene invalid. ## Load a Scene from Blob Android apps usually receive uploaded or locally stored scene data as bytes rather than a browser `Blob`. Decode those bytes as UTF-8 and pass the resulting scene string to `engine.scene.load(scene = ...)`. ```kotlin highlight-android-load-from-blob val sceneBytes = withContext(Dispatchers.IO) { context.assets.open("imgly-assets/ly.img.templates/templates/cesdk_postcard_1.scene") .use { it.readBytes() } } val sceneString = sceneBytes.toString(Charsets.UTF_8) val scene = engine.scene.load(scene = sceneString, waitForResources = true) ``` The sample reads the same bundled scene as bytes before decoding it. This pattern works for data from a file picker, app-private storage, or any API that returns the `.scene` file as a `ByteArray`. ## Modify a Loaded Scene After loading, the scene is immediately editable. Find the blocks you want to update and use the regular block APIs to change them. ```kotlin highlight-android-modify-loaded-scene val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(block = text, enabled = true) ``` The sample loads a postcard scene that contains a text block, then enables a drop shadow on that text. If the user should be able to undo the load, call `engine.editor.undo()` from your own undo workflow. ## Scene Files vs Archives A scene file is lightweight because it stores only references to external assets. If those URLs stop resolving, the scene still loads but affected images, fonts, audio, or video may be missing. For self-contained packages with bundled assets, save and load archives instead. Pass the archive URI to the same `engine.scene.load(sceneUri = ...)` call — the engine detects scene vs archive content automatically. Both scenes and archives use the `.imgly` extension; `.scene` and `.zip` files also load. See [Import Design from Archive](https://img.ly/docs/cesdk/android/open-the-editor/import-design/from-archive-dde9fa/) for the full archive workflow. ## Troubleshooting ### Scene Fails to Load - Verify that the URI points to a valid `.scene` file and is reachable from the Android device. - Keep scene data compatible with the CE.SDK version used by the app. - Use HTTPS or configure Android network security explicitly if your app must load from another scheme. ### Assets Are Missing After Loading - Scene files keep asset URLs by reference, so those URLs must remain accessible. - Use scene archives when the scene should carry its assets with it. - Configure a [URI resolver](https://img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624/) when your app needs to redirect old asset paths to a new location. ### String or Byte Data Is Invalid - Store the exact string returned by `engine.scene.saveToString(...)`. - Decode byte data with the same character set used when the scene was saved. - Avoid wrapping the scene content in extra JSON or metadata before passing it to `engine.scene.load(scene = ...)`. ## API Reference | Method | Description | | ------ | ----------- | | `engine.scene.load(sceneUri=_, waitForResources=_)` | Load a scene or archive from a URI (content detected automatically) | | `engine.scene.load(scene=_, waitForResources=_)` | Load a scene from serialized scene contents | | `engine.scene.loadArchive(archiveUri=_, waitForResources=_)` | Load a scene archive from a URI | | `engine.scene.saveToString(scene=_)` | Serialize a scene for storage | | `engine.block.findByType(type=_)` | Find blocks by typed block kind | | `engine.block.supportsDropShadow(block=_)` | Check whether a block exposes drop shadow properties | | `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable a block's drop shadow | | `engine.block.isDropShadowEnabled(block=_)` | Check whether a block's drop shadow is enabled | | `engine.editor.undo()` | Revert the latest undoable editor operation | --- ## 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 load and create scenes, set the zoom level, and configure URI resolvers." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/overview-99444b/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Overview](https://img.ly/docs/cesdk/android/open-the-editor/overview-99444b/) --- CreativeEditor SDK (CE.SDK) offers multiple ways to open the editor. Whether you're starting with a blank canvas or importing complex layered files, CE.SDK gives you the building blocks to launch an editing session tailored to your users' needs. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Ways to Open the Editor You can initialize CE.SDK in several ways depending on your content pipeline: - [Start with a Blank Canvas](https://img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05/)
Create new content from scratch by defining the canvas dimensions manually or programmatically. - [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/)
Restore a saved scene from serialized scene data, a scene file, or a self-contained archive. - Create from Media
Initialize the editor with a preloaded [image](https://img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e/) or [video](https://img.ly/docs/cesdk/android/open-the-editor/from-video-86beb0/). - [Create From Template](https://img.ly/docs/cesdk/android/open-the-editor/from-template-46c096/)
Kick off the editor with a predefined template, including placeholders and editing constraints. - [Import a Design](https://img.ly/docs/cesdk/android/open-the-editor/import-design-73b9c5/)
Import external designs with the relevant importer, then load the resulting scene or archive in the SDK. ## Set the Zoom Level After a scene is open, use [Set Zoom Level](https://img.ly/docs/cesdk/android/open-the-editor/set-zoom-level-d31896/) to control the viewport and focus the canvas on the content your user should inspect next. ## Using Low-Quality / High-Quality Assets To ensure responsive editing and high-quality exports, CE.SDK allows you to dynamically switch between asset resolutions: - **Edit with Low-Res Assets**
Load smaller versions of images or videos during the editing process to reduce memory usage and improve performance. - **Export with High-Res Assets**
Swap out low-res placeholders for full-quality assets just before exporting. This can be handled using the Scene or Block APIs by switching asset paths or making use of source sets for fills. > **Note:** This pattern is commonly used in design systems that require high-resolution > print or web output while maintaining editing performance. ## Working with Watermarked or Placeholder Media CE.SDK supports licensing-based workflows where full-resolution assets are only available after purchase or user action: - **Use Watermarked or Preview Media on Load**
Start with branded, obfuscated, or watermarked assets to limit unauthorized use. - **Swap with Purchased Assets Post-Checkout**
Replace asset URIs within the same scene structure using a one-time update, ensuring consistency without disrupting layout or styling. ## Implementing a Custom URI Resolver Use [URI resolver APIs](https://img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624/) to intercept and customize asset loading: - **Why Use a URI Resolver?**
Handle dynamic URL rewriting, signed query parameters, asset migration, CDN fallbacks, or redirects to internal mirrors. - **How It Works**
The engine routes every asset URI through your custom resolver function. This function returns the final, resolved URI used for the current fetch operation. - **Recommended Use Cases**: - Append signed query params - Redirect public assets to internal mirrors - Refresh tokenized URLs before they expire --- ## 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 Zoom Level" description: "Control the canvas zoom level with the CE.SDK Engine - set and read zoom, frame blocks, follow content with auto-fit, clamp the camera, and observe zoom changes." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/set-zoom-level-d31896/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [Set Zoom Level](https://img.ly/docs/cesdk/android/open-the-editor/set-zoom-level-d31896/) --- ```kotlin file=@cesdk_android_examples/engine-guides-set-zoom-level/SetZoomLevel.kt reference-only import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.withTimeout import kotlinx.coroutines.yield import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.UnstableEngineApi import ly.img.engine.ZoomAutoFitAxis data class SetZoomLevelSummary( val zoom100: Float, val zoom50: Float, val autoFitEnabled: Boolean, val autoFitDisabled: Boolean, val zoomClampingEnabled: Boolean, val positionClampingEnabled: Boolean, val finalZoom: Float, ) @OptIn(UnstableEngineApi::class) suspend fun setZoomLevel(engine: Engine): SetZoomLevelSummary = coroutineScope { 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.setZoomLevel(level = 1F) val zoom100 = engine.scene.getZoomLevel() engine.scene.setZoomLevel(level = zoom100 * 0.5F) val zoom50 = engine.scene.getZoomLevel() engine.scene.zoomToBlock( block = page, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, ) engine.scene.immediateZoomToBlock( block = page, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, forceUpdate = true, ) engine.scene.enableZoomAutoFit( block = page, axis = ZoomAutoFitAxis.BOTH, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, ) val autoFitEnabled = engine.scene.isZoomAutoFitEnabled(page) engine.scene.disableZoomAutoFit(page) val autoFitDisabled = engine.scene.isZoomAutoFitEnabled(page).not() val zoomClampingEnabled = configureZoomClamping(engine = engine, page = page) val positionClampingEnabled = configurePositionClamping(engine = engine, scene = scene) engine.scene.setZoomLevel(level = 2F) val finalZoom = engine.scene.getZoomLevel() val summary = SetZoomLevelSummary( zoom100 = zoom100, zoom50 = zoom50, autoFitEnabled = autoFitEnabled, autoFitDisabled = autoFitDisabled, zoomClampingEnabled = zoomClampingEnabled, positionClampingEnabled = positionClampingEnabled, finalZoom = finalZoom, ) engine.scene.disableCameraZoomClamping() engine.scene.disableCameraPositionClamping() summary } @OptIn(UnstableEngineApi::class) fun configureZoomClamping( engine: Engine, page: DesignBlock, ): Boolean { engine.scene.enableCameraZoomClamping( blocks = listOf(page), minZoomLimit = 0.125F, maxZoomLimit = 8F, ) val zoomClampingEnabled = engine.scene.isCameraZoomClampingEnabled(page) return zoomClampingEnabled } fun observeZoomLevel( engine: Engine, zoomControlScope: CoroutineScope, onZoomChanged: (Float) -> Unit, ): Job = engine.scene.onZoomLevelChanged() .onEach { onZoomChanged(engine.scene.getZoomLevel()) } .launchIn(zoomControlScope) suspend fun verifyZoomChangeSubscription( engine: Engine, zoomControlScope: CoroutineScope, pumpEngine: () -> Unit, ): Float { val observedEventCount = CompletableDeferred() val observedZoomLevels = mutableListOf() val zoomEvents = observeZoomLevel( engine = engine, zoomControlScope = zoomControlScope, ) { zoomLevel -> observedZoomLevels += zoomLevel if (observedZoomLevels.size == 3) { observedEventCount.complete(observedZoomLevels.size) } } // The offscreen smoke test has no render loop, so yield until the Flow is // subscribed and manually pump the engine after each zoom change. yield() pumpEngine() listOf(1F, 2F, 3F).forEach { zoomLevel -> engine.scene.setZoomLevel(level = zoomLevel) pumpEngine() yield() } withTimeout(5_000) { while (isActive && observedEventCount.isCompleted.not()) { pumpEngine() yield() } observedEventCount.await() } zoomEvents.cancel() return observedZoomLevels.last() } @OptIn(UnstableEngineApi::class) fun configurePositionClamping( engine: Engine, scene: DesignBlock, ): Boolean { engine.scene.enableCameraPositionClamping( blocks = listOf(scene), paddingLeft = 10F, paddingTop = 10F, paddingRight = 10F, paddingBottom = 10F, scaledPaddingLeft = 0F, scaledPaddingTop = 0F, scaledPaddingRight = 0F, scaledPaddingBottom = 0F, ) val positionClampingEnabled = engine.scene.isCameraPositionClampingEnabled(scene) return positionClampingEnabled } ``` Control how much of a design is visible by driving the camera zoom from code. Set an exact zoom level, frame a block, follow content as it resizes, constrain the camera, and react to zoom changes through the Engine `scene` API. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-set-zoom-level) The zoom level is a ratio between design dots and screen pixels. A zoom level of `1F` shows one design dot as one screen pixel; `2F` shows it as two. The snippets below operate on the active scene and a valid `page` block in that scene. ## Get and Set the Zoom Level Set an absolute zoom level with `engine.scene.setZoomLevel()` and read the current value with `engine.scene.getZoomLevel()`. Reading first lets you apply a relative change, such as halving the current zoom. ```kotlin highlight-android-get-set-zoom-level engine.scene.setZoomLevel(level = 1F) val zoom100 = engine.scene.getZoomLevel() engine.scene.setZoomLevel(level = zoom100 * 0.5F) val zoom50 = engine.scene.getZoomLevel() ``` ## Zoom to a Block Frame a specific block - the scene, a page, or any element - with `engine.scene.zoomToBlock()`. Padding is measured in screen pixels and leaves space around the block. Use `engine.scene.immediateZoomToBlock()` when the layout is already up to date and you need the camera to move synchronously. ```kotlin highlight-android-zoom-to-block engine.scene.zoomToBlock( block = page, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, ) engine.scene.immediateZoomToBlock( block = page, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, forceUpdate = true, ) ``` ## Auto-Fit Zoom Auto-fit continuously refits a block as its bounding box changes. Choose the axis with `ZoomAutoFitAxis.BOTH`, `ZoomAutoFitAxis.HORIZONTAL`, or `ZoomAutoFitAxis.VERTICAL`. Auto-fit only has a visible effect while the zoom is not controlled by an editor UI layer, and calling `setZoomLevel()` or `zoomToBlock()` disables it. ```kotlin highlight-android-auto-fit-zoom engine.scene.enableZoomAutoFit( block = page, axis = ZoomAutoFitAxis.BOTH, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, ) val autoFitEnabled = engine.scene.isZoomAutoFitEnabled(page) ``` ## Disable Auto-Fit Stop following a block with `engine.scene.disableZoomAutoFit()`, and check whether auto-fit is still active with `engine.scene.isZoomAutoFitEnabled()`. ```kotlin highlight-android-disable-auto-fit engine.scene.disableZoomAutoFit(page) val autoFitDisabled = engine.scene.isZoomAutoFitEnabled(page).not() ``` ## Limit the Zoom Range Use `engine.scene.enableCameraZoomClamping()` to keep the camera zoom within a range relative to one or more blocks. Negative limits mean unbounded. The camera-clamping APIs are annotated with `@UnstableEngineApi`, so opt in at the calling scope. ```kotlin highlight-android-zoom-clamping @OptIn(UnstableEngineApi::class) fun configureZoomClamping( engine: Engine, page: DesignBlock, ): Boolean { engine.scene.enableCameraZoomClamping( blocks = listOf(page), minZoomLimit = 0.125F, maxZoomLimit = 8F, ) val zoomClampingEnabled = engine.scene.isCameraZoomClampingEnabled(page) return zoomClampingEnabled } ``` ## Constrain the Camera Position Use `engine.scene.enableCameraPositionClamping()` to keep panning within the bounds of the supplied blocks. Padding defines how far the camera can move past those bounds; scaled padding grows with the zoom level until it reaches five times the initial value. ```kotlin highlight-android-position-clamping @OptIn(UnstableEngineApi::class) fun configurePositionClamping( engine: Engine, scene: DesignBlock, ): Boolean { engine.scene.enableCameraPositionClamping( blocks = listOf(scene), paddingLeft = 10F, paddingTop = 10F, paddingRight = 10F, paddingBottom = 10F, scaledPaddingLeft = 0F, scaledPaddingTop = 0F, scaledPaddingRight = 0F, scaledPaddingBottom = 0F, ) val positionClampingEnabled = engine.scene.isCameraPositionClampingEnabled(scene) return positionClampingEnabled } ``` ## Subscribe to Zoom Changes `engine.scene.onZoomLevelChanged()` returns a `Flow` that emits whenever the zoom changes. Collect it from your UI scope to keep custom controls in sync, and cancel the returned job when the control leaves the screen. ```kotlin highlight-android-subscribe-zoom-changes fun observeZoomLevel( engine: Engine, zoomControlScope: CoroutineScope, onZoomChanged: (Float) -> Unit, ): Job = engine.scene.onZoomLevelChanged() .onEach { onZoomChanged(engine.scene.getZoomLevel()) } .launchIn(zoomControlScope) ``` ## API Reference | Method | Description | | --- | --- | | `engine.scene.setZoomLevel(level=_)` | Set the active scene's zoom level. | | `engine.scene.getZoomLevel()` | Read the active scene's current zoom level. | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Asynchronously frame a block after its dimensions are known, with optional per-side padding. | | `engine.scene.immediateZoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_, forceUpdate=_)` | Frame a block synchronously; pass `forceUpdate=true` to run a layout pass first. | | `engine.scene.enableZoomAutoFit(block=_, axis=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Continuously refit a block on the selected `ZoomAutoFitAxis`. | | `engine.scene.disableZoomAutoFit(block=_)` | Stop a previously enabled auto-fit. | | `engine.scene.isZoomAutoFitEnabled(block=_)` | Query whether auto-fit is enabled for a block or scene. | | `engine.scene.enableCameraZoomClamping(blocks=_, minZoomLimit=_, maxZoomLimit=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Constrain the zoom range relative to the supplied blocks. Experimental. | | `engine.scene.disableCameraZoomClamping()` | Remove zoom clamping. Experimental. | | `engine.scene.isCameraZoomClampingEnabled(blockOrScene=_)` | Query whether zoom clamping is enabled. Experimental. | | `engine.scene.enableCameraPositionClamping(blocks=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_, scaledPaddingLeft=_, scaledPaddingTop=_, scaledPaddingRight=_, scaledPaddingBottom=_)` | Keep the camera position inside the supplied block bounds. Experimental. | | `engine.scene.disableCameraPositionClamping()` | Remove position clamping. Experimental. | | `engine.scene.isCameraPositionClampingEnabled(blockOrScene=_)` | Query whether position clamping is enabled. Experimental. | | `engine.scene.onZoomLevelChanged()` | Subscribe to zoom-level change events as a Kotlin `Flow`. | ## Troubleshooting | Problem | Resolution | | --- | --- | | Zoom level does not change | Confirm a scene exists before calling zoom methods, and that no editor UI layer is overriding the zoom. | | Auto-fit has no effect | Only one block per scene can drive auto-fit, and `setZoomLevel()` or `zoomToBlock()` disables it. Pass a valid block that belongs to the active scene. | | Zoom feels capped | An active zoom clamp limits the range. Check with `isCameraZoomClampingEnabled()` and adjust or remove it. | ## Next Steps - [Start With Blank Canvas](https://img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05/) - Start the editor with an empty scene to zoom into. - [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Open an existing design before adjusting the camera. --- ## 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: "URI Resolver" description: "Intercept and transform asset URIs before CE.SDK loads them for authentication, redirects, and custom resolution logic." platform: android url: "https://img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) > [URI Resolver](https://img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624/) --- ```kotlin file=@cesdk_android_examples/engine-guides-uri-resolver/UriResolver.kt reference-only import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine private const val ASSET_BASE_PATH = "file:///android_asset" suspend fun uriResolver(engine: Engine): List = withContext(Dispatchers.Main) { val originalBasePath = engine.editor.getSettingString("basePath") engine.editor.setSettingString(keypath = "basePath", value = ASSET_BASE_PATH) try { val relativeAsset = Uri.parse("images/poster.jpg") val protectedAsset = Uri.parse("https://assets.example.com/protected/poster.jpg") val defaultRelativeUri = engine.editor.getAbsoluteUri(uri = relativeAsset) val defaultAbsoluteUri = engine.editor.getAbsoluteUri(uri = protectedAsset) val appAssetHost = "app.example.com" val storageHost = "assets.example.com" val appAsset = Uri.parse("https://$appAssetHost/images/poster.jpg") engine.editor.setUriResolver { uri -> if (uri.host == appAssetHost) { uri .buildUpon() .authority(storageHost) .build() } else { engine.editor.defaultUriResolver(uri) } } val rewrittenAppAssetUri = engine.editor.getAbsoluteUri(uri = appAsset) val protectedHost = "assets.example.com" val protectedPrefix = "/protected/" val token = "precomputed-session-token" engine.editor.setUriResolver { uri -> val shouldAuthenticate = uri.host == protectedHost && uri.path?.startsWith(protectedPrefix) == true if (shouldAuthenticate) { uri .buildUpon() .appendQueryParameter("token", token) .build() } else { engine.editor.defaultUriResolver(uri) } } val authenticatedUri = engine.editor.getAbsoluteUri(uri = protectedAsset) val delegatedRelativeUri = engine.editor.getAbsoluteUri(uri = relativeAsset) engine.editor.setUriResolver(null) val restoredRelativeUri = engine.editor.getAbsoluteUri(uri = relativeAsset) listOf( defaultRelativeUri, defaultAbsoluteUri, rewrittenAppAssetUri, authenticatedUri, delegatedRelativeUri, restoredRelativeUri, ) } finally { engine.editor.setUriResolver(null) engine.editor.setUriResolverAsync(null) engine.editor.setSettingString(keypath = "basePath", value = originalBasePath) } } ``` Learn how to intercept and transform asset URIs in CE.SDK, enabling authentication and custom resolution logic. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-uri-resolver) When CE.SDK loads an asset, it resolves the requested URI to an absolute `Uri` before fetching it. You can intercept this step to add authentication tokens, redirect to a different host, or transform URIs to match your application's asset storage. ## Default URI Resolution By default, CE.SDK resolves relative paths against the Engine `basePath` setting. Absolute URIs with a scheme, such as `https://` or `file://`, pass through unchanged. Use `getAbsoluteUri(uri=_)` to preview how a path resolves without loading the asset: ```kotlin highlight-android-test-resolution val relativeAsset = Uri.parse("images/poster.jpg") val protectedAsset = Uri.parse("https://assets.example.com/protected/poster.jpg") val defaultRelativeUri = engine.editor.getAbsoluteUri(uri = relativeAsset) val defaultAbsoluteUri = engine.editor.getAbsoluteUri(uri = protectedAsset) ``` ## Custom URI Resolver Register a custom resolver with `setUriResolver(resolver=_)`. CE.SDK routes each requested `Uri` through your resolver and fetches the absolute `Uri` it returns for that request only. ```kotlin highlight-android-set-resolver val appAssetHost = "app.example.com" val storageHost = "assets.example.com" val appAsset = Uri.parse("https://$appAssetHost/images/poster.jpg") engine.editor.setUriResolver { uri -> if (uri.host == appAssetHost) { uri .buildUpon() .authority(storageHost) .build() } else { engine.editor.defaultUriResolver(uri) } } val rewrittenAppAssetUri = engine.editor.getAbsoluteUri(uri = appAsset) ``` Call `defaultUriResolver(uri=_)` for paths you do not transform. Without that fallback, unmatched relative paths stay unresolved. ## Adding Authentication A common use case is attaching a token to protected asset URIs. The synchronous resolver is not a `suspend` function, so generate tokens before registering it and only read those prepared values inside the resolver. ```kotlin highlight-android-authentication val protectedHost = "assets.example.com" val protectedPrefix = "/protected/" val token = "precomputed-session-token" engine.editor.setUriResolver { uri -> val shouldAuthenticate = uri.host == protectedHost && uri.path?.startsWith(protectedPrefix) == true if (shouldAuthenticate) { uri .buildUpon() .appendQueryParameter("token", token) .build() } else { engine.editor.defaultUriResolver(uri) } } val authenticatedUri = engine.editor.getAbsoluteUri(uri = protectedAsset) val delegatedRelativeUri = engine.editor.getAbsoluteUri(uri = relativeAsset) ``` Your asset server validates the query parameter and serves the protected asset. If each request needs a backend exchange for a short-lived URL, register a suspendable resolver with `setUriResolverAsync(resolver=_)` instead. ## Removing a Resolver Pass `null` to the resolver setter to restore default behavior. A later synchronous or asynchronous resolver replaces the active custom resolver. ```kotlin highlight-android-remove-resolver engine.editor.setUriResolver(null) val restoredRelativeUri = engine.editor.getAbsoluteUri(uri = relativeAsset) ``` ## Key Constraints - **Return absolute URIs**: Always return a `Uri` with a scheme, such as `https://`, `file://`, or `content://`. - **Choose sync or async deliberately**: Use `setUriResolver(resolver=_)` for precomputed values. Use `setUriResolverAsync(resolver=_)` when resolution must suspend for I/O. - **Keep one resolver active**: New resolver registrations replace previous ones. - **Delegate unmatched paths**: Call `defaultUriResolver(uri=_)` for any URI your custom logic does not handle. ## API Reference | API | Purpose | | --- | --- | | `engine.editor.getAbsoluteUri(uri=_)` | Resolves a `Uri` through the active custom resolver or default resolver without loading the asset | | `engine.editor.setUriResolver(resolver=_)` | Registers a non-suspending resolver that returns an absolute `Uri` | | `engine.editor.setUriResolver(null)` | Clears the active custom resolver through the synchronous setter | | `engine.editor.setUriResolverAsync(resolver=_)` | Registers a suspendable resolver for I/O-backed URI resolution | | `engine.editor.setUriResolverAsync(null)` | Clears the active custom resolver through the asynchronous setter | | `engine.editor.defaultUriResolver(uri=_)` | Applies CE.SDK's default `basePath` resolution for delegated URIs | ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) — Learn how to configure CE.SDK to match your application's functional, visual, and performance requirements. - [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) — Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely. --- ## 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: "Outlines" description: "Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal." platform: android url: "https://img.ly/docs/cesdk/android/outlines-b7820c/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/android/outlines-b7820c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/outlines/overview-dfeb12/) - Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal. - [Using Strokes](https://img.ly/docs/cesdk/android/outlines/strokes-c2e621/) - Add and customize outlines around shapes, text, or images using stroke settings. - [Shadows and Glows](https://img.ly/docs/cesdk/android/outlines/shadows-and-glows-6610fa/) - Add visual depth and emphasis to design elements using drop shadows and glow 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: "Overview" description: "Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal." platform: android url: "https://img.ly/docs/cesdk/android/outlines/overview-dfeb12/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/android/outlines-b7820c/) > [Overview](https://img.ly/docs/cesdk/android/outlines/overview-dfeb12/) --- In CreativeEditor SDK (CE.SDK), *outlines* refer to visual enhancements added around design elements. They include strokes, shadows, and glows, each serving to emphasize, separate, or stylize content. Outlines help improve visibility, create visual contrast, and enhance the overall aesthetic of a design. You can add, edit, and remove outlines both through the CE.SDK user interface and programmatically via the API. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Next Steps - [Using Strokes](https://img.ly/docs/cesdk/android/outlines/strokes-c2e621/) - Add and customize outlines around shapes, text, and images. - [Shadows and Glows](https://img.ly/docs/cesdk/android/outlines/shadows-and-glows-6610fa/) - Add depth and emphasis with shadow and glow 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: "Shadows and Glows" description: "Add visual depth and emphasis to design elements using drop shadows and glow effects." platform: android url: "https://img.ly/docs/cesdk/android/outlines/shadows-and-glows-6610fa/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/android/outlines-b7820c/) > [Shadows and Glows](https://img.ly/docs/cesdk/android/outlines/shadows-and-glows-6610fa/) --- ```kotlin file=@cesdk_android_examples/engine-guides-shadows-and-glows/ShadowsAndGlows.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.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.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.ShapeType import ly.img.engine.SizeMode import ly.img.engine.Typeface private const val TAG = "ShadowsAndGlows" suspend fun shadowsAndGlows(engine: Engine): List = withContext(Dispatchers.Main) { 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 textBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = textBlock, text = "Soft Shadow") val regularFont = Font( uri = Uri.parse("file:///android_asset/imgly-assets/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ) val typeface = Typeface(name = "Fira Sans", fonts = listOf(regularFont)) engine.block.setFont( block = textBlock, fontFileUri = regularFont.uri, typeface = typeface, ) engine.block.setWidthMode(block = textBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = textBlock, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = textBlock, value = 640F) engine.block.setHeight(block = textBlock, value = 96F) engine.block.setTextFontSize(block = textBlock, fontSize = 36F) engine.block.setTextColor(block = textBlock, color = Color.fromRGBA(r = 0.08F, g = 0.12F, b = 0.2F, a = 1F)) engine.block.setPositionX(block = textBlock, value = 72F) engine.block.setPositionY(block = textBlock, value = 64F) engine.block.appendChild(parent = page, child = textBlock) val glowBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = glowBlock, shape = engine.block.createShape(ShapeType.Rect)) val glowImageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = glowImageFill, property = "fill/image/imageFileURI", value = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) engine.block.setFill(block = glowBlock, fill = glowImageFill) engine.block.setPositionX(block = glowBlock, value = 104F) engine.block.setPositionY(block = glowBlock, value = 210F) engine.block.setWidth(block = glowBlock, value = 220F) engine.block.setHeight(block = glowBlock, value = 180F) engine.block.appendChild(parent = page, child = glowBlock) val shapeBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = shapeBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block = shapeBlock, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = shapeBlock, color = Color.fromRGBA(r = 0.94F, g = 0.22F, b = 0.37F, a = 1F), ) engine.block.setPositionX(block = shapeBlock, value = 440F) engine.block.setPositionY(block = shapeBlock, value = 210F) engine.block.setWidth(block = shapeBlock, value = 220F) engine.block.setHeight(block = shapeBlock, value = 180F) engine.block.appendChild(parent = page, child = shapeBlock) val textSupportsDropShadow = engine.block.supportsDropShadow(textBlock) if (textSupportsDropShadow) { engine.block.setDropShadowEnabled(block = textBlock, enabled = true) } if (textSupportsDropShadow) { engine.block.setDropShadowColor( block = textBlock, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.45F), ) } if (textSupportsDropShadow) { engine.block.setDropShadowOffsetX(block = textBlock, offsetX = 12F) engine.block.setDropShadowOffsetY(block = textBlock, offsetY = 16F) } if (textSupportsDropShadow) { engine.block.setDropShadowBlurRadiusX(block = textBlock, blurRadiusX = 18F) engine.block.setDropShadowBlurRadiusY(block = textBlock, blurRadiusY = 18F) } if (engine.block.supportsDropShadow(shapeBlock)) { engine.block.setDropShadowEnabled(block = shapeBlock, enabled = true) engine.block.setDropShadowClip(block = shapeBlock, clip = true) } val glowSupported = engine.block.supportsEffects(glowBlock) val glowEffect = if (glowSupported) { engine.block.createEffect(type = EffectType.Glow).also { effect -> engine.block.appendEffect(block = glowBlock, effectBlock = effect) } } else { null } glowEffect?.let { effect -> engine.block.setFloat(block = effect, property = "effect/glow/size", value = 8F) engine.block.setFloat(block = effect, property = "effect/glow/amount", value = 0.7F) engine.block.setFloat(block = effect, property = "effect/glow/darkness", value = 0.25F) } if (engine.block.supportsDropShadow(shapeBlock)) { engine.block.setDropShadowEnabled(block = shapeBlock, enabled = true) engine.block.setDropShadowColor( block = shapeBlock, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.35F), ) engine.block.setDropShadowOffsetX(block = shapeBlock, offsetX = 10F) engine.block.setDropShadowOffsetY(block = shapeBlock, offsetY = 14F) engine.block.setDropShadowBlurRadiusX(block = shapeBlock, blurRadiusX = 14F) engine.block.setDropShadowBlurRadiusY(block = shapeBlock, blurRadiusY = 14F) } if (engine.block.supportsEffects(shapeBlock)) { val combinedGlow = engine.block.createEffect(type = EffectType.Glow) engine.block.appendEffect(block = shapeBlock, effectBlock = combinedGlow) engine.block.setFloat(block = combinedGlow, property = "effect/glow/size", value = 6F) engine.block.setFloat(block = combinedGlow, property = "effect/glow/amount", value = 0.5F) engine.block.setFloat(block = combinedGlow, property = "effect/glow/darkness", value = 0.15F) } if (textSupportsDropShadow) { engine.block.setDropShadowEnabled(block = textBlock, enabled = false) val shadowIsDisabled = !engine.block.isDropShadowEnabled(textBlock) Log.i(TAG, "Drop shadow disabled: $shadowIsDisabled") engine.block.setDropShadowEnabled(block = textBlock, enabled = true) } glowEffect?.let { effect -> engine.block.setEffectEnabled(effectBlock = effect, enabled = false) val glowIsDisabled = !engine.block.isEffectEnabled(effect) Log.i(TAG, "Glow disabled: $glowIsDisabled") engine.block.setEffectEnabled(effectBlock = effect, enabled = true) } glowEffect?.let { effect -> val glowIndex = engine.block.getEffects(glowBlock).indexOf(effect) require(glowIndex >= 0) { "The glow effect must be attached before it can be removed." } engine.block.removeEffect(block = glowBlock, index = glowIndex) engine.block.destroy(effect) } if (glowSupported) { engine.block.createEffect(type = EffectType.Glow).also { effect -> engine.block.appendEffect(block = glowBlock, effectBlock = effect) engine.block.setFloat(block = effect, property = "effect/glow/size", value = 8F) engine.block.setFloat(block = effect, property = "effect/glow/amount", value = 0.7F) engine.block.setFloat(block = effect, property = "effect/glow/darkness", value = 0.25F) } } engine.block.forceLoadResources(listOf(textBlock, glowBlock, shapeBlock)) listOf(textBlock, glowBlock, shapeBlock) } ``` Add visual depth and emphasis to design elements using drop shadows and glow effects in CE.SDK. ![Android result preview showing image glow and combined shape shadow and glow](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-shadows-and-glows) Drop shadows create the illusion of elements floating above the canvas, while glow effects add luminous halos that make elements stand out. On Android, drop shadows are native block properties and glow effects are configured through the effects system. This guide covers configuring drop shadows with dedicated Engine API methods and applying glow effects with `EffectType.Glow`. ## Using the Built-In UI The Android editor does not provide built-in controls for drop shadows; configure them with the Engine APIs below. Glow can be available to users through the effects sheet (`SheetType.Effect`) depending on your editor configuration. ## Drop Shadow Configuration Drop shadows are native block properties configured directly on supported blocks. ### Check Support and Enable Before configuring drop shadows, verify that the selected block supports them. Then enable the shadow. ```kotlin highlight-android-check-drop-shadow-support val textSupportsDropShadow = engine.block.supportsDropShadow(textBlock) ``` ```kotlin highlight-android-enable-drop-shadow if (textSupportsDropShadow) { engine.block.setDropShadowEnabled(block = textBlock, enabled = true) } ``` ### Set Shadow Color Configure the shadow color with `setDropShadowColor()`. This example uses the Float `Color.fromRGBA()` overload, where red, green, blue, and alpha values range from `0.0` to `1.0`. ```kotlin highlight-android-set-drop-shadow-color if (textSupportsDropShadow) { engine.block.setDropShadowColor( block = textBlock, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.45F), ) } ``` ### Set Shadow Position Control horizontal and vertical offset with `setDropShadowOffsetX()` and `setDropShadowOffsetY()`. Positive values move the shadow right and down; negative values move it left and up. ```kotlin highlight-android-set-drop-shadow-offset if (textSupportsDropShadow) { engine.block.setDropShadowOffsetX(block = textBlock, offsetX = 12F) engine.block.setDropShadowOffsetY(block = textBlock, offsetY = 16F) } ``` ### Configure Blur Radius Set shadow softness with `setDropShadowBlurRadiusX()` and `setDropShadowBlurRadiusY()`. Higher values create softer shadows. ```kotlin highlight-android-set-drop-shadow-blur if (textSupportsDropShadow) { engine.block.setDropShadowBlurRadiusX(block = textBlock, blurRadiusX = 18F) engine.block.setDropShadowBlurRadiusY(block = textBlock, blurRadiusY = 18F) } ``` ### Shadow Clipping For shape blocks, `setDropShadowClip(..., true)` removes the shadow from the shape or stroke area, so the shadow appears outside the opaque shape instead of filling its interior. Enable the shape shadow first, and set clipping on a graphic shape block rather than on a text block. ```kotlin highlight-android-set-shape-shadow-clip if (engine.block.supportsDropShadow(shapeBlock)) { engine.block.setDropShadowEnabled(block = shapeBlock, enabled = true) engine.block.setDropShadowClip(block = shapeBlock, clip = true) } ``` ## Glow Effect Configuration Glow effects are effect blocks. Create them through the effects system and attach them to blocks that support effects. ### Check Support and Create Glow Use `supportsEffects()` before adding a glow. Create the effect with the type-safe `EffectType.Glow` constant and append it to the target block. ```kotlin highlight-android-check-glow-support val glowSupported = engine.block.supportsEffects(glowBlock) ``` ```kotlin highlight-android-create-glow-effect val glowEffect = if (glowSupported) { engine.block.createEffect(type = EffectType.Glow).also { effect -> engine.block.appendEffect(block = glowBlock, effectBlock = effect) } } else { null } ``` ### Configure Glow Parameters Glow parameters are effect properties, so Android configures them with `setFloat()`: - `effect/glow/size` - Controls the spread of the glow - `effect/glow/amount` - Controls glow intensity from `0.0` to `1.0` - `effect/glow/darkness` - Controls the darkness/opacity of the glow ```kotlin highlight-android-configure-glow glowEffect?.let { effect -> engine.block.setFloat(block = effect, property = "effect/glow/size", value = 8F) engine.block.setFloat(block = effect, property = "effect/glow/amount", value = 0.7F) engine.block.setFloat(block = effect, property = "effect/glow/darkness", value = 0.25F) } ``` ## Combining Shadows and Glows Drop shadows and glow effects can both be applied to the same block. Drop shadows render independently from the effects stack, so both treatments appear at the same time. ```kotlin highlight-android-combine-shadow-glow if (engine.block.supportsDropShadow(shapeBlock)) { engine.block.setDropShadowEnabled(block = shapeBlock, enabled = true) engine.block.setDropShadowColor( block = shapeBlock, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.35F), ) engine.block.setDropShadowOffsetX(block = shapeBlock, offsetX = 10F) engine.block.setDropShadowOffsetY(block = shapeBlock, offsetY = 14F) engine.block.setDropShadowBlurRadiusX(block = shapeBlock, blurRadiusX = 14F) engine.block.setDropShadowBlurRadiusY(block = shapeBlock, blurRadiusY = 14F) } if (engine.block.supportsEffects(shapeBlock)) { val combinedGlow = engine.block.createEffect(type = EffectType.Glow) engine.block.appendEffect(block = shapeBlock, effectBlock = combinedGlow) engine.block.setFloat(block = combinedGlow, property = "effect/glow/size", value = 6F) engine.block.setFloat(block = combinedGlow, property = "effect/glow/amount", value = 0.5F) engine.block.setFloat(block = combinedGlow, property = "effect/glow/darkness", value = 0.15F) } ``` ## Managing Shadow and Glow State ### Toggle Drop Shadows Enable or disable a drop shadow without removing its configured color, offset, or blur values. Query the current state with `isDropShadowEnabled()`. ```kotlin highlight-android-toggle-shadow if (textSupportsDropShadow) { engine.block.setDropShadowEnabled(block = textBlock, enabled = false) val shadowIsDisabled = !engine.block.isDropShadowEnabled(textBlock) Log.i(TAG, "Drop shadow disabled: $shadowIsDisabled") engine.block.setDropShadowEnabled(block = textBlock, enabled = true) } ``` ### Toggle Glow Effects Enable or disable a glow effect without removing it from the block. Query the current state with `isEffectEnabled()`. ```kotlin highlight-android-toggle-glow glowEffect?.let { effect -> engine.block.setEffectEnabled(effectBlock = effect, enabled = false) val glowIsDisabled = !engine.block.isEffectEnabled(effect) Log.i(TAG, "Glow disabled: $glowIsDisabled") engine.block.setEffectEnabled(effectBlock = effect, enabled = true) } ``` ### Remove Glow Effects To remove an existing glow effect, read the block effect list with `getEffects()`, pass the matching index to `removeEffect()`, and destroy the detached effect block when you no longer need it. ```kotlin highlight-android-remove-glow-effect glowEffect?.let { effect -> val glowIndex = engine.block.getEffects(glowBlock).indexOf(effect) require(glowIndex >= 0) { "The glow effect must be attached before it can be removed." } engine.block.removeEffect(block = glowBlock, index = glowIndex) engine.block.destroy(effect) } ``` ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Shadow is not visible | The block does not support drop shadows, the shadow is disabled, or the color alpha is too low | Check `supportsDropShadow()`, call `setDropShadowEnabled(..., true)`, and use a visible alpha value | | Shadow appears too sharp or too far away | Offset or blur values are too small or too large for the block size | Adjust `setDropShadowOffsetX/Y()` and `setDropShadowBlurRadiusX/Y()` together | | Glow is not visible | The block does not support effects, the effect is disabled, or size and amount are near zero | Check `supportsEffects()`, call `setEffectEnabled(..., true)`, and increase `effect/glow/size` or `effect/glow/amount` | | Rendering feels expensive | Large blur or glow values can cost more on mobile devices | Use moderate values and disable shadows or glows while running intensive editing operations | ## API Reference | API | Purpose | | --- | --- | | `engine.block.supportsDropShadow(block)` | Checks if a block supports drop shadow properties | | `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enables or disables the block's drop shadow | | `engine.block.isDropShadowEnabled(block)` | Checks whether the block's drop shadow is enabled | | `engine.block.setDropShadowColor(block=_, color=_)` | Sets the drop shadow color | | `engine.block.getDropShadowColor(block)` | Reads the current drop shadow color | | `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Sets the horizontal shadow offset | | `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Sets the vertical shadow offset | | `engine.block.getDropShadowOffsetX(block)` | Reads the horizontal shadow offset | | `engine.block.getDropShadowOffsetY(block)` | Reads the vertical shadow offset | | `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Sets horizontal shadow blur | | `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Sets vertical shadow blur | | `engine.block.getDropShadowBlurRadiusX(block)` | Reads horizontal shadow blur | | `engine.block.getDropShadowBlurRadiusY(block)` | Reads vertical shadow blur | | `engine.block.setDropShadowClip(block=_, clip=_)` | Controls shadow clipping behavior for shape blocks | | `engine.block.getDropShadowClip(block)` | Reads the current shadow clipping state | | `engine.block.supportsEffects(block)` | Checks if a block supports effects | | `engine.block.createEffect(type=EffectType.Glow)` | Creates a glow effect block | | `engine.block.insertEffect(block=_, effectBlock=_, index=_)` | Inserts an effect at a specific stack position | | `engine.block.appendEffect(block=_, effectBlock=_)` | Adds an effect to a block's effect stack | | `engine.block.setFloat(block=_, property="effect/glow/size", value=_)` | Sets the glow spread | | `engine.block.setFloat(block=_, property="effect/glow/amount", value=_)` | Sets the glow intensity | | `engine.block.setFloat(block=_, property="effect/glow/darkness", value=_)` | Sets the glow darkness | | `engine.block.setEffectEnabled(effectBlock=_, enabled=_)` | Enables or disables an effect | | `engine.block.isEffectEnabled(effectBlock)` | Checks whether an effect is enabled | | `engine.block.getEffects(block)` | Reads the effects attached to a block | | `engine.block.removeEffect(block=_, index=_)` | Removes an effect from a block's effect stack | | `engine.block.destroy(block=_)` | Destroys a detached or unused effect block | ## Next Steps - [Using Strokes](https://img.ly/docs/cesdk/android/outlines/strokes-c2e621/) - Add border outlines to 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. - [Blur Effects](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) - Apply blur effects to elements --- ## 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: "Using Strokes" description: "Add and customize outlines around shapes, text, or images using stroke settings." platform: android url: "https://img.ly/docs/cesdk/android/outlines/strokes-c2e621/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/android/outlines-b7820c/) > [Using Strokes](https://img.ly/docs/cesdk/android/outlines/strokes-c2e621/) --- ```kotlin file=@cesdk_android_examples/engine-guides-outlines-stroke/UsingStrokes.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.RGBAColor import ly.img.engine.ShapeType import ly.img.engine.StrokeCornerGeometry import ly.img.engine.StrokePosition import ly.img.engine.StrokeStyle fun usingStrokes(engine: Engine): UsingStrokesResult? { 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 = 400F) engine.block.setHeight(block, value = 300F) engine.block.setPositionX(block, value = 200F) engine.block.setPositionY(block, value = 150F) engine.block.setFill(block, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block, color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.95F, a = 1F), ) engine.block.appendChild(parent = page, child = block) val canHaveStroke = engine.block.supportsStroke(block = block) if (!canHaveStroke) { return null } engine.block.setStrokeEnabled(block = block, enabled = true) val strokeIsEnabled = engine.block.isStrokeEnabled(block = block) engine.block.setStrokeColor( block = block, color = Color.fromRGBA(r = 0F, g = 0.4F, b = 0.9F, a = 1F), ) val strokeColor = engine.block.getStrokeColor(block = block) engine.block.setStrokeWidth(block = block, width = 8F) val strokeWidth = engine.block.getStrokeWidth(block = block) engine.block.setStrokeStyle(block = block, style = StrokeStyle.DASHED) val strokeStyle = engine.block.getStrokeStyle(block = block) engine.block.setStrokePosition(block = block, position = StrokePosition.OUTER) val strokePosition = engine.block.getStrokePosition(block = block) engine.block.setStrokeCornerGeometry(block = block, geometry = StrokeCornerGeometry.ROUND) val strokeCornerGeometry = engine.block.getStrokeCornerGeometry(block = block) check(strokeIsEnabled) check(strokeColor is RGBAColor) check(strokeWidth > 0F) check(strokeStyle == StrokeStyle.DASHED) check(strokePosition == StrokePosition.OUTER) check(strokeCornerGeometry == StrokeCornerGeometry.ROUND) return UsingStrokesResult( canHaveStroke = canHaveStroke, strokeIsEnabled = engine.block.isStrokeEnabled(block = block), strokeColor = engine.block.getStrokeColor(block = block), strokeWidth = engine.block.getStrokeWidth(block = block), strokeStyle = engine.block.getStrokeStyle(block = block), strokePosition = engine.block.getStrokePosition(block = block), strokeCornerGeometry = engine.block.getStrokeCornerGeometry(block = block), ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-outlines-stroke/UsingStrokesResult.kt reference-only import ly.img.engine.Color import ly.img.engine.StrokeCornerGeometry import ly.img.engine.StrokePosition import ly.img.engine.StrokeStyle data class UsingStrokesResult( val canHaveStroke: Boolean, val strokeIsEnabled: Boolean = false, val strokeColor: Color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0F), val strokeWidth: Float = 0F, val strokeStyle: StrokeStyle = StrokeStyle.SOLID, val strokePosition: StrokePosition = StrokePosition.CENTER, val strokeCornerGeometry: StrokeCornerGeometry = StrokeCornerGeometry.MITER, ) ``` Add outlines around shapes, text, and graphics to create emphasis, separation, or decorative effects. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-outlines-stroke) Strokes are visual outlines on design blocks. This guide focuses on controlling stroke color, width, line pattern, position relative to the block edge, and corner geometry through the block API. Mutating stroke properties requires the `stroke/change` scope. Additional cap, custom dash, and PDF overprint APIs are listed in the API Reference. This guide covers the built-in Android editor controls for strokes and the programmatic APIs for applying the same settings. The code snippets use a rectangle graphic block named `block`, but the same API calls apply to any block after `supportsStroke()` returns `true`. ## Using the Built-in Stroke UI The Android editor shows stroke controls in the Fill & Stroke sheet, represented by `SheetType.FillStroke`, for selected blocks that support strokes and allow the `stroke/change` scope. That sheet can render fill controls, stroke controls, or both depending on the selected block. The color row controls stroke on/off: choosing no color disables the stroke, while choosing a swatch or custom color enables and colors it. - **No-color and color swatches** - Disable the stroke or apply a preset RGBA stroke color - **Color picker** - Choose a custom stroke color - **Width slider** - Adjust stroke thickness in design units - **Style property picker** - Select solid, dashed, rounded-dashed, dotted, long-dashed, or long round-ended dashed patterns - **Position and join property pickers** - Adjust stroke placement and corner joins when available for the selected block ## Checking Stroke Support Before applying stroke settings, check whether the block supports strokes. Return before the mutation calls when `supportsStroke()` is `false`. ```kotlin highlight-android-check-support val canHaveStroke = engine.block.supportsStroke(block = block) if (!canHaveStroke) { return null } ``` ## Enabling Strokes Enable strokes with `setStrokeEnabled()`, then read the state back with `isStrokeEnabled()` when your app needs to update UI or validate the change. ```kotlin highlight-android-enable-stroke engine.block.setStrokeEnabled(block = block, enabled = true) val strokeIsEnabled = engine.block.isStrokeEnabled(block = block) ``` ## Setting Stroke Color Use `setStrokeColor()` with normalized RGBA components from `0F` to `1F`. The sample sets a blue stroke color and reads it back with `getStrokeColor()`. ```kotlin highlight-android-stroke-color engine.block.setStrokeColor( block = block, color = Color.fromRGBA(r = 0F, g = 0.4F, b = 0.9F, a = 1F), ) val strokeColor = engine.block.getStrokeColor(block = block) ``` ## Setting Stroke Width Set the thickness in design units with `setStrokeWidth()`. Larger values create more prominent outlines, and `getStrokeWidth()` returns the current value. ```kotlin highlight-android-stroke-width engine.block.setStrokeWidth(block = block, width = 8F) val strokeWidth = engine.block.getStrokeWidth(block = block) ``` ## Stroke Styles Use `setStrokeStyle()` to control the line pattern. Android exposes the available styles as `StrokeStyle` enum values: - **`StrokeStyle.SOLID`** - Continuous line - **`StrokeStyle.DASHED`** - Square-ended dashes with gaps - **`StrokeStyle.DASHED_ROUND`** - Round-ended dashes with gaps - **`StrokeStyle.DOTTED`** - Circular dots - **`StrokeStyle.LONG_DASHED`** - Longer square-ended dashes - **`StrokeStyle.LONG_DASHED_ROUND`** - Longer round-ended dashes ```kotlin highlight-android-stroke-style engine.block.setStrokeStyle(block = block, style = StrokeStyle.DASHED) val strokeStyle = engine.block.getStrokeStyle(block = block) ``` ## Stroke Position Use `setStrokePosition()` to control where the stroke renders relative to the block edge: - **`StrokePosition.CENTER`** - Centered on the edge - **`StrokePosition.INNER`** - Rendered inside the block boundary - **`StrokePosition.OUTER`** - Rendered outside the block boundary Inner strokes stay within the block bounds, while outer strokes extend beyond them. ```kotlin highlight-android-stroke-position engine.block.setStrokePosition(block = block, position = StrokePosition.OUTER) val strokePosition = engine.block.getStrokePosition(block = block) ``` ## Stroke Corner Geometry Use `setStrokeCornerGeometry()` to control how stroke corners are joined. The effect is easiest to see on rectangular shapes. - **`StrokeCornerGeometry.MITER`** - Sharp pointed corners - **`StrokeCornerGeometry.ROUND`** - Smoothly curved corners - **`StrokeCornerGeometry.BEVEL`** - Flat cut corners ```kotlin highlight-android-stroke-corner engine.block.setStrokeCornerGeometry(block = block, geometry = StrokeCornerGeometry.ROUND) val strokeCornerGeometry = engine.block.getStrokeCornerGeometry(block = block) ``` ## Troubleshooting If strokes do not appear as expected, check these common issues: - **Stroke not visible** - Verify `isStrokeEnabled()` returns `true` and the width is greater than `0F`. - **Stroke color appears wrong** - Use normalized color components from `0F` to `1F`, not `0` to `255`. - **Stroke affects the visual bounds** - Use `StrokePosition.INNER` to keep the stroke inside the block boundary, or `StrokePosition.OUTER` when the outline should extend beyond it. - **Block does not support strokes** - Guard mutations with `supportsStroke()` before applying stroke properties. ## API Reference | Method | Description | |--------|-------------| | `engine.block.supportsStroke(block=_)` | Check whether a block supports strokes | | `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable the stroke | | `engine.block.isStrokeEnabled(block=_)` | Check whether the stroke is enabled | | `engine.block.setStrokeOverprint(block=_, overprint=_)` | Mark eligible spot-color strokes for PDF overprint output | | `engine.block.getStrokeOverprint(block=_)` | Check whether stroke overprint is enabled | | `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color | | `engine.block.getStrokeColor(block=_)` | Get the current stroke color | | `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke thickness in design units | | `engine.block.getStrokeWidth(block=_)` | Get the current stroke width | | `engine.block.setStrokeStyle(block=_, style=_)` | Set the stroke line pattern | | `engine.block.getStrokeStyle(block=_)` | Get the current stroke style | | `engine.block.setStrokeStartCap(block=_, cap=_)` | Set the cap at the start of an open stroked path | | `engine.block.getStrokeStartCap(block=_)` | Get the start cap for an open stroked path | | `engine.block.setStrokeEndCap(block=_, cap=_)` | Set the cap at the end of an open stroked path | | `engine.block.getStrokeEndCap(block=_)` | Get the end cap for an open stroked path | | `engine.block.setStrokeDashStartCap(block=_, cap=_)` | Set the leading cap for each dash segment | | `engine.block.getStrokeDashStartCap(block=_)` | Get the leading cap for each dash segment | | `engine.block.setStrokeDashEndCap(block=_, cap=_)` | Set the trailing cap for each dash segment | | `engine.block.getStrokeDashEndCap(block=_)` | Get the trailing cap for each dash segment | | `engine.block.setStrokeDashArray(block=_, dashArray=_)` | Set a custom dash pattern in design units | | `engine.block.getStrokeDashArray(block=_)` | Get the custom dash pattern | | `engine.block.setStrokeDashOffset(block=_, dashOffset=_)` | Shift the custom dash pattern along the stroke | | `engine.block.getStrokeDashOffset(block=_)` | Get the dash pattern offset | | `engine.block.setStrokePosition(block=_, position=_)` | Set the stroke position relative to the edge | | `engine.block.getStrokePosition(block=_)` | Get the current stroke position | | `engine.block.setStrokeCornerGeometry(block=_, geometry=_)` | Set the stroke corner join geometry | | `engine.block.getStrokeCornerGeometry(block=_)` | Get the current stroke corner geometry | ### Related Types | Type | Values | Description | |------|--------|-------------| | `StrokeStyle` | `SOLID`, `DASHED`, `DASHED_ROUND`, `DOTTED`, `LONG_DASHED`, `LONG_DASHED_ROUND` | Selects the preset line pattern | | `StrokePosition` | `CENTER`, `INNER`, `OUTER` | Controls where the stroke is drawn relative to the block edge | | `StrokeCornerGeometry` | `MITER`, `ROUND`, `BEVEL` | Controls how stroke corners join | | `StrokeCap` | `BUTT`, `ROUND`, `SQUARE` | Controls open-path and dash-segment end caps | ## Next Steps - [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) - Apply colors to design elements programmatically - [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) - Add solid colors, gradients, images, or videos inside blocks - [Shadows and Glows](https://img.ly/docs/cesdk/android/outlines/shadows-and-glows-6610fa/) - Add depth with shadow and glow effects - [Create and Edit Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) - Create shape blocks you can style with strokes --- ## 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: "Improve Performance" description: "Optimize CE.SDK Android integration with source sets, memory monitoring, export tuning, and lifecycle best practices." platform: android url: "https://img.ly/docs/cesdk/android/performance-3c12eb/" --- > 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/) > [Improve Performance](https://img.ly/docs/cesdk/android/performance-3c12eb/) --- ```kotlin file=@cesdk_android_examples/engine-guides-performance/Performance.kt reference-only import android.net.Uri import android.util.Log import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit 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 ly.img.engine.Source import java.nio.ByteBuffer import java.util.Locale import kotlin.math.max import kotlin.math.roundToInt private const val TAG = "PerformanceGuide" data class PerformanceSummary( val sourceSetWidths: List, val usedMemoryBytes: Long, val availableMemoryBytes: Long, val maxExportSize: Int, val maxExportSizeIsKnownLimit: Boolean, val exportWithinSizeLimit: Boolean, val exportedJpeg: ByteBuffer, val assetBasePath: String, ) suspend fun performance(engine: Engine): PerformanceSummary { val assetBasePath = "file:///android_asset/assets" engine.editor.setSettingString(keypath = "basePath", value = assetBasePath) val scene = engine.scene.create(designUnit = DesignUnit.PIXEL) val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(block = page, value = 1920F) engine.block.setHeight(block = page, value = 1080F) engine.block.appendChild(parent = scene, child = page) val imageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = imageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block = imageBlock, value = 1280F) engine.block.setHeight(block = imageBlock, value = 853F) engine.block.setPositionX(block = imageBlock, value = 320F) engine.block.setPositionY(block = imageBlock, value = 113F) val imageFill = engine.block.createFill(FillType.Image) engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", 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.setFill(block = imageBlock, fill = imageFill) engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER) engine.block.appendChild(parent = page, child = imageBlock) val configuredSourceWidths = engine.block .getSourceSet(block = imageFill, property = "fill/image/sourceSet") .map(Source::width) .sorted() check(configuredSourceWidths == listOf(512, 1024, 2048)) val usedMemory = engine.editor.getUsedMemory() val availableMemory = engine.editor.getAvailableMemory() val totalMemory = usedMemory + availableMemory if (totalMemory > 0L) { val usagePercentage = usedMemory.toDouble() / totalMemory.toDouble() * 100.0 Log.i(TAG, "Memory usage: ${String.format(Locale.US, "%.2f", usagePercentage)}%") } val exportOptions = ExportOptions( jpegQuality = 0.8F, targetWidth = 1280F, targetHeight = 720F, ) val maxExportSize = engine.editor.getMaxExportSize() val maxExportSizeIsKnownLimit = maxExportSize != Int.MAX_VALUE val exportWithinSizeLimit = if (!maxExportSizeIsKnownLimit) { true } else if ( engine.scene.getDesignUnit() == DesignUnit.PIXEL && engine.block.getWidthMode(page) == SizeMode.ABSOLUTE && engine.block.getHeightMode(page) == SizeMode.ABSOLUTE ) { val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) val targetWidth = exportOptions.targetWidth ?: pageWidth val targetHeight = exportOptions.targetHeight ?: pageHeight val fillScale = max(targetWidth / pageWidth, targetHeight / pageHeight) val renderedWidth = (pageWidth * fillScale).roundToInt() val renderedHeight = (pageHeight * fillScale).roundToInt() renderedWidth <= maxExportSize && renderedHeight <= maxExportSize } else { true } check(exportWithinSizeLimit) { "Requested export dimensions exceed the device export limit." } val exportedJpeg = engine.block.export( block = page, mimeType = MimeType.JPEG, options = exportOptions, ) check(exportedJpeg.hasRemaining()) { "JPEG export is empty." } return PerformanceSummary( sourceSetWidths = configuredSourceWidths, usedMemoryBytes = usedMemory, availableMemoryBytes = availableMemory, maxExportSize = maxExportSize, maxExportSizeIsKnownLimit = maxExportSizeIsKnownLimit, exportWithinSizeLimit = exportWithinSizeLimit, exportedJpeg = exportedJpeg.asReadOnlyBuffer(), assetBasePath = engine.editor.getSettingString("basePath"), ) } fun releasePerformanceEngine( engine: Engine, finishingSession: Boolean, ) { if (!engine.isEngineRunning()) return if (engine.isBound()) { engine.unbind() } if (finishingSession) { engine.stop() } else { engine.editor.setAppIsPaused(paused = true) engine.pause() } } fun resumePerformanceEngine( engine: Engine, bindRenderTarget: () -> Unit, ) { if (!engine.isEngineRunning()) return if (!engine.isBound()) { bindRenderTarget() } engine.editor.setAppIsPaused(paused = false) engine.unpause() } ``` Optimize CE.SDK integration for faster loading, lower memory pressure, and reliable exports on Android. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-performance) ![Exported Android performance sample](./assets/android.export.webp) CE.SDK ships a fully featured engine runtime. On Android, performance work usually comes from downloading and starting the `Engine` only when an editing session needs it, loading smaller media while editing, tuning export dimensions, and releasing engine resources when the session ends. This guide covers source sets for large assets, memory monitoring with the editor APIs, export size and quality tuning, and the Android lifecycle calls that keep idle engine instances from doing unnecessary work. ## Delay Engine and Editor Loading Keep CE.SDK out of the initial install path when many users may never open an editing flow. Android supports this with [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery): move the CE.SDK engine dependency, editor dependency, or copied editor module into a dynamic feature module, then request that feature when the user enters the editor. This is the Android counterpart to code splitting on web platforms. The app shell can stay small, while the engine/editor code and native libraries are delivered on demand. See [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) for the dynamic feature setup notes and the current Android size expectations. Even when CE.SDK ships with the base app, still delay runtime startup. Create or start the `Engine` when the editing session begins, and stop it when the session is finished. ## Managing Large Assets High-resolution images and videos consume significant memory. Use source sets to give the engine multiple resolution variants so it can pick the smallest source that still looks good at the current drawing size. ### Use Source Sets A source set is a list of `Source` entries with different resolutions for the same image or video. The engine picks the variant whose dimensions best match the current viewport and uses higher-resolution entries when the output needs them. ```kotlin highlight-android-source-sets engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", 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, ), ), ) ``` This reduces memory pressure during editing while preserving export quality. See [Source Sets](https://img.ly/docs/cesdk/android/import-media/source-sets-5679c8/) for the full API, including video source sets and asset-source integration. ### Additional Optimization Tips - Remove unused blocks from the scene when they are no longer needed. - Prefer source assets sized for your expected editing and export targets. - Keep only one interactive engine instance active at a time. ## Memory Management Use the editor memory APIs to observe how much memory the engine currently holds and how much headroom remains. Track these values across long sessions to detect leaks or decide when to free unused assets. ```kotlin highlight-android-memory-monitoring val usedMemory = engine.editor.getUsedMemory() val availableMemory = engine.editor.getAvailableMemory() val totalMemory = usedMemory + availableMemory if (totalMemory > 0L) { val usagePercentage = usedMemory.toDouble() / totalMemory.toDouble() * 100.0 Log.i(TAG, "Memory usage: ${String.format(Locale.US, "%.2f", usagePercentage)}%") } ``` `getUsedMemory()` and `getAvailableMemory()` both return byte counts as `Long`. Log or report these values with your normal app telemetry so you can compare behavior across representative Android devices. ## Export Optimization Tune export resolution and quality to balance fidelity against time and memory. The settings below apply to image exports; video exports use dedicated video export options. ### Optimize Export Settings `ExportOptions` controls compression and output scaling. Lowering `targetWidth`, `targetHeight`, and `jpegQuality` produces smaller files faster, at the cost of fidelity. ```kotlin highlight-android-export-settings val exportOptions = ExportOptions( jpegQuality = 0.8F, targetWidth = 1280F, targetHeight = 720F, ) ``` Pass the same options to `engine.block.export` after validating the requested output size against the device limit. | Property | Type | Purpose | | --- | --- | --- | | `targetWidth` / `targetHeight` | `Float?` | Optional output dimensions in pixels. The block is rendered large enough to fill the target while keeping its aspect ratio. Leave both `null` to use the block's intrinsic size. | | `jpegQuality` | `Float` | JPEG quality in the range `(0, 1]`. Lower values trade quality for smaller files. Defaults to `0.9`. | | `pngCompressionLevel` | `Int` | PNG compression from `0` to `9`. Higher values produce smaller files but take longer to encode. Defaults to `5`. | | `webpQuality` | `Float` | WebP quality in the range `(0, 1]`. At `1.0`, WebP uses a lossless encoding path that usually produces smaller files than PNG. Defaults to `1.0`. | ### Export Size Limits Different devices support different maximum export sizes. Call `getMaxExportSize()` before starting a large export, then branch on `Int.MAX_VALUE`: that value means the Android binding does not know a finite device limit. ```kotlin highlight-android-max-export-size val maxExportSize = engine.editor.getMaxExportSize() val maxExportSizeIsKnownLimit = maxExportSize != Int.MAX_VALUE val exportWithinSizeLimit = if (!maxExportSizeIsKnownLimit) { true } else if ( engine.scene.getDesignUnit() == DesignUnit.PIXEL && engine.block.getWidthMode(page) == SizeMode.ABSOLUTE && engine.block.getHeightMode(page) == SizeMode.ABSOLUTE ) { val pageWidth = engine.block.getWidth(page) val pageHeight = engine.block.getHeight(page) val targetWidth = exportOptions.targetWidth ?: pageWidth val targetHeight = exportOptions.targetHeight ?: pageHeight val fillScale = max(targetWidth / pageWidth, targetHeight / pageHeight) val renderedWidth = (pageWidth * fillScale).roundToInt() val renderedHeight = (pageHeight * fillScale).roundToInt() renderedWidth <= maxExportSize && renderedHeight <= maxExportSize } else { true } ``` `getWidth()` and `getHeight()` return values in design units, so use them as fallback output dimensions only when the scene uses `DesignUnit.PIXEL` and the block width and height modes are `SizeMode.ABSOLUTE`. When `ExportOptions` sets `targetWidth` or `targetHeight`, compare the rendered output box against the limit instead. A finite returned value is an upper bound for both output edges; exports can still fail for memory, codec, or scene-complexity reasons. ## Engine Lifecycle and Asset Loading Create or start the `Engine` when the editing session begins, then keep a strong reference for the lifetime of that session. Before loading scenes that depend on bundled CE.SDK assets, point `basePath` at the copied or hosted Android asset folder. ```kotlin highlight-android-asset-base-path engine.editor.setSettingString(keypath = "basePath", value = assetBasePath) ``` The sample uses `file:///android_asset/assets`, which expects the extracted CE.SDK Android asset bundle in your app assets. Use an HTTPS URL when you host the same asset folder yourself. See [Serve Assets From Your Server](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for the full asset-hosting setup. When a render target is disposed, unbind it so the engine does not keep a destroyed surface. If the editing session continues in memory, pause the engine so it stops spending processing time; when the session really ends, stop the engine to release native resources. ```kotlin highlight-android-lifecycle fun releasePerformanceEngine( engine: Engine, finishingSession: Boolean, ) { if (!engine.isEngineRunning()) return if (engine.isBound()) { engine.unbind() } if (finishingSession) { engine.stop() } else { engine.editor.setAppIsPaused(paused = true) engine.pause() } } ``` When the same screen becomes visible again, bind a new render target if the previous one was released, then clear the paused state and unpause the engine before continuing the editing session. ```kotlin highlight-android-resume-lifecycle fun resumePerformanceEngine( engine: Engine, bindRenderTarget: () -> Unit, ) { if (!engine.isEngineRunning()) return if (!engine.isBound()) { bindRenderTarget() } engine.editor.setAppIsPaused(paused = false) engine.unpause() } ``` Call these lifecycle methods on the main thread. `stop()` makes the engine APIs unavailable until `start()` runs again, so use it only when the editing session is finished. ## Troubleshooting ### Memory warnings or crashes Monitor memory with `getUsedMemory()` and `getAvailableMemory()` and react when usage climbs. Common remediations are removing unused blocks, lowering the `maxImageSize` setting, or stopping the engine and creating a fresh editing session. ### Export hangs or fails Validate the requested output dimensions against `getMaxExportSize()` before exporting and lower `targetWidth` or `targetHeight` for large designs. For persistent failures, reduce scene complexity or input asset resolution. ### Slow asset loading Use source sets so editing loads smaller media. For default CE.SDK assets, host the asset folder close to your users or bundle it in the app when offline access matters. ## API Reference | Method | Description | | --- | --- | | `engine.editor.setSettingString(keypath="basePath", value=_)` | Configure where CE.SDK assets are loaded from. | | `engine.block.setSourceSet(block=_, property="fill/image/sourceSet", sourceSet=_)` | Provide multiple image resolutions for a fill. | | `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Read the configured source set. | | `engine.editor.getUsedMemory()` | Get current engine memory usage in bytes. | | `engine.editor.getAvailableMemory()` | Get remaining available memory in bytes. | | `engine.editor.getMaxExportSize()` | Get the maximum export edge length in pixels, or `Int.MAX_VALUE` when the limit is unknown. | | `engine.scene.getDesignUnit()` | Read the scene's design unit before comparing dimensions with pixel limits. | | `engine.block.getWidthMode(block=_)` | Check whether a block's width is an absolute value. | | `engine.block.getHeightMode(block=_)` | Check whether a block's height is an absolute value. | | `engine.block.getWidth(block=_)` | Read the block width before validating export dimensions. | | `engine.block.getHeight(block=_)` | Read the block height before validating export dimensions. | | `ExportOptions(jpegQuality=_, targetWidth=_, targetHeight=_)` | Configure image compression and output scaling. | | `engine.block.export(block=_, mimeType=MimeType.JPEG, options=_)` | Export a block with the selected image settings. | | `engine.isEngineRunning()` | Check whether the engine is currently started. | | `engine.isBound()` | Check whether the engine is bound to a render surface or offscreen context. | | `engine.unbind()` | Release the current render binding. | | `engine.stop()` | Release native engine resources and make APIs unavailable until restart. | | `engine.editor.setAppIsPaused(paused=_)` | Inform the engine that the app is paused so it can reduce resource usage. | | `engine.pause()` | Stop engine processing while keeping resources in memory for a later resume. | | `engine.unpause()` | Resume processing after a temporary pause. | ## Next Steps - [Architecture](https://img.ly/docs/cesdk/android/concepts/architecture-6ea9b2/) - Understand CE.SDK structure and components - [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Run CE.SDK programmatically without any user interface - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Learn about export formats and 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: "Plugins" description: "Extend CE.SDK for Android with plugins that add focused editor capabilities." platform: android url: "https://img.ly/docs/cesdk/android/plugins-693c48/" --- > 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:** [Plugins](https://img.ly/docs/cesdk/android/plugins-693c48/) --- CE.SDK plugins package focused editor capabilities so you can add them to an existing configuration without rebuilding the surrounding editor experience. Use plugins when you want to expose a reusable feature, configure where it appears in the editor UI, and keep the integration code close to the behavior it enables. --- ## Related Pages - [AI Image Generation](https://img.ly/docs/cesdk/android/plugins/ai-image-generation-aimg01/) - Add AI image generation to CE.SDK for Android with prompt-based creation and image replacement through the IMG.LY Gateway. - [Custom Feature Plugin](https://img.ly/docs/cesdk/android/plugins/custom-plugin-ju8y5t/) - Build a reusable Android editor plugin that wraps callbacks, state, and UI component configuration. - [Background Removal](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) - Add a background removal action to the CE.SDK Android editor. - [Create Cutout](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3/) - Create cutout paths for cutting printers to produce die-cut stickers, iron-on decals, and custom-shaped prints. --- ## 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: "AI Image Generation" description: "Add AI image generation to CE.SDK for Android with prompt-based creation and image replacement through the IMG.LY Gateway." platform: android url: "https://img.ly/docs/cesdk/android/plugins/ai-image-generation-aimg01/" --- > 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:** [Plugins](https://img.ly/docs/cesdk/android/plugins-693c48/) > [AI Image Generation](https://img.ly/docs/cesdk/android/plugins/ai-image-generation-aimg01/) --- ```kotlin file=@cesdk_android_examples/editor-guides-ai-image-generation/AIImageGenerationEditorSolution.kt reference-only import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.component.Dock import ly.img.editor.core.component.InspectorBar 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.ai.core.gateway.AIGatewayConfig import ly.img.editor.plugin.ai.core.gateway.AIGatewayImageModel import ly.img.editor.plugin.ai.imageGeneration.AIImageGenerationPlugin import ly.img.editor.plugin.ai.imageGeneration.rememberAIImageGeneration import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit // Add this composable to your NavHost. @Composable fun AIImageGenerationEditorSolution( license: String, aiGatewayApiKey: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::AIImageGenerationPlugin) { aiGatewayConfig = AIGatewayConfig( apiKey = aiGatewayApiKey, ) } }, onClose = onClose, ) } @Composable private fun AIImageGenerationEditorSolutionWithConfigurations( license: String, aiGatewayApiKey: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::AIImageGenerationPlugin) { aiGatewayConfig = AIGatewayConfig(apiKey = aiGatewayApiKey) dockModifier = { addFirst { Dock.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } inspectorBarModifier = { addFirst { InspectorBar.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } } }, onClose = onClose, ) } @Composable private fun AIImageGenerationEditorSolutionWithGatewayConfig( license: String, aiGatewayApiKey: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::AIImageGenerationPlugin) { aiGatewayConfig = AIGatewayConfig( apiKey = aiGatewayApiKey, model = AIGatewayImageModel.GptImage2, gatewayUrl = "https://gateway.img.ly", httpClient = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(120, TimeUnit.SECONDS) .writeTimeout(120, TimeUnit.SECONDS) .build(), ) } }, onClose = onClose, ) } @Composable private fun AIImageGenerationEditorSolutionWithButtonPlacement( license: String, aiGatewayApiKey: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::AIImageGenerationPlugin) { aiGatewayConfig = AIGatewayConfig(apiKey = aiGatewayApiKey) dockModifier = { addLast { Dock.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } inspectorBarModifier = { addLast { InspectorBar.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } } }, onClose = onClose, ) } ``` Add AI-powered image generation to your editor so users can create visuals from text prompts or transform existing images. ![AI image generation in the Android editor](./assets/android.hero.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-ai-image-generation) The `AIImageGenerationPlugin` adds two integration points to the editor: a dock button for text-to-image generation and an inspector bar button for image-to-image replacement. Both actions connect to the [IMG.LY Gateway](https://img.ly/dashboard) through `AIGatewayConfig`. This guide builds on the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). The same plugin works with any editor configuration that hosts a dock and inspector bar. ## Prerequisites You need an IMG.LY Gateway API key with access to the model family you want to use, such as FLUX.2 (`bfl/flux-2`) or GPT Image 2 (`openai/gpt-image-2`). The host app also needs network access to send generation requests and load the returned images. > **Note:** You can acquire an API key at [img.ly/dashboard](https://img.ly/dashboard). Register, generate the API key and contact IMG.LY support to credit the balance for Gateway usage. ## Install the Plugin Add the AI image generation plugin package to the module that hosts CE.SDK. The plugin includes the shared AI Gateway client. ```groovy dependencies { implementation("ly.img:plugin-ai-image-generation:$UBQ_VERSION$") } ``` ## Use the Plugin Import the editor configuration APIs, the plugin and the Gateway configuration types where you create your editor configuration: ```kotlin highlight-android-imports import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.component.Dock import ly.img.editor.core.component.InspectorBar 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.ai.core.gateway.AIGatewayConfig import ly.img.editor.plugin.ai.core.gateway.AIGatewayImageModel import ly.img.editor.plugin.ai.imageGeneration.AIImageGenerationPlugin import ly.img.editor.plugin.ai.imageGeneration.rememberAIImageGeneration import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit ``` Register `AIImageGenerationPlugin` alongside your existing editor configuration and provide the Gateway API key: ```kotlin highlight-android-basic-setup Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::AIImageGenerationPlugin) { aiGatewayConfig = AIGatewayConfig( apiKey = aiGatewayApiKey, ) } }, onClose = onClose, ) ``` The plugin adds a “Generate” button to the dock and an “Edit” button to the inspector bar. The dock action creates a new image block from a prompt. The inspector action appears when the current selection is a non-sticker block with an image fill and replaces that image content. > **Warning:** Image generation requires network access and a valid IMG.LY Gateway API key. Without both, generation requests fail with authentication or network errors. ## Configure the Plugin The Android plugin exposes the Gateway configuration and modifiers for the two editor actions. This example shows all three options with their default button placements: ```kotlin highlight-android-configuration Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::AIImageGenerationPlugin) { aiGatewayConfig = AIGatewayConfig(apiKey = aiGatewayApiKey) dockModifier = { addFirst { Dock.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } inspectorBarModifier = { addFirst { InspectorBar.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } } }, onClose = onClose, ) ``` ### Choose a Model `AIGatewayConfig` defaults to FLUX.2. To use GPT Image 2, set `model` to `AIGatewayImageModel.GptImage2`. You can also provide a Gateway-compatible endpoint and a custom `OkHttpClient` for shared interceptors or timeouts. ```kotlin highlight-android-gateway-config aiGatewayConfig = AIGatewayConfig( apiKey = aiGatewayApiKey, model = AIGatewayImageModel.GptImage2, gatewayUrl = "https://gateway.img.ly", httpClient = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(120, TimeUnit.SECONDS) .writeTimeout(120, TimeUnit.SECONDS) .build(), ) ``` | Model | Text-to-Image ID | Image-to-Image ID | | --- | --- | --- | | FLUX.2 (default) | `bfl/flux-2` | `bfl/flux-2-edit` | | GPT Image 2 | `openai/gpt-image-2` | `openai/gpt-image-2-edit` | The model must be available to the API key configured in the [IMG.LY Dashboard](https://img.ly/dashboard). ### Customize Button Placement The plugin prepends both buttons by default. Use `dockModifier` and `inspectorBarModifier` to change their positions with the same list builder operations available to other dock and inspector bar customizations. The following dock modifier moves the text-to-image action to the end: ```kotlin highlight-android-dock-modifier dockModifier = { addLast { Dock.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } ``` The inspector bar modifier moves the image-to-image action to the end: ```kotlin highlight-android-inspector-bar-modifier inspectorBarModifier = { addLast { InspectorBar.Button.rememberAIImageGeneration(aiGatewayConfig = it) } } ``` ## How Generation Works **Text-to-image:** The dock action opens the generation sheet, creates a pending image block, streams the Gateway response and applies the generated image URL when the request completes. **Image-to-image:** The inspector bar action reads the selected image fill URI. The plugin uploads local images through the Gateway upload flow before it sends the prompt and source image URL to the image-to-image model. The built-in generation sheet provides prompt, style, output type and format controls. For image-to-image requests, the plugin omits the output format so the Gateway derives the dimensions from the selected source image. ## Troubleshooting - If the buttons are missing, verify that the plugin dependency is installed and `AIImageGenerationPlugin` is composed with the editor configuration. - If the inspector action is hidden, select a non-sticker block with an image fill. - If generation fails, verify network access, model access, the Gateway balance and the values in `AIGatewayConfig`. - If the prompt cannot be submitted, enter non-empty prompt text. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builderFactory=_)` | Creates the Design Editor configuration used by the example. | | `EditorConfiguration.then(builderFactory=_)` | Composes the image generation plugin with the base editor configuration. | | `AIImageGenerationPlugin.aiGatewayConfig` | Sets the Gateway configuration used by generation requests. | | `AIGatewayConfig(apiKey=_, model=_, gatewayUrl=_, httpClient=_)` | Configures the API key, model family, Gateway endpoint and HTTP client. | | `AIImageGenerationPlugin.dockModifier` | Changes where the plugin inserts its dock button. | | `Dock.Button.rememberAIImageGeneration(aiGatewayConfig=_, builder=_)` | Creates the dock button for text-to-image generation. | | `AIImageGenerationPlugin.inspectorBarModifier` | Changes where the plugin inserts its inspector bar button. | | `InspectorBar.Button.rememberAIImageGeneration(aiGatewayConfig=_, builder=_)` | Creates the inspector bar button for image-to-image replacement. | ## Next Steps - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — Customize dock items and ordering. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) — Customize inspector bar actions and ordering. - [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) — Start from the editor preset used by the 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: "Custom Feature Plugin" description: "Build a reusable Android editor plugin that wraps callbacks, state, and UI component configuration." platform: android url: "https://img.ly/docs/cesdk/android/plugins/custom-plugin-ju8y5t/" --- > 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:** [Plugins](https://img.ly/docs/cesdk/android/plugins-693c48/) > [Custom Feature Plugin](https://img.ly/docs/cesdk/android/plugins/custom-plugin-ju8y5t/) --- ```kotlin file=@cesdk_android_examples/editor-guides-plugins-custom-plugin/CustomFeaturePluginEditorSolution.kt reference-only import android.net.Uri import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.editor.BasicConfigurationBuilder import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.EditorScope import ly.img.editor.core.ScopedProperty import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.modify import ly.img.editor.core.component.remember import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.EditorConfigurationBuilder import ly.img.editor.core.configuration.remember import ly.img.editor.core.configuration.then import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Image import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.HorizontalBlockAlignment import ly.img.engine.ShapeType import ly.img.engine.SizeMode import ly.img.engine.VerticalBlockAlignment import java.io.File import java.nio.ByteBuffer import java.util.UUID @Composable fun CustomFeaturePluginEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::CustomFeaturePlugin) { randomImageUri = "https://img.ly/static/ubq_samples/sample_1.jpg".toUri() } }, onClose = onClose, ) } open class CustomFeaturePlugin : EditorConfigurationBuilder() { var randomImageUri: Uri by editorContext.mutableStateOf( key = "com.example.editor.customFeature.randomImageUri", initial = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) private var isLoading: Boolean by editorContext.mutableStateOf( key = BasicConfigurationBuilder.KEY_STATE_SHOW_LOADING, initial = false, ) override var onCreate: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val editorScope = this coroutineScope { launch { parentConfiguration?.onCreate?.invoke(editorScope) // The parent Design configuration hides loading after scene setup. isLoading = true } launch { Log.d(TAG, "CustomFeaturePlugin setup started.") delay(3_000) Log.d(TAG, "CustomFeaturePlugin setup finished.") } } } finally { isLoading = false } } override var onExport: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val scene = requireNotNull(editorContext.engine.scene.get()) { "A scene is required before exporting." } val archive = editorContext.engine.scene.saveToArchive(scene = scene) val file = writeArchiveToTempFile(archive) Toast.makeText(editorContext.activity, "Saved scene archive to ${file.name}", Toast.LENGTH_SHORT).show() Log.d(TAG, "Saved scene archive to ${file.absolutePath}") } finally { isLoading = false } } override var dock: ScopedProperty?>? = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("com.example.component.dock.button.customFeature") } vectorIcon = { IconPack.Image } textString = { "Image" } contentDescription = { "Add image" } onClick = { addImageBlockFromPlugin() } } } } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } override var canvasMenu: ScopedProperty?>? = { null } private fun EditorScope.addImageBlockFromPlugin() { val engine = editorContext.engine val page = requireNotNull(engine.scene.getCurrentPage()) { "A current page is required before adding an image block." } val imageBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(ShapeType.Rect) val fill = engine.block.createFill(FillType.Image) engine.block.setShape(block = imageBlock, shape = shape) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = randomImageUri, ) engine.block.setFill(block = imageBlock, fill = fill) engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER) engine.block.setWidthMode(block = imageBlock, mode = SizeMode.PERCENT) engine.block.setWidth(block = imageBlock, value = 0.5F) engine.block.appendChild(parent = page, child = imageBlock) if (engine.block.isAlignable(listOf(imageBlock))) { engine.block.alignHorizontally(listOf(imageBlock), alignment = HorizontalBlockAlignment.CENTER) engine.block.alignVertically(listOf(imageBlock), alignment = VerticalBlockAlignment.CENTER) } engine.block.setSelected(block = imageBlock, selected = true) } private suspend fun writeArchiveToTempFile(archive: ByteBuffer): File = withContext(Dispatchers.IO) { File.createTempFile("custom-feature-${UUID.randomUUID()}", ".imgly").apply { outputStream().channel.use { channel -> channel.write(archive) } } } private companion object { const val TAG = "CustomFeaturePlugin" } } ``` Package repeatable editor behavior in a custom Android plugin that can be chained onto an existing editor configuration. ![Custom feature plugin button in the Android editor dock](./assets/custom-feature-plugin-android.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-plugins-custom-plugin) ## When to Use Plugins You usually do not need a plugin for the first version of an editor customization. Start by changing the editor configuration inline where the editor is created. Inline configuration is easier to read, debug, and adapt when the behavior belongs to one editor entry point. If you want a complete editor surface, start from a [Starter Kit](https://img.ly/docs/cesdk/android/starterkits-kxg120/). Bring the Starter Kit into your project, then adapt its configuration inline until the product behavior is clear. Create a custom plugin when the same configuration needs to be reused across multiple editors, products, or situations. A plugin is useful when callbacks, component changes, feature flags, assets, and shared state should travel together as one scaffolded unit instead of being copied into every editor setup. ## What the Example Does `CustomFeaturePlugin` starts from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) configuration and adds a reusable feature layer on top: - It prepends one custom image button to the inherited dock. - It hides the canvas menu completely. - It leaves the navigation bar and inspector bar unchanged. - It exposes `randomImageUri` as a plugin property. - It reuses the Starter Kit loading state with `BasicConfigurationBuilder.KEY_STATE_SHOW_LOADING`. - It replaces `onCreate` and `onExport` while preserving the Design Starter Kit scene setup. ## Create the Plugin On Android, a custom editor plugin is a class that inherits from `EditorConfigurationBuilder`. ```kotlin open class CustomFeaturePlugin : EditorConfigurationBuilder() ``` Builder properties let the plugin configure callbacks, state, and editor components in one place. ## Apply the Plugin Apply the plugin after the base configuration. The builder block passed to `then(::CustomFeaturePlugin)` configures plugin-specific properties for this editor entry point. ```kotlin highlight-android-apply-plugin Editor( license = license, configuration = { EditorConfiguration .remember(::DesignConfigurationBuilder) .then(::CustomFeaturePlugin) { randomImageUri = "https://img.ly/static/ubq_samples/sample_1.jpg".toUri() } }, onClose = onClose, ) ``` The chain order matters: the Design Starter Kit creates the parent configuration, then `CustomFeaturePlugin` extends or replaces selected parts of that parent configuration. ## Custom Properties Use builder properties to expose the values that should change between editor entry points. This plugin exposes `randomImageUri` for the dock button and reuses the Design Starter Kit loading state so long-running work shows the existing loading overlay. ```kotlin highlight-android-plugin-state var randomImageUri: Uri by editorContext.mutableStateOf( key = "com.example.editor.customFeature.randomImageUri", initial = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), ) private var isLoading: Boolean by editorContext.mutableStateOf( key = BasicConfigurationBuilder.KEY_STATE_SHOW_LOADING, initial = false, ) ``` The `randomImageUri` property belongs to the plugin and uses `editorContext.mutableStateOf` so it survives recompositions and configuration changes while the editor is open. The `isLoading` property uses `BasicConfigurationBuilder.KEY_STATE_SHOW_LOADING` so the plugin writes to the same state key as the Starter Kit loading overlay. > **Note:** Use `editorContext.mutableStateOf` when your plugin needs to declare or update > state. If your plugin only needs to read state that another configuration > already declared, read it with `editorContext.stateOf`. ## Extend Startup `onCreate` owns scene creation or loading. This plugin replaces `onCreate`, but it still delegates to `parentConfiguration?.onCreate` so the Design Starter Kit can create the scene. ```kotlin highlight-android-on-create override var onCreate: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val editorScope = this coroutineScope { launch { parentConfiguration?.onCreate?.invoke(editorScope) // The parent Design configuration hides loading after scene setup. isLoading = true } launch { Log.d(TAG, "CustomFeaturePlugin setup started.") delay(3_000) Log.d(TAG, "CustomFeaturePlugin setup finished.") } } } finally { isLoading = false } } ``` The two launched coroutines run in parallel. One runs the parent startup callback, while the other logs a custom setup operation and waits for three seconds to imitate asynchronous plugin work. The loading state is shown for the whole combined operation and hidden in `finally`. ## Override Export The plugin fully replaces export behavior by setting `onExport` and not calling the parent export callback. That makes the plugin responsible for the complete export workflow. ```kotlin highlight-android-on-export override var onExport: (suspend EditorScope.() -> Unit)? = { try { isLoading = true val scene = requireNotNull(editorContext.engine.scene.get()) { "A scene is required before exporting." } val archive = editorContext.engine.scene.saveToArchive(scene = scene) val file = writeArchiveToTempFile(archive) Toast.makeText(editorContext.activity, "Saved scene archive to ${file.name}", Toast.LENGTH_SHORT).show() Log.d(TAG, "Saved scene archive to ${file.absolutePath}") } finally { isLoading = false } } ``` This example saves the active scene as an archive and writes the returned `ByteBuffer` to a temporary `.scene.zip` file. ```kotlin highlight-android-write-archive private suspend fun writeArchiveToTempFile(archive: ByteBuffer): File = withContext(Dispatchers.IO) { File.createTempFile("custom-feature-${UUID.randomUUID()}", ".imgly").apply { outputStream().channel.use { channel -> channel.write(archive) } } } ``` Use this replacement style when your plugin owns validation, upload, handoff, or storage. If you only need to add behavior around the existing export, delegate to `parentConfiguration?.onExport` instead. ## Extend the Dock Components follow the same extension-or-replacement rule as callbacks. To add a dock item, read the inherited `Dock`, modify its list builder, and copy the dock with the updated builder. ```kotlin highlight-android-dock override var dock: ScopedProperty?>? = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("com.example.component.dock.button.customFeature") } vectorIcon = { IconPack.Image } textString = { "Image" } contentDescription = { "Add image" } onClick = { addImageBlockFromPlugin() } } } } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } ``` The custom button creates an image block from `randomImageUri`. The block is added at half the current page width with `SizeMode.PERCENT`, centered on the current page with the block alignment APIs, and set to `ContentFillMode.COVER` so the image fills the rectangle. ```kotlin highlight-android-add-image private fun EditorScope.addImageBlockFromPlugin() { val engine = editorContext.engine val page = requireNotNull(engine.scene.getCurrentPage()) { "A current page is required before adding an image block." } val imageBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(ShapeType.Rect) val fill = engine.block.createFill(FillType.Image) engine.block.setShape(block = imageBlock, shape = shape) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = randomImageUri, ) engine.block.setFill(block = imageBlock, fill = fill) engine.block.setContentFillMode(block = imageBlock, mode = ContentFillMode.COVER) engine.block.setWidthMode(block = imageBlock, mode = SizeMode.PERCENT) engine.block.setWidth(block = imageBlock, value = 0.5F) engine.block.appendChild(parent = page, child = imageBlock) if (engine.block.isAlignable(listOf(imageBlock))) { engine.block.alignHorizontally(listOf(imageBlock), alignment = HorizontalBlockAlignment.CENTER) engine.block.alignVertically(listOf(imageBlock), alignment = VerticalBlockAlignment.CENTER) } engine.block.setSelected(block = imageBlock, selected = true) } ``` The block uses a rect shape, an image fill, and the `fill/image/imageFileURI` URI property, then appends itself to the current page. ## Hide the Canvas Menu When the plugin should own a component region completely, replace it instead of modifying the parent component. Returning `null` removes the inherited canvas menu. ```kotlin highlight-android-canvas-menu override var canvasMenu: ScopedProperty?>? = { null } ``` This plugin does not touch the navigation bar or inspector bar, so both continue to come from the Design Starter Kit parent configuration. ## API Reference | API | Purpose | | --- | --- | | `EditorConfigurationBuilder` | Base class for reusable Android editor configuration plugins. | | `EditorConfiguration.remember(builderFactory=_)` | Creates the base editor configuration. | | `EditorConfiguration.then(builderFactory=_, builder=_)` | Chains a plugin or additional builder onto an existing configuration. | | `EditorConfigurationBuilder.parentConfiguration` | Gives the plugin access to callbacks and components from the previous configuration. | | `EditorConfigurationBuilder.onCreate` | Creates or loads the scene during editor startup. | | `EditorConfigurationBuilder.onExport` | Handles export requests from the editor UI. | | `EditorConfigurationBuilder.dock` | Configures or extends the dock component. | | `EditorConfigurationBuilder.canvasMenu` | Configures, replaces, or removes the canvas menu. | | `editorContext.mutableStateOf(key=_, initial=_)` | Stores plugin-owned state for the editor session. | | `editorContext.stateOf(key=_)` | Reads state that has already been declared by another configuration layer. | | `Dock.ListBuilder.modify(builder=_)` | Adds, removes, or reorders dock items without replacing the whole dock. | | `engine.scene.saveToArchive(scene=_)` | Saves a scene and its referenced assets into an archive. | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Creates the graphic block used for the custom image button. | | `engine.block.createFill(fillType=FillType.Image)` | Creates the image fill for the inserted graphic block. | | `engine.block.setWidthMode(block=_, mode=SizeMode.PERCENT)` | Sizes the inserted block relative to its parent page. | | `engine.block.setHeightMode(block=_, mode=SizeMode.AUTO)` | Lets the inserted block derive its height automatically. | | `engine.block.setContentFillMode(block=_, mode=ContentFillMode.CONTAIN)` | Preserves the image aspect ratio inside the block. | | `engine.block.alignHorizontally(blocks=_, alignment=HorizontalBlockAlignment.CENTER)` | Centers the inserted block horizontally on the page. | | `engine.block.alignVertically(blocks=_, alignment=VerticalBlockAlignment.CENTER)` | Centers the inserted block vertically on the page. | ## Troubleshooting | Problem | Cause | Fix | | --- | --- | --- | | The editor opens without the expected scene | The plugin replaced `onCreate` and skipped the parent setup | Call `parentConfiguration?.onCreate?.invoke(this)` or create/load a scene inside the plugin. | | Loading never appears | The plugin reads a state key before the parent configuration declares it | Chain the plugin after `DesignConfigurationBuilder`. | | The canvas menu still appears | Another later configuration layer replaces `canvasMenu` again | Apply `CustomFeaturePlugin` after that layer or hide the menu in the final configuration. | | Export behavior runs twice | The plugin calls the parent export callback and also performs a full export | Choose either extension or replacement for `onExport`. | ## Next Steps - [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) - Start from a complete design editor surface. - [Rearrange Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/) - Reorder editor UI components. --- ## 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: "Rules" description: "Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs." platform: android url: "https://img.ly/docs/cesdk/android/rules-1427c0/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/android/rules-1427c0/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) - Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs. - [Enforce Brand Guidelines](https://img.ly/docs/cesdk/android/rules/enforce-brand-guidelines-23a1e3/) - Learn how to restrict users to approved brand assets and prevent unauthorized modifications to brand elements. - [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. - [Moderate Content](https://img.ly/docs/cesdk/android/rules/moderate-content-d5ff7e/) - Extract images and text from CE.SDK designs with the Android Engine API, then integrate third-party moderation services to detect inappropriate content. --- ## 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: "Enforce Brand Guidelines" description: "Learn how to restrict users to approved brand assets and prevent unauthorized modifications to brand elements." platform: android url: "https://img.ly/docs/cesdk/android/rules/enforce-brand-guidelines-23a1e3/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/android/rules-1427c0/) > [Enforce Brand Guidelines](https://img.ly/docs/cesdk/android/rules/enforce-brand-guidelines-23a1e3/) --- ```kotlin file=@cesdk_android_examples/engine-guides-enforce-brand-guidelines/EnforceBrandGuidelines.kt reference-only import android.net.Uri 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.FillType import ly.img.engine.FindAssetsQuery import ly.img.engine.Font import ly.img.engine.FontStyle 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 import java.nio.ByteBuffer suspend fun enforceBrandGuidelines(engine: Engine): ByteBuffer { 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 logo = engine.block.create(DesignBlockType.Graphic) engine.block.setName(logo, name = "Locked brand logo") engine.block.setShape(logo, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(logo, value = 56F) engine.block.setPositionY(logo, value = 56F) engine.block.setWidth(logo, value = 184F) engine.block.setHeight(logo, value = 96F) engine.block.setFill(logo, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(logo, color = Color.fromHex("#FF17233A")) engine.block.appendChild(parent = page, child = logo) val logoText = engine.block.create(DesignBlockType.Text) engine.block.setName(logoText, name = "Locked brand wordmark") engine.block.setWidthMode(logoText, mode = SizeMode.AUTO) engine.block.setHeightMode(logoText, mode = SizeMode.AUTO) engine.block.setPositionX(logoText, value = 88F) engine.block.setPositionY(logoText, value = 90F) engine.block.replaceText(logoText, text = "BRAND") engine.block.setTextColor(logoText, color = Color.fromHex("#FFFFFFFF")) engine.block.appendChild(parent = page, child = logoText) val legalText = engine.block.create(DesignBlockType.Text) engine.block.setName(legalText, name = "Locked legal text") engine.block.setWidth(legalText, value = 680F) engine.block.setHeightMode(legalText, mode = SizeMode.AUTO) engine.block.setPositionX(legalText, value = 56F) engine.block.setPositionY(legalText, value = 520F) engine.block.replaceText(legalText, text = "Approved brand disclaimer") engine.block.setTextFontSize(legalText, fontSize = 12F) engine.block.setTextColor(legalText, color = Color.fromHex("#FF17233A")) engine.block.appendChild(parent = page, child = legalText) val headline = engine.block.create(DesignBlockType.Text) engine.block.setName(headline, name = "Editable campaign headline") engine.block.setWidth(headline, value = 680F) engine.block.setHeightMode(headline, mode = SizeMode.AUTO) engine.block.setPositionX(headline, value = 56F) engine.block.setPositionY(headline, value = 236F) engine.block.replaceText(headline, text = "Brand Offer") engine.block.setTextFontSize(headline, fontSize = 28F) engine.block.setTextColor(headline, color = Color.fromHex("#FF1F5EFF")) engine.block.appendChild(parent = page, child = headline) // Replace this sample asset URI with your bundled brand font file. val brandRegular = Font( uri = Uri.parse( "file:///android_asset/imgly-assets/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf", ), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ) val brandTypeface = Typeface(name = "Brand Sans", fonts = listOf(brandRegular)) val typefaceSourceId = "ly.img.typeface" val brandTextBlocks = listOf(logoText, legalText, headline) if (typefaceSourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = typefaceSourceId) } engine.asset.addLocalSource(sourceId = typefaceSourceId, supportedMimeTypes = emptyList()) engine.asset.addAsset( sourceId = typefaceSourceId, asset = AssetDefinition( id = "brand-sans", label = mapOf("en" to "Brand Sans"), payload = AssetPayload(typeface = brandTypeface), ), ) engine.asset.assetSourceContentsChanged(sourceId = typefaceSourceId) brandTextBlocks.forEach { textBlock -> engine.block.setTypeface(block = textBlock, typeface = brandTypeface) } val brandControlledScopes = listOf( "editor/select", "text/edit", "text/character", "fill/change", "layer/move", "layer/resize", "layer/rotate", "lifecycle/destroy", "lifecycle/duplicate", ) brandControlledScopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DEFER) } // Use the IDs of brand blocks that already exist in your scene. val lockedBrandBlocks = listOf(logo, logoText, legalText) lockedBrandBlocks.forEach { brandBlock -> brandControlledScopes.forEach { scope -> engine.block.setScopeEnabled(block = brandBlock, key = scope, enabled = false) } } // Use the ID of an editable text block that already exists in your scene. 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 = true) engine.block.setScopeEnabled(block = headline, key = "fill/change", enabled = true) 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) val approvedTypefaces = engine.asset.findAssets( sourceId = typefaceSourceId, query = FindAssetsQuery(perPage = 10, page = 0), ) val logoCanMove = engine.block.isAllowedByScope(block = logo, key = "layer/move") val logoCanBeDeleted = engine.block.isAllowedByScope(block = logo, key = "lifecycle/destroy") val legalTextCanBeEdited = engine.block.isAllowedByScope(block = legalText, key = "text/edit") val headlineCanBeEdited = engine.block.isAllowedByScope(block = headline, key = "text/edit") val headlineCanChangeTypeface = engine.block.isAllowedByScope(block = headline, key = "text/character") val approvedTypefaceDefinitions = approvedTypefaces.assets.mapNotNull { it.payload.typeface }.toSet() val brandTextUsesApprovedTypeface = brandTextBlocks.all { textBlock -> val typefaces = engine.block.getTypefaces(textBlock) typefaces.isNotEmpty() && typefaces.all { it in approvedTypefaceDefinitions } } check(approvedTypefaces.assets.size == 1) check(approvedTypefaceDefinitions.size == 1) check(brandTextUsesApprovedTypeface) check(!logoCanMove) check(!logoCanBeDeleted) check(!legalTextCanBeEdited) check(headlineCanBeEdited) check(headlineCanChangeTypeface) val exportedPng = engine.block.export(block = page, mimeType = MimeType.PNG) return exportedPng } ``` Learn how to restrict available fonts to brand typefaces and lock brand elements like logos and legal text, while keeping selected content editable. ![A branded Android export with locked logo and editable headline content](./assets/android.hero.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-enforce-brand-guidelines) Brand guidelines enforcement in CE.SDK combines two complementary approaches: restricting which assets users can choose and controlling what editing operations are permitted on brand elements. This guide restricts the available fonts to an approved set and uses the scopes system to lock brand elements like logos and legal text so they cannot be modified. On Android, to restrict the colors users can pick in the editor, configure `EditorConfiguration.colorPalette` in your editor configuration. The example builds on a scene with a single page; adapt the block creation to the scene your app edits. The snippets below use the Android Engine API directly. If you expose the same workflow through the CE.SDK editor UI, configure its font sheet separately so the visible font families match the approved typeface assets; the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) is a complete Android UI surface you can adapt around this workflow. ## Restricting Fonts to Brand Typefaces Register the `ly.img.typeface` asset source with only your approved typefaces instead of loading the default typeface source, so only brand fonts are available to choose from. Apply the approved typeface to existing text blocks before validating or exporting the scene. ```kotlin highlight-android-restrict-fonts // Replace this sample asset URI with your bundled brand font file. val brandRegular = Font( uri = Uri.parse( "file:///android_asset/imgly-assets/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf", ), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ) val brandTypeface = Typeface(name = "Brand Sans", fonts = listOf(brandRegular)) val typefaceSourceId = "ly.img.typeface" val brandTextBlocks = listOf(logoText, legalText, headline) if (typefaceSourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = typefaceSourceId) } engine.asset.addLocalSource(sourceId = typefaceSourceId, supportedMimeTypes = emptyList()) engine.asset.addAsset( sourceId = typefaceSourceId, asset = AssetDefinition( id = "brand-sans", label = mapOf("en" to "Brand Sans"), payload = AssetPayload(typeface = brandTypeface), ), ) engine.asset.assetSourceContentsChanged(sourceId = typefaceSourceId) brandTextBlocks.forEach { textBlock -> engine.block.setTypeface(block = textBlock, typeface = brandTypeface) } ``` Each `Typeface` has a display name and a list of `Font` entries. The sample uses a bundled Android font asset so the snippet is copy-safe; replace that URI with a bundled app asset or a self-hosted font file that belongs to your brand. ## Setting Global Scopes to Defer Scopes control which operations are permitted. Setting a global scope to `GlobalScope.DEFER` hands the decision to each block, so per-block settings take effect. ```kotlin highlight-android-global-scope-defer val brandControlledScopes = listOf( "editor/select", "text/edit", "text/character", "fill/change", "layer/move", "layer/resize", "layer/rotate", "lifecycle/destroy", "lifecycle/duplicate", ) brandControlledScopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DEFER) } ``` Without this, the global value applies everywhere and block-level settings are ignored. ## Creating and Locking Brand Elements Create or collect brand blocks such as a logo, wordmark, or legal text, then disable the scopes that must stay protected. The sample uses existing block IDs from its scene setup, but the same calls apply to blocks you create in code. ### Locking the Logo and Legal Text ```kotlin highlight-android-lock-brand-elements // Use the IDs of brand blocks that already exist in your scene. val lockedBrandBlocks = listOf(logo, logoText, legalText) lockedBrandBlocks.forEach { brandBlock -> brandControlledScopes.forEach { scope -> engine.block.setScopeEnabled(block = brandBlock, key = scope, enabled = false) } } ``` With these scopes disabled, protected blocks cannot be selected, moved, resized, recolored, edited, duplicated, or deleted. ## Creating Editable Content Areas While brand elements stay locked, other blocks can remain editable. Enable only the scopes users need on each editable block. ```kotlin highlight-android-editable-content // Use the ID of an editable text block that already exists in your scene. 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 = true) engine.block.setScopeEnabled(block = headline, key = "fill/change", enabled = true) 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 sample headline can be selected, edited, restyled, and recolored, but it cannot be moved, resized, rotated, or deleted. ## Validating Brand Compliance Confirm that the constraints are enforced with `engine.block.isAllowedByScope()`, which considers both the global and block-level scope settings. ```kotlin highlight-android-validate-compliance val approvedTypefaces = engine.asset.findAssets( sourceId = typefaceSourceId, query = FindAssetsQuery(perPage = 10, page = 0), ) val logoCanMove = engine.block.isAllowedByScope(block = logo, key = "layer/move") val logoCanBeDeleted = engine.block.isAllowedByScope(block = logo, key = "lifecycle/destroy") val legalTextCanBeEdited = engine.block.isAllowedByScope(block = legalText, key = "text/edit") val headlineCanBeEdited = engine.block.isAllowedByScope(block = headline, key = "text/edit") val headlineCanChangeTypeface = engine.block.isAllowedByScope(block = headline, key = "text/character") val approvedTypefaceDefinitions = approvedTypefaces.assets.mapNotNull { it.payload.typeface }.toSet() val brandTextUsesApprovedTypeface = brandTextBlocks.all { textBlock -> val typefaces = engine.block.getTypefaces(textBlock) typefaces.isNotEmpty() && typefaces.all { it in approvedTypefaceDefinitions } } check(approvedTypefaces.assets.size == 1) check(approvedTypefaceDefinitions.size == 1) check(brandTextUsesApprovedTypeface) check(!logoCanMove) check(!logoCanBeDeleted) check(!legalTextCanBeEdited) check(headlineCanBeEdited) check(headlineCanChangeTypeface) ``` Use these checks before saving, exporting, or handing a scene to another surface. ## Exporting the Result Export the page after configuring the brand rules. Locked blocks remain visible in the final output and render alongside the editable content. ```kotlin highlight-android-export val exportedPng = engine.block.export(block = page, mimeType = MimeType.PNG) ``` ## Troubleshooting - **Locked elements still movable**: Make sure the global scope is set to `GlobalScope.DEFER` before changing block-level settings; block-level values are ignored while the global scope allows or denies the operation directly. - **Brand elements still editable**: Confirm the matching scope, for example `lifecycle/destroy`, `text/edit`, `text/character`, or `fill/change`, is disabled on the specific block. - **Validation always passes**: `isAllowedByScope()` reflects the global scope unless it is deferred; verify the global scope before relying on block-level results. - **Validation finds unapproved fonts**: Apply the approved typeface to existing text blocks before export. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Asset | Register a local source for approved brand typefaces | | `engine.asset.findAllSources()` | Asset | Check whether the typeface source is already registered | | `engine.asset.removeSource(sourceId=_)` | Asset | Remove an existing source before registering the approved typeface source | | `engine.asset.addAsset(sourceId=_, asset=_)` | Asset | Add a typeface asset to the approved source | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Asset | Notify the engine that the source contents changed | | `engine.asset.findAssets(sourceId=_, query=_)` | Asset | Read back approved assets for validation | | `engine.block.setTypeface(block=_, typeface=_)` | Text | Apply the approved typeface to text blocks already in the scene | | `engine.block.getTypefaces(block=_)` | Text | Validate which typefaces existing text ranges use | | `engine.editor.setGlobalScope(key=_, globalScope=_)` | Scope | Defer a scope to block-level settings | | `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Scope | Enable or disable a scope on one block | | `engine.block.isAllowedByScope(block=_, key=_)` | Scope | Check whether an operation is allowed after global and block rules are combined | | `engine.block.export(block=_, mimeType=_)` | Block | Export the page with the configured brand elements | ## Next Steps - [Rules Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) — Understand the scopes system fundamentals - [Set Editing Constraints](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) — Configure template editing restrictions - [Color Palette](https://img.ly/docs/cesdk/android/user-interface/customization/color-palette-429fd9/) — Customize available colors in 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: "Lock Content" description: "Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system." platform: android url: "https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/android/rules-1427c0/) > [Lock Content](https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/) --- ```kotlin file=@cesdk_android_examples/engine-guides-lock-content/LockContent.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.GlobalScope import ly.img.engine.ShapeType import ly.img.engine.SizeMode private data class LockContentBlocks( val headline: DesignBlock, val mediaPlaceholder: DesignBlock, val movableBadge: DesignBlock, ) data class LockContentResult( val canEditHeadline: Boolean, val canRestyleHeadline: Boolean, val canMoveHeadline: Boolean, val canResizeHeadline: Boolean, val canReplaceMedia: Boolean, val canChangeMediaFillType: Boolean, val canMoveMedia: Boolean, val canResizeMedia: Boolean, val canReplaceBadge: Boolean, val canChangeBadgeFillType: Boolean, val canMoveBadge: Boolean, val canResizeBadge: Boolean, val headlineTextScopeEnabled: Boolean, val textEditGlobalScope: GlobalScope, ) data class TemporaryLockContentResult( val temporarilyLockedTextEditing: Boolean, val temporarilyEnabledSelection: Boolean, val globalScopesRestored: Boolean, val blockScopesRestored: Boolean, ) suspend fun lockContent( engine: Engine, restoreGlobalScopes: Boolean = false, ): LockContentResult { val blocks = createLockContentScene(engine) val scopes = engine.editor.findAllScopes() check("editor/select" in scopes) check("text/edit" in scopes) check("fill/change" in scopes) check("fill/changeType" in scopes) check("layer/move" in scopes) val originalGlobalScopes = if (restoreGlobalScopes) { scopes.associateWith { scope -> engine.editor.getGlobalScope(key = scope) } } else { emptyMap() } try { scopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY) } engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.headline, key = "editor/select", enabled = true) engine.block.setScopeEnabled(blocks.mediaPlaceholder, key = "editor/select", enabled = true) engine.block.setScopeEnabled(blocks.movableBadge, key = "editor/select", enabled = true) engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.headline, key = "text/edit", enabled = true) engine.block.setScopeEnabled(blocks.headline, key = "text/character", enabled = true) engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "fill/changeType", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.mediaPlaceholder, key = "fill/change", enabled = true) engine.block.setScopeEnabled(blocks.mediaPlaceholder, key = "fill/changeType", enabled = true) engine.block.setScopeEnabled(blocks.movableBadge, key = "fill/change", enabled = false) engine.block.setScopeEnabled(blocks.movableBadge, key = "fill/changeType", enabled = false) engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER) listOf(blocks.headline, blocks.mediaPlaceholder).forEach { block -> engine.block.setScopeEnabled(block, key = "layer/move", enabled = false) engine.block.setScopeEnabled(block, key = "layer/resize", enabled = false) } engine.block.setScopeEnabled(blocks.movableBadge, key = "layer/move", enabled = true) engine.block.setScopeEnabled(blocks.movableBadge, key = "layer/resize", enabled = true) val result = LockContentResult( canEditHeadline = engine.block.isAllowedByScope(blocks.headline, key = "text/edit"), canRestyleHeadline = engine.block.isAllowedByScope(blocks.headline, key = "text/character"), canMoveHeadline = engine.block.isAllowedByScope(blocks.headline, key = "layer/move"), canResizeHeadline = engine.block.isAllowedByScope(blocks.headline, key = "layer/resize"), canReplaceMedia = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "fill/change"), canChangeMediaFillType = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "fill/changeType"), canMoveMedia = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "layer/move"), canResizeMedia = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "layer/resize"), canReplaceBadge = engine.block.isAllowedByScope(blocks.movableBadge, key = "fill/change"), canChangeBadgeFillType = engine.block.isAllowedByScope(blocks.movableBadge, key = "fill/changeType"), canMoveBadge = engine.block.isAllowedByScope(blocks.movableBadge, key = "layer/move"), canResizeBadge = engine.block.isAllowedByScope(blocks.movableBadge, key = "layer/resize"), headlineTextScopeEnabled = engine.block.isScopeEnabled(blocks.headline, key = "text/edit"), textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit"), ) check(result.canEditHeadline) check(result.canRestyleHeadline) check(!result.canMoveHeadline) check(!result.canResizeHeadline) check(result.canReplaceMedia) check(result.canChangeMediaFillType) check(!result.canMoveMedia) check(!result.canResizeMedia) check(!result.canReplaceBadge) check(!result.canChangeBadgeFillType) check(result.canMoveBadge) check(result.canResizeBadge) check(result.headlineTextScopeEnabled) check(result.textEditGlobalScope == GlobalScope.DEFER) return result } finally { originalGlobalScopes.forEach { (scope, globalScope) -> engine.editor.setGlobalScope(key = scope, globalScope = globalScope) } } } suspend fun temporarilyLockContent(engine: Engine): TemporaryLockContentResult { val blocks = createLockContentScene(engine) val blockScopesToRestore = listOf( blocks.headline to "editor/select", blocks.mediaPlaceholder to "editor/select", blocks.movableBadge to "editor/select", blocks.headline to "text/edit", blocks.headline to "text/character", blocks.mediaPlaceholder to "fill/change", blocks.mediaPlaceholder to "fill/changeType", blocks.movableBadge to "fill/change", blocks.movableBadge to "fill/changeType", blocks.headline to "layer/move", blocks.headline to "layer/resize", blocks.mediaPlaceholder to "layer/move", blocks.mediaPlaceholder to "layer/resize", blocks.movableBadge to "layer/move", blocks.movableBadge to "layer/resize", ) val scopes = engine.editor.findAllScopes() val originalGlobalScopes = scopes.associateWith { scope -> engine.editor.getGlobalScope(key = scope) } val originalBlockScopes = blockScopesToRestore.map { (block, scope) -> Triple( first = block, second = scope, third = engine.block.isScopeEnabled(block, key = scope), ) } val temporarilyLockedTextEditing: Boolean val temporarilyEnabledSelection: Boolean try { scopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY) } engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.headline, key = "editor/select", enabled = true) temporarilyLockedTextEditing = !engine.block.isAllowedByScope(blocks.headline, key = "text/edit") temporarilyEnabledSelection = engine.block.isAllowedByScope(blocks.headline, key = "editor/select") } finally { originalBlockScopes.forEach { (block, scope, enabled) -> engine.block.setScopeEnabled(block, key = scope, enabled = enabled) } originalGlobalScopes.forEach { (scope, globalScope) -> engine.editor.setGlobalScope(key = scope, globalScope = globalScope) } } return TemporaryLockContentResult( temporarilyLockedTextEditing = temporarilyLockedTextEditing, temporarilyEnabledSelection = temporarilyEnabledSelection, globalScopesRestored = originalGlobalScopes.all { (scope, globalScope) -> engine.editor.getGlobalScope(key = scope) == globalScope }, blockScopesRestored = originalBlockScopes.all { (block, scope, enabled) -> engine.block.isScopeEnabled(block, key = scope) == enabled }, ) } private fun createLockContentScene(engine: Engine): LockContentBlocks { 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 headline = engine.block.create(DesignBlockType.Text) engine.block.setName(headline, "Editable headline") engine.block.setWidthMode(headline, mode = SizeMode.AUTO) engine.block.setHeightMode(headline, mode = SizeMode.AUTO) engine.block.setPositionX(headline, value = 64F) engine.block.setPositionY(headline, value = 64F) engine.block.replaceText(headline, text = "Summer campaign") engine.block.setTextColor(headline, color = Color.fromHex("#FF182133")) engine.block.appendChild(parent = page, child = headline) val mediaPlaceholder = engine.block.create(DesignBlockType.Graphic) engine.block.setName(mediaPlaceholder, "Replaceable image placeholder") engine.block.setShape(mediaPlaceholder, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(mediaPlaceholder, value = 280F) engine.block.setHeight(mediaPlaceholder, value = 180F) engine.block.setPositionX(mediaPlaceholder, value = 64F) engine.block.setPositionY(mediaPlaceholder, value = 180F) engine.block.setFill(mediaPlaceholder, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(mediaPlaceholder, color = Color.fromHex("#FFE6EEF8")) engine.block.appendChild(parent = page, child = mediaPlaceholder) val movableBadge = engine.block.create(DesignBlockType.Graphic) engine.block.setName(movableBadge, "Movable badge") engine.block.setShape(movableBadge, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setWidth(movableBadge, value = 140F) engine.block.setHeight(movableBadge, value = 140F) engine.block.setPositionX(movableBadge, value = 420F) engine.block.setPositionY(movableBadge, value = 220F) engine.block.setFill(movableBadge, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(movableBadge, color = Color.fromHex("#FFFFC857")) engine.block.appendChild(parent = page, child = movableBadge) return LockContentBlocks( headline = headline, mediaPlaceholder = mediaPlaceholder, movableBadge = movableBadge, ) } ``` Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-lock-content) CE.SDK uses scopes to control what users can modify in a design. Each scope gates a specific capability, such as moving, resizing, text editing, or image replacement. The permission system has two layers: global scopes set defaults for the whole scene, and block-level scopes override those defaults when the global scope is set to `GlobalScope.DEFER`. The snippets below use the Android Engine API directly. The same 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 these rules. This guide covers how to discover available scopes, lock an entire design, and selectively enable specific editing capabilities on individual blocks. ## Understanding the Scope Permission Model Global and block-level scopes combine to determine whether an operation is permitted. The global scope can be set to one of three values: | Global Scope | Block Scope | Result | | ------------------- | ----------- | --------- | | `GlobalScope.ALLOW` | any | Permitted | | `GlobalScope.DENY` | any | Blocked | | `GlobalScope.DEFER` | enabled | Permitted | | `GlobalScope.DEFER` | disabled | Blocked | When global is `GlobalScope.ALLOW`, the operation is always permitted. When global is `GlobalScope.DENY`, it is always blocked. When global is `GlobalScope.DEFER`, the block-level enabled state determines the outcome. ## Discovering Available Scopes Retrieve every available scope name with `engine.editor.findAllScopes()`. Use the returned identifiers with the global and block-level scope APIs. ```kotlin highlight-android-find-all-scopes val scopes = engine.editor.findAllScopes() ``` ## Locking an Entire Design To lock every editing operation, iterate through all scopes and set each global scope to `GlobalScope.DENY`. This keeps the code resilient when CE.SDK adds new scopes. ```kotlin highlight-android-lock-all scopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY) } ``` When all scopes are set to `GlobalScope.DENY`, the `editor/select` scope is locked too. Users cannot interact with a block they cannot select, so enable selection before enabling operation-specific scopes. ```kotlin highlight-android-enable-selection engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.headline, key = "editor/select", enabled = true) engine.block.setScopeEnabled(blocks.mediaPlaceholder, key = "editor/select", enabled = true) engine.block.setScopeEnabled(blocks.movableBadge, key = "editor/select", enabled = true) ``` ## Selective Locking Patterns Most integrations lock the full design first, then selectively enable the exact operations users should perform on specific blocks. ### Allowing Text Editing To let users edit text content and styling, set the `text/edit` and `text/character` global scopes to `GlobalScope.DEFER`, then enable them on the chosen text block. ```kotlin highlight-android-text-editing engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.headline, key = "text/edit", enabled = true) engine.block.setScopeEnabled(blocks.headline, key = "text/character", enabled = true) ``` ### Allowing Image Replacement To let users replace media while protecting layout, set `fill/change` and `fill/changeType` to `GlobalScope.DEFER`, then enable both scopes on the image placeholder block. ```kotlin highlight-android-image-replacement engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "fill/changeType", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(blocks.mediaPlaceholder, key = "fill/change", enabled = true) engine.block.setScopeEnabled(blocks.mediaPlaceholder, key = "fill/changeType", enabled = true) engine.block.setScopeEnabled(blocks.movableBadge, key = "fill/change", enabled = false) engine.block.setScopeEnabled(blocks.movableBadge, key = "fill/changeType", enabled = false) ``` ### Allowing Position Adjustments To let users reposition selected elements, set `layer/move` and `layer/resize` to `GlobalScope.DEFER`, then enable those scopes only on blocks that should be movable. ```kotlin highlight-android-position-adjustments engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER) listOf(blocks.headline, blocks.mediaPlaceholder).forEach { block -> engine.block.setScopeEnabled(block, key = "layer/move", enabled = false) engine.block.setScopeEnabled(block, key = "layer/resize", enabled = false) } engine.block.setScopeEnabled(blocks.movableBadge, key = "layer/move", enabled = true) engine.block.setScopeEnabled(blocks.movableBadge, key = "layer/resize", enabled = true) ``` ## Checking Permissions Use `engine.block.isAllowedByScope()` when you need the effective permission. It evaluates the global scope and the block-level scope together. ```kotlin highlight-android-check-permissions val result = LockContentResult( canEditHeadline = engine.block.isAllowedByScope(blocks.headline, key = "text/edit"), canRestyleHeadline = engine.block.isAllowedByScope(blocks.headline, key = "text/character"), canMoveHeadline = engine.block.isAllowedByScope(blocks.headline, key = "layer/move"), canResizeHeadline = engine.block.isAllowedByScope(blocks.headline, key = "layer/resize"), canReplaceMedia = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "fill/change"), canChangeMediaFillType = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "fill/changeType"), canMoveMedia = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "layer/move"), canResizeMedia = engine.block.isAllowedByScope(blocks.mediaPlaceholder, key = "layer/resize"), canReplaceBadge = engine.block.isAllowedByScope(blocks.movableBadge, key = "fill/change"), canChangeBadgeFillType = engine.block.isAllowedByScope(blocks.movableBadge, key = "fill/changeType"), canMoveBadge = engine.block.isAllowedByScope(blocks.movableBadge, key = "layer/move"), canResizeBadge = engine.block.isAllowedByScope(blocks.movableBadge, key = "layer/resize"), headlineTextScopeEnabled = engine.block.isScopeEnabled(blocks.headline, key = "text/edit"), textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit"), ) check(result.canEditHeadline) check(result.canRestyleHeadline) check(!result.canMoveHeadline) check(!result.canResizeHeadline) check(result.canReplaceMedia) check(result.canChangeMediaFillType) check(!result.canMoveMedia) check(!result.canResizeMedia) check(!result.canReplaceBadge) check(!result.canChangeBadgeFillType) check(result.canMoveBadge) check(result.canResizeBadge) check(result.headlineTextScopeEnabled) check(result.textEditGlobalScope == GlobalScope.DEFER) ``` Use `engine.block.isScopeEnabled()` to read only the block-level setting, and `engine.editor.getGlobalScope()` to read only the global setting. ## Saving and Restoring Scope State If your code applies locks temporarily, store the previous global and block-level scope values before changing them. Restore both layers when the operation completes. Skip this restoration step when the scene should remain locked after your function returns. ```kotlin highlight-android-save-scopes val blockScopesToRestore = listOf( blocks.headline to "editor/select", blocks.mediaPlaceholder to "editor/select", blocks.movableBadge to "editor/select", blocks.headline to "text/edit", blocks.headline to "text/character", blocks.mediaPlaceholder to "fill/change", blocks.mediaPlaceholder to "fill/changeType", blocks.movableBadge to "fill/change", blocks.movableBadge to "fill/changeType", blocks.headline to "layer/move", blocks.headline to "layer/resize", blocks.mediaPlaceholder to "layer/move", blocks.mediaPlaceholder to "layer/resize", blocks.movableBadge to "layer/move", blocks.movableBadge to "layer/resize", ) val scopes = engine.editor.findAllScopes() val originalGlobalScopes = scopes.associateWith { scope -> engine.editor.getGlobalScope(key = scope) } val originalBlockScopes = blockScopesToRestore.map { (block, scope) -> Triple( first = block, second = scope, third = engine.block.isScopeEnabled(block, key = scope), ) } ``` ```kotlin highlight-android-restore-scopes originalBlockScopes.forEach { (block, scope, enabled) -> engine.block.setScopeEnabled(block, key = scope, enabled = enabled) } originalGlobalScopes.forEach { (scope, globalScope) -> engine.editor.setGlobalScope(key = scope, globalScope = globalScope) } ``` ## 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 - [Rules Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) - Understand the broader rules system in CE.SDK - [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) - Configure template roles and editing permissions --- ## 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: "Moderate Content" description: "Extract images and text from CE.SDK designs with the Android Engine API, then integrate third-party moderation services to detect inappropriate content." platform: android url: "https://img.ly/docs/cesdk/android/rules/moderate-content-d5ff7e/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/android/rules-1427c0/) > [Moderate Content](https://img.ly/docs/cesdk/android/rules/moderate-content-d5ff7e/) --- ```kotlin file=@cesdk_android_examples/engine-guides-moderate-content/ModerateContent.kt reference-only import android.net.Uri import androidx.annotation.MainThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import ly.img.engine.DesignBlock 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 java.util.concurrent.ConcurrentHashMap enum class ModerationSeverity( val rank: Int, ) { SUCCESS(rank = 0), WARNING(rank = 1), FAILED(rank = 2), } data class ModerationCategory( val name: String, val confidence: Float, val state: ModerationSeverity, ) data class ModerationImageBlock( val block: DesignBlock, val uri: Uri, val name: String, ) data class ModerationTextBlock( val block: DesignBlock, val text: String, val name: String, ) data class ModerationResult( val id: String, val block: DesignBlock, val blockName: String, val contentKind: String, val state: ModerationSeverity, val categories: List, ) data class ModerationCache( val imageCategoriesByUri: MutableMap> = ConcurrentHashMap(), val textCategoriesByContent: MutableMap> = ConcurrentHashMap(), ) data class ModerationSummary( val results: List, val groupedResults: Map>, val detectedImageUris: List, val selectedBlock: DesignBlock?, val selectedAfterInteractiveSelect: Boolean, val exportedImage: ByteBuffer?, ) @MainThread fun getImageUrl( engine: Engine, block: DesignBlock, ): Uri? { if (!engine.block.supportsFill(block)) return null val fill = runCatching { engine.block.getFill(block) }.getOrNull() ?: return null if (engine.block.getType(fill) != FillType.Image.key) return null val sourceSetUri = runCatching { engine.block.getSourceSet(block = fill, property = "fill/image/sourceSet") .firstOrNull { it.uri.toString().isNotBlank() } ?.uri }.getOrNull() return sourceSetUri ?: runCatching { engine.block.getUri(block = fill, property = "fill/image/imageFileURI") }.getOrNull()?.takeIf { it.toString().isNotBlank() } } suspend fun checkAllImages( engine: Engine, moderationCache: ModerationCache, ): List = withContext(engine.dispatcher) { val imageBlocks = engine.block.findByType(DesignBlockType.Graphic).mapNotNull { block -> val uri = getImageUrl(engine = engine, block = block) ?: return@mapNotNull null ModerationImageBlock( block = block, uri = uri, name = engine.block.getName(block).ifBlank { "Image $block" }, ) } val categoriesByUri = imageBlocks .distinctBy { it.uri.toString() } .map { imageBlock -> async { imageBlock.uri.toString() to checkImageContentAPI( image = imageBlock, imageCache = moderationCache.imageCategoriesByUri, ) } } .awaitAll() .toMap() imageBlocks.map { imageBlock -> val categories = categoriesByUri.getValue(imageBlock.uri.toString()) ModerationResult( id = "image:${imageBlock.block}", block = imageBlock.block, blockName = imageBlock.name, contentKind = "image", state = categories.maxByOrNull { it.state.rank }?.state ?: ModerationSeverity.SUCCESS, categories = categories, ) } } @MainThread fun getTextContent( engine: Engine, block: DesignBlock, ): String? = engine.block .getString(block = block, property = "text/text") .trim() .takeIf(String::isNotEmpty) suspend fun checkAllText( engine: Engine, moderationCache: ModerationCache, ): List = withContext(engine.dispatcher) { val textBlocks = engine.block.findByType(DesignBlockType.Text).mapNotNull { block -> val text = getTextContent(engine = engine, block = block) ?: return@mapNotNull null ModerationTextBlock( block = block, text = text, name = engine.block.getName(block).ifBlank { "Text $block" }, ) } val categoriesByText = textBlocks .distinctBy(ModerationTextBlock::text) .map { textBlock -> async { textBlock.text to checkTextContentAPI( text = textBlock, textCache = moderationCache.textCategoriesByContent, ) } } .awaitAll() .toMap() textBlocks.map { textBlock -> val categories = categoriesByText.getValue(textBlock.text) ModerationResult( id = "text:${textBlock.block}", block = textBlock.block, blockName = textBlock.name, contentKind = "text", state = categories.maxByOrNull { it.state.rank }?.state ?: ModerationSeverity.SUCCESS, categories = categories, ) } } suspend fun checkImageContentAPI( image: ModerationImageBlock, imageCache: MutableMap>, ): List { val cacheKey = image.uri.toString() imageCache[cacheKey]?.let { cachedCategories -> return cachedCategories } val categories = withContext(Dispatchers.IO) { // Replace this block with a request to your backend moderation endpoint. listOf( ModerationCategory( name = "adult-content", confidence = 0.16F, state = severityFor(confidence = 0.16F), ), ModerationCategory( name = "violence", confidence = 0.08F, state = severityFor(confidence = 0.08F), ), ) } imageCache[cacheKey] = categories return categories } suspend fun checkTextContentAPI( text: ModerationTextBlock, textCache: MutableMap>, ): List { textCache[text.text]?.let { cachedCategories -> return cachedCategories } val categories = withContext(Dispatchers.IO) { // Replace this block with a request to your backend moderation endpoint. listOf( ModerationCategory( name = "profanity", confidence = 0.12F, state = severityFor(confidence = 0.12F), ), ModerationCategory( name = "policy-review", confidence = if (text.text.contains("review", ignoreCase = true)) 0.46F else 0.18F, state = severityFor( confidence = if (text.text.contains("review", ignoreCase = true)) 0.46F else 0.18F, ), ), ) } textCache[text.text] = categories return categories } fun severityFor(confidence: Float): ModerationSeverity = when { confidence > 0.8F -> ModerationSeverity.FAILED confidence > 0.4F -> ModerationSeverity.WARNING else -> ModerationSeverity.SUCCESS } fun summarizeModerationResults(results: List): Map> = results.groupBy(ModerationResult::state) fun resultsByPriority(results: List): List = results.sortedWith( compareByDescending { it.state.rank } .thenBy { it.contentKind } .thenBy { it.blockName }, ) suspend fun selectModerationResult( engine: Engine, result: ModerationResult, ) = withContext(engine.dispatcher) { if (!engine.block.isValid(result.block)) return@withContext engine.block.findAllSelected().forEach { selectedBlock -> engine.block.setSelected(block = selectedBlock, selected = false) } engine.block.setSelected(block = result.block, selected = true) } suspend fun validateBeforeExport( engine: Engine, targetBlock: DesignBlock, moderationCache: ModerationCache, ): ByteBuffer? { val imageResults = checkAllImages(engine = engine, moderationCache = moderationCache) val textResults = checkAllText(engine = engine, moderationCache = moderationCache) val violations = (imageResults + textResults).filter { it.state == ModerationSeverity.FAILED } if (violations.isNotEmpty()) { return null } return withContext(engine.dispatcher) { if (!engine.block.isValid(targetBlock)) return@withContext null engine.block.export(block = targetBlock, mimeType = MimeType.PNG) } } suspend fun moderateContent(engine: Engine): ModerationSummary = 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 imageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setName(imageBlock, name = "Campaign image") engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(imageBlock, value = 80F) engine.block.setPositionY(imageBlock, value = 80F) engine.block.setWidth(imageBlock, value = 320F) engine.block.setHeight(imageBlock, value = 220F) 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 sourceSetImageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setName(sourceSetImageBlock, name = "Responsive campaign image") engine.block.setShape(sourceSetImageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(sourceSetImageBlock, value = 430F) engine.block.setPositionY(sourceSetImageBlock, value = 80F) engine.block.setWidth(sourceSetImageBlock, value = 280F) engine.block.setHeight(sourceSetImageBlock, value = 220F) engine.block.appendChild(parent = page, child = sourceSetImageBlock) val sourceSetImageFill = engine.block.createFill(FillType.Image) engine.block.setSourceSet( block = sourceSetImageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), ), ) engine.block.setFill(block = sourceSetImageBlock, fill = sourceSetImageFill) val textBlock = engine.block.create(DesignBlockType.Text) engine.block.setName(textBlock, name = "Campaign headline") engine.block.setPositionX(textBlock, value = 80F) engine.block.setPositionY(textBlock, value = 340F) engine.block.setWidth(textBlock, value = 520F) engine.block.setHeight(textBlock, value = 80F) engine.block.setString( block = textBlock, property = "text/text", value = "Needs manual review before publishing", ) engine.block.appendChild(parent = page, child = textBlock) val moderationCache = ModerationCache() val imageResults = checkAllImages(engine = engine, moderationCache = moderationCache) val textResults = checkAllText(engine = engine, moderationCache = moderationCache) val results = resultsByPriority(imageResults + textResults) val selectedResult = results.firstOrNull() if (selectedResult != null) { selectModerationResult(engine = engine, result = selectedResult) } ModerationSummary( results = results, groupedResults = summarizeModerationResults(results), detectedImageUris = engine.block.findByType(DesignBlockType.Graphic).mapNotNull { block -> getImageUrl(engine = engine, block = block) }, selectedBlock = selectedResult?.block, selectedAfterInteractiveSelect = selectedResult?.let { engine.block.findAllSelected().contains(it.block) } ?: false, exportedImage = validateBeforeExport( engine = engine, targetBlock = page, moderationCache = moderationCache, ), ) } ``` Use CE.SDK's Engine API to extract images and text from designs, then integrate third-party moderation services to detect inappropriate content. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-moderate-content) CE.SDK does not provide prebuilt content moderation workflows. Instead, it provides engine APIs that extract images and text from designs for moderation by third-party services of your choice. This is intentional: moderation requirements are specific to each business, including which categories to check, what thresholds to apply, and which services to use. Content moderation helps maintain quality standards and comply with content policies. Unlike built-in validation rules that check technical aspects like resolution or layout, content moderation relies on external AI services (Sightengine, AWS Rekognition, OpenAI Moderation API) to analyze visual content and textual content. The sample uses these small value types to carry extracted content, per-category confidence scores, and the block IDs that let your app make moderation findings actionable: ```kotlin highlight-android-models enum class ModerationSeverity( val rank: Int, ) { SUCCESS(rank = 0), WARNING(rank = 1), FAILED(rank = 2), } data class ModerationCategory( val name: String, val confidence: Float, val state: ModerationSeverity, ) data class ModerationImageBlock( val block: DesignBlock, val uri: Uri, val name: String, ) data class ModerationTextBlock( val block: DesignBlock, val text: String, val name: String, ) data class ModerationResult( val id: String, val block: DesignBlock, val blockName: String, val contentKind: String, val state: ModerationSeverity, val categories: List, ) data class ModerationCache( val imageCategoriesByUri: MutableMap> = ConcurrentHashMap(), val textCategoriesByContent: MutableMap> = ConcurrentHashMap(), ) ``` ## Finding Content in Designs Locate image and text blocks, then extract the data your moderation backend needs. **Images**: Use `findByType(DesignBlockType.Graphic)` to find graphic blocks, then keep only blocks whose fill is an image fill. `getFill()` returns the fill block and `getType()` verifies `FillType.Image`. The helper prefers the highest-resolution non-empty source from `fill/image/sourceSet`, matching the Engine's source-set precedence, then falls back to `fill/image/imageFileURI`: ```kotlin highlight-android-get-image-url @MainThread fun getImageUrl( engine: Engine, block: DesignBlock, ): Uri? { if (!engine.block.supportsFill(block)) return null val fill = runCatching { engine.block.getFill(block) }.getOrNull() ?: return null if (engine.block.getType(fill) != FillType.Image.key) return null val sourceSetUri = runCatching { engine.block.getSourceSet(block = fill, property = "fill/image/sourceSet") .firstOrNull { it.uri.toString().isNotBlank() } ?.uri }.getOrNull() return sourceSetUri ?: runCatching { engine.block.getUri(block = fill, property = "fill/image/imageFileURI") }.getOrNull()?.takeIf { it.toString().isNotBlank() } } ``` Process every image by checking each unique URI against the moderation service. Repeated image fills reuse the same result from the workflow-owned `ModerationCache`. The extraction phase switches to `engine.dispatcher` before accessing Engine APIs, then the backend checks run concurrently: ```kotlin highlight-android-check-all-images suspend fun checkAllImages( engine: Engine, moderationCache: ModerationCache, ): List = withContext(engine.dispatcher) { val imageBlocks = engine.block.findByType(DesignBlockType.Graphic).mapNotNull { block -> val uri = getImageUrl(engine = engine, block = block) ?: return@mapNotNull null ModerationImageBlock( block = block, uri = uri, name = engine.block.getName(block).ifBlank { "Image $block" }, ) } val categoriesByUri = imageBlocks .distinctBy { it.uri.toString() } .map { imageBlock -> async { imageBlock.uri.toString() to checkImageContentAPI( image = imageBlock, imageCache = moderationCache.imageCategoriesByUri, ) } } .awaitAll() .toMap() imageBlocks.map { imageBlock -> val categories = categoriesByUri.getValue(imageBlock.uri.toString()) ModerationResult( id = "image:${imageBlock.block}", block = imageBlock.block, blockName = imageBlock.name, contentKind = "image", state = categories.maxByOrNull { it.state.rank }?.state ?: ModerationSeverity.SUCCESS, categories = categories, ) } } ``` Engine URIs can use app-local schemes such as `file`, `content`, `bundle`, or `buffer`. When your moderation provider requires a public URL, upload or proxy that resource through your authenticated backend before calling the provider. **Text**: Use `findByType(DesignBlockType.Text)` to find text blocks, then read the visible content from the `text/text` property: ```kotlin highlight-android-get-text-content @MainThread fun getTextContent( engine: Engine, block: DesignBlock, ): String? = engine.block .getString(block = block, property = "text/text") .trim() .takeIf(String::isNotEmpty) ``` Process every text block the same way, deduplicating repeated strings before launching backend requests: ```kotlin highlight-android-check-all-text suspend fun checkAllText( engine: Engine, moderationCache: ModerationCache, ): List = withContext(engine.dispatcher) { val textBlocks = engine.block.findByType(DesignBlockType.Text).mapNotNull { block -> val text = getTextContent(engine = engine, block = block) ?: return@mapNotNull null ModerationTextBlock( block = block, text = text, name = engine.block.getName(block).ifBlank { "Text $block" }, ) } val categoriesByText = textBlocks .distinctBy(ModerationTextBlock::text) .map { textBlock -> async { textBlock.text to checkTextContentAPI( text = textBlock, textCache = moderationCache.textCategoriesByContent, ) } } .awaitAll() .toMap() textBlocks.map { textBlock -> val categories = categoriesByText.getValue(textBlock.text) ModerationResult( id = "text:${textBlock.block}", block = textBlock.block, blockName = textBlock.name, contentKind = "text", state = categories.maxByOrNull { it.state.rank }?.state ?: ModerationSeverity.SUCCESS, categories = categories, ) } } ``` ## Integrating Moderation APIs Integrate external AI services such as Sightengine, AWS Rekognition, or the OpenAI Moderation API through your backend. Do not ship moderation provider credentials in the Android app. **Image Moderation** - The example uses a simulated backend response and stores results by URI in the workflow-owned cache. Replace the marked block with your own backend request: ```kotlin highlight-android-image-moderation-api suspend fun checkImageContentAPI( image: ModerationImageBlock, imageCache: MutableMap>, ): List { val cacheKey = image.uri.toString() imageCache[cacheKey]?.let { cachedCategories -> return cachedCategories } val categories = withContext(Dispatchers.IO) { // Replace this block with a request to your backend moderation endpoint. listOf( ModerationCategory( name = "adult-content", confidence = 0.16F, state = severityFor(confidence = 0.16F), ), ModerationCategory( name = "violence", confidence = 0.08F, state = severityFor(confidence = 0.08F), ), ) } imageCache[cacheKey] = categories return categories } ``` **Text Moderation** - Text checks follow the same shape and store results by text content in the same cache: ```kotlin highlight-android-text-moderation-api suspend fun checkTextContentAPI( text: ModerationTextBlock, textCache: MutableMap>, ): List { textCache[text.text]?.let { cachedCategories -> return cachedCategories } val categories = withContext(Dispatchers.IO) { // Replace this block with a request to your backend moderation endpoint. listOf( ModerationCategory( name = "profanity", confidence = 0.12F, state = severityFor(confidence = 0.12F), ), ModerationCategory( name = "policy-review", confidence = if (text.text.contains("review", ignoreCase = true)) 0.46F else 0.18F, state = severityFor( confidence = if (text.text.contains("review", ignoreCase = true)) 0.46F else 0.18F, ), ), ) } textCache[text.text] = categories return categories } ``` **Processing Results** - Map confidence scores to severity levels: failed above `0.8F`, warning above `0.4F`, and success at or below `0.4F`: ```kotlin highlight-android-threshold-mapping fun severityFor(confidence: Float): ModerationSeverity = when { confidence > 0.8F -> ModerationSeverity.FAILED confidence > 0.4F -> ModerationSeverity.WARNING else -> ModerationSeverity.SUCCESS } ``` ## Displaying Validation Results Group results by severity in your own UI, and sort them so failed or warning items appear first: ```kotlin highlight-android-display-results fun summarizeModerationResults(results: List): Map> = results.groupBy(ModerationResult::state) fun resultsByPriority(results: List): List = results.sortedWith( compareByDescending { it.state.rank } .thenBy { it.contentKind } .thenBy { it.blockName }, ) ``` Make results actionable by selecting the corresponding block when the user picks a finding. This helps them locate problematic content on the canvas. The helper switches to the Engine dispatcher and ignores findings whose block was removed while moderation was running: ```kotlin highlight-android-interactive-results suspend fun selectModerationResult( engine: Engine, result: ModerationResult, ) = withContext(engine.dispatcher) { if (!engine.block.isValid(result.block)) return@withContext engine.block.findAllSelected().forEach { selectedBlock -> engine.block.setSelected(block = selectedBlock, selected = false) } engine.block.setSelected(block = result.block, selected = true) } ``` ## Integration Points Run validation at the point that fits your workflow. The most common gate is export: validate the design and refuse to render it when violations exist. The example checks image and text results, then exports the target block to PNG only when no failed result remains: ```kotlin highlight-android-export-validation suspend fun validateBeforeExport( engine: Engine, targetBlock: DesignBlock, moderationCache: ModerationCache, ): ByteBuffer? { val imageResults = checkAllImages(engine = engine, moderationCache = moderationCache) val textResults = checkAllText(engine = engine, moderationCache = moderationCache) val violations = (imageResults + textResults).filter { it.state == ModerationSeverity.FAILED } if (violations.isNotEmpty()) { return null } return withContext(engine.dispatcher) { if (!engine.block.isValid(targetBlock)) return@withContext null engine.block.export(block = targetBlock, mimeType = MimeType.PNG) } } ``` Other integration points follow the same pattern: - **Pre-upload validation**: Check content before allowing uploads to your platform. - **Review queue**: Flag designs with warnings for manual review. - **Batch validation**: Check all content on demand when the user requests it. ## Best Practices **Security**: Proxy moderation requests through your backend to protect credentials. Apply rate limiting, require authentication, and log checks for compliance. **Performance**: Keep a `ModerationCache` for the workflow and pass it to both content checks and export validation. This deduplicates repeated image URIs and text content, preserves results between validation passes, and processes unique moderation requests concurrently. **User Experience**: Run checks asynchronously so the editor stays responsive. Provide clear messages and let users jump to flagged blocks. In an interactive export flow, pause edits while moderation runs or restart validation when the design changes. **Timing**: Validate at export time for the best balance between policy enforcement and creative freedom. ## Troubleshooting **Checks not running**: Verify that the engine is initialized, a scene is loaded, content exists, your backend endpoint is reachable, and moderation credentials are valid on the server. **Content not found**: Find image content by scanning graphic blocks and filtering for `FillType.Image`. A block's kind is free-form and may be unset, so fill type is the dependable image check. For text, skip empty `text/text` values. **API errors**: Check backend authentication, endpoint URLs, image URL accessibility, rate limits, and provider-specific error codes. **Inconsistent results**: Verify cache keys, threshold values, API response parsing, and confidence score ranges. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.findByType(type=DesignBlockType.Graphic)` | Find graphic blocks that may contain image fills | | `engine.block.findByType(type=DesignBlockType.Text)` | Find text blocks | | `engine.dispatcher` | Switch coroutine work to the Engine thread before accessing Engine APIs | | `engine.block.supportsFill(block=_)` | Check whether a block can have a fill | | `engine.block.getFill(block=_)` | Get the fill block attached to a design block | | `engine.block.getType(block=_)` | Read a block or fill type string, such as `FillType.Image.key` | | `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Read the fallback image URI when no source set is available | | `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Read responsive image sources in highest-resolution-first order | | `engine.block.getString(block=_, property="text/text")` | Read a text block's content | | `engine.block.getName(block=_)` | Read a block display name for result labels | | `engine.block.findAllSelected()` | Get currently selected blocks | | `engine.block.isValid(block=_)` | Check that a moderated block still exists before selecting or exporting it | | `engine.block.setSelected(block=_, selected=_)` | Select or deselect a block | | `engine.block.export(block=_, mimeType=MimeType.PNG)` | Render a block to PNG after validation passes | ### Properties | Property | Type | Description | | --- | --- | --- | | `fill/image/imageFileURI` | `Uri` | Fallback image URI when no source set is available | | `fill/image/sourceSet` | `List` | Preferred responsive image sources with URIs and pixel dimensions | | `text/text` | `String` | Text content of a text block | ## Next Steps Now that you understand content moderation, explore related validation features: - [Rules Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) — Understand the scopes and permission model behind CE.SDK 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: "Overview" description: "Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs." platform: android url: "https://img.ly/docs/cesdk/android/rules/overview-e27832/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/android/rules-1427c0/) > [Overview](https://img.ly/docs/cesdk/android/rules/overview-e27832/) --- Learn how CE.SDK's rules system enforces design constraints and controls editing permissions through the scopes mechanism. In CE.SDK, rules are design constraints and guardrails that control which editing operations are permitted. The primary mechanism for enforcing rules is the **scopes system**: permission flags that let you build guided editing experiences, maintain brand consistency, ensure design quality, and prevent unauthorized modifications. This guide covers the scopes system conceptually: the available scope categories, the difference between global and block-level scopes, and how to resolve the effective permission for a block. Concrete locking, brand, and moderation workflows belong in focused rules guides. ## What Are Scopes Scopes are permission flags that control specific editing capabilities. Each scope maps to an operation category and uses a string key such as `layer/move` or `fill/change`. CE.SDK groups scopes into four logical categories: **Layer operations** - Control positioning and transformation: - `layer/move`, `layer/resize`, `layer/rotate`, `layer/flip`, `layer/crop` - `layer/opacity`, `layer/blendMode`, `layer/visibility`, `layer/clipping` **Appearance** - Control visual effects and adjustments: - `appearance/adjustments`, `appearance/filter`, `appearance/effect` - `appearance/blur`, `appearance/shadow`, `appearance/animation` **Content editing** - Control content modifications: - `text/edit`, `text/character` - `fill/change`, `fill/changeType`, `stroke/change`, `shape/change` **Lifecycle** - Control block management: - `lifecycle/destroy`, `lifecycle/duplicate` - `editor/add`, `editor/select` When a scope is denied, the CE.SDK editor UI checks the same effective permission and disables matching controls where that operation is exposed. You can query the available scope keys with `engine.editor.findAllScopes()`. ## Setting Global Scopes Global scopes set editor-wide defaults that apply to every block. Each global scope has one of three permission levels: - `GlobalScope.ALLOW` - The operation is always permitted for every block. - `GlobalScope.DENY` - The operation is always blocked for every block. - `GlobalScope.DEFER` - Control is deferred to each block's individual scope setting. Use `engine.editor.setGlobalScope(key=_, globalScope=_)` when an operation should be allowed, blocked, or delegated consistently across the whole editing experience. Read the current default with `engine.editor.getGlobalScope(key=_)`. For example, a brand-controlled workflow can deny destructive operations globally, while a template workflow can defer selected operations so individual blocks decide what users may edit. ## Setting Block-Level Scopes Block-level scopes apply to individual blocks when the matching global scope is deferred. They let a scene mix locked and editable elements: a logo can stay fixed while a text placeholder remains editable, or a background can keep its size while another graphic can still be moved. Set a block-level permission with `engine.block.setScopeEnabled(block=_, key=_, enabled=_)`. Read the stored block-level value with `engine.block.isScopeEnabled(block=_, key=_)`. ## Checking Scope Permissions The effective permission comes from the combination of global and block-level settings. A global `GlobalScope.DENY` blocks the operation everywhere, a global `GlobalScope.ALLOW` permits it everywhere, and `GlobalScope.DEFER` lets the block-level setting decide. Before performing a protected operation programmatically, check the effective permission with `engine.block.isAllowedByScope(block=_, key=_)`. This resolves the global and block-level settings into the permission that CE.SDK enforces for that block. ## UI Integration The CE.SDK editor UI uses effective scope permissions when it presents editing controls. Denied operations can remove transform handles, hide or disable inspector controls, and prevent matching menu actions. This keeps the visible editing surface aligned with the same rules that protect programmatic operations. ## Common Use Cases Rules support several editing and compliance scenarios: - **Lock content** - Prevent changes to specific elements such as logos or legal text. - **Enforce brand guidelines** - Restrict fonts, colors, and styles to approved options. - **Moderate content** - Integrate external services to validate content appropriateness. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.editor.findAllScopes()` | Global | List the available scope keys | | `engine.editor.setGlobalScope(key=_, globalScope=_)` | Global | Set an editor-wide scope permission to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER` | | `engine.editor.getGlobalScope(key=_)` | Global | Get the current global scope value | | `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Block | Enable or disable a scope for a specific block | | `engine.block.isScopeEnabled(block=_, key=_)` | Block | Check whether a scope is enabled on a block | | `engine.block.isAllowedByScope(block=_, key=_)` | Effective | Check whether the operation is allowed after global and block-level scope resolution | ## Next Steps - [Lock Content](https://img.ly/docs/cesdk/android/rules/lock-content-9fa727/) - Lock specific elements and selectively enable editing capabilities. - [Enforce Brand Guidelines](https://img.ly/docs/cesdk/android/rules/enforce-brand-guidelines-23a1e3/) - Restrict approved assets and protect brand elements. - [Moderate Content](https://img.ly/docs/cesdk/android/rules/moderate-content-d5ff7e/) - Learn how to implement automated content moderation to flag potentially inappropriate images in your CE.SDK 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: "Security" description: "Learn how CE.SDK keeps your data private with client-side processing, secure licensing, and GDPR-compliant practices." platform: android url: "https://img.ly/docs/cesdk/android/security-777bfd/" --- > 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/) > [Security](https://img.ly/docs/cesdk/android/security-777bfd/) --- This document provides a comprehensive overview of CE.SDK's security practices, focusing on data handling, privacy, and our commitment to maintaining the highest standards of security for our customers and their end users. ## Key Security Features - **Client-Side Processing**: All image and design processing occurs directly on the user's device or your servers, not on our servers - **No Data Transmission**: Your content (e.g. images, designs, templates, videos, audio, etc.) is never uploaded to or processed on IMG.LY servers - **Minimal Data Collection**: We only collect device identifiers and count exports for licensing purposes - **GDPR Compliance**: Our data collection practices adhere to GDPR regulations - **Secure Licensing**: Enterprise licenses are secured with RSA SHA256 encryption ## Data Protection & Access Controls ### Data Collection CE.SDK requires minimal data to provide its services. The only potentially personally identifiable information (PII) collected includes device-specific identifiers such as `identifierForVendor` on iOS and `ANDROID_ID` on Android. These identifiers are: - Used solely for tracking monthly active users for our usage-based pricing models - Reset when the user reinstalls the app or resets their device - Collected under GDPR's legitimate interest provision (no explicit consent required as they are necessary for our licensing system) Additionally, we track export operations for billing purposes in usage-based pricing models. For enterprise customers who prefer more accurate tracking, integrators can provide their own userID. This allows for more precise measurement of usage without requiring additional device identifiers. ### Data Storage & Encryption **We do not collect or store user data beyond the device identifiers and export counts mentioned above.** Since CE.SDK operates entirely on the device or server running the engine: - All content processing happens on the device or server running the engine - No images, designs, or user content is transmitted to IMG.LY servers - No content data is stored on IMG.LY infrastructure We use standard HTTPS (SSL/TLS) encryption for all communications between CE.SDK instances and our licensing backend. ### Access Controls We are using established industry standard practices to handle sensitive customer data. Therefore access control concerns are minimized. The limited data we do handle is protected as follows: - Billing information is stored in Stripe and accessed only by members of our finance team and C-level executives - API keys and credentials are stored securely in 1Password or GitHub with granular access levels - All employees sign Confidentiality Agreements to protect customer information This refers to data of our direct customers, not their users or customers. ## Licensing System CE.SDK uses a licensing system that works as follows: 1. During instantiation, an API key is provided to the CE.SDK instance 2. This API key is held in memory (never stored permanently on the device) 3. The SDK validates the key with our licensing backend 4. Upon successful validation, the backend returns a temporary local license 5. This license is periodically refreshed to maintain valid usage For browser implementations, we protect licenses against misuse by pinning them to specific domains. For mobile applications, licenses are pinned to the application identifiers to prevent unauthorized use. For enterprise customers, we offer an alternative model: - A license file is passed directly to the instance - No communication with our licensing service is required - Licenses are secured using RSA SHA256 encryption ### CE.SDK Renderer CE.SDK Renderer is a specialized variant of CE.SDK that consists of a native Linux binary bundled in a Docker container. It uses GPU acceleration and native code to render scenes and archives to various export formats. Due to bundled third-party codecs (mainly H.264 & H.265) and their associated patent requirements, CE.SDK Renderer implements additional licensing communication beyond the standard licensing handshake: 1. **Initial License Validation**: The tool performs the standard license validation with our licensing backend 2. **Periodic Heartbeats**: After successful validation, it sends periodic heartbeats to our licensing backend to track the number of active instances 3. **Instance Limits**: We limit the maximum number of active instances per license based on the settings in your dashboard 4. **Activation Control**: If the instance limit is exceeded, further activations (launches) of the tool will fail with a descriptive error message This additional communication allows us to ensure compliance with codec licensing requirements while providing transparent usage tracking for your organization. As with all CE.SDK products, no user data (images, videos, designs, or other content) is transmitted to IMG.LY servers - only device identifiers and instance counts are collected for licensing purposes. ## Security Considerations for User Input As CE.SDK deals primarily with arbitrary user input, we've implemented specific security measures to handle data safely: - The CreativeEngine reads files from external resources to fetch images, fonts, structured data, and other sources for designs. These reads are safeguarded by platform-specific default measures. - The engine never loads executable code or attempts to execute any data acquired from dynamic content. It generally relies on provided mime types to decode image data or falls back to byte-level inspection to choose the appropriate decoder. - For data writing operations, we provide a callback that returns a pointer to the to-be-written data. The engine itself never unconditionally writes to an externally defined path. If it writes to files directly, these are part of internal directories and can't be modified externally. - Generated PDFs may have original image files embedded if the image was not altered via effects or blurs and the `exportPdfWithHighCompatibility` option was **not** enabled. This means a malicious image file could theoretically be included in the exported PDF. - Inline text-editing allows arbitrary input of strings by users. The engine uses platform-specific default inputs and APIs and doesn't apply additional sanitization. The acquired strings are stored and used exclusively for text rendering - they are neither executed nor used for file operations. ## Security Infrastructure ### Vulnerability Management We take a proactive approach to security vulnerability management: - We use GitHub to track dependency vulnerabilities - We regularly update affected dependencies - We don't maintain a private network, eliminating network vulnerability concerns in that context - We don't manually maintain servers or infrastructure, as we don't have live systems beyond those required for licensing - For storage and licensing, we use virtual instances in Google Cloud which are managed by the cloud provider - All security-related fixes are published in our public changelog at [https://img.ly/docs/cesdk/changelog/](https://img.ly/docs/cesdk/changelog/) ### Security Development Practices Our development practices emphasize security: - We rely on established libraries with proven security track records - We don't directly process sensitive user data in our code - Secrets (auth tokens, passwords, API credentials, certificates) are stored in GitHub or 1Password with granular access levels - We use RSA SHA256 encryption for our enterprise licenses - We rely on platform-standard SSL implementations for HTTPS communications ### API Key Management API keys for CE.SDK are handled securely: - Keys are passed during instantiation and held in memory only - Keys are never stored permanently on client devices - For web implementation, keys are pinned to specific domains to prevent unauthorized use - Enterprise licenses use a file-based approach that doesn't require API key validation ## Compliance IMG.LY complies with the General Data Protection Regulation (GDPR) in all our operations, including CE.SDK. Our Privacy Policy is publicly available at [https://img.ly/privacy-policy](https://img.ly/privacy-policy). Our local approach to content processing significantly reduces privacy and compliance concerns, as user content never leaves the environment running the engine. ## FAQ ### Does CE.SDK upload my images or designs to IMG.LY servers? No. CE.SDK processes all content locally on the user's device. Your images, designs, and other content are never transmitted to IMG.LY servers. ### What data does IMG.LY collect through CE.SDK? CE.SDK only collects device identifiers (such as identifierForVendor on iOS or ANDROID\_ID on Android) for licensing purposes and export counts. No user content or personal information is collected. ### How does IMG.LY protect API keys? API keys are never stored permanently; they are held in memory during SDK operation. For web implementations, keys are pinned to specific domains to prevent unauthorized use. ### Has IMG.LY experienced any security breaches? No, IMG.LY has not been involved in any cybersecurity breaches in the last 12 months. ### Does IMG.LY conduct security audits? As we don't store customer data directly, but rely on third parties to do so, we focus our security efforts on dependency tracking and vulnerability management through GitHub's security features. We don't conduct security audits. ## Additional Information For more detailed information about our data collection practices, please refer to our Data Privacy and Retention information below. Should you have any additional questions regarding security practices or require more information, please contact our team at [support@img.ly](mailto:support@img.ly). ## Data Privacy and Retention At IMG.LY, we prioritize your data privacy and ensure that apart from a minimal contractually stipulated set of interactions with our servers all other operations take place on your local device. Below is an overview of our data privacy and retention policies: ### **Data Processing** All data processed by CE.SDK remains strictly on the device or server running the engine. We do not transfer your data to our servers for processing. This means that operations such as rendering, editing, and other in-app functionalities happen entirely locally, ensuring that sensitive project or personal data stays with you. ### **Data Retention** We do not store any project-related data on our servers. Since all data operations occur locally, no information about your edits, images, or video content is retained by CE.SDK. The only data that interacts with our servers is related to license validation and telemetry related to usage tied to your pricing plan. ### **License Validation** CE.SDK performs a license validation check with our servers once upon initialization to validate the software license being used. This interaction is minimal and does not involve the transfer of any personal, project, or media data. ### **Event Tracking** While CE.SDK does not track user actions other than the exceptions listed below through telemetry or analytics by default, there are specific events tracked to manage customer usage, particularly for API key usage tracking. We gather the following information during these events: - **When the engine loads:** App identifier, platform, engine version, user ID (provided by the client), device ID (mobile only), and session ID. - **When a photo or video is exported:** User ID, device ID, session ID, media type (photo/video), format, resolution (width and height), page count (photo only), FPS (video only), and duration (video only). This tracking is solely for ensuring accurate usage calculation and managing monthly active user billing. Enterprise clients can opt out of this tracking under specific agreements. For a precise definition of which operations count as an export and when export events are recorded, see [Export Counting](https://img.ly/docs/cesdk/android/export-counting-613923/). ### **Personal Identifiable Information (PII)** The only PII that is potentially collected includes device-specific identifiers such as `identifierForVendor` on iOS and `ANDROID_ID` on Android. These IDs are used for tracking purposes and are reset when the user reinstalls the app or resets the device. No consent is required for these identifiers because they are crucial for our usage-based pricing models. This is covered by the GDPR as legitimate interest. ### **User Consent** As mentioned above, user consent is not required when solely using the CE.SDK. However, this may change depending on the specific enterprise agreement or additional regulatory requirements. IMG.LY is committed to maintaining compliance with **GDPR** and other applicable data protection laws, ensuring your privacy is respected at all times. For details consult our [privacy policy](https://img.ly/privacy-policy). --- ## 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: "Serve Assets" description: "Configure CE.SDK to load engine and content assets from your own servers instead of the IMG.LY CDN for production deployments." platform: android url: "https://img.ly/docs/cesdk/android/serve-assets-b0827c/" --- > 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/) > [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) --- ```kotlin file=@cesdk_android_examples/engine-guides-serve-assets/ServeAssets.kt reference-only import android.net.Uri import ly.img.engine.Engine private val serveAssetsDefaultSourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.caption.presets", ) private val serveAssetsSampleSourceIds = listOf( "ly.img.image", "ly.img.video", "ly.img.audio", "ly.img.templates", "ly.img.templates.premium", ) data class ServeAssetsResult( val defaultSourceIds: List, val sampleSourceIds: List, val basePath: String, ) private suspend fun registerServeAssetSources( engine: Engine, baseUri: Uri, sourceIds: List, matchersBySourceId: Map> = emptyMap(), ): List { val existingSourceIds = engine.asset.findAllSources().toSet() val registeredSourceIds = mutableListOf() sourceIds .filterNot(existingSourceIds::contains) .forEach { sourceId -> registeredSourceIds += engine.asset.addLocalSourceFromJSON( contentUri = baseUri.buildUpon() .appendPath(sourceId) .appendPath("content.json") .build(), matcher = matchersBySourceId[sourceId], ) } return registeredSourceIds } suspend fun serveAssets( engine: Engine, baseUri: Uri, ): ServeAssetsResult { registerServeAssetSources( engine = engine, baseUri = baseUri, sourceIds = serveAssetsDefaultSourceIds, ) registerServeAssetSources( engine = engine, baseUri = baseUri, sourceIds = serveAssetsSampleSourceIds, ) engine.editor.setSettingString( keypath = "basePath", value = baseUri.toString(), ) val sourceIds = engine.asset.findAllSources() return ServeAssetsResult( defaultSourceIds = serveAssetsDefaultSourceIds.filter(sourceIds::contains), sampleSourceIds = serveAssetsSampleSourceIds.filter(sourceIds::contains), basePath = engine.editor.getSettingString(keypath = "basePath"), ) } fun serveAssetsRemoteBaseUri(): Uri { return Uri.parse("https://cdn.your.custom.domain/assets") } fun serveAssetsAppAssetsBaseUri(): Uri { return Uri.parse("file:///android_asset/assets") } suspend fun serveAssetsWithFilteredStickers( engine: Engine, baseUri: Uri, ) { registerServeAssetSources( engine = engine, baseUri = baseUri, sourceIds = listOf("ly.img.sticker"), matchersBySourceId = mapOf( "ly.img.sticker" to listOf("ly.img.sticker.emoji.*"), ), ) } ``` Configure the Creative Engine to load its asset sources from your own server or Android app assets instead of the IMG.LY CDN. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-serve-assets) The editor loads assets from the IMG.LY CDN by default, which is convenient while you are getting started. For production, host those assets yourself so your app controls runtime availability, caching, and compliance requirements. ## Download the Assets Download the asset archive for the same CE.SDK Android version that your app uses. Android, iOS, and web archives are platform-specific, so use the `cesdk-android` package path for Android projects. [Download Assets (v$UBQ\_VERSION$)](https://cdn.img.ly/packages/imgly/cesdk-android/$UBQ_VERSION$/imgly-assets.zip) Or download and extract it from the command line: ```bash curl -O https://cdn.img.ly/packages/imgly/cesdk-android/$UBQ_VERSION$/imgly-assets.zip unzip imgly-assets.zip -d imgly-assets ``` The archive extracts an inner `assets/` directory. Deploy or copy that inner folder, for example `imgly-assets/assets/`, and point your `baseUri` at it. Inside that folder, CE.SDK expects one `ly.img.*` directory per asset source, the `fonts/` and `emoji/` directories, and preview thumbnail directories such as `ly.img.animation` and `ly.img.animation.text`. Thumbnail directories do not contain `content.json` manifests, so keep them at your asset base URI but do not register them as asset sources. ## Register the Default Asset Sources Each asset source is described by a `content.json` manifest. Register a source by pointing `engine.asset.addLocalSourceFromJSON(...)` at its manifest URI; the engine resolves the files in that source relative to the manifest location. ```kotlin highlight-android-default-source-ids private val serveAssetsDefaultSourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.caption.presets", ) ``` Use the same helper for every default source ID. The sample skips sources that are already registered so the setup is safe to call when your editor or test reuses an Engine instance. ```kotlin highlight-android-register-sources private suspend fun registerServeAssetSources( engine: Engine, baseUri: Uri, sourceIds: List, matchersBySourceId: Map> = emptyMap(), ): List { val existingSourceIds = engine.asset.findAllSources().toSet() val registeredSourceIds = mutableListOf() sourceIds .filterNot(existingSourceIds::contains) .forEach { sourceId -> registeredSourceIds += engine.asset.addLocalSourceFromJSON( contentUri = baseUri.buildUpon() .appendPath(sourceId) .appendPath("content.json") .build(), matcher = matchersBySourceId[sourceId], ) } return registeredSourceIds } ``` The default sources are: - `ly.img.sticker` — Stickers. - `ly.img.vector.shape` — Shapes and arrows. - `ly.img.filter` — LUT and duotone color filters. - `ly.img.color.palette` — Default color palette. - `ly.img.effect` — Effects. - `ly.img.blur` — Blurs. - `ly.img.typeface` — Typefaces. - `ly.img.crop.presets` — Crop presets. - `ly.img.page.presets` — Page resize presets. - `ly.img.text`, `ly.img.text.styles`, `ly.img.text.curves` — Text presets. - `ly.img.text.components` — Text design component library. - `ly.img.caption.presets` — Caption style presets for video captions. ## Register Sample Content Sources The archive also ships sample content for development and prototyping. Register these sources the same way when you want the editor to show the bundled demo images, videos, audio, or templates; replace them with your own content sources in production. ```kotlin highlight-android-sample-source-ids private val serveAssetsSampleSourceIds = listOf( "ly.img.image", "ly.img.video", "ly.img.audio", "ly.img.templates", "ly.img.templates.premium", ) ``` The sample content sources are: - `ly.img.image` — Sample images. - `ly.img.video` — Sample videos. - `ly.img.audio` — Sample audio. - `ly.img.templates` — Sample design templates. - `ly.img.templates.premium` — Premium sample design templates. ## Point the Base URI at Your Assets Set `baseUri` to the deployed or bundled inner `assets/` folder that contains the per-source directories, then register the sources from that location. For assets on your own server or CDN, use an absolute HTTPS URI that points at the deployed inner `assets/` folder: ```kotlin highlight-android-remote-base-uri return Uri.parse("https://cdn.your.custom.domain/assets") ``` For assets bundled with your app, copy the extracted `imgly-assets/assets/` folder into your app module's `src/main/assets/` directory. This produces paths such as `src/main/assets/assets/ly.img.sticker/content.json`, so the Android assets URI points at `file:///android_asset/assets`: ```kotlin highlight-android-app-assets-base-uri return Uri.parse("file:///android_asset/assets") ``` > **Note:** If you use the Android [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) or the `Editor` > composable, pass your self-hosted location through the `baseUri` parameter. > The editor sets the Engine `basePath` from that value before configuration > callbacks run, so your callback can register sources from > `editorContext.baseUri`. ## Customize Which Assets Load To register only part of a source, pass ID patterns to the `matcher` parameter when you register that source. Asset source IDs must be unique, so use this instead of registering the same source earlier without a matcher. Patterns support the `*` wildcard, and an asset is included when it matches any pattern. ```kotlin highlight-android-filtered-source registerServeAssetSources( engine = engine, baseUri = baseUri, sourceIds = listOf("ly.img.sticker"), matchersBySourceId = mapOf( "ly.img.sticker" to listOf("ly.img.sticker.emoji.*"), ), ) ``` For deeper curation, edit the `content.json` manifests before you deploy them to your server or app assets. ## Configure Engine-Level Assets The engine also loads font fallback files for Unicode coverage and the emoji font separately from asset sources. Point the `basePath` setting at the same location so those files load from your self-hosted assets: ```kotlin highlight-android-engine-level-assets engine.editor.setSettingString( keypath = "basePath", value = baseUri.toString(), ) ``` This setting affects: - **Font fallback files** — Used when text contains characters not covered by the selected font. Android currently loads fallback files from `{basePath}/fonts/font-{index}.ttf` only when `basePath` is an `http` or `https` URI. For `file://` and Android app-asset URIs, fallback font files still load from `https://cdn.img.ly/assets/v4`. - **Emoji font** — The default emoji font (`NotoColorEmoji.ttf`). Located at `{basePath}/emoji/NotoColorEmoji.ttf`; this resolves from local, app-asset, or remote base paths. Both directories are included in the asset archive. Deploy the extracted `fonts/` and `emoji/` directories with your remote asset base URI. For bundled app assets, include `emoji/` for local emoji loading, but keep in mind that fallback font files still use the IMG.LY CDN fallback today. ## API Reference | Method | Description | | --- | --- | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Register an asset source by loading its `content.json` manifest from a URI. Pass `matcher` ID patterns to filter which assets load. | | `engine.asset.findAllSources()` | Check which asset source IDs are already registered. | | `engine.editor.setSettingString(keypath="basePath", value=_)` | Set the base URI used for font fallback files, emoji assets, animation thumbnails, and other relative resource paths. | | `engine.editor.getSettingString(keypath="basePath")` | Read the current Engine base path. | ## Next Steps - [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) — How asset sources and assets fit together. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/android/insert-media/shapes-or-stickers-20ac68/) — Add shapes and stickers to your designs using CE.SDK. Create rectangles, ellipses, stars, polygons, lines, and custom vector paths programmatically or through the built-in 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: "Settings" description: "Explore all configurable editor settings and learn how to read, update, and observe them via the Settings API." platform: android url: "https://img.ly/docs/cesdk/android/settings-970c98/" --- > 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/) > [Settings](https://img.ly/docs/cesdk/android/settings-970c98/) --- ```kotlin file=@cesdk_android_examples/engine-guides-settings/Settings.kt reference-only import android.util.Log import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withTimeout import ly.img.engine.Color import ly.img.engine.Engine import ly.img.engine.GlobalScope import ly.img.engine.PropertyType private const val TAG = "SettingsGuide" suspend fun settings( engine: Engine, scope: CoroutineScope, afterSettingChange: suspend () -> Unit = {}, ) { val previousCropEnabled = engine.editor.getSettingBoolean("doubleClickToCropEnabled") val previousMaxPreviewResolution = engine.editor.getSettingInt("maxPreviewResolution") val previousSnappingThreshold = engine.editor.getSettingFloat("positionSnappingThreshold") val previousTitleSeparator = engine.editor.getSettingString("page/title/separator") val previousHighlightColor = engine.editor.getSettingColor("highlightColor") val previousSelectionMode = engine.editor.getSettingEnum("doubleClickSelectionMode") val previousRole = engine.editor.getRole() val previousMoveScope = engine.editor.getGlobalScope(key = "layer/move") val allSettings = engine.editor.findAllSettings() val selectionModeType = engine.editor.getSettingType("doubleClickSelectionMode") val selectionModeOptions = engine.editor.getSettingEnumOptions("doubleClickSelectionMode") check("doubleClickSelectionMode" in allSettings) check(selectionModeType == PropertyType.ENUM) check("Direct" in selectionModeOptions) check("Hierarchical" in selectionModeOptions) engine.editor.setSettingBoolean("doubleClickToCropEnabled", value = false) val cropEnabled = engine.editor.getSettingBoolean("doubleClickToCropEnabled") engine.editor.setSettingInt("maxPreviewResolution", value = 2048) val maxPreviewResolution = engine.editor.getSettingInt("maxPreviewResolution") engine.editor.setSettingFloat("positionSnappingThreshold", value = 2F) val snappingThreshold = engine.editor.getSettingFloat("positionSnappingThreshold") engine.editor.setSettingString("page/title/separator", value = " | ") val titleSeparator = engine.editor.getSettingString("page/title/separator") val highlightColor = Color.fromRGBA(r = 1F, g = 0F, b = 1F, a = 1F) engine.editor.setSettingColor("highlightColor", value = highlightColor) val currentHighlightColor = engine.editor.getSettingColor("highlightColor") engine.editor.setSettingEnum("doubleClickSelectionMode", value = "Direct") val selectionMode = engine.editor.getSettingEnum("doubleClickSelectionMode") check(!cropEnabled) check(maxPreviewResolution == 2048) check(snappingThreshold == 2F) check(titleSeparator == " | ") check(currentHighlightColor == highlightColor) check(selectionMode == "Direct") val settingsChanged = CompletableDeferred() val settingsJob = engine.editor.onSettingsChanged() .onEach { Log.i(TAG, "Editor settings have changed") settingsChanged.complete(Unit) } .launchIn(scope) engine.editor.setSettingBoolean("doubleClickToCropEnabled", value = true) val observedCropEnabled = engine.editor.getSettingBoolean("doubleClickToCropEnabled") afterSettingChange() withTimeout(1_000) { settingsChanged.await() } settingsJob.cancel() check(observedCropEnabled) val currentRole = engine.editor.getRole() val roleJob = engine.editor.onRoleChanged() .onEach { role -> Log.i(TAG, "Role changed to $role") } .launchIn(scope) engine.editor.setRole("Adopter") val appliedRole = engine.editor.getRole() roleJob.cancel() check(currentRole == previousRole) check(appliedRole == "Adopter") val allScopes = engine.editor.findAllScopes() engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) val moveScope = engine.editor.getGlobalScope(key = "layer/move") check("layer/move" in allScopes) check(moveScope == GlobalScope.DEFER) engine.editor.setRole(previousRole) engine.editor.setGlobalScope(key = "layer/move", globalScope = previousMoveScope) engine.editor.setSettingBoolean("doubleClickToCropEnabled", previousCropEnabled) engine.editor.setSettingInt("maxPreviewResolution", previousMaxPreviewResolution) engine.editor.setSettingFloat("positionSnappingThreshold", previousSnappingThreshold) engine.editor.setSettingString("page/title/separator", previousTitleSeparator) engine.editor.setSettingColor("highlightColor", previousHighlightColor) engine.editor.setSettingEnum("doubleClickSelectionMode", previousSelectionMode) } ``` Explore all configurable editor settings and learn how to read, update, and observe them via the Settings API. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-settings) Settings are configuration values that control CE.SDK editor behavior without modifying scene content. They are accessed via key paths, such as `page/title/show`, and support `Bool`, `Int`, `Float`, `String`, `Color`, and `Enum` values. Use settings to customize visual appearance, interaction behavior, resource paths, feature toggles, and editor roles for your Android integration. ## When to Change Settings Settings can be changed after engine initialization, but they fall into two categories based on when they should be modified. ### Initialization-Only Settings Some settings should only be set once during or immediately after engine initialization. Changing them later may have no effect or can leave already-loaded resources unchanged: - `license` - The license key validates on startup; changing it later has no effect. - `basePath` - The base URL for resolving assets should be set before loading resources. - `defaultFontFileUri` / `defaultEmojiFontFileUri` - Default fonts should be configured before creating text. - `maxImageSize` - The image memory limit does not affect images that are already loaded. ### Runtime Settings Most settings can be changed at runtime and take effect immediately: - **Visual appearance**: `highlightColor`, `snappingGuideColor`, `cropOverlayColor`, and `page/title/color`. - **Interaction behavior**: `doubleClickToCropEnabled`, `doubleClickSelectionMode`, `touch/*`, and `mouse/*`. - **Control gizmos**: `controlGizmo/resizeHandlesVisibility`, `controlGizmo/rotateHandlesVisibility`, and related handle visibility settings. - **Page display**: `page/title/show`, `page/dimOutOfPageAreas`, and `page/title/separator`. - **Feature toggles and limits**: `blockAnimations/enabled`, `useSystemFontFallback`, `forceSystemEmojis`, and `maxPreviewResolution`. - **Snapping thresholds**: `positionSnappingThreshold` and `rotationSnappingThreshold`. These runtime settings are commonly used to adapt the editor UI to different modes, user preferences, or workflow states. ## Using the Settings API ### Discover Available Settings Use `findAllSettings()` to enumerate available key paths, `getSettingType()` to inspect a setting's value type, and `getSettingEnumOptions()` to list valid enum values before writing an enum setting. ```kotlin highlight-android-discover-settings val allSettings = engine.editor.findAllSettings() val selectionModeType = engine.editor.getSettingType("doubleClickSelectionMode") val selectionModeOptions = engine.editor.getSettingEnumOptions("doubleClickSelectionMode") ``` ### Read and Write Settings The Android binding exposes one getter and setter pair per setting type. Match the method to the type returned by `getSettingType()` before writing the value. ```kotlin highlight-android-read-write-settings engine.editor.setSettingBoolean("doubleClickToCropEnabled", value = false) val cropEnabled = engine.editor.getSettingBoolean("doubleClickToCropEnabled") engine.editor.setSettingInt("maxPreviewResolution", value = 2048) val maxPreviewResolution = engine.editor.getSettingInt("maxPreviewResolution") engine.editor.setSettingFloat("positionSnappingThreshold", value = 2F) val snappingThreshold = engine.editor.getSettingFloat("positionSnappingThreshold") engine.editor.setSettingString("page/title/separator", value = " | ") val titleSeparator = engine.editor.getSettingString("page/title/separator") val highlightColor = Color.fromRGBA(r = 1F, g = 0F, b = 1F, a = 1F) engine.editor.setSettingColor("highlightColor", value = highlightColor) val currentHighlightColor = engine.editor.getSettingColor("highlightColor") engine.editor.setSettingEnum("doubleClickSelectionMode", value = "Direct") val selectionMode = engine.editor.getSettingEnum("doubleClickSelectionMode") ``` ### Subscribe to Settings Changes `onSettingsChanged()` returns a `Flow` that emits whenever any editor setting changes. The snippet starts collection in `scope`, changes a setting while subscribed, and then cleans up. Use a lifecycle-owned coroutine scope from your app, such as a ViewModel or Compose scope, so collection stops when that owner is disposed. ```kotlin highlight-android-observe-settings val settingsChanged = CompletableDeferred() val settingsJob = engine.editor.onSettingsChanged() .onEach { Log.i(TAG, "Editor settings have changed") settingsChanged.complete(Unit) } .launchIn(scope) engine.editor.setSettingBoolean("doubleClickToCropEnabled", value = true) val observedCropEnabled = engine.editor.getSettingBoolean("doubleClickToCropEnabled") ``` After the change has been observed, stop collecting the flow. ```kotlin highlight-android-observe-settings-cleanup withTimeout(1_000) { settingsChanged.await() } settingsJob.cancel() ``` ### Role Management Roles apply predefined defaults for scopes and settings. CE.SDK includes `Creator`, `Adopter`, `Presenter`, and `Viewer` roles; use `getRole()`, `setRole()`, and `onRoleChanged()` to read, apply, and observe the active role from the same lifecycle-owned `scope`. ```kotlin highlight-android-role-management val currentRole = engine.editor.getRole() val roleJob = engine.editor.onRoleChanged() .onEach { role -> Log.i(TAG, "Role changed to $role") } .launchIn(scope) engine.editor.setRole("Adopter") val appliedRole = engine.editor.getRole() roleJob.cancel() ``` ### Global Scopes Global scopes allow, deny, or defer whole capability groups such as layer movement. Use `findAllScopes()` to discover scope keys, then use `setGlobalScope()` and `getGlobalScope()` with `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER`. ```kotlin highlight-android-global-scopes val allScopes = engine.editor.findAllScopes() engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) val moveScope = engine.editor.getGlobalScope(key = "layer/move") ``` ## Available Settings ## Settings Type Editor Settings This section describes the all available editor settings. | Property | Type | Default | Description | | -------------------------------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `archival/bundleOnlyUsedFontVariants` | `Bool` | `false` | When enabled, `saveSceneToArchive` and `saveBlocksToArchive` bundle only the font variants actually referenced by text blocks. When disabled (default), every variant of each referenced typeface is bundled so the loaded scene can freely switch fonts without re-fetching assets. | | `basePath` | `String` | `"some-base-path"` | The root directory for resolving relative paths and `bundle://` URIs (on platforms that don't offer bundles.). Also used as the base URL for loading font fallback files and the default emoji font (when self-hosting assets). | | `blockAnimations/enabled` | `Bool` | `true` | Whether animations should be enabled or not. | | `borderOutlineColor` | `Color` | `{"r":0,"g":0,"b":0,"a":1}` | The border outline color. | | `camera/clamping/overshootMode` | `Enum` | `"Reverse"` | Controls what happens when the clamp area is smaller than the viewport. Center: the clamp area is centered in the viewport. Reverse: the clamp area can move inside the viewport until it hits the edges., Possible values: `"Center"`, `"Reverse"` | | `clampThumbnailTextureSizes` | `Bool` | `true` | Whether to clamp thumbnail texture sizes to reduce memory usage. | | `clearColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | The color with which the render target is cleared before scenes get rendered. Only used while renderMode == RenderMode::Preview. | | `colorMaskingSettings/maskColor` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | The current mask color. Defaults to white, which disabled all masking. | | `controlGizmo/blockScaleDownLimit` | `Float` | `8` | Scale-down limit for blocks in screen pixels when scaling them with the gizmos or with touch gestures. The limit is ensured to be at least 0.1 to prevent scaling to size zero. | | `controlGizmo/dynamicMoveHandleVisibility` | `Bool` | `true` | Deprecated: prefer `controlGizmo/moveHandleVisibility`. Whether the move handle visibility is dynamic based on block size. When enabled (default), the move handle only appears when the block is small enough that resize handles would cover the interaction area. Setting it to false is equivalent to `moveHandleVisibility: 'always'`. | | `controlGizmo/moveHandleVisibility` | `Enum` | `"auto"` | Controls when the standalone move handle is shown for a selected block. `'auto'` (default) shows it only when the block is too small for the resize handles to be usable; `'always'` shows it regardless of block size and also while the block is in text edit (input) mode, so it can be repositioned while typing; `'never'` hides it. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showMoveHandles` and `controlGizmo/dynamicMoveHandleVisibility` booleans., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/resizeHandlesVisibility` | `Enum` | `"auto"` | Controls when the non-proportional edge (resize) handles are shown. `'auto'` (default) shows them in transform edits; `'always'` also shows them while the block is in text edit (input) mode; `'never'` hides them. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showResizeHandles` boolean., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/rotateHandlesVisibility` | `Enum` | `"auto"` | Controls when the rotation handle is shown. `'auto'` (default) shows it in transform edits; `'always'` also shows it while the block is in text edit (input) mode; `'never'` hides it. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showRotateHandles` boolean., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/scaleHandlesVisibility` | `Enum` | `"auto"` | Controls when the proportional corner (scale) handles are shown. `'auto'` (default) shows them in transform edits; `'always'` also shows them while the block is in text edit (input) mode; `'never'` hides them. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showScaleHandles` boolean., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/showCropHandles` | `Bool` | `true` | Whether or not to show the handles to adjust the crop area during crop mode. | | `controlGizmo/showCropScaleHandles` | `Bool` | `true` | Whether or not to display the outer handles that scale the full image during crop. | | `controlGizmo/showMoveHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/moveHandleVisibility`. Master on/off switch for the move handle; setting it to false is equivalent to `moveHandleVisibility: 'never'`. | | `controlGizmo/showResizeHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/resizeHandlesVisibility`. Whether or not to display the non-proportional resize handles (edge handles). Setting it to false is equivalent to `resizeHandlesVisibility: 'never'`. | | `controlGizmo/showRotateHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/rotateHandlesVisibility`. Whether or not to show the rotation handle. Setting it to false is equivalent to `rotateHandlesVisibility: 'never'`. | | `controlGizmo/showScaleHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/scaleHandlesVisibility`. Whether or not to display the proportional scale handles (corner handles). Setting it to false is equivalent to `scaleHandlesVisibility: 'never'`. | | `cropOverlayColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0.39}` | Color of the dimming overlay that's added in crop mode. | | `defaultEmojiFontFileUri` | `String` | `""` | URI of default font file for emojis. | | `defaultFontFileUri` | `String` | `"bundle://ly.img.cesdk/fonts/imgly_font_inter_semibold.otf"` | URI of default font file. This font file is the default everywhere unless overriden in specific settings. | | `doubleClickSelectionMode` | `Enum` | `"Hierarchical"` | The current mode of selection on double-click., Possible values: `"Direct"`, `"Hierarchical"` | | `doubleClickToCropEnabled` | `Bool` | `true` | Whether double clicking on an image element should switch into the crop editing mode. | | `dragToSwapFills/enabled` | `Bool` | `false` | Whether pressing and holding an image element lifts its image into a drag. Releasing it over another image element exchanges the two images. | | `dragToSwapFills/longPressDurationMs` | `Float` | `400` | How long the pointer must stay pressed and still before the image lifts, in milliseconds. 0 lifts on the first update, so every drag swaps instead of moves. | | `errorStateColor` | `Color` | `{"r":1,"g":1,"b":1,"a":0.7}` | The error state color for design blocks. | | `fallbackCMYKProfileUri` | `String` | `""` | The URI of the ICC profile that previews CMYK colors when the document defines no CMYK profile. Empty means the bundled default profile. | | `fallbackFontUri` | `String` | `""` | The URI of the fallback font to use for text that is missing certain characters. | | `forceSystemEmojis` | `Bool` | `true` | Whether the system emojis should be used for text. | | `grid/color` | `Color` | `{"r":0.518,"g":0.518,"b":0.518,"a":0.3}` | Color of the grid lines. | | `grid/enabled` | `Bool` | `false` | Whether the background grid is shown on pages. | | `grid/snapEnabled` | `Bool` | `false` | Whether elements should snap to grid lines when dragged. | | `grid/spacingX` | `Float` | `10` | Horizontal spacing between vertical grid lines in design units. | | `grid/spacingY` | `Float` | `10` | Vertical spacing between horizontal grid lines in design units. | | `handleFillColor` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | The fill color for handles. | | `highlightColor` | `Color` | `{"r":0.2,"g":0.333,"b":1,"a":1}` | Color of the selection, hover, and group frames and for the handle outlines for non-placeholder elements. | | `license` | `String` | `""` | A valid license string in JWT format. | | `listIndentPerLevel` | `Float` | `0.75` | Width of each list indentation level in EM units. | | `maxImageSize` | `Int` | `4096` | The maximum size at which images are loaded into the engine. Images that exceed this size are down-scaled prior to rendering. Reducing this size further reduces the memory footprint. | | `maxPreviewResolution` | `Int` | `-1` | The maximum dimension (width or height) in physical pixels for preview rendering. When greater than 0, the scene is rendered at reduced resolution and upscaled for improved performance. Does not affect exports. Set to -1 to disable (default). | | `mouse/enableScroll` | `Bool` | `true` | Whether the engine processes mouse scroll events. | | `mouse/enableZoom` | `Bool` | `true` | Whether the engine processes mouse zoom events. | | `page/allowCropInteraction` | `Bool` | `true` | If crop interaction (by handles and gestures) should be possible when the enabled arrangements allow resizing. | | `page/allowMoveInteraction` | `Bool` | `false` | If move interaction (by handles and gestures) should be possible when the enabled arrangements allow moving and if the page layout is not controlled by the scene. | | `page/allowResizeInteraction` | `Bool` | `false` | If a resize interaction (by handles and gestures) should be possible when the enabled arrangements allow resizing. | | `page/allowRotateInteraction` | `Bool` | `false` | If rotation interaction (by handles and gestures) should be possible when the enabled arrangements allow rotation and if the page layout is not controlled by the scene. | | `page/allowShapeChange` | `Bool` | `false` | Whether pages support non-rectangular shapes. When false, `supportsShape` returns false for pages. | | `page/dimOutOfPageAreas` | `Bool` | `true` | Whether the opacity of the region outside of all pages should be reduced. | | `page/exclusionAreaFillColor` | `Color` | `{"r":0.79,"g":0.12,"b":0.4,"a":0.35}` | Color washed over an exclusion area. An exclusion area is an editor guide and is left out of an export by default. | | `page/exclusionAreaFrameColor` | `Color` | `{"r":0.79,"g":0.12,"b":0.4,"a":0.15}` | Color of the frame around an exclusion area. A fully transparent color draws no frame. | | `page/flipDimensionsOn90DegreeCropRotation` | `Bool` | `false` | Whether rotating the crop by 90 degrees should swap the block's width and height, causing the page aspect ratio to rotate with the content. Defaults to false. | | `page/highlightDropTarget` | `Bool` | `false` | Whether to highlight the page under a dragged element as a drop target. | | `page/highlightWhenCropping` | `Bool` | `false` | Whether highlighting should be automatically enabled on the current page when entering crop mode. | | `page/innerBorderColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | Color of the inner frame around the page. | | `page/marginFillColor` | `Color` | `{"r":0.79,"g":0.12,"b":0.4,"a":0.1}` | Color filled into the bleed margins of pages. | | `page/marginFrameColor` | `Color` | `{"r":0.79,"g":0.12,"b":0.4,"a":0.15}` | Color of frame around the bleed margin area of the pages. | | `page/marqueeSelectOnBodyDrag` | `Bool` | `false` | When enabled, a click+drag that starts on the page body performs a marquee selection of the blocks inside the page instead of moving the page. The page can still be moved by dragging its title (when visible in free layout) or by holding the command key (macOS) / control key (Windows/Linux) while clicking and dragging on the page body. Has no effect when the page is not movable (see `page/allowMoveInteraction` and scene layout constraints). | | `page/moveChildrenWhenCroppingFill` | `Bool` | `false` | Whether the children of the page should be transformed to match their old position relative to the page fill when a page fill is cropped. | | `page/outerBorderColor` | `Color` | `{"r":1,"g":1,"b":1,"a":0}` | Color of the outer frame around the page. | | `page/reparentBlocksToSceneWhenOutOfPage` | `Bool` | `false` | Whether blocks should be reparented to the scene when dragged outside all pages, and reparented back to a page when dragged over one. | | `page/restrictPageSelectionToBorderAndTitle` | `Bool` | `false` | When enabled, the page can only be selected by clicking on its title (when shown in free layout) or near its border. Clicks inside the page body no longer select the page; the click falls through to whatever block sits underneath. Independent of `page/marqueeSelectOnBodyDrag`. | | `page/restrictResizeInteractionToFixedAspectRatio` | `Bool` | `false` | If the resize interaction should be restricted to fixed aspect ratio resizing. | | `page/safetyFillColor` | `Color` | `{"r":0.13,"g":0.45,"b":0.85,"a":0.08}` | Color filled into the safety inset band of pages. | | `page/safetyFrameColor` | `Color` | `{"r":0.13,"g":0.45,"b":0.85,"a":0.35}` | Color of the frame around the safety inset of the pages. | | `page/safetyRevealDuringTransform` | `Bool` | `true` | Whether the safety inset appears only while a drag, a nudge or a keyboard resize is in it or comes near it, with its frame, its wash and its snapping. Set to false to draw it on every page all the time. | | `page/selectWhenNoBlocksSelected` | `Bool` | `false` | Whether the page should automatically be selected when no blocks are selected. | | `page/title/appendPageName` | `Bool` | `true` | Whether to append the page name to the title if a page name is set even if the name is not specified in the template or the template is not shown. | | `page/title/canEdit` | `Bool` | `false` | Whether double-clicking a page title enters text edit mode to rename the page. | | `page/title/color` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | Color of page titles visible in preview mode, can change with different themes. | | `page/title/fontFileUri` | `String` | `"bundle://ly.img.cesdk/fonts/imgly_font_inter_semibold.otf"` | Font of page titles. | | `page/title/separator` | `String` | `"-"` | Title label separator between the page number and the page name. | | `page/title/show` | `Bool` | `true` | Whether to show titles above each page. | | `page/title/showOnSinglePage` | `Bool` | `true` | Whether to hide the the page title when only a single page is given. | | `page/title/showPageTitleTemplate` | `Bool` | `true` | Whether to include the default page title from `page.titleTemplate`. | | `pageHighlightColor` | `Color` | `{"r":0.5,"g":0.5,"b":0.5,"a":0.2}` | Color of the outline of each page. | | `placeholderControls/showButton` | `Bool` | `true` | Show the placeholder button. | | `placeholderControls/showOverlay` | `Bool` | `true` | Show the overlay pattern. | | `placeholderHighlightColor` | `Color` | `{"r":0.77,"g":0.06,"b":0.95,"a":1}` | Color of the selection, hover, and group frames and for the handle outlines for placeholder elements. | | `playback/showAllBlocks` | `Bool` | `false` | When enabled, every block stays visible regardless of the current playback time, instead of being culled outside its time offset/duration. No effect on export. | | `positionSnappingThreshold` | `Float` | `4` | Position snapping threshold in screen space. | | `progressColor` | `Color` | `{"r":1,"g":1,"b":1,"a":0.7}` | The progress indicator color. | | `rotationSnappingGuideColor` | `Color` | `{"r":1,"g":0.004,"b":0.361,"a":1}` | Color of the rotation snapping guides. | | `rotationSnappingThreshold` | `Float` | `0.15` | Rotation snapping threshold in radians. | | `showBuildVersion` | `Bool` | `false` | Show the build version on the canvas. | | `snappingGuideColor` | `Color` | `{"r":1,"g":0.004,"b":0.361,"a":1}` | Color of the position snapping guides. | | `textVariableHighlightColor` | `Color` | `{"r":0.7,"g":0,"b":0.7,"a":1}` | Color of the text variable highlighting borders. | | `touch/dragStartCanSelect` | `Bool` | `true` | Whether dragging an element requires selecting it first. When not set, elements can be directly dragged. | | `touch/pinchAction` | `Enum` | `"Scale"` | The action to perform when a pinch gesture is performed., Possible values: `"None"`, `"Zoom"`, `"Scale"`, `"Auto"`, `"Dynamic"` | | `touch/rotateAction` | `Enum` | `"Rotate"` | Whether or not the two finger turn gesture can rotate selected elements., Possible values: `"None"`, `"Rotate"` | | `touch/singlePointPanning` | `Bool` | `true` | Whether or not dragging on the canvas should move the camera (scrolling). When not set, the scroll bars have to be used. | | `upload/supportedMimeTypes` | `String` | `""` | The MIME types supported for file uploads. | | `useSystemFontFallback` | `Bool` | `true` | Whether the IMG.LY hosted font fallback is used for fonts that are missing certain characters, covering most of the unicode range. | ### GlobalScopes | Member | Type | Default | Description | | ---------- | ------- | ------- | -------------------------------- | | text | `Scope` | `Allow` | Scope for text operations. | | fill | `Scope` | `Allow` | Scope for fill operations. | | stroke | `Scope` | `Allow` | Scope for stroke operations. | | shape | `Scope` | `Allow` | Scope for shape operations. | | layer | `Scope` | `Allow` | Scope for layer operations. | | appearance | `Scope` | `Allow` | Scope for appearance operations. | | lifecycle | `Scope` | `Allow` | Scope for lifecycle operations. | | editor | `Scope` | `Allow` | Scope for editor operations. | ## API Reference | Method | Purpose | | ----------------------------------------------------- | --------------------------------------------------------------- | | `engine.editor.findAllSettings()` | Get all available setting key paths. | | `engine.editor.getSettingType(keypath=_)` | Read the `PropertyType` for a setting key path. | | `engine.editor.getSettingEnumOptions(keypath=_)` | Get the valid values for an enum setting. | | `engine.editor.setSettingBoolean(keypath=_, value=_)` | Write a boolean setting. | | `engine.editor.getSettingBoolean(keypath=_)` | Read a boolean setting. | | `engine.editor.setSettingInt(keypath=_, value=_)` | Write an integer setting. | | `engine.editor.getSettingInt(keypath=_)` | Read an integer setting. | | `engine.editor.setSettingFloat(keypath=_, value=_)` | Write a float setting. | | `engine.editor.getSettingFloat(keypath=_)` | Read a float setting. | | `engine.editor.setSettingString(keypath=_, value=_)` | Write a string setting. | | `engine.editor.getSettingString(keypath=_)` | Read a string setting. | | `engine.editor.setSettingColor(keypath=_, value=_)` | Write a color setting as `RGBAColor`. | | `engine.editor.getSettingColor(keypath=_)` | Read a color setting as `RGBAColor`. | | `engine.editor.setSettingEnum(keypath=_, value=_)` | Write an enum setting using one of its supported string values. | | `engine.editor.getSettingEnum(keypath=_)` | Read an enum setting as a string value. | | `engine.editor.onSettingsChanged()` | Observe editor setting changes as a `Flow`. | | `engine.editor.getRole()` | Read the active editor role. | | `engine.editor.setRole(role=_)` | Apply a role and its role-dependent defaults. | | `engine.editor.onRoleChanged()` | Observe role changes as a `Flow`. | | `engine.editor.findAllScopes()` | Get all available global scope keys. | | `engine.editor.setGlobalScope(key=_, globalScope=_)` | Allow, deny, or defer a global scope. | | `engine.editor.getGlobalScope(key=_)` | Read the current `GlobalScope` value for a scope key. | ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) — Pass init-time values such as `license`, `userID`, and `baseURL` through `EngineSettings`. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) — Host the assets that `basePath` resolves against and register default asset sources on your own infrastructure. --- ## 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 and Edit Shapes" description: "Draw custom vector shapes, combine them with boolean operations, and insert QR codes into your designs." platform: android url: "https://img.ly/docs/cesdk/android/shapes-9f1b2c/" --- > 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 Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) --- --- ## Related Pages - [Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/) - Draw custom vector shapes and insert them into your design canvas. - [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 with CreativeEngine. - [Combine Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26/) - Combine multiple shapes using boolean operations to create custom compound 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: "Starter Kits" description: "Pre-configured editor solutions for common use cases. Launch your creative project with production-ready editor implementations." platform: android url: "https://img.ly/docs/cesdk/android/starterkits-kxg120/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) --- Launch your creative project with our pre-built, production-ready editor solutions. Each starter kit includes everything you need to integrate professional editing capabilities into your Android application. --- ## Related Pages - [Photo Editor in Android](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) - Add the Android Photo Editor starter kit to a new or existing app with a reusable starter-kit module. - [Design Editor in Android](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) - Add the Android Design Editor starter kit to a new or existing app with a reusable starter-kit module. - [Video Editor in Android](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) - Add the Android Video Editor starter kit to a new or existing app with a reusable starter-kit module. - [Memories in Android](https://img.ly/docs/cesdk/android/starterkits/memories-mmrs01/) - Add the Android Memories starter kit to a new or existing app with a reusable starter-kit module—turn photos and clips into a shareable memory montage. - [T-Shirt Designer in Android](https://img.ly/docs/cesdk/android/starterkits/t-shirt-designer-jwinqr/) - Add the Android Apparel Editor starter kit to a new or existing app with a reusable starter-kit module. - [Custom Built UIs](https://img.ly/docs/cesdk/android/starterkits/custom-built-uis-5a5cc4/) - Fully custom UIs built with React using the Engine API. Freely adapt to 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: "Custom Built UIs" description: "Fully custom UIs built with React using the Engine API. Freely adapt to your use case." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/custom-built-uis-5a5cc4/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [Custom Built UIs](https://img.ly/docs/cesdk/android/starterkits/custom-built-uis-5a5cc4/) --- Fully custom UIs built with React using the Engine API. Freely adapt to your use case. --- ## Related Pages - [Postcard Editor in Android](https://img.ly/docs/cesdk/android/starterkits/postcard-editor-03mijr/) - Add the Android Postcard Editor starter kit to a new or existing app with a reusable starter-kit module. --- ## 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 Editor in Android" description: "Add the Android Design Editor starter kit to a new or existing app with a reusable starter-kit module." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [Design Editor](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) --- ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/app/src/main/kotlin/ly/img/starterkit/example/ExampleBaseUri.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/app/src/main/kotlin/ly/img/starterkit/example/ExampleComposeNavigationScreen.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/app/src/main/kotlin/ly/img/starterkit/example/ExampleRenderTarget.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/app/src/main/kotlin/ly/img/starterkit/example/ExampleUIMode.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/starter-kit/src/main/kotlin/ly/img/editor/configuration/design/EditorActivity.kt reference-only package ly.img.editor.configuration.design import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnCreate.kt reference-only package ly.img.editor.configuration.design.callback import androidx.core.net.toUri import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.SystemGalleryAssetSource import ly.img.editor.core.library.data.SystemGalleryPermission import ly.img.engine.DesignBlockType /** * The callback that is invoked when the editor is created. */ suspend fun DesignConfigurationBuilder.onCreate( preCreateScene: suspend DesignConfigurationBuilder.() -> Unit = { onPreCreateScene() }, createScene: suspend DesignConfigurationBuilder.() -> Unit = { onCreateScene() }, loadAssetSources: suspend DesignConfigurationBuilder.() -> Unit = { onLoadAssetSources() }, postCreateScene: suspend DesignConfigurationBuilder.() -> Unit = { onPostCreateScene() }, finally: suspend DesignConfigurationBuilder.() -> Unit = { onCreateFinally() }, ) { try { preCreateScene() createScene() loadAssetSources() postCreateScene() } finally { finally() } } /** * The callback that is invoked before the scene is created. */ fun DesignConfigurationBuilder.onPreCreateScene() { showLoading = true // Enable horizontal sliding between pages editorContext.engine.editor.setSettingBoolean( keypath = "features/pageCarouselEnabled", value = true, ) } /** * The callback that is responsible for creating the scene. */ suspend fun DesignConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/design.scene".toUri()) } /** * The callback that loads all the required assets sources. */ suspend fun DesignConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } /** * The callback that is invoked right after [onCreateScene], after the scene is created. */ fun DesignConfigurationBuilder.onPostCreateScene() { editorContext.engine.block .findByType(DesignBlockType.Stack) .firstOrNull() ?.let { // Display all pages in a horizontal stack. editorContext.engine.block.setEnum( block = it, property = "stack/axis", value = "Horizontal", ) } } /** * The callback that is invoked as the last step of [onCreate]. * It always runs, no matter success or failure on previous steps. */ fun DesignConfigurationBuilder.onCreateFinally() { showLoading = false } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-design/starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnExport.kt reference-only package ly.img.editor.configuration.design.callback import kotlinx.coroutines.CancellationException import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.engine.MimeType import java.nio.ByteBuffer /** * The callback that is invoked when the export button is clicked. */ suspend fun DesignConfigurationBuilder.onExport( preExport: suspend DesignConfigurationBuilder.() -> Unit = { onPreExport() }, exportByteBuffer: suspend DesignConfigurationBuilder.() -> ByteBuffer = { onExportByteBuffer() }, postExport: suspend DesignConfigurationBuilder.(ByteBuffer) -> Unit = { onPostExport(it) }, error: suspend DesignConfigurationBuilder.(Exception) -> Unit = { onExportError(it) }, finally: suspend DesignConfigurationBuilder.() -> Unit = { onExportFinally() }, ) { try { preExport() val result = exportByteBuffer() postExport(result) } catch (exception: Exception) { error(exception) } finally { finally() } } /** * The callback that is invoked before the export is started. */ fun DesignConfigurationBuilder.onPreExport() { showLoading = true } /** * The callback that exports the content of the editor into [ByteBuffer]. */ suspend fun DesignConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) /** * The callback that is invoked after [onExportByteBuffer] and handles its output. */ suspend fun DesignConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF) shareFile(file = file, mimeType = MimeType.PDF) } /** * The callback that is invoked in case any of the export functions throw an exception. */ fun DesignConfigurationBuilder.onExportError(error: Exception) { if (error is CancellationException) { throw error } this.error = error } /** * The callback that is invoked as the last step of [onExportByteBuffer]. * It always runs, no matter success or failure on previous steps. */ fun DesignConfigurationBuilder.onExportFinally() { showLoading = false } ``` Professional design editing for your Android app—create graphics, templates, and multi-page documents. Runs entirely on the mobile device with no server dependencies. ![Design Editor starter kit screenshot](../../../../../../cesdk_android_showcases/starter-kits/starter-kit-design/screenshot.png) > **Reading time:** 10 minutes > > **Resources:** > > - [Download examples](https://github.com/imgly/starterkit-design-editor-android/archive/refs/heads/v$UBQ_VERSION$.zip) > > - [View source on GitHub](https://github.com/imgly/starterkit-design-editor-android/tree/v$UBQ_VERSION$) *** ## Pre-Requisites This guide assumes basic familiarity with Android and Kotlin. You will need: - Latest Android Studio - Kotlin: 1.9.10 or later - Gradle: 8.4 or later - Android: 7.0+ (API level 24+) ## Get Started Start with a complete, runnable Android starter kit project. ### Step 1: Clone the Repository ```bash git clone -b v$UBQ_VERSION$ https://github.com/imgly/starterkit-design-editor-android.git cd starterkit-design-editor-android ``` ### Step 2: Open and Run [Create and launch](https://developer.android.com/studio/run/managing-avds) a new android emulator or use an existing one or connect a physical device with `USB Debugging` on. Open the project in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` and run the `app` module from UI, or use: ```bash ./gradlew app:installDebug ``` The sample app launches `MainActivity` that has "Launch Editor" button. Clicking it launches `EditorActivity`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.design import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ## Get Started Integrate only the `starter-kit` library module into your existing Android app. ### Step 1: Run the Extraction Script From Your App Root Run this from your application root directory: ```bash repo="starterkit-design-editor-android" version="$UBQ_VERSION$" curl -0 "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit" ``` This extracts the `starter-kit/` library module into your android project. ### Step 2: Include the Module Declare the newly added android library module in your project: ```kotlin title = "settings.gradle.kts" include(":starter-kit") ``` ### Step 3: Add Dependency in Your App Module Include the starter kit module dependency in your app module: ```kotlin title = "app/build.gradle.kts" dependencies { implementation(project(":starter-kit")) } ``` ### Step 4: Include missing plugins Include plugins that may be missing in your project's root `build.gradle.kts` file: ```kotlin title = "build.gradle.kts" plugins { // Existing plugins here id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false } ``` ### Step 5: Add the IMG.LY Maven Repository Add `IMG.LY` maven repository path in your project: ```kotlin title = "settings.gradle.kts" 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") } } } } ``` If the project is open in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` in order to make all dependencies available. ### Step 6: Launch the Editor From Your UI If you use jetpack compose navigation in your app, simply add a new navigation destination and invoke the following composable: ```kotlin highlight-starter-kit-composable import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` > **Delete EditorActivity:** You can delete `starter-kit/src/main/kotlin/ly/img/editor/configuration/design/EditorActivity.kt` as it is needed only for legacy navigation. If you do not use jetpack compose navigation and use legacy android navigation, the starter kit has a special `EditorActivity` class with full encapsulated logic: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.design import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` Simply launch the activity from the activity of your app. Optionally, you can pass parameters to the editor: ```kotlin import android.content.Intent import ly.img.editor.configuration.design.EditorActivity fun launchEditor() { // "this" is the current activity val intent = Intent(this, EditorActivity::class.java).also { // Optionally pass parameters for EditorActivity to consume, i.e. your image/video/scene uri it.putExtra("my_param", "my_param_value") } startActivity(intent) } ``` In case you want to consume the parameter in `EditorActivity`: ```kotlin class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val param = intent.getStringExtra("my_param") ?: "default" ... } } ``` The full implementation of the starter kit lives in the `starter-kit/` folder: ```text starter-kit/ ├── build.gradle.kts # Starter kit library module config, includes ly.img:editor dependency └── src/main/ ├── AndroidManifest.xml # Starter kit manifest file, may contain permissions ├── assets/ │ └── scene/ │ └── design.scene # Default design scene that should be loaded └── kotlin/ly/img/editor/configuration/design/ ├── DesignConfigurationBuilder.kt # Editor configuration logic encapsulated in 1 place ├── EditorActivity.kt # Encapsulated editor for legacy navigation. Delete if you use jetpack compose navigation ├── callback/ │ ├── OnCreate.kt # Editor initialization logic │ ├── OnExport.kt # Export flow and handling │ └── OnLoaded.kt # Post onCreate logic └── component/ ├── CanvasMenu.kt # Canvas Menu component configuration ├── Dock.kt # Dock component configuration ├── InspectorBar.kt # Inspector Bar component configuration ├── NavigationBar.kt # Navigation Bar component configuration └── Overlay.kt # Overlay component configuration ``` ## Starter Kit as a Dynamic Feature Since `starter-kit` folder is an android library module, it is possible to turn it into a [dynamic feature](https://developer.android.com/guide/playcore/feature-delivery). This can be helpful if you want to lazy load the editor in order to reduce the download size of your app. See [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) for more details. ## Configuring the Starter Kit The starter kit that we provide contains a very generic structure and behavior, however we understand that every customer wants to configure it according to their needs. The good thing is that the starter kit implementation is part of your codebase and you can configure, add/remove/modify functionality as you wish. In addition, you may want to configure the editor based on your business logic, i.e. restore the scene file from previous edits, display different dock items for different users etc. This example demonstrates on how to pass, store and use external parameters in the starter kit. First, declare a new property to the builder class: ```kotlin ... import android.net.Uri @Stable class DesignConfigurationBuilder : BasicConfigurationBuilder() { /** * The scene uri that should be loaded in onCreate if not null. * Note that editorContext.mutableStateOf is used to store mutable objects in the editor scope that survive configuration changes. */ var sceneUri: Uri? by editorContext.mutableStateOf( key = "your.package.name.state.sceneUri", initial = null, ) ... } ``` Next, read your external parameter from activity intent extras (or jetpack compose screen arguments) and assign to the property of the builder: ```kotlin import androidx.core.net.toUri class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val sceneUri = intent.getStringExtra("sceneUri")?.toUri() Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) { sceneUri = sceneUri } }, onClose = { // Finish the activity, ignore any errors. finish() }, ) } } ``` Finally, make use of the property. The scene loading logic is located at `OnCreate.kt` file, as part of `onCreate` implementation (see next section for more details). We modify this function to load the `sceneUri` instead if the value is not null: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnCreate.kt" suspend fun DesignConfigurationBuilder.onCreateScene() { sceneUri?.let { safeSceneUri -> // Load the sceneUri if it's not null getOrLoadScene(sceneUri = safeSceneUri) } ?: run { // Otherwise stick to the default behavior of the starter kit getOrLoadScene(sceneUri = "file:///android_asset/scene/design.scene".toUri()) } } ``` ## Set Up a Scene The scene setup logic is located at `OnCreate.kt` file, as part of `onCreate` implementation: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnCreate.kt" highlight-starter-kit-on-create-scene suspend fun DesignConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/design.scene".toUri()) } ``` `getOrLoadScene` is a helper that loads an existing scene if available, or initializes one from `file:///android_asset/scene/design.scene` when no scene is active. CE.SDK offers multiple ways to load scene into the editor. Choose the method that matches your use case: ```kotlin // Load from an image uri - creates a new scene with the image editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri()) // Load from a template archive - restores a previously saved project editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri()) // Create a blank canvas - starts with an empty design scene editorContext.engine.scene.create() // Load from a scene file - restores a scene from .scene file editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri()) ``` > **More Loading Options:** See [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) for all available loading methods. ## Customize Assets The asset source setup is located in `OnCreate.kt` as part of `onCreate` implementation. Enable or disable individual sources: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnCreate.kt" highlight-starter-kit-on-load-asset-sources suspend fun DesignConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } ``` > **More Asses Sources:** See [Import Media](https://img.ly/docs/cesdk/android/import-media-4e3703/) for all available assets and loading mechanisms. For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for downloading assets, configuring `baseUri` and excluding unused sources to optimize load times. ## Customize Export Functionality Export handling logic is located in `OnExport.kt` as part of `onExport` callback. `onExportByteBuffer` controls what should be exported from the scene. It can be a scene or set of design blocks. For design editor, it makes sense to export the scene to a PDF content. `export` is a helper function that calls `editorContext.engine.block.export` under the hood: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnExport.kt" highlight-starter-kit-on-export-byte-buffer suspend fun DesignConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) ``` > **More Export Options:** See [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) guides for all available export and scene calls. `onPostExport` controls what should happen to the exported `ByteBuffer` content. You can upload the result to your server, save it to the device gallery or simply close the editor via `editorContext.eventHandler.send(EditorEvent.CloseEditor())`. Check `writeToFile`, `shareFile` and `shareUri` helper functions for potential implementations: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/design/callback/OnExport.kt" highlight-starter-kit-on-post-export suspend fun DesignConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF) shareFile(file = file, mimeType = MimeType.PDF) } ``` *** ## Customize (Optional) ### Base Uri The starter kit does not make any `baseUri` configuration, which means it points to `https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/assets`. If you want to store them in your own CDN or locally, assets can be accessed via [zip file](https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/imgly-assets.zip). For example, if you want to store them locally, unzip the content and place at `starter-kit/src/main/assets`: ```kotlin highlight-starter-kit-base-uri import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` ### UI Mode CE.SDK supports light and dark ui modes out of the box, plus automatic system preference detection. Switch between themes programmatically: ```kotlin highlight-starter-kit-ui-mode import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` See [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) for more details. ### Native Android Canvas CE.SDK supports rendering on two of the most popular native android views that allow GPU rendering: [TextureView](https://developer.android.com/reference/android/view/TextureView) and [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView): ```kotlin highlight-starter-kit-engine-render-target import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) } ``` ### Localization See [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) for supported languages, adding support to new languages and replacing existing keys. ### UI Layout All the configurable components are located at `starter-kit/src/main/kotlin/ly/img/editor/configuration/design/component`: - `CanvasMenu.kt` - see [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) for full configuration options. - `Dock.kt` - see [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) for full configuration options. - `InspectorBar.kt` - see [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) for full configuration options. - `NavigationBar.kt` - see [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) for full configuration options. - `Overlay.kt` - see [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) for full configuration options. *** ## Troubleshooting > **Free Trial:** [Sign up for a free trial](https://img.ly/forms/free-trial) to get a license key and remove the watermark. ### Editor doesn't load - **Check onCreate**: Ensure `onCreate` callback loads a scene and no coroutine is stuck infinitly - **Verify the baseURL**: Assets must be accessible from the CDN or your self-hosted location - **Check logcat errors**: Look for error in Android logcat ### Assets don't appear - **Check network requests**: Make sure the device/emulator is connected to the internet - **Self-host assets for production**: See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) to host assets on your infrastructure - **Check logcat errors**: Look for error in Android logcat ### Export fails or produces blank images - **Wait for content to load**: Ensure images are fully loaded before exporting - **Check logcat errors**: Look for error in Android logcat ### Watermark appears in production - **Add your license key**: Set the `license` property in your configuration - **Sign up for a trial**: Get a free trial license at [img.ly/forms/free-trial](https://img.ly/forms/free-trial) *** ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) – Complete list of initialization options - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) – Self-host engine assets for production - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) – Customize colors and appearance - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) – Add translations and language support --- ## 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 UI Configurations" description: "Choose one of our preconfigured Editor UIs to edit designs, videos, or photos." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/editor-ui-configurations-341d80/" --- > 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). --- Choose one of our preconfigured Editor UIs to edit designs, videos, or photos. --- ## 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: "Memories in Android" description: "Add the Android Memories starter kit to a new or existing app with a reusable starter-kit module—turn photos and clips into a shareable memory montage." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/memories-mmrs01/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [Memories](https://img.ly/docs/cesdk/android/starterkits/memories-mmrs01/) --- ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/app/src/main/kotlin/ly/img/starterkit/MainActivity.kt reference-only package ly.img.starterkit import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.viewmodel.compose.viewModel import ly.img.editor.configuration.memories.MemoriesApp import ly.img.editor.configuration.memories.MemoriesViewModel import ly.img.editor.core.theme.EditorTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() // Enable immersive mode compatibility WindowCompat.setDecorFitsSystemWindows(window, false) setContent { val viewModel: MemoriesViewModel = viewModel() val isFullscreen by viewModel.isFullscreen.collectAsState() // Handle fullscreen state changes LaunchedEffect(isFullscreen) { val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) if (isFullscreen) { // Hide system UI (status bar and navigation bar) windowInsetsController.hide( WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars(), ) windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } else { // Show system UI windowInsetsController.show( WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars(), ) } } EditorTheme { Surface { MemoriesApp( license = null, // pass your license, or null for evaluation mode (watermark) onExit = { finish() }, viewModel = viewModel, ) } } } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/starter-kit/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesConfiguration.kt reference-only package ly.img.editor.configuration.memories import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import ly.img.editor.BasicConfigurationBuilder import ly.img.editor.configuration.memories.callback.onCreateConfiguration import ly.img.editor.configuration.memories.callback.onExport import ly.img.editor.configuration.memories.component.bottomPanelConfiguration import ly.img.editor.configuration.memories.component.dockConfiguration import ly.img.editor.configuration.memories.component.navigationBarConfiguration import ly.img.editor.configuration.memories.component.rememberOverlay import ly.img.editor.configuration.memories.model.ExportStatus class MemoriesConfiguration( private val viewModel: MemoriesViewModel, ) : BasicConfigurationBuilder() { /** Drives the export progress overlay; null when no export is running. */ var exportStatus: ExportStatus? by editorContext.mutableStateOf(key = KEY_EXPORT_STATUS, initial = null) init { onCreate = onCreateConfiguration(viewModel) dock = dockConfiguration(viewModel) bottomPanel = bottomPanelConfiguration(viewModel) navigationBar = navigationBarConfiguration(viewModel) overlay = { rememberOverlay(viewModel) } onExport = { onExport() } // If onCreate (or any editor step) fails, never leave the loading overlay stuck: dismiss it // and surface the failure through the standard error dialog (BasicConfigurationBuilder.Overlay). onError = { viewModel.setEditorLoading(false) error = it } } private companion object { const val KEY_EXPORT_STATUS = "ly.img.editor.configuration.memories.exportStatus" } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/starter-kit/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesApp.kt reference-only package ly.img.editor.configuration.memories import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel import ly.img.editor.configuration.memories.screen.ImageSelectionScreen enum class AppScreen { ImageSelection, VideoEditor, } /** * The complete Memories experience: photo picker (device images and videos) → * loading/analysis → cinematic slideshow editor. * * This is the single entry point used by every host — the standalone app, the examples app, and * the showcases app — so the flow is identical everywhere. * * @param license CE.SDK license key, or null for evaluation mode (adds a watermark). * @param onExit invoked when the user navigates back from the picker (the root of the flow) — * e.g. finish the activity (standalone) or pop the back stack (demo apps). */ @Composable fun MemoriesApp( license: String?, onExit: () -> Unit, modifier: Modifier = Modifier, viewModel: MemoriesViewModel = viewModel(), ) { val context = LocalContext.current var currentScreen by rememberSaveable { mutableStateOf(AppScreen.ImageSelection) } LaunchedEffect(viewModel) { viewModel.setContext(context) // After process death the saved screen can restore to the editor while the ViewModel is // recreated empty — don't land in the editor with no media; return to the picker instead. if (currentScreen == AppScreen.VideoEditor && viewModel.selectedImages.value.isEmpty()) { currentScreen = AppScreen.ImageSelection } } when (currentScreen) { AppScreen.ImageSelection -> { // Back from the picker leaves the flow. ImageSelectionScreen registers its own // (higher-priority) BackHandler for multi-select mode, so this only fires otherwise. BackHandler { onExit() } Scaffold { paddingValues -> ImageSelectionScreen( viewModel = viewModel, onProceedToEditor = { viewModel.setEditorLoading(true) currentScreen = AppScreen.VideoEditor }, modifier = modifier.padding(paddingValues), ) } } AppScreen.VideoEditor -> { MemoriesEditor( license = license, viewModel = viewModel, onCloseEditor = { currentScreen = AppScreen.ImageSelection }, modifier = modifier, ) } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/starter-kit/src/main/kotlin/ly/img/editor/configuration/memories/scene/SceneSetup.kt reference-only package ly.img.editor.configuration.memories.scene import ly.img.editor.configuration.memories.util.PAGE_HEIGHT import ly.img.editor.configuration.memories.util.PAGE_WIDTH 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 /** Build the slideshow scene in code (no serialized blob): a video scene with one sized page. */ internal fun createSlideshowScene(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, PAGE_WIDTH) engine.block.setHeight(page, PAGE_HEIGHT) val fill = engine.block.createFill(FillType.Color) engine.block.setColor(fill, "fill/color/value", Color.fromRGBA(0f, 0f, 0f, 1f)) engine.block.setFill(page, fill) return page } /** * The persistent, single-purpose tracks of the slideshow, bottom-to-top in render order. The per- * slide media tracks are created later (in [createMainImageSequence]) and stack on top of these. */ internal data class TrackReferences( val textTrack: DesignBlock, val backgroundTrack: DesignBlock, val backgroundBlock: DesignBlock, val matteTrack: DesignBlock, val matteBlock: DesignBlock, ) internal fun setupTracks( engine: Engine, page: DesignBlock, ): TrackReferences { // background = per-style backdrop (hidden by default); matte = black rectangle so a crossfade // fades to black instead of revealing the backdrop. Appended first → they render behind the // media. The text (title) track sits just above them; the media tracks are appended on top later. val backgroundTrack = engine.block.create(DesignBlockType.Track) val matteTrack = engine.block.create(DesignBlockType.Track) val textTrack = engine.block.create(DesignBlockType.Track) // Tags let the rest of the kit re-locate tracks by role instead of retaining stale block ids. tagSlideshowTracks(engine, textTrack = textTrack, backgroundTrack = backgroundTrack, matteTrack = matteTrack) engine.block.appendChild(parent = page, child = backgroundTrack) engine.block.appendChild(parent = page, child = matteTrack) engine.block.appendChild(parent = page, child = textTrack) val backgroundBlock = fullPageBlock(engine, page) engine.block.setVisible(backgroundBlock, false) engine.block.appendChild(parent = backgroundTrack, child = backgroundBlock) val matteBlock = fullPageBlock(engine, page) val matteFill = engine.block.createFill(FillType.Color) engine.block.setColor(matteFill, "fill/color/value", Color.fromRGBA(0f, 0f, 0f, 1f)) engine.block.setFill(matteBlock, matteFill) engine.block.appendChild(parent = matteTrack, child = matteBlock) return TrackReferences(textTrack, backgroundTrack, backgroundBlock, matteTrack, matteBlock) } private fun fullPageBlock( engine: Engine, page: DesignBlock, ): DesignBlock { val block = engine.block.create(DesignBlockType.Graphic) engine.block.setScopeEnabled(block, "editor/select", false) engine.block.setShape(block, engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block, engine.block.getWidth(page)) engine.block.setHeight(block, engine.block.getHeight(page)) return block } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/starter-kit/src/main/kotlin/ly/img/editor/configuration/memories/style/VideoStyle.kt reference-only package ly.img.editor.configuration.memories.style /** * Catalog of the video styles offered in the Styles sheet. * * This is the single source of truth for a style: its name, typeface, preferred font * weights, the image-filter adjustments, the scene background and media scale, and the title * text color all live here. To add, remove, or tune a style, edit [VideoStyles.ALL] below — * nothing else needs to change. The generic application logic lives in [StyleApplier]. * * The bundled files a style needs — its looping backdrop [StyleBackground.Video] and its picker * thumbnail — are supplied by the [STYLE_SOURCE_ID] custom local asset source (see * [StyleAssetSource]). A style references them by [id], never by a hard-coded `file://` path. */ data class VideoStyle( /** Stable lowercase identifier used as the key everywhere (e.g. "noir"). */ val id: String, /** Human-facing name shown in the sheet (e.g. "Noir"). */ val displayName: String, /** Typeface name as it appears in the "ly.img.typeface" asset source. */ val typeface: String, /** Preferred font sub-families, most-preferred first; falls back to the first available. */ val fontWeights: List, /** Adjustment effect properties, e.g. ("effect/adjustments/saturation", -1.0f). Empty = no adjustments. */ val adjustments: List> = emptyList(), /** How much of the page the media fills. 1.0 = full-bleed; < 1.0 reveals the [background]. */ val mediaScale: Float = 1f, /** The backdrop shown behind the (scaled-down) media. */ val background: StyleBackground = StyleBackground.None, /** Title text color for this style (hex). White reads on the dark/video backdrops; Noir uses black. */ val titleTextColorHex: String = "#FFFFFF", /** Opaque ARGB color for the picker tile (shown behind the icon / as a placeholder). */ val previewBackground: Long, ) /** The backdrop a style paints behind the media once the media is scaled below full-bleed. */ sealed interface StyleBackground { /** No backdrop: the media fills the whole page (used by the unstyled default). */ object None : StyleBackground /** A flat color fill behind the media (e.g. white for Noir). [colorHex] is an "#RRGGBB" string. */ data class Solid( val colorHex: String, ) : StyleBackground /** * A looping video behind the media, supplied by the [STYLE_SOURCE_ID] asset source. * [assetId] is the id of the backdrop asset in that source (e.g. "hologram"). */ data class Video( val assetId: String, ) : StyleBackground } object VideoStyles { /** Neutral, unstyled: clean modern sans, full-bleed media, no filter or backdrop. */ val DEFAULT = VideoStyle( id = "default", displayName = "Default", typeface = "Montserrat", fontWeights = listOf("SemiBold", "Medium", "Regular"), previewBackground = 0xFFEFEBE9, ) /** Professional black & white on a clean white backdrop, with black title type. */ val NOIR = VideoStyle( id = "noir", displayName = "Noir", typeface = "Playfair Display", fontWeights = listOf("Bold", "SemiBold"), adjustments = listOf( "effect/adjustments/saturation" to -1.0f, "effect/adjustments/contrast" to 0.15f, "effect/adjustments/clarity" to 0.1f, ), mediaScale = 0.8f, background = StyleBackground.Solid(colorHex = "#FFFFFF"), titleTextColorHex = "#000000", previewBackground = 0xFF222222, ) /** Futuristic cool cast over a looping hologram backdrop. A blue temperature shift cools the media. */ val HOLOGRAM = VideoStyle( id = "hologram", displayName = "Hologram", typeface = "VT323", fontWeights = listOf("Regular"), adjustments = listOf( "effect/adjustments/temperature" to -0.4f, "effect/adjustments/contrast" to 0.1f, ), mediaScale = 0.8f, background = StyleBackground.Video(assetId = "hologram"), previewBackground = 0xFFE1F5FE, ) /** Playful, poppy filter over a looping bubblegum backdrop: punchy saturation, bright tones. */ val BUBBLEGUM = VideoStyle( id = "bubblegum", displayName = "Bubblegum", typeface = "Lobster Two", fontWeights = listOf("Bold", "Regular"), adjustments = listOf( "effect/adjustments/saturation" to 0.5f, "effect/adjustments/brightness" to 0.05f, "effect/adjustments/contrast" to 0.1f, ), mediaScale = 0.8f, background = StyleBackground.Video(assetId = "bubblegum"), previewBackground = 0xFFFCE4EC, ) /** All styles, in the order they appear in the Styles sheet. */ val ALL = listOf(DEFAULT, NOIR, HOLOGRAM, BUBBLEGUM) /** Resolves a style by its [id] (case-insensitive), falling back to [DEFAULT]. */ fun byId(id: String): VideoStyle = ALL.firstOrNull { it.id.equals(id, ignoreCase = true) } ?: DEFAULT } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/starter-kit/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt reference-only package ly.img.editor.configuration.memories.style import androidx.core.net.toUri import ly.img.engine.Engine import ly.img.engine.FindAssetsQuery /** * The custom **local** asset source that supplies the bundled style assets — the looping backdrop * videos and every style's picker thumbnail. The files stay local (in `src/main/assets`) and are * described by `assets/ly.img.memories.style/content.json`; the engine loads them through * [addLocalSourceFromJSON][ly.img.engine.AssetApi.addLocalSourceFromJSON], exactly like the default * IMG.LY sources. Keeping them behind an asset source means the style catalog references assets by * id ([VideoStyle]) instead of hard-coding `file:///android_asset/...` paths across the kit. */ const val STYLE_SOURCE_ID = "ly.img.memories.style" /** The bundled `content.json` describing [STYLE_SOURCE_ID], resolved as an `android_asset` URI. */ private const val STYLE_SOURCE_CONTENT_URI = "file:///android_asset/$STYLE_SOURCE_ID/content.json" /** * Register [STYLE_SOURCE_ID] from its bundled `content.json` (idempotent). Call once while loading * the other asset sources, before any style is applied or the Styles picker is shown. */ suspend fun Engine.registerStyleAssetSource() { if (STYLE_SOURCE_ID !in asset.findAllSources()) { asset.addLocalSourceFromJSON(contentUri = STYLE_SOURCE_CONTENT_URI.toUri()) } } /** * The picker thumbnail URI for each style, keyed by style id, read from [STYLE_SOURCE_ID]. Styles * without a bundled asset (e.g. the unstyled default) are simply absent from the map. */ suspend fun Engine.loadStyleThumbnails(): Map = asset.findAssets( sourceId = STYLE_SOURCE_ID, query = FindAssetsQuery(page = 0, perPage = 100), ).assets.mapNotNull { asset -> asset.meta?.get("thumbUri")?.let { asset.id to it } }.toMap() /** The backdrop video URI for a style, read from its [STYLE_SOURCE_ID] asset (null if absent). */ suspend fun Engine.styleBackgroundVideoUri(assetId: String): String? = asset.fetchAsset(sourceId = STYLE_SOURCE_ID, assetId = assetId)?.meta?.get("uri") ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-memories/starter-kit/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnExport.kt reference-only package ly.img.editor.configuration.memories.callback import kotlinx.coroutines.CancellationException import ly.img.editor.configuration.memories.MemoriesConfiguration import ly.img.editor.configuration.memories.model.ExportStatus import ly.img.engine.MimeType import java.nio.ByteBuffer /** * Render the slideshow page to an MP4, reporting progress through [MemoriesConfiguration.exportStatus] * so the overlay can show a progress circle, then a share button on success. */ suspend fun MemoriesConfiguration.onExport() { try { exportStatus = ExportStatus.Loading(progress = 0f) val buffer = exportSlideshow() val file = writeToFile(byteBuffer = buffer, mimeType = MimeType.MP4) exportStatus = ExportStatus.Success(file = file, mimeType = MimeType.MP4) } catch (cancellation: CancellationException) { exportStatus = null throw cancellation } catch (exception: Exception) { exportStatus = ExportStatus.Error(exception) } } private suspend fun MemoriesConfiguration.exportSlideshow(): ByteBuffer { val engine = editorContext.engine val page = requireNotNull(engine.scene.getCurrentPage()) return engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { progress -> if (progress.totalFrames > 0) { val fraction = progress.encodedFrames.toFloat() / progress.totalFrames val current = exportStatus // Only push a new state on a visible (~1%) change to avoid churning recomposition. if (current !is ExportStatus.Loading || fraction >= current.progress + 0.01f) { exportStatus = ExportStatus.Loading(progress = fraction) } } }, ) } ``` Turn a set of photos and video clips into a shareable memory montage on Android. The kit picks up media from the gallery, arranges it on a timeline with crossfades and title cards, applies styled looks, layers in audio, and exports an MP4—entirely on the device with no server dependencies. ![Memories starter kit screenshot](./assets/android.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [Download examples](https://github.com/imgly/starterkit-memories-editor-android/archive/refs/heads/v$UBQ_VERSION$.zip) > > - [View source on GitHub](https://github.com/imgly/starterkit-memories-editor-android/tree/v$UBQ_VERSION$) *** ## Pre-Requisites This guide assumes basic familiarity with Android and Kotlin. You will need: - Latest Android Studio - Kotlin: 1.9.10 or later - Gradle: 8.4 or later - Android: 7.0+ (API level 24+) ## Get Started Start with a complete, runnable Android starter kit project. ### Step 1: Clone the Repository ```bash git clone -b v$UBQ_VERSION$ https://github.com/imgly/starterkit-memories-editor-android.git cd starterkit-memories-editor-android ``` ### Step 2: Open and Run [Create and launch](https://developer.android.com/studio/run/managing-avds) a new android emulator or use an existing one, or connect a physical device with `USB Debugging` on. Open the project in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` and run the `app` module from the UI, or use: ```bash ./gradlew app:installDebug ``` The sample app launches `MainActivity`, which displays the full Memories flow: a photo picker that hands the selected media to the slideshow editor. ## Get Started Integrate only the `starter-kit` library module into your existing Android app. ### Step 1: Run the Extraction Script From Your App Root Run this from your application root directory: ```bash repo="starterkit-memories-editor-android" version="$UBQ_VERSION$" curl -0 "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit" "${repo}-${version}/starter-kit-dependencies.gradle" ``` This extracts two things into your project, side by side: the `starter-kit/` library module and its `starter-kit-dependencies.gradle`. The dependencies file lists the extra libraries the kit needs (Coil, ExifInterface, and a few Compose artifacts); `starter-kit/build.gradle.kts` applies it from right next to the module, so keep the two as siblings. ### Step 2: Include the Module Declare the newly added Android library module in your project: ```kotlin title = "settings.gradle.kts" include(":starter-kit") ``` ### Step 3: Add Dependency in Your App Module Include the starter kit module dependency in your app module: ```kotlin title = "app/build.gradle.kts" dependencies { implementation(project(":starter-kit")) } ``` ### Step 4: Add the IMG.LY Maven Repository Add the `IMG.LY` maven repository path in your project: ```kotlin title = "settings.gradle.kts" dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name = "IMG.LY Artifactory" url = uri("https://artifactory.img.ly/artifactory/maven") mavenContent { includeGroup("ly.img") } } } } ``` If the project is open in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` to make all dependencies available. The full implementation of the starter kit lives in the `starter-kit/` folder: ```text starter-kit/src/main/ ├── assets/ │ └── ly.img.memories.style/ # The kit's own local asset source (backdrops + picker thumbnails) │ ├── content.json # Describes the style assets; URIs use the {{base_url}} placeholder │ ├── thumbnails/ # noir.png, hologram.png, bubblegum.png │ └── videos/ # hologram.mp4, bubblegum.mp4 (looping style backdrops) └── kotlin/ly/img/editor/configuration/memories/ ├── MemoriesConfiguration.kt # Wires the editor's onCreate / dock / bottomPanel / navigationBar / overlay / onExport slots ├── MemoriesApp.kt # The full flow: photo picker → slideshow editor ├── MemoriesEditor.kt # Hosts the editor with the loading overlay and back handling ├── MemoriesViewModel.kt # Editor + montage state ├── callback/ │ ├── OnCreate.kt # Builds the scene, registers asset sources, wires event subscriptions │ └── OnExport.kt # Export flow and handling (MP4) ├── component/ # Editor UI slots: Dock, BottomPanel, NavigationBar, Overlay, ExportOverlay ├── scene/ # Timeline assembly (the tracks are the source of truth) │ ├── SceneSetup.kt # Builds the video scene and its tracks in code │ ├── Timeline.kt # Lays each photo/clip on its own track for the crossfade montage │ ├── TrackEditor.kt # Reads/writes the tracks and applies in-place edits │ ├── Title.kt # Title card + burst-image intro │ └── Playback.kt # Loop / volume helpers ├── style/ # The styled looks + their custom asset source │ ├── VideoStyle.kt # Style catalog (filter, backdrop, matte, typeface) referencing assets by id │ ├── StyleAssetSource.kt # Registers ly.img.memories.style via addLocalSourceFromJSON │ └── StyleApplier.kt # Applies a style to the slideshow ├── screen/ # ImageSelectionScreen (picker) + LoadingScreen ├── sheet/ # Styles and Volume bottom sheets ├── widget/ # Reusable composables (grid, timeline strip, video thumbnail, …) ├── iconPack/ # Compose vector icons ├── model/ # Small data models (ImageItem, TimelineImage, ExportStatus) └── util/ ├── Animations.kt # The slide animations (edit this to change the motion) └── Constants.kt # Timing, page size, title knobs ``` ## Set Up a Scene The Memories scene is built in code (no serialized scene file). The setup logic lives in `scene/SceneSetup.kt` and runs from the editor's `onCreate` (`callback/OnCreate.kt`): it creates a video scene, lays out the persistent background / matte / text tracks, then `scene/Timeline.kt` places one track per photo or clip so consecutive slides overlap for the crossfade while a later slide always renders above the earlier one. ## Styles From a Custom Asset Source The styled looks (Noir, Hologram, Bubblegum) get their **backdrops and picker thumbnails from the kit's own local asset source**, `ly.img.memories.style`, rather than hard-coded paths. The assets stay bundled under `starter-kit/src/main/assets/ly.img.memories.style/` and are described by `content.json`, which `style/StyleAssetSource.kt` registers with `engine.asset.addLocalSourceFromJSON(...)`—exactly like the default IMG.LY sources. `style/VideoStyle.kt` then references each backdrop and thumbnail **by asset id**, and `style/StyleApplier.kt` resolves the actual URI from the source at apply time. Because the asset URIs in `content.json` use the portable `{{base_url}}` placeholder (which the engine substitutes with the `basePath` the source is registered against) instead of an absolute `file:///android_asset/...` path, the same source definition also works if you reuse it on iOS or Web. ```json title="starter-kit/src/main/assets/ly.img.memories.style/content.json" { "uri": "{{base_url}}/ly.img.memories.style/videos/hologram.mp4", "thumbUri": "{{base_url}}/ly.img.memories.style/thumbnails/hologram.png" } ``` To add a look, drop its thumbnail (and backdrop, if any) into the `assets` folder, add an entry to `content.json`, and add the matching `VideoStyle` in `style/VideoStyle.kt`. ## Where to Change Things The starter kit ships a generic structure and behavior, but every part of it is in your codebase and meant to be customized. The most common edit points: - **Slide animations** — `util/Animations.kt`. Add, remove, or tune the in/out animation pairs a slide can use. - **Looks / filters (styles)** — `style/VideoStyle.kt`. Each style bundles a filter, backdrop, matte, and title typeface, referencing the bundled assets by id (see above). - **Timing, page size, title** — `util/Constants.kt` (clip duration, crossfade overlap, canvas size, title duration). - **Timeline assembly** — `scene/SceneSetup.kt`, `scene/Timeline.kt`, and `scene/Title.kt`. ## Customize Export Functionality Export handling lives in `callback/OnExport.kt`. The montage is rendered to an MP4 `ByteBuffer`; from there you can upload it to your server, save it to the device gallery, or share it. Closing the editor is driven by the `onCloseEditor` callback passed into `MemoriesEditor` (wired to the navigation-bar close button via `rememberCloseEditor` in `component/NavigationBar.kt`). > **More Export Options:** See [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) for all available export and scene calls. *** ## Troubleshooting > **Free Trial:** [Sign up for a free trial](https://img.ly/forms/free-trial) to get a license key and remove the watermark. ### Editor doesn't load - **Check onCreate**: Ensure the `onCreate` callback finishes building the scene and no coroutine is stuck. - **Verify the baseURL**: Assets must be reachable from the CDN or your self-hosted location. - **Check logcat errors**: Look for errors in Android logcat. ### Export fails or produces blank output - **Wait for content to load**: Ensure media is fully loaded before exporting. - **Check logcat errors**: Look for errors in Android logcat. *** ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) – Complete list of initialization options - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) – Self-host engine assets for production - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) – Customize colors and appearance - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) – Add translations and language support --- ## 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: "Photo Editor in Android" description: "Add the Android Photo Editor starter kit to a new or existing app with a reusable starter-kit module." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [Photo Editor](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) --- ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/app/src/main/kotlin/ly/img/starterkit/example/ExampleBaseUri.kt reference-only package ly.img.starterkit.example 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.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/app/src/main/kotlin/ly/img/starterkit/example/ExampleComposeNavigationScreen.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/app/src/main/kotlin/ly/img/starterkit/example/ExampleForceCrop.kt reference-only package ly.img.starterkit.example import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.component.data.ForceCropConfiguration import ly.img.editor.core.component.data.ForceCropMode import ly.img.editor.core.component.data.ForceCropPresetCandidate import ly.img.editor.core.event.EditorEvent suspend fun PhotoConfigurationBuilder.onLoaded() { // Apply force crop, allow 1:1, 16:9 or 9:16 val configuration = ForceCropConfiguration( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-1-1", presetCandidates = listOf( ForceCropPresetCandidate( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-16-9", ), ForceCropPresetCandidate( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-9-16", ), ), mode = ForceCropMode.IfNeeded(threshold = 0.01f), ) editorContext.eventHandler.send( event = EditorEvent.ApplyForceCrop( designBlock = requireNotNull(editorContext.engine.scene.getCurrentPage()), configuration = configuration, ), ) // Existing body here } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/app/src/main/kotlin/ly/img/starterkit/example/ExampleRenderTarget.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/app/src/main/kotlin/ly/img/starterkit/example/ExampleUIMode.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/EditorActivity.kt reference-only package ly.img.editor.configuration.photo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnCreate.kt reference-only @file:Suppress("UnusedReceiverParameter") package ly.img.editor.configuration.photo.callback import android.net.Uri import android.util.SizeF import androidx.core.net.toUri import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.engine.DesignBlock import ly.img.engine.SceneLayout /** * The callback that is invoked when the editor is created. */ suspend fun PhotoConfigurationBuilder.onCreate( preCreateScene: suspend PhotoConfigurationBuilder.() -> Unit = { onPreCreateScene() }, createScene: suspend PhotoConfigurationBuilder.() -> Unit = { onCreateScene() }, loadAssetSources: suspend PhotoConfigurationBuilder.() -> Unit = { onLoadAssetSources() }, postCreateScene: suspend PhotoConfigurationBuilder.() -> Unit = { onPostCreateScene() }, finally: suspend PhotoConfigurationBuilder.() -> Unit = { onCreateFinally() }, ) { try { preCreateScene() createScene() loadAssetSources() postCreateScene() } finally { finally() } } /** * The callback that is invoked before the scene is created. */ fun PhotoConfigurationBuilder.onPreCreateScene() { showLoading = true editorContext.engine.editor.setSettingBoolean(keypath = "page/moveChildrenWhenCroppingFill", value = true) editorContext.engine.editor.setSettingBoolean(keypath = "page/selectWhenNoBlocksSelected", value = true) editorContext.engine.editor.setSettingBoolean(keypath = "page/highlightWhenCropping", value = true) editorContext.engine.editor.setSettingBoolean(keypath = "doubleClickToCropEnabled", value = false) } /** * The callback that is responsible for creating the scene. */ suspend fun PhotoConfigurationBuilder.onCreateScene() { getOrCreateSceneFromImage(imageUri = "https://cdn.img.ly/assets/demo/v3/ly.img.image/images/sample_1.jpg".toUri()) } suspend fun PhotoConfigurationBuilder.getOrCreateSceneFromImage( imageUri: Uri, size: SizeF? = null, dpi: Float = 300F, pixelScaleFactor: Float = 1F, sceneLayout: SceneLayout = SceneLayout.FREE, ): DesignBlock = editorContext.engine.scene.get() ?: editorContext.engine.scene.createFromImage( imageUri = imageUri, dpi = dpi, pixelScaleFactor = pixelScaleFactor, sceneLayout = sceneLayout, ).also { size?.let { val page = requireNotNull(editorContext.engine.scene.getCurrentPage()) editorContext.engine.block.setWidth(block = page, value = size.width) editorContext.engine.block.setHeight(block = page, value = size.height) } } /** * The callback that loads all the required assets sources. */ suspend fun PhotoConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } } /** * The callback that is invoked right after [onCreateScene], after the scene is created. */ fun PhotoConfigurationBuilder.onPostCreateScene() { val page = requireNotNull(editorContext.engine.scene.getCurrentPage()) editorContext.engine.block.setScopeEnabled(block = page, key = "layer/move", enabled = false) } /** * The callback that is invoked as the last step of [onCreate]. * It always runs, no matter success or failure on previous steps. */ fun PhotoConfigurationBuilder.onCreateFinally() { showLoading = false } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-photo/starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnExport.kt reference-only package ly.img.editor.configuration.photo.callback import kotlinx.coroutines.CancellationException import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.engine.MimeType import java.nio.ByteBuffer /** * The callback that is invoked when the export button is clicked. */ suspend fun PhotoConfigurationBuilder.onExport( preExport: suspend PhotoConfigurationBuilder.() -> Unit = { onPreExport() }, exportByteBuffer: suspend PhotoConfigurationBuilder.() -> ByteBuffer = { onExportByteBuffer() }, postExport: suspend PhotoConfigurationBuilder.(ByteBuffer) -> Unit = { onPostExport(it) }, error: suspend PhotoConfigurationBuilder.(Exception) -> Unit = { onExportError(it) }, finally: suspend PhotoConfigurationBuilder.() -> Unit = { onExportFinally() }, ) { try { preExport() val result = exportByteBuffer() postExport(result) } catch (exception: Exception) { error(exception) } finally { finally() } } /** * The callback that is invoked before the export is started. */ fun PhotoConfigurationBuilder.onPreExport() { showLoading = true } /** * The callback that exports the content of the editor into [ByteBuffer]. */ suspend fun PhotoConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PNG, ) /** * The callback that is invoked after [onExportByteBuffer] and handles its output. */ suspend fun PhotoConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PNG) shareFile(file = file, mimeType = MimeType.PNG) } /** * The callback that is invoked in case any of the export functions throw an exception. */ fun PhotoConfigurationBuilder.onExportError(error: Exception) { if (error is CancellationException) { throw error } this.error = error } /** * The callback that is invoked as the last step of [onExportByteBuffer]. * It always runs, no matter success or failure on previous steps. */ fun PhotoConfigurationBuilder.onExportFinally() { showLoading = false } ``` Professional photo editing for your Android app—crop, filter, adjust, and remove backgrounds. Runs entirely on the mobile device with no server dependencies. ![Photo Editor starter kit screenshot](../../../../../../cesdk_android_showcases/starter-kits/starter-kit-photo/screenshot.png) > **Reading time:** 10 minutes > > **Resources:** > > - [Download examples](https://github.com/imgly/starterkit-photo-editor-android/archive/refs/heads/v$UBQ_VERSION$.zip) > > - [View source on GitHub](https://github.com/imgly/starterkit-photo-editor-android/tree/v$UBQ_VERSION$) *** ## Pre-Requisites This guide assumes basic familiarity with Android and Kotlin. You will need: - Latest Android Studio - Kotlin: 1.9.10 or later - Gradle: 8.4 or later - Android: 7.0+ (API level 24+) ## Get Started Start with a complete, runnable Android starter kit project. ### Step 1: Clone the Repository ```bash git clone -b v$UBQ_VERSION$ https://github.com/imgly/starterkit-photo-editor-android.git cd starterkit-photo-editor-android ``` ### Step 2: Open and Run [Create and launch](https://developer.android.com/studio/run/managing-avds) a new android emulator or use an existing one or connect a physical device with `USB Debugging` on. Open the project in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` and run the `app` module from UI, or use: ```bash ./gradlew app:installDebug ``` The sample app launches `MainActivity` that has "Launch Editor" button. Clicking it launches `EditorActivity`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.photo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ## Get Started Integrate only the `starter-kit` library module into your existing Android app. ### Step 1: Run the Extraction Script From Your App Root Run this from your application root directory: ```bash repo="starterkit-photo-editor-android" version="$UBQ_VERSION$" curl -0 "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit" ``` This extracts the `starter-kit/` library module into your android project. ### Step 2: Include the Module Declare the newly added android library module in your project: ```kotlin title = "settings.gradle.kts" include(":starter-kit") ``` ### Step 3: Add Dependency in Your App Module Include the starter kit module dependency in your app module: ```kotlin title = "app/build.gradle.kts" dependencies { implementation(project(":starter-kit")) } ``` ### Step 4: Include missing plugins Include plugins that may be missing in your project's root `build.gradle.kts` file: ```kotlin title = "build.gradle.kts" plugins { // Existing plugins here id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false } ``` ### Step 5: Add the IMG.LY Maven Repository Add `IMG.LY` maven repository path in your project: ```kotlin title = "settings.gradle.kts" 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") } } } } ``` If the project is open in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` in order to make all dependencies available. ### Step 6: Launch the Editor From Your UI If you use jetpack compose navigation in your app, simply add a new navigation destination and invoke the following composable: ```kotlin highlight-starter-kit-composable import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` > **Delete EditorActivity:** You can delete `starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/EditorActivity.kt` as it is needed only for legacy navigation. If you do not use jetpack compose navigation and use legacy android navigation, the starter kit has a special `EditorActivity` class with full encapsulated logic: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.photo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` Simply launch the activity from the activity of your app. Optionally, you can pass parameters to the editor: ```kotlin import android.content.Intent import ly.img.editor.configuration.photo.EditorActivity fun launchEditor() { // "this" is the current activity val intent = Intent(this, EditorActivity::class.java).also { // Optionally pass parameters for EditorActivity to consume, i.e. your image/video/scene uri it.putExtra("my_param", "my_param_value") } startActivity(intent) } ``` In case you want to consume the parameter in `EditorActivity`: ```kotlin class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val param = intent.getStringExtra("my_param") ?: "default" ... } } ``` The full implementation of the starter kit lives in the `starter-kit/` folder: ```text starter-kit/ ├── build.gradle.kts # Starter kit library module config, includes ly.img:editor dependency └── src/main/ ├── AndroidManifest.xml # Starter kit manifest file, may contain permissions └── kotlin/ly/img/editor/configuration/photo/ ├── EditorActivity.kt # Encapsulated editor for legacy navigation. Delete if you use jetpack compose navigation ├── PhotoConfigurationBuilder.kt # Editor configuration logic encapsulated in 1 place ├── callback/ │ ├── OnCreate.kt # Editor initialization logic │ ├── OnExport.kt # Export flow and handling │ └── OnLoaded.kt # Post onCreate logic └── component/ ├── CanvasMenu.kt # Canvas Menu component configuration ├── Dock.kt # Dock component configuration ├── InspectorBar.kt # Inspector Bar component configuration ├── NavigationBar.kt # Navigation Bar component configuration └── Overlay.kt # Overlay component configuration ``` ## Starter Kit as a Dynamic Feature Since `starter-kit` folder is an android library module, it is possible to turn it into a [dynamic feature](https://developer.android.com/guide/playcore/feature-delivery). This can be helpful if you want to lazy load the editor in order to reduce the download size of your app. See [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) for more details. ## Configuring the Starter Kit The starter kit that we provide contains a very generic structure and behavior, however we understand that every customer wants to configure it according to their needs. The good thing is that the starter kit implementation is part of your codebase and you can configure, add/remove/modify functionality as you wish. In addition, you may want to configure the editor based on your business logic, i.e. restore the scene file from previous edits, display different dock items for different users etc. This example demonstrates on how to pass, store and use external parameters in the starter kit. First, declare a new property to the builder class: ```kotlin ... import android.net.Uri @Stable class PhotoConfigurationBuilder : BasicConfigurationBuilder() { /** * The scene uri that should be loaded in onCreate if not null. * Note that editorContext.mutableStateOf is used to store mutable objects in the editor scope that survive configuration changes. */ var sceneUri: Uri? by editorContext.mutableStateOf( key = "your.package.name.state.sceneUri", initial = null, ) ... } ``` Next, read your external parameter from activity intent extras (or jetpack compose screen arguments) and assign to the property of the builder: ```kotlin import androidx.core.net.toUri class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val sceneUri = intent.getStringExtra("sceneUri")?.toUri() Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) { sceneUri = sceneUri } }, onClose = { // Finish the activity, ignore any errors. finish() }, ) } } ``` Finally, make use of the property. The scene loading logic is located at `OnCreate.kt` file, as part of `onCreate` implementation (see next section for more details). We modify this function to load the `sceneUri` instead if the value is not null: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnCreate.kt" suspend fun PhotoConfigurationBuilder.onCreateScene() { sceneUri?.let { safeSceneUri -> // Load the sceneUri if it's not null getOrLoadScene(sceneUri = safeSceneUri) } ?: run { // Otherwise stick to the default behavior of the starter kit getOrCreateSceneFromImage(imageUri = "https://cdn.img.ly/assets/demo/v3/ly.img.image/images/sample_1.jpg".toUri()) } } ``` ## Set Up a Scene The scene setup logic is located at `OnCreate.kt` file, as part of `onCreate` implementation: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnCreate.kt" highlight-starter-kit-on-create-scene suspend fun PhotoConfigurationBuilder.onCreateScene() { getOrCreateSceneFromImage(imageUri = "https://cdn.img.ly/assets/demo/v3/ly.img.image/images/sample_1.jpg".toUri()) } suspend fun PhotoConfigurationBuilder.getOrCreateSceneFromImage( imageUri: Uri, size: SizeF? = null, dpi: Float = 300F, pixelScaleFactor: Float = 1F, sceneLayout: SceneLayout = SceneLayout.FREE, ): DesignBlock = editorContext.engine.scene.get() ?: editorContext.engine.scene.createFromImage( imageUri = imageUri, dpi = dpi, pixelScaleFactor = pixelScaleFactor, sceneLayout = sceneLayout, ).also { size?.let { val page = requireNotNull(editorContext.engine.scene.getCurrentPage()) editorContext.engine.block.setWidth(block = page, value = size.width) editorContext.engine.block.setHeight(block = page, value = size.height) } } ``` `getOrCreateSceneFromImage` is a helper that loads an existing scene if available, or initializes one from an image uri. You may want to pass your own `imageUri` to the `onCreate` function and use it to load the scene via this helper. CE.SDK offers multiple ways to load scene into the editor. Choose the method that matches your use case: ```kotlin // Load from an image uri - creates a new scene with the image editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri()) // Load from a template archive - restores a previously saved project editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri()) // Create a blank canvas - starts with an empty design scene editorContext.engine.scene.create() // Load from a scene file - restores a scene from .scene file editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri()) ``` > **More Loading Options:** See [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) for all available loading methods. ## Force Crop Require users to crop images to specific dimensions before saving. This is useful for profile pictures, social media posts, or any workflow requiring consistent image sizes. Send `EditorEvent.ApplyForceCrop` event before or after the existing logic of `onLoaded`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnLoaded.kt" highlight-starter-kit-force-crop import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.component.data.ForceCropConfiguration import ly.img.editor.core.component.data.ForceCropMode import ly.img.editor.core.component.data.ForceCropPresetCandidate import ly.img.editor.core.event.EditorEvent suspend fun PhotoConfigurationBuilder.onLoaded() { // Apply force crop, allow 1:1, 16:9 or 9:16 val configuration = ForceCropConfiguration( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-1-1", presetCandidates = listOf( ForceCropPresetCandidate( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-16-9", ), ForceCropPresetCandidate( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-9-16", ), ), mode = ForceCropMode.IfNeeded(threshold = 0.01f), ) editorContext.eventHandler.send( event = EditorEvent.ApplyForceCrop( designBlock = requireNotNull(editorContext.engine.scene.getCurrentPage()), configuration = configuration, ), ) // Existing body here } ``` See [Force Crop](https://img.ly/docs/cesdk/android/user-interface/customization/force-crop-c2854e/) for more details. ## Customize Assets The asset source setup is located in `OnCreate.kt` as part of `onCreate` implementation. Enable or disable individual sources: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnCreate.kt" highlight-starter-kit-on-load-asset-sources suspend fun PhotoConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } } ``` > **More Asses Sources:** See [Import Media](https://img.ly/docs/cesdk/android/import-media-4e3703/) for all available assets and loading mechanisms. For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for downloading assets, configuring `baseUri` and excluding unused sources to optimize load times. ## Customize Export Functionality Export handling logic is located in `OnExport.kt` as part of `onExport` callback. `onExportByteBuffer` controls what should be exported from the scene. It can be a scene or set of design blocks. For photo designer, it makes sense to export the scene to a PNG or JPEG content. `export` is a helper function that calls `editorContext.engine.block.export` under the hood: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnExport.kt" highlight-starter-kit-on-export-byte-buffer suspend fun PhotoConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PNG, ) ``` > **More Export Options:** See [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) guides for all available export and scene calls. `onPostExport` controls what should happen to the exported `ByteBuffer` content. You can upload the result to your server, save it to the device gallery or simply close the editor via `editorContext.eventHandler.send(EditorEvent.CloseEditor())`. Check `writeToFile`, `shareFile` and `shareUri` helper functions for potential implementations: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/callback/OnExport.kt" highlight-starter-kit-on-post-export suspend fun PhotoConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PNG) shareFile(file = file, mimeType = MimeType.PNG) } ``` *** ## Customize (Optional) ### Base Uri The starter kit does not make any `baseUri` configuration, which means it points to `https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/assets`. If you want to store them in your own CDN or locally, assets can be accessed via [zip file](https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/imgly-assets.zip). For example, if you want to store them locally, unzip the content and place at `starter-kit/src/main/assets`: ```kotlin highlight-starter-kit-base-uri 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.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` ### UI Mode CE.SDK supports light and dark ui modes out of the box, plus automatic system preference detection. Switch between themes programmatically: ```kotlin highlight-starter-kit-ui-mode import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` See [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) for more details. ### Native Android Canvas CE.SDK supports rendering on two of the most popular native android views that allow GPU rendering: [TextureView](https://developer.android.com/reference/android/view/TextureView) and [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView): ```kotlin highlight-starter-kit-engine-render-target import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.photo.PhotoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::PhotoConfigurationBuilder) }, onClose = onClose, ) } ``` ### Localization See [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) for supported languages, adding support to new languages and replacing existing keys. ### UI Layout All the configurable components are located at `starter-kit/src/main/kotlin/ly/img/editor/configuration/photo/component`: - `CanvasMenu.kt` - see [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) for full configuration options. - `Dock.kt` - see [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) for full configuration options. - `InspectorBar.kt` - see [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) for full configuration options. - `NavigationBar.kt` - see [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) for full configuration options. - `Overlay.kt` - see [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) for full configuration options. *** ## Troubleshooting > **Free Trial:** [Sign up for a free trial](https://img.ly/forms/free-trial) to get a license key and remove the watermark. ### Editor doesn't load - **Check onCreate**: Ensure `onCreate` callback loads a scene and no coroutine is stuck infinitly - **Verify the baseURL**: Assets must be accessible from the CDN or your self-hosted location - **Check logcat errors**: Look for error in Android logcat ### Assets don't appear - **Check network requests**: Make sure the device/emulator is connected to the internet - **Self-host assets for production**: See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) to host assets on your infrastructure - **Check logcat errors**: Look for error in Android logcat ### Export fails or produces blank images - **Wait for content to load**: Ensure images are fully loaded before exporting - **Check logcat errors**: Look for error in Android logcat ### Watermark appears in production - **Add your license key**: Set the `license` property in your configuration - **Sign up for a trial**: Get a free trial license at [img.ly/forms/free-trial](https://img.ly/forms/free-trial) *** ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) – Complete list of initialization options - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) – Self-host engine assets for production - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) – Customize colors and appearance - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) – Add translations and language support --- ## 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: "Postcard Editor in Android" description: "Add the Android Postcard Editor starter kit to a new or existing app with a reusable starter-kit module." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/postcard-editor-03mijr/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [Custom Built UIs](https://img.ly/docs/cesdk/android/starterkits/custom-built-uis-5a5cc4/) > [Postcard UI](https://img.ly/docs/cesdk/android/starterkits/postcard-editor-03mijr/) --- ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/app/src/main/kotlin/ly/img/starterkit/example/ExampleBaseUri.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/app/src/main/kotlin/ly/img/starterkit/example/ExampleComposeNavigationScreen.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/app/src/main/kotlin/ly/img/starterkit/example/ExampleRenderTarget.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/app/src/main/kotlin/ly/img/starterkit/example/ExampleUIMode.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/EditorActivity.kt reference-only package ly.img.editor.configuration.postcard import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnCreate.kt reference-only @file:Suppress("UnusedReceiverParameter") package ly.img.editor.configuration.postcard.callback import androidx.core.net.toUri import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.SystemGalleryAssetSource import ly.img.editor.core.library.data.SystemGalleryPermission import ly.img.engine.GlobalScope /** * The callback that is invoked when the editor is created. */ suspend fun PostcardConfigurationBuilder.onCreate( preCreateScene: suspend PostcardConfigurationBuilder.() -> Unit = { onPreCreateScene() }, createScene: suspend PostcardConfigurationBuilder.() -> Unit = { onCreateScene() }, loadAssetSources: suspend PostcardConfigurationBuilder.() -> Unit = { onLoadAssetSources() }, postCreateScene: suspend PostcardConfigurationBuilder.() -> Unit = { onPostCreateScene() }, finally: suspend PostcardConfigurationBuilder.() -> Unit = { onCreateFinally() }, ) { try { preCreateScene() createScene() loadAssetSources() postCreateScene() } finally { finally() } } /** * The callback that is invoked before the scene is created. */ fun PostcardConfigurationBuilder.onPreCreateScene() { showLoading = true editorContext.engine.editor.setGlobalScope("editor/add", GlobalScope.DEFER) } /** * The callback that is responsible for creating the scene. */ suspend fun PostcardConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/postcard.scene".toUri()) } /** * The callback that loads all the required assets sources. */ suspend fun PostcardConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri // Postcard intentionally registers a custom subset of default sources (no // ly.img.color.palette / ly.img.page.presets — those are provided by the postcard builder). val sourceIds = listOf( "ly.img.sticker", "ly.img.vector.shape", "ly.img.filter", "ly.img.crop.presets", "ly.img.effect", "ly.img.blur", "ly.img.typeface", "ly.img.text", "ly.img.text.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } /** * The callback that is invoked right after [onCreateScene], after the scene is created. */ fun PostcardConfigurationBuilder.onPostCreateScene() { // Do nothing } /** * The callback that is invoked as the last step of [onCreate]. * It always runs, no matter success or failure on previous steps. */ fun PostcardConfigurationBuilder.onCreateFinally() { showLoading = false } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-postcard/starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnExport.kt reference-only package ly.img.editor.configuration.postcard.callback import kotlinx.coroutines.CancellationException import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.engine.MimeType import java.nio.ByteBuffer /** * The callback that is invoked when the export button is clicked. */ suspend fun PostcardConfigurationBuilder.onExport( preExport: suspend PostcardConfigurationBuilder.() -> Unit = { onPreExport() }, exportByteBuffer: suspend PostcardConfigurationBuilder.() -> ByteBuffer = { onExportByteBuffer() }, postExport: suspend PostcardConfigurationBuilder.(ByteBuffer) -> Unit = { onPostExport(it) }, error: suspend PostcardConfigurationBuilder.(Exception) -> Unit = { onExportError(it) }, finally: suspend PostcardConfigurationBuilder.() -> Unit = { onExportFinally() }, ) { try { preExport() val result = exportByteBuffer() postExport(result) } catch (exception: Exception) { error(exception) } finally { finally() } } /** * The callback that is invoked before the export is started. */ fun PostcardConfigurationBuilder.onPreExport() { showLoading = true } /** * The callback that exports the content of the editor into [ByteBuffer]. */ suspend fun PostcardConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) /** * The callback that is invoked after [onExportByteBuffer] and handles its output. */ suspend fun PostcardConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF) shareFile(file = file, mimeType = MimeType.PDF) } /** * The callback that is invoked in case any of the export functions throw an exception. */ fun PostcardConfigurationBuilder.onExportError(error: Exception) { if (error is CancellationException) { throw error } this.error = error } /** * The callback that is invoked as the last step of [onExportByteBuffer]. * It always runs, no matter success or failure on previous steps. */ fun PostcardConfigurationBuilder.onExportFinally() { showLoading = false } ``` Professional postcard editing for your Android app—create and personalize postcards and greeting cards. Runs entirely on the mobile device with no server dependencies. ![Postcard Editor starter kit screenshot](../../../../../../cesdk_android_showcases/starter-kits/starter-kit-postcard/screenshot.png) > **Reading time:** 10 minutes > > **Resources:** > > - [Download examples](https://github.com/imgly/starterkit-postcard-editor-android/archive/refs/heads/v$UBQ_VERSION$.zip) > > - [View source on GitHub](https://github.com/imgly/starterkit-postcard-editor-android/tree/v$UBQ_VERSION$) *** ## Pre-Requisites This guide assumes basic familiarity with Android and Kotlin. You will need: - Latest Android Studio - Kotlin: 1.9.10 or later - Gradle: 8.4 or later - Android: 7.0+ (API level 24+) ## Get Started Start with a complete, runnable Android starter kit project. ### Step 1: Clone the Repository ```bash git clone -b v$UBQ_VERSION$ https://github.com/imgly/starterkit-postcard-editor-android.git cd starterkit-postcard-editor-android ``` ### Step 2: Open and Run [Create and launch](https://developer.android.com/studio/run/managing-avds) a new android emulator or use an existing one or connect a physical device with `USB Debugging` on. Open the project in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` and run the `app` module from UI, or use: ```bash ./gradlew app:installDebug ``` The sample app launches `MainActivity` that has "Launch Editor" button. Clicking it launches `EditorActivity`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.postcard import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ## Get Started Integrate only the `starter-kit` library module into your existing Android app. ### Step 1: Run the Extraction Script From Your App Root Run this from your application root directory: ```bash repo="starterkit-postcard-editor-android" version="$UBQ_VERSION$" curl -0 "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit" ``` This extracts the `starter-kit/` library module into your android project. ### Step 2: Include the Module Declare the newly added android library module in your project: ```kotlin title = "settings.gradle.kts" include(":starter-kit") ``` ### Step 3: Add Dependency in Your App Module Include the starter kit module dependency in your app module: ```kotlin title = "app/build.gradle.kts" dependencies { implementation(project(":starter-kit")) } ``` ### Step 4: Include missing plugins Include plugins that may be missing in your project's root `build.gradle.kts` file: ```kotlin title = "build.gradle.kts" plugins { // Existing plugins here id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false } ``` ### Step 5: Add the IMG.LY Maven Repository Add `IMG.LY` maven repository path in your project: ```kotlin title = "settings.gradle.kts" 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") } } } } ``` If the project is open in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` in order to make all dependencies available. ### Step 6: Launch the Editor From Your UI If you use jetpack compose navigation in your app, simply add a new navigation destination and invoke the following composable: ```kotlin highlight-starter-kit-composable import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` > **Delete EditorActivity:** You can delete `starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/EditorActivity.kt` as it is needed only for legacy navigation. If you do not use jetpack compose navigation and use legacy android navigation, the starter kit has a special `EditorActivity` class with full encapsulated logic: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.postcard import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` Simply launch the activity from the activity of your app. Optionally, you can pass parameters to the editor: ```kotlin import android.content.Intent import ly.img.editor.configuration.postcard.EditorActivity fun launchEditor() { // "this" is the current activity val intent = Intent(this, EditorActivity::class.java).also { // Optionally pass parameters for EditorActivity to consume, i.e. your image/video/scene uri it.putExtra("my_param", "my_param_value") } startActivity(intent) } ``` In case you want to consume the parameter in `EditorActivity`: ```kotlin class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val param = intent.getStringExtra("my_param") ?: "default" ... } } ``` The full implementation of the starter kit lives in the `starter-kit/` folder: ```text starter-kit/ ├── build.gradle.kts # Starter kit library module config, includes ly.img:editor dependency └── src/main/ ├── AndroidManifest.xml # Starter kit manifest file, may contain permissions ├── assets/ │ └── scene/ │ └── postcard.scene # Default postcard scene that should be loaded └── kotlin/ly/img/editor/configuration/postcard/ ├── EditorActivity.kt # Encapsulated editor for legacy navigation. Delete if you use jetpack compose navigation ├── PostcardConfigurationBuilder.kt # Editor configuration logic encapsulated in 1 place ├── callback/ │ ├── OnCreate.kt # Editor initialization logic │ ├── OnExport.kt # Export flow and handling │ └── OnLoaded.kt # Post onCreate logic ├── component/ │ ├── CanvasMenu.kt # Canvas Menu component configuration │ ├── Dock.kt # Dock component configuration │ ├── InspectorBar.kt # Inspector Bar component configuration │ ├── NavigationBar.kt # Navigation Bar component configuration │ └── Overlay.kt # Overlay component configuration └── ext └── Engine.kt # Engine extension functions required by the starter kit ``` ## Starter Kit as a Dynamic Feature Since `starter-kit` folder is an android library module, it is possible to turn it into a [dynamic feature](https://developer.android.com/guide/playcore/feature-delivery). This can be helpful if you want to lazy load the editor in order to reduce the download size of your app. See [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) for more details. ## Configuring the Starter Kit The starter kit that we provide contains a very generic structure and behavior, however we understand that every customer wants to configure it according to their needs. The good thing is that the starter kit implementation is part of your codebase and you can configure, add/remove/modify functionality as you wish. In addition, you may want to configure the editor based on your business logic, i.e. restore the scene file from previous edits, display different dock items for different users etc. This example demonstrates on how to pass, store and use external parameters in the starter kit. First, declare a new property to the builder class: ```kotlin ... import android.net.Uri @Stable class PostcardConfigurationBuilder : BasicConfigurationBuilder() { /** * The scene uri that should be loaded in onCreate if not null. * Note that editorContext.mutableStateOf is used to store mutable objects in the editor scope that survive configuration changes. */ var sceneUri: Uri? by editorContext.mutableStateOf( key = "your.package.name.state.sceneUri", initial = null, ) ... } ``` Next, read your external parameter from activity intent extras (or jetpack compose screen arguments) and assign to the property of the builder: ```kotlin import androidx.core.net.toUri class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val sceneUri = intent.getStringExtra("sceneUri")?.toUri() Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) { sceneUri = sceneUri } }, onClose = { // Finish the activity, ignore any errors. finish() }, ) } } ``` Finally, make use of the property. The scene loading logic is located at `OnCreate.kt` file, as part of `onCreate` implementation (see next section for more details). We modify this function to load the `sceneUri` instead if the value is not null: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnCreate.kt" suspend fun PostcardConfigurationBuilder.onCreateScene() { sceneUri?.let { safeSceneUri -> // Load the sceneUri if it's not null getOrLoadScene(sceneUri = safeSceneUri) } ?: run { // Otherwise stick to the default behavior of the starter kit getOrLoadScene(sceneUri = "file:///android_asset/scene/postcard.scene".toUri()) } } ``` ## Set Up a Scene The scene setup logic is located at `OnCreate.kt` file, as part of `onCreate` implementation: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnCreate.kt" highlight-starter-kit-on-create-scene suspend fun PostcardConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/postcard.scene".toUri()) } ``` `getOrCreateSceneFromImage` is a helper that loads an existing scene if available, or initializes one from `file:///android_asset/scene/postcard.scene` when no scene is active. CE.SDK offers multiple ways to load scene into the editor. Choose the method that matches your use case: ```kotlin // Load from an image uri - creates a new scene with the image editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri()) // Load from a template archive - restores a previously saved project editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri()) // Create a blank canvas - starts with an empty design scene editorContext.engine.scene.create() // Load from a scene file - restores a scene from .scene file editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri()) ``` > **More Loading Options:** See [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) for all available loading methods. ## Customize Assets The asset source setup is located in `OnCreate.kt` as part of `onCreate` implementation. Enable or disable individual sources: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnCreate.kt" highlight-starter-kit-on-load-asset-sources suspend fun PostcardConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri // Postcard intentionally registers a custom subset of default sources (no // ly.img.color.palette / ly.img.page.presets — those are provided by the postcard builder). val sourceIds = listOf( "ly.img.sticker", "ly.img.vector.shape", "ly.img.filter", "ly.img.crop.presets", "ly.img.effect", "ly.img.blur", "ly.img.typeface", "ly.img.text", "ly.img.text.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } ``` > **More Asses Sources:** See [Import Media](https://img.ly/docs/cesdk/android/import-media-4e3703/) for all available assets and loading mechanisms. For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for downloading assets, configuring `baseUri` and excluding unused sources to optimize load times. ## Customize Export Functionality Export handling logic is located in `OnExport.kt` as part of `onExport` callback. `onExportByteBuffer` controls what should be exported from the scene. It can be a scene or set of design blocks. For postcard editor, it makes sense to export the scene to a PDF content. `export` is a helper function that calls `editorContext.engine.block.export` under the hood: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnExport.kt" highlight-starter-kit-on-export-byte-buffer suspend fun PostcardConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) ``` > **More Export Options:** See [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) guides for all available export and scene calls. `onPostExport` controls what should happen to the exported `ByteBuffer` content. You can upload the result to your server, save it to the device gallery or simply close the editor via `editorContext.eventHandler.send(EditorEvent.CloseEditor())`. Check `writeToFile`, `shareFile` and `shareUri` helper functions for potential implementations: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/callback/OnExport.kt" highlight-starter-kit-on-post-export suspend fun PostcardConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF) shareFile(file = file, mimeType = MimeType.PDF) } ``` *** ## Customize (Optional) ### Base Uri The starter kit does not make any `baseUri` configuration, which means it points to `https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/assets`. If you want to store them in your own CDN or locally, assets can be accessed via [zip file](https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/imgly-assets.zip). For example, if you want to store them locally, unzip the content and place at `starter-kit/src/main/assets`: ```kotlin highlight-starter-kit-base-uri import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` ### UI Mode CE.SDK supports light and dark ui modes out of the box, plus automatic system preference detection. Switch between themes programmatically: ```kotlin highlight-starter-kit-ui-mode import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` See [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) for more details. ### Native Android Canvas CE.SDK supports rendering on two of the most popular native android views that allow GPU rendering: [TextureView](https://developer.android.com/reference/android/view/TextureView) and [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView): ```kotlin highlight-starter-kit-engine-render-target import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.postcard.PostcardConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::PostcardConfigurationBuilder) }, onClose = onClose, ) } ``` ### Localization See [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) for supported languages, adding support to new languages and replacing existing keys. ### UI Layout All the configurable components are located at `starter-kit/src/main/kotlin/ly/img/editor/configuration/postcard/component`: - `CanvasMenu.kt` - see [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) for full configuration options. - `Dock.kt` - see [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) for full configuration options. - `InspectorBar.kt` - see [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) for full configuration options. - `NavigationBar.kt` - see [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) for full configuration options. - `Overlay.kt` - see [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) for full configuration options. *** ## Troubleshooting > **Free Trial:** [Sign up for a free trial](https://img.ly/forms/free-trial) to get a license key and remove the watermark. ### Editor doesn't load - **Check onCreate**: Ensure `onCreate` callback loads a scene and no coroutine is stuck infinitly - **Verify the baseURL**: Assets must be accessible from the CDN or your self-hosted location - **Check logcat errors**: Look for error in Android logcat ### Assets don't appear - **Check network requests**: Make sure the device/emulator is connected to the internet - **Self-host assets for production**: See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) to host assets on your infrastructure - **Check logcat errors**: Look for error in Android logcat ### Export fails or produces blank images - **Wait for content to load**: Ensure images are fully loaded before exporting - **Check logcat errors**: Look for error in Android logcat ### Watermark appears in production - **Add your license key**: Set the `license` property in your configuration - **Sign up for a trial**: Get a free trial license at [img.ly/forms/free-trial](https://img.ly/forms/free-trial) *** ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) – Complete list of initialization options - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) – Self-host engine assets for production - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) – Customize colors and appearance - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) – Add translations and language support --- ## 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: "T-Shirt Designer in Android" description: "Add the Android Apparel Editor starter kit to a new or existing app with a reusable starter-kit module." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/t-shirt-designer-jwinqr/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [T-Shirt Designer](https://img.ly/docs/cesdk/android/starterkits/t-shirt-designer-jwinqr/) --- ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/app/src/main/kotlin/ly/img/starterkit/example/ExampleBaseUri.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/app/src/main/kotlin/ly/img/starterkit/example/ExampleComposeNavigationScreen.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/app/src/main/kotlin/ly/img/starterkit/example/ExampleRenderTarget.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/app/src/main/kotlin/ly/img/starterkit/example/ExampleUIMode.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/EditorActivity.kt reference-only package ly.img.editor.configuration.apparel import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnCreate.kt reference-only @file:OptIn(UnstableEditorApi::class) package ly.img.editor.configuration.apparel.callback import androidx.core.net.toUri import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.UnstableEditorApi import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.SystemGalleryAssetSource import ly.img.editor.core.library.data.SystemGalleryPermission import ly.img.engine.BlendMode import ly.img.engine.Color import ly.img.engine.DesignBlockType import ly.img.engine.ShapeType import ly.img.engine.StrokeStyle /** * The callback that is invoked when the editor is created. */ suspend fun ApparelConfigurationBuilder.onCreate( preCreateScene: suspend ApparelConfigurationBuilder.() -> Unit = { onPreCreateScene() }, createScene: suspend ApparelConfigurationBuilder.() -> Unit = { onCreateScene() }, loadAssetSources: suspend ApparelConfigurationBuilder.() -> Unit = { onLoadAssetSources() }, postCreateScene: suspend ApparelConfigurationBuilder.() -> Unit = { onPostCreateScene() }, finally: suspend ApparelConfigurationBuilder.() -> Unit = { onCreateFinally() }, ) { try { preCreateScene() createScene() loadAssetSources() postCreateScene() } finally { finally() } } /** * The callback that is invoked before the scene is created. */ fun ApparelConfigurationBuilder.onPreCreateScene() { showLoading = true editorContext.engine.editor.setSettingBoolean( keypath = "ubq://page/dimOutOfPageAreas", value = false, ) } /** * The callback that is responsible for creating the scene. */ suspend fun ApparelConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/apparel.scene".toUri()) } /** * The callback that loads all the required assets sources. */ suspend fun ApparelConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } /** * The callback that is invoked right after [onCreateScene], after the scene is created. */ fun ApparelConfigurationBuilder.onPostCreateScene() { val engine = editorContext.engine val page = requireNotNull(engine.scene.getCurrentPage()) engine.block.setClipped(block = page, clipped = true) engine.block.setBoolean(block = page, property = "fill/enabled", value = false) // Add dotted outline design block. engine.block.findByName(ApparelConfigurationBuilder.Companion.OUTLINE_BLOCK_NAME).firstOrNull() ?: run { val outline = engine.block.create(blockType = DesignBlockType.Graphic) engine.block.setName(block = outline, name = ApparelConfigurationBuilder.Companion.OUTLINE_BLOCK_NAME) engine.block.setShape( block = outline, shape = engine.block.createShape(ShapeType.Rect), ) engine.block.appendChild(parent = page, child = outline) engine.block.setFillEnabled(block = outline, enabled = false) engine.block.setStrokeEnabled(block = outline, enabled = true) engine.block.setStrokeColor( block = outline, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F), ) engine.block.setStrokeStyle(block = outline, style = StrokeStyle.DOTTED) engine.block.setStrokeWidth(block = outline, width = 1.0F) engine.block.setBlendMode(block = outline, blendMode = BlendMode.DIFFERENCE) engine.block.setScopeEnabled( block = outline, key = "editor/select", enabled = false, ) engine.block.setScopeEnabled( block = outline, key = "lifecycle/destroy", enabled = false, ) } } /** * The callback that is invoked as the last step of [onCreate]. * It always runs, no matter success or failure on previous steps. */ fun ApparelConfigurationBuilder.onCreateFinally() { showLoading = false } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-apparel/starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnExport.kt reference-only package ly.img.editor.configuration.apparel.callback import kotlinx.coroutines.CancellationException import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.engine.MimeType import java.nio.ByteBuffer /** * The callback that is invoked when the export button is clicked. */ suspend fun ApparelConfigurationBuilder.onExport( preExport: suspend ApparelConfigurationBuilder.() -> Unit = { onPreExport() }, exportByteBuffer: suspend ApparelConfigurationBuilder.() -> ByteBuffer = { onExportByteBuffer() }, postExport: suspend ApparelConfigurationBuilder.(ByteBuffer) -> Unit = { onPostExport(it) }, error: suspend ApparelConfigurationBuilder.(Exception) -> Unit = { onExportError(it) }, finally: suspend ApparelConfigurationBuilder.() -> Unit = { onExportFinally() }, ) { try { preExport() val result = exportByteBuffer() postExport(result) } catch (exception: Exception) { error(exception) } finally { finally() } } /** * The callback that is invoked before the export is started. */ fun ApparelConfigurationBuilder.onPreExport() { showLoading = true } /** * The callback that exports the content of the editor into [ByteBuffer]. */ suspend fun ApparelConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) /** * The callback that is invoked after [onExportByteBuffer] and handles its output. */ suspend fun ApparelConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF) shareFile(file = file, mimeType = MimeType.PDF) } /** * The callback that is invoked in case any of the export functions throw an exception. */ fun ApparelConfigurationBuilder.onExportError(error: Exception) { if (error is CancellationException) { throw error } this.error = error } /** * The callback that is invoked as the last step of [onExportByteBuffer]. * It always runs, no matter success or failure on previous steps. */ fun ApparelConfigurationBuilder.onExportFinally() { showLoading = false } ``` Professional apparel editing for your Android app. Build print-ready t-shirt designs with a focused mobile UI and a reusable starter-kit architecture on top of CE.SDK. Runs entirely on the mobile device with no server dependencies. ![Apparel Editor starter kit screenshot](../../../../../../cesdk_android_showcases/starter-kits/starter-kit-apparel/screenshot.png) > **Reading time:** 10 minutes > > **Resources:** > > - [Download examples](https://github.com/imgly/starterkit-apparel-editor-android/archive/refs/heads/v$UBQ_VERSION$.zip) > > - [View source on GitHub](https://github.com/imgly/starterkit-apparel-editor-android/tree/v$UBQ_VERSION$) *** ## Pre-Requisites This guide assumes basic familiarity with Android and Kotlin. You will need: - Latest Android Studio - Kotlin: 1.9.10 or later - Gradle: 8.4 or later - Android: 7.0+ (API level 24+) ## Get Started Start with a complete, runnable Android starter kit project. ### Step 1: Clone the Repository ```bash git clone -b v$UBQ_VERSION$ https://github.com/imgly/starterkit-apparel-editor-android.git cd starterkit-apparel-editor-android ``` ### Step 2: Open and Run [Create and launch](https://developer.android.com/studio/run/managing-avds) a new android emulator or use an existing one or connect a physical device with `USB Debugging` on. Open the project in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` and run the `app` module from UI, or use: ```bash ./gradlew app:installDebug ``` The sample app launches `MainActivity` that has "Launch Editor" button. Clicking it launches `EditorActivity`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.apparel import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ## Get Started Integrate only the `starter-kit` library module into your existing Android app. ### Step 1: Run the Extraction Script From Your App Root Run this from your application root directory: ```bash repo="starterkit-apparel-editor-android" version="$UBQ_VERSION$" curl -0 "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit" ``` This extracts the `starter-kit/` library module into your android project. ### Step 2: Include the Module Declare the newly added android library module in your project: ```kotlin title = "settings.gradle.kts" include(":starter-kit") ``` ### Step 3: Add Dependency in Your App Module Include the starter kit module dependency in your app module: ```kotlin title = "app/build.gradle.kts" dependencies { implementation(project(":starter-kit")) } ``` ### Step 4: Include missing plugins Include plugins that may be missing in your project's root `build.gradle.kts` file: ```kotlin title = "build.gradle.kts" plugins { // Existing plugins here id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false } ``` ### Step 5: Add the IMG.LY Maven Repository Add `IMG.LY` maven repository path in your project: ```kotlin title = "settings.gradle.kts" 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") } } } } ``` If the project is open in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` in order to make all dependencies available. ### Step 6: Launch the Editor From Your UI If you use jetpack compose navigation in your app, simply add a new navigation destination and invoke the following composable: ```kotlin highlight-starter-kit-composable import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` > **Delete EditorActivity:** You can delete `starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/EditorActivity.kt` as it is needed only for legacy navigation. If you do not use jetpack compose navigation and use legacy android navigation, the starter kit has a special `EditorActivity` class with full encapsulated logic: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.apparel import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` Simply launch the activity from the activity of your app. Optionally, you can pass parameters to the editor: ```kotlin import android.content.Intent import ly.img.editor.configuration.apparel.EditorActivity fun launchEditor() { // "this" is the current activity val intent = Intent(this, EditorActivity::class.java).also { // Optionally pass parameters for EditorActivity to consume, i.e. your image/video/scene uri it.putExtra("my_param", "my_param_value") } startActivity(intent) } ``` In case you want to consume the parameter in `EditorActivity`: ```kotlin class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val param = intent.getStringExtra("my_param") ?: "default" ... } } ``` The full implementation of the starter kit lives in the `starter-kit/` folder: ```text starter-kit/ ├── build.gradle.kts # Starter kit library module config, includes ly.img:editor dependency └── src/main/ ├── AndroidManifest.xml # Starter kit manifest file, may contain permissions ├── assets/ │ └── scene/ │ └── apparel.scene # Default apparel scene that should be loaded └── kotlin/ly/img/editor/configuration/apparel/ ├── ApparelConfigurationBuilder.kt # Editor configuration logic encapsulated in 1 place ├── EditorActivity.kt # Encapsulated editor for legacy navigation. Delete if you use jetpack compose navigation ├── callback/ │ ├── OnCreate.kt # Editor initialization logic │ ├── OnExport.kt # Export flow and handling │ └── OnLoaded.kt # Post onCreate logic └── component/ ├── CanvasMenu.kt # Canvas Menu component configuration ├── Dock.kt # Dock component configuration ├── InspectorBar.kt # Inspector Bar component configuration ├── NavigationBar.kt # Navigation Bar component configuration └── Overlay.kt # Overlay component configuration ``` ## Starter Kit as a Dynamic Feature Since `starter-kit` folder is an android library module, it is possible to turn it into a [dynamic feature](https://developer.android.com/guide/playcore/feature-delivery). This can be helpful if you want to lazy load the editor in order to reduce the download size of your app. See [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) for more details. ## Configuring the Starter Kit The starter kit that we provide contains a very generic structure and behavior, however we understand that every customer wants to configure it according to their needs. The good thing is that the starter kit implementation is part of your codebase and you can configure, add/remove/modify functionality as you wish. In addition, you may want to configure the editor based on your business logic, i.e. restore the scene file from previous edits, display different dock items for different users etc. This example demonstrates on how to pass, store and use external parameters in the starter kit. First, declare a new property to the builder class: ```kotlin ... import android.net.Uri @Stable class ApparelConfigurationBuilder : BasicConfigurationBuilder() { /** * The scene uri that should be loaded in onCreate if not null. * Note that editorContext.mutableStateOf is used to store mutable objects in the editor scope that survive configuration changes. */ var sceneUri: Uri? by editorContext.mutableStateOf( key = "your.package.name.state.sceneUri", initial = null, ) ... } ``` Next, read your external parameter from activity intent extras (or jetpack compose screen arguments) and assign to the property of the builder: ```kotlin import androidx.core.net.toUri class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val sceneUri = intent.getStringExtra("sceneUri")?.toUri() Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) { sceneUri = sceneUri } }, onClose = { // Finish the activity, ignore any errors. finish() }, ) } } ``` Finally, make use of the property. The scene loading logic is located at `OnCreate.kt` file, as part of `onCreate` implementation (see next section for more details). We modify this function to load the `sceneUri` instead if the value is not null: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnCreate.kt" suspend fun ApparelConfigurationBuilder.onCreateScene() { sceneUri?.let { safeSceneUri -> // Load the sceneUri if it's not null getOrLoadScene(sceneUri = safeSceneUri) } ?: run { // Otherwise stick to the default behavior of the starter kit getOrLoadScene(sceneUri = "file:///android_asset/scene/apparel.scene".toUri()) } } ``` ## Set Up a Scene The scene setup logic is located at `OnCreate.kt` file, as part of `onCreate` implementation: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnCreate.kt" highlight-starter-kit-on-create-scene suspend fun ApparelConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/apparel.scene".toUri()) } ``` `getOrCreateSceneFromImage` is a helper that loads an existing scene if available, or initializes one from `file:///android_asset/scene/apparel.scene` when no scene is active. CE.SDK offers multiple ways to load scene into the editor. Choose the method that matches your use case: ```kotlin // Load from an image uri - creates a new scene with the image editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri()) // Load from a template archive - restores a previously saved project editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri()) // Create a blank canvas - starts with an empty design scene editorContext.engine.scene.create() // Load from a scene file - restores a scene from .scene file editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri()) ``` > **More Loading Options:** See [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) for all available loading methods. ## Customize Assets The asset source setup is located in `OnCreate.kt` as part of `onCreate` implementation. Enable or disable individual sources: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnCreate.kt" highlight-starter-kit-on-load-asset-sources suspend fun ApparelConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } ``` > **More Asses Sources:** See [Import Media](https://img.ly/docs/cesdk/android/import-media-4e3703/) for all available assets and loading mechanisms. For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for downloading assets, configuring `baseUri` and excluding unused sources to optimize load times. ## Customize Export Functionality Export handling logic is located in `OnExport.kt` as part of `onExport` callback. `onExportByteBuffer` controls what should be exported from the scene. It can be a scene or set of design blocks. For t-shirt designer, it makes sense to export the scene to a PDF content. `export` is a helper function that calls `editorContext.engine.block.export` under the hood: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnExport.kt" highlight-starter-kit-on-export-byte-buffer suspend fun ApparelConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) ``` > **More Export Options:** See [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) guides for all available export and scene calls. `onPostExport` controls what should happen to the exported `ByteBuffer` content. You can upload the result to your server, save it to the device gallery or simply close the editor via `editorContext.eventHandler.send(EditorEvent.CloseEditor())`. Check `writeToFile`, `shareFile` and `shareUri` helper functions for potential implementations: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/callback/OnExport.kt" highlight-starter-kit-on-post-export suspend fun ApparelConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF) shareFile(file = file, mimeType = MimeType.PDF) } ``` *** ## Customize (Optional) ### Base Uri The starter kit does not make any `baseUri` configuration, which means it points to `https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/assets`. If you want to store them in your own CDN or locally, assets can be accessed via [zip file](https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/imgly-assets.zip). For example, if you want to store them locally, unzip the content and place at `starter-kit/src/main/assets`: ```kotlin highlight-starter-kit-base-uri import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` ### UI Mode CE.SDK supports light and dark ui modes out of the box, plus automatic system preference detection. Switch between themes programmatically: ```kotlin highlight-starter-kit-ui-mode import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` See [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) for more details. ### Native Android Canvas CE.SDK supports rendering on two of the most popular native android views that allow GPU rendering: [TextureView](https://developer.android.com/reference/android/view/TextureView) and [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView): ```kotlin highlight-starter-kit-engine-render-target import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.apparel.ApparelConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::ApparelConfigurationBuilder) }, onClose = onClose, ) } ``` ### Localization See [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) for supported languages, adding support to new languages and replacing existing keys. ### UI Layout All the configurable components are located at `starter-kit/src/main/kotlin/ly/img/editor/configuration/apparel/component`: - `CanvasMenu.kt` - see [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) for full configuration options. - `Dock.kt` - see [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) for full configuration options. - `InspectorBar.kt` - see [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) for full configuration options. - `NavigationBar.kt` - see [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) for full configuration options. - `Overlay.kt` - see [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) for full configuration options. *** ## Troubleshooting > **Free Trial:** [Sign up for a free trial](https://img.ly/forms/free-trial) to get a license key and remove the watermark. ### Editor doesn't load - **Check onCreate**: Ensure `onCreate` callback loads a scene and no coroutine is stuck infinitly - **Verify the baseURL**: Assets must be accessible from the CDN or your self-hosted location - **Check logcat errors**: Look for error in Android logcat ### Assets don't appear - **Check network requests**: Make sure the device/emulator is connected to the internet - **Self-host assets for production**: See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) to host assets on your infrastructure - **Check logcat errors**: Look for error in Android logcat ### Export fails or produces blank images - **Wait for content to load**: Ensure images are fully loaded before exporting - **Check logcat errors**: Look for error in Android logcat ### Watermark appears in production - **Add your license key**: Set the `license` property in your configuration - **Sign up for a trial**: Get a free trial license at [img.ly/forms/free-trial](https://img.ly/forms/free-trial) *** ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) – Complete list of initialization options - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) – Self-host engine assets for production - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) – Customize colors and appearance - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) – Add translations and language support --- ## 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 Editor in Android" description: "Add the Android Video Editor starter kit to a new or existing app with a reusable starter-kit module." platform: android url: "https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/" --- > 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:** [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) > [Video Editor](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) --- ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/app/src/main/kotlin/ly/img/starterkit/example/ExampleBaseUri.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/app/src/main/kotlin/ly/img/starterkit/example/ExampleComposeNavigationScreen.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/app/src/main/kotlin/ly/img/starterkit/example/ExampleRenderTarget.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/app/src/main/kotlin/ly/img/starterkit/example/ExampleUIMode.kt reference-only package ly.img.starterkit.example import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/app/src/main/kotlin/ly/img/starterkit/example/ExampleVideoDurationConstraints.kt reference-only package ly.img.starterkit.example import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.event.EditorEvent import kotlin.time.Duration.Companion.seconds fun VideoConfigurationBuilder.onLoaded() { // Enforce all videos to be between 10 and 20 seconds val event = EditorEvent.ApplyVideoDurationConstraints( minDuration = 10.seconds, maxDuration = 20.seconds, ) editorContext.eventHandler.send(event) // Existing body here } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/starter-kit/src/main/kotlin/ly/img/editor/configuration/video/EditorActivity.kt reference-only package ly.img.editor.configuration.video import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnCreate.kt reference-only @file:OptIn(UnstableEditorApi::class) @file:Suppress("UnusedReceiverParameter") package ly.img.editor.configuration.video.callback import androidx.core.net.toUri import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.UnstableEditorApi import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.SystemGalleryAssetSource import ly.img.editor.core.library.data.SystemGalleryPermission import ly.img.engine.Color /** * The callback that is invoked when the editor is created. */ suspend fun VideoConfigurationBuilder.onCreate( preCreateScene: suspend VideoConfigurationBuilder.() -> Unit = { onPreCreateScene() }, createScene: suspend VideoConfigurationBuilder.() -> Unit = { onCreateScene() }, loadAssetSources: suspend VideoConfigurationBuilder.() -> Unit = { onLoadAssetSources() }, postCreateScene: suspend VideoConfigurationBuilder.() -> Unit = { onPostCreateScene() }, finally: suspend VideoConfigurationBuilder.() -> Unit = { onCreateFinally() }, ) { try { preCreateScene() createScene() loadAssetSources() postCreateScene() } finally { finally() } } /** * The callback that is invoked before the scene is created. */ fun VideoConfigurationBuilder.onPreCreateScene() { showLoading = true val engine = editorContext.engine engine.editor.setSettingEnum(keypath = "touch/pinchAction", value = "Scale") engine.editor.setSettingBoolean( keypath = "controlGizmo/showRotateHandles", value = false, ) engine.editor.setSettingBoolean( keypath = "controlGizmo/showScaleHandles", value = false, ) engine.editor.setSettingBoolean(keypath = "controlGizmo/showMoveHandles", value = false) engine.editor.setSettingBoolean(keypath = "touch/singlePointPanning", value = false) engine.editor.setSettingColor( keypath = "page/innerBorderColor", value = Color.fromRGBA(0.67f, 0.67f, 0.67f, 0.5f), ) } /** * The callback that is responsible for creating the scene. */ suspend fun VideoConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/video.scene".toUri()) } /** * The callback that loads all the required assets sources. */ suspend fun VideoConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.caption.presets", "ly.img.image", "ly.img.audio", "ly.img.video", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) editorContext.engine.asset.addLocalSource( sourceId = "ly.img.audio.upload", supportedMimeTypes = listOf( "audio/x-m4a", "audio/mp3", "audio/mpeg", ), ) editorContext.engine.asset.addLocalSource( sourceId = "ly.img.video.upload", supportedMimeTypes = listOf( "video/mp4", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } /** * The callback that is invoked right after [onCreateScene], after the scene is created. */ fun VideoConfigurationBuilder.onPostCreateScene() { // Do nothing } /** * The callback that is invoked as the last step of [onCreate]. * It always runs, no matter success or failure on previous steps. */ fun VideoConfigurationBuilder.onCreateFinally() { showLoading = false } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnExport.kt reference-only package ly.img.editor.configuration.video.callback import kotlinx.coroutines.CancellationException import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.configuration.video.model.ExportStatus import ly.img.engine.ExportVideoOptions import ly.img.engine.MimeType import ly.img.engine.VideoBitrate import java.nio.ByteBuffer /** * The callback that is invoked when the export button is clicked. */ suspend fun VideoConfigurationBuilder.onExport( preExport: suspend VideoConfigurationBuilder.() -> Unit = { onPreExport() }, exportByteBuffer: suspend VideoConfigurationBuilder.() -> ByteBuffer = { onExportByteBuffer() }, postExport: suspend VideoConfigurationBuilder.(ByteBuffer) -> Unit = { onPostExport(it) }, error: suspend VideoConfigurationBuilder.(Exception) -> Unit = { onExportError(it) }, finally: suspend VideoConfigurationBuilder.() -> Unit = { onExportFinally() }, ) { try { preExport() val result = exportByteBuffer() postExport(result) } catch (exception: Exception) { error(exception) } finally { finally() } } /** * The callback that is invoked before the export is started. */ fun VideoConfigurationBuilder.onPreExport() { exportStatus = ExportStatus.Loading(progress = 0F) } /** * The callback that exports the content of the editor into [ByteBuffer]. */ suspend fun VideoConfigurationBuilder.onExportByteBuffer(): ByteBuffer { val targetDesignBlock = requireNotNull(editorContext.engine.scene.getCurrentPage()) return editorContext.engine.block.exportVideo( block = targetDesignBlock, timeOffset = 0.0, duration = editorContext.engine.block.getDuration(targetDesignBlock), mimeType = MimeType.MP4, progressCallback = { progress -> val lastExportStatus = exportStatus val newProgress = progress.encodedFrames.toFloat() / progress.totalFrames // Update export UI whenever the progress changes if (lastExportStatus !is ExportStatus.Loading || newProgress >= lastExportStatus.progress + 0.01F) { exportStatus = ExportStatus.Loading(progress = newProgress) } }, // videoBitrate: VideoBitrate.Auto derives a bounded bitrate from the resolution/framerate. options = ExportVideoOptions(videoBitrate = VideoBitrate.Auto), ) } /** * The callback that is invoked after [onExportByteBuffer] and handles its output. */ suspend fun VideoConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.MP4) exportStatus = ExportStatus.Success(file = file, mimeType = MimeType.MP4) } /** * The callback that is invoked in case any of the export functions throw an exception. */ fun VideoConfigurationBuilder.onExportError(error: Exception) { if (error is CancellationException) { exportStatus = null throw error } else { exportStatus = ExportStatus.Error(exception = error) } } /** * The callback that is invoked as the last step of [onExportByteBuffer]. * It always runs, no matter success or failure on previous steps. */ fun VideoConfigurationBuilder.onExportFinally() { showCancelExportConfirmationDialog = false } ``` ```kotlin file=@cesdk_android_examples/../cesdk_android_showcases/starter-kits/starter-kit-video/starter-kit/src/main/kotlin/ly/img/editor/configuration/video/component/Dock.kt reference-only @file:Suppress("UnusedReceiverParameter", "UnusedFlow") @file:OptIn(ExperimentalCoroutinesApi::class) package ly.img.editor.configuration.video.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import kotlinx.coroutines.ExperimentalCoroutinesApi import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorTrigger import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberAudiosLibrary import ly.img.editor.core.component.rememberCaptions import ly.img.editor.core.component.rememberOverlaysLibrary import ly.img.editor.core.component.rememberResizeAll import ly.img.editor.core.component.rememberStickersAndShapesLibrary import ly.img.editor.core.component.rememberSystemCamera import ly.img.editor.core.component.rememberSystemGallery import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.component.rememberVoiceoverRecord import ly.img.editor.core.iconpack.AddCameraBackground import ly.img.editor.core.iconpack.AddGalleryBackground import ly.img.editor.core.iconpack.IconPack /** * The configuration of the component that is displayed as horizontal list of items at the bottom of the editor. */ @Composable fun VideoConfigurationBuilder.rememberDock() = Dock.remember { scope = { val activeSceneTrigger by EditorTrigger.remember { editorContext.engine.scene.onActiveChanged() } // Update Dock whenever the active scene changes. remember(this, activeSceneTrigger) { Dock.Scope(parentScope = this) } } horizontalArrangement = { // All buttons should have equal size Arrangement.SpaceEvenly } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery { vectorIcon = { IconPack.AddGalleryBackground } } } add { // As an alternative to the system camera we also provide our own camera tech accessible via [Dock.Button.rememberImglyCamera]. // In order to make it work the following dependency is required: // implementation "ly.img:camera:". Dock.Button.rememberSystemCamera(captureVideo = { true }) { vectorIcon = { IconPack.AddCameraBackground } } } add { Dock.Button.rememberOverlaysLibrary() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberStickersAndShapesLibrary() } add { Dock.Button.rememberCaptions() } add { Dock.Button.rememberAudiosLibrary() } add { Dock.Button.rememberVoiceoverRecord() } add { Dock.Button.rememberResizeAll() } } } } ``` Professional video editing for your Android app—edit clips, add effects, trim footage, and export to MP4. Runs entirely on the mobile device with no server dependencies. ![Video Editor starter kit screenshot](../../../../../../cesdk_android_showcases/starter-kits/starter-kit-video/screenshot.png) > **Reading time:** 10 minutes > > **Resources:** > > - [Download examples](https://github.com/imgly/starterkit-video-editor-android/archive/refs/heads/v$UBQ_VERSION$.zip) > > - [View source on GitHub](https://github.com/imgly/starterkit-video-editor-android/tree/v$UBQ_VERSION$) *** ## Pre-Requisites This guide assumes basic familiarity with Android and Kotlin. You will need: - Latest Android Studio - Kotlin: 1.9.10 or later - Gradle: 8.4 or later - Android: 7.0+ (API level 24+) ## Get Started Start with a complete, runnable Android starter kit project. ### Step 1: Clone the Repository ```bash git clone -b v$UBQ_VERSION$ https://github.com/imgly/starterkit-video-editor-android.git cd starterkit-video-editor-android ``` ### Step 2: Open and Run [Create and launch](https://developer.android.com/studio/run/managing-avds) a new android emulator or use an existing one or connect a physical device with `USB Debugging` on. Open the project in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` and run the `app` module from UI, or use: ```bash ./gradlew app:installDebug ``` The sample app launches `MainActivity` that has "Launch Editor" button. Clicking it launches `EditorActivity`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.video import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` ## Get Started Integrate only the `starter-kit` library module into your existing Android app. ### Step 1: Run the Extraction Script From Your App Root Run this from your application root directory: ```bash repo="starterkit-video-editor-android" version="$UBQ_VERSION$" curl -0 "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit" ``` This extracts the `starter-kit/` library module into your android project. ### Step 2: Include the Module Declare the newly added android library module in your project: ```kotlin title = "settings.gradle.kts" include(":starter-kit") ``` ### Step 3: Add Dependency in Your App Module Include the starter kit module dependency in your app module: ```kotlin title = "app/build.gradle.kts" dependencies { implementation(project(":starter-kit")) } ``` ### Step 4: Include missing plugins Include plugins that may be missing in your project's root `build.gradle.kts` file: ```kotlin title = "build.gradle.kts" plugins { // Existing plugins here id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false } ``` ### Step 5: Add the IMG.LY Maven Repository Add `IMG.LY` maven repository path in your project: ```kotlin title = "settings.gradle.kts" 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") } } } } ``` If the project is open in Android Studio, sync gradle via `File -> Sync Project With Gradle Files` in order to make all dependencies available. ### Step 6: Launch the Editor From Your UI If you use jetpack compose navigation in your app, simply add a new navigation destination and invoke the following composable: ```kotlin highlight-starter-kit-composable import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // onClose should pop screen from the backstack of jetpack compose navigation @Composable fun EditorScreen(onClose: (Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` > **Delete EditorActivity:** You can delete `starter-kit/src/main/kotlin/ly/img/editor/configuration/video/EditorActivity.kt` as it is needed only for legacy navigation. If you do not use jetpack compose navigation and use legacy android navigation, the starter kit has a special `EditorActivity` class with full encapsulated logic: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/EditorActivity.kt" highlight-starter-kit-activity-full package ly.img.editor.configuration.video import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember /** * Encapsulated editor to be used in legacy activity navigation. * Delete this file if you are using jetpack compose navigation. */ class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This is required to remove the default action bar on top. setTheme(android.R.style.Theme_Material_Light_NoActionBar) // This is required, so that the editor is displayed full screen on relatively older devices. enableEdgeToEdge() setContent { Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = { // Finish the activity, potentially handle errors. finish() }, ) } } } ``` Simply launch the activity from the activity of your app. Optionally, you can pass parameters to the editor: ```kotlin import android.content.Intent import ly.img.editor.configuration.video.EditorActivity fun launchEditor() { // "this" is the current activity val intent = Intent(this, EditorActivity::class.java).also { // Optionally pass parameters for EditorActivity to consume, i.e. your image/video/scene uri it.putExtra("my_param", "my_param_value") } startActivity(intent) } ``` In case you want to consume the parameter in `EditorActivity`: ```kotlin class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val param = intent.getStringExtra("my_param") ?: "default" ... } } ``` The full implementation of the starter kit lives in the `starter-kit/` folder: ```text starter-kit/ ├── build.gradle.kts # Starter kit library module config, includes ly.img:editor dependency └── src/main/ ├── AndroidManifest.xml # Starter kit manifest file, may contain permissions ├── assets/ │ └── scene/ │ └── video.scene # Default video scene that should be loaded └── kotlin/ly/img/editor/configuration/video/ ├── EditorActivity.kt # Encapsulated editor for legacy navigation. Delete if you use jetpack compose navigation ├── VideoConfigurationBuilder.kt # Editor configuration logic encapsulated in 1 place ├── callback/ │ ├── OnCreate.kt # Editor initialization logic │ ├── OnExport.kt # Export flow and handling │ └── OnLoaded.kt # Post onCreate logic ├── component/ │ ├── BottomPanel.kt # Bottom Panel component configuration │ ├── CanvasMenu.kt # Canvas Menu component configuration │ ├── Dock.kt # Dock component configuration │ ├── InspectorBar.kt # Inspector Bar component configuration │ ├── NavigationBar.kt # Navigation Bar component configuration │ └── Overlay.kt # Overlay component configuration ├── iconPack/ │ ├── CheckCircleOutline.kt # Check-circle with outline icon │ ├── ErrorOutline.kt # Error with outline icon │ └── IconPack.kt # Icon pack of the starter kit to access all the icons └── model/ └── ExportStatus.kt # Export status model used by the overlay ``` ## Starter Kit as a Dynamic Feature Since `starter-kit` folder is an android library module, it is possible to turn it into a [dynamic feature](https://developer.android.com/guide/playcore/feature-delivery). This can be helpful if you want to lazy load the editor in order to reduce the download size of your app. See [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) for more details. ## Configuring the Starter Kit The starter kit that we provide contains a very generic structure and behavior, however we understand that every customer wants to configure it according to their needs. The good thing is that the starter kit implementation is part of your codebase and you can configure, add/remove/modify functionality as you wish. In addition, you may want to configure the editor based on your business logic, i.e. restore the scene file from previous edits, display different dock items for different users etc. This example demonstrates on how to pass, store and use external parameters in the starter kit. First, declare a new property to the builder class: ```kotlin ... import android.net.Uri @Stable class VideoConfigurationBuilder : BasicConfigurationBuilder() { /** * The scene uri that should be loaded in onCreate if not null. * Note that editorContext.mutableStateOf is used to store mutable objects in the editor scope that survive configuration changes. */ var sceneUri: Uri? by editorContext.mutableStateOf( key = "your.package.name.state.sceneUri", initial = null, ) ... } ``` Next, read your external parameter from activity intent extras (or jetpack compose screen arguments) and assign to the property of the builder: ```kotlin import androidx.core.net.toUri class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val sceneUri = intent.getStringExtra("sceneUri")?.toUri() Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) { sceneUri = sceneUri } }, onClose = { // Finish the activity, ignore any errors. finish() }, ) } } ``` Finally, make use of the property. The scene loading logic is located at `OnCreate.kt` file, as part of `onCreate` implementation (see next section for more details). We modify this function to load the `sceneUri` instead if the value is not null: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnCreate.kt" suspend fun VideoConfigurationBuilder.onCreateScene() { sceneUri?.let { safeSceneUri -> // Load the sceneUri if it's not null getOrLoadScene(sceneUri = safeSceneUri) } ?: run { // Otherwise stick to the default behavior of the starter kit getOrLoadScene(sceneUri = "file:///android_asset/scene/video.scene".toUri()) } } ``` ## Set Up a Scene The scene setup logic is located at `OnCreate.kt` file, as part of `onCreate` implementation: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnCreate.kt" highlight-starter-kit-on-create-scene suspend fun VideoConfigurationBuilder.onCreateScene() { getOrLoadScene(sceneUri = "file:///android_asset/scene/video.scene".toUri()) } ``` `getOrLoadScene` is a helper that loads an existing scene if available, or initializes one from `file:///android_asset/scene/video.scene` when no scene is active. CE.SDK offers multiple ways to load scene into the editor. Choose the method that matches your use case: ```kotlin // Load from an image uri - creates a new scene with the image editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri()) // Load from a video uri - creates a new scene with the video editorContext.engine.scene.createFromVideo(videoUri = "https://example.com/video.mp4".toUri()) // Load from a template archive - restores a previously saved project editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri()) // Create a blank canvas - starts with an empty video scene editorContext.engine.scene.createForVideo() // Load from a scene file - restores a scene from .scene file editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri()) ``` > **More Loading Options:** See [Open the Editor](https://img.ly/docs/cesdk/android/open-the-editor-23a1db/) for all available loading methods. ## Video Duration Constraints Video duration constraints can be configured in the starter kit examples to enforce minimum and maximum clip durations. Send `EditorEvent.ApplyVideoDurationConstraints` event before or after the existing logic of `onLoaded`: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnLoaded.kt" highlight-starter-kit-constraints import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.event.EditorEvent import kotlin.time.Duration.Companion.seconds fun VideoConfigurationBuilder.onLoaded() { // Enforce all videos to be between 10 and 20 seconds val event = EditorEvent.ApplyVideoDurationConstraints( minDuration = 10.seconds, maxDuration = 20.seconds, ) editorContext.eventHandler.send(event) // Existing body here } ``` ## Enable IMG.LY Camera Instead of the system camera it is possible to use the camera feature provided by IMG.LY: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/component/Dock.kt" highlight-starter-kit-imgly-camera Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery { vectorIcon = { IconPack.AddGalleryBackground } } } add { // As an alternative to the system camera we also provide our own camera tech accessible via [Dock.Button.rememberImglyCamera]. // In order to make it work the following dependency is required: // implementation "ly.img:camera:". Dock.Button.rememberSystemCamera(captureVideo = { true }) { vectorIcon = { IconPack.AddCameraBackground } } } add { Dock.Button.rememberOverlaysLibrary() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberStickersAndShapesLibrary() } add { Dock.Button.rememberCaptions() } add { Dock.Button.rememberAudiosLibrary() } add { Dock.Button.rememberVoiceoverRecord() } add { Dock.Button.rememberResizeAll() } } ``` `Dock.Button.rememberSystemCamera` should be replaced by `Dock.Button.rememberImglyCamera`. In addition, IMG.LY camera dependency should be added: ```kotlin title = "starter-kit/build.gradle.kts" implementation "ly.img:camera:$UBQ_VERSION$" ``` > **IMG.LY Camera Version:** Note that the version of the camera dependency must match with the editor version in order to avoid interoperability issues. ## Customize Assets The asset source setup is located in `OnCreate.kt` as part of `onCreate` implementation. Enable or disable individual sources: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnCreate.kt" highlight-starter-kit-on-load-asset-sources suspend fun VideoConfigurationBuilder.onLoadAssetSources() { // Load asset sources in parallel from content.json files coroutineScope { val baseUri = editorContext.baseUri val sourceIds = 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.caption.presets", "ly.img.image", "ly.img.audio", "ly.img.video", ) sourceIds.forEach { id -> launch { editorContext.engine.asset.addLocalSourceFromJSON( contentUri = "$baseUri/$id/content.json".toUri(), ) } } } // Load local asset sources editorContext.engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf( "image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp", ), ) editorContext.engine.asset.addLocalSource( sourceId = "ly.img.audio.upload", supportedMimeTypes = listOf( "audio/x-m4a", "audio/mp3", "audio/mpeg", ), ) editorContext.engine.asset.addLocalSource( sourceId = "ly.img.video.upload", supportedMimeTypes = listOf( "video/mp4", ), ) // Register gallery asset sources listOf( AssetSourceType.GalleryAllVisuals, AssetSourceType.GalleryImage, AssetSourceType.GalleryVideo, ).forEach { type -> editorContext.engine.asset.addSource( source = SystemGalleryAssetSource( context = editorContext.engine.applicationContext, type = type, ), ) } SystemGalleryPermission.setMode(systemGalleryConfiguration) } ``` > **More Asses Sources:** See [Import Media](https://img.ly/docs/cesdk/android/import-media-4e3703/) for all available assets and loading mechanisms. For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for downloading assets, configuring `baseUri` and excluding unused sources to optimize load times. ## Customize Export Functionality Export handling logic is located in `OnExport.kt` as part of `onExport` callback. `onExportByteBuffer` controls what should be exported from the scene. It can be a scene or set of design blocks. For video editor, it makes sense to export the scene to an MP4 content: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnExport.kt" highlight-starter-kit-on-export-byte-buffer suspend fun VideoConfigurationBuilder.onExportByteBuffer(): ByteBuffer { val targetDesignBlock = requireNotNull(editorContext.engine.scene.getCurrentPage()) return editorContext.engine.block.exportVideo( block = targetDesignBlock, timeOffset = 0.0, duration = editorContext.engine.block.getDuration(targetDesignBlock), mimeType = MimeType.MP4, progressCallback = { progress -> val lastExportStatus = exportStatus val newProgress = progress.encodedFrames.toFloat() / progress.totalFrames // Update export UI whenever the progress changes if (lastExportStatus !is ExportStatus.Loading || newProgress >= lastExportStatus.progress + 0.01F) { exportStatus = ExportStatus.Loading(progress = newProgress) } }, // videoBitrate: VideoBitrate.Auto derives a bounded bitrate from the resolution/framerate. options = ExportVideoOptions(videoBitrate = VideoBitrate.Auto), ) } ``` > **More Export Options:** See [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and [Save](https://img.ly/docs/cesdk/android/export-save-publish/save-c8b124/) guides for all available export and scene calls. `onPostExport` controls what should happen to the exported `ByteBuffer` content. You can upload the result to your server, save it to the device gallery or simply close the editor via `editorContext.eventHandler.send(EditorEvent.CloseEditor())`. Check `writeToFile`, `shareFile` and `shareUri` helper functions for potential implementations: ```kotlin title = "starter-kit/src/main/kotlin/ly/img/editor/configuration/video/callback/OnExport.kt" highlight-starter-kit-on-post-export suspend fun VideoConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) { val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.MP4) exportStatus = ExportStatus.Success(file = file, mimeType = MimeType.MP4) } ``` *** ## Customize (Optional) ### Base Uri The starter kit does not make any `baseUri` configuration, which means it points to `https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/assets`. If you want to store them in your own CDN or locally, assets can be accessed via [zip file](https://cdn.img.ly/packages/imgly/cesdk-engine/$UBQ_VERSION$/imgly-assets.zip). For example, if you want to store them locally, unzip the content and place at `starter-kit/src/main/assets`: ```kotlin highlight-starter-kit-base-uri import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark baseUri = "file:///android_asset".toUri(), // this points to android assets configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` ### UI Mode CE.SDK supports light and dark ui modes out of the box, plus automatic system preference detection. Switch between themes programmatically: ```kotlin highlight-starter-kit-ui-mode import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` See [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) for more details. ### Native Android Canvas CE.SDK supports rendering on two of the most popular native android views that allow GPU rendering: [TextureView](https://developer.android.com/reference/android/view/TextureView) and [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView): ```kotlin highlight-starter-kit-engine-render-target import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.engine.EngineRenderTarget @Composable fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) { Editor( license = null, // pass null or empty for evaluation mode with watermark engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) }, onClose = onClose, ) } ``` ### Localization See [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) for supported languages, adding support to new languages and replacing existing keys. ### UI Layout All the configurable components are located at `starter-kit/src/main/kotlin/ly/img/editor/configuration/video/component`: - `BottomPanel.kt` - `Timeline` component is added here. - `CanvasMenu.kt` - see [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) for full configuration options. - `Dock.kt` - see [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) for full configuration options. - `InspectorBar.kt` - see [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) for full configuration options. - `NavigationBar.kt` - see [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) for full configuration options. - `Overlay.kt` - see [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) for full configuration options. Video export content is rendered here via `VideoOverlay` composable function. *** ## Troubleshooting > **Free Trial:** [Sign up for a free trial](https://img.ly/forms/free-trial) to get a license key and remove the watermark. ### Editor doesn't load - **Check onCreate**: Ensure `onCreate` callback loads a scene and no coroutine is stuck infinitly - **Verify the baseURL**: Assets must be accessible from the CDN or your self-hosted location - **Check logcat errors**: Look for error in Android logcat ### Assets don't appear - **Check network requests**: Make sure the device/emulator is connected to the internet - **Self-host assets for production**: See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) to host assets on your infrastructure - **Check logcat errors**: Look for error in Android logcat ### Export fails or produces blank images - **Wait for content to load**: Ensure images are fully loaded before exporting - **Check logcat errors**: Look for error in Android logcat ### Watermark appears in production - **Add your license key**: Set the `license` property in your configuration - **Sign up for a trial**: Get a free trial license at [img.ly/forms/free-trial](https://img.ly/forms/free-trial) *** ## Next Steps - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) – Complete list of initialization options - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) – Self-host engine assets for production - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) – Customize colors and appearance - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) – Add translations and language support --- ## 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 and Edit Stickers" description: "Create and customize stickers using image fills for icons, logos, emoji, and multi-color graphics." platform: android url: "https://img.ly/docs/cesdk/android/stickers-3d4e5f/" --- > 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 Stickers](https://img.ly/docs/cesdk/android/stickers-3d4e5f/) --- --- ## Related Pages - [Create Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-stickers-cc46e5/) - Create stickers in CE.SDK using image fills for icons, logos, emoji, and multi-color graphics. - [Edit Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-stickers-609679/) - Modify stickers programmatically by swapping the image, transforming the block, changing appearance, and duplicating it. - [Create Cutout](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3/) - Create cutout paths for cutting printers to produce die-cut stickers, iron-on decals, and custom-shaped prints. --- ## 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: "Combine Shapes" description: "Combine multiple shapes using boolean operations to create custom compound designs." platform: android url: "https://img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26/" --- > 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 Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) > [Combine Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26/) --- ```kotlin file=@cesdk_android_examples/engine-guides-bool-ops/BoolOps.kt reference-only import ly.img.engine.BooleanOperation 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 combineShapes(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 circle1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle1, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle1, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = circle1, color = Color.fromRGBA(r = 1F, g = 0.4F, b = 0.4F, a = 1F), ) engine.block.setPositionX(circle1, value = 90F) engine.block.setPositionY(circle1, value = 70F) engine.block.setWidth(circle1, value = 96F) engine.block.setHeight(circle1, value = 96F) engine.block.appendChild(parent = page, child = circle1) val circle2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle2, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle2, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = circle2, color = Color.fromRGBA(r = 0.4F, g = 1F, b = 0.4F, a = 1F), ) engine.block.setPositionX(circle2, value = 162F) engine.block.setPositionY(circle2, value = 70F) engine.block.setWidth(circle2, value = 96F) engine.block.setHeight(circle2, value = 96F) engine.block.appendChild(parent = page, child = circle2) val circle3 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle3, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle3, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = circle3, color = Color.fromRGBA(r = 0.4F, g = 0.4F, b = 1F, a = 1F), ) engine.block.setPositionX(circle3, value = 126F) engine.block.setPositionY(circle3, value = 130F) engine.block.setWidth(circle3, value = 120F) engine.block.setHeight(circle3, value = 120F) engine.block.appendChild(parent = page, child = circle3) val unionBlocks = listOf(circle1, circle2, circle3) val canCombineUnion = engine.block.isCombinable(unionBlocks) check(canCombineUnion) { "Choose graphic or text blocks that can be combined." } val unionResult = engine.block.combine( blocks = unionBlocks, op = BooleanOperation.UNION, ) check(engine.block.isValid(unionResult)) 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(block = imageBlock, fill = imageFill) engine.block.setPositionX(imageBlock, value = 450F) engine.block.setPositionY(imageBlock, value = 70F) engine.block.setWidth(imageBlock, value = 260F) engine.block.setHeight(imageBlock, value = 170F) engine.block.appendChild(parent = page, child = imageBlock) val textBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = textBlock, text = "CUTOUT") engine.block.setTextFontSize(block = textBlock, fontSize = 88F) engine.block.setPositionX(textBlock, value = 468F) engine.block.setPositionY(textBlock, value = 115F) engine.block.setWidth(textBlock, value = 224F) engine.block.setHeight(textBlock, value = 80F) engine.block.appendChild(parent = page, child = textBlock) engine.block.sendToBack(imageBlock) // Load the image fill and text font resources before combining. engine.block.forceLoadResources(listOf(imageBlock, textBlock)) val differenceBlocks = listOf(imageBlock, textBlock) check(engine.block.isCombinable(differenceBlocks)) val differenceResult = engine.block.combine( blocks = differenceBlocks, op = BooleanOperation.DIFFERENCE, ) check(engine.block.isValid(differenceResult)) val intersectionCircle1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape( intersectionCircle1, shape = engine.block.createShape(ShapeType.Ellipse), ) engine.block.setFill(intersectionCircle1, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = intersectionCircle1, color = Color.fromRGBA(r = 1F, g = 0.55F, b = 0.12F, a = 1F), ) engine.block.setPositionX(intersectionCircle1, value = 130F) engine.block.setPositionY(intersectionCircle1, value = 385F) engine.block.setWidth(intersectionCircle1, value = 150F) engine.block.setHeight(intersectionCircle1, value = 150F) engine.block.appendChild(parent = page, child = intersectionCircle1) val intersectionCircle2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape( intersectionCircle2, shape = engine.block.createShape(ShapeType.Ellipse), ) engine.block.setFill(intersectionCircle2, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = intersectionCircle2, color = Color.fromRGBA(r = 0.12F, g = 0.74F, b = 0.92F, a = 1F), ) engine.block.setPositionX(intersectionCircle2, value = 220F) engine.block.setPositionY(intersectionCircle2, value = 385F) engine.block.setWidth(intersectionCircle2, value = 150F) engine.block.setHeight(intersectionCircle2, value = 150F) engine.block.appendChild(parent = page, child = intersectionCircle2) val intersectionBlocks = listOf(intersectionCircle1, intersectionCircle2) check(engine.block.isCombinable(intersectionBlocks)) val intersectionResult = engine.block.combine( blocks = intersectionBlocks, op = BooleanOperation.INTERSECTION, ) check(engine.block.isValid(intersectionResult)) val xorCircle1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(xorCircle1, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(xorCircle1, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = xorCircle1, color = Color.fromRGBA(r = 0.12F, g = 0.74F, b = 0.92F, a = 1F), ) engine.block.setPositionX(xorCircle1, value = 470F) engine.block.setPositionY(xorCircle1, value = 380F) engine.block.setWidth(xorCircle1, value = 150F) engine.block.setHeight(xorCircle1, value = 150F) engine.block.appendChild(parent = page, child = xorCircle1) val xorCircle2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(xorCircle2, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(xorCircle2, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = xorCircle2, color = Color.fromRGBA(r = 0.12F, g = 0.74F, b = 0.92F, a = 1F), ) engine.block.setPositionX(xorCircle2, value = 560F) engine.block.setPositionY(xorCircle2, value = 380F) engine.block.setWidth(xorCircle2, value = 150F) engine.block.setHeight(xorCircle2, value = 150F) engine.block.appendChild(parent = page, child = xorCircle2) val xorBlocks = listOf(xorCircle1, xorCircle2) check(engine.block.isCombinable(xorBlocks)) val xorResult = engine.block.combine( blocks = xorBlocks, op = BooleanOperation.XOR, ) check(engine.block.isValid(xorResult)) } ``` Combine multiple shapes using boolean operations to create custom compound designs programmatically. ![Boolean operations preview showing Union, Difference, Intersection, and XOR results](./assets/android.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-bool-ops) CE.SDK provides four boolean operations for graphic and text blocks: Union, Difference, Intersection, and XOR. Use them to merge simple primitives, create cutouts, isolate overlapping areas, or remove overlaps from a compound shape. This guide covers checking combinability, applying the four operations with Android's `BooleanOperation` enum, controlling fill inheritance through block order, and avoiding common scope issues. ## Understanding Boolean Operations Boolean operations create a new block from the geometry of multiple input blocks. The result replaces the input blocks when the required scopes allow CE.SDK to duplicate and destroy them. | Operation | Android enum | Result | | --- | --- | --- | | Union | `BooleanOperation.UNION` | Merges all block areas into one compound shape | | Difference | `BooleanOperation.DIFFERENCE` | Subtracts upper blocks from the bottom-most base block | | Intersection | `BooleanOperation.INTERSECTION` | Keeps only the areas where all blocks overlap | | XOR | `BooleanOperation.XOR` | Keeps non-overlapping areas and removes intersections | For Union and XOR, the new block inherits the fill from the top-most block. For Difference and Intersection, it inherits the fill from the bottom-most block. The operation order follows visual stacking order, not the order of the block IDs in the list. Reorder blocks with `engine.block.bringToFront(...)` or `engine.block.sendToBack(...)` before combining when fill inheritance matters. > **Note:** **Only these block types can be combined*** Graphic blocks > * Text blocks ## Check If Blocks Can Be Combined Before combining blocks, call `engine.block.isCombinable(...)`. It returns `true` only when the selection contains at least two compatible graphic or text blocks on the same page and each block has the `lifecycle/duplicate` scope enabled. ```kotlin highlight-android-check-combinability val unionBlocks = listOf(circle1, circle2, circle3) val canCombineUnion = engine.block.isCombinable(unionBlocks) check(canCombineUnion) { "Choose graphic or text blocks that can be combined." } ``` Use the same set of blocks for the later `combine(...)` call so the checked selection and the mutated selection cannot drift. ## Combining with Union Union merges several blocks into one compound outline. In this sample, three overlapping circles become a single block and inherit the blue fill from the top-most circle. ```kotlin highlight-android-combine-union val unionResult = engine.block.combine( blocks = unionBlocks, op = BooleanOperation.UNION, ) ``` Use Union for merged logos, compound icons, and shapes built from simple primitives. ## Combining with Difference Difference subtracts upper blocks from the bottom-most base block. Place the base block behind the subtracting blocks before combining so the result keeps the intended fill. ```kotlin highlight-android-combine-difference engine.block.sendToBack(imageBlock) // Load the image fill and text font resources before combining. engine.block.forceLoadResources(listOf(imageBlock, textBlock)) val differenceBlocks = listOf(imageBlock, textBlock) check(engine.block.isCombinable(differenceBlocks)) val differenceResult = engine.block.combine( blocks = differenceBlocks, op = BooleanOperation.DIFFERENCE, ) ``` The sample places an image block behind a text block, loads the image resource, and then removes the text shape from the image. `engine.block.forceLoadResources(...)` loads both the image fill and text font resources before the operation reads their shapes. ## Combining with Intersection Intersection keeps only the area shared by all selected blocks. The result inherits the bottom-most block's fill. The sample uses two overlapping circles to create a lens-shaped result. ```kotlin highlight-android-combine-intersection val intersectionBlocks = listOf(intersectionCircle1, intersectionCircle2) check(engine.block.isCombinable(intersectionBlocks)) val intersectionResult = engine.block.combine( blocks = intersectionBlocks, op = BooleanOperation.INTERSECTION, ) ``` Use Intersection for overlap effects, lens shapes, and geometric masks. ## Combining with XOR XOR keeps the non-overlapping areas of the selected blocks and removes their intersections. ```kotlin highlight-android-combine-xor val xorBlocks = listOf(xorCircle1, xorCircle2) check(engine.block.isCombinable(xorBlocks)) val xorResult = engine.block.combine( blocks = xorBlocks, op = BooleanOperation.XOR, ) ``` Use XOR for exclusion shapes, cut-through overlaps, and inverted intersection patterns. ## Understanding Fill Inheritance Combined blocks inherit fill and related appearance properties from the prioritized block. For Union and XOR, the prioritized block is the top-most input block. For Difference and Intersection, it is the bottom-most input block. Control this before combining by moving the block whose fill should be inherited with `engine.block.bringToFront(...)` or `engine.block.sendToBack(...)`. ## Scope Requirements Combining blocks uses the same scope system as other block mutations: - `lifecycle/duplicate` must be enabled for every input block. `engine.block.isCombinable(...)` checks this, along with the two-block and same-page requirements, before you call `combine(...)`. - `lifecycle/destroy` must be enabled when the input blocks should be replaced by the combined result. If scoped content blocks a combination workflow, inspect the relevant scope with `engine.block.isScopeEnabled(...)` and update it with `engine.block.setScopeEnabled(...)` only when your editing rules allow that change. ## Troubleshooting ### Combination Fails - Check the block list with `engine.block.isCombinable(...)` before calling `combine(...)`. - Make sure every input is a graphic or text block. - Select at least two compatible blocks on the same page. - Verify that the selected blocks still exist and have not already been consumed by a previous boolean operation. ### Wrong Fill on the Result - For Union and XOR, move the block whose fill should be inherited to the front. - For Difference and Intersection, send the block whose fill should be inherited to the back. - Re-run the combinability check after changing the selection or replacing any block. ### Original Blocks Remain If the combined result appears but the original blocks remain, the input blocks may not have `lifecycle/destroy` enabled. Check that scope with `engine.block.isScopeEnabled(...)` and only enable it with `engine.block.setScopeEnabled(...)` when your app's editing rules allow the originals to be removed. ### Unexpected Shape Result - Boolean operations use block order. Union and XOR start from the highest sort order; Difference and Intersection start from the lowest sort order. - Control visual stacking with `engine.block.bringToFront(...)` or `engine.block.sendToBack(...)` before combining. - Load image fill and text font resources before combining those blocks so the operation can resolve their shapes. ## API Reference | Method | Description | | --- | --- | | `engine.block.isCombinable(blocks=_)` | Check whether blocks can be combined | | `engine.block.combine(blocks=_, op=_)` | Perform a boolean operation on compatible blocks | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a graphic block | | `engine.block.create(blockType=DesignBlockType.Text)` | Create a text block | | `engine.block.createShape(type=_)` | Create a shape for a graphic block | | `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block | | `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 solid color fill | | `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an RGBA color value | | `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.appendChild(parent=_, child=_)` | Add a block to a parent | | `engine.block.replaceText(block=_, text=_)` | Replace text block content | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text size | | `engine.block.setString(block=_, property="fill/image/imageFileURI", value=_)` | Set an image fill URI | | `engine.block.forceLoadResources(blocks=_)` | Load resources needed by blocks | | `engine.block.bringToFront(block=_)` | Move a block to the highest sort order | | `engine.block.sendToBack(block=_)` | Move a block to the lowest sort order | | `engine.block.isScopeEnabled(block=_, key=_)` | Check whether a scope is enabled | | `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Enable or disable a scope | ## Next Steps - [Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/) — Draw the building-block shapes that boolean operations combine. - [Edit Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) — Modify shape properties before or after combining. - [Create Cutout](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3/) — Generate cutout paths for die-cut prints and stickers. --- ## 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 Cutout" description: "Create cutout paths for cutting printers to produce die-cut stickers, iron-on decals, and custom-shaped prints." platform: android url: "https://img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3/" --- > 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 Stickers](https://img.ly/docs/cesdk/android/stickers-3d4e5f/) > [Create Cutout](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3/) > [Plugins](https://img.ly/docs/cesdk/android/plugins-693c48/) > [Cutout Library](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3/) --- ```kotlin file=@cesdk_android_examples/engine-guides-cutouts/Cutouts.kt reference-only import ly.img.engine.Color import ly.img.engine.CutoutOperation import ly.img.engine.CutoutType 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 Cutouts( val blockCutoutSmoothing: Float, val combinedCutoutType: String, val combinedCutoutOffset: Float, val dashedSpotColorName: String, val pdfByteCount: Int, ) suspend fun cutouts(engine: Engine): Cutouts { 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 sourceShape = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(sourceShape, shape = engine.block.createShape(ShapeType.Star)) engine.block.setWidth(sourceShape, value = 140F) engine.block.setHeight(sourceShape, value = 140F) engine.block.setPositionX(sourceShape, value = 70F) engine.block.setPositionY(sourceShape, value = 80F) engine.block.setFill(sourceShape, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = sourceShape, color = Color.fromRGBA(r = 0.95F, g = 0.75F, b = 0.15F, a = 1F), ) engine.block.appendChild(parent = page, child = sourceShape) val circle = engine.block.createCutoutFromPath( "M 0,75 a 75,75 0 1,1 150,0 a 75,75 0 1,1 -150,0 Z", ) engine.block.appendChild(parent = page, child = circle) engine.block.setPositionX(circle, value = 200F) engine.block.setPositionY(circle, value = 225F) val blockCutout = engine.block.createCutoutFromBlocks( blocks = listOf(sourceShape), vectorizeDistanceThreshold = 2F, simplifyDistanceThreshold = 4F, useExistingShapeInformation = true, ) engine.block.appendChild(parent = page, child = blockCutout) engine.block.setPositionX(blockCutout, value = 70F) engine.block.setPositionY(blockCutout, value = 80F) engine.block.setEnum( block = circle, property = "cutout/type", value = CutoutType.DASHED.key, ) engine.block.setFloat(block = circle, property = "cutout/offset", value = 5F) engine.block.setFloat(block = blockCutout, property = "cutout/smoothing", value = 2F) val square = engine.block.createCutoutFromPath("M 0,0 H 150 V 150 H 0 Z") engine.block.appendChild(parent = page, child = square) engine.block.setPositionX(square, value = 450F) engine.block.setPositionY(square, value = 225F) engine.block.setFloat(block = square, property = "cutout/offset", value = 8F) val combined = engine.block.createCutoutFromOperation( blocks = listOf(circle, square), op = CutoutOperation.UNION, ) engine.block.appendChild(parent = page, child = combined) engine.block.destroy(circle) engine.block.destroy(square) engine.block.setSpotColorForCutoutType( type = CutoutType.DASHED, name = "KissCutContour", ) engine.editor.setSpotColor( name = "KissCutContour", color = Color.fromRGBA(r = 0F, g = 0.4F, b = 0.9F, a = 1F), ) val pdfData = engine.block.export(block = page, mimeType = MimeType.PDF) return Cutouts( blockCutoutSmoothing = engine.block.getFloat(blockCutout, "cutout/smoothing"), combinedCutoutType = engine.block.getEnum(combined, "cutout/type"), combinedCutoutOffset = engine.block.getFloat(combined, "cutout/offset"), dashedSpotColorName = engine.block.getSpotColorForCutoutType(CutoutType.DASHED), pdfByteCount = pdfData.remaining(), ) } ``` Create cutout paths for cutting printers to produce die-cut stickers, iron-on decals, and custom-shaped prints programmatically. ![Cutout paths exported from the Android sample scene](./assets/android.hero.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-cutouts) Cutouts define outline paths that cutting printers cut with a blade rather than print with ink. CE.SDK supports creating cutouts from SVG paths, generating them from block contours, and combining them with boolean operations. This guide covers creating cutouts programmatically, configuring cutout types, offset, and smoothing, combining cutouts with boolean operations, customizing spot colors for printer compatibility, and exporting print-ready PDFs. ## Understanding Cutouts Cutouts are `//ly.img.ubq/cutout` blocks that contain SVG paths interpreted by cutting printers as cut lines. Printers recognize cutouts through specially named spot colors: `CutContour` for solid continuous cuts and `PerfCutContour` for dashed perforated cuts. A spot color’s RGB value affects only on-screen rendering, not printer behavior. Spot colors have no built-in RGB approximation, so a cutout whose spot color you haven’t defined renders with a generic magenta fallback. Use `engine.editor.setSpotColor()` to give a spot color an on-screen color. > **Note:** Cutouts export to PDF format with spot color information preserved. Cutting > printers read the spot colors to identify cut paths. ## Creating Cutouts from SVG Paths Create cutouts using `engine.block.createCutoutFromPath(path)` with standard SVG path syntax. The path coordinates define the cutout dimensions. ```kotlin highlight-android-create-cutout-from-path val circle = engine.block.createCutoutFromPath( "M 0,75 a 75,75 0 1,1 150,0 a 75,75 0 1,1 -150,0 Z", ) engine.block.appendChild(parent = page, child = circle) engine.block.setPositionX(circle, value = 200F) engine.block.setPositionY(circle, value = 225F) ``` The method accepts standard SVG path commands: `M` (move), `L` (line), `H` (horizontal), `V` (vertical), `C` (cubic curve), `Q` (quadratic curve), `A` (arc), and `Z` (close path). ## Creating Cutouts from Blocks Generate a cutout from existing block contours with `engine.block.createCutoutFromBlocks()`. CE.SDK can reuse vector information from shapes, SVG-based text, and stickers, or vectorize raster content when needed. ```kotlin highlight-android-create-cutout-from-blocks val blockCutout = engine.block.createCutoutFromBlocks( blocks = listOf(sourceShape), vectorizeDistanceThreshold = 2F, simplifyDistanceThreshold = 4F, useExistingShapeInformation = true, ) engine.block.appendChild(parent = page, child = blockCutout) engine.block.setPositionX(blockCutout, value = 70F) engine.block.setPositionY(blockCutout, value = 80F) ``` `vectorizeDistanceThreshold` controls how closely the generated path follows raster contours. `simplifyDistanceThreshold` reduces path complexity, and `useExistingShapeInformation` lets CE.SDK reuse an existing vector path when one is available. ## Configuring Cutout Type Set the cutout type using `engine.block.setEnum()` to control whether the printer creates a continuous or perforated cut line. ```kotlin highlight-android-configure-cutout-type engine.block.setEnum( block = circle, property = "cutout/type", value = CutoutType.DASHED.key, ) ``` `Solid` creates a continuous cutting line, while `Dashed` creates a perforated cutting line for tear-away edges. ## Configuring Cutout Offset and Smoothing Adjust the distance between the cutout line and the source path using `engine.block.setFloat()` with the `cutout/offset` property. Use `cutout/smoothing` to round sharp corners in generated or hand-authored paths. ```kotlin highlight-android-configure-cutout-offset engine.block.setFloat(block = circle, property = "cutout/offset", value = 5F) engine.block.setFloat(block = blockCutout, property = "cutout/smoothing", value = 2F) ``` Positive offset values expand the cutout outward from the path. Use offset to add bleed or margin around designs for cleaner cuts. ## Creating Multiple Cutouts Create additional cutouts with different properties to demonstrate various shapes and configurations. ```kotlin highlight-android-create-square-cutout val square = engine.block.createCutoutFromPath("M 0,0 H 150 V 150 H 0 Z") engine.block.appendChild(parent = page, child = square) engine.block.setPositionX(square, value = 450F) engine.block.setPositionY(square, value = 225F) engine.block.setFloat(block = square, property = "cutout/offset", value = 8F) ``` Each cutout can have independent type, offset, and smoothing settings based on your production requirements. ## Combining Cutouts with Boolean Operations Combine multiple cutouts into compound shapes using `engine.block.createCutoutFromOperation(blocks, op)`. Available operations are `UNION`, `DIFFERENCE`, `INTERSECTION`, and `XOR`. ```kotlin highlight-android-combine-cutouts val combined = engine.block.createCutoutFromOperation( blocks = listOf(circle, square), op = CutoutOperation.UNION, ) engine.block.appendChild(parent = page, child = combined) engine.block.destroy(circle) engine.block.destroy(square) ``` The combined cutout inherits the type from the first cutout in the array and has an offset of `0`. Destroy the original cutouts after combining to avoid duplicate cuts. > **Note:** When using `DIFFERENCE`, the first cutout is the base that others subtract > from. For other operations, the order affects which cutout's type is > inherited. ## Customizing Spot Colors Assign a custom spot color name to each cutout type with `engine.block.setSpotColorForCutoutType()`. Then use `engine.editor.setSpotColor()` to define how that custom spot color renders on screen. ```kotlin highlight-android-customize-spot-color engine.block.setSpotColorForCutoutType( type = CutoutType.DASHED, name = "KissCutContour", ) engine.editor.setSpotColor( name = "KissCutContour", color = Color.fromRGBA(r = 0F, g = 0.4F, b = 0.9F, a = 1F), ) ``` Spot color names, such as `CutContour`, `PerfCutContour`, or printer-specific names like `KissCutContour`, are what printers recognize. Use the name required by your printer, and treat the RGB color as an on-screen approximation. ## Exporting Cutouts Export the design with cutouts to PDF format to preserve spot color information for cutting printers. ```kotlin highlight-android-export val pdfData = engine.block.export(block = page, mimeType = MimeType.PDF) ``` The PDF output contains spot colors that cutting printers read to identify cut paths. Persist the returned data with your app's storage layer when you need to hand the file to a print workflow. ## Troubleshooting ### Cutout Not Visible Cutouts render using their spot color’s on-screen RGB approximation. Spot colors have no built-in approximation, so an undefined one falls back to magenta — which means a cutout is normally visible even before you call `engine.editor.setSpotColor()`. If a cutout isn’t visible, verify it’s appended to the page hierarchy and positioned within the visible canvas area, and confirm its spot color approximation isn’t set to a color that blends into the background. ### Printer Not Cutting Check that spot color names match your printer's requirements. Some printers need specific names like `CutContour` or `Thru-cut`. Consult your printer documentation. ### Combined Cutout Has Wrong Type Combined cutouts inherit the type from the first cutout in the array. Reorder the array or set the type explicitly after combination. ### Cutout Path Too Complex Increase `simplifyDistanceThreshold` when generating cutouts from complex blocks to reduce path complexity. For authored SVG paths, simplify the source path before creating the cutout. ## API Reference | Method | Category | Purpose | | --------------------------------------------------------------------------------------------------------------------------- | --------- | -------------------------------------- | | `engine.block.createCutoutFromPath(path=_)` | Cutout | Create cutout from SVG path string | | `engine.block.createCutoutFromBlocks(blocks=_, vectorizeDistanceThreshold=_, simplifyDistanceThreshold=_, useExistingShapeInformation=_)` | Cutout | Create cutout from block contours | | `engine.block.createCutoutFromOperation(blocks=_, op=_)` | Cutout | Combine cutouts with boolean operation | | `engine.block.setSpotColorForCutoutType(type=_, name=_)` | Cutout | Assign a spot color name to a cutout type | | `engine.block.getSpotColorForCutoutType(type=_)` | Cutout | Read the spot color name for a cutout type | | `engine.block.setEnum(block=_, property="cutout/type", value=_)` | Property | Set cutout type | | `engine.block.getEnum(block=_, property="cutout/type")` | Property | Read cutout type | | `engine.block.setFloat(block=_, property="cutout/offset", value=_)` | Property | Set cutout offset distance | | `engine.block.setFloat(block=_, property="cutout/smoothing", value=_)` | Property | Set corner smoothing threshold | | `engine.block.getFloat(block=_, property="cutout/offset")` | Property | Read cutout offset distance | | `engine.block.getFloat(block=_, property="cutout/smoothing")` | Property | Read corner smoothing threshold | | `engine.block.appendChild(parent=_, child=_)` | Hierarchy | Add cutout to scene | | `engine.block.setPositionX(block=_, value=_)` | Transform | Position cutout on the x-axis | | `engine.block.setPositionY(block=_, value=_)` | Transform | Position cutout on the y-axis | | `engine.block.destroy(block=_)` | Lifecycle | Remove cutout from scene | | `engine.block.export(block=_, mimeType=_)` | Export | Export page with cutouts to PDF | | `engine.editor.setSpotColor(name=_, color=_)` | Editor | Customize spot color rendering | ## Next Steps - **[Combine Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26/)** - Boolean operations on graphic blocks - **[Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/)** - Create geometric shapes programmatically - **[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: "Create Shapes" description: "Draw custom vector shapes and insert them into your design canvas." platform: android url: "https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/" --- > 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 Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) > [Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-shapes/CreateShapes.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.GradientColorStop import ly.img.engine.ShapeType suspend fun createShapes(engine: Engine): DesignBlock { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 300F) engine.block.setHeight(page, value = 200F) engine.block.appendChild(parent = scene, child = page) val testGraphic = engine.block.create(DesignBlockType.Graphic) val graphicSupportsShapes = engine.block.supportsShape(testGraphic) val testText = engine.block.create(DesignBlockType.Text) val textSupportsShapes = engine.block.supportsShape(testText) check(graphicSupportsShapes) check(!textSupportsShapes) engine.block.destroy(testText) engine.block.destroy(testGraphic) val rectGraphic = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(rectGraphic, shape = rectShape) val redFill = engine.block.createFill(FillType.Color) engine.block.setFill(rectGraphic, fill = redFill) engine.block.setFillSolidColor( block = rectGraphic, color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ) engine.block.setWidth(rectGraphic, value = 64F) engine.block.setHeight(rectGraphic, value = 64F) engine.block.appendChild(parent = page, child = rectGraphic) engine.block.setPositionX(rectGraphic, value = 10F) engine.block.setPositionY(rectGraphic, value = 10F) check(engine.block.getShape(rectGraphic) == rectShape) check(engine.block.getFill(rectGraphic) == redFill) val ellipseGraphic = engine.block.create(DesignBlockType.Graphic) val ellipseShape = engine.block.createShape(ShapeType.Ellipse) engine.block.setShape(ellipseGraphic, shape = ellipseShape) val gradientFill = engine.block.createFill(FillType.LinearGradient) engine.block.setGradientColorStops( block = gradientFill, property = "fill/gradient/colors", colorStops = listOf( GradientColorStop( stop = 0F, color = Color.fromRGBA(r = 0.2F, g = 0.6F, b = 0.9F, a = 1F), ), GradientColorStop( stop = 1F, color = Color.fromRGBA(r = 0.9F, g = 0.3F, b = 0.6F, a = 1F), ), ), ) engine.block.setFill(ellipseGraphic, fill = gradientFill) engine.block.setWidth(ellipseGraphic, value = 64F) engine.block.setHeight(ellipseGraphic, value = 64F) engine.block.appendChild(parent = page, child = ellipseGraphic) engine.block.setPositionX(ellipseGraphic, value = 82F) engine.block.setPositionY(ellipseGraphic, value = 10F) check(engine.block.getGradientColorStops(gradientFill, property = "fill/gradient/colors").size == 2) val exampleStarShape = engine.block.createShape(ShapeType.Star) val starProperties = engine.block.findAllProperties(exampleStarShape) check("shape/star/points" in starProperties) engine.block.destroy(exampleStarShape) val starGraphic = engine.block.create(DesignBlockType.Graphic) val starShape = engine.block.createShape(ShapeType.Star) engine.block.setShape(starGraphic, shape = starShape) val yellowFill = engine.block.createFill(FillType.Color) engine.block.setFill(starGraphic, fill = yellowFill) engine.block.setFillSolidColor( block = starGraphic, color = Color.fromRGBA(r = 1F, g = 0.8F, b = 0F, a = 1F), ) engine.block.setWidth(starGraphic, value = 64F) engine.block.setHeight(starGraphic, value = 64F) engine.block.appendChild(parent = page, child = starGraphic) engine.block.setPositionX(starGraphic, value = 154F) engine.block.setPositionY(starGraphic, value = 10F) engine.block.setInt(starShape, property = "shape/star/points", value = 5) engine.block.setFloat(starShape, property = "shape/star/innerDiameter", value = 0.5F) check(engine.block.getInt(starShape, property = "shape/star/points") == 5) check(engine.block.getFloat(starShape, property = "shape/star/innerDiameter") == 0.5F) val polygonGraphic = engine.block.create(DesignBlockType.Graphic) val polygonShape = engine.block.createShape(ShapeType.Polygon) engine.block.setShape(polygonGraphic, shape = polygonShape) val greenFill = engine.block.createFill(FillType.Color) engine.block.setFill(polygonGraphic, fill = greenFill) engine.block.setFillSolidColor( block = polygonGraphic, color = Color.fromRGBA(r = 0.2F, g = 0.8F, b = 0.3F, a = 1F), ) engine.block.setWidth(polygonGraphic, value = 64F) engine.block.setHeight(polygonGraphic, value = 64F) engine.block.appendChild(parent = page, child = polygonGraphic) engine.block.setPositionX(polygonGraphic, value = 226F) engine.block.setPositionY(polygonGraphic, value = 10F) engine.block.setInt(polygonShape, property = "shape/polygon/sides", value = 8) check(engine.block.getInt(polygonShape, property = "shape/polygon/sides") == 8) val lineGraphic = engine.block.create(DesignBlockType.Graphic) val lineShape = engine.block.createShape(ShapeType.Line) engine.block.setShape(lineGraphic, shape = lineShape) engine.block.setStrokeColor( block = lineGraphic, color = Color.fromRGBA(r = 0.6F, g = 0.2F, b = 0.9F, a = 1F), ) engine.block.setWidth(lineGraphic, value = 64F) // For line shapes, block height controls the visible line thickness. engine.block.setHeight(lineGraphic, value = 10F) engine.block.appendChild(parent = page, child = lineGraphic) engine.block.setPositionX(lineGraphic, value = 10F) engine.block.setPositionY(lineGraphic, value = 109F) val vectorPathGraphic = engine.block.create(DesignBlockType.Graphic) val vectorPathShape = engine.block.createShape(ShapeType.VectorPath) engine.block.setShape(vectorPathGraphic, shape = vectorPathShape) engine.block.setString( block = vectorPathShape, property = "shape/vector_path/path", value = "M 0,0 L 100,50 L 0,100 Z", ) val orangeFill = engine.block.createFill(FillType.Color) engine.block.setFill(vectorPathGraphic, fill = orangeFill) engine.block.setFillSolidColor( block = vectorPathGraphic, color = Color.fromRGBA(r = 1F, g = 0.5F, b = 0F, a = 1F), ) engine.block.setWidth(vectorPathGraphic, value = 64F) engine.block.setHeight(vectorPathGraphic, value = 64F) engine.block.appendChild(parent = page, child = vectorPathGraphic) engine.block.setPositionX(vectorPathGraphic, value = 82F) engine.block.setPositionY(vectorPathGraphic, value = 82F) check(engine.block.getString(vectorPathShape, property = "shape/vector_path/path").isNotEmpty()) val roundedRectGraphic = engine.block.create(DesignBlockType.Graphic) val roundedRectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(roundedRectGraphic, shape = roundedRectShape) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusTL", value = 5F) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusTR", value = 5F) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusBL", value = 5F) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusBR", value = 5F) val cyanFill = engine.block.createFill(FillType.Color) engine.block.setFill(roundedRectGraphic, fill = cyanFill) engine.block.setFillSolidColor( block = roundedRectGraphic, color = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.8F, a = 1F), ) engine.block.setWidth(roundedRectGraphic, value = 64F) engine.block.setHeight(roundedRectGraphic, value = 64F) engine.block.appendChild(parent = page, child = roundedRectGraphic) engine.block.setPositionX(roundedRectGraphic, value = 154F) engine.block.setPositionY(roundedRectGraphic, value = 82F) check(engine.block.getFloat(roundedRectShape, property = "shape/rect/cornerRadiusTL") == 5F) val currentShape = engine.block.getShape(rectGraphic) val currentShapeType = ShapeType.get(engine.block.getType(currentShape)) check(currentShapeType == ShapeType.Rect) val oldShape = engine.block.getShape(rectGraphic) val newShape = engine.block.createShape(ShapeType.Ellipse) engine.block.setShape(rectGraphic, shape = newShape) engine.block.destroy(oldShape) check(engine.block.getShape(rectGraphic) == newShape) engine.scene.zoomToBlock( page, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) return page } ``` Create geometric shapes with the Engine API and combine them with fills so they appear on the design canvas. ![Android Create Shapes scene preview showing rectangles, ellipses, stars, polygons, lines, and vector paths](./assets/android.hero.png) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-shapes) Shapes define the geometry of a graphic block. Closed shapes use fills for their visible content, such as a solid color, gradient, image, or video. Line shapes render as strokes. The snippets use a sample page named `page` and focus on the shape, fill, and block API calls. Use the complete Kotlin source linked above when you want the runnable scene setup. ## Understanding Shapes and Graphic Blocks ### What Are Shapes? Shapes are block-like geometry definitions that stay independent until you attach them to a graphic block. Android exposes the built-in shape kinds through `ShapeType`, so you can create shapes with type-safe values such as `ShapeType.Rect` or `ShapeType.Star`. Shape and fill are independent. You can replace a rectangle with a star while keeping the same fill, or change a fill while preserving the current shape. ### The Graphic Block System Graphic blocks bring shapes and fills together. A graphic block can hold: - **Shape**: Rectangle, ellipse, star, polygon, line, or vector path geometry - **Fill**: Color, gradient, image, or video content that makes closed shapes visible - **Stroke**: Outline or line rendering, including the color of `ShapeType.Line` - **Effects**: Optional filters, blur, or shadows applied to the filled shape - **Transform**: Position, rotation, and scale values on the graphic block ### Available Shape Types CE.SDK provides six built-in shape types on Android: - `ShapeType.Rect`: Rectangles with optional independent corner radii - `ShapeType.Ellipse`: Circles and ovals - `ShapeType.Star`: Stars with configurable points and inner diameter - `ShapeType.Polygon`: Regular polygons with a configurable number of sides - `ShapeType.Line`: Straight lines spanning the block dimensions - `ShapeType.VectorPath`: Custom shapes from SVG path data ## Checking Shape Support Before applying a shape, verify that the target block type supports shapes. Graphic blocks support shapes, while text blocks do not. ```kotlin highlight-android-check-shape-support val testGraphic = engine.block.create(DesignBlockType.Graphic) val graphicSupportsShapes = engine.block.supportsShape(testGraphic) val testText = engine.block.create(DesignBlockType.Text) val textSupportsShapes = engine.block.supportsShape(testText) check(graphicSupportsShapes) check(!textSupportsShapes) engine.block.destroy(testText) engine.block.destroy(testGraphic) ``` Use `supportsShape()` when you work with dynamic selections or unknown block types. It lets you skip unsupported blocks before calling `setShape()`. ## Creating Basic Shapes ### Creating a Rectangle Create a graphic block, attach a rectangle shape, add a color fill, and append the block to the page. ```kotlin highlight-android-create-rectangle val rectGraphic = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(rectGraphic, shape = rectShape) val redFill = engine.block.createFill(FillType.Color) engine.block.setFill(rectGraphic, fill = redFill) engine.block.setFillSolidColor( block = rectGraphic, color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ) engine.block.setWidth(rectGraphic, value = 64F) engine.block.setHeight(rectGraphic, value = 64F) engine.block.appendChild(parent = page, child = rectGraphic) engine.block.setPositionX(rectGraphic, value = 10F) engine.block.setPositionY(rectGraphic, value = 10F) ``` Closed shapes remain invisible until the graphic block has both a shape and a fill. The block width, height, and position control the shape's final canvas bounds. ### Creating Other Shape Types The creation pattern stays the same across closed shape types: create a graphic block, create the shape, attach the shape, apply a fill, then size and place the block. Line shapes use stroke rendering instead of fill rendering. Ellipse with a linear gradient fill: ```kotlin highlight-android-create-ellipse val ellipseGraphic = engine.block.create(DesignBlockType.Graphic) val ellipseShape = engine.block.createShape(ShapeType.Ellipse) engine.block.setShape(ellipseGraphic, shape = ellipseShape) val gradientFill = engine.block.createFill(FillType.LinearGradient) engine.block.setGradientColorStops( block = gradientFill, property = "fill/gradient/colors", colorStops = listOf( GradientColorStop( stop = 0F, color = Color.fromRGBA(r = 0.2F, g = 0.6F, b = 0.9F, a = 1F), ), GradientColorStop( stop = 1F, color = Color.fromRGBA(r = 0.9F, g = 0.3F, b = 0.6F, a = 1F), ), ), ) engine.block.setFill(ellipseGraphic, fill = gradientFill) engine.block.setWidth(ellipseGraphic, value = 64F) engine.block.setHeight(ellipseGraphic, value = 64F) engine.block.appendChild(parent = page, child = ellipseGraphic) engine.block.setPositionX(ellipseGraphic, value = 82F) engine.block.setPositionY(ellipseGraphic, value = 10F) ``` Star with a color fill: ```kotlin highlight-android-create-star val starGraphic = engine.block.create(DesignBlockType.Graphic) val starShape = engine.block.createShape(ShapeType.Star) engine.block.setShape(starGraphic, shape = starShape) val yellowFill = engine.block.createFill(FillType.Color) engine.block.setFill(starGraphic, fill = yellowFill) engine.block.setFillSolidColor( block = starGraphic, color = Color.fromRGBA(r = 1F, g = 0.8F, b = 0F, a = 1F), ) engine.block.setWidth(starGraphic, value = 64F) engine.block.setHeight(starGraphic, value = 64F) engine.block.appendChild(parent = page, child = starGraphic) engine.block.setPositionX(starGraphic, value = 154F) engine.block.setPositionY(starGraphic, value = 10F) ``` Polygon with a color fill: ```kotlin highlight-android-create-polygon val polygonGraphic = engine.block.create(DesignBlockType.Graphic) val polygonShape = engine.block.createShape(ShapeType.Polygon) engine.block.setShape(polygonGraphic, shape = polygonShape) val greenFill = engine.block.createFill(FillType.Color) engine.block.setFill(polygonGraphic, fill = greenFill) engine.block.setFillSolidColor( block = polygonGraphic, color = Color.fromRGBA(r = 0.2F, g = 0.8F, b = 0.3F, a = 1F), ) engine.block.setWidth(polygonGraphic, value = 64F) engine.block.setHeight(polygonGraphic, value = 64F) engine.block.appendChild(parent = page, child = polygonGraphic) engine.block.setPositionX(polygonGraphic, value = 226F) engine.block.setPositionY(polygonGraphic, value = 10F) ``` Line with a stroke color. For line shapes, block height controls the visible thickness: ```kotlin highlight-android-create-line val lineGraphic = engine.block.create(DesignBlockType.Graphic) val lineShape = engine.block.createShape(ShapeType.Line) engine.block.setShape(lineGraphic, shape = lineShape) engine.block.setStrokeColor( block = lineGraphic, color = Color.fromRGBA(r = 0.6F, g = 0.2F, b = 0.9F, a = 1F), ) engine.block.setWidth(lineGraphic, value = 64F) // For line shapes, block height controls the visible line thickness. engine.block.setHeight(lineGraphic, value = 10F) engine.block.appendChild(parent = page, child = lineGraphic) engine.block.setPositionX(lineGraphic, value = 10F) engine.block.setPositionY(lineGraphic, value = 109F) ``` Vector path with SVG path data: ```kotlin highlight-android-create-vector-path val vectorPathGraphic = engine.block.create(DesignBlockType.Graphic) val vectorPathShape = engine.block.createShape(ShapeType.VectorPath) engine.block.setShape(vectorPathGraphic, shape = vectorPathShape) engine.block.setString( block = vectorPathShape, property = "shape/vector_path/path", value = "M 0,0 L 100,50 L 0,100 Z", ) val orangeFill = engine.block.createFill(FillType.Color) engine.block.setFill(vectorPathGraphic, fill = orangeFill) engine.block.setFillSolidColor( block = vectorPathGraphic, color = Color.fromRGBA(r = 1F, g = 0.5F, b = 0F, a = 1F), ) engine.block.setWidth(vectorPathGraphic, value = 64F) engine.block.setHeight(vectorPathGraphic, value = 64F) engine.block.appendChild(parent = page, child = vectorPathGraphic) engine.block.setPositionX(vectorPathGraphic, value = 82F) engine.block.setPositionY(vectorPathGraphic, value = 82F) ``` Vector paths use SVG path syntax for a single path. For complex multi-path artwork, use an SVG asset through an image fill instead. ## Configuring Shape Properties ### Discovering Properties Each shape type has its own properties. Use `findAllProperties()` on a shape block when you need to inspect the available property keys. ```kotlin highlight-android-discover-properties val exampleStarShape = engine.block.createShape(ShapeType.Star) val starProperties = engine.block.findAllProperties(exampleStarShape) check("shape/star/points" in starProperties) engine.block.destroy(exampleStarShape) ``` ### Rectangle Corner Radius Rectangles support independent corner radius values. Set each corner in design units on the shape block. ```kotlin highlight-android-configure-properties val roundedRectGraphic = engine.block.create(DesignBlockType.Graphic) val roundedRectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(roundedRectGraphic, shape = roundedRectShape) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusTL", value = 5F) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusTR", value = 5F) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusBL", value = 5F) engine.block.setFloat(roundedRectShape, property = "shape/rect/cornerRadiusBR", value = 5F) val cyanFill = engine.block.createFill(FillType.Color) engine.block.setFill(roundedRectGraphic, fill = cyanFill) engine.block.setFillSolidColor( block = roundedRectGraphic, color = Color.fromRGBA(r = 0F, g = 0.8F, b = 0.8F, a = 1F), ) engine.block.setWidth(roundedRectGraphic, value = 64F) engine.block.setHeight(roundedRectGraphic, value = 64F) engine.block.appendChild(parent = page, child = roundedRectGraphic) engine.block.setPositionX(roundedRectGraphic, value = 154F) engine.block.setPositionY(roundedRectGraphic, value = 82F) ``` ### Star and Polygon Properties For stars, set `shape/star/points` and `shape/star/innerDiameter`. For polygons, set `shape/polygon/sides`. ```kotlin highlight-android-configure-star-properties engine.block.setInt(starShape, property = "shape/star/points", value = 5) engine.block.setFloat(starShape, property = "shape/star/innerDiameter", value = 0.5F) ``` ```kotlin highlight-android-configure-polygon-properties engine.block.setInt(polygonShape, property = "shape/polygon/sides", value = 8) ``` ## Combining Shapes with Fills Closed shapes define geometry but remain invisible without fills. Fills provide the visible content inside the shape, including solid colors, gradients, images, and videos. Line shapes render through their stroke color instead. The sample uses color fills for most closed shapes, a gradient fill for the ellipse, and a stroke color for the line. For a complete overview of fill types, see the [Fills](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) guide. ## Managing Shapes ### Retrieving Shapes Use `getShape()` to access the shape currently attached to a graphic block. Read the shape type with `getType()` and map it back to `ShapeType`. ```kotlin highlight-android-retrieve-shape val currentShape = engine.block.getShape(rectGraphic) val currentShapeType = ShapeType.get(engine.block.getType(currentShape)) ``` ### Replacing Shapes When replacing a shape, apply the new shape and then destroy the old shape. `setShape()` does not destroy the previous shape automatically. ```kotlin highlight-android-replace-shape val oldShape = engine.block.getShape(rectGraphic) val newShape = engine.block.createShape(ShapeType.Ellipse) engine.block.setShape(rectGraphic, shape = newShape) engine.block.destroy(oldShape) ``` Destroy any unused shape blocks you create. When you destroy a graphic block, CE.SDK also destroys the shape attached to that block. ## Troubleshooting If a shape does not appear, check these common causes: - **Missing fill or stroke**: For closed shapes, call `setFill()` with a color, gradient, image, or video fill after attaching the shape. For line shapes, set a stroke color. - **Missing shape**: Call `setShape()` before expecting the graphic block to render shape geometry. - **Zero size**: Set non-zero width and height on the graphic block. - **Unsupported block**: Guard unknown blocks with `supportsShape()` before calling `setShape()`. - **Replaced shape still allocated**: Destroy the old shape after a successful replacement. ## API Reference | Method | Description | | --- | --- | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a graphic block that can hold a shape and fill. | | `engine.block.createShape(type=_)` | Create a shape block using a `ShapeType` value. | | `engine.block.supportsShape(block=_)` | Check whether a block can hold a shape. | | `engine.block.setShape(block=_, shape=_)` | Attach a shape block to a graphic block. | | `engine.block.getShape(block=_)` | Get the shape block currently attached to a graphic block. | | `engine.block.getType(block=_)` | Read a block type key, including shape type keys. | | `ShapeType.get(key=_)` | Convert a shape type key into a `ShapeType` value. | | `engine.block.findAllProperties(block=_)` | List available property keys for a shape block. | | `engine.block.createFill(fillType=_)` | Create a fill block, such as `FillType.Color` or `FillType.LinearGradient`. | | `engine.block.setFill(block=_, fill=_)` | Attach a fill block to a graphic block. | | `engine.block.getFill(block=_)` | Get the fill block currently attached to a graphic block. | | `engine.block.setFillSolidColor(block=_, color=_)` | Set the solid color for the color fill attached to a graphic block. | | `engine.block.setGradientColorStops(block=_, property="fill/gradient/colors", colorStops=_)` | Set gradient color stops on a gradient fill. | | `engine.block.getGradientColorStops(block=_, property="fill/gradient/colors")` | Read gradient color stops from a gradient fill. | | `engine.block.setStrokeColor(block=_, color=_)` | Set the visible color of a line shape or other stroked block. | | `engine.block.setFloat(block=_, property="shape/rect/cornerRadiusTL", value=_)` | Set the top-left corner radius for a rectangle shape. | | `engine.block.setFloat(block=_, property="shape/rect/cornerRadiusTR", value=_)` | Set the top-right corner radius for a rectangle shape. | | `engine.block.setFloat(block=_, property="shape/rect/cornerRadiusBL", value=_)` | Set the bottom-left corner radius for a rectangle shape. | | `engine.block.setFloat(block=_, property="shape/rect/cornerRadiusBR", value=_)` | Set the bottom-right corner radius for a rectangle shape. | | `engine.block.setInt(block=_, property="shape/star/points", value=_)` | Set the number of points on a star shape. | | `engine.block.setFloat(block=_, property="shape/star/innerDiameter", value=_)` | Set the inner diameter ratio for a star shape. | | `engine.block.setInt(block=_, property="shape/polygon/sides", value=_)` | Set the number of sides on a polygon shape. | | `engine.block.setString(block=_, property="shape/vector_path/path", value=_)` | Set vector path SVG path data. | | `engine.block.getString(block=_, property="shape/vector_path/path")` | Read vector path SVG path data. | | `engine.block.setWidth(block=_, value=_)` | Set the graphic block width. | | `engine.block.setHeight(block=_, value=_)` | Set the graphic block height. | | `engine.block.setPositionX(block=_, value=_)` | Set the graphic block x-position. | | `engine.block.setPositionY(block=_, value=_)` | Set the graphic block y-position. | | `engine.block.appendChild(parent=_, child=_)` | Add the graphic block to a page or scene hierarchy. | | `engine.block.destroy(block=_)` | Destroy an unused shape, fill, or block. | ### Related Types | Type | Values | Description | | --- | --- | --- | | `ShapeType` | `Rect`, `Ellipse`, `Star`, `Polygon`, `Line`, `VectorPath` | Type-safe shape identifiers exposed by the Android binding. | | `FillType` | `Color`, `LinearGradient`, `RadialGradient`, `ConicalGradient`, `Image`, `Video`, `PixelStream` | Fill kinds that can make a shape visible. | | `GradientColorStop` | `stop`, `color` | Describes one color stop in a gradient fill. | ## Next Steps - [Edit Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) - Modify shape properties and transforms - [Combine Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26/) - Use boolean operations for complex shapes - [Create and Edit Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) - Explore the shape guide family - [Color Fills](https://img.ly/docs/cesdk/android/fills/color-7129cd/) — Get to know all available fill types and their properties - [Gradient](https://img.ly/docs/cesdk/android/fills/gradient-0ff079/) — Documentation for Gradients - [Image Fills](https://img.ly/docs/cesdk/android/fills/image-e9cb5c/) — Documentation for Image Fills - [Fills](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Add solid colors, gradients, images, or videos inside shapes - [Create Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-stickers-cc46e5/) — Create stickers in CE.SDK using image fills for icons, logos, emoji, and multi-color graphics --- ## 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 Stickers" description: "Create stickers in CE.SDK using image fills for icons, logos, emoji, and multi-color graphics." platform: android url: "https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-stickers-cc46e5/" --- > 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 Stickers](https://img.ly/docs/cesdk/android/stickers-3d4e5f/) > [Create Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-stickers-cc46e5/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-stickers/CreateStickers.kt reference-only import android.net.Uri import ly.img.engine.ContentFillMode 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 createStickers(engine: Engine): ByteBuffer { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 450F) engine.block.setHeight(page, value = 250F) engine.block.appendChild(parent = scene, child = page) fun createStickerFromImage( engine: Engine, parent: DesignBlock, imageUri: Uri, x: Float, y: Float, size: Float, ): DesignBlock { val sticker = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(sticker, shape = engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = imageUri, ) engine.block.setFill(sticker, fill = imageFill) engine.block.setWidth(sticker, value = size) engine.block.setHeight(sticker, value = size) engine.block.setPositionX(sticker, value = x) engine.block.setPositionY(sticker, value = y) if (engine.block.supportsContentFillMode(sticker)) { engine.block.setContentFillMode(sticker, mode = ContentFillMode.CONTAIN) } engine.block.setKind(sticker, kind = "sticker") engine.block.appendChild(parent = parent, child = sticker) return sticker } val firstStickerUri = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ) val secondStickerUri = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ) val firstSticker = createStickerFromImage( engine = engine, parent = page, imageUri = firstStickerUri, x = 95F, y = 85F, size = 80F, ) val secondSticker = createStickerFromImage( engine = engine, parent = page, imageUri = secondStickerUri, x = 275F, y = 85F, size = 80F, ) check(engine.block.getKind(firstSticker) == "sticker") check(engine.block.getKind(secondSticker) == "sticker") check(engine.block.getContentFillMode(firstSticker) == ContentFillMode.CONTAIN) check(engine.block.getWidth(firstSticker) == 80F) check(engine.block.getHeight(secondSticker) == 80F) engine.block.forceLoadResources(listOf(page, firstSticker, secondSticker)) val exportedPage = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 900F, targetHeight = 500F), ) return exportedPage } ``` Create stickers from images for use in your designs, including icons, logos, emoji, and detailed multi-color graphics that keep their original appearance. ![Android sticker page with two image-fill stickers](./assets/android.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-stickers) Stickers are graphic blocks with image fills. They work well when the source image should stay intact, such as emoji, brand marks, or detailed artwork. Unlike shapes, stickers are not meant to be recolored or combined with vector boolean operations. ## Creating Stickers From Images The Android Engine API creates sticker blocks by combining a graphic block, a rectangular shape, and an image fill. The helper below accepts an image `Uri`, assigns the image fill, sizes and positions the block, sets `ContentFillMode.CONTAIN`, and marks the block with the Android sticker kind. ```kotlin highlight-android-create-from-image fun createStickerFromImage( engine: Engine, parent: DesignBlock, imageUri: Uri, x: Float, y: Float, size: Float, ): DesignBlock { val sticker = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(sticker, shape = engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = imageUri, ) engine.block.setFill(sticker, fill = imageFill) engine.block.setWidth(sticker, value = size) engine.block.setHeight(sticker, value = size) engine.block.setPositionX(sticker, value = x) engine.block.setPositionY(sticker, value = y) if (engine.block.supportsContentFillMode(sticker)) { engine.block.setContentFillMode(sticker, mode = ContentFillMode.CONTAIN) } engine.block.setKind(sticker, kind = "sticker") engine.block.appendChild(parent = parent, child = sticker) return sticker } ``` `ContentFillMode.CONTAIN` keeps the whole image visible inside the frame. The `sticker` kind lets the editor UI treat the block as a sticker instead of as a generic image block. ## Adding Multiple Stickers Use the same helper for every sticker image you want to add to the page. The sample loads two SVG stickers from the bundled Android asset directory and places them on a 450 by 250 pixel page with the same size and y-position. ```kotlin highlight-android-add-stickers val firstStickerUri = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ) val secondStickerUri = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ) val firstSticker = createStickerFromImage( engine = engine, parent = page, imageUri = firstStickerUri, x = 95F, y = 85F, size = 80F, ) val secondSticker = createStickerFromImage( engine = engine, parent = page, imageUri = secondStickerUri, x = 275F, y = 85F, size = 80F, ) ``` SVG stickers scale cleanly and preserve multiple paths, gradients, and colors. Raster stickers such as PNG or JPG also work, but their source resolution should be high enough for the displayed size. ## Sticker vs Shape Decision Choose between stickers and shapes based on the artwork and editing behavior your app needs: | Requirement | Use Stickers | Use Shapes | | --- | --- | --- | | Multi-color graphics | Yes | No, shapes use a single fill | | Recolorable artwork | No | Yes | | Preserve original artwork | Yes | Not applicable | | Boolean operations | No | Yes | | Complex paths or gradients | Yes | Limited | | Icons, logos, emoji | Preferred | Only for simple vector artwork | ## Troubleshooting ### Sticker Not Appearing Verify that the image URI returns a supported image, the block was appended to the page, and the sticker has non-zero width and height. ### Manually Created Sticker Is Blank Graphic blocks need a shape before a fill can render. Create a rectangle with `engine.block.createShape(type=ShapeType.Rect)` and assign it with `engine.block.setShape(block=_, shape=_)` before applying the image fill. ### Sticker Appears Blurry Use SVG for scalable artwork. For raster stickers, make sure the source image resolution is at least as large as the displayed sticker frame. ### Sticker Appears Cropped Check `engine.block.supportsContentFillMode(block=_)` and set `engine.block.setContentFillMode(block=_, mode=ContentFillMode.CONTAIN)` so the full image stays visible. ### Sticker Cannot Be Recolored This is expected. Stickers preserve their source colors. Use shapes when users need to recolor simple vector artwork. ### Wrong Editor Behavior Set the block kind to `sticker` with `engine.block.setKind(block=_, kind="sticker")`. Android editor UI components use this kind to classify the block as a sticker. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create the graphic block that holds the sticker. | | `engine.block.createShape(type=ShapeType.Rect)` | Create the rectangular shape required for the graphic block to render. | | `engine.block.setShape(block=_, shape=_)` | Assign the shape to the graphic block. | | `engine.block.createFill(fillType=FillType.Image)` | Create an image fill for the sticker artwork. | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on the image fill. | | `engine.block.setFill(block=_, fill=_)` | Apply the image fill to the graphic block. | | `engine.block.setWidth(block=_, value=_)` | Set the sticker frame width. | | `engine.block.setHeight(block=_, value=_)` | Set the sticker frame height. | | `engine.block.setPositionX(block=_, value=_)` | Set the sticker's horizontal position. | | `engine.block.setPositionY(block=_, value=_)` | Set the sticker's vertical position. | | `engine.block.supportsContentFillMode(block=_)` | Check whether the block supports content fill modes. | | `engine.block.setContentFillMode(block=_, mode=ContentFillMode.CONTAIN)` | Keep the whole image visible inside the sticker frame. | | `engine.block.setKind(block=_, kind="sticker")` | Mark the block as a sticker for editor UI behavior. | | `engine.block.appendChild(parent=_, child=_)` | Add the sticker to the page. | ## Next Steps - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/android/insert-media/shapes-or-stickers-20ac68/) - Learn how users can add shapes and stickers in the editor. - [Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/) - Create recolorable vector shapes. - [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) - Learn how fills control the visible content of 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: "Edit Shapes" description: "Modify shape geometry, color, size, position, and corner radius with CreativeEngine." platform: android url: "https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/" --- > 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 Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) > [Edit Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) --- ```kotlin file=@cesdk_android_examples/engine-guides-edit-shapes/EditShapes.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.RGBAColor import ly.img.engine.ShapeType import kotlin.math.PI import kotlin.math.abs suspend fun editShapes(engine: Engine): EditShapesResult { 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 graphic = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(graphic, shape = rectShape) val initialFill = engine.block.createFill(FillType.Color) engine.block.setFill(graphic, fill = initialFill) engine.block.setColor( block = initialFill, property = "fill/color/value", value = Color.fromRGBA(r = 0.93F, g = 0.94F, b = 0.96F, a = 1F), ) engine.block.setWidth(graphic, value = 240F) engine.block.setHeight(graphic, value = 160F) engine.block.setPositionX(graphic, value = 80F) engine.block.setPositionY(graphic, value = 80F) engine.block.appendChild(parent = page, child = graphic) val supportsGraphicShape = engine.block.supportsShape(block = graphic) val text = engine.block.create(DesignBlockType.Text) val supportsTextShape = engine.block.supportsShape(block = text) val currentShape = engine.block.getShape(block = graphic) val initialShapeType = engine.block.getType(block = currentShape) check(supportsGraphicShape) check(!supportsTextShape) check(initialShapeType == ShapeType.Rect.key) engine.block.destroy(text) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusTL", value = 24F) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusTR", value = 24F) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusBR", value = 24F) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusBL", value = 24F) val cornerRadiusTopLeft = engine.block.getFloat(block = currentShape, property = "shape/rect/cornerRadiusTL") check(abs(cornerRadiusTopLeft - 24F) < 0.001F) val starShape = engine.block.createShape(type = ShapeType.Star) engine.block.setShape(block = graphic, shape = starShape) engine.block.destroy(block = currentShape) val replacementShapeType = engine.block.getType(block = engine.block.getShape(block = graphic)) check(replacementShapeType == ShapeType.Star.key) val starProperties = engine.block.findAllProperties(block = starShape) engine.block.setInt(block = starShape, property = "shape/star/points", value = 6) engine.block.setFloat(block = starShape, property = "shape/star/innerDiameter", value = 0.45F) val starPoints = engine.block.getInt(block = starShape, property = "shape/star/points") val starInnerDiameter = engine.block.getFloat(block = starShape, property = "shape/star/innerDiameter") check("shape/star/points" in starProperties) check(starPoints == 6) check(abs(starInnerDiameter - 0.45F) < 0.001F) val fill = engine.block.getFill(block = graphic) val fillColor = Color.fromRGBA(r = 0.1F, g = 0.46F, b = 0.85F, a = 1F) engine.block.setColor(block = fill, property = "fill/color/value", value = fillColor) val appliedFillColor = engine.block.getColor(block = fill, property = "fill/color/value") as RGBAColor check(appliedFillColor == fillColor) engine.block.setPositionX(block = graphic, value = 160F) engine.block.setPositionY(block = graphic, value = 120F) engine.block.setWidth(block = graphic, value = 320F) engine.block.setHeight(block = graphic, value = 220F) val positionX = engine.block.getPositionX(block = graphic) val positionY = engine.block.getPositionY(block = graphic) val width = engine.block.getWidth(block = graphic) val height = engine.block.getHeight(block = graphic) val rotationRadians = (15F * PI / 180F).toFloat() engine.block.setRotation(block = graphic, radians = rotationRadians) val rotation = engine.block.getRotation(block = graphic) check(abs(positionX - 160F) < 0.001F) check(abs(positionY - 120F) < 0.001F) check(abs(width - 320F) < 0.001F) check(abs(height - 220F) < 0.001F) check(abs(rotation - rotationRadians) < 0.001F) val previewPngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 640F, targetHeight = 480F), ) check(previewPngData.hasRemaining()) { "edit shapes preview export is empty" } return EditShapesResult( supportsGraphicShape = supportsGraphicShape, supportsTextShape = supportsTextShape, initialShapeType = initialShapeType, cornerRadiusTopLeft = cornerRadiusTopLeft, replacementShapeType = replacementShapeType, starProperties = starProperties, starPoints = starPoints, starInnerDiameter = starInnerDiameter, fillColor = appliedFillColor, positionX = positionX, positionY = positionY, width = width, height = height, rotation = rotation, previewPngData = previewPngData, ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-edit-shapes/EditShapesResult.kt reference-only import ly.img.engine.RGBAColor import java.nio.ByteBuffer data class EditShapesResult( val supportsGraphicShape: Boolean, val supportsTextShape: Boolean, val initialShapeType: String, val cornerRadiusTopLeft: Float, val replacementShapeType: String, val starProperties: List, val starPoints: Int, val starInnerDiameter: Float, val fillColor: RGBAColor, val positionX: Float, val positionY: Float, val width: Float, val height: Float, val rotation: Float, val previewPngData: ByteBuffer, ) ``` Edit graphic block shapes by replacing their geometry, changing shape-specific properties, updating fills, and transforming the block. ![Edited star shape preview](./assets/edit-shapes-android.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-edit-shapes) The snippets use an existing `graphic` block with a rectangle shape and color fill. Shape APIs operate on the shape block attached to the graphic block, while size, position, rotation, and fills remain properties of the graphic block itself. ## Accessing Shapes Check whether a block supports shape editing before reading or replacing its shape. Graphic blocks support shapes, while blocks such as text do not. ```kotlin highlight-android-access-shapes val supportsGraphicShape = engine.block.supportsShape(block = graphic) val text = engine.block.create(DesignBlockType.Text) val supportsTextShape = engine.block.supportsShape(block = text) val currentShape = engine.block.getShape(block = graphic) val initialShapeType = engine.block.getType(block = currentShape) ``` Use the returned shape handle with normal block APIs such as `getType()` and `findAllProperties()`. ## Editing Shape Geometry Rectangle corner radius is stored on the attached rectangle shape. Set each corner explicitly when you need rounded corners. ```kotlin highlight-android-rounded-corners engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusTL", value = 24F) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusTR", value = 24F) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusBR", value = 24F) engine.block.setFloat(block = currentShape, property = "shape/rect/cornerRadiusBL", value = 24F) ``` To replace the geometry, create another shape type and assign it with `setShape()`. The previous shape is not destroyed automatically after replacement, so destroy it when you no longer need it. ```kotlin highlight-android-replace-shape val starShape = engine.block.createShape(type = ShapeType.Star) engine.block.setShape(block = graphic, shape = starShape) engine.block.destroy(block = currentShape) val replacementShapeType = engine.block.getType(block = engine.block.getShape(block = graphic)) ``` ## Editing Shape Properties Different shape types expose different property keys. Use `findAllProperties()` to discover the available keys, then use the setter that matches the property type. ```kotlin highlight-android-shape-properties val starProperties = engine.block.findAllProperties(block = starShape) engine.block.setInt(block = starShape, property = "shape/star/points", value = 6) engine.block.setFloat(block = starShape, property = "shape/star/innerDiameter", value = 0.45F) ``` For example, star shapes expose `shape/star/points` and `shape/star/innerDiameter`, while rectangle shapes expose one corner-radius property per corner. ## Editing Fill Color Shape color comes from the graphic block's fill. Read the fill block, then set the `fill/color/value` color property. ```kotlin highlight-android-fill-color val fill = engine.block.getFill(block = graphic) val fillColor = Color.fromRGBA(r = 0.1F, g = 0.46F, b = 0.85F, a = 1F) engine.block.setColor(block = fill, property = "fill/color/value", value = fillColor) ``` Use normalized RGBA components from `0F` to `1F` when creating colors with `Color.fromRGBA()`. ## Moving and Resizing Shapes Move, resize, and rotate the graphic block rather than the attached shape block. Position and dimensions use design units; rotation uses radians. ```kotlin highlight-android-transform-shape engine.block.setPositionX(block = graphic, value = 160F) engine.block.setPositionY(block = graphic, value = 120F) engine.block.setWidth(block = graphic, value = 320F) engine.block.setHeight(block = graphic, value = 220F) val positionX = engine.block.getPositionX(block = graphic) val positionY = engine.block.getPositionY(block = graphic) val width = engine.block.getWidth(block = graphic) val height = engine.block.getHeight(block = graphic) val rotationRadians = (15F * PI / 180F).toFloat() engine.block.setRotation(block = graphic, radians = rotationRadians) ``` ## Troubleshooting ### Shape Not Changing - Verify `supportsShape()` returns `true` for the block you are editing. - Call `setShape()` on the graphic block, not on the shape block. - Destroy the previous shape only after the new shape is assigned if you do not need to reuse it. ### Property Update Not Applying - Use `findAllProperties()` on the current shape block to confirm the property key exists. - Match the setter to the property type, for example `setInt()` for `shape/star/points` and `setFloat()` for corner radius values. - Remember that shape-specific properties change when you replace one shape type with another. ### Fill or Transform Looks Wrong - Update `fill/color/value` on the fill block returned by `getFill()`. - Apply size, position, and rotation to the graphic block. - Make sure the graphic block is attached to a page before relying on rendered output. ## API Reference | Method | Description | |--------|-------------| | `engine.block.create(blockType=_)` | Create a design block such as a graphic or text block | | `engine.block.supportsShape(block=_)` | Check whether a block has an editable shape | | `engine.block.getShape(block=_)` | Get the shape block attached to a graphic block | | `engine.block.getType(block=_)` | Read the type key of a block or shape block | | `engine.block.findAllProperties(block=_)` | List the property keys exposed by a block or shape | | `engine.block.createShape(type=_)` | Create a shape block such as `ShapeType.Rect` or `ShapeType.Star` | | `engine.block.setShape(block=_, shape=_)` | Attach a shape block to a graphic block | | `engine.block.destroy(block=_)` | Destroy an unused block or detached shape | | `engine.block.setFloat(block=_, property="shape/rect/cornerRadiusTL", value=_)` | Set float shape properties such as rectangle corner radius | | `engine.block.getFloat(block=_, property="shape/rect/cornerRadiusTL")` | Read float shape properties | | `engine.block.setInt(block=_, property="shape/star/points", value=_)` | Set integer shape properties | | `engine.block.getInt(block=_, property="shape/star/points")` | Read integer shape properties | | `engine.block.getFill(block=_)` | Get the fill block attached to a graphic block | | `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an RGBA color from normalized channel values | | `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set the color value on a color fill | | `engine.block.getColor(block=_, property="fill/color/value")` | Read the color value from a color fill | | `engine.block.setPositionX(block=_, value=_)` | Set the graphic block's x position | | `engine.block.getPositionX(block=_)` | Read the graphic block's x position | | `engine.block.setPositionY(block=_, value=_)` | Set the graphic block's y position | | `engine.block.getPositionY(block=_)` | Read the graphic block's y position | | `engine.block.setWidth(block=_, value=_)` | Set the graphic block's width | | `engine.block.getWidth(block=_)` | Read the graphic block's width | | `engine.block.setHeight(block=_, value=_)` | Set the graphic block's height | | `engine.block.getHeight(block=_)` | Read the graphic block's height | | `engine.block.setRotation(block=_, radians=_)` | Rotate the graphic block around its center | | `engine.block.getRotation(block=_)` | Read the graphic block's rotation | ## Next Steps - [Create Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0/) — Create and configure geometric shapes programmatically. - [Edit Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-stickers-609679/) — Edit existing sticker blocks by changing their image fill and transforms. --- ## 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 Stickers" description: "Modify stickers programmatically by swapping the image, transforming the block, changing appearance, and duplicating it." platform: android url: "https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-stickers-609679/" --- > 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 Stickers](https://img.ly/docs/cesdk/android/stickers-3d4e5f/) > [Edit Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-stickers-609679/) --- ```kotlin file=@cesdk_android_examples/engine-guides-edit-stickers/EditStickers.kt reference-only import android.net.Uri import ly.img.engine.BlurType import ly.img.engine.Color import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.EffectType 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 kotlin.math.PI import kotlin.math.abs suspend fun editStickers(engine: Engine): EditStickersResult { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(block = page, value = 450F) engine.block.setHeight(block = page, value = 250F) engine.block.appendChild(parent = scene, child = page) val sticker = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = sticker, shape = engine.block.createShape(ShapeType.Rect)) val stickerFill = engine.block.createFill(FillType.Image) engine.block.setUri( block = stickerFill, property = "fill/image/imageFileURI", value = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ), ) engine.block.setFill(block = sticker, fill = stickerFill) require(engine.block.supportsContentFillMode(block = sticker)) { "Sticker graphic blocks must support content fill modes." } engine.block.setContentFillMode(block = sticker, mode = ContentFillMode.CONTAIN) engine.block.setKind(block = sticker, kind = "sticker") engine.block.setWidth(block = sticker, value = 120F) engine.block.setHeight(block = sticker, value = 120F) engine.block.setPositionX(block = sticker, value = 150F) engine.block.setPositionY(block = sticker, value = 65F) engine.block.appendChild(parent = page, child = sticker) val fill = engine.block.getFill(block = sticker) val replacementUri = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = replacementUri, ) val currentImageUri = engine.block.getUri( block = fill, property = "fill/image/imageFileURI", ) check(currentImageUri == replacementUri) engine.block.setWidth(block = sticker, value = 120F) engine.block.setHeight(block = sticker, value = 120F) engine.block.setFlipHorizontal(block = sticker, flip = true) engine.block.setRotation(block = sticker, radians = (PI / 12).toFloat()) engine.block.setPositionX(block = sticker, value = 150F) engine.block.setPositionY(block = sticker, value = 65F) val positionX = engine.block.getPositionX(block = sticker) val positionY = engine.block.getPositionY(block = sticker) val width = engine.block.getWidth(block = sticker) val height = engine.block.getHeight(block = sticker) val rotation = engine.block.getRotation(block = sticker) val isFlippedHorizontally = engine.block.isFlipHorizontal(block = sticker) check(abs(positionX - 150F) < 0.001F) { "Expected sticker x=150.0, got $positionX." } check(abs(positionY - 65F) < 0.001F) { "Expected sticker y=65.0, got $positionY." } check(abs(width - 120F) < 0.001F) { "Expected sticker width=120.0, got $width." } check(abs(height - 120F) < 0.001F) { "Expected sticker height=120.0, got $height." } check(abs(rotation - (PI / 12).toFloat()) < 0.001F) { "Expected sticker rotation=${(PI / 12).toFloat()}, got $rotation." } check(isFlippedHorizontally) { "Expected sticker to be flipped horizontally." } if (engine.block.supportsContentFillMode(block = sticker)) { engine.block.setContentFillMode(block = sticker, mode = ContentFillMode.COVER) } val coverMode = engine.block.getContentFillMode(block = sticker) check(coverMode == ContentFillMode.COVER) engine.block.setContentFillMode(block = sticker, mode = ContentFillMode.CONTAIN) val finalContentFillMode = engine.block.getContentFillMode(block = sticker) require(engine.block.supportsOpacity(block = sticker)) { "Sticker graphic blocks must support opacity." } engine.block.setOpacity(block = sticker, value = 0.88F) val opacity = engine.block.getOpacity(block = sticker) check(abs(opacity - 0.88F) < 0.001F) require(engine.block.supportsDropShadow(block = sticker)) { "Sticker graphic blocks must support drop shadows." } engine.block.setDropShadowEnabled(block = sticker, enabled = true) engine.block.setDropShadowColor( block = sticker, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.4F), ) engine.block.setDropShadowOffsetX(block = sticker, offsetX = 4F) engine.block.setDropShadowOffsetY(block = sticker, offsetY = 4F) engine.block.setDropShadowBlurRadiusX(block = sticker, blurRadiusX = 6F) engine.block.setDropShadowBlurRadiusY(block = sticker, blurRadiusY = 6F) val dropShadowEnabled = engine.block.isDropShadowEnabled(block = sticker) val dropShadowColor = engine.block.getDropShadowColor(block = sticker) val dropShadowOffsetX = engine.block.getDropShadowOffsetX(block = sticker) val dropShadowOffsetY = engine.block.getDropShadowOffsetY(block = sticker) val dropShadowBlurRadiusX = engine.block.getDropShadowBlurRadiusX(block = sticker) val dropShadowBlurRadiusY = engine.block.getDropShadowBlurRadiusY(block = sticker) check(dropShadowEnabled) check(dropShadowOffsetX == 4F) check(dropShadowOffsetY == 4F) check(dropShadowBlurRadiusX == 6F) check(dropShadowBlurRadiusY == 6F) val copy = engine.block.duplicate(block = sticker) engine.block.setPositionX(block = copy, value = 300F) engine.block.setPositionY(block = copy, value = 65F) val duplicateKind = engine.block.getKind(block = copy) val duplicatePositionX = engine.block.getPositionX(block = copy) val duplicatePositionY = engine.block.getPositionY(block = copy) check(duplicateKind == "sticker") check(duplicatePositionX == 300F) check(duplicatePositionY == 65F) require(engine.block.supportsStroke(block = sticker)) { "Sticker graphic blocks must support strokes." } engine.block.setStrokeEnabled(block = sticker, enabled = true) engine.block.setStrokeColor( block = sticker, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F), ) engine.block.setStrokeWidth(block = sticker, width = 4F) val strokeEnabled = engine.block.isStrokeEnabled(block = sticker) val strokeColor = engine.block.getStrokeColor(block = sticker) val strokeWidth = engine.block.getStrokeWidth(block = sticker) check(strokeEnabled) check(strokeWidth == 4F) require(engine.block.supportsBlur(block = sticker)) { "Sticker graphic blocks must support blur." } val blur = engine.block.createBlur(type = BlurType.Uniform) engine.block.setBlur(block = sticker, blurBlock = blur) engine.block.setBlurEnabled(block = sticker, enabled = true) val blurEnabled = engine.block.isBlurEnabled(block = sticker) check(blurEnabled) require(engine.block.supportsEffects(block = sticker)) { "Sticker graphic blocks must support effects." } val glow = engine.block.createEffect(type = EffectType.Glow) engine.block.appendEffect(block = sticker, effectBlock = glow) val effects = engine.block.getEffects(block = sticker) val effectEnabled = engine.block.isEffectEnabled(effectBlock = glow) check(effects.contains(glow)) check(effectEnabled) engine.block.forceLoadResources(listOf(page, sticker, copy)) val previewPngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 900F, targetHeight = 500F), ) check(previewPngData.hasRemaining()) { "edit stickers preview export is empty" } return EditStickersResult( currentImageUri = currentImageUri, positionX = positionX, positionY = positionY, width = width, height = height, rotation = rotation, isFlippedHorizontally = isFlippedHorizontally, coverMode = coverMode, finalContentFillMode = finalContentFillMode, opacity = opacity, dropShadowEnabled = dropShadowEnabled, dropShadowColor = dropShadowColor, dropShadowOffsetX = dropShadowOffsetX, dropShadowOffsetY = dropShadowOffsetY, dropShadowBlurRadiusX = dropShadowBlurRadiusX, dropShadowBlurRadiusY = dropShadowBlurRadiusY, duplicateKind = duplicateKind, duplicatePositionX = duplicatePositionX, duplicatePositionY = duplicatePositionY, strokeEnabled = strokeEnabled, strokeColor = strokeColor, strokeWidth = strokeWidth, blurEnabled = blurEnabled, effectCount = effects.size, effectEnabled = effectEnabled, previewPngData = previewPngData, ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-edit-stickers/EditStickersResult.kt reference-only import android.net.Uri import ly.img.engine.Color import ly.img.engine.ContentFillMode import java.nio.ByteBuffer data class EditStickersResult( val currentImageUri: Uri, val positionX: Float, val positionY: Float, val width: Float, val height: Float, val rotation: Float, val isFlippedHorizontally: Boolean, val coverMode: ContentFillMode, val finalContentFillMode: ContentFillMode, val opacity: Float, val dropShadowEnabled: Boolean, val dropShadowColor: Color, val dropShadowOffsetX: Float, val dropShadowOffsetY: Float, val dropShadowBlurRadiusX: Float, val dropShadowBlurRadiusY: Float, val duplicateKind: String, val duplicatePositionX: Float, val duplicatePositionY: Float, val strokeEnabled: Boolean, val strokeColor: Color, val strokeWidth: Float, val blurEnabled: Boolean, val effectCount: Int, val effectEnabled: Boolean, val previewPngData: ByteBuffer, ) ``` Edit stickers after they have been placed in a scene by swapping their source image, transforming the block, changing appearance, and duplicating the configured sticker. ![Edited Android sticker output with duplicate stickers, a drop shadow, white stroke, blur, and glow effect.](./assets/edit-stickers-android.png) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-edit-stickers) Stickers are graphic blocks with image fills and the `"sticker"` kind tag. After a sticker exists in a scene, use block APIs for transforms, opacity, shadows, strokes, blur, effects, and duplication. Use the sticker's image fill when you need to replace the source artwork. ## Replacing the Sticker Image Swap the artwork by reading the sticker's fill with `getFill()` and writing a new URI to `fill/image/imageFileURI`. The block keeps its size, transform, appearance, and `"sticker"` kind. ```kotlin highlight-android-replace-image val fill = engine.block.getFill(block = sticker) val replacementUri = Uri.parse( "file:///android_asset/imgly-assets/" + "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = replacementUri, ) val currentImageUri = engine.block.getUri( block = fill, property = "fill/image/imageFileURI", ) ``` Set the URI on the fill returned by `getFill()`, not on the sticker block itself. The property belongs to the image fill. ## Transforming Stickers Move, resize, rotate, and flip the sticker through the same transform setters used by other graphic blocks. Position and size use design units, and rotation uses radians. ```kotlin highlight-android-transform engine.block.setWidth(block = sticker, value = 120F) engine.block.setHeight(block = sticker, value = 120F) engine.block.setFlipHorizontal(block = sticker, flip = true) engine.block.setRotation(block = sticker, radians = (PI / 12).toFloat()) engine.block.setPositionX(block = sticker, value = 150F) engine.block.setPositionY(block = sticker, value = 65F) val positionX = engine.block.getPositionX(block = sticker) val positionY = engine.block.getPositionY(block = sticker) val width = engine.block.getWidth(block = sticker) val height = engine.block.getHeight(block = sticker) val rotation = engine.block.getRotation(block = sticker) val isFlippedHorizontally = engine.block.isFlipHorizontal(block = sticker) ``` `setRotation()` affects the rotated bounds. When you need exact final placement, apply the rotation before setting the final `setPositionX()` and `setPositionY()` values. The sample mirrors the sticker with `setFlipHorizontal()` and reads the state with `isFlipHorizontal()`. Use `setFlipVertical()` and `isFlipVertical()` for vertical mirroring. ## Changing the Content Fill Mode `ContentFillMode` controls how the image fits inside the sticker bounds. `CONTAIN` keeps the full sticker image visible, `COVER` fills the frame and may crop, and `CROP` enables manual crop positioning. ```kotlin highlight-android-content-fill-mode if (engine.block.supportsContentFillMode(block = sticker)) { engine.block.setContentFillMode(block = sticker, mode = ContentFillMode.COVER) } val coverMode = engine.block.getContentFillMode(block = sticker) ``` Check `supportsContentFillMode()` before setting the mode because the property only exists on content-bearing blocks. ## Adjusting Opacity Opacity changes the whole sticker block, including its image fill and appearance effects. Use values from `0F` for fully transparent to `1F` for fully opaque. ```kotlin highlight-android-opacity require(engine.block.supportsOpacity(block = sticker)) { "Sticker graphic blocks must support opacity." } engine.block.setOpacity(block = sticker, value = 0.88F) val opacity = engine.block.getOpacity(block = sticker) ``` ## Adding a Drop Shadow Drop shadows are block appearance properties, not fill properties. Enable the shadow, then set its color, offset, and blur radius. ```kotlin highlight-android-drop-shadow require(engine.block.supportsDropShadow(block = sticker)) { "Sticker graphic blocks must support drop shadows." } engine.block.setDropShadowEnabled(block = sticker, enabled = true) engine.block.setDropShadowColor( block = sticker, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.4F), ) engine.block.setDropShadowOffsetX(block = sticker, offsetX = 4F) engine.block.setDropShadowOffsetY(block = sticker, offsetY = 4F) engine.block.setDropShadowBlurRadiusX(block = sticker, blurRadiusX = 6F) engine.block.setDropShadowBlurRadiusY(block = sticker, blurRadiusY = 6F) val dropShadowEnabled = engine.block.isDropShadowEnabled(block = sticker) val dropShadowColor = engine.block.getDropShadowColor(block = sticker) val dropShadowOffsetX = engine.block.getDropShadowOffsetX(block = sticker) val dropShadowOffsetY = engine.block.getDropShadowOffsetY(block = sticker) val dropShadowBlurRadiusX = engine.block.getDropShadowBlurRadiusX(block = sticker) val dropShadowBlurRadiusY = engine.block.getDropShadowBlurRadiusY(block = sticker) ``` ## Duplicating Stickers `duplicate()` creates an independent copy with the same image fill, transform, kind, and appearance configuration. Move the copy afterward when only its placement should change. ```kotlin highlight-android-duplicate val copy = engine.block.duplicate(block = sticker) engine.block.setPositionX(block = copy, value = 300F) engine.block.setPositionY(block = copy, value = 65F) val duplicateKind = engine.block.getKind(block = copy) val duplicatePositionX = engine.block.getPositionX(block = copy) val duplicatePositionY = engine.block.getPositionY(block = copy) ``` ## Adding a Stroke Strokes draw around the sticker block bounds. Enable the stroke, then set the color and width. ```kotlin highlight-android-stroke require(engine.block.supportsStroke(block = sticker)) { "Sticker graphic blocks must support strokes." } engine.block.setStrokeEnabled(block = sticker, enabled = true) engine.block.setStrokeColor( block = sticker, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F), ) engine.block.setStrokeWidth(block = sticker, width = 4F) val strokeEnabled = engine.block.isStrokeEnabled(block = sticker) val strokeColor = engine.block.getStrokeColor(block = sticker) val strokeWidth = engine.block.getStrokeWidth(block = sticker) ``` The stroke follows the rectangular bounds of the graphic block. For stickers with transparent edges or irregular silhouettes, a drop shadow often looks more natural than an outline. ## Applying Blur Create a blur block, attach it to the sticker, and enable it. Disabling blur later keeps the blur block attached so you can re-enable the same configuration. ```kotlin highlight-android-blur require(engine.block.supportsBlur(block = sticker)) { "Sticker graphic blocks must support blur." } val blur = engine.block.createBlur(type = BlurType.Uniform) engine.block.setBlur(block = sticker, blurBlock = blur) engine.block.setBlurEnabled(block = sticker, enabled = true) val blurEnabled = engine.block.isBlurEnabled(block = sticker) ``` ## Applying Effects Effects attach to the sticker with `appendEffect()`. Multiple effects stack in the order you append them. ```kotlin highlight-android-effects require(engine.block.supportsEffects(block = sticker)) { "Sticker graphic blocks must support effects." } val glow = engine.block.createEffect(type = EffectType.Glow) engine.block.appendEffect(block = sticker, effectBlock = glow) val effects = engine.block.getEffects(block = sticker) val effectEnabled = engine.block.isEffectEnabled(effectBlock = glow) ``` Use typed Android effect constants such as `EffectType.TiltShift`, `EffectType.Glow`, `EffectType.Pixelize`, or `EffectType.Posterize` instead of raw type strings. ## Limitations Stickers preserve the colors of their source image. This means: - **Recoloring is not supported.** Sticker fills are image fills, not solid color fills. - **Boolean operations are not supported.** Combining blocks works on vector path geometry, not image fills. If you need recolorable or combinable artwork, use shapes instead. See [Edit Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/). ## Troubleshooting ### Sticker Image Did Not Change Verify that the new URI points to a supported image format and that `setUri()` runs on the fill returned by `getFill()`. Writing `fill/image/imageFileURI` on the sticker block does not update the image fill. ### Sticker Appears Cropped or Padded Use `ContentFillMode.CONTAIN` to keep the whole image visible. Use `ContentFillMode.COVER` when the image should fill the frame and cropping is acceptable. ### Drop Shadow or Stroke Is Not Visible Check that the feature is enabled and that the alpha, stroke width, and shadow blur radius are greater than zero. ### Sticker Cannot Be Recolored This is expected. Stickers use image fills that preserve source colors. Use a shape with a color or gradient fill when your users need recoloring. ## API Reference | Method | Description | | --- | --- | | `engine.block.getFill(block=_)` | Get the image fill attached to a sticker | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the sticker image URI on the fill | | `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Read the current sticker image URI from the fill | | `engine.block.supportsContentFillMode(block=_)` | Check whether the block supports content fill modes | | `engine.block.setContentFillMode(block=_, mode=_)` | Switch between `CONTAIN`, `COVER`, and `CROP` | | `engine.block.getContentFillMode(block=_)` | Read the current content fill mode | | `engine.block.setPositionX(block=_, value=_)` | Set the sticker's x position | | `engine.block.getPositionX(block=_)` | Read the sticker's x position | | `engine.block.setPositionY(block=_, value=_)` | Set the sticker's y position | | `engine.block.getPositionY(block=_)` | Read the sticker's y position | | `engine.block.setWidth(block=_, value=_)` | Set the sticker width | | `engine.block.getWidth(block=_)` | Read the sticker width | | `engine.block.setHeight(block=_, value=_)` | Set the sticker height | | `engine.block.getHeight(block=_)` | Read the sticker height | | `engine.block.setRotation(block=_, radians=_)` | Rotate the sticker around its center | | `engine.block.getRotation(block=_)` | Read the sticker rotation in radians | | `engine.block.setFlipHorizontal(block=_, flip=_)` | Mirror the sticker horizontally | | `engine.block.isFlipHorizontal(block=_)` | Read the horizontal flip state | | `engine.block.setFlipVertical(block=_, flip=_)` | Mirror the sticker vertically | | `engine.block.isFlipVertical(block=_)` | Read the vertical flip state | | `engine.block.supportsOpacity(block=_)` | Check whether the block supports opacity | | `engine.block.setOpacity(block=_, value=_)` | Set the sticker opacity | | `engine.block.getOpacity(block=_)` | Read the sticker opacity | | `engine.block.supportsDropShadow(block=_)` | Check whether the block supports drop shadows | | `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Toggle the drop shadow | | `engine.block.isDropShadowEnabled(block=_)` | Read the drop shadow state | | `engine.block.setDropShadowColor(block=_, color=_)` | Set the drop shadow color | | `engine.block.getDropShadowColor(block=_)` | Read the drop shadow color | | `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set the drop shadow x offset | | `engine.block.getDropShadowOffsetX(block=_)` | Read the drop shadow x offset | | `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set the drop shadow y offset | | `engine.block.getDropShadowOffsetY(block=_)` | Read the drop shadow y offset | | `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Set the drop shadow x blur radius | | `engine.block.getDropShadowBlurRadiusX(block=_)` | Read the drop shadow x blur radius | | `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Set the drop shadow y blur radius | | `engine.block.getDropShadowBlurRadiusY(block=_)` | Read the drop shadow y blur radius | | `engine.block.duplicate(block=_)` | Duplicate the sticker and attach the copy to the same parent | | `engine.block.getKind(block=_)` | Read the `"sticker"` kind from the duplicate | | `engine.block.supportsStroke(block=_)` | Check whether the block supports strokes | | `engine.block.setStrokeEnabled(block=_, enabled=_)` | Toggle the stroke | | `engine.block.isStrokeEnabled(block=_)` | Read the stroke state | | `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color | | `engine.block.getStrokeColor(block=_)` | Read the stroke color | | `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke width | | `engine.block.getStrokeWidth(block=_)` | Read the stroke width | | `engine.block.supportsBlur(block=_)` | Check whether the block supports blur | | `engine.block.createBlur(type=BlurType.Uniform)` | Create a uniform blur block | | `engine.block.setBlur(block=_, blurBlock=_)` | Attach a blur block to the sticker | | `engine.block.getBlur(block=_)` | Read the attached blur block | | `engine.block.setBlurEnabled(block=_, enabled=_)` | Toggle the attached blur | | `engine.block.isBlurEnabled(block=_)` | Read the blur state | | `engine.block.supportsEffects(block=_)` | Check whether the block supports effects | | `engine.block.createEffect(type=EffectType.Glow)` | Create a typed effect block | | `engine.block.appendEffect(block=_, effectBlock=_)` | Attach an effect block to the sticker | | `engine.block.getEffects(block=_)` | Read the attached effect blocks | | `engine.block.isEffectEnabled(effectBlock=_)` | Read whether an effect block is enabled | ## Next Steps - [Create Stickers](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-stickers-cc46e5/) - Build stickers programmatically from image URLs. - [Edit Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) - Modify recolorable, combinable shapes with the same transform APIs. - [Combine Shapes](https://img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26/) - Use boolean operations to merge vector shapes. --- ## 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" description: "Add, style, and customize text layers in your design using CE.SDK’s flexible text editing tools." platform: android url: "https://img.ly/docs/cesdk/android/text-8a993a/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/text/overview-0bd620/) - Add, style, and customize text layers in your design using CE.SDK’s flexible text editing tools. - [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) - Create and configure text blocks with custom fonts, rich text styling, and dynamic sizing options. - [Edit Text](https://img.ly/docs/cesdk/android/text/edit-c5106b/) - Edit text content directly on the canvas or through the properties panel. - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Text Decorations](https://img.ly/docs/cesdk/android/text/decorations-d3c0a1/) - Add underline, strikethrough, and overline decorations to text with customizable styles, colors, and thickness. - [Text on a Path](https://img.ly/docs/cesdk/android/text/text-on-path-e3b8a2/) - Place text along an SVG path — a circle, arch, wave, or any curve — using the built-in Text on Path sheet or the setTextOnPath engine API. - [Text Designs](https://img.ly/docs/cesdk/android/text/text-designs-a1b2c3/) - Create and customize text component libraries using predefined text designs that appear in your asset library. - [Text Enumerations](https://img.ly/docs/cesdk/android/text/enumerations-b5c1d2/) - Add bullet lists and numbered lists to text blocks in CE.SDK using per-paragraph list styles and nesting levels. - [Auto-Size](https://img.ly/docs/cesdk/android/text/auto-size-5331b3/) - Configure text blocks to automatically adapt their dimensions or font size for dynamic content. - [Text Effects](https://img.ly/docs/cesdk/android/text/effects-2dc9fc/) - Add visual depth and interest to text blocks using drop shadows and stroke outlines. - [Emojis](https://img.ly/docs/cesdk/android/text/emojis-510651/) - Insert and style emojis alongside text for expressive, modern typographic designs. - [Adjust Text Spacing](https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/) - Control letter spacing, line height, paragraph line height, and paragraph spacing in text blocks. - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) - Load and manage custom fonts to match brand guidelines or user preferences. - [Variable Fonts](https://img.ly/docs/cesdk/android/text/variable-fonts-32e788/) - Use variable fonts to render multiple font weights and styles from a single font file. - [Text and Language Support](https://img.ly/docs/cesdk/android/text/language-support-a0f010/) - Create designs that work across languages and writing systems with RTL text, complex scripts, and multilingual font support. --- ## 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 Text" description: "Create and configure text blocks with custom fonts, rich text styling, and dynamic sizing options." platform: android url: "https://img.ly/docs/cesdk/android/text/add-4f5011/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-add/AddText.kt reference-only import android.net.Uri import ly.img.editor.defaultBaseUri import ly.img.engine.Color import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.HorizontalAlignment import ly.img.engine.SizeMode import ly.img.engine.TextCase import ly.img.engine.Typeface data class AddText( val titleText: String, val richTextColors: List, val richTextWeights: List, val richTextStyles: List, val richTextFontSizes: List, val richTextCases: List, val richTextTypefaceCount: Int, val richTextTypefaceNames: List, val autoSizeWidthMode: SizeMode, val autoSizeHeightMode: SizeMode, val caseTransform: TextCase, val toggleCanBold: Boolean, val toggleCanItalic: Boolean, val toggledWeights: List, val toggledStyles: List, val modifiedText: String, val horizontalAlignment: HorizontalAlignment?, val lineHeight: Float, val letterSpacing: Float, ) fun addText(engine: Engine): AddText { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 1100F) engine.block.appendChild(parent = scene, child = page) val margin = 40F val fontSize = 80F val lineSpacing = 100F val pageWidth = engine.block.getWidth(page) val titleBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = titleBlock) engine.block.replaceText(titleBlock, text = "Welcome to CE.SDK") engine.block.setWidthMode(titleBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(titleBlock, mode = SizeMode.AUTO) engine.block.setPositionX(titleBlock, value = margin) engine.block.setPositionY(titleBlock, value = margin) // Use a reachable location for the CE.SDK font assets. val cesdkTypefaceBaseUri = defaultBaseUri .buildUpon() .appendPath("ly.img.typeface") .appendPath("fonts") .build() fun fontUri( family: String, fileName: String, ): Uri = cesdkTypefaceBaseUri .buildUpon() .appendPath(family) .appendPath(fileName) .build() val caveatRegular = Font( uri = fontUri(family = "Caveat", fileName = "Caveat-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ) val caveatBold = Font( uri = fontUri(family = "Caveat", fileName = "Caveat-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ) val caveatTypeface = Typeface(name = "Caveat", fonts = listOf(caveatRegular, caveatBold)) engine.block.setFont(titleBlock, fontFileUri = caveatBold.uri, typeface = caveatTypeface) engine.block.setTextFontSize(titleBlock, fontSize = fontSize) val robotoTypeface = Typeface( name = "Roboto", fonts = listOf( Font( uri = fontUri(family = "Roboto", fileName = "Roboto-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), Font( uri = fontUri(family = "Roboto", fileName = "Roboto-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = fontUri(family = "Roboto", fileName = "Roboto-Italic.ttf"), subFamily = "Italic", weight = FontWeight.NORMAL, style = FontStyle.ITALIC, ), Font( uri = fontUri(family = "Roboto", fileName = "Roboto-BoldItalic.ttf"), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), ), ) val robotoRegular = robotoTypeface.fonts.firstOrNull { it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL } ?: error("Roboto regular font variant is required for text styling.") val richTextBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = richTextBlock) engine.block.replaceText(richTextBlock, text = "Rich text with colors and styles") engine.block.setPositionX(richTextBlock, value = margin) engine.block.setPositionY(richTextBlock, value = margin + lineSpacing) engine.block.setWidthMode(richTextBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(richTextBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(richTextBlock, fontSize = fontSize) engine.block.setFont( block = richTextBlock, fontFileUri = robotoRegular.uri, typeface = robotoTypeface, ) // "Rich" (UTF-16 range [0, 4)) in blue. engine.block.setTextColor( block = richTextBlock, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F), from = 0, to = 4, ) // "text" (UTF-16 range [5, 9)) in bold. engine.block.setTextFontWeight( block = richTextBlock, fontWeight = FontWeight.BOLD, from = 5, to = 9, ) // "with" (UTF-16 range [10, 14)) in italic. engine.block.setTextFontStyle( block = richTextBlock, fontStyle = FontStyle.ITALIC, from = 10, to = 14, ) // "colors" (UTF-16 range [15, 21)) in orange and larger type. engine.block.setTextColor( block = richTextBlock, color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.1F, a = 1F), from = 15, to = 21, ) engine.block.setTextFontSize( block = richTextBlock, fontSize = 96F, from = 15, to = 21, ) // "and" (UTF-16 range [22, 25)) uses another typeface. engine.block.setTypeface( block = richTextBlock, typeface = caveatTypeface, from = 22, to = 25, ) // "styles" (UTF-16 range [26, 32)) in green uppercase. engine.block.setTextColor( block = richTextBlock, color = Color.fromRGBA(r = 0.2F, g = 0.7F, b = 0.3F, a = 1F), from = 26, to = 32, ) engine.block.setTextCase( block = richTextBlock, textCase = TextCase.UPPER_CASE, from = 26, to = 32, ) val autoSizeBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = autoSizeBlock) engine.block.replaceText(autoSizeBlock, text = "Auto-sizing text block") engine.block.setPositionX(autoSizeBlock, value = margin) engine.block.setPositionY(autoSizeBlock, value = margin + lineSpacing * 2) engine.block.setWidth(autoSizeBlock, value = pageWidth - margin * 2) engine.block.setWidthMode(autoSizeBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(autoSizeBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(autoSizeBlock, fontSize = fontSize) val caseBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = caseBlock) engine.block.replaceText(caseBlock, text = "uppercase text") engine.block.setPositionX(caseBlock, value = margin) engine.block.setPositionY(caseBlock, value = margin + lineSpacing * 3) engine.block.setWidthMode(caseBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(caseBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(caseBlock, fontSize = fontSize) engine.block.setTextCase(caseBlock, textCase = TextCase.UPPER_CASE) val alignedBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = alignedBlock) engine.block.replaceText(alignedBlock, text = "Centered Text\nWith Line Spacing") engine.block.setPositionX(alignedBlock, value = margin) engine.block.setPositionY(alignedBlock, value = margin + lineSpacing * 4) engine.block.setWidth(alignedBlock, value = pageWidth - margin * 2) engine.block.setWidthMode(alignedBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(alignedBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(alignedBlock, fontSize = fontSize) engine.block.setTextHorizontalAlignment(alignedBlock, alignment = HorizontalAlignment.Center) engine.block.setTextLineHeight(alignedBlock, lineHeight = 1.5F) engine.block.setFloat(alignedBlock, property = "text/letterSpacing", value = 0.05F) val lineHeight = engine.block.getTextLineHeight(alignedBlock, paragraphIndex = 0) val letterSpacing = engine.block.getFloat(alignedBlock, property = "text/letterSpacing") val toggleBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = toggleBlock) engine.block.replaceText(toggleBlock, text = "Toggle Bold and Italic") engine.block.setPositionX(toggleBlock, value = margin) engine.block.setPositionY(toggleBlock, value = margin + lineSpacing * 7) engine.block.setWidthMode(toggleBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(toggleBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(toggleBlock, fontSize = fontSize) engine.block.setFont(toggleBlock, fontFileUri = robotoRegular.uri, typeface = robotoTypeface) val canToggleBold = engine.block.canToggleBoldFont(toggleBlock, from = 7, to = 11) if (canToggleBold) { engine.block.toggleBoldFont(toggleBlock, from = 7, to = 11) } val canToggleItalic = engine.block.canToggleItalicFont(toggleBlock, from = 16, to = 22) if (canToggleItalic) { engine.block.toggleItalicFont(toggleBlock, from = 16, to = 22) } val modifyBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = modifyBlock) engine.block.replaceText(modifyBlock, text = "Hello World") engine.block.setPositionX(modifyBlock, value = margin) engine.block.setPositionY(modifyBlock, value = margin + lineSpacing * 8) engine.block.setWidthMode(modifyBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(modifyBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(modifyBlock, fontSize = fontSize) engine.block.replaceText(modifyBlock, text = "CE.SDK", from = 6, to = 11) val richTextTypefaces = engine.block.getTypefaces(richTextBlock, from = 22, to = 25) return AddText( titleText = engine.block.getString(titleBlock, property = "text/text"), richTextColors = listOf( engine.block.getTextColors(richTextBlock, from = 0, to = 4).single(), engine.block.getTextColors(richTextBlock, from = 15, to = 21).single(), engine.block.getTextColors(richTextBlock, from = 26, to = 32).single(), ), richTextWeights = engine.block.getTextFontWeights(richTextBlock, from = 5, to = 9), richTextStyles = engine.block.getTextFontStyles(richTextBlock, from = 10, to = 14), richTextFontSizes = engine.block.getTextFontSizes(richTextBlock, from = 15, to = 21), richTextCases = engine.block.getTextCases(richTextBlock, from = 26, to = 32), richTextTypefaceCount = richTextTypefaces.size, richTextTypefaceNames = richTextTypefaces.map { it.name }, autoSizeWidthMode = engine.block.getWidthMode(autoSizeBlock), autoSizeHeightMode = engine.block.getHeightMode(autoSizeBlock), caseTransform = engine.block.getTextCases(caseBlock).first(), toggleCanBold = canToggleBold, toggleCanItalic = canToggleItalic, toggledWeights = engine.block.getTextFontWeights(toggleBlock, from = 7, to = 11), toggledStyles = engine.block.getTextFontStyles(toggleBlock, from = 16, to = 22), modifiedText = engine.block.getString(modifyBlock, property = "text/text"), horizontalAlignment = engine.block.getTextHorizontalAlignment(alignedBlock), lineHeight = lineHeight, letterSpacing = letterSpacing, ) } ``` Create and configure text blocks in CE.SDK with custom fonts, rich text styling, and dynamic sizing options. ![Rendered text scene with styled text blocks](./assets/android.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-add) Text blocks are fundamental design elements for displaying titles, captions, labels, and body text. CE.SDK provides range-based styling APIs so a single text block can contain multiple colors, font weights, font styles, and text cases. The snippets below assume you already have an `Engine` instance and the target page block you want to add text to. ## Create Text Blocks Create a text block with `engine.block.create(DesignBlockType.Text)` and attach it to a page with `appendChild()`. Use `replaceText()` for both initial text and later text replacements. ```kotlin highlight-android-create-text val titleBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = titleBlock) engine.block.replaceText(titleBlock, text = "Welcome to CE.SDK") engine.block.setWidthMode(titleBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(titleBlock, mode = SizeMode.AUTO) engine.block.setPositionX(titleBlock, value = margin) engine.block.setPositionY(titleBlock, value = margin) ``` Text blocks can use automatic sizing. Set the width and height modes to `SizeMode.AUTO` when the block should fit its text content. ## Apply Fonts and Typefaces Apply a font with `setFont()`. Android expects both the concrete font file `Uri` and a `Typeface` that describes the font family and available variants. ```kotlin highlight-android-set-font // Use a reachable location for the CE.SDK font assets. val cesdkTypefaceBaseUri = defaultBaseUri .buildUpon() .appendPath("ly.img.typeface") .appendPath("fonts") .build() fun fontUri( family: String, fileName: String, ): Uri = cesdkTypefaceBaseUri .buildUpon() .appendPath(family) .appendPath(fileName) .build() val caveatRegular = Font( uri = fontUri(family = "Caveat", fileName = "Caveat-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ) val caveatBold = Font( uri = fontUri(family = "Caveat", fileName = "Caveat-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ) val caveatTypeface = Typeface(name = "Caveat", fonts = listOf(caveatRegular, caveatBold)) engine.block.setFont(titleBlock, fontFileUri = caveatBold.uri, typeface = caveatTypeface) engine.block.setTextFontSize(titleBlock, fontSize = fontSize) ``` Use `setFont()` when you want to replace the block's active font file. Use `setTypeface()` when you want CE.SDK to keep current formatting, such as bold or italic, as far as the new typeface supports it. The sample uses public CE.SDK CDN font assets so the code can run as published. Replace the base URI with the location where your integration bundles or hosts CE.SDK assets. When later snippets apply weight, style, or toggle formatting, use a typeface that includes the variants those operations need. ```kotlin highlight-android-font-variants val robotoTypeface = Typeface( name = "Roboto", fonts = listOf( Font( uri = fontUri(family = "Roboto", fileName = "Roboto-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), Font( uri = fontUri(family = "Roboto", fileName = "Roboto-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = fontUri(family = "Roboto", fileName = "Roboto-Italic.ttf"), subFamily = "Italic", weight = FontWeight.NORMAL, style = FontStyle.ITALIC, ), Font( uri = fontUri(family = "Roboto", fileName = "Roboto-BoldItalic.ttf"), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), ), ) val robotoRegular = robotoTypeface.fonts.firstOrNull { it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL } ?: error("Roboto regular font variant is required for text styling.") ``` ## Style Text Ranges Style part of a text block by passing `from` and `to` to the range-based text APIs. Android uses start-inclusive and end-exclusive UTF-16 code unit offsets for these ranges. ```kotlin highlight-android-rich-text-styling // "Rich" (UTF-16 range [0, 4)) in blue. engine.block.setTextColor( block = richTextBlock, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.8F, a = 1F), from = 0, to = 4, ) // "text" (UTF-16 range [5, 9)) in bold. engine.block.setTextFontWeight( block = richTextBlock, fontWeight = FontWeight.BOLD, from = 5, to = 9, ) // "with" (UTF-16 range [10, 14)) in italic. engine.block.setTextFontStyle( block = richTextBlock, fontStyle = FontStyle.ITALIC, from = 10, to = 14, ) // "colors" (UTF-16 range [15, 21)) in orange and larger type. engine.block.setTextColor( block = richTextBlock, color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.1F, a = 1F), from = 15, to = 21, ) engine.block.setTextFontSize( block = richTextBlock, fontSize = 96F, from = 15, to = 21, ) // "and" (UTF-16 range [22, 25)) uses another typeface. engine.block.setTypeface( block = richTextBlock, typeface = caveatTypeface, from = 22, to = 25, ) // "styles" (UTF-16 range [26, 32)) in green uppercase. engine.block.setTextColor( block = richTextBlock, color = Color.fromRGBA(r = 0.2F, g = 0.7F, b = 0.3F, a = 1F), from = 26, to = 32, ) engine.block.setTextCase( block = richTextBlock, textCase = TextCase.UPPER_CASE, from = 26, to = 32, ) ``` The same range pattern applies to text colors, font weights, font styles, font sizes, typefaces, and text cases. Use UTF-16 code unit offsets for every range, especially when text contains emoji or surrogate pairs. ## Configure Auto-Sizing Combine a fixed width with an automatic height when text should wrap and grow vertically with its content. ```kotlin highlight-android-auto-sizing val autoSizeBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = autoSizeBlock) engine.block.replaceText(autoSizeBlock, text = "Auto-sizing text block") engine.block.setPositionX(autoSizeBlock, value = margin) engine.block.setPositionY(autoSizeBlock, value = margin + lineSpacing * 2) engine.block.setWidth(autoSizeBlock, value = pageWidth - margin * 2) engine.block.setWidthMode(autoSizeBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(autoSizeBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(autoSizeBlock, fontSize = fontSize) ``` Use `SizeMode.ABSOLUTE` when a dimension should use an explicit design-unit value, `SizeMode.PERCENT` when it should follow the parent size, and `SizeMode.AUTO` when CE.SDK should derive it from content. ## Apply Text Case Transformations Use `setTextCase()` to change the rendered casing without changing the underlying text string. ```kotlin highlight-android-text-case val caseBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = caseBlock) engine.block.replaceText(caseBlock, text = "uppercase text") engine.block.setPositionX(caseBlock, value = margin) engine.block.setPositionY(caseBlock, value = margin + lineSpacing * 3) engine.block.setWidthMode(caseBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(caseBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(caseBlock, fontSize = fontSize) engine.block.setTextCase(caseBlock, textCase = TextCase.UPPER_CASE) ``` Available text cases are `TextCase.NORMAL`, `TextCase.UPPER_CASE`, `TextCase.LOWER_CASE`, and `TextCase.TITLE_CASE`. ## Set Text Alignment and Spacing Set paragraph alignment with the typed Android helper when possible. Use typed line-height APIs for paragraph line height, and keep the generic float property only for letter spacing. ```kotlin highlight-android-text-alignment val alignedBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = alignedBlock) engine.block.replaceText(alignedBlock, text = "Centered Text\nWith Line Spacing") engine.block.setPositionX(alignedBlock, value = margin) engine.block.setPositionY(alignedBlock, value = margin + lineSpacing * 4) engine.block.setWidth(alignedBlock, value = pageWidth - margin * 2) engine.block.setWidthMode(alignedBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(alignedBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(alignedBlock, fontSize = fontSize) engine.block.setTextHorizontalAlignment(alignedBlock, alignment = HorizontalAlignment.Center) engine.block.setTextLineHeight(alignedBlock, lineHeight = 1.5F) engine.block.setFloat(alignedBlock, property = "text/letterSpacing", value = 0.05F) val lineHeight = engine.block.getTextLineHeight(alignedBlock, paragraphIndex = 0) val letterSpacing = engine.block.getFloat(alignedBlock, property = "text/letterSpacing") ``` Line height is a multiplier of the font size, so `1.5F` means 150%. Letter spacing is a proportion of the font size. ## Toggle Bold and Italic Use the toggle helpers when you want CE.SDK to switch between normal and formatted variants. Check support first because each toggle needs a matching counterpart variant for the current range and style. Include normal, bold, italic, and bold italic variants when both toggles should work across normal, bold, italic, and bold italic text. ```kotlin highlight-android-toggle-bold-italic val canToggleBold = engine.block.canToggleBoldFont(toggleBlock, from = 7, to = 11) if (canToggleBold) { engine.block.toggleBoldFont(toggleBlock, from = 7, to = 11) } val canToggleItalic = engine.block.canToggleItalicFont(toggleBlock, from = 16, to = 22) if (canToggleItalic) { engine.block.toggleItalicFont(toggleBlock, from = 16, to = 22) } ``` The `from` and `to` arguments target the same UTF-16 code unit ranges used by the styling APIs. ## Modify Text Content Use `replaceText()` with a range to replace part of the text while keeping surrounding content. Omitting `from` and `to` replaces the full text string. ```kotlin highlight-android-modify-text val modifyBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = modifyBlock) engine.block.replaceText(modifyBlock, text = "Hello World") engine.block.setPositionX(modifyBlock, value = margin) engine.block.setPositionY(modifyBlock, value = margin + lineSpacing * 8) engine.block.setWidthMode(modifyBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(modifyBlock, mode = SizeMode.AUTO) engine.block.setTextFontSize(modifyBlock, fontSize = fontSize) engine.block.replaceText(modifyBlock, text = "CE.SDK", from = 6, to = 11) ``` Use `removeText()` with the same range convention when you need to delete text from a block. ## Troubleshooting ### Text Not Displaying - Verify that `replaceText()` set the text content. - Check that `appendChild()` attaches the text block to a page. - Ensure the text block has a width or uses automatic sizing. ### Font Not Loading - Verify the font `Uri` is reachable from the device. - Check that the `Typeface` metadata matches the font files. - Include every font variant needed by bold and italic toggles. ### Range Styling Not Applying - Verify that `from` is less than `to`. - Count ranges as UTF-16 code unit offsets, especially when text contains emoji or surrogate pairs. - Check that the active typeface supports the requested font weight or style. ## API Reference | API | Category | Purpose | | --- | --- | --- | | `engine.block.create(blockType=_)` | Block | Create a new text block. | | `engine.block.appendChild(parent=_, child=_)` | Block | Attach the text block to a page or scene hierarchy. | | `engine.block.replaceText(block=_, text=_)` | Text | Set or replace text content. | | `engine.block.replaceText(block=_, text=_, from=_, to=_)` | Text | Replace a UTF-16 text range. | | `engine.block.removeText(block=_, from=_, to=_)` | Text | Remove a UTF-16 text range. | | `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Text | Bind a concrete font file and typeface to a text block. | | `engine.block.setTypeface(block=_, typeface=_, from=_, to=_)` | Text | Apply a typeface while preserving compatible formatting. | | `engine.block.getTypefaces(block=_, from=_, to=_)` | Text | Read typefaces applied to a UTF-16 range. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Text | Set the font size for the whole text block. | | `engine.block.setTextFontSize(block=_, fontSize=_, from=_, to=_)` | Text | Apply font size to a UTF-16 range. | | `engine.block.getTextFontSizes(block=_, from=_, to=_)` | Text | Read font sizes applied to a UTF-16 range. | | `engine.block.setTextColor(block=_, color=_, from=_, to=_)` | Text | Apply text color to a UTF-16 range. | | `engine.block.getTextColors(block=_, from=_, to=_)` | Text | Read text colors applied to a UTF-16 range. | | `engine.block.setTextFontWeight(block=_, fontWeight=_, from=_, to=_)` | Text | Apply font weight to a UTF-16 range. | | `engine.block.getTextFontWeights(block=_, from=_, to=_)` | Text | Read font weights applied to a UTF-16 range. | | `engine.block.setTextFontStyle(block=_, fontStyle=_, from=_, to=_)` | Text | Apply font style to a UTF-16 range. | | `engine.block.getTextFontStyles(block=_, from=_, to=_)` | Text | Read font styles applied to a UTF-16 range. | | `engine.block.setTextCase(block=_, textCase=_)` | Text | Apply a visual text case transformation. | | `engine.block.setTextCase(block=_, textCase=_, from=_, to=_)` | Text | Apply a visual text case transformation to a UTF-16 range. | | `engine.block.getTextCases(block=_, from=_, to=_)` | Text | Read text case transformations applied to a UTF-16 range. | | `engine.block.canToggleBoldFont(block=_, from=_, to=_)` | Text | Check whether a UTF-16 range can switch to or from bold. | | `engine.block.toggleBoldFont(block=_, from=_, to=_)` | Text | Toggle bold styling for a UTF-16 range. | | `engine.block.canToggleItalicFont(block=_, from=_, to=_)` | Text | Check whether a UTF-16 range can switch to or from italic. | | `engine.block.toggleItalicFont(block=_, from=_, to=_)` | Text | Toggle italic styling for a UTF-16 range. | | `engine.block.setWidthMode(block=_, mode=_)` | Layout | Set how CE.SDK resolves a block width. | | `engine.block.setHeightMode(block=_, mode=_)` | Layout | Set how CE.SDK resolves a block height. | | `engine.block.setWidth(block=_, value=_)` | Layout | Set an explicit block width. | | `engine.block.setHeight(block=_, value=_)` | Layout | Set an explicit block height. | | `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.setTextHorizontalAlignment(block=_, alignment=_)` | Text | Set horizontal alignment for the block and clear paragraph-level overrides. | | `engine.block.getTextHorizontalAlignment(block=_)` | Text | Read the block-level horizontal alignment. | | `engine.block.setTextLineHeight(block=_, lineHeight=_)` | Text | Set line height for all paragraphs. | | `engine.block.getTextLineHeight(block=_, paragraphIndex=_)` | Text | Read paragraph line height. | | `engine.block.setFloat(block=_, property="text/letterSpacing", value=_)` | Text | Set letter spacing relative to font size. | | `engine.block.getFloat(block=_, property="text/letterSpacing")` | Text | Read letter spacing relative to font size. | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Auto-Size](https://img.ly/docs/cesdk/android/text/auto-size-5331b3/) — Configure text blocks to automatically adapt their dimensions or font size for dynamic content. - [Adjust Text Spacing](https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/) - Control letter spacing, line height, and paragraph spacing in CE.SDK text blocks for precise typographic control. - [Emojis](https://img.ly/docs/cesdk/android/text/emojis-510651/) - Insert and style emojis alongside text for expressive, modern typographic designs. - [Text Effects](https://img.ly/docs/cesdk/android/text/effects-2dc9fc/) — Apply visual effects to text blocks including drop shadows and stroke outlines. --- ## 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 Text Spacing" description: "Control letter spacing, line height, paragraph line height, and paragraph spacing in text blocks." platform: android url: "https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Adjust Spacing](https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-adjust-spacing/TextAdjustSpacing.kt reference-only import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.SizeMode data class TextAdjustSpacing( val letterSpacing: Float, val lineHeight: Float, val paragraph0LineHeight: Float, val paragraph1LineHeight: Float, val resetParagraph0LineHeight: Float, val resetBlockLineHeight: Float, val paragraphSpacing: Float, ) fun textAdjustSpacing(engine: Engine): TextAdjustSpacing { val scene = engine.scene.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "Hello\nWorld\nCE.SDK") engine.block.setFloat(text, property = "text/letterSpacing", value = 0.1F) val letterSpacing = engine.block.getFloat(text, property = "text/letterSpacing") engine.block.setFloat(text, property = "text/lineHeight", value = 1.5F) val lineHeight = engine.block.getFloat(text, property = "text/lineHeight") engine.block.setTextLineHeight(text, lineHeight = 2.0F, paragraphIndex = 0) val paragraph0LineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 0) val paragraph1LineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 1) engine.block.setTextLineHeight(text, lineHeight = null, paragraphIndex = 0) val resetParagraph0LineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 0) engine.block.setTextLineHeight(text, lineHeight = 1.8F) val resetBlockLineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 1) engine.block.setFloat(text, property = "text/paragraphSpacing", value = 1.2F) val paragraphSpacing = engine.block.getFloat(text, property = "text/paragraphSpacing") check(letterSpacing == 0.1F) check(lineHeight == 1.5F) check(paragraph0LineHeight == 2.0F) check(paragraph1LineHeight == 1.5F) check(resetParagraph0LineHeight == 1.5F) check(resetBlockLineHeight == 1.8F) check(paragraphSpacing == 1.2F) return TextAdjustSpacing( letterSpacing = letterSpacing, lineHeight = lineHeight, paragraph0LineHeight = paragraph0LineHeight, paragraph1LineHeight = paragraph1LineHeight, resetParagraph0LineHeight = resetParagraph0LineHeight, resetBlockLineHeight = resetBlockLineHeight, paragraphSpacing = paragraphSpacing, ) } ``` Control letter spacing, line height, and paragraph spacing in text blocks using the Block API. ![Android text spacing preview showing adjusted letter spacing, line height, and paragraph spacing](./assets/android.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-adjust-spacing) CE.SDK stores text spacing on text blocks. Use `engine.block.setFloat()` and `engine.block.getFloat()` for block-level letter spacing, line height, and paragraph spacing. Use `engine.block.setTextLineHeight()` and `engine.block.getTextLineHeight()` when individual paragraphs need their own line-height values. This guide focuses on the programmatic Block API. The sample operates on an existing text block with enough characters, lines, and paragraph breaks to make each spacing change observable. ## Letter Spacing Control the horizontal space between characters with the `text/letterSpacing` property. Positive values spread characters apart, while negative values tighten them. ```kotlin highlight-android-letter-spacing engine.block.setFloat(text, property = "text/letterSpacing", value = 0.1F) val letterSpacing = engine.block.getFloat(text, property = "text/letterSpacing") ``` Letter spacing, also called tracking, adjusts the density of a text block without changing the text content. ## Line Height Control the vertical distance between lines with the `text/lineHeight` property. The value is a multiplier of the font size, so `1.5F` renders lines at 150% of the font size. ```kotlin highlight-android-line-height engine.block.setFloat(text, property = "text/lineHeight", value = 1.5F) val lineHeight = engine.block.getFloat(text, property = "text/lineHeight") ``` This block-level value applies to every paragraph unless a paragraph has its own override. ## Per-Paragraph Line Height Override individual paragraphs with `engine.block.setTextLineHeight()` and a zero-based `paragraphIndex`. Passing `null` clears the paragraph override, and calling `setTextLineHeight()` without a paragraph index updates the block-level value while clearing all paragraph overrides. ```kotlin highlight-android-paragraph-line-height engine.block.setTextLineHeight(text, lineHeight = 2.0F, paragraphIndex = 0) val paragraph0LineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 0) val paragraph1LineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 1) engine.block.setTextLineHeight(text, lineHeight = null, paragraphIndex = 0) val resetParagraph0LineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 0) engine.block.setTextLineHeight(text, lineHeight = 1.8F) val resetBlockLineHeight = engine.block.getTextLineHeight(text, paragraphIndex = 1) ``` - `paragraph0LineHeight` returns the paragraph-specific override. - `paragraph1LineHeight` falls back to the block-level value. - `resetParagraph0LineHeight` returns the block-level value after the override is cleared. - `resetBlockLineHeight` returns `1.8F` after `setTextLineHeight(text, lineHeight = 1.8F)` updates the block-level value and clears paragraph overrides. ## Paragraph Spacing Add vertical space after paragraph breaks with the `text/paragraphSpacing` property. The value is an EM-based paragraph gap relative to the text's font size, not an absolute pixel distance, and it only affects text that contains newline characters. Values typically range from `0.0F` to `2.5F`; larger values are accepted but produce very large gaps. ```kotlin highlight-android-paragraph-spacing engine.block.setFloat(text, property = "text/paragraphSpacing", value = 1.2F) val paragraphSpacing = engine.block.getFloat(text, property = "text/paragraphSpacing") ``` Single-paragraph text does not show a visible paragraph-spacing change because there is no paragraph break to separate. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.setFloat(block=_, property="text/letterSpacing", value=_)` | Set block-level letter spacing. | | `engine.block.getFloat(block=_, property="text/letterSpacing")` | Read the current block-level letter spacing. | | `engine.block.setFloat(block=_, property="text/lineHeight", value=_)` | Set the block-level line-height multiplier. | | `engine.block.getFloat(block=_, property="text/lineHeight")` | Read the current block-level line-height multiplier. | | `engine.block.setTextLineHeight(block=_, lineHeight=_, paragraphIndex=_)` | Set or clear a paragraph-specific line-height override. | | `engine.block.setTextLineHeight(block=_, lineHeight=_)` | Set the block-level line-height multiplier and clear paragraph overrides. | | `engine.block.getTextLineHeight(block=_, paragraphIndex=_)` | Read the effective line height for a paragraph. | | `engine.block.setFloat(block=_, property="text/paragraphSpacing", value=_)` | Set the paragraph gap relative to font size. | | `engine.block.getFloat(block=_, property="text/paragraphSpacing")` | Read the current paragraph-gap value. | ### Properties Reference | Property | Type | Purpose | | --- | --- | --- | | `text/letterSpacing` | Float | Space between characters. | | `text/lineHeight` | Float | Block-level multiplier for vertical line distance. | | `text/paragraphSpacing` | Float | EM-based gap added after paragraph breaks. | ## Troubleshooting **Spacing changes are not visible**: Check that the text block contains the right kind of content: multiple characters for letter spacing, multiple lines for line height, and paragraph breaks for paragraph spacing. **Line height looks larger than expected**: Line height is a multiplier, not an absolute pixel value. A value of `1.5F` means 150% of the current font size. **Paragraph spacing has no effect**: Verify that the text contains newline characters. Paragraph spacing is only visible between paragraphs. The value is relative to the text's font size (typically `0.0F` to `2.5F`), not the `text/lineHeight` multiplier or a pixel distance. ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) - Insert text blocks into your CE.SDK scene. - [Auto-Size](https://img.ly/docs/cesdk/android/text/auto-size-5331b3/) - Configure text blocks to automatically adapt their dimensions or font size for dynamic content. --- ## 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-Size" description: "Configure text blocks to automatically adapt their dimensions or font size for dynamic content." platform: android url: "https://img.ly/docs/cesdk/android/text/auto-size-5331b3/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Auto-Size](https://img.ly/docs/cesdk/android/text/auto-size-5331b3/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-auto-size/TextAutoSize.kt reference-only import kotlinx.coroutines.delay 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 TextAutoSize( val autoWidthMode: SizeMode, val autoHeightMode: SizeMode, val automaticFontSizeEnabled: Boolean, val minAutomaticFontSize: Float, val maxAutomaticFontSize: Float, val hasClippedLines: Boolean, ) suspend fun textAutoSize(engine: Engine): TextAutoSize { 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 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("#FFF7F5EF")) engine.block.appendChild(parent = page, child = background) val autoText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = autoText) engine.block.setWidthMode(autoText, mode = SizeMode.AUTO) engine.block.setHeightMode(autoText, mode = SizeMode.AUTO) engine.block.replaceText(autoText, text = "Auto-sized text") engine.block.setTextFontSize(autoText, fontSize = 48F) engine.block.setTextColor(autoText, color = Color.fromHex("#FF1F2937")) engine.block.setPositionX(autoText, value = 50F) engine.block.setPositionY(autoText, value = 50F) val wrappedText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = wrappedText) engine.block.setWidthMode(wrappedText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(wrappedText, value = 250F) engine.block.setHeightMode(wrappedText, mode = SizeMode.AUTO) engine.block.replaceText( wrappedText, text = "This text has a fixed width but auto height, so it wraps to multiple lines", ) engine.block.setTextFontSize(wrappedText, fontSize = 48F) engine.block.setTextColor(wrappedText, color = Color.fromHex("#FF334155")) engine.block.setPositionX(wrappedText, value = 50F) engine.block.setPositionY(wrappedText, value = 150F) val scaledText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = scaledText) engine.block.setWidthMode(scaledText, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(scaledText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(scaledText, value = 300F) engine.block.setHeight(scaledText, value = 80F) engine.block.setBoolean(scaledText, property = "text/automaticFontSizeEnabled", value = true) engine.block.replaceText(scaledText, text = "Auto-scaled font") engine.block.setTextColor(scaledText, color = Color.fromHex("#FF1D4ED8")) engine.block.setPositionX(scaledText, value = 50F) engine.block.setPositionY(scaledText, value = 350F) val constrainedText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = constrainedText) engine.block.setWidthMode(constrainedText, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(constrainedText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(constrainedText, value = 300F) engine.block.setHeight(constrainedText, value = 80F) engine.block.setBoolean(constrainedText, property = "text/automaticFontSizeEnabled", value = true) engine.block.setFloat(constrainedText, property = "text/minAutomaticFontSize", value = 12F) engine.block.setFloat(constrainedText, property = "text/maxAutomaticFontSize", value = 48F) engine.block.replaceText( constrainedText, text = "Edit this text to see automatic font scaling in a 12-48 pt range", ) engine.block.setTextColor(constrainedText, color = Color.fromHex("#FF7C2D12")) engine.block.setPositionX(constrainedText, value = 50F) engine.block.setPositionY(constrainedText, value = 460F) val widthMode = engine.block.getWidthMode(autoText) val heightMode = engine.block.getHeightMode(autoText) val isAutomaticFontSizeEnabled = engine.block.getBoolean( scaledText, property = "text/automaticFontSizeEnabled", ) val minAutomaticFontSize = engine.block.getFloat( constrainedText, property = "text/minAutomaticFontSize", ) val maxAutomaticFontSize = engine.block.getFloat( constrainedText, property = "text/maxAutomaticFontSize", ) val clippedText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = clippedText) engine.block.setWidthMode(clippedText, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(clippedText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(clippedText, value = 300F) engine.block.setHeight(clippedText, value = 60F) engine.block.replaceText( clippedText, text = "This line fits.\nThis line overflows.\nThis line is clipped.", ) engine.block.setTextFontSize(clippedText, fontSize = 48F) engine.block.setBoolean(clippedText, property = "text/clipLinesOutsideOfFrame", value = true) var hasClippedLines = false var layoutPasses = 0 while (!hasClippedLines && layoutPasses < 10) { yield() delay(16) // Let the bound engine process a frame before reading layout-backed state. hasClippedLines = engine.block.getBoolean( clippedText, property = "text/hasClippedLines", ) layoutPasses += 1 } engine.block.setTextColor(clippedText, color = Color.fromHex("#FF991B1B")) engine.block.setPositionX(clippedText, value = 430F) engine.block.setPositionY(clippedText, value = 350F) return TextAutoSize( autoWidthMode = widthMode, autoHeightMode = heightMode, automaticFontSizeEnabled = isAutomaticFontSizeEnabled, minAutomaticFontSize = minAutomaticFontSize, maxAutomaticFontSize = maxAutomaticFontSize, hasClippedLines = hasClippedLines, ) } ``` Configure text blocks to automatically adapt their dimensions or font size for dynamic content. ![Android text auto-size example showing text blocks with auto dimensions, automatic font sizing, and clipped overflow](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-auto-size) CE.SDK provides two approaches for handling dynamic text content. Auto size modes let text blocks resize to fit their content, while automatic font sizing scales the font to fit within fixed boundaries. This guide covers configuring size modes, enabling automatic font sizing, setting font size constraints, and detecting clipped text with the Android Engine APIs. ## Text Size Modes Text blocks support `SizeMode.ABSOLUTE`, `SizeMode.PERCENT`, and `SizeMode.AUTO` for each dimension. Use `engine.block.setWidthMode()` and `engine.block.setHeightMode()` to decide whether the block keeps an explicit size, follows its parent, or grows from its content. ### Auto Width and Height When both dimensions use `SizeMode.AUTO`, the text block expands freely to fit its content. This works well for single-line labels that grow horizontally. ```kotlin highlight-android-auto-width-height val autoText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = autoText) engine.block.setWidthMode(autoText, mode = SizeMode.AUTO) engine.block.setHeightMode(autoText, mode = SizeMode.AUTO) engine.block.replaceText(autoText, text = "Auto-sized text") engine.block.setTextFontSize(autoText, fontSize = 48F) engine.block.setTextColor(autoText, color = Color.fromHex("#FF1F2937")) engine.block.setPositionX(autoText, value = 50F) engine.block.setPositionY(autoText, value = 50F) ``` We set both width and height modes to `SizeMode.AUTO`, then write the text and choose a font size. CE.SDK calculates the block dimensions from the rendered text. ### Fixed Width with Auto Height For wrapped multi-line text, keep the width fixed and let the height adjust. The text wraps within the defined width and the block grows vertically as needed. ```kotlin highlight-android-fixed-width-auto-height val wrappedText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = wrappedText) engine.block.setWidthMode(wrappedText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(wrappedText, value = 250F) engine.block.setHeightMode(wrappedText, mode = SizeMode.AUTO) engine.block.replaceText( wrappedText, text = "This text has a fixed width but auto height, so it wraps to multiple lines", ) engine.block.setTextFontSize(wrappedText, fontSize = 48F) engine.block.setTextColor(wrappedText, color = Color.fromHex("#FF334155")) engine.block.setPositionX(wrappedText, value = 50F) engine.block.setPositionY(wrappedText, value = 150F) ``` This pattern is useful for template placeholders where the width should stay aligned with the design but content length varies. ### Querying Size Modes Read current modes with `engine.block.getWidthMode()` and `engine.block.getHeightMode()`. ```kotlin highlight-android-query-size-modes val widthMode = engine.block.getWidthMode(autoText) val heightMode = engine.block.getHeightMode(autoText) ``` ## Automatic Font Sizing For text blocks with bounded dimensions, enable automatic font sizing to scale the font within the frame. The engine calculates the largest font size that fits the content. ```kotlin highlight-android-automatic-font-sizing val scaledText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = scaledText) engine.block.setWidthMode(scaledText, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(scaledText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(scaledText, value = 300F) engine.block.setHeight(scaledText, value = 80F) engine.block.setBoolean(scaledText, property = "text/automaticFontSizeEnabled", value = true) engine.block.replaceText(scaledText, text = "Auto-scaled font") engine.block.setTextColor(scaledText, color = Color.fromHex("#FF1D4ED8")) engine.block.setPositionX(scaledText, value = 50F) engine.block.setPositionY(scaledText, value = 350F) ``` The example uses `SizeMode.ABSOLUTE` for both dimensions before enabling `text/automaticFontSizeEnabled`. `SizeMode.PERCENT` can also work when the parent provides concrete dimensions; automatic font sizing only has a bounded frame to solve against when neither dimension is `SizeMode.AUTO`. ### Setting Font Size Constraints Constrain the automatic scaling range with `text/minAutomaticFontSize` and `text/maxAutomaticFontSize`. These constraints apply to the automatic sizing algorithm, not to manual font size edits made elsewhere in your app. ```kotlin highlight-android-font-size-constraints val constrainedText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = constrainedText) engine.block.setWidthMode(constrainedText, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(constrainedText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(constrainedText, value = 300F) engine.block.setHeight(constrainedText, value = 80F) engine.block.setBoolean(constrainedText, property = "text/automaticFontSizeEnabled", value = true) engine.block.setFloat(constrainedText, property = "text/minAutomaticFontSize", value = 12F) engine.block.setFloat(constrainedText, property = "text/maxAutomaticFontSize", value = 48F) engine.block.replaceText( constrainedText, text = "Edit this text to see automatic font scaling in a 12-48 pt range", ) engine.block.setTextColor(constrainedText, color = Color.fromHex("#FF7C2D12")) engine.block.setPositionX(constrainedText, value = 50F) engine.block.setPositionY(constrainedText, value = 460F) ``` With constraints set, the font size scales within the 12-48 pt range as content length changes. Use minimum values to keep dynamic text readable and maximum values to preserve the design hierarchy. ## Text Clipping New text blocks clip overflowing lines by default. Use a bounded frame when text must stay inside a fixed area, keep or set `text/clipLinesOutsideOfFrame` to `true`, then read `text/hasClippedLines` after the engine has updated text layout. The property is layout-backed, so the sample waits for the bound engine update loop to advance layout before reading the value. ```kotlin highlight-android-text-clipping val clippedText = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = clippedText) engine.block.setWidthMode(clippedText, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(clippedText, mode = SizeMode.ABSOLUTE) engine.block.setWidth(clippedText, value = 300F) engine.block.setHeight(clippedText, value = 60F) engine.block.replaceText( clippedText, text = "This line fits.\nThis line overflows.\nThis line is clipped.", ) engine.block.setTextFontSize(clippedText, fontSize = 48F) engine.block.setBoolean(clippedText, property = "text/clipLinesOutsideOfFrame", value = true) var hasClippedLines = false var layoutPasses = 0 while (!hasClippedLines && layoutPasses < 10) { yield() delay(16) // Let the bound engine process a frame before reading layout-backed state. hasClippedLines = engine.block.getBoolean( clippedText, property = "text/hasClippedLines", ) layoutPasses += 1 } ``` The clipping readback helps you decide whether to enable automatic font sizing, switch the height mode to `SizeMode.AUTO`, or adjust the frame dimensions. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.create(blockType=DesignBlockType.Text)` | Create a text block. | | `engine.block.appendChild(parent=_, child=_)` | Add the text block to the scene hierarchy. | | `engine.block.replaceText(block=_, text=_)` | Set the full text content. | | `engine.block.setWidthMode(block=_, mode=_)` | Set the text block's width mode. | | `engine.block.getWidthMode(block=_)` | Read the current width mode. | | `engine.block.setHeightMode(block=_, mode=_)` | Set the text block's height mode. | | `engine.block.getHeightMode(block=_)` | Read the current height mode. | | `engine.block.setWidth(block=_, value=_, maintainCrop=false)` | Set an explicit text block width. | | `engine.block.setHeight(block=_, value=_, maintainCrop=false)` | Set an explicit text block height. | | `engine.block.setPositionX(block=_, value=_)` | Position the text block on the x-axis. | | `engine.block.setPositionY(block=_, value=_)` | Position the text block on the y-axis. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the base text font size. | | `engine.block.setTextColor(block=_, color=_)` | Set the text color. | | `engine.block.setBoolean(block=_, property="text/automaticFontSizeEnabled", value=_)` | Enable or disable automatic font sizing. | | `engine.block.getBoolean(block=_, property="text/automaticFontSizeEnabled")` | Read whether automatic font sizing is enabled. | | `engine.block.setFloat(block=_, property="text/minAutomaticFontSize", value=_)` | Set the minimum automatic font size. | | `engine.block.getFloat(block=_, property="text/minAutomaticFontSize")` | Read the minimum automatic font size. | | `engine.block.setFloat(block=_, property="text/maxAutomaticFontSize", value=_)` | Set the maximum automatic font size. | | `engine.block.getFloat(block=_, property="text/maxAutomaticFontSize")` | Read the maximum automatic font size. | | `engine.block.setBoolean(block=_, property="text/clipLinesOutsideOfFrame", value=_)` | Hide or show text lines outside the frame. | | `engine.block.getBoolean(block=_, property="text/hasClippedLines")` | Detect whether hidden text lines overflow the frame. | ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Text does not resize | One dimension still uses `SizeMode.ABSOLUTE` | Set both dimensions to `SizeMode.AUTO` for free-growing text, or combine fixed width with `SizeMode.AUTO` height for wrapped text | | Font size does not scale | The text block does not have bounded width and height, or `text/automaticFontSizeEnabled` is disabled | Use non-auto width and height modes such as `SizeMode.ABSOLUTE`, or `SizeMode.PERCENT` with a concrete parent size, then enable automatic font sizing | | Text becomes too small | The automatic font size range has no readable lower bound | Set `text/minAutomaticFontSize` to the smallest acceptable size for the design | | Text is clipped unexpectedly | A fixed frame is hiding overflow without automatic font sizing | Check `text/hasClippedLines`, enable automatic font sizing, or switch the height mode to `SizeMode.AUTO` | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Adjust Text Spacing](https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/) - Control letter spacing, line height, and paragraph spacing in text blocks. - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) - Load and manage custom fonts to match brand guidelines or user preferences. --- ## 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: "Customize Fonts" description: "Load and manage custom fonts to match brand guidelines or user preferences." platform: android url: "https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) --- ```kotlin file=@cesdk_android_examples/editor-guides-text-custom-fonts/CustomFontsEditorSolution.kt reference-only import android.net.Uri import androidx.compose.runtime.Composable import kotlinx.coroutines.delay import ly.img.editor.Editor import ly.img.editor.core.EditorScope import ly.img.editor.core.UnstableEditorApi 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.AssetSourceType import ly.img.editor.core.sheet.SheetType import ly.img.engine.AssetDefinition import ly.img.engine.AssetPayload import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FindAssetsQuery import ly.img.engine.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.SizeMode import ly.img.engine.Typeface private const val CUSTOM_TYPEFACE_SOURCE_ID = "my-custom-typefaces" private const val BRAND_TYPEFACE_ASSET_ID = "ly.img.typeface.brand_sans" private const val BRAND_SANS_REGULAR_FILE = "BrandSans-Regular.ttf" private const val BRAND_SANS_BOLD_FILE = "BrandSans-Bold.ttf" private fun brandFontUri(fileName: String): Uri = "file:///android_asset" .let(Uri::parse) .buildUpon() .appendEncodedPath("custom-fonts") .appendEncodedPath(fileName) .build() private fun brandTypeface(): Typeface = Typeface( name = "Brand Sans", fonts = listOf( Font( uri = brandFontUri(BRAND_SANS_REGULAR_FILE), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), Font( uri = brandFontUri(BRAND_SANS_BOLD_FILE), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), ), ) private fun brandTypefaceAsset() = AssetDefinition( id = BRAND_TYPEFACE_ASSET_ID, label = mapOf("en" to "Brand Sans"), tags = mapOf("en" to listOf("brand", "sans", "headline")), payload = AssetPayload( typeface = brandTypeface(), ), ) private fun createCustomTypefaceSource( engine: Engine, sourceId: String = CUSTOM_TYPEFACE_SOURCE_ID, ) { if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = emptyList(), ) engine.asset.addAsset( sourceId = sourceId, asset = brandTypefaceAsset(), ) engine.asset.assetSourceContentsChanged(sourceId = sourceId) } private fun replaceEditorTypefaceSource(engine: Engine) { createCustomTypefaceSource( engine = engine, sourceId = AssetSourceType.Typeface.sourceId, ) } @OptIn(UnstableEditorApi::class) private fun EditorScope.openBrandTypefaceSheet(text: DesignBlock) { editorContext.eventHandler.send( EditorEvent.Sheet.Open( SheetType.Font( designBlock = text, fontFamilies = listOf(BRAND_TYPEFACE_ASSET_ID), ), ), ) } private fun applyBrandTypefaceToText( engine: Engine, text: DesignBlock, typeface: Typeface, ) { val regularFont = typeface.fonts.firstOrNull { it.subFamily == "Regular" && it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL } ?: error("Brand Sans Regular font is missing.") engine.block.setFont( block = text, fontFileUri = regularFont.uri, typeface = typeface, ) engine.block.setTypeface( block = text, typeface = typeface, from = 0, to = 5, ) val currentTypeface = engine.block.getTypeface(block = text) val rangeTypefaces = engine.block.getTypefaces(block = text, from = 0, to = 5) check(currentTypeface.name == typeface.name) check(rangeTypefaces.any { it.name == typeface.name }) } data class CustomFontsResult( val sourceId: String, val queriedTypefaceName: String, val appliedTypefaceName: String, val rangeTypefaceNames: List, ) suspend fun customFonts( engine: Engine, sourceId: String = CUSTOM_TYPEFACE_SOURCE_ID, ): CustomFontsResult { createCustomTypefaceSource( engine = engine, sourceId = sourceId, ) val typeface = brandTypeface() val scene = engine.scene.create() val page = engine.block.create(blockType = DesignBlockType.Page) engine.block.setWidth(block = page, value = 1080F) engine.block.setHeight(block = page, value = 1080F) engine.block.appendChild(parent = scene, child = page) val text = engine.block.create(blockType = DesignBlockType.Text) engine.block.replaceText(block = text, text = "Brand fonts") engine.block.setWidthMode(block = text, mode = SizeMode.AUTO) engine.block.setHeightMode(block = text, mode = SizeMode.AUTO) engine.block.setTextFontSize(block = text, fontSize = 36F) engine.block.setPositionX(block = text, value = 240F) engine.block.setPositionY(block = text, value = 500F) engine.block.appendChild(parent = page, child = text) applyBrandTypefaceToText( engine = engine, text = text, typeface = typeface, ) engine.block.setSelected(block = text, selected = true) engine.scene.zoomToBlock( block = page, paddingLeft = 80F, paddingTop = 80F, paddingRight = 80F, paddingBottom = 80F, ) val queriedTypeface = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(query = "Brand", page = 0, perPage = 10), ).assets.firstNotNullOf { it.payload.typeface } return CustomFontsResult( sourceId = sourceId, queriedTypefaceName = queriedTypeface.name, appliedTypefaceName = engine.block.getTypeface(block = text).name, rangeTypefaceNames = engine.block .getTypefaces(block = text, from = 0, to = 5) .map { it.name }, ) } @Composable fun CustomFontsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { val engine = editorContext.engine replaceEditorTypefaceSource( engine = engine, ) val typeface = brandTypeface() val scene = engine.scene.create() val page = engine.block.create(blockType = DesignBlockType.Page) engine.block.setWidth(block = page, value = 1080F) engine.block.setHeight(block = page, value = 1080F) engine.block.appendChild(parent = scene, child = page) val text = engine.block.create(blockType = DesignBlockType.Text) engine.block.replaceText(block = text, text = "Brand fonts") engine.block.setWidthMode(block = text, mode = SizeMode.AUTO) engine.block.setHeightMode(block = text, mode = SizeMode.AUTO) engine.block.setTextFontSize(block = text, fontSize = 36F) engine.block.setPositionX(block = text, value = 240F) engine.block.setPositionY(block = text, value = 500F) engine.block.appendChild(parent = page, child = text) applyBrandTypefaceToText( engine = engine, text = text, typeface = typeface, ) engine.block.setSelected(block = text, selected = true) engine.scene.zoomToBlock( block = page, paddingLeft = 80F, paddingTop = 80F, paddingRight = 80F, paddingBottom = 80F, ) } onLoaded = { val text = editorContext.engine.block.findByType(DesignBlockType.Text).firstOrNull() if (text != null) { editorContext.engine.block.setSelected(block = text, selected = true) // Let the editor publish the new selection before opening the contextual font sheet. delay(500) openBrandTypefaceSheet(text = text) } } } }, onClose = onClose, ) } ``` Load custom typefaces into CE.SDK and apply them through the Android editor UI or the CreativeEngine Block API. ![Custom font sheet in the Android editor](./assets/android.hero.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-text-custom-fonts) CE.SDK ships with default typefaces, but you can replace or extend them with font files from your own app bundle, CDN, or asset host. On Android, typefaces are regular asset payloads: define the font family, register it in an asset source, then pass the typeface asset IDs to the editor font sheet or query them from your own code. This guide shows how to define a two-weight typeface, register it as an Android asset source, expose it to the CE.SDK editor UI, and apply it to a text block programmatically. ## Typeface and Font Structure A `Typeface` represents one font family. Its `fonts` list contains the concrete variants that CE.SDK can choose from when text is regular, bold, italic, or another supported weight. Each `Font` needs: - `uri` - a reachable font file, such as a bundled asset or HTTPS URL - `subFamily` - the display name of the variant, such as `Regular` or `Bold` - `weight` - one of the Android `FontWeight` enum values - `style` - `FontStyle.NORMAL` or `FontStyle.ITALIC` ```kotlin highlight-android-typeface-structure private const val CUSTOM_TYPEFACE_SOURCE_ID = "my-custom-typefaces" private const val BRAND_TYPEFACE_ASSET_ID = "ly.img.typeface.brand_sans" private const val BRAND_SANS_REGULAR_FILE = "BrandSans-Regular.ttf" private const val BRAND_SANS_BOLD_FILE = "BrandSans-Bold.ttf" private fun brandFontUri(fileName: String): Uri = "file:///android_asset" .let(Uri::parse) .buildUpon() .appendEncodedPath("custom-fonts") .appendEncodedPath(fileName) .build() private fun brandTypeface(): Typeface = Typeface( name = "Brand Sans", fonts = listOf( Font( uri = brandFontUri(BRAND_SANS_REGULAR_FILE), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), Font( uri = brandFontUri(BRAND_SANS_BOLD_FILE), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), ), ) ``` The sample uses neutral `BrandSans` demo assets bundled with the Android example. Replace those asset URIs with font files your app owns. ## Create a Custom Typeface Asset Source Add the typeface to the asset system with `engine.asset.addLocalSource()` and `engine.asset.addAsset()`. The important part is the `AssetPayload(typeface = ...)` value, which marks the asset as a typeface asset. ```kotlin highlight-android-typeface-asset private fun brandTypefaceAsset() = AssetDefinition( id = BRAND_TYPEFACE_ASSET_ID, label = mapOf("en" to "Brand Sans"), tags = mapOf("en" to listOf("brand", "sans", "headline")), payload = AssetPayload( typeface = brandTypeface(), ), ) ``` Register the source and notify CE.SDK that its contents changed: ```kotlin highlight-android-register-source private fun createCustomTypefaceSource( engine: Engine, sourceId: String = CUSTOM_TYPEFACE_SOURCE_ID, ) { if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = emptyList(), ) engine.asset.addAsset( sourceId = sourceId, asset = brandTypefaceAsset(), ) engine.asset.assetSourceContentsChanged(sourceId = sourceId) } ``` The sample removes an existing source with the same ID before adding it again so repeated editor launches stay deterministic. ## Update the Typeface Source The Android editor font sheet resolves typeface assets from `AssetSourceType.Typeface.sourceId`. Register your curated typeface asset under that source ID when you want the standard font sheet to be able to load it. ```kotlin highlight-android-update-library private fun replaceEditorTypefaceSource(engine: Engine) { createCustomTypefaceSource( engine = engine, sourceId = AssetSourceType.Typeface.sourceId, ) } ``` This source registration does not change which rows the built-in sheet shows. When you open `SheetType.Font`, pass the custom asset IDs through `fontFamilies`; otherwise the sheet uses its default fixed font-family filter and hides custom typefaces that are not in that list. ```kotlin highlight-android-font-sheet-filter @OptIn(UnstableEditorApi::class) private fun EditorScope.openBrandTypefaceSheet(text: DesignBlock) { editorContext.eventHandler.send( EditorEvent.Sheet.Open( SheetType.Font( designBlock = text, fontFamilies = listOf(BRAND_TYPEFACE_ASSET_ID), ), ), ) } ``` To extend the default typefaces, add your assets to the existing `AssetSourceType.Typeface.sourceId` source without removing it first and include both default and custom asset IDs in `fontFamilies`. If you only apply fonts programmatically, use any source ID and query that source directly. ## Set Up the Editor Wire the typeface source update into the editor configuration before the sample scene is created. The same runnable setup creates a text block in `onCreate`, selects it after loading, and opens the font sheet with the custom typeface ID in `fontFamilies`. ```kotlin highlight-android-editor-setup @Composable fun CustomFontsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { val engine = editorContext.engine replaceEditorTypefaceSource( engine = engine, ) val typeface = brandTypeface() val scene = engine.scene.create() val page = engine.block.create(blockType = DesignBlockType.Page) engine.block.setWidth(block = page, value = 1080F) engine.block.setHeight(block = page, value = 1080F) engine.block.appendChild(parent = scene, child = page) val text = engine.block.create(blockType = DesignBlockType.Text) engine.block.replaceText(block = text, text = "Brand fonts") engine.block.setWidthMode(block = text, mode = SizeMode.AUTO) engine.block.setHeightMode(block = text, mode = SizeMode.AUTO) engine.block.setTextFontSize(block = text, fontSize = 36F) engine.block.setPositionX(block = text, value = 240F) engine.block.setPositionY(block = text, value = 500F) engine.block.appendChild(parent = page, child = text) applyBrandTypefaceToText( engine = engine, text = text, typeface = typeface, ) engine.block.setSelected(block = text, selected = true) engine.scene.zoomToBlock( block = page, paddingLeft = 80F, paddingTop = 80F, paddingRight = 80F, paddingBottom = 80F, ) } onLoaded = { val text = editorContext.engine.block.findByType(DesignBlockType.Text).firstOrNull() if (text != null) { editorContext.engine.block.setSelected(block = text, selected = true) // Let the editor publish the new selection before opening the contextual font sheet. delay(500) openBrandTypefaceSheet(text = text) } } } }, onClose = onClose, ) } ``` ## Apply Fonts Programmatically Use `engine.block.setFont()` when you want to set the font file for the whole text block and reset existing text formatting. Use `engine.block.setTypeface()` when you want CE.SDK to preserve the current formatting as far as the new family supports it. ```kotlin highlight-android-apply-font private fun applyBrandTypefaceToText( engine: Engine, text: DesignBlock, typeface: Typeface, ) { val regularFont = typeface.fonts.firstOrNull { it.subFamily == "Regular" && it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL } ?: error("Brand Sans Regular font is missing.") engine.block.setFont( block = text, fontFileUri = regularFont.uri, typeface = typeface, ) engine.block.setTypeface( block = text, typeface = typeface, from = 0, to = 5, ) val currentTypeface = engine.block.getTypeface(block = text) val rangeTypefaces = engine.block.getTypefaces(block = text, from = 0, to = 5) check(currentTypeface.name == typeface.name) check(rangeTypefaces.any { it.name == typeface.name }) } ``` The `from` and `to` parameters use UTF-16 code unit offsets. In the sample, `0` to `5` covers the word `Brand` in `Brand fonts`. ## Troubleshooting - **The font is not visible in the editor UI**: Register the asset under `AssetSourceType.Typeface.sourceId`, call `engine.asset.assetSourceContentsChanged()`, and open `SheetType.Font` with `fontFamilies` containing your custom typeface asset ID. - **The font file does not load**: Check that the URI is reachable from the device and points to a valid TTF, OTF, WOFF, or WOFF2 file. - **Bold or italic is unavailable**: Add a `Font` entry with the matching `FontWeight` and `FontStyle`. - **`getTypeface()` fails on a new text block**: Apply a font first with `setFont()` or `setTypeface()` so the block has an explicit typeface. ## API Reference | Method | Purpose | | ------ | ------- | | `engine.asset.findAllSources()` | Check whether a typeface source is already registered | | `engine.asset.removeSource(sourceId=_)` | Remove an existing source before replacing the typeface library | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create a local source for custom typeface assets | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add the typeface asset definition to a local source | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify CE.SDK after changing the local source contents | | `engine.asset.findAssets(sourceId=_, query=_)` | Query typeface assets from a source | | `editorContext.eventHandler.send(event=_)` | Dispatch editor UI events from an editor configuration callback or component | | `EditorEvent.Sheet.Open(type=_)` | Open a bottom sheet in the CE.SDK editor UI | | `SheetType.Font(designBlock=_, fontFamilies=_)` | Configure the font sheet for a text block and filter it to custom typeface asset IDs | | `engine.scene.create()` | Create a scene for the sample editor setup | | `engine.block.create(blockType=_)` | Create the page and text blocks used by the sample scene | | `engine.block.setWidth(block=_, value=_)` | Set the sample page width | | `engine.block.setHeight(block=_, value=_)` | Set the sample page height | | `engine.block.appendChild(parent=_, child=_)` | Add the page to the scene and the text block to the page | | `engine.block.replaceText(block=_, text=_)` | Set the text block contents | | `engine.block.setWidthMode(block=_, mode=_)` | Let the text block fit its content width | | `engine.block.setHeightMode(block=_, mode=_)` | Let the text block fit its content height | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the text size in the sample scene | | `engine.block.setPositionX(block=_, value=_)` | Position the text block on the x-axis | | `engine.block.setPositionY(block=_, value=_)` | Position the text block on the y-axis | | `engine.block.findByType(type=_)` | Find the text block whose font sheet should open | | `engine.block.setSelected(block=_, selected=_)` | Select the text block before opening the contextual font sheet | | `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Set a font file for the entire text block and reset formatting | | `engine.block.setTypeface(block=_, typeface=_, from=_, to=_)` | Apply a typeface while preserving compatible text formatting | | `engine.block.getTypeface(block=_)` | Read the base typeface of a text block | | `engine.block.getTypefaces(block=_, from=_, to=_)` | Read unique typefaces in a text range | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) — Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) — Insert text blocks into your CE.SDK scene. - [Text Decorations](https://img.ly/docs/cesdk/android/text/decorations-d3c0a1/) — Add underline, strikethrough, and overline decorations to text with customizable styles, colors, and thickness. --- ## 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 Decorations" description: "Add underline, strikethrough, and overline decorations to text with customizable styles, colors, and thickness." platform: android url: "https://img.ly/docs/cesdk/android/text/decorations-d3c0a1/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Text Decorations](https://img.ly/docs/cesdk/android/text/decorations-d3c0a1/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-decorations/TextDecorations.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.SizeMode import ly.img.engine.TextDecorationConfig import ly.img.engine.TextDecorationLine import ly.img.engine.TextDecorationStyle fun textDecorations( 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 = 100, height = 100) val scene = engine.scene.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "Hello CE.SDK") // Toggle underline on the entire text engine.block.toggleTextDecorationUnderline(text) // Toggle strikethrough on the entire text engine.block.toggleTextDecorationStrikethrough(text) // Toggle overline on the entire text engine.block.toggleTextDecorationOverline(text) // Calling toggle again removes the decoration engine.block.toggleTextDecorationOverline(text) // Query the current decoration configurations // Returns a list of unique TextDecorationConfig values in the range val decorations = engine.block.getTextDecorations(text) // Each config contains: lines, style, underlineColor, underlineThickness, underlineOffset, skipInk // Set a specific decoration style // Available styles: SOLID, DOUBLE, DOTTED, DASHED, WAVY engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), style = TextDecorationStyle.DASHED, ), ) // Set a custom underline color (only applies to underlines) // Strikethrough and overline always use the text color engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ), ) // Adjust the underline thickness // Default is 1.0, values above 1.0 make the line thicker engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineThickness = 2.0f, ), ) // Adjust the underline position relative to the font default // 0 = font default, positive values move further from baseline, negative values move closer engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineOffset = 0.1f, ), ) // Apply decorations to a specific character range using UTF-16 indices // Toggle underline on characters 0-5 ("Hello") engine.block.toggleTextDecorationUnderline(text, from = 0, to = 5) // Set strikethrough on characters 6-12 ("CE.SDK") engine.block.setTextDecoration( text, TextDecorationConfig(lines = setOf(TextDecorationLine.STRIKETHROUGH)), from = 6, to = 12, ) // Query decorations in a specific range val subrangeDecorations = engine.block.getTextDecorations(text, from = 0, to = 5) // Combine multiple decoration lines on the same text // All active lines share the same style and thickness engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE, TextDecorationLine.STRIKETHROUGH), ), ) // Remove all decorations engine.block.setTextDecoration( text, TextDecorationConfig(lines = setOf(TextDecorationLine.NONE)), ) engine.stop() } ``` Add underline, strikethrough, and overline decorations to text blocks with configurable styles, colors, and thickness. CE.SDK supports three types of text decorations: underline, strikethrough, and overline. Decorations can be toggled on and off, customized with different line styles, and applied to specific character ranges. The `TextDecorationConfig` data class holds all configuration for a decoration. ## Toggle Decorations Toggle decorations using `engine.block.toggleTextDecorationUnderline()`, `engine.block.toggleTextDecorationStrikethrough()`, and `engine.block.toggleTextDecorationOverline()`. If all characters in the range already have the decoration, it is removed; otherwise, it is added to all. ```kotlin highlight-toggle-decorations // Toggle underline on the entire text engine.block.toggleTextDecorationUnderline(text) // Toggle strikethrough on the entire text engine.block.toggleTextDecorationStrikethrough(text) // Toggle overline on the entire text engine.block.toggleTextDecorationOverline(text) // Calling toggle again removes the decoration engine.block.toggleTextDecorationOverline(text) ``` ## Query Decorations Query the current decorations using `engine.block.getTextDecorations()`. It returns an ordered list of unique `TextDecorationConfig` values. Each config includes the active `lines` set, `style`, optional `underlineColor`, `underlineThickness`, `underlineOffset`, and `skipInk`. ```kotlin highlight-query-decorations // Query the current decoration configurations // Returns a list of unique TextDecorationConfig values in the range val decorations = engine.block.getTextDecorations(text) // Each config contains: lines, style, underlineColor, underlineThickness, underlineOffset, skipInk ``` ## Custom Decoration Styles Set a specific decoration style using `engine.block.setTextDecoration()` with a `TextDecorationConfig`. Available styles are `SOLID` (default), `DOUBLE`, `DOTTED`, `DASHED`, and `WAVY`. ```kotlin highlight-custom-style // Set a specific decoration style // Available styles: SOLID, DOUBLE, DOTTED, DASHED, WAVY engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), style = TextDecorationStyle.DASHED, ), ) ``` ## Underline Color Set a custom underline color that differs from the text color. The `underlineColor` property only applies to underlines; strikethrough and overline always use the text color. ```kotlin highlight-underline-color // Set a custom underline color (only applies to underlines) // Strikethrough and overline always use the text color engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ), ) ``` ## Decoration Thickness Adjust the underline thickness using the `underlineThickness` property. The default is `1.0f`. Values above `1.0f` make the underline thicker. ```kotlin highlight-thickness // Adjust the underline thickness // Default is 1.0, values above 1.0 make the line thicker engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineThickness = 2.0f, ), ) ``` ## Underline Offset Adjust the underline position using the `underlineOffset` property, which acts as a relative multiplier on the font-default distance. The actual position is computed as `fontDefault * (1 + underlineOffset)`. The default is `0f`, which uses the font's default underline position. Positive values move the underline proportionally further from the baseline, negative values move it proportionally closer. ```kotlin highlight-offset // Adjust the underline position relative to the font default // 0 = font default, positive values move further from baseline, negative values move closer engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineOffset = 0.1f, ), ) ``` ## Subrange Decorations Apply decorations to a specific character range using UTF-16 indices `[from, to)`. Both toggle and set operations accept `from` and `to` parameters for subrange targeting. ```kotlin highlight-subrange // Apply decorations to a specific character range using UTF-16 indices // Toggle underline on characters 0-5 ("Hello") engine.block.toggleTextDecorationUnderline(text, from = 0, to = 5) // Set strikethrough on characters 6-12 ("CE.SDK") engine.block.setTextDecoration( text, TextDecorationConfig(lines = setOf(TextDecorationLine.STRIKETHROUGH)), from = 6, to = 12, ) // Query decorations in a specific range val subrangeDecorations = engine.block.getTextDecorations(text, from = 0, to = 5) ``` ## Combine Decorations Combine multiple decoration types by passing a set of `TextDecorationLine` values. All active lines share the same style and thickness. ```kotlin highlight-combine // Combine multiple decoration lines on the same text // All active lines share the same style and thickness engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE, TextDecorationLine.STRIKETHROUGH), ), ) ``` ## Remove Decorations Remove all decorations by setting the lines to `setOf(TextDecorationLine.NONE)`. ```kotlin highlight-remove // Remove all decorations engine.block.setTextDecoration( text, TextDecorationConfig(lines = setOf(TextDecorationLine.NONE)), ) ``` ## Full Code Here's the full code: ```kotlin highlight-text-decorations 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.SizeMode import ly.img.engine.TextDecorationConfig import ly.img.engine.TextDecorationLine import ly.img.engine.TextDecorationStyle fun textDecorations( 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 = 100, height = 100) val scene = engine.scene.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "Hello CE.SDK") // Toggle underline on the entire text engine.block.toggleTextDecorationUnderline(text) // Toggle strikethrough on the entire text engine.block.toggleTextDecorationStrikethrough(text) // Toggle overline on the entire text engine.block.toggleTextDecorationOverline(text) // Calling toggle again removes the decoration engine.block.toggleTextDecorationOverline(text) // Query the current decoration configurations // Returns a list of unique TextDecorationConfig values in the range val decorations = engine.block.getTextDecorations(text) // Each config contains: lines, style, underlineColor, underlineThickness, underlineOffset, skipInk // Set a specific decoration style // Available styles: SOLID, DOUBLE, DOTTED, DASHED, WAVY engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), style = TextDecorationStyle.DASHED, ), ) // Set a custom underline color (only applies to underlines) // Strikethrough and overline always use the text color engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ), ) // Adjust the underline thickness // Default is 1.0, values above 1.0 make the line thicker engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineThickness = 2.0f, ), ) // Adjust the underline position relative to the font default // 0 = font default, positive values move further from baseline, negative values move closer engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE), underlineOffset = 0.1f, ), ) // Apply decorations to a specific character range using UTF-16 indices // Toggle underline on characters 0-5 ("Hello") engine.block.toggleTextDecorationUnderline(text, from = 0, to = 5) // Set strikethrough on characters 6-12 ("CE.SDK") engine.block.setTextDecoration( text, TextDecorationConfig(lines = setOf(TextDecorationLine.STRIKETHROUGH)), from = 6, to = 12, ) // Query decorations in a specific range val subrangeDecorations = engine.block.getTextDecorations(text, from = 0, to = 5) // Combine multiple decoration lines on the same text // All active lines share the same style and thickness engine.block.setTextDecoration( text, TextDecorationConfig( lines = setOf(TextDecorationLine.UNDERLINE, TextDecorationLine.STRIKETHROUGH), ), ) // Remove all decorations engine.block.setTextDecoration( text, TextDecorationConfig(lines = setOf(TextDecorationLine.NONE)), ) engine.stop() } ``` --- ## 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 Text" description: "Edit text content directly on the canvas or through the properties panel." platform: android url: "https://img.ly/docs/cesdk/android/text/edit-c5106b/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Edit Text](https://img.ly/docs/cesdk/android/text/edit-c5106b/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-edit/EditText.kt reference-only import ly.img.editor.defaultBaseUri import ly.img.engine.Color import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.FindAssetsQuery import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.MimeType import ly.img.engine.SizeMode import ly.img.engine.TextCase import java.nio.ByteBuffer suspend fun editText(engine: Engine): ByteBuffer { 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 text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.setPositionX(text, value = 80F) engine.block.setPositionY(text, value = 120F) val typefaceSourceId = "ly.img.typeface" if (!engine.asset.findAllSources().contains(typefaceSourceId)) { engine.asset.addLocalSourceFromJSON( contentUri = defaultBaseUri.buildUpon() .appendPath(typefaceSourceId) .appendPath("content.json") .build(), ) } val typeface = engine.asset.findAssets( sourceId = "ly.img.typeface", query = FindAssetsQuery(query = "", page = 0, perPage = 100), ).assets .mapNotNull { asset -> asset.payload.typeface } .firstOrNull { candidate -> val fonts = candidate.fonts fonts.any { font -> font.weight == FontWeight.NORMAL && font.style == FontStyle.NORMAL } && fonts.any { font -> font.weight == FontWeight.BOLD && font.style == FontStyle.NORMAL } && fonts.any { font -> font.weight == FontWeight.NORMAL && font.style == FontStyle.ITALIC } && fonts.any { font -> font.weight == FontWeight.BOLD && font.style == FontStyle.ITALIC } } ?: error("Typeface with regular, bold, italic, and bold italic fonts not found") val regularFont = typeface.fonts.first { font -> font.weight == FontWeight.NORMAL && font.style == FontStyle.NORMAL } engine.block.setFont(block = text, fontFileUri = regularFont.uri, typeface = typeface) engine.block.setTypeface(block = text, typeface = typeface) val blockTypeface = engine.block.getTypeface(text) engine.block.replaceText(text, text = "Winter Sale") engine.block.replaceText(text, text = " Design", from = 6, to = 6) engine.block.replaceText(text, text = "Campaign", from = 14, to = 18) engine.block.removeText(text, from = 0, to = 7) val campaignStart = 7 val campaignEnd = 15 engine.block.setTextColor( block = text, color = Color.fromRGBA(r = 0.1F, g = 0.32F, b = 0.78F, a = 1F), from = campaignStart, to = campaignEnd, ) engine.block.setTextFontWeight(text, fontWeight = FontWeight.BOLD, from = campaignStart, to = campaignEnd) engine.block.setTextFontStyle(text, fontStyle = FontStyle.ITALIC, from = campaignStart, to = campaignEnd) engine.block.setTextFontSize(text, fontSize = 24F) engine.block.setTextFontSize(text, fontSize = 32F, from = campaignStart, to = campaignEnd) engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) val designEnd = 6 if (engine.block.canToggleBoldFont(text, from = 0, to = designEnd)) { engine.block.toggleBoldFont(text, from = 0, to = designEnd) } if (engine.block.canToggleItalicFont(text, from = 0, to = designEnd)) { engine.block.toggleItalicFont(text, from = 0, to = designEnd) } val colors = engine.block.getTextColors(text) val fontWeights = engine.block.getTextFontWeights(text) val fontStyles = engine.block.getTextFontStyles(text) val fontSizes = engine.block.getTextFontSizes(text) val textCases = engine.block.getTextCases(text) val typefaces = engine.block.getTypefaces(text) engine.block.forceLoadResources(blocks = listOf(text)) val lineCount = engine.block.getTextVisibleLineCount(text) val firstLine = engine.block.getTextVisibleLineContent(text, lineIndex = 0) val firstLineBounds = engine.block.getTextLineBoundingBoxRect(text, index = 0) val fontMetrics = engine.editor.getFontMetrics(fontFileUri = regularFont.uri.toString()) check(colors.isNotEmpty()) check(fontWeights.contains(FontWeight.BOLD)) check(fontStyles.contains(FontStyle.ITALIC)) check(fontSizes.contains(24F) && fontSizes.contains(32F)) check(textCases == listOf(TextCase.TITLE_CASE)) check(blockTypeface.name == typeface.name) check(typefaces.isNotEmpty()) check(lineCount > 0) check(firstLine.isNotBlank()) check(firstLineBounds.width() > 0F) check(fontMetrics.unitsPerEm > 0F) return engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 1280F, targetHeight = 720F), ) } ``` Edit text block content from Android by replacing text, formatting UTF-16 ranges, reading formatting state, and managing the active text selection. ![Edit Text Android Engine sample showing the final styled text](./assets/android.hero.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-edit) CE.SDK supports two text-editing paths. The CE.SDK editor UI lets users enter text editing mode on a selected text block, while the Android Engine API edits text content and formatting directly. This guide focuses on the Engine APIs and assumes you already have an `Engine` instance. Range-based text APIs use UTF-16 code unit offsets. A range is `[from, to)`, so `from` is included and `to` points to the first code unit after the range. For `replaceText()` and `removeText()`, leaving both values at the default `-1` targets the whole text block. Formatting and query APIs use the active text selection while a text block is being edited, or the whole text block otherwise. ## Creating a Text Block Create a text block, append it to a page, and let the block size itself from its content. The surrounding sample creates the scene and page before this snippet. ```kotlin highlight-android-create-text val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.setPositionX(text, value = 80F) engine.block.setPositionY(text, value = 120F) ``` ## Setting a Typeface After the typeface asset source is loaded, select a typeface that provides regular, bold, italic, and bold italic font variants by metadata and assign its regular font with `engine.block.setFont()`. The sample requires all four variants because later steps toggle bold and italic formatting and apply a bold italic range. Use `setTypeface()` when you want to change the block typeface while retaining existing formatting where possible. ```kotlin highlight-android-set-typeface val typeface = engine.asset.findAssets( sourceId = "ly.img.typeface", query = FindAssetsQuery(query = "", page = 0, perPage = 100), ).assets .mapNotNull { asset -> asset.payload.typeface } .firstOrNull { candidate -> val fonts = candidate.fonts fonts.any { font -> font.weight == FontWeight.NORMAL && font.style == FontStyle.NORMAL } && fonts.any { font -> font.weight == FontWeight.BOLD && font.style == FontStyle.NORMAL } && fonts.any { font -> font.weight == FontWeight.NORMAL && font.style == FontStyle.ITALIC } && fonts.any { font -> font.weight == FontWeight.BOLD && font.style == FontStyle.ITALIC } } ?: error("Typeface with regular, bold, italic, and bold italic fonts not found") val regularFont = typeface.fonts.first { font -> font.weight == FontWeight.NORMAL && font.style == FontStyle.NORMAL } engine.block.setFont(block = text, fontFileUri = regularFont.uri, typeface = typeface) engine.block.setTypeface(block = text, typeface = typeface) val blockTypeface = engine.block.getTypeface(text) ``` Selecting a font by `weight` and `style` keeps the sample stable if the asset source changes the order of the `fonts` array. ## Replacing and Removing Text Use `engine.block.replaceText()` to replace the whole text, insert content at one offset, or replace a specific range. Use `engine.block.removeText()` to delete a range. ```kotlin highlight-android-replace-remove-text engine.block.replaceText(text, text = "Winter Sale") engine.block.replaceText(text, text = " Design", from = 6, to = 6) engine.block.replaceText(text, text = "Campaign", from = 14, to = 18) engine.block.removeText(text, from = 0, to = 7) ``` The sample starts with `Winter Sale`, inserts ` Design`, replaces `Sale` with `Campaign`, then removes the first word so the final text reads `Design Campaign`. ## Applying Text Formatting Formatting APIs accept the same UTF-16 `from` and `to` offsets. You can apply color, font weight, font style, size, and text case to the whole block or to a smaller range. ```kotlin highlight-android-format-ranges val campaignStart = 7 val campaignEnd = 15 engine.block.setTextColor( block = text, color = Color.fromRGBA(r = 0.1F, g = 0.32F, b = 0.78F, a = 1F), from = campaignStart, to = campaignEnd, ) engine.block.setTextFontWeight(text, fontWeight = FontWeight.BOLD, from = campaignStart, to = campaignEnd) engine.block.setTextFontStyle(text, fontStyle = FontStyle.ITALIC, from = campaignStart, to = campaignEnd) engine.block.setTextFontSize(text, fontSize = 24F) engine.block.setTextFontSize(text, fontSize = 32F, from = campaignStart, to = campaignEnd) engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) ``` Use the toggle helpers when your UI should invert the current bold or italic state for a range. Check `canToggleBoldFont()` or `canToggleItalicFont()` before calling the matching toggle method. Decoration toggles are also available for toolbar controls that need underline, strikethrough, or overline formatting. ```kotlin highlight-android-toggle-formatting val designEnd = 6 if (engine.block.canToggleBoldFont(text, from = 0, to = designEnd)) { engine.block.toggleBoldFont(text, from = 0, to = designEnd) } if (engine.block.canToggleItalicFont(text, from = 0, to = designEnd)) { engine.block.toggleItalicFont(text, from = 0, to = designEnd) } ``` ## Querying Text Properties Read formatting back with the matching getter APIs. Formatting getters such as `getTextColors()`, `getTextFontWeights()`, `getTextFontStyles()`, `getTextFontSizes()`, and `getTextDecorations()` return ordered unique values; `getTextCases()` and `getTypefaces()` return the ordered values reported for the selected range. ```kotlin highlight-android-query-formatting val colors = engine.block.getTextColors(text) val fontWeights = engine.block.getTextFontWeights(text) val fontStyles = engine.block.getTextFontStyles(text) val fontSizes = engine.block.getTextFontSizes(text) val textCases = engine.block.getTextCases(text) val typefaces = engine.block.getTypefaces(text) ``` For example, `getTextFontSizes()` returns both sizes after the sample applies a `24F` size to the whole block and a larger `32F` size to the `Campaign` range. ## Cursor and Selection Ranges `getTextCursorRange()` and `setTextCursorRange()` operate on the text block that is currently being edited in the CE.SDK editor UI. They require an active inline text-editing session and are not part of this headless Engine sample. Pass `-1..-1` to select all text when your editor flow is already editing a text block. ## Line Information After the text resources are loaded, query visible line details for layout-aware UI such as overlays or measurement tools. ```kotlin highlight-android-line-info engine.block.forceLoadResources(blocks = listOf(text)) val lineCount = engine.block.getTextVisibleLineCount(text) val firstLine = engine.block.getTextVisibleLineContent(text, lineIndex = 0) val firstLineBounds = engine.block.getTextLineBoundingBoxRect(text, index = 0) ``` `getTextVisibleLineCount()` returns the number of rendered lines, `getTextVisibleLineContent()` returns the content for one visible line, and `getTextLineBoundingBoxRect()` returns that line's bounds in the scene's global coordinate space. ## Font Metrics Use `engine.editor.getFontMetrics()` when you need raw values from a font file for custom layout calculations or text decoration placement. ```kotlin highlight-android-font-metrics val fontMetrics = engine.editor.getFontMetrics(fontFileUri = regularFont.uri.toString()) ``` The returned values are in the font's design-unit coordinate space and include ascender, descender, units per em, line gap, cap height, x-height, underline, and strikeout metrics. ## Troubleshooting - Text does not update: verify the block is valid and has the `"text/edit"` scope before calling `replaceText()` or `removeText()`. - Range edits affect the wrong substring: count UTF-16 code units, not user-perceived characters. Emoji and some combined glyphs can occupy multiple code units. - Bold or italic toggles do nothing: make sure the active typeface includes the matching normal, bold, italic, or bold italic variant. - Cursor range calls fail: select a text block and switch the editor to `Text` mode before calling `setTextCursorRange()`. - Line information returns zero lines: force-load text resources after setting the text content and typeface. ## API Reference | API | Purpose | | --- | --- | | `engine.block.create(blockType=DesignBlockType.Text)` | Create a text block. | | `engine.block.appendChild(parent=_, child=_)` | Add the text block to the page hierarchy. | | `engine.block.setWidthMode(block=_, mode=SizeMode.AUTO)` | Let the text block width follow its content. | | `engine.block.setHeightMode(block=_, mode=SizeMode.AUTO)` | Let the text block height follow its content. | | `engine.block.setPositionX(block=_, value=_)` | Set the text block's horizontal position. | | `engine.block.setPositionY(block=_, value=_)` | Set the text block's vertical position. | | `engine.asset.findAssets(sourceId=_, query=_)` | Find a typeface asset. | | `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Set the text block's font and typeface, resetting existing formatting. | | `engine.block.setTypeface(block=_, typeface=_, from=-1, to=-1)` | Change the text block's typeface while preserving formatting where possible. | | `engine.block.getTypeface(block=_)` | Read the text block's base typeface property. | | `engine.block.replaceText(block=_, text=_, from=-1, to=-1)` | Replace, insert, or overwrite text content. | | `engine.block.removeText(block=_, from=-1, to=-1)` | Remove text content from a UTF-16 range. | | `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create the text color value used by the formatting snippet. | | `engine.block.setTextColor(block=_, color=_, from=-1, to=-1)` | Apply text color to a range. | | `engine.block.setTextFontWeight(block=_, fontWeight=_, from=-1, to=-1)` | Apply font weight to a range. | | `engine.block.setTextFontStyle(block=_, fontStyle=_, from=-1, to=-1)` | Apply font style to a range. | | `engine.block.setTextFontSize(block=_, fontSize=_, from=-1, to=-1)` | Apply font size to a range. | | `engine.block.setTextCase(block=_, textCase=_, from=-1, to=-1)` | Apply a text case transform to a range. | | `engine.block.canToggleBoldFont(block=_, from=-1, to=-1)` | Check whether bold can be toggled for a range. | | `engine.block.toggleBoldFont(block=_, from=-1, to=-1)` | Toggle bold for a range. | | `engine.block.canToggleItalicFont(block=_, from=-1, to=-1)` | Check whether italic can be toggled for a range. | | `engine.block.toggleItalicFont(block=_, from=-1, to=-1)` | Toggle italic for a range. | | `engine.block.toggleTextDecorationUnderline(block=_, from=-1, to=-1)` | Toggle underline decoration for a range. | | `engine.block.toggleTextDecorationStrikethrough(block=_, from=-1, to=-1)` | Toggle strikethrough decoration for a range. | | `engine.block.toggleTextDecorationOverline(block=_, from=-1, to=-1)` | Toggle overline decoration for a range. | | `engine.block.getTextColors(block=_, from=-1, to=-1)` | Get ordered unique text colors in a range. | | `engine.block.getTextFontWeights(block=_, from=-1, to=-1)` | Get ordered unique font weights in a range. | | `engine.block.getTextFontStyles(block=_, from=-1, to=-1)` | Get ordered unique font styles in a range. | | `engine.block.getTextFontSizes(block=_, from=-1, to=-1)` | Get ordered unique font sizes in a range. | | `engine.block.getTextDecorations(block=_, from=-1, to=-1)` | Get ordered unique decoration configurations in a range. | | `engine.block.getTextCases(block=_, from=-1, to=-1)` | Get text case values in a range. | | `engine.block.getTypefaces(block=_, from=-1, to=-1)` | Get typefaces used in a range. | | `engine.block.setTextCursorRange(range=_)` | Set the active text cursor or selection range. | | `engine.block.getTextCursorRange()` | Read the active text cursor or selection range. | | `engine.block.forceLoadResources(blocks=_)` | Load text resources before querying visible line information. | | `engine.block.getTextVisibleLineCount(block=_)` | Get the number of rendered text lines. | | `engine.block.getTextVisibleLineContent(block=_, lineIndex=_)` | Get the text content of a visible line. | | `engine.block.getTextLineBoundingBoxRect(block=_, index=_)` | Get visible line bounds in global scene coordinates. | | `engine.editor.getFontMetrics(fontFileUri=_)` | Read raw metrics from a font file URI. | ## Next Steps - [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) - Insert text blocks into your CE.SDK scene. - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Auto-Size](https://img.ly/docs/cesdk/android/text/auto-size-5331b3/) - Configure text blocks to automatically adapt their dimensions or font size for dynamic content. - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) - Load and manage custom fonts to match brand guidelines or user preferences. --- ## 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 Effects" description: "Add visual depth and interest to text blocks using drop shadows and stroke outlines." platform: android url: "https://img.ly/docs/cesdk/android/text/effects-2dc9fc/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Text Effects](https://img.ly/docs/cesdk/android/text/effects-2dc9fc/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-effects/TextEffects.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.MimeType import ly.img.engine.ShapeType import ly.img.engine.SizeMode import ly.img.engine.StrokePosition import ly.img.engine.StrokeStyle import java.nio.ByteBuffer suspend fun textEffects(engine: Engine): TextEffectsResult { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 500F) 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 = 500F) 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 shadowText = engine.block.create(DesignBlockType.Text) engine.block.replaceText(shadowText, text = "Drop Shadow") engine.block.setTextFontSize(shadowText, fontSize = 90F) engine.block.setWidthMode(shadowText, mode = SizeMode.AUTO) engine.block.setHeightMode(shadowText, mode = SizeMode.AUTO) engine.block.setPositionX(shadowText, value = 50F) engine.block.setPositionY(shadowText, value = 50F) engine.block.appendChild(parent = page, child = shadowText) check(engine.block.supportsDropShadow(block = shadowText)) engine.block.setDropShadowEnabled(block = shadowText, enabled = true) engine.block.setDropShadowColor( block = shadowText, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.6F), ) engine.block.setDropShadowOffsetX(block = shadowText, offsetX = 5F) engine.block.setDropShadowOffsetY(block = shadowText, offsetY = 5F) engine.block.setDropShadowBlurRadiusX(block = shadowText, blurRadiusX = 10F) engine.block.setDropShadowBlurRadiusY(block = shadowText, blurRadiusY = 10F) val outlineText = engine.block.create(DesignBlockType.Text) engine.block.replaceText(outlineText, text = "Outline") engine.block.setTextFontSize(outlineText, fontSize = 90F) engine.block.setWidthMode(outlineText, mode = SizeMode.AUTO) engine.block.setHeightMode(outlineText, mode = SizeMode.AUTO) engine.block.setPositionX(outlineText, value = 50F) engine.block.setPositionY(outlineText, value = 180F) engine.block.appendChild(parent = page, child = outlineText) check(engine.block.supportsStroke(block = outlineText)) engine.block.setStrokeEnabled(block = outlineText, enabled = true) engine.block.setStrokeWidth(block = outlineText, width = 2F) engine.block.setStrokeColor( block = outlineText, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F), ) engine.block.setStrokeStyle(block = outlineText, style = StrokeStyle.SOLID) engine.block.setStrokePosition(block = outlineText, position = StrokePosition.CENTER) val pngData = engine.block.export(block = page, mimeType = MimeType.PNG) check(pngData.hasRemaining()) { "Text effects PNG export is empty" } val pngBytes = ByteArray(pngData.remaining()) pngData.asReadOnlyBuffer().get(pngBytes) return TextEffectsResult( dropShadowEnabled = engine.block.isDropShadowEnabled(block = shadowText), dropShadowColor = engine.block.getDropShadowColor(block = shadowText), dropShadowOffsetX = engine.block.getDropShadowOffsetX(block = shadowText), dropShadowOffsetY = engine.block.getDropShadowOffsetY(block = shadowText), dropShadowBlurRadiusX = engine.block.getDropShadowBlurRadiusX(block = shadowText), dropShadowBlurRadiusY = engine.block.getDropShadowBlurRadiusY(block = shadowText), strokeEnabled = engine.block.isStrokeEnabled(block = outlineText), strokeWidth = engine.block.getStrokeWidth(block = outlineText), strokeColor = engine.block.getStrokeColor(block = outlineText), strokeStyle = engine.block.getStrokeStyle(block = outlineText), strokePosition = engine.block.getStrokePosition(block = outlineText), pngData = ByteBuffer.wrap(pngBytes).asReadOnlyBuffer(), ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-text-effects/TextEffectsResult.kt reference-only import ly.img.engine.Color import ly.img.engine.StrokePosition import ly.img.engine.StrokeStyle import java.nio.ByteBuffer data class TextEffectsResult( val dropShadowEnabled: Boolean, val dropShadowColor: Color, val dropShadowOffsetX: Float, val dropShadowOffsetY: Float, val dropShadowBlurRadiusX: Float, val dropShadowBlurRadiusY: Float, val strokeEnabled: Boolean, val strokeWidth: Float, val strokeColor: Color, val strokeStyle: StrokeStyle, val strokePosition: StrokePosition, val pngData: ByteBuffer, ) ``` Add visual depth and interest to text blocks using drop shadows and stroke outlines. ![Android text blocks with drop shadow and stroke outline](./assets/android.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-effects) Text effects that are directly supported on text blocks include drop shadows for depth and stroke outlines for text borders. These visual effects are distinct from text styling properties like colors, fonts, and backgrounds. This guide covers how to apply text effects programmatically using the Block API. ## Using the Built-in Effects UI The CE.SDK editor UI exposes stroke controls for selected text blocks through the Fill & Stroke sheet when `supportsStroke()` is `true` and the `stroke/change` scope is allowed for the block. Drop shadows are not exposed through the current Android editor UI; configure text shadows programmatically with the Block API as shown below. For complete editor UI setup, start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). Generic blur, filter, and effect-stack controls belong to Filters & Effects and apply only to visual blocks that report support. ## Drop Shadows Drop shadows add depth and emphasis to text. In Android integrations, configure text shadow properties programmatically with dedicated Block API methods. ```kotlin highlight-android-drop-shadow check(engine.block.supportsDropShadow(block = shadowText)) engine.block.setDropShadowEnabled(block = shadowText, enabled = true) engine.block.setDropShadowColor( block = shadowText, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 0.6F), ) engine.block.setDropShadowOffsetX(block = shadowText, offsetX = 5F) engine.block.setDropShadowOffsetY(block = shadowText, offsetY = 5F) engine.block.setDropShadowBlurRadiusX(block = shadowText, blurRadiusX = 10F) engine.block.setDropShadowBlurRadiusY(block = shadowText, blurRadiusY = 10F) ``` The drop shadow API provides control over color, position, and blur. The offset values position the shadow relative to the text, while the blur radius controls shadow softness. Horizontal and vertical blur can be configured independently for asymmetric effects. ## Stroke Outlines Stroke outlines add a colored border around text. Enable stroke with `setStrokeEnabled()`, then configure width, color, style, and position. ```kotlin highlight-android-stroke check(engine.block.supportsStroke(block = outlineText)) engine.block.setStrokeEnabled(block = outlineText, enabled = true) engine.block.setStrokeWidth(block = outlineText, width = 2F) engine.block.setStrokeColor( block = outlineText, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F), ) engine.block.setStrokeStyle(block = outlineText, style = StrokeStyle.SOLID) engine.block.setStrokePosition(block = outlineText, position = StrokePosition.CENTER) ``` The stroke width is specified in design units. Text blocks support `StrokePosition.CENTER`, `StrokePosition.INNER`, and `StrokePosition.OUTER` via `setStrokePosition()`. Stroke styles include `StrokeStyle.SOLID`, `StrokeStyle.DASHED`, `StrokeStyle.DOTTED`, and other line patterns. ## Other Effects Text blocks do not support the blur or effect-stack APIs. Use `supportsBlur()` and `supportsEffects()` before applying those APIs to other block types, and see [Filters & Effects Overview](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) for generic block effects. ## API Reference | Method | Purpose | |--------|---------| | `engine.block.supportsDropShadow(block=_)` | Check whether a block supports drop shadows | | `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable the drop shadow | | `engine.block.isDropShadowEnabled(block=_)` | Check whether the drop shadow is enabled | | `engine.block.setDropShadowColor(block=_, color=_)` | Set the shadow color | | `engine.block.getDropShadowColor(block=_)` | Get the current shadow color | | `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set the horizontal shadow offset | | `engine.block.getDropShadowOffsetX(block=_)` | Get the current horizontal shadow offset | | `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set the vertical shadow offset | | `engine.block.getDropShadowOffsetY(block=_)` | Get the current vertical shadow offset | | `engine.block.setDropShadowBlurRadiusX(block=_, blurRadiusX=_)` | Set the horizontal blur radius | | `engine.block.getDropShadowBlurRadiusX(block=_)` | Get the current horizontal blur radius | | `engine.block.setDropShadowBlurRadiusY(block=_, blurRadiusY=_)` | Set the vertical blur radius | | `engine.block.getDropShadowBlurRadiusY(block=_)` | Get the current vertical blur radius | | `engine.block.supportsStroke(block=_)` | Check whether a block supports strokes | | `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable the stroke | | `engine.block.isStrokeEnabled(block=_)` | Check whether the stroke is enabled | | `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke width | | `engine.block.getStrokeWidth(block=_)` | Get the current stroke width | | `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color | | `engine.block.getStrokeColor(block=_)` | Get the current stroke color | | `engine.block.setStrokeStyle(block=_, style=_)` | Set the stroke line pattern | | `engine.block.getStrokeStyle(block=_)` | Get the current stroke line pattern | | `engine.block.setStrokePosition(block=_, position=_)` | Set the stroke position relative to the text edge | | `engine.block.getStrokePosition(block=_)` | Get the current stroke position | ## Troubleshooting **Drop shadow not visible**: Ensure `setDropShadowEnabled()` is called with `true`. Verify `supportsDropShadow()` returns `true` for the text block, then adjust the shadow color, offset, and blur so the shadow is visible against the background. **Stroke not visible**: Ensure `setStrokeEnabled()` is called with `true` and stroke width is greater than `0F`. **Stroke too thick or thin**: Adjust the value passed to `setStrokeWidth()` to control outline thickness. ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Configure fonts, colors, alignment, and other styling options. - [Using Strokes](https://img.ly/docs/cesdk/android/outlines/strokes-c2e621/) - Work with stroke controls beyond text outlines. - [Filters & Effects Overview](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) - Explore visual effects such as blur, duotone, LUTs, and chroma keying. - [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: "Emojis" description: "Insert and style emojis alongside text for expressive, modern typographic designs." platform: android url: "https://img.ly/docs/cesdk/android/text/emojis-510651/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Emojis](https://img.ly/docs/cesdk/android/text/emojis-510651/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-with-emojis/TextWithEmojis.kt reference-only import android.net.Uri 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 suspend fun textWithEmojis( engine: Engine, appAssetBaseUri: Uri, ): TextWithEmojisResult { val originalEmojiFontUri = engine.editor.getSettingString(keypath = "defaultEmojiFontFileUri") val originalForceSystemEmojis = engine.editor.getSettingBoolean(keypath = "forceSystemEmojis") try { val configuredEmojiFontUri = engine.editor.getSettingString( keypath = "defaultEmojiFontFileUri", ) val assetBaseUri = engine.editor .getSettingString(keypath = "basePath") .trimEnd('/') val defaultEmojiFontUri = "$assetBaseUri/emoji/NotoColorEmoji.ttf" val emojiFontUri = appAssetBaseUri .buildUpon() .appendPath("emoji") .appendPath("NotoColorEmoji.ttf") .build() .toString() engine.editor.setSettingString( keypath = "defaultEmojiFontFileUri", value = emojiFontUri, ) engine.editor.setSettingBoolean( keypath = "forceSystemEmojis", value = true, ) val forceSystemEmojis = engine.editor.getSettingBoolean( keypath = "forceSystemEmojis", ) val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 1000F) engine.block.setHeight(page, value = 640F) 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 = 1000F) engine.block.setHeight(background, value = 640F) 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 primaryText = "Emoji text 🎉✨" val emojiTextBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = emojiTextBlock) engine.block.replaceText(block = emojiTextBlock, text = primaryText) engine.block.setTextFontSize(block = emojiTextBlock, fontSize = 16F) engine.block.setWidth(block = emojiTextBlock, value = 880F) engine.block.setWidthMode(block = emojiTextBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = emojiTextBlock, mode = SizeMode.AUTO) engine.block.setPositionX(block = emojiTextBlock, value = 60F) engine.block.setPositionY(block = emojiTextBlock, value = 64F) val emojiExamples = listOf( "Single: 😀 🎉", "Flags: 🇩🇪 🇺🇸", "Family: 👨‍👩‍👧", "Tone: 👋🏽 👍🏾", ) emojiExamples.forEachIndexed { index, text -> val textBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = textBlock) engine.block.replaceText(block = textBlock, text = text) engine.block.setTextFontSize(block = textBlock, fontSize = 12F) engine.block.setWidthMode(block = textBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(block = textBlock, mode = SizeMode.AUTO) engine.block.setPositionX(block = textBlock, value = 60F) engine.block.setPositionY(block = textBlock, value = 190F + index * 72F) } val pngData = engine.block.export(block = page, mimeType = MimeType.PNG) check(pngData.hasRemaining()) { "Text with emojis PNG export is empty." } val pngBytes = ByteArray(pngData.remaining()) pngData.asReadOnlyBuffer().get(pngBytes) return TextWithEmojisResult( configuredEmojiFontUri = configuredEmojiFontUri, defaultEmojiFontUri = defaultEmojiFontUri, activeEmojiFontUri = engine.editor.getSettingString(keypath = "defaultEmojiFontFileUri"), forceSystemEmojis = forceSystemEmojis, primaryText = primaryText, emojiExamples = emojiExamples, exportedPng = ByteBuffer.wrap(pngBytes).asReadOnlyBuffer(), ) } finally { engine.editor.setSettingString( keypath = "defaultEmojiFontFileUri", value = originalEmojiFontUri, ) engine.editor.setSettingBoolean( keypath = "forceSystemEmojis", value = originalForceSystemEmojis, ) } } ``` ```kotlin file=@cesdk_android_examples/engine-guides-text-with-emojis/TextWithEmojisResult.kt reference-only import java.nio.ByteBuffer data class TextWithEmojisResult( val configuredEmojiFontUri: String, val defaultEmojiFontUri: String, val activeEmojiFontUri: String, val forceSystemEmojis: Boolean, val primaryText: String, val emojiExamples: List, val exportedPng: ByteBuffer, ) ``` Configure emoji rendering in CE.SDK text blocks with a dedicated emoji font for consistent display across Android devices. ![Rendered Android scene with emoji text examples](./assets/android.hero.png) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-with-emojis) Emojis are Unicode characters that can be single code points, flag sequences, joined family symbols, or skin tone variants. CE.SDK renders them through the same text engine as other text content, with a configurable emoji font for consistent output. This guide covers the default emoji font, how to set an emoji font URI, and how to add emoji text programmatically. ## Default Emoji Font When `defaultEmojiFontFileUri` is empty, CE.SDK uses Noto Color Emoji from the active asset base URI at `emoji/NotoColorEmoji.ttf`. Android initializes `basePath` as `file:///android_asset`, so the default emoji font URI resolves to `file:///android_asset/emoji/NotoColorEmoji.ttf` unless your app changes `basePath`. Read the configured override with `getSettingString()`. An empty string means CE.SDK falls back to the default font URI derived from the active asset base. ```kotlin highlight-android-default-emoji-font val configuredEmojiFontUri = engine.editor.getSettingString( keypath = "defaultEmojiFontFileUri", ) val assetBaseUri = engine.editor .getSettingString(keypath = "basePath") .trimEnd('/') val defaultEmojiFontUri = "$assetBaseUri/emoji/NotoColorEmoji.ttf" ``` ## Configuring the Emoji Font Set `defaultEmojiFontFileUri` when you want to use another emoji font or host the font from a specific location. The URI can point to a reader-owned CDN URL or an Android asset path such as `file:///android_asset/emoji/CustomEmoji.ttf`. The sample function receives your app-owned asset base URI as `appAssetBaseUri`. Point that base URI at a bundled or self-hosted CE.SDK asset directory that contains `emoji/NotoColorEmoji.ttf`, then derive the font URI from that base: ```kotlin highlight-android-configure-emoji-font val emojiFontUri = appAssetBaseUri .buildUpon() .appendPath("emoji") .appendPath("NotoColorEmoji.ttf") .build() .toString() engine.editor.setSettingString( keypath = "defaultEmojiFontFileUri", value = emojiFontUri, ) ``` ## Adding Emojis to Text Blocks Create a text block with `DesignBlockType.Text`, attach it to the page, and use `replaceText()` to insert text that contains emoji characters. ```kotlin highlight-android-create-text-with-emojis val primaryText = "Emoji text 🎉✨" val emojiTextBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = emojiTextBlock) engine.block.replaceText(block = emojiTextBlock, text = primaryText) engine.block.setTextFontSize(block = emojiTextBlock, fontSize = 16F) engine.block.setWidth(block = emojiTextBlock, value = 880F) engine.block.setWidthMode(block = emojiTextBlock, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = emojiTextBlock, mode = SizeMode.AUTO) engine.block.setPositionX(block = emojiTextBlock, value = 60F) engine.block.setPositionY(block = emojiTextBlock, value = 64F) ``` CE.SDK handles common emoji forms automatically, including single code points, flag sequences, zero-width-joiner sequences, and skin tone modifiers. ```kotlin highlight-android-emoji-examples val emojiExamples = listOf( "Single: 😀 🎉", "Flags: 🇩🇪 🇺🇸", "Family: 👨‍👩‍👧", "Tone: 👋🏽 👍🏾", ) emojiExamples.forEachIndexed { index, text -> val textBlock = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = textBlock) engine.block.replaceText(block = textBlock, text = text) engine.block.setTextFontSize(block = textBlock, fontSize = 12F) engine.block.setWidthMode(block = textBlock, mode = SizeMode.AUTO) engine.block.setHeightMode(block = textBlock, mode = SizeMode.AUTO) engine.block.setPositionX(block = textBlock, value = 60F) engine.block.setPositionY(block = textBlock, value = 190F + index * 72F) } ``` ## The `forceSystemEmojis` Setting `forceSystemEmojis` defaults to `true`. With this setting enabled, CE.SDK uses the configured emoji font for emoji graphemes even when the current text font contains an emoji glyph. When disabled, CE.SDK only falls back to the emoji font if the active text font cannot render the emoji. ```kotlin highlight-android-force-system-emojis engine.editor.setSettingBoolean( keypath = "forceSystemEmojis", value = true, ) val forceSystemEmojis = engine.editor.getSettingBoolean( keypath = "forceSystemEmojis", ) ``` ## Troubleshooting **Emojis look different than expected**: CE.SDK uses Noto Color Emoji by default. Set `defaultEmojiFontFileUri` to a different emoji font if your design needs another emoji style. **Missing emojis**: Verify that the configured font supports the Unicode emoji sequences your content uses. **Font download is large**: The default Noto Color Emoji file is about 9.9 MB. Host it with the rest of your CE.SDK assets or bundle it with your app when startup network cost matters. **Emoji font does not load**: Check that the URI is reachable from the Android device and that remote hosts allow font downloads. ## API Reference | API | Category | Purpose | | --- | --- | --- | | `engine.editor.getSettingString(keypath="basePath")` | Editor | Read the active asset base URI used to derive the standard emoji font URI. | | `engine.editor.getSettingString(keypath="defaultEmojiFontFileUri")` | Editor | Read the configured emoji font URI override. | | `engine.editor.setSettingString(keypath="defaultEmojiFontFileUri", value=_)` | Editor | Set the emoji font URI override. | | `engine.editor.getSettingBoolean(keypath="forceSystemEmojis")` | Editor | Read whether CE.SDK forces emoji graphemes to use the emoji font. | | `engine.editor.setSettingBoolean(keypath="forceSystemEmojis", value=_)` | Editor | Configure whether CE.SDK always uses the emoji font for emoji graphemes. | | `engine.block.create(blockType=_)` | Block | Create a text block with `DesignBlockType.Text`. | | `engine.block.appendChild(parent=_, child=_)` | Block | Attach the text block to the scene hierarchy. | | `engine.block.replaceText(block=_, text=_)` | Text | Set text content, including Unicode emoji characters. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Text | Set the font size for the text block. | | `engine.block.setWidth(block=_, value=_)` | Layout | Set an explicit text block width. | | `engine.block.setWidthMode(block=_, mode=_)` | Layout | Configure how CE.SDK resolves the block width. | | `engine.block.setHeightMode(block=_, mode=_)` | Layout | Configure how CE.SDK resolves the block height. | | `engine.block.setPositionX(block=_, value=_)` | Layout | Set the block's horizontal position. | | `engine.block.setPositionY(block=_, value=_)` | Layout | Set the block's vertical position. | ## Next Steps - [Text Overview](https://img.ly/docs/cesdk/android/text/overview-0bd620/) - Learn about text editing capabilities in CE.SDK. - [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) - Insert text blocks into your CE.SDK scene. - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) — Load and manage custom fonts to match brand guidelines or user preferences. --- ## 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 Enumerations" description: "Add bullet lists and numbered lists to text blocks in CE.SDK using per-paragraph list styles and nesting levels." platform: android url: "https://img.ly/docs/cesdk/android/text/enumerations-b5c1d2/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Text Enumerations](https://img.ly/docs/cesdk/android/text/enumerations-b5c1d2/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-enumerations/TextEnumerations.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.ListStyle import ly.img.engine.SizeMode fun textEnumerations( 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 text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "First item\nSecond item\nThird item") // Apply ordered list style to all paragraphs (paragraphIndex defaults to -1 = all) engine.block.setTextListStyle(text, listStyle = ListStyle.ORDERED) // Override the third paragraph (index 2) to unordered engine.block.setTextListStyle(text, listStyle = ListStyle.UNORDERED, paragraphIndex = 2) // Set the second paragraph (index 1) to nesting level 1 (one indent deep) engine.block.setTextListLevel(text, listLevel = 1, paragraphIndex = 1) // Read back the nesting level to confirm val level = engine.block.getTextListLevel(text, paragraphIndex = 1) println("Second paragraph nesting level: $level") // Atomically set both list style and nesting level in one call // Sets paragraph 0 to ordered style at nesting level 0 (outermost) engine.block.setTextListStyle( text, listStyle = ListStyle.ORDERED, paragraphIndex = 0, listLevel = 0, ) // Get all paragraph indices in the text block val allIndices = engine.block.getTextParagraphIndices(text) println("All paragraph indices: $allIndices") // Get indices overlapping a specific grapheme range val rangeIndices = engine.block.getTextParagraphIndices(text, from = 0, to = 10) println("Indices for range [0, 10): $rangeIndices") // Read back the list style and nesting level for each paragraph val styles = allIndices.map { index -> engine.block.getTextListStyle(text, paragraphIndex = index) } val levels = allIndices.map { index -> engine.block.getTextListLevel(text, paragraphIndex = index) } println("Paragraph styles: $styles") println("Paragraph levels: $levels") engine.stop() } ``` Apply bullet and numbered list styles to text blocks, control nesting levels, and query the list configuration of any paragraph. CE.SDK formats lists per paragraph. Each paragraph in a text block has an independent `ListStyle` (`NONE`, `UNORDERED`, or `ORDERED`) and a zero-based nesting level. This example runs in the offscreen engine. For evaluation mode, pass `license = null`; for production, pass your CE.SDK license string. ## Setup Create a text block with three paragraphs separated by newline characters. ```kotlin highlight-textEnumerations-setup val scene = engine.scene.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "First item\nSecond item\nThird item") ``` ## Apply List Styles Use `engine.block.setTextListStyle()` to apply a list style to one or all paragraphs. Passing no `paragraphIndex` (default `-1`) applies the style to every paragraph. | Value | Renders as | |-------|-----------| | `ListStyle.UNORDERED` | Bullet marker (•) | | `ListStyle.ORDERED` | Auto-incrementing number (1., 2., …) | | `ListStyle.NONE` | Plain text (removes list formatting) | ```kotlin highlight-textEnumerations-applyListStyles // Apply ordered list style to all paragraphs (paragraphIndex defaults to -1 = all) engine.block.setTextListStyle(text, listStyle = ListStyle.ORDERED) // Override the third paragraph (index 2) to unordered engine.block.setTextListStyle(text, listStyle = ListStyle.UNORDERED, paragraphIndex = 2) ``` > **Note:** Write operations require the `"text/character"` scope on the text block. ## Manage Nesting Levels Control the visual depth of list items with `engine.block.setTextListLevel()`. The level is zero-based (`0` is outermost). Use `engine.block.getTextListLevel()` to read the current depth. ```kotlin highlight-textEnumerations-manageNesting // Set the second paragraph (index 1) to nesting level 1 (one indent deep) engine.block.setTextListLevel(text, listLevel = 1, paragraphIndex = 1) // Read back the nesting level to confirm val level = engine.block.getTextListLevel(text, paragraphIndex = 1) println("Second paragraph nesting level: $level") ``` Nesting has no visible effect when the list style is `ListStyle.NONE`. ## Atomic Style and Level Assignment Pass the optional `listLevel` parameter to `setTextListStyle()` to set style and nesting in one call. ```kotlin highlight-textEnumerations-atomic // Atomically set both list style and nesting level in one call // Sets paragraph 0 to ordered style at nesting level 0 (outermost) engine.block.setTextListStyle( text, listStyle = ListStyle.ORDERED, paragraphIndex = 0, listLevel = 0, ) ``` ## Resolve Paragraph Indices Use `engine.block.getTextParagraphIndices()` to find which 0-based paragraphs overlap a grapheme range. Passing no range returns all paragraph indices. For selection-driven flows, first call `engine.block.getTextCursorRange()` and pass its start/end indices into `getTextParagraphIndices()`. ```kotlin highlight-textEnumerations-paragraphIndices // Get all paragraph indices in the text block val allIndices = engine.block.getTextParagraphIndices(text) println("All paragraph indices: $allIndices") // Get indices overlapping a specific grapheme range val rangeIndices = engine.block.getTextParagraphIndices(text, from = 0, to = 10) println("Indices for range [0, 10): $rangeIndices") ``` ## Query List Styles Read back styles and levels with `engine.block.getTextListStyle()` and `engine.block.getTextListLevel()`. Both getters require a non-negative `paragraphIndex`. ```kotlin highlight-textEnumerations-queryListStyles // Read back the list style and nesting level for each paragraph val styles = allIndices.map { index -> engine.block.getTextListStyle(text, paragraphIndex = index) } val levels = allIndices.map { index -> engine.block.getTextListLevel(text, paragraphIndex = index) } println("Paragraph styles: $styles") println("Paragraph levels: $levels") ``` The output confirms styles `[ORDERED, ORDERED, UNORDERED]` and levels `[0, 1, 0]`. ## Full Code ```kotlin highlight-textEnumerations 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.ListStyle import ly.img.engine.SizeMode fun textEnumerations( 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 text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "First item\nSecond item\nThird item") // Apply ordered list style to all paragraphs (paragraphIndex defaults to -1 = all) engine.block.setTextListStyle(text, listStyle = ListStyle.ORDERED) // Override the third paragraph (index 2) to unordered engine.block.setTextListStyle(text, listStyle = ListStyle.UNORDERED, paragraphIndex = 2) // Set the second paragraph (index 1) to nesting level 1 (one indent deep) engine.block.setTextListLevel(text, listLevel = 1, paragraphIndex = 1) // Read back the nesting level to confirm val level = engine.block.getTextListLevel(text, paragraphIndex = 1) println("Second paragraph nesting level: $level") // Atomically set both list style and nesting level in one call // Sets paragraph 0 to ordered style at nesting level 0 (outermost) engine.block.setTextListStyle( text, listStyle = ListStyle.ORDERED, paragraphIndex = 0, listLevel = 0, ) // Get all paragraph indices in the text block val allIndices = engine.block.getTextParagraphIndices(text) println("All paragraph indices: $allIndices") // Get indices overlapping a specific grapheme range val rangeIndices = engine.block.getTextParagraphIndices(text, from = 0, to = 10) println("Indices for range [0, 10): $rangeIndices") // Read back the list style and nesting level for each paragraph val styles = allIndices.map { index -> engine.block.getTextListStyle(text, paragraphIndex = index) } val levels = allIndices.map { index -> engine.block.getTextListLevel(text, paragraphIndex = index) } println("Paragraph styles: $styles") println("Paragraph levels: $levels") engine.stop() } ``` ## Troubleshooting - List markers are not visible: confirm you enabled the `"text/character"` scope before calling setters. - `getTextListStyle()` or `getTextListLevel()` fails: pass a non-negative paragraph index from `getTextParagraphIndices()`. - Nesting level appears unchanged: verify the paragraph style is not `ListStyle.NONE`. - Ordered numbers restart unexpectedly: inserting `ListStyle.NONE` paragraphs can split numbering runs. ## API Reference | Method | Purpose | |--------|---------| | engine.block.setTextListStyle() | Apply list style to one or all paragraphs, optionally setting nesting level atomically | | engine.block.getTextListStyle() | Get the list style of a specific paragraph | | engine.block.setTextListLevel() | Set the nesting level of one or all paragraphs | | engine.block.getTextListLevel() | Get the nesting level of a specific paragraph | | engine.block.getTextParagraphIndices() | Get paragraph indices overlapping a grapheme range | ## Next Steps [Text Decorations](https://img.ly/docs/cesdk/android/text/decorations-d3c0a1/) [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) [Edit Text](https://img.ly/docs/cesdk/android/text/edit-c5106b/) --- ## 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 and Language Support" description: "Create designs that work across languages and writing systems with RTL text, complex scripts, and multilingual font support." platform: android url: "https://img.ly/docs/cesdk/android/text/language-support-a0f010/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Language Support](https://img.ly/docs/cesdk/android/text/language-support-a0f010/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-language-support/LanguageSupport.kt reference-only import android.net.Uri import kotlinx.coroutines.delay import kotlinx.coroutines.yield import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.HorizontalAlignment import ly.img.engine.Typeface private object LanguageLayout { const val PAGE_WIDTH = 800F const val PAGE_HEIGHT = 1200F const val TEXT_X = 50F const val TEXT_WIDTH = 700F const val FONT_SIZE = 42F const val GAP = 24F } private const val GUIDE_ASSET_BASE_URI = "file:///android_asset/imgly-assets" private const val GUIDE_FONT_BASE_URI = "$GUIDE_ASSET_BASE_URI/fonts" private val BRAND_LABEL_FONT_URI = Uri.parse("$GUIDE_FONT_BASE_URI/font-10.ttf") private val ARABIC_SUPPORT_FONT_URI = Uri.parse("$GUIDE_FONT_BASE_URI/font-6.ttf") private val KOREAN_SUPPORT_FONT_URI = Uri.parse("$GUIDE_FONT_BASE_URI/font-30.ttf") private data class MultilingualTypefaces( val label: Typeface, val rtl: Typeface, val korean: Typeface, ) private fun multilingualTypefaces() = MultilingualTypefaces( label = Typeface( name = "BrandLabel", fonts = listOf( Font( uri = BRAND_LABEL_FONT_URI, subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ), rtl = Typeface( name = "ArabicSupport", fonts = listOf( Font( uri = ARABIC_SUPPORT_FONT_URI, subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ), korean = Typeface( name = "KoreanSupport", fonts = listOf( Font( uri = KOREAN_SUPPORT_FONT_URI, subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ), ) private data class LanguageTextElement( val text: String, val typeface: Typeface, val fontWeight: FontWeight, val fontStyle: FontStyle = FontStyle.NORMAL, val fontSubFamily: String? = null, val height: Float = 140F, val alignment: HorizontalAlignment = HorizontalAlignment.Auto, ) private fun createTextBlock( engine: Engine, page: DesignBlock, element: LanguageTextElement, yPosition: Float, ): DesignBlock { val textBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = textBlock, text = element.text) engine.block.setPositionX(textBlock, value = LanguageLayout.TEXT_X) engine.block.setPositionY(textBlock, value = yPosition) engine.block.setWidth(textBlock, value = LanguageLayout.TEXT_WIDTH) engine.block.setHeight(textBlock, value = element.height) engine.block.setTextFontSize(textBlock, fontSize = LanguageLayout.FONT_SIZE) val font = element.typeface.fonts.firstOrNull { font -> font.weight == element.fontWeight && font.style == element.fontStyle && (element.fontSubFamily == null || font.subFamily == element.fontSubFamily) } ?: error( "Missing ${element.fontWeight}/${element.fontStyle}" + " font variant for ${element.typeface.name}", ) engine.block.setFont( block = textBlock, fontFileUri = font.uri, typeface = element.typeface, ) engine.block.setTextHorizontalAlignment(block = textBlock, alignment = element.alignment) engine.block.appendChild(parent = page, child = textBlock) return textBlock } private suspend fun waitForResolvedRtlAlignment( engine: Engine, block: DesignBlock, initialAlignment: HorizontalAlignment, ): HorizontalAlignment { var effectiveAlignment = initialAlignment var layoutPasses = 0 while (effectiveAlignment != HorizontalAlignment.Right && layoutPasses < 10) { // Offscreen text layout may resolve on the next frame after fonts load. yield() delay(16) effectiveAlignment = engine.block.getTextEffectiveHorizontalAlignment(block) layoutPasses += 1 } return effectiveAlignment } suspend fun textLanguageSupport(engine: Engine): LanguageSupportResult { val typefaces = multilingualTypefaces() val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = LanguageLayout.PAGE_WIDTH) engine.block.setHeight(page, value = LanguageLayout.PAGE_HEIGHT) engine.block.appendChild(parent = scene, child = page) val textElements = listOf( LanguageTextElement( text = "RTL Arabic", typeface = typefaces.label, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), LanguageTextElement( text = "هذا مثال.", typeface = typefaces.rtl, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", height = 160F, ), LanguageTextElement( text = "Korean", typeface = typefaces.label, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), LanguageTextElement( text = "이는 한 예입니다.", typeface = typefaces.korean, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), ) val textBlocks = mutableListOf() var currentY = 50F for (element in textElements) { textBlocks.add(createTextBlock(engine, page, element, currentY)) currentY += element.height + LanguageLayout.GAP } val rtlBlock = textBlocks[1] engine.block.setTextHorizontalAlignment( block = rtlBlock, alignment = HorizontalAlignment.Auto, ) engine.block.forceLoadResources(listOf(rtlBlock)) val effectiveRtlAlignment = engine.block.getTextEffectiveHorizontalAlignment(rtlBlock) val verifiedEffectiveRtlAlignment = waitForResolvedRtlAlignment( engine = engine, block = rtlBlock, initialAlignment = effectiveRtlAlignment, ) val mixedText = "Campaign: مرحبا" val mixedBlock = createTextBlock( engine = engine, page = page, element = LanguageTextElement( text = mixedText, typeface = typefaces.label, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), yPosition = currentY, ) val rtlStart = mixedText.indexOf("مرحبا") engine.block.setTypeface( block = mixedBlock, typeface = typefaces.rtl, from = rtlStart, to = mixedText.length, ) val mixedTypefaces = engine.block.getTypefaces(mixedBlock) currentY += 140F + LanguageLayout.GAP val paragraphBlock = createTextBlock( engine = engine, page = page, element = LanguageTextElement( text = "English headline\nمرحبا بالعالم", typeface = typefaces.rtl, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", height = 180F, ), yPosition = currentY, ) val paragraphIndices = engine.block.getTextParagraphIndices(paragraphBlock) val rtlParagraphIndex = paragraphIndices.last() engine.block.setTextHorizontalAlignment( block = paragraphBlock, alignment = HorizontalAlignment.Right, paragraphIndex = rtlParagraphIndex, ) val paragraphOverride = engine.block.getTextHorizontalAlignment( block = paragraphBlock, paragraphIndex = rtlParagraphIndex, ) engine.block.setTextHorizontalAlignment( block = paragraphBlock, alignment = null, paragraphIndex = rtlParagraphIndex, ) val clearedParagraphOverride = engine.block.getTextHorizontalAlignment( block = paragraphBlock, paragraphIndex = rtlParagraphIndex, ) currentY += 180F + LanguageLayout.GAP engine.variable.set(key = "localized_headline", value = "عنوان الحملة") createTextBlock( engine = engine, page = page, element = LanguageTextElement( text = "{{localized_headline}}", typeface = typefaces.rtl, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", height = 120F, ), yPosition = currentY, ) val localizedHeadline = engine.variable.get(key = "localized_headline") return LanguageSupportResult( textValues = textBlocks.map { block -> engine.block.getString(block = block, property = "text/text") }, effectiveRtlAlignment = verifiedEffectiveRtlAlignment, paragraphIndices = paragraphIndices, paragraphOverride = paragraphOverride, clearedParagraphOverride = clearedParagraphOverride, mixedTypefaceNames = mixedTypefaces.map { it.name }, localizedHeadline = localizedHeadline, ) } ``` Support right-to-left text, complex scripts, and multilingual typography in Android designs using CE.SDK text rendering and font APIs. ![Text and Language Support example showing multilingual text in different scripts](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-language-support) CE.SDK renders Unicode text, bidirectional layout, and script-specific glyph shaping through the Engine. This guide shows how to define typefaces for different scripts, apply them to text blocks, control right-to-left alignment, and bind multilingual template text to variables. ## Using the Built-in Text UI for Multilingual Content The CE.SDK editor UI accepts text in any Unicode script and uses the Engine's text shaping to render right-to-left and complex-script content. Users can type Arabic, Hebrew, Korean, Indic scripts, and mixed-direction text directly on the canvas while the editor preserves the text content as a normal text block. The text formatting controls expose typeface, size, color, and horizontal alignment settings. If your integration adds custom typefaces for specific scripts, expose them through your typeface asset source so users can pick them from the font panel. > **Note:** The [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) provides a complete > Android editing surface. This guide focuses on the Engine APIs that the UI > uses underneath. ## Programmatic Font and Typeface Management ### Configure Typefaces for Language Support Define one `Typeface` per font family you want to apply. Each `Font` points to a font file URI and declares its weight and style so CE.SDK can keep formatting stable when you switch typefaces. ```kotlin highlight-android-configure-fonts private data class MultilingualTypefaces( val label: Typeface, val rtl: Typeface, val korean: Typeface, ) private fun multilingualTypefaces() = MultilingualTypefaces( label = Typeface( name = "BrandLabel", fonts = listOf( Font( uri = BRAND_LABEL_FONT_URI, subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ), rtl = Typeface( name = "ArabicSupport", fonts = listOf( Font( uri = ARABIC_SUPPORT_FONT_URI, subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ), korean = Typeface( name = "KoreanSupport", fonts = listOf( Font( uri = KOREAN_SUPPORT_FONT_URI, subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ), ) ``` The sample uses bundled Android asset fonts for the Latin labels, Arabic text, and Korean text. In your app, use font files that include the glyphs and OpenType layout tables required by the scripts you support. ### Create a Text Block Helper Keep layout and text-block setup in one helper so every multilingual text block uses the same sizing, typeface, font size, and alignment defaults. ```kotlin highlight-android-helper-function private data class LanguageTextElement( val text: String, val typeface: Typeface, val fontWeight: FontWeight, val fontStyle: FontStyle = FontStyle.NORMAL, val fontSubFamily: String? = null, val height: Float = 140F, val alignment: HorizontalAlignment = HorizontalAlignment.Auto, ) private fun createTextBlock( engine: Engine, page: DesignBlock, element: LanguageTextElement, yPosition: Float, ): DesignBlock { val textBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = textBlock, text = element.text) engine.block.setPositionX(textBlock, value = LanguageLayout.TEXT_X) engine.block.setPositionY(textBlock, value = yPosition) engine.block.setWidth(textBlock, value = LanguageLayout.TEXT_WIDTH) engine.block.setHeight(textBlock, value = element.height) engine.block.setTextFontSize(textBlock, fontSize = LanguageLayout.FONT_SIZE) val font = element.typeface.fonts.firstOrNull { font -> font.weight == element.fontWeight && font.style == element.fontStyle && (element.fontSubFamily == null || font.subFamily == element.fontSubFamily) } ?: error( "Missing ${element.fontWeight}/${element.fontStyle}" + " font variant for ${element.typeface.name}", ) engine.block.setFont( block = textBlock, fontFileUri = font.uri, typeface = element.typeface, ) engine.block.setTextHorizontalAlignment(block = textBlock, alignment = element.alignment) engine.block.appendChild(parent = page, child = textBlock) return textBlock } ``` `HorizontalAlignment.Auto` lets the Engine resolve left or right alignment from the text direction. Use an explicit alignment only when the design requires a fixed visual position. ### Create the Scene Create a page for the multilingual text blocks. The sample uses a simple design scene because this guide teaches text behavior, not editor UI setup. ```kotlin highlight-android-create-scene val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = LanguageLayout.PAGE_WIDTH) engine.block.setHeight(page, value = LanguageLayout.PAGE_HEIGHT) engine.block.appendChild(parent = scene, child = page) ``` ### Apply Fonts Programmatically Build a small list of text elements and create one block per language. The same helper applies the matching typeface and leaves alignment on `Auto` unless a specific block needs an override. ```kotlin highlight-android-apply-fonts val textElements = listOf( LanguageTextElement( text = "RTL Arabic", typeface = typefaces.label, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), LanguageTextElement( text = "هذا مثال.", typeface = typefaces.rtl, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", height = 160F, ), LanguageTextElement( text = "Korean", typeface = typefaces.label, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), LanguageTextElement( text = "이는 한 예입니다.", typeface = typefaces.korean, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), ) val textBlocks = mutableListOf() var currentY = 50F for (element in textElements) { textBlocks.add(createTextBlock(engine, page, element, currentY)) currentY += element.height + LanguageLayout.GAP } ``` Use this pattern for templates that combine labels, right-to-left copy, and other scripts on one page. The text content stays in normal `text/text` properties, so you can still edit, style, or bind it later. ## Working with Right-to-Left Text ### Understand Automatic RTL Detection CE.SDK determines text direction from Unicode bidirectional properties. Strong right-to-left characters, such as Arabic or Hebrew letters, establish RTL flow; Latin words, numbers, spaces, and punctuation are laid out in context. ### Resolve Effective Alignment Use `setTextHorizontalAlignment(...)` with `HorizontalAlignment.Auto` to keep a block direction-aware, then read `getTextEffectiveHorizontalAlignment(...)` when your UI needs the concrete resolved direction. In interactive editor UI, this readback usually happens as the UI state refreshes. For offscreen or headless samples, load the text block resources before reading the effective alignment: ```kotlin highlight-android-rtl-alignment engine.block.setTextHorizontalAlignment( block = rtlBlock, alignment = HorizontalAlignment.Auto, ) engine.block.forceLoadResources(listOf(rtlBlock)) val effectiveRtlAlignment = engine.block.getTextEffectiveHorizontalAlignment(rtlBlock) ``` `getTextEffectiveHorizontalAlignment(...)` returns the alignment the Engine lays the block out with. Only `Auto` is resolved — against the text content — so the getter never returns `Auto`. Every other value, `Justify` included, comes back verbatim. ### Apply a Typeface to a Text Range You can apply a typeface to an entire text block or to a substring. The Android `from` and `to` arguments are UTF-16 code unit offsets, so derive them from the Kotlin string instead of counting user-perceived characters by hand. ```kotlin highlight-android-range-typeface val mixedText = "Campaign: مرحبا" val mixedBlock = createTextBlock( engine = engine, page = page, element = LanguageTextElement( text = mixedText, typeface = typefaces.label, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", ), yPosition = currentY, ) val rtlStart = mixedText.indexOf("مرحبا") engine.block.setTypeface( block = mixedBlock, typeface = typefaces.rtl, from = rtlStart, to = mixedText.length, ) val mixedTypefaces = engine.block.getTypefaces(mixedBlock) ``` This example applies the Latin label typeface to the whole block first, then uses the Arabic substring range to apply the RTL typeface only where it is needed. ### Override Paragraph Alignment Text blocks have a block-level horizontal alignment and optional paragraph-level overrides. Use `getTextParagraphIndices(...)` to discover valid paragraph indices, set an override for one paragraph, and pass `null` to clear it again. ```kotlin highlight-android-paragraph-alignment val paragraphBlock = createTextBlock( engine = engine, page = page, element = LanguageTextElement( text = "English headline\nمرحبا بالعالم", typeface = typefaces.rtl, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", height = 180F, ), yPosition = currentY, ) val paragraphIndices = engine.block.getTextParagraphIndices(paragraphBlock) val rtlParagraphIndex = paragraphIndices.last() engine.block.setTextHorizontalAlignment( block = paragraphBlock, alignment = HorizontalAlignment.Right, paragraphIndex = rtlParagraphIndex, ) val paragraphOverride = engine.block.getTextHorizontalAlignment( block = paragraphBlock, paragraphIndex = rtlParagraphIndex, ) engine.block.setTextHorizontalAlignment( block = paragraphBlock, alignment = null, paragraphIndex = rtlParagraphIndex, ) val clearedParagraphOverride = engine.block.getTextHorizontalAlignment( block = paragraphBlock, paragraphIndex = rtlParagraphIndex, ) ``` Changing the block-level alignment clears paragraph overrides. Reapply any paragraph-specific overrides after you change the whole block. ## Complex Script Support CE.SDK applies complex-script shaping automatically when the selected font contains the required glyphs and layout tables. Arabic contextual forms, mandatory ligatures, diacritical marks, Indic conjuncts, Thai marks, and Korean syllables do not need separate Engine configuration. For reliable output, test each target language with representative production copy. Fonts that cover the Unicode block but lack shaping data can still render ligatures or marks incorrectly. ## Creating Multilingual Design Templates Variables let one design carry different localized strings without rebuilding the scene. Set values through `engine.variable`, then reference the variable key inside the text block with double curly braces. ```kotlin highlight-android-variables engine.variable.set(key = "localized_headline", value = "عنوان الحملة") createTextBlock( engine = engine, page = page, element = LanguageTextElement( text = "{{localized_headline}}", typeface = typefaces.rtl, fontWeight = FontWeight.NORMAL, fontSubFamily = "Regular", height = 120F, ), yPosition = currentY, ) val localizedHeadline = engine.variable.get(key = "localized_headline") ``` When you build templates for multiple markets, leave enough room for text expansion, test both left-to-right and right-to-left strings, and choose typefaces that cover every target script. ## Font Fallback and Character Coverage When text contains characters that the selected typeface cannot render, CE.SDK falls back to other available fonts before showing replacement glyphs. This keeps text visible, but fallback fonts can change visual weight, metrics, or line breaks. To keep multilingual output predictable: - Use fonts with explicit coverage for the scripts in your templates. - Include the required weights and styles for each typeface you expose. - Test representative Arabic, Hebrew, CJK, Indic, and mixed-direction strings. - Keep font URIs reachable from the Android runtime before export or rendering. ## Troubleshooting ### Text Displays as Squares or Question Marks The selected font does not contain glyphs for the text. Use a typeface with the required Unicode coverage and make sure every referenced font URI is reachable from the device. ### RTL Text Renders Left-to-Right Keep block alignment on `Auto` or set it to `Right` for fixed RTL layouts. If mixed content still looks wrong, verify that the string contains strong RTL characters and that the selected font supports the script. ### Ligatures or Diacritics Display Incorrectly Use a font designed for the target script. Complex scripts need OpenType shaping tables such as GSUB and GPOS in addition to the base character glyphs. ### Variable Placeholders Render Literally Set variables before rendering or exporting the scene, and make sure the placeholder key in the text block matches the key passed to `engine.variable`. ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.create()` | Create the design scene that owns the page. | | `engine.block.create(blockType=_)` | Create text and page blocks. | | `engine.block.appendChild(parent=_, child=_)` | Add a page to the scene or a text block to the page. | | `engine.block.setWidth(block=_, value=_)` | Set block width. | | `engine.block.setHeight(block=_, value=_)` | Set block height. | | `engine.block.setPositionX(block=_, value=_)` | Set the text block's horizontal position. | | `engine.block.setPositionY(block=_, value=_)` | Set the text block's vertical position. | | `engine.block.replaceText(block=_, text=_)` | Set text content, including variable placeholders. | | `engine.block.getString(block=_, property="text/text")` | Read text content from a text block. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text size for the block or range. | | `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Apply a concrete font file from a typeface to a text block. | | `engine.block.setTypeface(block=_, typeface=_, from=_, to=_)` | Apply a typeface to the whole text block or a UTF-16 range. | | `engine.block.getTypefaces(block=_, from=_, to=_)` | Read the typefaces used by the whole block or range. | | `engine.block.forceLoadResources(blocks=_)` | Load referenced font resources before deterministic offscreen readback. | | `engine.block.setTextHorizontalAlignment(block=_, alignment=_, paragraphIndex=_)` | Set block-level or paragraph-level horizontal alignment; pass `null` to clear a paragraph override. | | `engine.block.getTextHorizontalAlignment(block=_, paragraphIndex=_)` | Read the block-level value or a paragraph override. | | `engine.block.getTextEffectiveHorizontalAlignment(block=_)` | Read the alignment used for layout: explicit values, including `Justify`, come back unchanged; `Auto` resolves to `Left` or `Right` from the text direction. | | `engine.block.getTextParagraphIndices(block=_, from=_, to=_)` | Find paragraph indices that overlap a text range. | | `engine.variable.set(key=_, value=_)` | Set a text variable value. | | `engine.variable.get(key=_)` | Read a text variable value. | ## Next Steps - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) - Load and manage custom fonts to match brand guidelines or user preferences. - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [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. - [Edit Text](https://img.ly/docs/cesdk/android/text/edit-c5106b/) - Edit text content directly on the canvas or through the properties panel. --- ## 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, style, and customize text layers in your design using CE.SDK’s flexible text editing tools." platform: android url: "https://img.ly/docs/cesdk/android/text/overview-0bd620/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Overview](https://img.ly/docs/cesdk/android/text/overview-0bd620/) --- Add editable text blocks to Android designs, then style, resize, animate, and update them through the CE.SDK editor UI or CreativeEngine APIs. In CreativeEditor SDK (CE.SDK), text is an editable [design block](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) that carries text content, layout, styling, and effects. Use the editor UI when users should write and adjust copy themselves, or use CreativeEngine APIs when your app needs to create templates, localize scenes, or update text from data. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Text Capabilities in CE.SDK Text blocks can be created, edited, selected, moved, resized, and layered with the rest of a scene. The editor UI supports inline editing for user-driven workflows, while CreativeEngine lets integrations update text content, geometry, styling, and animation programmatically. Use text blocks for titles, labels, captions, localized marketing copy, template placeholders, and data-driven layouts. The same block model works across image and video scenes, so text can be part of static designs or time-based compositions. ## Styling, Formatting, Effects, and Spacing CE.SDK separates text content from its visual treatment. You can style complete text blocks or ranges with typefaces, font sizes, colors, backgrounds, font weights, styles, and case transformations. Text also supports decoration and layout controls: - Use [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) for colors, backgrounds, typefaces, and range formatting. - Use [Text Decorations](https://img.ly/docs/cesdk/android/text/decorations-d3c0a1/) for underline, strikethrough, and overline styles. - Use [Adjust Text Spacing](https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/) for letter spacing, line height, and paragraph spacing. Effects such as strokes, shadows, and glows help text stand out from busy designs. Choose them when contrast or emphasis matters, and keep the effect values consistent with the surrounding template style. ## Text Containers and Dynamic Content Text frames can be fixed when a template needs strict boundaries, or configured to adapt when the content length changes. Dynamic sizing is useful for localized strings, user-generated captions, and placeholders populated from external data. When content can vary significantly, plan the text frame, wrapping behavior, and minimum readable font size together. This keeps longer copy from clipping and helps short copy remain visually balanced. ## Emojis, Fonts, and Language Support Text blocks can include Unicode emoji characters alongside regular text. CE.SDK renders emojis with a configurable emoji font, so Android integrations can keep emoji appearance consistent across devices when needed. See [Emojis](https://img.ly/docs/cesdk/android/text/emojis-510651/) for the Android-specific setup. Typefaces are managed through the asset system, which lets integrations provide brand fonts or curated font collections. For multilingual designs, use fonts with the character coverage your content requires; CE.SDK handles Unicode text, mixed scripts, right-to-left text, and font fallback when suitable fonts are available. ## UI and API Workflows Use the CE.SDK editor UI when people need direct control over copy, typography, and placement. This is the right fit for creative workflows where text is part of an interactive editing session. Use CreativeEngine APIs when your app owns the content pipeline. Programmatic text editing is useful for automation, template personalization, batch generation, localization, and server- or app-driven scene updates. ## Next Steps - [Edit Text](https://img.ly/docs/cesdk/android/text/edit-c5106b/) - Update text content directly on the canvas or from your app. - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, backgrounds, and range formatting. - [Adjust Text Spacing](https://img.ly/docs/cesdk/android/text/adjust-spacing-c1a3b6/) - Control letter spacing, line height, and paragraph spacing. - [Emojis](https://img.ly/docs/cesdk/android/text/emojis-510651/) - Insert emoji characters and configure emoji 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: "Text Styling" description: "Apply fonts, colors, alignment, and other styling options to customize text appearance." platform: android url: "https://img.ly/docs/cesdk/android/text/styling-269c48/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-properties/TextProperties.kt reference-only import android.net.Uri import ly.img.engine.AnimationType import ly.img.engine.Color import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.SizeMode import ly.img.engine.TextCase import ly.img.engine.Typeface suspend fun textProperties(engine: Engine) { val scene = engine.scene.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(text, mode = SizeMode.ABSOLUTE) engine.block.setWidth(text, value = 520F) engine.block.setHeight(text, value = 220F) // Use a large font size so the styling changes are clearly visible. engine.block.setTextFontSize(text, fontSize = 160F) engine.block.replaceText(text, text = "Hello World") // Insert a "!" at the UTF-16 offset after "Hello World". engine.block.replaceText(text, text = "!", from = 11) // Replace "World" with "Alex". engine.block.replaceText(text, text = "Alex", from = 6, to = 11) // Remove "Hello ". engine.block.removeText(text, from = 0, to = 6) engine.scene.zoomToBlock( block = text, paddingLeft = 100F, paddingTop = 100F, paddingRight = 100F, paddingBottom = 100F, ) val yellow = Color.fromRGBA(r = 255, g = 230, b = 0) val black = Color.fromRGBA(r = 0, g = 0, b = 0) engine.block.setTextColor(text, color = yellow) engine.block.setTextColor(text, color = black, from = 1, to = 4) val allColors = engine.block.getTextColors(text) val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5) engine.block.setTextBackgroundColor(text, color = yellow, from = 1, to = 4) // A fully transparent color removes the background again: // engine.block.setTextBackgroundColor(text, color = Color.fromRGBA(r = 0, g = 0, b = 0, a = 0), from = 1, to = 4) val allBackgroundColors = engine.block.getTextBackgroundColors(text) val backgroundColorsInRange = engine.block.getTextBackgroundColors(text, from = 2, to = 5) // Grow the background around the text and round its corners engine.block.setFloat(text, "text/backgroundPadding/left", 4F) engine.block.setFloat(text, "text/backgroundPadding/right", 4F) engine.block.setFloat(text, "text/backgroundPadding/top", 2F) engine.block.setFloat(text, "text/backgroundPadding/bottom", 2F) engine.block.setFloat(text, "text/backgroundCornerRadius", 4F) if (engine.block.supportsBackgroundColor(text)) { engine.block.setBackgroundColorEnabled(text, enabled = true) val backgroundColorEnabled = engine.block.isBackgroundColorEnabled(text) val backgroundColor = engine.block.getBackgroundColor(text) engine.block.setBackgroundColor(text, color = Color.fromRGBA(r = 242, g = 242, b = 242)) engine.block.setFloat(text, property = "backgroundColor/paddingLeft", value = 10F) engine.block.setFloat(text, property = "backgroundColor/paddingRight", value = 10F) engine.block.setFloat(text, property = "backgroundColor/paddingTop", value = 8F) engine.block.setFloat(text, property = "backgroundColor/paddingBottom", value = 8F) engine.block.setFloat(text, property = "backgroundColor/cornerRadius", value = 8F) } val animation = engine.block.createAnimation(AnimationType.Slide) engine.block.setEnum(animation, property = "textAnimationWritingStyle", value = "Block") engine.block.setInAnimation(text, animation = animation) engine.block.setOutAnimation(text, animation = animation) engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) val textCases = engine.block.getTextCases(text) val bundledAssetsBaseUri = "file:///android_asset/imgly-assets" val typeface = Typeface( name = "Fira 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, ), Font( uri = Uri.parse( "$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Italic.ttf", ), subFamily = "Italic", weight = FontWeight.NORMAL, style = FontStyle.ITALIC, ), Font( uri = Uri.parse( "$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-BoldItalic.ttf", ), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), ), ) val regularFont = typeface.fonts.firstOrNull { it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL } ?: error("Typeface must include a normal regular font.") engine.block.setFont(text, fontFileUri = regularFont.uri, typeface = typeface) engine.block.setTypeface(text, typeface = typeface, from = 1, to = 4) val currentDefaultTypeface = engine.block.getTypeface(text) val currentTypefaces = engine.block.getTypefaces(text) val currentTypefacesOfRange = engine.block.getTypefaces(text, from = 1, to = 4) if (engine.block.canToggleBoldFont(text)) { engine.block.toggleBoldFont(text) } if (engine.block.canToggleItalicFont(text, from = 1, to = 4)) { engine.block.toggleItalicFont(text, from = 1, to = 4) } engine.block.setTextFontWeight(text, fontWeight = FontWeight.BOLD) val fontWeights = engine.block.getTextFontWeights(text) engine.block.setTextFontStyle(text, fontStyle = FontStyle.ITALIC) val fontStyles = engine.block.getTextFontStyles(text) } ``` Style text blocks programmatically with colors, backgrounds, typefaces, and formatting. ![Text styling preview showing yellow and black text on a rounded light background](./assets/android.hero.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-properties) CE.SDK exposes text styling through the Block API. We can edit text content, apply colors and backgrounds, manage typefaces, and control case, weight, and style for a whole text block or for UTF-16 text ranges. ## Editing Text Content Use `engine.block.replaceText()` and `engine.block.removeText()` to replace, insert, or remove text. The optional `from` and `to` parameters are UTF-16 code unit offsets in the half-open range `[from, to)`. ```kotlin highlight-android-edit-text engine.block.replaceText(text, text = "Hello World") // Insert a "!" at the UTF-16 offset after "Hello World". engine.block.replaceText(text, text = "!", from = 11) // Replace "World" with "Alex". engine.block.replaceText(text, text = "Alex", from = 6, to = 11) // Remove "Hello ". engine.block.removeText(text, from = 0, to = 6) ``` When both range arguments are omitted, `replaceText()` replaces the entire string. Passing only `from` inserts at that offset, while passing both `from` and `to` replaces the selected range. ## Text Colors Use `engine.block.setTextColor()` to color the whole text block or a specific range. `engine.block.getTextColors()` returns the unique colors in the order they appear in the requested text range. ```kotlin highlight-android-text-colors val yellow = Color.fromRGBA(r = 255, g = 230, b = 0) val black = Color.fromRGBA(r = 0, g = 0, b = 0) engine.block.setTextColor(text, color = yellow) engine.block.setTextColor(text, color = black, from = 1, to = 4) val allColors = engine.block.getTextColors(text) val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5) ``` The example first applies yellow to the complete string, then changes the middle range to black. Querying the range from `2` to `5` returns black first because that color appears first inside the requested range. ## Text Run Backgrounds Use `engine.block.setTextBackgroundColor()` to draw a background color behind the whole text block or a specific range. `engine.block.getTextBackgroundColors()` returns the unique background colors in the order they appear in the requested text range. Text without a background color is reported as a fully transparent color. ```kotlin highlight-android-text-run-background engine.block.setTextBackgroundColor(text, color = yellow, from = 1, to = 4) // A fully transparent color removes the background again: // engine.block.setTextBackgroundColor(text, color = Color.fromRGBA(r = 0, g = 0, b = 0, a = 0), from = 1, to = 4) val allBackgroundColors = engine.block.getTextBackgroundColors(text) val backgroundColorsInRange = engine.block.getTextBackgroundColors(text, from = 2, to = 5) // Grow the background around the text and round its corners engine.block.setFloat(text, "text/backgroundPadding/left", 4F) engine.block.setFloat(text, "text/backgroundPadding/right", 4F) engine.block.setFloat(text, "text/backgroundPadding/top", 2F) engine.block.setFloat(text, "text/backgroundPadding/bottom", 2F) engine.block.setFloat(text, "text/backgroundCornerRadius", 4F) ``` A fully transparent color removes the background from a range. The run background is independent of the block-level background color and is drawn on top of the block-level background. The `text/backgroundPadding/*` properties grow the background around the text. The `text/backgroundCornerRadius` property rounds its corners. Both apply to every text run of the block. Caption blocks get the same properties under `caption/`, and a caption track keeps them in sync. ## Text Backgrounds Use the typed background color APIs to check support, enable the background, and set or read its color. Padding and corner radius still use block property access because Android does not expose typed helpers for those text background properties. ```kotlin highlight-android-text-background if (engine.block.supportsBackgroundColor(text)) { engine.block.setBackgroundColorEnabled(text, enabled = true) val backgroundColorEnabled = engine.block.isBackgroundColorEnabled(text) val backgroundColor = engine.block.getBackgroundColor(text) engine.block.setBackgroundColor(text, color = Color.fromRGBA(r = 242, g = 242, b = 242)) engine.block.setFloat(text, property = "backgroundColor/paddingLeft", value = 10F) engine.block.setFloat(text, property = "backgroundColor/paddingRight", value = 10F) engine.block.setFloat(text, property = "backgroundColor/paddingTop", value = 8F) engine.block.setFloat(text, property = "backgroundColor/paddingBottom", value = 8F) engine.block.setFloat(text, property = "backgroundColor/cornerRadius", value = 8F) } ``` Text backgrounds inherit animations from their text block when the animation writing style is set to `Block`. ```kotlin highlight-android-background-animation val animation = engine.block.createAnimation(AnimationType.Slide) engine.block.setEnum(animation, property = "textAnimationWritingStyle", value = "Block") engine.block.setInAnimation(text, animation = animation) engine.block.setOutAnimation(text, animation = animation) ``` ## Text Case Transformations Use `engine.block.setTextCase()` to change how text renders without changing the stored string value. Available values are `TextCase.NORMAL`, `TextCase.UPPER_CASE`, `TextCase.LOWER_CASE`, and `TextCase.TITLE_CASE`. ```kotlin highlight-android-text-case engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) val textCases = engine.block.getTextCases(text) ``` `engine.block.getTextCases()` returns the ordered list of text case transformations in the requested range. ## Typefaces and Fonts Use `engine.block.setFont()` when you want to change the font and reset existing text formatting. Use `engine.block.setTypeface()` when you want to change the typeface while preserving existing formatting as far as the new typeface supports it. The sample expects the Fira Sans font files to be bundled under the app asset path `assets/imgly-assets/...`. Bundle your own fonts at a matching app asset path, or replace the URIs with font URIs your app can resolve. ```kotlin highlight-android-typeface val bundledAssetsBaseUri = "file:///android_asset/imgly-assets" val typeface = Typeface( name = "Fira 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, ), Font( uri = Uri.parse( "$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Italic.ttf", ), subFamily = "Italic", weight = FontWeight.NORMAL, style = FontStyle.ITALIC, ), Font( uri = Uri.parse( "$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-BoldItalic.ttf", ), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), ), ) val regularFont = typeface.fonts.firstOrNull { it.weight == FontWeight.NORMAL && it.style == FontStyle.NORMAL } ?: error("Typeface must include a normal regular font.") engine.block.setFont(text, fontFileUri = regularFont.uri, typeface = typeface) engine.block.setTypeface(text, typeface = typeface, from = 1, to = 4) val currentDefaultTypeface = engine.block.getTypeface(text) val currentTypefaces = engine.block.getTypefaces(text) val currentTypefacesOfRange = engine.block.getTypefaces(text, from = 1, to = 4) ``` A `Typeface` contains the typeface name and the available font variants. Select variants by metadata such as `weight` and `style` so the code does not depend on font list order. ## Font Weights and Styles Use `engine.block.canToggleBoldFont()` and `engine.block.canToggleItalicFont()` before calling the corresponding toggle methods. The typeface must include the requested weight and style combination. ```kotlin highlight-android-font-styles if (engine.block.canToggleBoldFont(text)) { engine.block.toggleBoldFont(text) } if (engine.block.canToggleItalicFont(text, from = 1, to = 4)) { engine.block.toggleItalicFont(text, from = 1, to = 4) } engine.block.setTextFontWeight(text, fontWeight = FontWeight.BOLD) val fontWeights = engine.block.getTextFontWeights(text) engine.block.setTextFontStyle(text, fontStyle = FontStyle.ITALIC) val fontStyles = engine.block.getTextFontStyles(text) ``` For direct formatting, `engine.block.setTextFontWeight()` and `engine.block.setTextFontStyle()` apply weight or style to the requested text range. Use the corresponding getters to inspect the active values. ## API Reference | Method | Purpose | |--------|---------| | `engine.block.replaceText(block=_, text=_, from=_, to=_)` | Replace or insert text at UTF-16 offsets | | `engine.block.removeText(block=_, from=_, to=_)` | Remove text at UTF-16 offsets | | `engine.block.setTextColor(block=_, color=_, from=_, to=_)` | Set the text color for all text or a range | | `engine.block.getTextColors(block=_, from=_, to=_)` | Get ordered unique text colors for a range | | `engine.block.setTextBackgroundColor(block=_, color=_, from=_, to=_)` | Set the text background color for all text or a range | | `engine.block.getTextBackgroundColors(block=_, from=_, to=_)` | Get ordered unique text background colors for a range | | `engine.block.supportsBackgroundColor(block=_)` | Check whether a block supports background color properties | | `engine.block.setBackgroundColorEnabled(block=_, enabled=_)` | Enable or disable the text background | | `engine.block.isBackgroundColorEnabled(block=_)` | Check whether the text background is enabled | | `engine.block.getBackgroundColor(block=_)` | Read the text background color | | `engine.block.setBackgroundColor(block=_, color=_)` | Set the text background color | | `engine.block.setFloat(block=_, property="backgroundColor/paddingLeft", value=_)` | Set one background padding side | | `engine.block.setFloat(block=_, property="backgroundColor/paddingRight", value=_)` | Set one background padding side | | `engine.block.setFloat(block=_, property="backgroundColor/paddingTop", value=_)` | Set one background padding side | | `engine.block.setFloat(block=_, property="backgroundColor/paddingBottom", value=_)` | Set one background padding side | | `engine.block.setFloat(block=_, property="backgroundColor/cornerRadius", value=_)` | Set the background corner radius | | `engine.block.createAnimation(type=_)` | Create an animation block | | `engine.block.setEnum(block=_, property="textAnimationWritingStyle", value="Block")` | Make text background animation follow the text block | | `engine.block.setInAnimation(block=_, animation=_)` | Assign an in-animation to the text block | | `engine.block.setOutAnimation(block=_, animation=_)` | Assign an out-animation to the text block | | `engine.block.setTextCase(block=_, textCase=_, from=_, to=_)` | Apply a text case transformation | | `engine.block.getTextCases(block=_, from=_, to=_)` | Get ordered text case transformations for a range | | `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Change the font and reset existing formatting | | `engine.block.setTypeface(block=_, typeface=_, from=_, to=_)` | Change the typeface while preserving formatting | | `engine.block.getTypeface(block=_)` | Get the text block's default typeface | | `engine.block.getTypefaces(block=_, from=_, to=_)` | Get ordered unique typefaces for a range | | `engine.block.canToggleBoldFont(block=_, from=_, to=_)` | Check whether bold can be toggled | | `engine.block.toggleBoldFont(block=_, from=_, to=_)` | Toggle normal and bold font weight | | `engine.block.canToggleItalicFont(block=_, from=_, to=_)` | Check whether italic can be toggled | | `engine.block.toggleItalicFont(block=_, from=_, to=_)` | Toggle normal and italic font style | | `engine.block.setTextFontWeight(block=_, fontWeight=_, from=_, to=_)` | Set font weight for all text or a range | | `engine.block.getTextFontWeights(block=_, from=_, to=_)` | Get ordered unique font weights for a range | | `engine.block.setTextFontStyle(block=_, fontStyle=_, from=_, to=_)` | Set font style for all text or a range | | `engine.block.getTextFontStyles(block=_, from=_, to=_)` | Get ordered unique font styles for a range | ## Troubleshooting **`getTypeface()` throws an error**: New text blocks do not have an explicit typeface until you call `setFont()`. **Font toggles do nothing**: Check that the active typeface contains the weight and style variant you want to toggle to. **The text background is not visible**: Make sure `setBackgroundColorEnabled()` is called with `enabled = true`. This switch belongs to the block background. A run background needs no switch, and text on a path hides only the block background. **Text case looks different from the string value**: Text case transformations affect rendering only. They do not rewrite the text string. **Formatting resets after changing fonts**: Use `setTypeface()` instead of `setFont()` when you want to preserve existing formatting. --- ## 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 Designs" description: "Create and customize text component libraries using predefined text designs that appear in your asset library." platform: android url: "https://img.ly/docs/cesdk/android/text/text-designs-a1b2c3/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Text Designs](https://img.ly/docs/cesdk/android/text/text-designs-a1b2c3/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-text-designs/TextDesigns.kt reference-only import android.net.Uri import kotlinx.coroutines.delay import kotlinx.coroutines.yield import ly.img.engine.AssetDefinition 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.FindAssetsQuery import ly.img.engine.MimeType import ly.img.engine.SizeMode import java.io.File import java.nio.ByteBuffer data class TextDesignsResult( val componentBlock: DesignBlock, val insertedBlock: DesignBlock, val sourceId: String, val assetCount: Int, val contentJson: String, val archiveByteCount: Int, val thumbnailByteCount: Int, val previewPng: ByteBuffer, ) suspend fun textDesigns(engine: Engine): TextDesignsResult { val sourceId = "ly.img.text.components" return 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 previewFill = engine.block.createFill(FillType.Color) engine.block.setFill(page, fill = previewFill) engine.block.setFillSolidColor(block = page, color = Color.fromHex("#FFF1F5F9")) val textDesign = engine.block.create(DesignBlockType.Text) engine.block.replaceText(textDesign, text = "Launch Day") engine.block.setTextFontSize(textDesign, fontSize = 96F) engine.block.setTextColor(textDesign, color = Color.fromRGBA(r = 0.14F, g = 0.2F, b = 0.28F, a = 1F)) engine.block.setPositionX(textDesign, value = 60F) engine.block.setPositionY(textDesign, value = 180F) engine.block.setWidthMode(textDesign, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(textDesign, mode = SizeMode.ABSOLUTE) engine.block.setWidth(textDesign, value = 680F) engine.block.setHeight(textDesign, value = 180F) engine.block.setClipped(block = textDesign, clipped = true) engine.block.setBoolean(textDesign, property = "text/clipLinesOutsideOfFrame", value = true) engine.block.setBoolean(textDesign, property = "text/automaticFontSizeEnabled", value = true) engine.block.setFloat(textDesign, property = "text/minAutomaticFontSize", value = 28F) engine.block.setFloat(textDesign, property = "text/maxAutomaticFontSize", value = 96F) engine.block.setScopeEnabled(block = textDesign, key = "text/edit", enabled = true) engine.block.setScopeEnabled(block = textDesign, key = "layer/resize", enabled = true) engine.block.setScopeEnabled(block = textDesign, key = "layer/rotate", enabled = false) engine.block.appendChild(parent = page, child = textDesign) engine.block.forceLoadResources(blocks = listOf(textDesign)) waitForScheduledEngineUpdate() engine.block.forceLoadResources(blocks = listOf(textDesign)) val archiveBuffer = engine.block.saveToArchive(blocks = listOf(textDesign)) val archiveByteCount = archiveBuffer.remaining() val archiveFile = File.createTempFile("brand-title", ".zip").apply { outputStream().channel.use { channel -> val archiveBytes = archiveBuffer.asReadOnlyBuffer() while (archiveBytes.hasRemaining()) { channel.write(archiveBytes) } } } val thumbnailBuffer = engine.block.export( block = textDesign, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 400F, targetHeight = 320F), ) val thumbnailByteCount = thumbnailBuffer.remaining() val thumbnailFile = File.createTempFile("brand-title-thumbnail", ".png").apply { outputStream().channel.use { channel -> val thumbnailBytes = thumbnailBuffer.asReadOnlyBuffer() while (thumbnailBytes.hasRemaining()) { channel.write(thumbnailBytes) } } } val contentJson = """ { "version": "5.0.0", "id": "$sourceId", "assets": [ { "id": "$sourceId.brand-title", "label": { "en": "Brand Title" }, "meta": { "uri": "{{base_url}}/data/brand-title/blocks.blocks", "thumbUri": "{{base_url}}/thumbnails/brand-title.png", "mimeType": "application/ubq-blocks-string" } } ] } """.trimIndent() if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = emptyList(), applyAsset = { asset -> val archiveUri = asset.meta?.get("uri")?.let(Uri::parse) ?: error("Text design asset ${asset.id} is missing meta.uri.") val loadedBlock = engine.block.loadFromArchive(archiveUri).first() val currentPage = engine.scene.getCurrentPage() ?: error("Text design assets can only be inserted when the scene has a current page.") engine.block.appendChild(parent = currentPage, child = loadedBlock) loadedBlock }, ) engine.asset.addAsset( sourceId = sourceId, asset = AssetDefinition( id = "$sourceId.brand-title", label = mapOf("en" to "Brand Title"), meta = mapOf( "uri" to Uri.fromFile(archiveFile).toString(), "thumbUri" to Uri.fromFile(thumbnailFile).toString(), "mimeType" to "application/ubq-blocks-string", ), ), ) engine.asset.assetSourceContentsChanged(sourceId = sourceId) val queryResult = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val textDesignAsset = queryResult.assets.first() val insertedBlock = engine.asset.applyAssetSourceAsset( sourceId = sourceId, asset = textDesignAsset, ) ?: error("Applying the text design should insert a block.") engine.block.setPositionY(insertedBlock, value = 390F) engine.block.forceLoadResources(blocks = listOf(page, textDesign, insertedBlock)) val previewPng = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) TextDesignsResult( componentBlock = textDesign, insertedBlock = insertedBlock, sourceId = sourceId, assetCount = queryResult.total, contentJson = contentJson, archiveByteCount = archiveByteCount, thumbnailByteCount = thumbnailByteCount, previewPng = previewPng, ) } finally { if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } } } suspend fun registerHostedTextDesigns( engine: Engine, sourceId: String, contentJson: String, basePath: String, ): String { if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } val registeredSourceId = engine.asset.addLocalSourceFromJSON(contentJSON = contentJson, basePath = basePath) return registeredSourceId } private suspend fun waitForScheduledEngineUpdate() { // Offscreen text layout is resolved on scheduled engine frames before export. repeat(3) { yield() delay(16) } } ``` Create and customize text designs that users can insert from the CE.SDK asset library. ![Android text design example showing a fixed text frame with automatic font sizing](./assets/text-designs-android.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-text-designs) Text designs, also known as text components, are serialized text blocks stored in an asset source. Each asset entry points to a `blocks.blocks` file and a thumbnail so the editor can show the component and insert it into the current scene. > **Note:** The [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) includes the CE.SDK editor UI that displays text design assets. This guide focuses on the Engine asset source behind that UI. ## Content.json Structure Text design libraries use a `content.json` file with asset metadata. Keep the source ID as `ly.img.text.components` when the assets should appear in the default text components section of the CE.SDK editor UI. | Property | Purpose | | --- | --- | | `version` | Asset source format version. Use `5.0.0` for current text design sources. | | `id` | Asset source ID. Use `ly.img.text.components` for the built-in text components section. | | `assets[].id` | Unique asset ID, usually namespaced under the source ID. | | `assets[].label` | Localized asset label shown in the library. | | `assets[].meta.uri` | URI of the extracted `blocks.blocks` file. | | `assets[].meta.thumbUri` | URI of the 400x320 PNG thumbnail. | | `assets[].meta.mimeType` | MIME type for text components: `application/ubq-blocks-string`. | ## Create a Text Design Create a text block with a fixed frame before you serialize it as a reusable text design. The sample keeps the frame absolute, clips overflow, enables automatic font sizing, and sets min/max font-size bounds so edited text stays inside the intended layout. ```kotlin highlight-android-create-text-design val textDesign = engine.block.create(DesignBlockType.Text) engine.block.replaceText(textDesign, text = "Launch Day") engine.block.setTextFontSize(textDesign, fontSize = 96F) engine.block.setTextColor(textDesign, color = Color.fromRGBA(r = 0.14F, g = 0.2F, b = 0.28F, a = 1F)) engine.block.setPositionX(textDesign, value = 60F) engine.block.setPositionY(textDesign, value = 180F) engine.block.setWidthMode(textDesign, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(textDesign, mode = SizeMode.ABSOLUTE) engine.block.setWidth(textDesign, value = 680F) engine.block.setHeight(textDesign, value = 180F) engine.block.setClipped(block = textDesign, clipped = true) engine.block.setBoolean(textDesign, property = "text/clipLinesOutsideOfFrame", value = true) engine.block.setBoolean(textDesign, property = "text/automaticFontSizeEnabled", value = true) engine.block.setFloat(textDesign, property = "text/minAutomaticFontSize", value = 28F) engine.block.setFloat(textDesign, property = "text/maxAutomaticFontSize", value = 96F) engine.block.setScopeEnabled(block = textDesign, key = "text/edit", enabled = true) engine.block.setScopeEnabled(block = textDesign, key = "layer/resize", enabled = true) engine.block.setScopeEnabled(block = textDesign, key = "layer/rotate", enabled = false) engine.block.appendChild(parent = page, child = textDesign) ``` The base font size remains part of the serialized design, while automatic font sizing lets CE.SDK scale the rendered text between the configured bounds when the text changes. Edit and transform scopes control what users can change after insertion. ## Serialize the Design Use `engine.block.saveToArchive()` for new text design libraries. The archive bundles the `blocks.blocks` file with referenced resources such as fonts and images. ```kotlin highlight-android-save-archive engine.block.forceLoadResources(blocks = listOf(textDesign)) val archiveBuffer = engine.block.saveToArchive(blocks = listOf(textDesign)) val archiveByteCount = archiveBuffer.remaining() val archiveFile = File.createTempFile("brand-title", ".zip").apply { outputStream().channel.use { channel -> val archiveBytes = archiveBuffer.asReadOnlyBuffer() while (archiveBytes.hasRemaining()) { channel.write(archiveBytes) } } } ``` Extract the archive before deployment and host the complete extracted directory. Android can also load the zip archive directly when you provide a custom `applyAsset` callback, as shown below. ### Legacy: Using saveToString() `engine.block.saveToString()` is still available for older integrations, but it does not bundle resources. Use it only when every referenced resource already has a stable URI that remains accessible wherever the text design is loaded. ## Generate a Thumbnail Export a 400x320 PNG thumbnail for each text design. The asset library uses the thumbnail to show users what the component looks like before they insert it. ```kotlin highlight-android-export-thumbnail val thumbnailBuffer = engine.block.export( block = textDesign, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 400F, targetHeight = 320F), ) val thumbnailByteCount = thumbnailBuffer.remaining() val thumbnailFile = File.createTempFile("brand-title-thumbnail", ".png").apply { outputStream().channel.use { channel -> val thumbnailBytes = thumbnailBuffer.asReadOnlyBuffer() while (thumbnailBytes.hasRemaining()) { channel.write(thumbnailBytes) } } } ``` Store thumbnails next to your extracted text design files so both `uri` and `thumbUri` can use the same base URL. ## Create the content.json File Add one asset entry per text design. The `uri` and `thumbUri` values can use the `{{base_url}}` placeholder. Pass an explicit `basePath` when registering the source so Android resolves the placeholder to the directory that contains `content.json`. ```kotlin highlight-android-create-content-json val contentJson = """ { "version": "5.0.0", "id": "$sourceId", "assets": [ { "id": "$sourceId.brand-title", "label": { "en": "Brand Title" }, "meta": { "uri": "{{base_url}}/data/brand-title/blocks.blocks", "thumbUri": "{{base_url}}/thumbnails/brand-title.png", "mimeType": "application/ubq-blocks-string" } } ] } """.trimIndent() ``` For production hosting, keep this structure: ```text /ly.img.text.components/ |-- content.json |-- data/ | `-- brand-title/ | `-- blocks.blocks `-- thumbnails/ `-- brand-title.png ``` If the archive contains fonts or images, extract and host those subdirectories beside `blocks.blocks`. ## Register the Asset Source For app-local sources or test fixtures, register a local source and provide a custom `applyAsset` callback. The snippet replaces the default text component source before registering it, then loads the saved archive and appends the inserted block to the current page. ```kotlin highlight-android-register-local-source if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = emptyList(), applyAsset = { asset -> val archiveUri = asset.meta?.get("uri")?.let(Uri::parse) ?: error("Text design asset ${asset.id} is missing meta.uri.") val loadedBlock = engine.block.loadFromArchive(archiveUri).first() val currentPage = engine.scene.getCurrentPage() ?: error("Text design assets can only be inserted when the scene has a current page.") engine.block.appendChild(parent = currentPage, child = loadedBlock) loadedBlock }, ) engine.asset.addAsset( sourceId = sourceId, asset = AssetDefinition( id = "$sourceId.brand-title", label = mapOf("en" to "Brand Title"), meta = mapOf( "uri" to Uri.fromFile(archiveFile).toString(), "thumbUri" to Uri.fromFile(thumbnailFile).toString(), "mimeType" to "application/ubq-blocks-string", ), ), ) engine.asset.assetSourceContentsChanged(sourceId = sourceId) val queryResult = engine.asset.findAssets( sourceId = sourceId, query = FindAssetsQuery(page = 0, perPage = 10), ) val textDesignAsset = queryResult.assets.first() val insertedBlock = engine.asset.applyAssetSourceAsset( sourceId = sourceId, asset = textDesignAsset, ) ?: error("Applying the text design should insert a block.") engine.block.setPositionY(insertedBlock, value = 390F) ``` After adding assets, call `engine.asset.assetSourceContentsChanged()` so the asset library and queries see the new entries. ## Host the Source for Production After uploading the extracted files and `content.json`, load the JSON and register the hosted source with an explicit base path. The snippet removes an existing `ly.img.text.components` source first, then registers the replacement source. Use the directory that contains `content.json` as `basePath`, and keep asset paths in the JSON relative to that directory. ```kotlin highlight-android-register-hosted-source if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } val registeredSourceId = engine.asset.addLocalSourceFromJSON(contentJSON = contentJson, basePath = basePath) ``` When this source uses the `ly.img.text.components` ID, the CE.SDK editor UI can display its assets in the text components section. ## Troubleshooting **Components do not appear**: Check that the source ID is `ly.img.text.components`, the source is registered, and `mimeType` is `application/ubq-blocks-string`. **The component fails to load**: Verify that `meta.uri` points to the extracted `blocks.blocks` file, or use `loadFromArchive()` when your callback receives a zip archive. **Thumbnails do not load**: Confirm that `thumbUri` resolves to a 400x320 PNG and that your server allows the app to fetch it. **Fonts are missing after insertion**: Prefer `saveToArchive()` and host every extracted font file next to the extracted `blocks.blocks` file. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.create(blockType=_)` | Create the text block that becomes the reusable design. | | `engine.block.replaceText(block=_, text=_)` | Set the text content. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the base font size. | | `engine.block.setTextColor(block=_, color=_)` | Set the text color. | | `engine.block.setPositionX(block=_, value=_)` | Set the block's x position. | | `engine.block.setPositionY(block=_, value=_)` | Set the block's y position. | | `engine.block.setWidthMode(block=_, mode=_)` | Set the block width mode. | | `engine.block.setHeightMode(block=_, mode=_)` | Set the block height mode. | | `engine.block.setWidth(block=_, value=_)` | Set the fixed text frame width. | | `engine.block.setHeight(block=_, value=_)` | Set the fixed text frame height. | | `engine.block.setClipped(block=_, clipped=_)` | Clip rendered contents to the block frame. | | `engine.block.setBoolean(block=_, property="text/clipLinesOutsideOfFrame", value=_)` | Hide overflowing text lines outside the fixed frame. | | `engine.block.setBoolean(block=_, property="text/automaticFontSizeEnabled", value=_)` | Enable automatic font sizing for a bounded text frame. | | `engine.block.setFloat(block=_, property="text/minAutomaticFontSize", value=_)` | Set the minimum automatic font size. | | `engine.block.setFloat(block=_, property="text/maxAutomaticFontSize", value=_)` | Set the maximum automatic font size. | | `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Configure block-level editing scopes. | | `engine.block.forceLoadResources(blocks=_)` | Load resources before serialization. | | `engine.block.saveToArchive(blocks=_)` | Save text design blocks and resources to an archive. | | `engine.block.saveToString(blocks=_, allowedResourceSchemes=_)` | Legacy string serialization without bundled resources. | | `engine.block.export(block=_, mimeType=_, options=_)` | Export the text design thumbnail. | | `engine.block.loadFromArchive(archiveUri=_)` | Load text design blocks from a saved archive. | | `engine.scene.getCurrentPage()` | Read the current page before appending a loaded text design. | | `engine.block.appendChild(parent=_, child=_)` | Attach the loaded text design to the current page. | | `engine.asset.findAllSources()` | Check whether the default text component source is already registered. | | `engine.asset.removeSource(sourceId=_)` | Remove an existing source before replacing it with custom text designs. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_, applyAsset=_)` | Register a local source with custom insertion behavior. | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add a text design asset to the local source. | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify the asset system after source content changes. | | `engine.asset.findAssets(sourceId=_, query=_)` | Query text design assets from the source. | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_)` | Apply a selected text design asset to the scene. | | `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_)` | Register a hosted `content.json` source with an explicit base path. | ## Next Steps - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) - Configure CE.SDK to load engine and content assets from your own servers instead of the IMG.LY CDN for production deployments. - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) — Load and manage custom fonts to match brand guidelines or user preferences. - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Auto-Size](https://img.ly/docs/cesdk/android/text/auto-size-5331b3/) - Configure text blocks to automatically adapt their dimensions or font size for dynamic content. --- ## 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 on a Path" description: "Place text along an SVG path — a circle, arch, wave, or any curve — using the built-in Text on Path sheet or the setTextOnPath engine API." platform: android url: "https://img.ly/docs/cesdk/android/text/text-on-path-e3b8a2/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Text on a Path](https://img.ly/docs/cesdk/android/text/text-on-path-e3b8a2/) --- ```kotlin file=@cesdk_android_examples/engine-guides-text-text-on-path/TextOnPath.kt reference-only import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.MimeType import java.nio.ByteBuffer data class TextOnPath( val pathBeforeClear: String?, val verticalAlignment: String, val offset: Float, val isFlipped: Boolean, val pathAfterClear: String?, val curvedPreviewPng: ByteBuffer, ) suspend fun textOnPath(engine: Engine): TextOnPath { // Demo scaffolding: a square page that frames the curved text for the // preview export below. Creating the scene with `DesignUnit.PIXEL` pairs // the font-size unit to Pixel, so the `setTextFontSize` value below // renders at the scale the SVG path's local coordinates assume. // `setTextOnPath` sizes the block to span about 7 font heights of the // curve's larger dimension, so the page needs to be generous relative to // the font size below or the curve renders larger than the page. val scene = engine.scene.create(designUnit = DesignUnit.PIXEL) val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 480F) engine.block.setHeight(page, value = 480F) 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 = "TEXT ON A PATH") engine.block.setTextFontSize(text, fontSize = 48F) // The path must contain a single subpath; the block resizes to match the // path's aspect ratio and word wrapping is disabled. val circlePath = "M 60,119.5 A 59.5,59.5 0 1,1 60.01,119.5 Z" engine.block.setTextOnPath(text, svgPath = circlePath) // Demo scaffolding: setTextOnPath resizes the block to the path's aspect // ratio, so center it on the page now that its size is known. engine.block.setPositionX(text, value = (480F - engine.block.getWidth(text)) / 2F) engine.block.setPositionY(text, value = (480F - engine.block.getHeight(text)) / 2F) engine.block.setEnum(text, property = "text/verticalAlignment", value = "Center") val verticalAlignment = engine.block.getEnum(text, property = "text/verticalAlignment") // The offset is a proportion of the path length, clamped to [-1, 1]. engine.block.setTextOnPathOffset(text, offset = 0.05F) val offset = engine.block.getTextOnPathOffset(text) // Export a preview of the curved state so the smoke test can verify the // rendered result. val curvedPreviewPng = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 1200F, targetHeight = 1200F), ) engine.block.setTextOnPathFlipped(text, flipped = true) val isFlipped = engine.block.getTextOnPathFlipped(text) val pathBeforeClear = engine.block.getTextOnPath(text) // Passing null removes the curve and restores a straight, auto-sized baseline. engine.block.setTextOnPath(text, svgPath = null) val pathAfterClear = engine.block.getTextOnPath(text) check(pathBeforeClear == circlePath) check(verticalAlignment == "Center") check(offset == 0.05F) check(isFlipped) check(pathAfterClear == null) check(curvedPreviewPng.hasRemaining()) { "Curved text preview export is empty." } return TextOnPath( pathBeforeClear = pathBeforeClear, verticalAlignment = verticalAlignment, offset = offset, isFlipped = isFlipped, pathAfterClear = pathAfterClear, curvedPreviewPng = curvedPreviewPng.asReadOnlyBuffer(), ) } ``` Curve a text block so its characters follow an SVG path — an arch, a full circle, or any custom curve — instead of a straight baseline. ![The words TEXT ON A PATH curving along the left arc of a circular path, with vertical alignment Center and a slight offset along the path](./assets/android.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-text-text-on-path) Text on a path makes a text block's baseline follow an SVG curve instead of a straight line — useful for badges, circular seals, and arched headlines. It applies to text blocks only and disables word wrapping while a path is active: explicit line breaks in the text collapse to spaces. Setting a path resizes the block to match the path's aspect ratio. ## Using the Built-in Path UI The CE.SDK editor UI lets users curve text interactively: select a text block and tap **Text on Path** in the inspector bar. The **None**, **Circle**, **Arch**, **Wave**, and **Elevate** presets come from `ly.img.text.curves`, so you can customize the set. Applying one also replaces the horizontal and vertical alignment, direction, and offset with the preset values. The block keeps its text. A preset sets the text only when it makes a new block. The sheet exposes **Path Position**, **Direction**, and **Offset** for both preset and custom SVG paths. The rest of this guide covers the Engine API behind that experience, which applies text on a path programmatically. ## Applying Curved Text Presets from the Asset Library The same **Curved Text** presets also surface as a section of the asset library's Text tab. Applying a preset from the library inserts a new text block with that curve already applied, ready for you to replace its placeholder copy. ## Creating the Text Block We create a text block, give it a short headline, set a font size, and append it to a page. Creating and styling text in depth is covered in [Add Text](https://img.ly/docs/cesdk/android/text/add-4f5011/) and [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/). ```kotlin highlight-android-create-text val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = text) engine.block.replaceText(text, text = "TEXT ON A PATH") engine.block.setTextFontSize(text, fontSize = 48F) ``` The block starts out with a normal, straight baseline. ## Placing Text on the Path We curve the text onto a circle with `engine.block.setTextOnPath()`, passing an SVG path string in the block's local coordinate space. The path must contain exactly one subpath (a single leading `M`); the block resizes to match the path's aspect ratio and word wrapping turns off. ```kotlin highlight-android-place-on-path // The path must contain a single subpath; the block resizes to match the // path's aspect ratio and word wrapping is disabled. val circlePath = "M 60,119.5 A 59.5,59.5 0 1,1 60.01,119.5 Z" engine.block.setTextOnPath(text, svgPath = circlePath) ``` The characters now follow the circle instead of a straight line. ## Positioning the Text Vertically We set where the text sits relative to the path with the `text/verticalAlignment` enum — `Top`, `Center`, or `Bottom` — and read the current value back with `engine.block.getEnum()`. ```kotlin highlight-android-vertical-position engine.block.setEnum(text, property = "text/verticalAlignment", value = "Center") val verticalAlignment = engine.block.getEnum(text, property = "text/verticalAlignment") ``` `Center` runs the baseline through the middle of the glyphs, while `Top` and `Bottom` sit the text on the inner or outer edge of the curve. ## Offsetting Along the Path We slide the text along the path with `engine.block.setTextOnPathOffset()`, a proportional value clamped to `[-1, 1]` and centered at zero, and read it back with `engine.block.getTextOnPathOffset()`. The same value is exposed as the `text/pathOffset` float property. Both ends of the range wrap back to the same position as `0`, so `1` and `-1` don't move the text further than a value just short of them. ```kotlin highlight-android-offset // The offset is a proportion of the path length, clamped to [-1, 1]. engine.block.setTextOnPathOffset(text, offset = 0.05F) val offset = engine.block.getTextOnPathOffset(text) ``` Positive values move the text forward along the path; negative values move it back. ## Flipping the Direction We flip the text to the other side of the curve — reversing its direction — with `engine.block.setTextOnPathFlipped()`, and read whether it's flipped back with `engine.block.getTextOnPathFlipped()`. ```kotlin highlight-android-direction engine.block.setTextOnPathFlipped(text, flipped = true) val isFlipped = engine.block.getTextOnPathFlipped(text) ``` Flipping is what turns the bottom half of a circular badge right-side up. ## Reading and Clearing the Path We read the block's current path with `engine.block.getTextOnPath()`, which returns the SVG string or `null`. To remove the curve and restore a straight, auto-sized baseline, pass `null` to `setTextOnPath()`. ```kotlin highlight-android-read-and-clear val pathBeforeClear = engine.block.getTextOnPath(text) // Passing null removes the curve and restores a straight, auto-sized baseline. engine.block.setTextOnPath(text, svgPath = null) val pathAfterClear = engine.block.getTextOnPath(text) ``` Clearing the path returns the block to a normal, straight text block with automatic sizing. ## Configuring Availability The **Text on Path** button is a standard inspector bar component with the id `ly.img.component.inspectorBar.button.textOnPath`, created by `InspectorBar.Button.rememberTextOnPath`. To hide, reorder, or replace it, customize the inspector bar's item list in your editor configuration — see [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/). By default the button appears when the selected block is a text block and the block's `"text/character"` scope is allowed, so the feature also disappears for text blocks whose editing scope is locked down. Hiding the button doesn't affect the other surfaces that list the same `ly.img.text.curves` presets — the Curved Text section of the asset library's Text tab remains available unless you also customize the asset library. ## Troubleshooting - **The path string is rejected** (`EngineException` with code `BLOCK.TEXT_ON_PATH_INVALID_SVG_PATH`): the value isn't a valid SVG path — check the `d` attribute. - **The path has multiple subpaths** (`BLOCK.TEXT_ON_PATH_MULTIPLE_SUBPATHS`): supply a single continuous contour with one leading `M`. - **The path has no measurable length** (`BLOCK.TEXT_ON_PATH_NO_MEASURABLE_CONTOUR`): a lone `M` has zero length — give the path drawable length. - **Text runs off the end of the path**: the block isn't grown to fit the text, so text longer than the path overflows. Shorten the text or enlarge the block. - **The Text on Path button doesn't appear**: the selection isn't a text block, the block's `"text/character"` scope isn't allowed, or the button was removed from the inspector bar. - **The block background disappears**: a text block on a path draws no block background, because the rectangle does not follow the curve. The setting stays, and the background draws again when the path is removed. Use the background color of the text runs instead. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.setTextOnPath(block=_, svgPath=_)` | Curve a text block along an SVG path (single subpath); pass `null` to clear. | | `engine.block.getTextOnPath(block=_)` | Get the block's current path SVG string, or `null`. | | `engine.block.setTextOnPathOffset(block=_, offset=_)` | Set the proportional offset (`[-1, 1]`) of the text along the path. | | `engine.block.getTextOnPathOffset(block=_)` | Get the current path offset. | | `engine.block.setTextOnPathFlipped(block=_, flipped=_)` | Flip the text to the other side of the path (reverse direction). | | `engine.block.getTextOnPathFlipped(block=_)` | Get whether the text is flipped. | | `engine.block.setEnum(block=_, property="text/verticalAlignment", value=_)` | Set where the text sits relative to the path (`Top`/`Center`/`Bottom`). | | `engine.block.getEnum(block=_, property="text/verticalAlignment")` | Read the current vertical alignment. | | `engine.block.create(blockType=_)` | Create the text block to curve. | | `engine.block.appendChild(parent=_, child=_)` | Attach the text block to a page. | | `engine.block.replaceText(block=_, text=_)` | Set the block's text content. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the font size of the text. | ### Properties Reference | Property | Type | Purpose | | --- | --- | --- | | `text/verticalAlignment` | Enum (`Top`, `Center`, `Bottom`) | Where the text sits relative to the path; defaults to `Top`. | | `text/pathOffset` | Float | The proportional offset along the path — the same value as `setTextOnPathOffset()`. | | `text/pathFlipped` | Boolean | Whether the text sits on the opposite side of the path — the same value as `setTextOnPathFlipped()`. | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) — fonts, sizing, color, and alignment for the curved text - [Text Effects](https://img.ly/docs/cesdk/android/text/effects-2dc9fc/) — add shadows and effects to the text - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) — customize or hide the editor's inspector bar 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: "Variable Fonts" description: "Use variable fonts to render multiple font weights and styles from a single font file." platform: android url: "https://img.ly/docs/cesdk/android/text/variable-fonts-32e788/" --- > 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 Text](https://img.ly/docs/cesdk/android/text-8a993a/) > [Variable Fonts](https://img.ly/docs/cesdk/android/text/variable-fonts-32e788/) --- ```kotlin file=@cesdk_android_examples/editor-guides-text-variable-fonts/VariableFontsEditorSolution.kt reference-only import android.net.Uri import androidx.compose.runtime.Composable import kotlinx.coroutines.delay import ly.img.editor.Editor import ly.img.editor.core.EditorScope import ly.img.editor.core.UnstableEditorApi 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.AssetSourceType import ly.img.editor.core.sheet.SheetType import ly.img.engine.AssetDefinition import ly.img.engine.AssetPayload import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit import ly.img.engine.Engine import ly.img.engine.Font import ly.img.engine.FontStyle import ly.img.engine.FontWeight import ly.img.engine.HorizontalAlignment import ly.img.engine.SizeMode import ly.img.engine.Typeface private const val VARIABLE_TYPEFACE_SOURCE_ID = "my-variable-fonts" private const val JOST_TYPEFACE_ASSET_ID = "ly.img.typeface.jost" // Jost is a variable font: one file covers all weights from 100 to 900. private val JOST_VARIABLE_FONT_URI: Uri = Uri.parse("https://cdn.jsdelivr.net/fontsource/fonts/jost:vf@5/latin-wght-normal.woff2") // The sub-family name CE.SDK shows for each of the nine standard weights. private val WEIGHT_SUB_FAMILIES = linkedMapOf( FontWeight.THIN to "Thin", FontWeight.EXTRA_LIGHT to "Extra Light", FontWeight.LIGHT to "Light", FontWeight.NORMAL to "Regular", FontWeight.MEDIUM to "Medium", FontWeight.SEMI_BOLD to "Semi Bold", FontWeight.BOLD to "Bold", FontWeight.EXTRA_BOLD to "Extra Bold", FontWeight.HEAVY to "Heavy", ) /** * Builds one [Font] entry per weight and style combination. Every entry points at the * same file, which is what marks the typeface as a variable font. */ private fun variableFontCombinations( uri: Uri, variantWeight: Boolean, variantItalic: Boolean, ): List { val weights = if (variantWeight) { WEIGHT_SUB_FAMILIES.keys.toList() } else { listOf(FontWeight.NORMAL) } val styles = if (variantItalic) { listOf(FontStyle.NORMAL, FontStyle.ITALIC) } else { listOf(FontStyle.NORMAL) } return styles.flatMap { style -> weights.map { weight -> val weightLabel = WEIGHT_SUB_FAMILIES.getValue(weight) Font( uri = uri, subFamily = if (style == FontStyle.ITALIC) "$weightLabel Italic" else weightLabel, weight = weight, style = style, ) } } } private fun jostTypeface(): Typeface = Typeface( name = "Jost", fonts = variableFontCombinations( uri = JOST_VARIABLE_FONT_URI, variantWeight = true, // This file has no `ital` axis, so italic entries would render upright. variantItalic = false, ), ) private fun jostTypefaceAsset() = AssetDefinition( id = JOST_TYPEFACE_ASSET_ID, label = mapOf("en" to "Jost"), tags = mapOf("en" to listOf("variable", "sans")), meta = mapOf("languages" to "latin"), payload = AssetPayload( typeface = jostTypeface(), ), ) private fun createVariableFontSource( engine: Engine, sourceId: String = VARIABLE_TYPEFACE_SOURCE_ID, ) { if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = emptyList(), ) engine.asset.addAsset( sourceId = sourceId, asset = jostTypefaceAsset(), ) engine.asset.assetSourceContentsChanged(sourceId = sourceId) } /** * Adds the variable font to the source the editor font sheet reads from, keeping the * built-in typefaces in place. */ private fun addVariableFontToEditorLibrary(engine: Engine) { val typefaceSourceId = AssetSourceType.Typeface.sourceId if (typefaceSourceId !in engine.asset.findAllSources()) { // The font sheet source only exists once an editor configuration has registered it. engine.asset.addLocalSource( sourceId = typefaceSourceId, supportedMimeTypes = emptyList(), ) } engine.asset.addAsset( sourceId = typefaceSourceId, asset = jostTypefaceAsset(), ) engine.asset.assetSourceContentsChanged(sourceId = typefaceSourceId) } @OptIn(UnstableEditorApi::class) private fun EditorScope.openVariableFontSheet(text: DesignBlock) { editorContext.eventHandler.send( EditorEvent.Sheet.Open( SheetType.Font( designBlock = text, fontFamilies = listOf(JOST_TYPEFACE_ASSET_ID), ), ), ) } private data class WeightSample( val weight: FontWeight, val label: String, ) private val WEIGHT_SAMPLES = listOf( WeightSample(weight = FontWeight.THIN, label = "Thin 100"), WeightSample(weight = FontWeight.NORMAL, label = "Regular 400"), WeightSample(weight = FontWeight.BOLD, label = "Bold 700"), WeightSample(weight = FontWeight.HEAVY, label = "Heavy 900"), ) /** * Creates one text block per sample weight. Every block renders from the same font file, * because the typeface resolves the weight to an axis value instead of another file. */ private fun createWeightSamples( engine: Engine, page: DesignBlock, typeface: Typeface, ): List = WEIGHT_SAMPLES.mapIndexed { index, sample -> val text = engine.block.create(blockType = DesignBlockType.Text) engine.block.appendChild(parent = page, child = text) engine.block.replaceText(block = text, text = sample.label) engine.block.setTextFontSize(block = text, fontSize = 56F) engine.block.setTextHorizontalAlignment(block = text, alignment = HorizontalAlignment.Center) engine.block.setWidthMode(block = text, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = text, value = 700F) engine.block.setHeightMode(block = text, mode = SizeMode.AUTO) engine.block.setPositionX(block = text, value = 50F) // 200 rather than 160 keeps the first sample clear of the headline's selection handles. engine.block.setPositionY(block = text, value = 200F + index * 105F) engine.block.setTypeface(block = text, typeface = typeface) engine.block.setTextFontWeight(block = text, fontWeight = sample.weight) text } /** * Switches an existing text block to another weight. The engine resolves the matching * variant from the typeface and renders it from the already loaded font file. */ private fun switchHeadlineWeight( engine: Engine, headline: DesignBlock, ): List { engine.block.setTextFontWeight(block = headline, fontWeight = FontWeight.EXTRA_BOLD) // If the font file also provides an `ital` axis, styles switch the same way: // engine.block.setTextFontStyle(block = headline, fontStyle = FontStyle.ITALIC) return engine.block.getTextFontWeights(block = headline) } /** * Demo scaffolding: builds the sample page and the headline the weight-switching snippet * operates on. Replace this with your own scene setup. */ private fun createSampleScene( engine: Engine, typeface: Typeface, ): Pair { // A Pixel design unit also makes setTextFontSize interpret its value in pixels, // so the page size and the font sizes below share one unit. val scene = engine.scene.create(designUnit = DesignUnit.PIXEL) val page = engine.block.create(blockType = DesignBlockType.Page) engine.block.setWidth(block = page, value = 800F) engine.block.setHeight(block = page, value = 600F) engine.block.appendChild(parent = scene, child = page) val headline = engine.block.create(blockType = DesignBlockType.Text) engine.block.appendChild(parent = page, child = headline) engine.block.replaceText(block = headline, text = "Variable Fonts") engine.block.setTextFontSize(block = headline, fontSize = 64F) engine.block.setTextHorizontalAlignment(block = headline, alignment = HorizontalAlignment.Center) engine.block.setWidthMode(block = headline, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = headline, value = 700F) engine.block.setHeightMode(block = headline, mode = SizeMode.AUTO) engine.block.setPositionX(block = headline, value = 50F) engine.block.setPositionY(block = headline, value = 48F) engine.block.setTypeface(block = headline, typeface = typeface) return page to headline } data class VariableFontsResult( val sourceId: String, val generatedFontCount: Int, val sampleWeights: List, val headlineWeights: List, ) fun variableFonts( engine: Engine, sourceId: String = VARIABLE_TYPEFACE_SOURCE_ID, ): VariableFontsResult { createVariableFontSource(engine = engine, sourceId = sourceId) val typeface = jostTypeface() val (page, headline) = createSampleScene(engine = engine, typeface = typeface) val samples = createWeightSamples(engine = engine, page = page, typeface = typeface) val headlineWeights = switchHeadlineWeight(engine = engine, headline = headline) return VariableFontsResult( sourceId = sourceId, generatedFontCount = typeface.fonts.size, sampleWeights = samples.flatMap { engine.block.getTextFontWeights(block = it) }, headlineWeights = headlineWeights, ) } @Composable fun VariableFontsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { val engine = editorContext.engine val typeface = jostTypeface() val (page, headline) = createSampleScene(engine = engine, typeface = typeface) createWeightSamples(engine = engine, page = page, typeface = typeface) switchHeadlineWeight(engine = engine, headline = headline) engine.scene.zoomToBlock( block = page, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) } onLoaded = { val engine = editorContext.engine addVariableFontToEditorLibrary(engine = engine) val headline = engine.block.findByType(DesignBlockType.Text).firstOrNull() if (headline != null) { engine.block.setSelected(block = headline, selected = true) // Let the editor publish the new selection before opening the contextual font sheet. delay(500) openVariableFontSheet(text = headline) } } } }, onClose = onClose, ) } ``` Use variable fonts to offer a full range of font weights and styles from a single font file. ![Text blocks at four different weights, all rendered from one variable font file in the Android editor](./assets/android.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-text-variable-fonts) Variable fonts are OpenType fonts that pack multiple variations of a font family into a single file. Instead of loading one file per weight, you register a single file and CE.SDK renders each variant by applying variation axis values. This reduces network requests and simplifies font management, especially for typefaces with many weights. This guide covers how CE.SDK detects variable fonts, how to build the font entries for a variable font file, how to register the result as a custom typeface, and how to switch between weights and styles both in the editor UI and programmatically. ## How CE.SDK Handles Variable Fonts A `Typeface` is a font family with a `fonts` list, where each `Font` declares a `uri`, `subFamily`, `weight` and `style`. CE.SDK treats a typeface as a variable font when multiple fonts in the list share the same file URI. Internally, the engine encodes the selected weight and style as axis values on the font's URI (for example `font.woff2#wght=700&ital=1`), so each variant has a unique identity while sharing one underlying font resource. CE.SDK supports two variation axes: - `wght` — the font weight, mapped from the nine `FontWeight` values `THIN` (100) through `HEAVY` (900) - `ital` — the italic style, mapped from `FontStyle.NORMAL` (0) and `FontStyle.ITALIC` (1) Axis values outside the range the font file supports are clamped to the nearest supported value. Aside from how the `fonts` list is built, variable fonts need no special handling: the same typeface and font APIs work for static and variable fonts, and exports render the selected variant. ## Generate Font Variants A variable font typeface needs one `Font` entry per weight and style combination, all pointing at the same file. The helper below derives those entries from the `FontWeight` and `FontStyle` enums, so the variants come from enum values rather than from registering a separate font file per weight. The `variantWeight` flag controls weight variants: when `true`, the helper generates entries for all nine standard weights. The `variantItalic` flag controls italic variants: only pass `true` when the font file actually provides an `ital` axis, otherwise the italic entries would render upright. Jost, the font we use here, has a weight axis but no italic axis: ```kotlin highlight-android-generate-variants // Jost is a variable font: one file covers all weights from 100 to 900. private val JOST_VARIABLE_FONT_URI: Uri = Uri.parse("https://cdn.jsdelivr.net/fontsource/fonts/jost:vf@5/latin-wght-normal.woff2") // The sub-family name CE.SDK shows for each of the nine standard weights. private val WEIGHT_SUB_FAMILIES = linkedMapOf( FontWeight.THIN to "Thin", FontWeight.EXTRA_LIGHT to "Extra Light", FontWeight.LIGHT to "Light", FontWeight.NORMAL to "Regular", FontWeight.MEDIUM to "Medium", FontWeight.SEMI_BOLD to "Semi Bold", FontWeight.BOLD to "Bold", FontWeight.EXTRA_BOLD to "Extra Bold", FontWeight.HEAVY to "Heavy", ) /** * Builds one [Font] entry per weight and style combination. Every entry points at the * same file, which is what marks the typeface as a variable font. */ private fun variableFontCombinations( uri: Uri, variantWeight: Boolean, variantItalic: Boolean, ): List { val weights = if (variantWeight) { WEIGHT_SUB_FAMILIES.keys.toList() } else { listOf(FontWeight.NORMAL) } val styles = if (variantItalic) { listOf(FontStyle.NORMAL, FontStyle.ITALIC) } else { listOf(FontStyle.NORMAL) } return styles.flatMap { style -> weights.map { weight -> val weightLabel = WEIGHT_SUB_FAMILIES.getValue(weight) Font( uri = uri, subFamily = if (style == FontStyle.ITALIC) "$weightLabel Italic" else weightLabel, weight = weight, style = style, ) } } } private fun jostTypeface(): Typeface = Typeface( name = "Jost", fonts = variableFontCombinations( uri = JOST_VARIABLE_FONT_URI, variantWeight = true, // This file has no `ital` axis, so italic entries would render upright. variantItalic = false, ), ) ``` With both flags set to `true`, the helper returns 18 entries (nine weights, each in normal and italic). ## Register the Variable Font Typeface We register the variable font like any custom font: create a local asset source and add an asset whose `AssetPayload(typeface = ...)` holds the generated fonts. See [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) for the full asset source workflow: ```kotlin highlight-android-register-typeface private fun jostTypefaceAsset() = AssetDefinition( id = JOST_TYPEFACE_ASSET_ID, label = mapOf("en" to "Jost"), tags = mapOf("en" to listOf("variable", "sans")), meta = mapOf("languages" to "latin"), payload = AssetPayload( typeface = jostTypeface(), ), ) private fun createVariableFontSource( engine: Engine, sourceId: String = VARIABLE_TYPEFACE_SOURCE_ID, ) { if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } engine.asset.addLocalSource( sourceId = sourceId, supportedMimeTypes = emptyList(), ) engine.asset.addAsset( sourceId = sourceId, asset = jostTypefaceAsset(), ) engine.asset.assetSourceContentsChanged(sourceId = sourceId) } ``` The sample removes an existing source with the same ID before adding it again so repeated launches stay deterministic. ## Show the Font in the Typeface Library To make the variable font selectable in the editor, add the asset to the source the font sheet reads from, `AssetSourceType.Typeface.sourceId`. Adding to that source instead of replacing it keeps any typefaces an editor configuration already registered. A configuration that registers none leaves the source absent, so create it first when it is missing: ```kotlin highlight-android-update-library /** * Adds the variable font to the source the editor font sheet reads from, keeping the * built-in typefaces in place. */ private fun addVariableFontToEditorLibrary(engine: Engine) { val typefaceSourceId = AssetSourceType.Typeface.sourceId if (typefaceSourceId !in engine.asset.findAllSources()) { // The font sheet source only exists once an editor configuration has registered it. engine.asset.addLocalSource( sourceId = typefaceSourceId, supportedMimeTypes = emptyList(), ) } engine.asset.addAsset( sourceId = typefaceSourceId, asset = jostTypefaceAsset(), ) engine.asset.assetSourceContentsChanged(sourceId = typefaceSourceId) } ``` Registering the asset does not change which rows the built-in sheet shows. When you open `SheetType.Font`, pass the custom asset IDs through `fontFamilies`; otherwise the sheet uses its default font-family filter and hides typefaces that are not in that list: ```kotlin highlight-android-font-sheet-filter @OptIn(UnstableEditorApi::class) private fun EditorScope.openVariableFontSheet(text: DesignBlock) { editorContext.eventHandler.send( EditorEvent.Sheet.Open( SheetType.Font( designBlock = text, fontFamilies = listOf(JOST_TYPEFACE_ASSET_ID), ), ), ) } ``` Call both from your editor configuration once the scene has loaded. Every generated variant then appears in the font style list, and switching between them updates the rendering without loading another file. ## Apply Weights to Text Blocks Programmatically, we apply the variable font with `engine.block.setTypeface()` and pick a variant with `engine.block.setTextFontWeight()`. Here we create four text blocks at different weights, all rendered from the same file: ```kotlin highlight-android-apply-weights private data class WeightSample( val weight: FontWeight, val label: String, ) private val WEIGHT_SAMPLES = listOf( WeightSample(weight = FontWeight.THIN, label = "Thin 100"), WeightSample(weight = FontWeight.NORMAL, label = "Regular 400"), WeightSample(weight = FontWeight.BOLD, label = "Bold 700"), WeightSample(weight = FontWeight.HEAVY, label = "Heavy 900"), ) /** * Creates one text block per sample weight. Every block renders from the same font file, * because the typeface resolves the weight to an axis value instead of another file. */ private fun createWeightSamples( engine: Engine, page: DesignBlock, typeface: Typeface, ): List = WEIGHT_SAMPLES.mapIndexed { index, sample -> val text = engine.block.create(blockType = DesignBlockType.Text) engine.block.appendChild(parent = page, child = text) engine.block.replaceText(block = text, text = sample.label) engine.block.setTextFontSize(block = text, fontSize = 56F) engine.block.setTextHorizontalAlignment(block = text, alignment = HorizontalAlignment.Center) engine.block.setWidthMode(block = text, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = text, value = 700F) engine.block.setHeightMode(block = text, mode = SizeMode.AUTO) engine.block.setPositionX(block = text, value = 50F) // 200 rather than 160 keeps the first sample clear of the headline's selection handles. engine.block.setPositionY(block = text, value = 200F + index * 105F) engine.block.setTypeface(block = text, typeface = typeface) engine.block.setTextFontWeight(block = text, fontWeight = sample.weight) text } ``` ## Switch Weights and Styles Weight and style can change at any time, on whole blocks or on selected text ranges. The engine resolves the matching variant from the typeface, applies the axis values and renders it from the already loaded font file: ```kotlin highlight-android-switch-weight /** * Switches an existing text block to another weight. The engine resolves the matching * variant from the typeface and renders it from the already loaded font file. */ private fun switchHeadlineWeight( engine: Engine, headline: DesignBlock, ): List { engine.block.setTextFontWeight(block = headline, fontWeight = FontWeight.EXTRA_BOLD) // If the font file also provides an `ital` axis, styles switch the same way: // engine.block.setTextFontStyle(block = headline, fontStyle = FontStyle.ITALIC) return engine.block.getTextFontWeights(block = headline) } ``` The `from` and `to` parameters of `setTextFontWeight()` and `setTextFontStyle()` are UTF-16 code unit offsets, so they count surrogate pairs as two positions. The same variants drive the editor UI: users switch between them in the font style list, and bold or italic toggles resolve against the generated font entries. ## Troubleshooting - **All weights render the same**: The font file is not a variable font or lacks a `wght` axis. Verify the file contains the axes you need, for example with a font inspection tool. - **Italic variants render upright**: The font file has no `ital` axis. Only pass `true` for italic variants when the file provides one; italic-only families often ship as a separate file. - **A weight looks different than expected**: Axis values outside the font's supported range are clamped. For example, requesting `THIN` (100) from a font whose weight axis starts at 300 renders at 300. - **The font is not visible in the editor UI**: Register the asset under `AssetSourceType.Typeface.sourceId`, call `engine.asset.assetSourceContentsChanged()`, and open `SheetType.Font` with `fontFamilies` containing your typeface asset ID. - **The font file does not load**: Check that the URI is reachable from the device and points to a valid TTF, OTF, WOFF, or WOFF2 file. ## API Reference | Method | Purpose | | ------ | ------- | | `engine.asset.findAllSources()` | Check whether the typeface source is already registered | | `engine.asset.removeSource(sourceId=_)` | Remove an existing source before re-registering the typeface | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Create a local source for the variable font typeface | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add the typeface asset definition to a source | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify CE.SDK after changing the source contents | | `editorContext.eventHandler.send(event=_)` | Dispatch editor UI events from an editor configuration callback | | `EditorEvent.Sheet.Open(type=_)` | Open a bottom sheet in the CE.SDK editor UI | | `SheetType.Font(designBlock=_, fontFamilies=_)` | Filter the font sheet to your typeface asset IDs | | `engine.block.create(blockType=_)` | Create the text blocks used by the weight samples | | `engine.block.appendChild(parent=_, child=_)` | Add each text block to the page | | `engine.block.replaceText(block=_, text=_)` | Set the text block contents | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set the sample text size | | `engine.block.setTextHorizontalAlignment(block=_, alignment=_)` | Center the sample text blocks | | `engine.block.setWidthMode(block=_, mode=_)` | Give the text block a fixed width instead of auto-sizing | | `engine.block.setWidth(block=_, value=_)` | Set the sample text block width | | `engine.block.setHeightMode(block=_, mode=_)` | Let the text block fit its content height | | `engine.block.setPositionX(block=_, value=_)` | Position the text block on the x-axis | | `engine.block.setPositionY(block=_, value=_)` | Position the text block on the y-axis | | `engine.block.setTypeface(block=_, typeface=_, from=_, to=_)` | Apply the variable font typeface to a text block or range | | `engine.block.setTextFontWeight(block=_, fontWeight=_, from=_, to=_)` | Switch the rendered weight of the variable font | | `engine.block.setTextFontStyle(block=_, fontStyle=_, from=_, to=_)` | Switch between normal and italic rendering | | `engine.block.getTextFontWeights(block=_, from=_, to=_)` | Query the font weights used in a text range | ## Next Steps - [Customize Fonts](https://img.ly/docs/cesdk/android/text/custom-fonts-9565b3/) — the full custom typeface workflow, including static multi-file fonts - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) — fills, sizing, color, and alignment for text 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: "To v1.19" description: "Learn what changed in v1.19 and how to update your implementation to stay compatible." platform: android url: "https://img.ly/docs/cesdk/android/to-v1-19-55bcad/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/android/upgrade-4f8715/) > [To v1.19](https://img.ly/docs/cesdk/android/to-v1-19-55bcad/) --- Version v1.19 of CreativeEngineSDK and CreativeEditorSDK introduces structural changes to many of the current design blocks, making them more composable and more powerful. Along with this update, there are mandatory license changes that require attention. This comes with a number of breaking changes. This document will explain the changes and describe the steps you need to take to adapt them to your setup. ## **Initialization** The initialization of the `Engine` has changed. Now `start` is a suspending function and it requires a new parameter `license` which is the API key you received from our dashboard. There is also a new optional parameter `userId` an optional unique ID tied to your application's user. This helps us accurately calculate monthly active users (MAU). Especially useful when one person uses the app on multiple devices with a sign-in feature, ensuring they're counted once. Providing this aids in better data accuracy. ```kotlin engine.start(license = "", userId = "") ``` ## **DesignBlockType** This class is not an enum class anymore, but a sealed class. These are the transformations of all `DesignBlockType` types: Removed: - `DesignBlockType.IMAGE` - `DesignBlockType.VIDEO` - `DesignBlockType.STICKER` - `DesignBlockType.VECTOR_PATH` - `DesignBlockType.RECT_SHAPE` - `DesignBlockType.LINE_SHAPE` - `DesignBlockType.STAR_SHAPE` - `DesignBlockType.POLYGON_SHAPE` - `DesignBlockType.ELLIPSE_SHAPE` - `DesignBlockType.COLOR_FILL` - `DesignBlockType.IMAGE_FILL` - `DesignBlockType.VIDEO_FILL` - `DesignBlockType.LINEAR_GRADIENT_FILL` - `DesignBlockType.RADIAL_GRADIENT_FILL` - `DesignBlockType.CONICAL_GRADIENT_FILL` Renamed: - `DesignBlockType.SCENE` -> `DesignBlockType.Scene` - `DesignBlockType.STACK` -> `DesignBlockType.Stack` - `DesignBlockType.CAMERA` -> `DesignBlockType.Camera` - `DesignBlockType.PAGE` -> `DesignBlockType.Page` - `DesignBlockType.AUDIO` -> `DesignBlockType.Audio` - `DesignBlockType.TEXT` -> `DesignBlockType.Text` - `DesignBlockType.GROUP` -> `DesignBlockType.Group` Added: - `DesignBlockType.Graphic` - `DesignBlockType.Cutout` Note that `DesignBlockType.values()` can be used to get the list of all instances mentioned above. ## **Graphic Design Block** A new generic `DesignBlockType.Graphic` type has been introduced, that forms the basis of the new unified block structure. ## **Shapes** Similar to how the fill of a block is a separate object which can be attached to and replaced on a design block, we have now introduced a similar concept for the shape of a block. You use the new `createShape`, `getShape` and `setShape` APIs in order to define the shape of a design block. Only the new `DesignBlockType.Graphic` block allows to change its shape with these APIs. The new available shape types are: - `ShapeType.Rect` - `ShapeType.Line` - `ShapeType.Ellipse` - `ShapeType.Polygon` - `ShapeType.Star` - `ShapeType.VectorPath` Note that `ShapeType.values()` can be used to get the list of all instances mentioned above. The following design block types are now removed in favor of using a `DesignBlockType.Graphic` block with one of the above mentioned shape instances: - `DesignBlockType.RECT_SHAPE` - `DesignBlockType.LINE_SHAPE` - `DesignBlockType.ELLIPSE_SHAPE` - `DesignBlockType.POLYGON_SHAPE` - `DesignBlockType.STAR_SHAPE` - `DesignBlockType.VECTOR_PATH` This structural change means that the shape-specific properties (e.g. the number of sides of a polygon) are not available on the design block anymore but on the shape instances instead. You will have to add calls to `getShape` to get the instance id of the shape instance and then pass that to the property getter and setter APIs. Also, remember to change property key strings in the getter and setter calls from plural `shapes/…` to singular `shape/…` to match the new type identifiers. ## **Image and Sticker** Previously, `DesignBlockType.IMAGE` and `DesignBlockType.STICKER` were their own high-level design block types. They neither support the fill APIs nor the effects APIs. Both of these blocks are now removed in favor of using a `DesignBlockType.Graphic` block with an image fill (`FillType.Image`) and using the effects APIs instead of the legacy image block’s numerous effects properties. At its core, the sticker block has always just been an image block that is heavily limited in its capabilities. You can neither crop it, nor apply any effects to it. In order to replicate the difference as closely as possible in the new unified structure, more fine-grained scopes have been added. You can now limit the adopter’s ability to crop a block and to edit its appearance. Note that since these scopes only apply to a user of the editor with the “Adopter” role, a “Creator” user will now have all of the same editing options for both images and for blocks that used to be stickers. ## **Scopes** The following is the list of changes to the design block scopes: - (Breaking) The permission to crop a block was split from `content/replace` and `design/style` into a separate scope: `layer/crop`. - Deprecated the `design/arrange` scope and renamed `design/arrange/move` → `layer/move` `design/arrange/resize` → `layer/resize` `design/arrange/rotate` → `layer/rotate` `design/arrange/flip` → `layer/flip` - Deprecated the `content/replace` scope. For `DesignBlockType.Text` blocks, it is replaced with the new `text/edit` scope. For other blocks it is replaced with `fill/change`. - Deprecated the `design/style` scope and replaced it with the following fine-grained scopes: `text/character`, `stroke/change`, `layer/opacity`, `layer/blendMode`, `layer/visibility`, `layer/clipping`, `appearance/adjustments`, `appearance/filter`, `appearance/effect`, `appearance/blur`, `appearance/shadow` - Introduced `fill/change`, `stroke/change`, and `shape/change` scopes that control whether the fill, stroke or shape of a block may be edited by a user with an "Adopter" role. - The deprecated scopes are automatically mapped to their new corresponding scopes by the scope APIs for now until they will be removed completely in a future update. ## **Kind** While the new unified block structure both simplifies a lot of code and makes design blocks more powerful, it also means that many of the design blocks that used to have unique type ids now all have the same generic `DesignBlockType.Graphic` type, which means that calls to the `findByType` cannot be used to filter blocks based on their legacy type ids any more. Simultaneously, there are many instances in which different blocks in the scene which might have the same type and underlying technical structure have different semantic roles in the document and should therefore be treated differently by the user interface. To solve both of these problems, we have introduced the concept of a block “kind”. This is a mutable string that can be used to tag different blocks with a semantic label. You can get the kind of a block using the `getKind` API and you can query blocks with a specific kind using the `findByKind` API. CreativeEngine provides the following default kind values: - `image` - `video` - `sticker` - `scene` - `camera` - `stack` - `page` - `audio` - `text` - `shape` - `group` Unlike the immutable design block type id, you can change the kind of a block with the new `setKind` API. It is important to remember that the underlying structure and properties of a design block are not strictly defined by its kind, since the kind, shape, fill and effects of a block can be changed independent of each other. Therefore, a user-interface should not make assumptions about available properties of a block purely based on its kind. > **Note:** **Note**Due to legacy reasons, blocks with the kind "sticker" will continue to not allow > their contents to be cropped. This special behavior will be addressed and replaced > with a more general-purpose implementation in a future update. ​ ## **Asset Definitions** The asset definitions have been updated to reflect the deprecation of legacy block type ids and the introduction of the “kind” property. In addition to the “blockType” meta property, you can now also define the `“shapeType”` ,`“fillType”` and `“kind”` of the block that should be created by the default implementation of the applyAsset function. - `“blockType”` defaults to `DesignBlockType.Graphic.key (“//ly.img.ubq/graphic”)` if left unspecified. - `“shapeType”` defaults to `ShapeType.Rect.key (“//ly.img.ubq/shape/rect”)` if left unspecified - `“fillType”` defaults to `FillType.Color.key (“//ly.img.ubq/fill/color”)` if left unspecified Video block asset definitions used to specify the `“blockType”` as `“//ly.img.ubq/fill/video“ (FillType.Video.key)`. The `“fillType”` meta asset property should now be used instead for such fill type ids. ## **Automatic Migration** CreativeEngine will always continue to support scene files that contain the now removed legacy block types. Those design blocks will be automatically replaced by the equivalent new unified block structure when the scene is loaded, which means that the types of all legacy blocks will change to `DesignBlockType.Graphic`. Note that this can mean that a block gains new capabilities that it did not have before. For example, the line shape block did not have any stroke properties, so the `hasStroke` API used to return `false`. However, after the automatic migration its `DesignBlockType.Graphic` design block replacement supports both strokes and fills, so the `hasStroke` API now returns `true` . Similarly, the image block did not support fills or effects, but the `DesignBlockType.Graphic` block does. ## **Types and API Signatures** To improve the type safety of our APIs, we have moved away from using the `DesignBlockType` enum and replaced with a set of types. Those changes have affected the following APIs: - `BlockApi.create()` - `BlockApi.createFill()` - `BlockApi.createEffect()` - `BlockApi.createBlur()` - `BlockApi.findByType()` > **Note:** **Note**All the functions above still support the string overload variants, however, their > usage will cause lint warnings in favor of type safe overloads. ## **Code Examples** This section will show some code examples of the breaking changes and how it would look like after migrating. ```kotlin /** Block Creation */ // Creating an Image before migration val image = engine.block.create(DesignBlockType.IMAGE) engine.block.setString( block = image, property = "image/imageFileURI", value = "https://domain.com/link-to-image.jpg" ) // Creating an Image after migration val block = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://domain.com/link-to-image.jpg" ) engine.block.setShape(block, shape = rectShape) engine.block.setFill(block, fill = imageFill) engine.block.setKind(block, kind = "image") // Creating a star shape before migration val star = engine.block.create(DesignBlockType.STAR_SHAPE) engine.block.setInt(star, property = "shapes/star/points", value = 8) // Creating a star shape after migration val block = engine.block.create(DesignBlockType.Graphic) val starShape = engine.block.createShape(ShapeType.Star) val colorFill = engine.block.createFill(FillType.Color) engine.block.setInt(block = starShape, property = "shape/star/points", value = 8) engine.block.setShape(block, shape = starShape) engine.block.setFill(block, fill = colorFill) engine.block.setKind(block, kind = "shape") // Creating a sticker before migration val sticker = engine.block.create(DesignBlockType.STICKER) engine.block.setString( block = sticker, property = "sticker/imageFileURI", value = "https://domain.com/link-to-sticker.png" ) // Creating a sticker after migration val block = engine.block.create(DesignBlockType.Graphic) val rectShape = engine.block.createShape(ShapeType.Rect) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://domain.com/link-to-sticker.png" ) engine.block.setShape(block, shape = rectShape) engine.block.setFill(block, fill = imageFill) engine.block.setKind(block, kind = "sticker") /** Block Creation */ ``` ```kotlin /** Block Exploration */ // Query all images in the scene before migration val images = engine.block.findByType(DesignBlockType.IMAGE) // Query all images in the scene after migration val images = engine.block.findByType(DesignBlockType.Graphic).filter { block -> val fill = engine.block.getFill(block) engine.block.isValid(fill) && engine.block.getType(fill) == FillType.Image.key } // Query all stickers in the scene before migration val stickers = engine.block.findByType(DesignBlockType.STICKER) // Query all stickers in the scene after migration val stickers = engine.block.findByKind("sticker") // Query all Polygon shapes in the scene before migration val polygons = engine.block.findByType(DesignBlockType.POLYGON_SHAPE) // Query all Polygon shapes in the scene after migration val polygons = engine.block.findByType(DesignBlockType.Graphic).filter { block -> val shape = engine.block.getShape(block) engine.block.isValid(shape) && engine.block.getType(shape) == ShapeType.Polygon.key } /** Block Exploration */ ``` --- ## 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 v1.73" description: "Learn what changed in v1.73 and how to update your implementation to stay compatible." platform: android url: "https://img.ly/docs/cesdk/android/to-v1-73-ab14fb/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/android/upgrade-4f8715/) > [To v1.73](https://img.ly/docs/cesdk/android/to-v1-73-ab14fb/) --- Version 1.73 introduces a major Android UI architecture change: - `PhotoEditor`, `DesignEditor`, `VideoEditor`, `PostcardEditor` and `ApparelEditor` are no longer the primary integration path. - A single `Editor` composable is now the foundation for all solutions. - The former solution UIs are now deprecated and instead provided as [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) that you copy into your app and customize per your business logic. This guide covers the Android migration path and lists the public API changes with before/after examples. ## Choose Your Migration Path ### Path A: Adopt a Starter Kit (Recommended) Use the starter kit that matches your previous solution: | Legacy Composable | Starter Kit Guide | GitHub Repo | |---|---|---| | `PhotoEditor` | [Photo Editor](https://img.ly/docs/cesdk/android/starterkits/photo-editor-r6kq0u/) | [imgly/starterkit-photo-editor-android](https://github.com/imgly/starterkit-photo-editor-android) | | `DesignEditor` | [Design Editor](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) | [imgly/starterkit-design-editor-android](https://github.com/imgly/starterkit-design-editor-android) | | `VideoEditor` | [Video Editor](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) | [imgly/starterkit-video-editor-android](https://github.com/imgly/starterkit-video-editor-android) | | `PostcardEditor` | [Postcard Editor](https://img.ly/docs/cesdk/android/starterkits/postcard-editor-03mijr/) | [imgly/starterkit-postcard-editor-android](https://github.com/imgly/starterkit-postcard-editor-android) | | `ApparelEditor` | [T-Shirt Designer](https://img.ly/docs/cesdk/android/starterkits/t-shirt-designer-jwinqr/) | [imgly/starterkit-apparel-editor-android](https://github.com/imgly/starterkit-apparel-editor-android) | Each starter kit includes: - A demo `:app` module to run the starter kit. - A reusable `:starter-kit` Android library module. - A configuration builder (`{starter-kit-name}ConfigurationBuilder`) class as the starting point. - Encapsulated callback/component setup that you can modify directly in your app codebase per your business logic. ### Path B: Build a Fully Custom Editor We recommend using one of our starter kits, as they help with editor setup and provide a proven structure and architecture. However, you can still use the `Editor` composable without any configuration builder and provide your own scene creation, assets, and UI component configuration inline. Note that by default `Editor` launches an empty editor, meaning you have to configure all the components manually: ```kotlin Editor( license = "", userId = "", baseUri = "...".toUri(), engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, configuration = { EditorConfiguration.remember { onCreate = { ... } onExport = { ... } dock = { ... } ... overlay = { ... } } }, onClose = { ... }, ) ``` ## Public API Changes ### 1. Editor Entry Point #### Before: ```kotlin val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", ) val editorConfiguration = EditorConfiguration.rememberForDesign() DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, onClose = onClose, ) ``` #### Now: ```kotlin Editor( license = "", userId = "", configuration = { // Note that DesignConfigurationBuilder comes with the starter kit and not available otherwise EditorConfiguration.remember(::DesignConfigurationBuilder) }, onClose = onClose, ) ``` #### What changed - Added: `ly.img.editor.Editor(...)` composable. - Deprecated with `DeprecationLevel.ERROR`: `PhotoEditor`, `DesignEditor`, `VideoEditor`, `PostcardEditor`, `ApparelEditor` composables. - Deleted `EngineConfiguration` class. - Moved `EditorConfiguration` class from `ly.img.editor` to `ly.img.editor.core.configuration`. ### 2. Configuration Changes Before v1.73, configuration was split across two remember calls: - `EngineConfiguration.remember(...)` for engine configuration and engine callbacks. - `EditorConfiguration.remember(...)` for UI and event wiring. From v1.73, those are unified into: - `Editor(...)` composable parameters for high level configurations. - `EditorConfiguration.remember { ... }` for all callbacks, UI and event wiring. Below you can find the migration of all configuration options: #### Before: ```kotlin val engineConfiguration = EngineConfiguration.remember( license = "", userId = "", baseUri = "file:///android_asset/".toUri(), renderTarget = EngineRenderTarget.SURFACE_VIEW, onCreate = { ... }, onLoaded = { ... }, onExport = { ... }, onUpload = { asset: AssetDefinition, sourceType: UploadAssetSourceType -> asset }, onClose = { ... }, onError = { throwable: Throwable -> ... }, ) val editorConfiguration = EditorConfiguration.remember( initialState = remember { EditorUiState() }, uiMode = EditorUiMode.SYSTEM, assetLibrary = ..., colorPalette = ..., onEvent = { state: EditorUiState, event: EditorEvent -> /* Return updated EditorUiState */ }, overlay = { state: EditorUiState -> ... }, dock = { ... }, inspectorBar = { ... }, canvasMenu = { ... }, navigationBar = { ... }, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, onClose = { throwable: Throwable? -> ... }, ) ``` #### Now: ```kotlin Editor( license = "", userId = "", baseUri = "file:///android_asset/".toUri(), engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, uiMode = EditorUiMode.SYSTEM, configuration = { EditorConfiguration.remember { onCreate = { ... } onLoaded = { ... } onExport = { ... } onClose = { // hasUnsavedChanges: Boolean param is removed, you can use the following replacement: val hasUnsavedChanges = editorContext.engine.editor.canUndo() } onEvent = { event: EditorEvent -> // 1. state: EditorUiState param is removed // 2. lambda does not return updated EditorUiState anymore // Check the next section for more details on EditorUiState migration } onUpload = { asset: AssetDefinition, sourceType: UploadAssetSourceType -> asset } onError = { throwable: Throwable -> ... } colorPalette = { ... } assetLibrary = { ... } dock = { ... } navigationBar = { ... } inspectorBar = { ... } canvasMenu = { ... } bottomPanel = { ... } overlay = { // state: EditorUiState param is removed } } }, onClose = { throwable: Throwable? -> ... }, ) ``` #### What changed - Moved all callbacks into unified `EditorConfiguration`. - Moved general configuration parameters (that is, `license`, `engineRenderTarget`) to `Editor` composable directly. - `EditorConfiguration.remember` is a builder DSL instead of regular composable function. - Removed `hasUnsavedChanges: Boolean` from the `onClose` callback. You can use `engine.editor.canUndo()` instead. - External editor UI state is completely removed: `initialState` is removed and `EditorUiState` usages are removed from `onEvent` and `overlay` lambdas. More details in the next section. ### 3. External Editor State In the legacy API, external UI state around editor operations was wired through `EditorConfiguration` and `ly.img.editor.Events`. #### Before: This was the typical legacy flow: 1. Provide `initialState` in `EditorConfiguration.remember(...)`, for example `initialState = remember { EditorUiState() }`. 2. Send events from engine callbacks using `ly.img.editor.Events`, for example `ShowLoading` / `HideLoading`. 3. Capture and reduce events in `onEvent`, commonly via `EditorDefaults.onEvent(...)`. 4. Render from the updated `state` in `overlay`. ```kotlin val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { try { editorContext.eventHandler.send(ShowLoading) if (editorContext.engine.scene.get() == null) { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.appendChild(parent = scene, child = page) } } finally { editorContext.eventHandler.send(HideLoading) } }, ) val editorConfiguration = EditorConfiguration.remember( initialState = remember { EditorUiState() }, onEvent = { state: EditorUiState, event: EditorEvent -> EditorDefaults.onEvent(editorContext.activity, state, event) }, overlay = { state: EditorUiState -> if (state.isLoading) { // render loading UI } }, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, onClose = { throwable: Throwable? -> ... }, ) ``` #### Now: Use plain `Compose` state `showLoading`: ```kotlin var showLoading by remember { mutableStateOf(false) } Editor( license = "", configuration = { EditorConfiguration.remember { onCreate = { try { // Set value from callbacks and components showLoading = true if (editorContext.engine.scene.get() == null) { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.appendChild(parent = scene, child = page) } } finally { showLoading = false } } // No onEvent block required. overlay = { EditorComponent.remember { id = { EditorComponentId("my.package.overlay") } decoration = { // Read value in compose callbacks if (showLoading) { // render loading UI } } } } } }, onClose = { throwable: Throwable? -> ... }, ) ``` > **Warning:** All legacy events from `ly.img.editor.Events.kt` (for example `ShowLoading`, `HideLoading`) are deleted. Use recommended approach mentioned in this section. > **Warning:** `overlay` configuration lambda now also follows the pattern of other UI components and returns `EditorComponent`. > **Tip:** `onEvent` callback should only be used to observe `EditorEvent`s fired by you or by the internals of the editor. ### 4. Editor Defaults #### Before: - When using `EngineConfiguration.remember(...)`, callbacks other than `onCreate` had generic default implementations (`onLoaded`, `onExport`, `onUpload`, `onClose`, `onError`). - `onCreate` was commonly provided by solution helpers such as `EngineConfiguration.rememberForDesign(...)` (and equivalent helpers for other solutions). - UI component defaults were also solution-driven. For example, `EditorConfiguration.rememberForDesign(...)` provided default component wiring via helpers like `Dock.rememberForDesign()`, `NavigationBar.rememberForDesign()`, `InspectorBar.remember()`, and `CanvasMenu.remember()`. #### Now: - There is no default `onCreate`. If you do not provide one, the editor creates a blank scene with a single 1080x1080 page only to ensure something is visible. - Default `onExport` does nothing. - Default `onClose` directly closes the editor. - Editor does not show any overlay content, that is, loading indicators and dialogs. Overlay rendering must be provided manually. - All components (that is, `Dock`, `Overlay`) are not provided by default at all. You have to explicitly provide instances. - Note that components containing list of buttons (`Dock`, `NavigationBar`, `InspectorBar` and `CanvasMenu`) are by default empty. You have to provide your own `listBuilder` implementations: ```kotlin Editor( license = "", configuration = { EditorConfiguration.remember { onCreate = { // Must be implemented } onExport = { // Must be implemented } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberAdjustments() } add { Dock.Button.rememberFilter() } add { Dock.Button.rememberEffect() } } } } } overlay = { // Render overlays if you want to render loading indicators, confirmation dialogs, export dialogs etc. EditorComponent.remember { id = { EditorComponentId("my.package.overlay") } decoration = { // Render custom overlay } } } } }, onClose = { throwable: Throwable? -> ... }, ) ``` > **Warning:** Since `Editor` no longer ships any default behavior, the legacy `EditorDefaults` class is removed. ### 5. Component API Parameter-heavy `remember(...)` calls were common across all components. Now they are replaced with DSL builders: #### Before: ```kotlin Dock.Button.remember( id = EditorComponentId("my.button"), vectorIcon = { IconPack.AddText }, text = { "Text" }, onClick = { /* ... */ }, ) ``` #### Now: ```kotlin Dock.Button.remember { id = { EditorComponentId("my.button") } vectorIcon = { IconPack.AddText } textString = { "Text" } onClick = { /* ... */ } } ``` A new configuration of `Modifier` is added to all components: ```kotlin Dock.remember { modifier = { Modifier.shadow(5.dp) } horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberAdjustments() } } } } ``` > **Tip:** Combine `modifier` and `decoration` together for powerful configurations. - `Dock.Button`, `InspectorBar.Button`, `NavigationBar.Button` and `CanvasMenu.Button` components are combined into a single `Button` component. - `CanvasMenu.Divider` component is replaced with a generic `Divider` component. - A new `Timeline` component is introduced that provides powerful editing tools for editors that work with videos. - A new `bottomPanel` configuration option is introduced in `EditorConfiguration` that allows rendering just above the dock. For instance, this configuration can be used to render `Timeline` in editors that work with videos. ```kotlin Editor( license = "", configuration = { EditorConfiguration.remember { bottomPanel = { Timeline.remember() } } }, onClose = { throwable: Throwable? -> ... }, ) ``` ### 6. `EditorContext` Changes Configuration options used to be exposed as direct `EditorContext` properties (for example `colorPalette`, `assetLibrary`, `dock`, `overlay`, etc.).\ These are replaced by a unified `configuration: StateFlow` property, which allows both reading the current value and observing value changes of `EditorConfiguration`: #### Before: ```kotlin onCreate = { // Example in a regular callback val assetLibrary = editorContext.assetLibrary } dock = { // Example in a composable callback val assetLibrary = editorContext.assetLibrary } ``` #### Now: ```kotlin onCreate = { // Example in a regular callback, accessing current value val assetLibrary = editorContext.configuration.value?.assetLibrary // Example in a regular callback, observing the value editorContext.coroutineScope.launch { editorContext.configuration.collect { val mostRecentAssetLibrary = it?.assetLibrary } } } dock = { // Example in composable callback, accessing current value val assetLibrary = editorContext.configuration.value?.assetLibrary // Example in a composable callback, observing the value val mostRecentConfiguration by editorContext.configuration.collectAsState() val mostRecentAssetLibrary = mostRecentConfiguration?.assetLibrary } ``` A new `coroutineScope` property is introduced and should be used to launch coroutines from editor callbacks/components. - The coroutine scope is always alive while the editor is running. - It also survives configuration changes. ```kotlin editorContext.coroutineScope.launch { // long-running editor work } ``` ### 7. `EditorState` Changes `EditorState` has been expanded and now contains many more valuable properties. #### Before: ```kotlin data class EditorState( val canvasInsets: Rect = Rect.Zero, val activeSheet: SheetType? = null, val isTouchActive: Boolean = false, val isHistoryEnabled: Boolean = true, val viewMode: EditorViewMode = EditorViewMode.Edit(), ) ``` #### Now: ```kotlin @Immutable data class EditorState( val insets: Insets = Insets.Zero, // equivalent to canvasInsets before val extraInsets: Insets = Insets.Zero, val activeSheet: SheetType? = null, val activeSheetState: SheetState? = null, val isTouchActive: Boolean = false, val isHistoryEnabled: Boolean = true, val isBackHandlerEnabled: Boolean = false, val viewMode: EditorViewMode = EditorViewMode.Edit(), val dimensions: Dimensions = Dimensions(), val minVideoDuration: Duration? = null, val maxVideoDuration: Duration? = null, ) data class Dimensions( val editor: Size = Size.Zero, val navigationBar: Size = Size.Zero, val bottomPanel: Size = Size.Zero, val dock: Size = Size.Zero, val inspectorBar: Size = Size.Zero, ) ``` ### 8. New `EditorEvent`s The new architecture adds several public events for runtime editor updates: 1. `EditorEvent.Insets.SetExtra` Sets extra canvas insets on top of what is already calculated based on system status and navigation bars, IMG.LY dock and navigation bars, currently open sheet, timeline, system keyboard, etc.: ```kotlin editorContext.eventHandler.send( EditorEvent.Insets.SetExtra( insets = Insets(bottom = 24.dp), ), ) ``` > **Tip:** `insets` value ends up in `EditorState.extraInsets` and updates `EditorState.insets` too. 2. `EditorEvent.ApplyForceCrop` An event for enforcing crop configuration to a specific `designBlock`: ```kotlin // Force 1:1, 16:9 or 9:16 aspect ratios to the block val configuration = ForceCropConfiguration( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-1-1", presetCandidates = listOf( ForceCropPresetCandidate( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-16-9", ), ForceCropPresetCandidate( sourceId = "ly.img.crop.presets", presetId = "aspect-ratio-9-16", ), ), mode = ForceCropMode.IfNeeded(threshold = 0.01f), ) editorContext.eventHandler.send( EditorEvent.ApplyForceCrop( designBlock = block, configuration = configuration, ), ) ``` See [Force Crop](https://img.ly/docs/cesdk/android/user-interface/customization/force-crop-c2854e/) for more details. 3. `EditorEvent.ApplyVideoDurationConstraints` An event for applying video duration constraints: ```kotlin editorContext.eventHandler.send( EditorEvent.ApplyVideoDurationConstraints( minDuration = 2.seconds, maxDuration = 30.seconds, ), ) ``` > **Tip:** `minDuration` and `maxDuration` values end up in `EditorState`. --- ## 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 v1.77" description: "Learn what changed in v1.77 for Android — migrate your asset sources and branch on the new engine error codes." platform: android url: "https://img.ly/docs/cesdk/android/to-v1-77-ac6ca9/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/android/upgrade-4f8715/) > [To v1.77](https://img.ly/docs/cesdk/android/to-v1-77-ac6ca9/) --- This guide covers what changed for Android in v1.77: an asset-source registration migration, and new structured engine error codes. Both are described below. ## Asset sources Version 1.77 moves asset-source registration out of the engine binding and updates the default asset content. As part of this update, several built-in asset source IDs were renamed or merged. Two things change for your Android app: - The `Engine.addDefaultAssetSources(...)` and `Engine.addDemoAssetSources(...)` helpers, the `DefaultAssetSource` and `DemoAssetSource` enums, and the `Engine.Companion.assetBaseUri` constant are deprecated. Register sources directly with `Engine.asset.addLocalSourceFromJSON(contentUri)`. - Because the default base URI now serves the updated content, the deprecated helpers can no longer load the renamed or merged source IDs. Those sources are skipped silently, so the affected asset-library panels appear empty until you migrate. ## Do I need to migrate? You are affected if any of the following apply: - You call `engine.addDefaultAssetSources()` or `engine.addDemoAssetSources()`. - You reference a built-in source by one of the renamed IDs in the table below. - You self-host the CE.SDK assets. If you never registered asset sources yourself and do not self-host, the [Starter Kits](https://img.ly/docs/cesdk/android/starterkits-kxg120/) already register the current sources for you—bump the SDK and you are done. ## Asset Source ID Changes ### Renamed sources | Old ID (up to v1.76) | New ID (v1.77+) | |---|---| | `ly.img.vectorpath` | `ly.img.vector.shape` | | `ly.img.colors.defaultPalette` | `ly.img.color.palette` | | `ly.img.textComponents` | `ly.img.text.components` | ### Merged sources | Old IDs (up to v1.76) | New ID (v1.77+) | Notes | |---|---|---| | `ly.img.filter.lut` + `ly.img.filter.duotone` | `ly.img.filter` | A single source now covers LUT and duotone filters | ### Newly registered sources The default starter kits now register three text sources from the CDN. `ly.img.text` replaces the previous in-app programmatic text source (`ly.img.asset.source.text`): - `ly.img.text`—plain text presets (title, headline, body). - `ly.img.text.styles`—decorative text styles. - `ly.img.text.curves`—curved text presets. ## Scenario A: You use the default asset library If you relied on `addDefaultAssetSources` or `addDemoAssetSources` (directly, or through a pre-v1.73 solution view), replace those calls with explicit `addLocalSourceFromJSON` registrations that use the new IDs. #### Before ```kotlin engine.addDefaultAssetSources() engine.addDemoAssetSources() ``` #### Now ```kotlin val baseUri = Uri.parse("https://cdn.img.ly/packages/imgly/cesdk-android/$UBQ_VERSION$/assets") // Default content sources, plus the demo image source. 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.styles", "ly.img.text.curves", "ly.img.text.components", "ly.img.image", ).forEach { id -> engine.asset.addLocalSourceFromJSON(Uri.parse("$baseUri/$id/content.json")) } engine.asset.addLocalSource( sourceId = "ly.img.image.upload", supportedMimeTypes = listOf("image/jpeg", "image/png", "image/heic", "image/heif", "image/svg+xml", "image/gif", "image/apng", "image/bmp"), ) // A video editor also registers the video and audio sources (content + uploads): listOf("ly.img.video", "ly.img.audio").forEach { id -> engine.asset.addLocalSourceFromJSON(Uri.parse("$baseUri/$id/content.json")) } engine.asset.addLocalSource( sourceId = "ly.img.video.upload", supportedMimeTypes = listOf("video/mp4"), ) engine.asset.addLocalSource( sourceId = "ly.img.audio.upload", supportedMimeTypes = listOf("audio/x-m4a", "audio/mp3", "audio/mpeg"), ) ``` If you use a [Starter Kit](https://img.ly/docs/cesdk/android/starterkits-kxg120/), this registration already lives in the starter kit's asset-loading callback and is updated for you. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for the complete, current source list and registration reference. ## Scenario B: You self-host your assets If you serve the assets from your own location, download the updated asset bundle and point your base URI at it: 1. Download `https://cdn.img.ly/packages/imgly/cesdk-android/$UBQ_VERSION$/imgly-assets.zip` and copy the `assets/` directory to your hosting location. 2. Update your `baseUri` to your hosted path. 3. Register the renamed and newly added source IDs from the tables above. The old IDs no longer resolve. ## Why is my asset library empty or showing "Cannot connect to service"? After upgrading, an empty shapes, filter, color-palette, or text panel means a source ID no longer resolves at the base URI. This happens when: - The deprecated `addDefaultAssetSources` or `addDemoAssetSources` helpers requested an old ID (for example `ly.img.vectorpath`) that the CDN no longer serves. Register the new ID instead, following Scenario A. - You self-host and still serve content from v1.76 or earlier. Serve the updated assets and register the new IDs, following Scenario B. > **Tip:** The IMG.LY CDN is intended for development and evaluation. For production, self-host the assets and point your `baseUri` at your own location. See [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/). ## Engine error codes Engine errors now carry a stable catalog `code`, a developer-facing hint, typed args, and an optional docs anchor. Existing code that reads `EngineException.message` keeps working; new code can branch on the stable `code`. `EngineException` (still in `ly.img.engine`) now exposes the structured fields directly. The constructor is **source-compatible** — `EngineException("msg")` still works — so existing callers that read `e.message` keep working unchanged. ```kotlin import ly.img.engine.EngineErrorCode import ly.img.engine.EngineException try { engine.scene.create() } catch (e: EngineException) { when (e.code) { EngineErrorCode.SCENE_NOT_VALID -> toast("No scene is loaded.") // Any code you don't handle explicitly — fall back to message. else -> toast(e.message ?: "Unknown error") } e.docsUrl?.let { openInBrowser(it) } } ``` `EngineErrorCode` is an `object` of `String` constants — branch on `EngineErrorCode.SCENE_NOT_VALID` instead of a hand-typed string. The `code` itself stays a plain string, so an unrecognized or future code never breaks your build — always keep an `else` branch. ### Typed args `EngineException.args` is a `Map` whose values are `Boolean`, `Long`, `Double`, or `String` — the original primitive type is preserved across the JNI boundary. Cast in Kotlin: ```kotlin val fontSize = e.args["fontSize"] as? Long val blockId = e.args["blockId"] as? Long ``` ### What is not a breaking change - `EngineException.message` still returns the engine's rendered English string, and the `EngineException(message)` constructor signature is unchanged (new fields have default values), so existing code keeps working. ### What might surprise you - **`code` is an append-only contract.** Existing codes are never renamed or repurposed, and new codes are added over time — which is why you should always keep an `else` branch. Generic failures wrapped from third-party libraries surface under a catch-all code such as `UTILS.STD_EXPECTED_STRING_ERROR`. - **`silent: true`** flags errors the catalog marks as expected limitations — programmatically observable but suppressed from logs. --- ## 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 v1.83" description: "Learn what changed in v1.83 for Android — the video timeline renders its tracks alone until you declare its header and buttons." platform: android url: "https://img.ly/docs/cesdk/android/to-v1-83-6ee1d1/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/android/upgrade-4f8715/) > [To v1.83](https://img.ly/docs/cesdk/android/to-v1-83-6ee1d1/) --- Version 1.83 makes the video timeline configurable. A timeline you build with `Timeline.remember()` alone now renders its tracks and nothing else: no header, and no "Add Clip" or "Add Audio" button. Your code keeps compiling, so check your video editor after you upgrade. ## What changed Before v1.83, `Timeline` always showed three surfaces around its tracks: - The **header**, with the timecode, the play/pause button, the loop toggle, and the expand/collapse toggle. - The **"Add Clip" button** after the last clip of the background track. - The **"Add Audio" button** below the overlay tracks. Version 1.83 adds `headerListBuilder`, `addClipButton`, and `addAudioButton` to `TimelineBuilder`, so you set each of the three surfaces. **This is a breaking change:** the defaults of these properties render none of them. ## Do I need to migrate? You are affected if your configuration builds the timeline without these properties, as every configuration written before v1.83 does. The Video Editor starter kit built its timeline this way before v1.83, in `component/BottomPanel.kt`: ```kotlin @Composable fun rememberBottomPanel() = Timeline.remember() ``` You are not affected if you start from the v1.83 [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/). It declares the three surfaces, so its timeline does not change. ## Declare the timeline's surfaces Set the header and both buttons on the timeline's builder. The v1.83 Video Editor starter kit builds its timeline this way. You get the three surfaces back, and you can change each one. #### Before ```kotlin import androidx.compose.runtime.Composable import ly.img.editor.core.component.Timeline import ly.img.editor.core.component.remember @Composable fun rememberBottomPanel() = Timeline.remember() ``` #### Now ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import ly.img.editor.core.component.Timeline import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberAddAudio import ly.img.editor.core.component.rememberAddClip import ly.img.editor.core.component.rememberLoop import ly.img.editor.core.component.rememberPlayPause import ly.img.editor.core.component.rememberTimecode import ly.img.editor.core.component.rememberToggleExpanded @Composable fun rememberBottomPanel() = Timeline.remember { headerListBuilder = { Timeline.HeaderListBuilder.remember { aligned(alignment = Alignment.Start) { add { Timeline.Label.rememberTimecode() } } aligned(alignment = Alignment.CenterHorizontally) { add { Timeline.Button.rememberPlayPause() } } aligned(alignment = Alignment.End) { add { Timeline.Button.rememberLoop() } add { Timeline.Button.rememberToggleExpanded() } } } } addClipButton = { Timeline.Button.rememberAddClip() } addAudioButton = { Timeline.Button.rememberAddAudio() } } ``` The snippet keeps the name `rememberBottomPanel()`, so it replaces `component/BottomPanel.kt` in a starter kit you copied before v1.83. The v1.83 starter kit keeps the same block in `rememberTimeline()` in `component/Timeline.kt`, and `rememberBottomPanel()` calls it. If you set up the bottom panel yourself, return the same `Timeline.remember` block from your `bottomPanel`. Leave out an assignment to remove that surface. To keep the loop toggle beside the play button, move `rememberLoop()` into the centered group. The [Video Timeline](https://img.ly/docs/cesdk/android/user-interface/customization/video-timeline-7a0400/) guide shows how to change the header, choose what the two buttons offer, and set the height of the timeline. --- ## 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: "Upgrade" description: "Learn how to upgrade CE.SDK and apply required changes when migrating between major SDK versions." platform: android url: "https://img.ly/docs/cesdk/android/upgrade-4f8715/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/android/upgrade-4f8715/) --- --- ## Related Pages - [To v1.19](https://img.ly/docs/cesdk/android/to-v1-19-55bcad/) - Learn what changed in v1.19 and how to update your implementation to stay compatible. - [To v1.73](https://img.ly/docs/cesdk/android/to-v1-73-ab14fb/) - Learn what changed in v1.73 and how to update your implementation to stay compatible. - [To v1.77](https://img.ly/docs/cesdk/android/to-v1-77-ac6ca9/) - Learn what changed in v1.77 for Android — migrate your asset sources and branch on the new engine error codes. - [To v1.83](https://img.ly/docs/cesdk/android/to-v1-83-6ee1d1/) - Learn what changed in v1.83 for Android — the video timeline renders its tracks alone until you declare its header and buttons. --- ## 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 Template" description: "Apply template scenes to an existing scene with the CE.SDK Engine API for Android, preserving your page dimensions and design unit." platform: android url: "https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/" --- > 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/) > [Apply a Template](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) --- ```kotlin file=@cesdk_android_examples/engine-guides-apply-template/ApplyTemplate.kt reference-only import android.net.Uri import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.FontUnit import ly.img.engine.MimeType import ly.img.engine.SizeMode import java.io.File import java.nio.ByteBuffer import kotlin.math.abs data class ApplyTemplate( val uriWidth: Float, val uriHeight: Float, val switchedWidth: Float, val switchedHeight: Float, val stringWidth: Float, val stringHeight: Float, val serializedTemplateLength: Int, val previewPngData: ByteBuffer, ) suspend fun applyTemplate(engine: Engine): ApplyTemplate { val firstTemplateString = createTemplateString(engine = engine, headlineText = "Spring Sale") val secondTemplateString = createTemplateString(engine = engine, headlineText = "New Arrivals") val firstTemplateFile = File.createTempFile("spring-sale-template", ".imgly").apply { writeText(firstTemplateString) } val secondTemplateFile = File.createTempFile("new-arrivals-template", ".imgly").apply { writeText(secondTemplateString) } try { return applyTemplates( engine = engine, firstTemplateFile = firstTemplateFile, secondTemplateFile = secondTemplateFile, firstTemplateString = firstTemplateString, ) } finally { firstTemplateFile.delete() secondTemplateFile.delete() } } private suspend fun applyTemplates( engine: Engine, firstTemplateFile: File, secondTemplateFile: File, firstTemplateString: String, ): ApplyTemplate { val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL) val page = engine.scene.getPages().firstOrNull() ?: engine.block.create(DesignBlockType.Page).also { engine.block.appendChild(parent = scene, child = it) } val targetWidth = 1080F val targetHeight = 1920F engine.block.setFloat( block = scene, property = "scene/pageDimensions/width", value = targetWidth, ) engine.block.setFloat( block = scene, property = "scene/pageDimensions/height", value = targetHeight, ) engine.block.setWidthMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = page, value = targetWidth) engine.block.setHeight(block = page, value = targetHeight) val templateUri = Uri.fromFile(firstTemplateFile) engine.scene.applyTemplate(templateUri = templateUri) val appliedScene = requireNotNull(engine.scene.get()) { "No scene loaded after applying the template." } val appliedWidth = engine.block.getFloat( block = appliedScene, property = "scene/pageDimensions/width", ) val appliedHeight = engine.block.getFloat( block = appliedScene, property = "scene/pageDimensions/height", ) check(abs(appliedWidth - targetWidth) < 0.001F) { "Expected applied width $targetWidth but was $appliedWidth." } check(abs(appliedHeight - targetHeight) < 0.001F) { "Expected applied height $targetHeight but was $appliedHeight." } val alternativeTemplateUri = Uri.fromFile(secondTemplateFile) engine.scene.applyTemplate(templateUri = alternativeTemplateUri) val switchedScene = requireNotNull(engine.scene.get()) { "No scene loaded after switching templates." } val switchedWidth = engine.block.getFloat( block = switchedScene, property = "scene/pageDimensions/width", ) val switchedHeight = engine.block.getFloat( block = switchedScene, property = "scene/pageDimensions/height", ) check(abs(switchedWidth - targetWidth) < 0.001F) { "Expected switched width $targetWidth but was $switchedWidth." } check(abs(switchedHeight - targetHeight) < 0.001F) { "Expected switched height $targetHeight but was $switchedHeight." } val templateString = firstTemplateString engine.scene.applyTemplate(template = templateString) val stringScene = requireNotNull(engine.scene.get()) { "No scene loaded after applying the template string." } val stringWidth = engine.block.getFloat( block = stringScene, property = "scene/pageDimensions/width", ) val stringHeight = engine.block.getFloat( block = stringScene, property = "scene/pageDimensions/height", ) check(abs(stringWidth - targetWidth) < 0.001F) { "Expected string-applied width $targetWidth but was $stringWidth." } check(abs(stringHeight - targetHeight) < 0.001F) { "Expected string-applied height $targetHeight but was $stringHeight." } val previewPage = engine.scene.getPages().first() val previewPngData = engine.block.export( block = previewPage, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 360F, targetHeight = 640F), ) return ApplyTemplate( uriWidth = appliedWidth, uriHeight = appliedHeight, switchedWidth = switchedWidth, switchedHeight = switchedHeight, stringWidth = stringWidth, stringHeight = stringHeight, serializedTemplateLength = firstTemplateString.length, previewPngData = previewPngData, ) } private suspend fun createTemplateString( engine: Engine, headlineText: String, ): String { val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL) val page = engine.scene.getPages().firstOrNull() ?: engine.block.create(DesignBlockType.Page).also { engine.block.appendChild(parent = scene, child = it) } engine.block.setFloat( block = scene, property = "scene/pageDimensions/width", value = 600F, ) engine.block.setFloat( block = scene, property = "scene/pageDimensions/height", value = 600F, ) engine.block.setWidthMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = page, value = 600F) engine.block.setHeight(block = page, value = 600F) val headline = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = headline, text = headlineText) engine.block.setPositionX(block = headline, value = 72F) engine.block.setPositionY(block = headline, value = 96F) engine.block.setWidth(block = headline, value = 456F) engine.block.setHeight(block = headline, value = 120F) engine.block.appendChild(parent = page, child = headline) return engine.scene.saveToString(scene = scene) } ``` Apply template content to an existing scene with the Android Engine API while keeping your current page dimensions and design unit. Template content is automatically scaled to fit, so you can switch layouts without changing the canvas size. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-apply-template) Applying a template loads the template's content into the current scene while keeping the scene's design unit and page dimensions. The content is resized to fit those dimensions. This differs from `engine.scene.load(sceneUri=_)`, which replaces the entire scene, including its dimensions. This guide covers applying templates from a URI and from a serialized string, verifying that page dimensions stay fixed, and switching between templates on the same scene. ## When to Use Apply vs Load Use `engine.scene.applyTemplate(templateUri=_)` or `engine.scene.applyTemplate(template=_)` when you want to: - **Switch templates**: Let users preview different templates while keeping a consistent canvas size. - **Standardize output dimensions**: Generate content with fixed sizes, such as social-media formats or print sizes. - **Batch process with templates**: Apply different templates to a pre-configured scene without dimension drift. Use `engine.scene.load(sceneUri=_)` when you need the template's original dimensions. **Key distinction**: Loading replaces everything, including dimensions; applying keeps your dimensions and resizes the template's content to fit. ## Prepare Template Inputs The sample creates two small template scenes so the URI and string examples run without external storage. The helper builds a square template scene, adds a text block, and serializes it to a `.scene` string. ```kotlin highlight-android-create-template private suspend fun createTemplateString( engine: Engine, headlineText: String, ): String { val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL) val page = engine.scene.getPages().firstOrNull() ?: engine.block.create(DesignBlockType.Page).also { engine.block.appendChild(parent = scene, child = it) } engine.block.setFloat( block = scene, property = "scene/pageDimensions/width", value = 600F, ) engine.block.setFloat( block = scene, property = "scene/pageDimensions/height", value = 600F, ) engine.block.setWidthMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = page, value = 600F) engine.block.setHeight(block = page, value = 600F) val headline = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = headline, text = headlineText) engine.block.setPositionX(block = headline, value = 72F) engine.block.setPositionY(block = headline, value = 96F) engine.block.setWidth(block = headline, value = 456F) engine.block.setHeight(block = headline, value = 120F) engine.block.appendChild(parent = page, child = headline) return engine.scene.saveToString(scene = scene) } ``` Use those serialized template strings directly, or write them to temporary `.scene` files when the URI overload is the API under test. In your app, these values usually come from bundled files, app storage, or your backend. ```kotlin highlight-android-template-inputs val firstTemplateString = createTemplateString(engine = engine, headlineText = "Spring Sale") val secondTemplateString = createTemplateString(engine = engine, headlineText = "New Arrivals") val firstTemplateFile = File.createTempFile("spring-sale-template", ".imgly").apply { writeText(firstTemplateString) } val secondTemplateFile = File.createTempFile("new-arrivals-template", ".imgly").apply { writeText(secondTemplateString) } ``` ## Apply a Template from URI Create a scene and set the scene-level page dimensions you want to keep. `applyTemplate` reads these dimensions from `scene/pageDimensions/width` and `scene/pageDimensions/height`, then resizes the template content to fit them. ```kotlin highlight-android-setup val scene = engine.scene.create(designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.PIXEL) val page = engine.scene.getPages().firstOrNull() ?: engine.block.create(DesignBlockType.Page).also { engine.block.appendChild(parent = scene, child = it) } val targetWidth = 1080F val targetHeight = 1920F engine.block.setFloat( block = scene, property = "scene/pageDimensions/width", value = targetWidth, ) engine.block.setFloat( block = scene, property = "scene/pageDimensions/height", value = targetHeight, ) engine.block.setWidthMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(block = page, mode = SizeMode.ABSOLUTE) engine.block.setWidth(block = page, value = targetWidth) engine.block.setHeight(block = page, value = targetHeight) ``` Call `engine.scene.applyTemplate(templateUri=_)` with a URI to a `.scene` template file. The sample writes a temporary template file to keep the guide self-contained; the same method accepts any reachable `.scene` URI from app storage or an asset host. ```kotlin highlight-android-apply-from-uri val templateUri = Uri.fromFile(firstTemplateFile) engine.scene.applyTemplate(templateUri = templateUri) ``` ## Verify Preserved Dimensions Applying a template reloads the scene, so query the current scene again with `engine.scene.get()` before reading its scene-level page dimensions. The width and height match the values set during setup, confirming the template adopted your dimensions instead of its own. ```kotlin highlight-android-verify-dimensions val appliedScene = requireNotNull(engine.scene.get()) { "No scene loaded after applying the template." } val appliedWidth = engine.block.getFloat( block = appliedScene, property = "scene/pageDimensions/width", ) val appliedHeight = engine.block.getFloat( block = appliedScene, property = "scene/pageDimensions/height", ) check(abs(appliedWidth - targetWidth) < 0.001F) { "Expected applied width $targetWidth but was $appliedWidth." } check(abs(appliedHeight - targetHeight) < 0.001F) { "Expected applied height $targetHeight but was $appliedHeight." } ``` ## Template Switching Apply another template to the same scene. Each call replaces the content while preserving the page dimensions and design unit, which is the basis for a preview experience where users explore different templates without affecting their canvas size. ```kotlin highlight-android-template-switching val alternativeTemplateUri = Uri.fromFile(secondTemplateFile) engine.scene.applyTemplate(templateUri = alternativeTemplateUri) ``` ## Apply a Template from String For templates stored in a database or returned from an API, pass the serialized scene contents to `engine.scene.applyTemplate(template=_)` as a string instead of a URI. The example creates a serialized template to keep the sample self-contained; in your app, the string comes from your own storage. ```kotlin highlight-android-apply-from-string val templateString = firstTemplateString engine.scene.applyTemplate(template = templateString) ``` ## Troubleshooting ### No Scene Loaded `engine.scene.applyTemplate(templateUri=_)` and `engine.scene.applyTemplate(template=_)` require an existing scene. Create one first with `engine.scene.create()` or load one with `engine.scene.load(sceneUri=_)`; otherwise the call fails. ### Template Not Accessible Verify the template URI is reachable and valid. For remote URLs, check network connectivity; for bundled files, confirm the asset path resolves. ### Content Not Scaling as Expected Template content scales to fit the current scene-level page dimensions. Set `scene/pageDimensions/width` and `scene/pageDimensions/height` before applying the template so the content adjusts to the size you expect. ## API Reference | Method | Category | Purpose | | --------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------ | | `engine.scene.applyTemplate(templateUri=_)` | Scene | Apply a template from a URI, preserving current dimensions | | `engine.scene.applyTemplate(template=_)` | Scene | Apply a template from a serialized scene string, preserving current dimensions | | `engine.scene.create(designUnit=_, fontSizeUnit=_)` | Scene | Create a new scene as the target for template application | | `engine.scene.get()` | Scene | Read the active scene after template application | | `engine.scene.getPages()` | Scene | Read the scene's page blocks | | `engine.scene.load(sceneUri=_)` | Scene | Load a scene when you want to replace the current dimensions | | `engine.scene.saveToString(scene=_)` | Scene | Serialize a scene as a string | | `engine.block.create(blockType=DesignBlockType.Page)` | Block | Create a page block | | `engine.block.create(blockType=DesignBlockType.Text)` | Block | Create a text block for the template content | | `engine.block.appendChild(parent=_, child=_)` | Block | Add the page to the scene | | `engine.block.setFloat(block=_, property="scene/pageDimensions/width", value=_)` | Block | Set the scene-level page width | | `engine.block.setFloat(block=_, property="scene/pageDimensions/height", value=_)` | Block | Set the scene-level page height | | `engine.block.getFloat(block=_, property="scene/pageDimensions/width")` | Block | Read the scene-level page width | | `engine.block.getFloat(block=_, property="scene/pageDimensions/height")` | Block | Read the scene-level page height | | `engine.block.setWidthMode(block=_, mode=_)` | Block | Set the page width sizing mode | | `engine.block.setHeightMode(block=_, mode=_)` | Block | Set the page height sizing mode | | `engine.block.setWidth(block=_, value=_)` | Block | Set the page width | | `engine.block.setHeight(block=_, value=_)` | Block | Set the page height | | `engine.block.replaceText(block=_, text=_)` | Block | Populate the text block in the template | | `engine.block.setPositionX(block=_, value=_)` | Block | Position the template text horizontally | | `engine.block.setPositionY(block=_, value=_)` | Block | Position the template text vertically | ## Next Steps - [Templates Overview](https://img.ly/docs/cesdk/android/use-templates/overview-ae74e1/) — Understanding templates in CE.SDK - [Use Templates Programmatically](https://img.ly/docs/cesdk/android/use-templates/programmatic-9349f3/) — Comprehensive programmatic template workflows - [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) — Use the Engine directly without a prebuilt 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: "Generate From Template" description: "Generate finished designs from templates with the CE.SDK Engine API for Android by loading, populating variables, and exporting to images and PDFs." platform: android url: "https://img.ly/docs/cesdk/android/use-templates/generate-334e15/" --- > 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/) > [Generate From Template](https://img.ly/docs/cesdk/android/use-templates/generate-334e15/) --- ```kotlin file=@cesdk_android_examples/engine-guides-use-templates-generate/UseTemplatesGenerate.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.ExportOptions import ly.img.engine.FillType import ly.img.engine.FontUnit import ly.img.engine.MimeType import ly.img.engine.ShapeType import java.nio.ByteBuffer private val generatedTemplateVariableKeys = listOf("recipientName", "message") suspend fun useTemplatesGenerate( engine: Engine, assetBaseUri: Uri, ): UseTemplatesGenerateResult { val existingVariableKeys = engine.variable.findAll().toSet() val previousVariables = generatedTemplateVariableKeys .filter(existingVariableKeys::contains) .associateWith(engine.variable::get) return try { generatedTemplateVariableKeys .filter(existingVariableKeys::contains) .forEach(engine.variable::remove) generateFromTemplate(engine = engine, assetBaseUri = assetBaseUri) } finally { val currentVariableKeys = engine.variable.findAll().toSet() generatedTemplateVariableKeys .filter(currentVariableKeys::contains) .forEach(engine.variable::remove) previousVariables.forEach { (key, value) -> engine.variable.set(key = key, value = value) } } } private suspend fun generateFromTemplate( engine: Engine, assetBaseUri: Uri, ): UseTemplatesGenerateResult { val templateString = createGenerateTemplate(engine = engine, assetBaseUri = assetBaseUri) engine.scene.load( scene = templateString, overrideEditorConfig = true, waitForResources = true, ) val variableNames = engine.variable.findAll() val defaultRecipient = engine.variable.get(key = "recipientName") engine.variable.set(key = "recipientName", value = "Avery") engine.variable.set(key = "message", value = "Wishing you a wonderful year ahead!") val placeholders = engine.block.findAllPlaceholders() val imagePlaceholder = engine.block.findByName(name = "Image").first() val namedPlaceholder = engine.block.getName(imagePlaceholder) val replacementImageUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_2.jpg") .build() val imageFill = engine.block.getFill(imagePlaceholder) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = replacementImageUri, ) engine.block.resetCrop(block = imagePlaceholder) val page = engine.scene.getPages().first() engine.block.forceLoadResources(blocks = listOf(page)) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ).asReadOnlyBuffer() val scene = requireNotNull(engine.scene.get()) { "No scene loaded for export." } val pdfData = engine.block.export( block = scene, mimeType = MimeType.PDF, ).asReadOnlyBuffer() val records = listOf( "Jordan" to "Congratulations on the new home!", "Riley" to "Thank you for everything.", ) val batchExports = mutableListOf() for ((recipientName, message) in records) { engine.scene.load( scene = templateString, overrideEditorConfig = true, waitForResources = true, ) engine.variable.set(key = "recipientName", value = recipientName) engine.variable.set(key = "message", value = message) val recordPage = engine.scene.getPages().first() engine.block.forceLoadResources(blocks = listOf(recordPage)) batchExports += engine.block.export( block = recordPage, mimeType = MimeType.PNG, ).asReadOnlyBuffer() } return UseTemplatesGenerateResult( variableNames = variableNames, defaultRecipient = defaultRecipient, placeholderCount = placeholders.size, namedPlaceholder = namedPlaceholder, replacementImageUri = replacementImageUri, pngData = pngData, pdfData = pdfData, batchExports = batchExports, ) } private suspend fun createGenerateTemplate( engine: Engine, assetBaseUri: Uri, ): String { 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 = 600F) engine.block.appendChild(parent = scene, child = page) val pageFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = page, fill = pageFill) engine.block.setFillSolidColor( block = page, color = Color.fromHex("#F7F4EE"), ) val imageBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setName(imageBlock, name = "Image") engine.block.setShape(imageBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(imageBlock, value = 50F) engine.block.setPositionY(imageBlock, value = 140F) engine.block.setWidth(imageBlock, value = 320F) engine.block.setHeight(imageBlock, value = 320F) engine.block.setContentFillMode(imageBlock, mode = ContentFillMode.COVER) val imageFill = engine.block.createFill(FillType.Image) val initialImageUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_1.jpg") .build() engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = initialImageUri, ) engine.block.setFill(block = imageBlock, fill = imageFill) engine.block.setPlaceholderEnabled(block = imageBlock, enabled = true) engine.block.appendChild(parent = page, child = imageBlock) val greetingBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(greetingBlock, text = "Dear {{recipientName}},") engine.block.setTextFontSize(greetingBlock, fontSize = 44F) engine.block.setTextColor(greetingBlock, color = Color.fromHex("#25211D")) engine.block.setPositionX(greetingBlock, value = 420F) engine.block.setPositionY(greetingBlock, value = 180F) engine.block.setWidth(greetingBlock, value = 330F) engine.block.setHeight(greetingBlock, value = 90F) engine.block.appendChild(parent = page, child = greetingBlock) val messageBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(messageBlock, text = "{{message}}") engine.block.setTextFontSize(messageBlock, fontSize = 28F) engine.block.setTextColor(messageBlock, color = Color.fromHex("#5D534B")) engine.block.setPositionX(messageBlock, value = 420F) engine.block.setPositionY(messageBlock, value = 280F) engine.block.setWidth(messageBlock, value = 320F) engine.block.setHeight(messageBlock, value = 160F) engine.block.appendChild(parent = page, child = messageBlock) engine.variable.set(key = "recipientName", value = "Friend") engine.variable.set(key = "message", value = "Best wishes") engine.block.forceLoadResources(blocks = listOf(page)) return engine.scene.saveToString(scene = scene) } ``` Turn templates into finished designs with the Android Engine API. Load a template, populate its variables and image placeholders with your own data, and export the result to a PNG or PDF. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-use-templates-generate) Template generation transforms a reusable template into a finished design by populating data and exporting the result. Load a template with `engine.scene.load()`, replace its variables and placeholders with the Variable and Block APIs, then export it with `engine.block.export()`. This guide covers loading templates, populating variables, updating placeholder content, exporting to images and PDFs, and running batch generation. To merge a template into an existing scene while keeping its canvas size, see [Apply Templates](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/). For a deeper look at replacing content, see [Replace Content](https://img.ly/docs/cesdk/android/use-templates/replace-content-4c482b/). ## Loading Templates Load a template as the active scene before populating and exporting it. Pass a serialized string from your storage to `engine.scene.load(scene=_)`, use `engine.scene.load(sceneUri=_)` for a remote or bundled `.scene` file, or call `engine.scene.loadArchive(archiveUri=_)` for an archive that includes its assets. Set `overrideEditorConfig = true` to import the template's registered variables and settings. The example builds and serializes an inline template for a self-contained sample; production templates normally come from your storage or a template authoring workflow. ```kotlin highlight-android-generate-load engine.scene.load( scene = templateString, overrideEditorConfig = true, waitForResources = true, ) ``` ## Populating Variables Templates reference variables with `{{variableName}}` tokens in their text blocks. Setting a variable replaces every matching token throughout the scene. ### Discover Available Variables Use `engine.variable.findAll()` to list the variables a template registers, then read a current value with `engine.variable.get()`. Variable names are case-sensitive. ```kotlin highlight-android-generate-discover-variables val variableNames = engine.variable.findAll() val defaultRecipient = engine.variable.get(key = "recipientName") ``` ### Set Variable Values Assign a value with `engine.variable.set()`. The matching text tokens update immediately. ```kotlin highlight-android-generate-populate-variables engine.variable.set(key = "recipientName", value = "Avery") engine.variable.set(key = "message", value = "Wishing you a wonderful year ahead!") ``` ## Updating Placeholder Content Templates can contain placeholder blocks for images and other content. Discover them with `engine.block.findAllPlaceholders()`, or find a known slot by its stable name with `engine.block.findByName()`. ```kotlin highlight-android-generate-find-placeholders val placeholders = engine.block.findAllPlaceholders() val imagePlaceholder = engine.block.findByName(name = "Image").first() val namedPlaceholder = engine.block.getName(imagePlaceholder) ``` ### Update Image Placeholders Read the placeholder's fill with `engine.block.getFill()`, then update its image URI with `engine.block.setUri()`. The sample receives `assetBaseUri` from the host app so the URI can follow your bundled or self-hosted asset configuration. ```kotlin highlight-android-generate-update-image val replacementImageUri = assetBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_2.jpg") .build() val imageFill = engine.block.getFill(imagePlaceholder) engine.block.setUri( block = imageFill, property = "fill/image/imageFileURI", value = replacementImageUri, ) engine.block.resetCrop(block = imagePlaceholder) ``` ## Exporting to Images Find the populated page and export it to PNG with `engine.block.export()`. `ExportOptions` controls the target dimensions and format-specific options. ```kotlin highlight-android-generate-export-image val page = engine.scene.getPages().first() engine.block.forceLoadResources(blocks = listOf(page)) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ).asReadOnlyBuffer() ``` ## Exporting to PDF Export the scene block with `MimeType.PDF` to include every page in a multi-page document. ```kotlin highlight-android-generate-export-pdf val scene = requireNotNull(engine.scene.get()) { "No scene loaded for export." } val pdfData = engine.block.export( block = scene, mimeType = MimeType.PDF, ).asReadOnlyBuffer() ``` ## Batch Generation Workflows Drive one template with multiple data records. Serialize the template once, then reload it with `overrideEditorConfig = true` for each record before setting variables and exporting. Reloading restores the template's registered defaults so values do not leak between records. ```kotlin highlight-android-generate-batch val records = listOf( "Jordan" to "Congratulations on the new home!", "Riley" to "Thank you for everything.", ) val batchExports = mutableListOf() for ((recipientName, message) in records) { engine.scene.load( scene = templateString, overrideEditorConfig = true, waitForResources = true, ) engine.variable.set(key = "recipientName", value = recipientName) engine.variable.set(key = "message", value = message) val recordPage = engine.scene.getPages().first() engine.block.forceLoadResources(blocks = listOf(recordPage)) batchExports += engine.block.export( block = recordPage, mimeType = MimeType.PNG, ).asReadOnlyBuffer() } ``` ## Troubleshooting ### Template Fails to Load Confirm that the input contains valid scene data and is compatible with your SDK version. For URI-based templates, verify that the URI is reachable and use `loadArchive()` instead of `load()` for archive files. ### Variables Not Updating Ensure the key passed to `engine.variable.set()` exactly matches the `{{token}}` in the template. Use `engine.variable.findAll()` to inspect registered variable names. ### Export Returns Empty Output Confirm that every referenced asset is reachable and that all blocks are attached to the page hierarchy. Call `engine.block.forceLoadResources()` before exporting content with remote images or fonts. ### Image Placeholder Not Found Verify that the name passed to `engine.block.findByName()` matches the block name exactly. Use `engine.block.findAllPlaceholders()` to discover every placeholder in the scene. ## API Reference | Method | Category | Purpose | | ----------------------------------------------------- | -------- | -------------------------------------------- | | `engine.scene.load(scene=_)` | Scene | Load a template from a serialized string | | `engine.scene.load(sceneUri=_)` | Scene | Load a template from a remote or bundled URI | | `engine.scene.loadArchive(archiveUri=_)` | Scene | Load a template archive with embedded assets | | `engine.scene.saveToString(scene=_)` | Scene | Serialize a template for batch processing | | `engine.scene.get()` | Scene | Get the active scene block | | `engine.scene.getPages()` | Scene | Get the pages in the active scene | | `engine.variable.findAll()` | Variable | List every registered variable | | `engine.variable.set(key=_, value=_)` | Variable | Set and register a variable value | | `engine.variable.get(key=_)` | Variable | Read a variable value | | `engine.block.findByName(name=_)` | Block | Find blocks by name | | `engine.block.findAllPlaceholders()` | Block | Discover all placeholder blocks | | `engine.block.getName(block=_)` | Block | Read a block's stable name | | `engine.block.getFill(block=_)` | Block | Get a block's fill | | `engine.block.setUri(block=_, property=_, value=_)` | Block | Set an image fill URI | | `engine.block.resetCrop(block=_)` | Block | Recalculate crop after replacing an image | | `engine.block.export(block=_, mimeType=_, options=_)` | Block | Export a block to PNG, JPEG, or PDF | ## Next Steps - [Templates Overview](https://img.ly/docs/cesdk/android/use-templates/overview-ae74e1/) — Understand how templates work in CE.SDK - [Apply Templates](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) — Merge a template into an existing scene while preserving its dimensions - [Replace Content](https://img.ly/docs/cesdk/android/use-templates/replace-content-4c482b/) — Update variables and placeholders in depth - [Use Templates Programmatically](https://img.ly/docs/cesdk/android/use-templates/programmatic-9349f3/) — Build and personalize templates entirely in code --- ## 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: "Template Library" description: "Learn how to provide a set of predefined templates in the CreativeEditor SDK." platform: android url: "https://img.ly/docs/cesdk/android/use-templates/library-b3c704/" --- > 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/) > [Template Library](https://img.ly/docs/cesdk/android/use-templates/library-b3c704/) --- ```kotlin file=@cesdk_android_examples/engine-guides-use-templates-library/TemplateLibrary.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import ly.img.engine.AssetDefinition import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FindAssetsQuery suspend fun templateLibrary( engine: Engine, monitoringScope: CoroutineScope, assetBaseUri: Uri, ): TemplateLibraryResult = coroutineScope { val templateSourceId = "my.custom.templates" val hostedSourceId = "ly.img.templates" listOf(templateSourceId, hostedSourceId).forEach { sourceId -> if (sourceId in engine.asset.findAllSources()) { engine.asset.removeSource(sourceId = sourceId) } } val observedAddedSourceIds = mutableListOf() val observedRemovedSourceIds = mutableListOf() val sourceMonitorJobs = monitorTemplateSources( engine = engine, monitoringScope = monitoringScope, observedAddedSourceIds = observedAddedSourceIds, observedRemovedSourceIds = observedRemovedSourceIds, ) // Create the design scene that selected templates will be applied to. 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.asset.addLocalSource( sourceId = templateSourceId, supportedMimeTypes = emptyList(), applyAsset = { asset -> val templateUri = asset.meta?.get("uri")?.let(Uri::parse) ?: return@addLocalSource null engine.scene.applyTemplate(templateUri = templateUri) null }, ) 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 blankTemplateUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_blank_1.scene") .build() val blankThumbnailUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("thumbnails") .appendPath("cesdk_blank_1.png") .build() val templates = listOf( AssetDefinition( id = "business-card", label = mapOf("en" to "Business Card"), tags = mapOf("en" to listOf("business", "card")), groups = listOf("business"), meta = mapOf( "uri" to businessCardTemplateUri.toString(), "thumbUri" to businessCardThumbnailUri.toString(), ), ), AssetDefinition( id = "blank-canvas", label = mapOf("en" to "Blank Canvas"), tags = mapOf("en" to listOf("blank", "canvas")), groups = listOf("basics"), meta = mapOf( "uri" to blankTemplateUri.toString(), "thumbUri" to blankThumbnailUri.toString(), ), ), ) templates.forEach { template -> engine.asset.addAsset(sourceId = templateSourceId, asset = template) } engine.asset.assetSourceContentsChanged(sourceId = templateSourceId) val blankTemplate = engine.asset.fetchAsset( sourceId = templateSourceId, assetId = "blank-canvas", ) ?: error("Expected the blank template asset to be registered.") val createdBlock = engine.asset.applyAssetSourceAsset( sourceId = templateSourceId, asset = blankTemplate, ) check(createdBlock == null) val contentJsonUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("content.json") .build() val loadedHostedSourceId = engine.asset.addLocalSourceFromJSON( contentUri = contentJsonUri, matcher = null, ) val businessTemplates = engine.asset.findAssets( sourceId = templateSourceId, query = FindAssetsQuery(page = 0, perPage = 20, groups = listOf("business")), ) val allTemplates = engine.asset.findAssets( sourceId = templateSourceId, query = FindAssetsQuery(page = 0, perPage = 20), ) val groups = engine.asset.getGroups(sourceId = templateSourceId).orEmpty() val registeredSources = engine.asset.findAllSources() check(templateSourceId in registeredSources) check(loadedHostedSourceId in registeredSources) engine.asset.removeSource(sourceId = loadedHostedSourceId) val result = TemplateLibraryResult( templateSourceRegistered = templateSourceId in engine.asset.findAllSources(), businessTemplateIds = businessTemplates.assets.map { asset -> asset.id }, allTemplateCount = allTemplates.total, groups = groups, hostedSourceId = loadedHostedSourceId, hostedSourceRemoved = loadedHostedSourceId !in engine.asset.findAllSources(), observedAddedSourceIds = observedAddedSourceIds, observedRemovedSourceIds = observedRemovedSourceIds, sourceMonitorJobs = sourceMonitorJobs, pageCountAfterApply = engine.block.findByType(DesignBlockType.Page).size, ) result } fun monitorTemplateSources( engine: Engine, monitoringScope: CoroutineScope, observedAddedSourceIds: MutableList, observedRemovedSourceIds: MutableList, ): List { val addedJob = monitoringScope.launch(Dispatchers.Main.immediate) { engine.asset.onAssetSourceAdded().collect { sourceId -> observedAddedSourceIds += sourceId } } val removedJob = monitoringScope.launch(Dispatchers.Main.immediate) { engine.asset.onAssetSourceRemoved().collect { sourceId -> observedRemovedSourceIds += sourceId } } return listOf(addedJob, removedJob) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-use-templates-library/TemplateLibraryResult.kt reference-only import kotlinx.coroutines.Job data class TemplateLibraryResult( val templateSourceRegistered: Boolean, val businessTemplateIds: List, val allTemplateCount: Int, val groups: List, val hostedSourceId: String, val hostedSourceRemoved: Boolean, val observedAddedSourceIds: List, val observedRemovedSourceIds: List, val sourceMonitorJobs: List, val pageCountAfterApply: Int, ) ``` Configure and populate a Template Library with the Android Engine API so your app can offer predefined design templates that users browse, select, and apply to the current scene. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-use-templates-library) Templates are pre-designed scenes stored as assets within an asset source. Each template asset keeps the URI of its `.scene` file in `meta.uri`, and the source's apply callback loads that scene when the template is selected. This makes template sources different from image or sticker sources: applying a template updates the current scene instead of creating one block. This guide covers creating a custom template source, registering template assets, loading a hosted JSON catalog, querying templates, and managing template sources. ## Setup Create a design scene before applying templates. Applying a template keeps the current scene's design unit and page dimensions, fitting the template content into that target scene. ```kotlin highlight-android-setup // Create the design scene that selected templates will be applied to. 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) ``` ## Creating Custom Template Sources Register a local source with `engine.asset.addLocalSource()`. The `applyAsset` callback reads the selected asset's `meta.uri` value and applies that `.scene` file with `engine.scene.applyTemplate(templateUri=_)`. Return `null` because template application mutates the current scene instead of creating a new block. ```kotlin highlight-android-create-source engine.asset.addLocalSource( sourceId = templateSourceId, supportedMimeTypes = emptyList(), applyAsset = { asset -> val templateUri = asset.meta?.get("uri")?.let(Uri::parse) ?: return@addLocalSource null engine.scene.applyTemplate(templateUri = templateUri) null }, ) ``` Add template assets with `engine.asset.addAsset()`. Each asset needs a stable `id`, a localized `label`, and metadata for the `.scene` file plus the preview thumbnail. Use the same base URI that your Android integration uses for hosted or bundled assets. ```kotlin highlight-android-add-assets 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 blankTemplateUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_blank_1.scene") .build() val blankThumbnailUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("thumbnails") .appendPath("cesdk_blank_1.png") .build() val templates = listOf( AssetDefinition( id = "business-card", label = mapOf("en" to "Business Card"), tags = mapOf("en" to listOf("business", "card")), groups = listOf("business"), meta = mapOf( "uri" to businessCardTemplateUri.toString(), "thumbUri" to businessCardThumbnailUri.toString(), ), ), AssetDefinition( id = "blank-canvas", label = mapOf("en" to "Blank Canvas"), tags = mapOf("en" to listOf("blank", "canvas")), groups = listOf("basics"), meta = mapOf( "uri" to blankTemplateUri.toString(), "thumbUri" to blankThumbnailUri.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. - `tags` - Localized search terms. - `groups` - Categories used for filtering. - `meta.uri` - URI of the `.scene` file loaded by the apply callback. - `meta.thumbUri` - Thumbnail URI shown by clients that render the source. When a UI or custom picker selects a template, pass the asset to `engine.asset.applyAssetSourceAsset()`. CE.SDK invokes the source's callback and applies the template to the active scene. ```kotlin highlight-android-apply-template val createdBlock = engine.asset.applyAssetSourceAsset( sourceId = templateSourceId, asset = blankTemplate, ) ``` ## From Remote URI For production catalogs, keep template metadata in a hosted `content.json` file and register it with `engine.asset.addLocalSourceFromJSON()`. Android catalogs resolve `{{base_url}}` placeholders, not arbitrary relative URLs: for a catalog at `//content.json`, CE.SDK replaces `{{base_url}}` with `/`, so template and thumbnail entries should include the source folder, such as `{{base_url}}//templates/template.scene`. ```kotlin highlight-android-load-json-source val contentJsonUri = assetBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("content.json") .build() val loadedHostedSourceId = engine.asset.addLocalSourceFromJSON( contentUri = contentJsonUri, matcher = null, ) ``` Use `addLocalSource()` plus `addAsset()` when the source needs custom template application behavior. Use `addLocalSourceFromJSON()` when you want to register the catalog metadata from a remote JSON file. ## Querying Templates Programmatically Search a source with `engine.asset.findAssets()`. Filter by `groups`, search free text with `query`, and page through results with `page` and `perPage`. Read groups with `engine.asset.getGroups()` when your UI needs to build filters from the registered source. ```kotlin highlight-android-query-templates val businessTemplates = engine.asset.findAssets( sourceId = templateSourceId, query = FindAssetsQuery(page = 0, perPage = 20, groups = listOf("business")), ) val allTemplates = engine.asset.findAssets( sourceId = templateSourceId, query = FindAssetsQuery(page = 0, perPage = 20), ) val groups = engine.asset.getGroups(sourceId = templateSourceId).orEmpty() ``` The returned `FindAssetsResult` exposes the matching `assets`, the `currentPage`, the `nextPage` (`-1` when there is no further page), and the `total` count across all pages. ## Managing Template Sources List sources with `engine.asset.findAllSources()` and remove a source with `engine.asset.removeSource()`. This is useful when your app swaps template catalogs for a different workspace, brand, or user role. ```kotlin highlight-android-manage-sources val registeredSources = engine.asset.findAllSources() check(templateSourceId in registeredSources) check(loadedHostedSourceId in registeredSources) engine.asset.removeSource(sourceId = loadedHostedSourceId) ``` Subscribe to source lifecycle events when your app needs to refresh a picker or cache after a catalog changes. Launch the collectors on the main dispatcher before adding or removing sources, and cancel the returned jobs with the scope that owns the observer. ```kotlin highlight-android-monitor-sources fun monitorTemplateSources( engine: Engine, monitoringScope: CoroutineScope, observedAddedSourceIds: MutableList, observedRemovedSourceIds: MutableList, ): List { val addedJob = monitoringScope.launch(Dispatchers.Main.immediate) { engine.asset.onAssetSourceAdded().collect { sourceId -> observedAddedSourceIds += sourceId } } val removedJob = monitoringScope.launch(Dispatchers.Main.immediate) { engine.asset.onAssetSourceRemoved().collect { sourceId -> observedRemovedSourceIds += sourceId } } return listOf(addedJob, removedJob) } ``` ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Templates do not appear | The source ID is not registered or the catalog has no matching assets | Confirm the ID with `findAllSources()` and query the source with `findAssets()` | | Template selection does nothing | The source was loaded without an apply callback | Use `addLocalSource()` with `applyAsset` for sources that should apply templates | | Template fails to load | The asset is missing a valid `.scene` URI in `meta.uri` | Store a reachable `.scene` URI for this `applyTemplate(templateUri=_)` path | | Thumbnails do not load | `meta.thumbUri` is missing or unreachable | Store a reachable thumbnail URI next to each template asset | ## API Reference | Method | Description | | --- | --- | | `engine.scene.create()` | Create the scene that templates are applied to | | `engine.block.create(blockType=_)` | Create the page used by the setup scene | | `engine.block.setWidth(block=_, value=_)` | Set the setup page width | | `engine.block.setHeight(block=_, value=_)` | Set the setup page height | | `engine.block.appendChild(parent=_, child=_)` | Add the page to the scene | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_, applyAsset=_)` | Register a local template source with custom selection behavior | | `engine.scene.applyTemplate(templateUri=_)` | Apply a `.scene` template URI to the current scene | | `engine.asset.addAsset(sourceId=_, asset=_)` | Add a template definition to a local source | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notify listeners that a source's assets changed | | `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(contentUri=_, matcher=_)` | Register a source from a hosted JSON catalog | | `engine.asset.findAssets(sourceId=_, query=_)` | Query templates with filtering and pagination | | `engine.asset.getGroups(sourceId=_)` | Read the available groups from a source | | `engine.asset.findAllSources()` | List registered source IDs | | `engine.asset.removeSource(sourceId=_)` | Remove a registered source | | `engine.asset.onAssetSourceAdded()` | Observe source registration events | | `engine.asset.onAssetSourceRemoved()` | Observe source removal events | ## Next Steps - [Apply Templates](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) - Apply a template scene to the current design from a string or URI. - [Asset Sources](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) - Understand how asset sources organize and deliver content. - [Serve Assets](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) - Host your templates, scenes, and thumbnails for production. --- ## 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 browse, apply, and dynamically populate templates in CE.SDK to streamline design workflows." platform: android url: "https://img.ly/docs/cesdk/android/use-templates/overview-ae74e1/" --- > 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/) > [Use Templates Overview](https://img.ly/docs/cesdk/android/use-templates/overview-ae74e1/) --- Templates in CreativeEditor SDK (CE.SDK) are pre-designed layouts that serve as starting points for generating static designs, videos, or print-ready outputs. Templates can be used to produce a wide range of media, including images, PDFs, and videos. Instead of creating a design from scratch, you can use a template to quickly produce content by adapting pre-defined elements like text, images, and layout structures. Using templates offers significant advantages: faster content creation, consistent visual style, and scalable design workflows across many outputs. CE.SDK supports two modes of using templates: - **Fully Programmatic**: Generate content variations automatically by merging external data into templates without user intervention. - **User-Assisted**: Let users load a template, customize editable elements, and export the result manually. Template-based generation can be performed entirely on the client, entirely on a server, or in a hybrid setup where users interact with templates client-side before triggering automated server-side generation. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Output Formats When Using Templates When generating outputs from templates, CE.SDK supports: Templates are format-aware, allowing you to design once and export to multiple formats seamlessly. For example, a single marketing template could be used to produce a social media graphic, a printable flyer, and a promotional video, all using the same underlying design. --- ## 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: "Use Templates Programmatically" description: "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." platform: android url: "https://img.ly/docs/cesdk/android/use-templates/programmatic-9349f3/" --- > 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/) > [Programmatic](https://img.ly/docs/cesdk/android/use-templates/programmatic-9349f3/) --- ```kotlin file=@cesdk_android_examples/engine-guides-use-templates-programmatic/UseTemplatesProgrammatically.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.ExportOptions import ly.img.engine.FillType import ly.img.engine.FontUnit import ly.img.engine.MimeType import ly.img.engine.ShapeType import java.nio.ByteBuffer @Suppress("DEPRECATION") suspend fun useTemplatesProgrammatically( engine: Engine, templateBaseUri: Uri, ): UseTemplatesProgrammaticallyResult { 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 = 600F) engine.block.appendChild(parent = scene, child = page) val pageFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = page, fill = pageFill) engine.block.setFillSolidColor( block = page, color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.95F, a = 1F), ) engine.variable.set(key = "recipientName", value = "Template") engine.variable.set(key = "customMessage", value = "This is a template example.") val titleBlock = engine.block.create(DesignBlockType.Text) engine.block.setName(titleBlock, name = "title") engine.block.replaceText(titleBlock, text = "Hello, {{recipientName}}!") engine.block.setTextFontSize(titleBlock, fontSize = 48F) engine.block.setTextColor(titleBlock, color = Color.fromHex("#333333")) engine.block.setPositionX(titleBlock, value = 50F) engine.block.setPositionY(titleBlock, value = 50F) engine.block.setWidth(titleBlock, value = 700F) engine.block.setHeight(titleBlock, value = 80F) engine.block.appendChild(parent = page, child = titleBlock) val messageBlock = engine.block.create(DesignBlockType.Text) engine.block.setName(messageBlock, name = "message") engine.block.replaceText(messageBlock, text = "{{customMessage}}") engine.block.setTextFontSize(messageBlock, fontSize = 28F) engine.block.setTextColor(messageBlock, color = Color.fromHex("#4D4D4D")) engine.block.setPositionX(messageBlock, value = 50F) engine.block.setPositionY(messageBlock, value = 140F) engine.block.setWidth(messageBlock, value = 700F) engine.block.setHeight(messageBlock, value = 120F) engine.block.appendChild(parent = page, child = messageBlock) 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 = 50F) engine.block.setPositionY(imageBlock, value = 300F) engine.block.setWidth(imageBlock, value = 700F) engine.block.setHeight(imageBlock, value = 220F) engine.block.setContentFillMode(imageBlock, mode = ContentFillMode.COVER) val imageFill = engine.block.createFill(FillType.Image) val imageUri = templateBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_1.jpg") .build() engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = imageUri.toString(), ) 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(block = placeholderFill, enabled = true) } engine.block.setPlaceholderEnabled(block = imageBlock, enabled = true) val placeholderBlocks = engine.block.findAllPlaceholders() val placeholderBehaviorEnabled = engine.block.supportsPlaceholderBehavior(placeholderFill) && engine.block.isPlaceholderBehaviorEnabled(placeholderFill) val placeholderEnabled = engine.block.isPlaceholderEnabled(imageBlock) val variableNames = engine.variable.findAll() val currentRecipientName = engine.variable.get(key = "recipientName") val titleUsesVariables = engine.block.referencesAnyVariables(titleBlock) val mediaPlaceholder = engine.block.findByName(name = "hero-image").first() val mediaFill = runCatching { engine.block.getFill(mediaPlaceholder) } .getOrNull() ?.takeIf { fill -> engine.block.getType(fill) == FillType.Image.key } ?: engine.block.createFill(FillType.Image).also { fill -> engine.block.setFill(block = mediaPlaceholder, fill = fill) } val replacementImageUri = templateBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_2.jpg") .build() engine.block.setString( block = mediaFill, property = "fill/image/imageFileURI", value = replacementImageUri.toString(), ) engine.block.forceLoadResources(blocks = listOf(page)) val templateString = engine.scene.saveToString(scene = scene) val templateArchive = engine.scene.saveToArchive(scene = scene) val personalizedCards = mutableListOf() val recipients = listOf( "Avery" to "Congratulations on the launch.", "Jordan" to "Thanks for your work this quarter.", ) for ((recipientName, customMessage) in recipients) { engine.variable.set(key = "recipientName", value = recipientName) engine.variable.set(key = "customMessage", value = customMessage) val cardData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) personalizedCards += cardData } val resetExports = mutableListOf() val records = listOf( "Riley" to "Welcome to the team.", "Morgan" to "Great work on the campaign.", ) for ((recipientName, customMessage) in records) { engine.scene.load(scene = templateString, overrideEditorConfig = true, waitForResources = true) val recordPage = engine.scene.getPages().first() engine.variable.set(key = "recipientName", value = recipientName) engine.variable.set(key = "customMessage", value = customMessage) val recordData = engine.block.export(block = recordPage, mimeType = MimeType.PNG) resetExports += recordData } engine.variable.remove(key = "customMessage") val variablesAfterRemoval = engine.variable.findAll() val existingTemplateUri = templateBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_business_card_1.scene") .build() engine.scene.load(sceneUri = existingTemplateUri, overrideEditorConfig = true, waitForResources = true) val loadedTemplatePageCount = engine.scene.getPages().size UseTemplatesProgrammaticallyResult( templateString = templateString, templateArchive = templateArchive, variableNames = variableNames, currentRecipientName = currentRecipientName, titleUsesVariables = titleUsesVariables, placeholderCount = placeholderBlocks.size, placeholderBehaviorEnabled = placeholderBehaviorEnabled, placeholderEnabled = placeholderEnabled, personalizedCards = personalizedCards, resetExports = resetExports, variablesAfterRemoval = variablesAfterRemoval, loadedTemplatePageCount = loadedTemplatePageCount, ) } finally { engine.variable.findAll().forEach { key -> runCatching { engine.variable.remove(key) } } previousVariables.forEach(engine.variable::set) } } ``` ```kotlin file=@cesdk_android_examples/engine-guides-use-templates-programmatic/UseTemplatesProgrammaticallyResult.kt reference-only import java.nio.ByteBuffer data class UseTemplatesProgrammaticallyResult( val templateString: String, val templateArchive: ByteBuffer, val variableNames: List, val currentRecipientName: String, val titleUsesVariables: Boolean, val placeholderCount: Int, val placeholderBehaviorEnabled: Boolean, val placeholderEnabled: Boolean, val personalizedCards: List, val resetExports: List, val variablesAfterRemoval: List, val loadedTemplatePageCount: Int, ) ``` Automate template workflows with CE.SDK's Android Engine API for batch processing, personalization, and headless design generation. ![Personalized template card exported by the Android Engine sample](./assets/use-templates-programmatic-android.png) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-use-templates-programmatic) Templates are scenes with predefined structures that support dynamic content through variables and placeholders. This guide uses the Engine API directly, without opening the CE.SDK editor UI. You build a greeting card template from scratch, bind variables, mark swappable media, save it for reuse, batch-export personalized designs, and load an existing template. ## Creating Templates from Scratch Build a template by creating a scene with `engine.scene.create(...)`, then arranging blocks with `engine.block.create(blockType=_)` and `engine.block.appendChild(parent=_, child=_)`. ```kotlin highlight-android-create-template 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 = 600F) engine.block.appendChild(parent = scene, child = page) val pageFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = page, fill = pageFill) engine.block.setFillSolidColor( block = page, color = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.95F, a = 1F), ) engine.variable.set(key = "recipientName", value = "Template") engine.variable.set(key = "customMessage", value = "This is a template example.") val titleBlock = engine.block.create(DesignBlockType.Text) engine.block.setName(titleBlock, name = "title") engine.block.replaceText(titleBlock, text = "Hello, {{recipientName}}!") engine.block.setTextFontSize(titleBlock, fontSize = 48F) engine.block.setTextColor(titleBlock, color = Color.fromHex("#333333")) engine.block.setPositionX(titleBlock, value = 50F) engine.block.setPositionY(titleBlock, value = 50F) engine.block.setWidth(titleBlock, value = 700F) engine.block.setHeight(titleBlock, value = 80F) engine.block.appendChild(parent = page, child = titleBlock) val messageBlock = engine.block.create(DesignBlockType.Text) engine.block.setName(messageBlock, name = "message") engine.block.replaceText(messageBlock, text = "{{customMessage}}") engine.block.setTextFontSize(messageBlock, fontSize = 28F) engine.block.setTextColor(messageBlock, color = Color.fromHex("#4D4D4D")) engine.block.setPositionX(messageBlock, value = 50F) engine.block.setPositionY(messageBlock, value = 140F) engine.block.setWidth(messageBlock, value = 700F) engine.block.setHeight(messageBlock, value = 120F) engine.block.appendChild(parent = page, child = messageBlock) ``` The page uses a solid background fill. The title block contains `Hello, {{recipientName}}!` and the message block contains `{{customMessage}}`. These double-brace tokens define where text variables replace content at runtime. ## Working with Placeholders Use placeholders for content areas that your app can replace programmatically or expose as editable template slots. The sample receives `templateBaseUri` from the host app so image URIs follow your bundled or self-hosted asset configuration. It then creates a named image block, assigns an image fill, enables placeholder behavior on the fill when supported, and marks the graphic block as a placeholder. ```kotlin highlight-android-configure-placeholder 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 = 50F) engine.block.setPositionY(imageBlock, value = 300F) engine.block.setWidth(imageBlock, value = 700F) engine.block.setHeight(imageBlock, value = 220F) engine.block.setContentFillMode(imageBlock, mode = ContentFillMode.COVER) val imageFill = engine.block.createFill(FillType.Image) val imageUri = templateBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_1.jpg") .build() engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = imageUri.toString(), ) 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(block = placeholderFill, enabled = true) } engine.block.setPlaceholderEnabled(block = imageBlock, enabled = true) val placeholderBlocks = engine.block.findAllPlaceholders() ``` `engine.block.findAllPlaceholders()` returns the placeholder blocks in the current scene. For named slots, use `engine.block.findByName(name=_)` and update the block or its fill. ## Text Variables for Dynamic Content Variables drive text replacement throughout a template. Define them with `engine.variable.set(key=_, value=_)`; any text containing the matching `{{key}}` updates automatically. Read a current value with `engine.variable.get(key=_)`, list every key with `engine.variable.findAll()`, and check variable usage with `engine.block.referencesAnyVariables(block=_)`. ```kotlin highlight-android-manage-variables val variableNames = engine.variable.findAll() val currentRecipientName = engine.variable.get(key = "recipientName") val titleUsesVariables = engine.block.referencesAnyVariables(titleBlock) ``` ## Populating Template Content Updating a variable refreshes every text block that references it, so you do not need to find and edit individual text blocks. Map external data fields, such as JSON records or database rows, onto variable keys. For media slots, find the named placeholder block, reuse its existing image fill when it has one, or attach a new image fill before setting the replacement URI. ```kotlin highlight-android-populate-media val mediaPlaceholder = engine.block.findByName(name = "hero-image").first() val mediaFill = runCatching { engine.block.getFill(mediaPlaceholder) } .getOrNull() ?.takeIf { fill -> engine.block.getType(fill) == FillType.Image.key } ?: engine.block.createFill(FillType.Image).also { fill -> engine.block.setFill(block = mediaPlaceholder, fill = fill) } val replacementImageUri = templateBaseUri.buildUpon() .appendPath("ly.img.image") .appendPath("images") .appendPath("sample_2.jpg") .build() engine.block.setString( block = mediaFill, property = "fill/image/imageFileURI", value = replacementImageUri.toString(), ) ``` ## Saving Templates for Reuse Serialize a template to a portable string with `engine.scene.saveToString(scene=_)`. To bundle the template with its loaded assets, call `engine.scene.saveToArchive(scene=_)`. ```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) ``` The serialized string contains the scene structure, block properties, and variable definitions. Store it in a database, write it to disk, or send it to another service that can load CE.SDK scenes. ## Batch Processing with Templates Batch processing populates one template with many records. The sample updates variables for each recipient and exports the current page as a PNG. ```kotlin highlight-android-batch-processing val recipients = listOf( "Avery" to "Congratulations on the launch.", "Jordan" to "Thanks for your work this quarter.", ) for ((recipientName, customMessage) in recipients) { engine.variable.set(key = "recipientName", value = recipientName) engine.variable.set(key = "customMessage", value = customMessage) val cardData = engine.block.export( block = page, mimeType = MimeType.PNG, options = ExportOptions(targetWidth = 800F, targetHeight = 600F), ) personalizedCards += cardData } ``` `ExportOptions(targetWidth=_, targetHeight=_)` controls the output resolution. `engine.block.export(block=_, mimeType=_, options=_)` returns the rendered image data for storage, upload, or further processing. ## Data-Driven Workflows When population changes state you do not want to carry between records, reload the saved template string before each record. Pass `overrideEditorConfig = true` so variables and other persisted editor configuration from the saved template replace the current values before you apply that record's data and export. ```kotlin highlight-android-data-driven val records = listOf( "Riley" to "Welcome to the team.", "Morgan" to "Great work on the campaign.", ) for ((recipientName, customMessage) in records) { engine.scene.load(scene = templateString, overrideEditorConfig = true, waitForResources = true) val recordPage = engine.scene.getPages().first() engine.variable.set(key = "recipientName", value = recipientName) engine.variable.set(key = "customMessage", value = customMessage) val recordData = engine.block.export(block = recordPage, mimeType = MimeType.PNG) resetExports += recordData } ``` This pattern works for personalized certificates, greeting cards, and social media graphics generated from a single design. ## Managing Variables Variable keys are case-sensitive and persist with the scene. Remove a key with `engine.variable.remove(key=_)` when it is no longer needed. Text that still references the key keeps the literal `{{token}}`. ```kotlin highlight-android-remove-variable engine.variable.remove(key = "customMessage") val variablesAfterRemoval = engine.variable.findAll() ``` ## Loading Existing Templates Templates do not have to be built in code. Load a pre-built `.scene` template from a URI with `engine.scene.load(sceneUri=_)`. This replaces the current scene content with the template's pages and blocks. Pass `overrideEditorConfig = true` when the template's saved variables and editor configuration should replace the current engine state. ```kotlin highlight-android-load-existing val existingTemplateUri = templateBaseUri.buildUpon() .appendPath("ly.img.templates") .appendPath("templates") .appendPath("cesdk_business_card_1.scene") .build() engine.scene.load(sceneUri = existingTemplateUri, overrideEditorConfig = true, waitForResources = true) val loadedTemplatePageCount = engine.scene.getPages().size ``` For templates bundled with their assets, use `engine.scene.load(sceneUri=_)`. To merge a `.scene` template into the current scene instead of replacing it, use `engine.scene.applyTemplate(templateUri=_)`. ## API Reference | Method | Description | | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `engine.scene.create(designUnit=_, fontSizeUnit=_)` | Create a blank scene to build a template. | | `engine.scene.load(scene=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene from a serialized template string, optionally restoring saved editor configuration such as variables. | | `engine.scene.load(sceneUri=_, overrideEditorConfig=_, waitForResources=_)` | Load a scene or archive from a local or remote URI (content detected automatically) | | `engine.scene.loadArchive(archiveUri=_)` | Load a scene archive from a URI | | `engine.scene.applyTemplate(templateUri=_)` | Apply a `.scene` template to the current scene. | | `engine.scene.getPages()` | Return the pages in the current scene. | | `engine.scene.saveToString(scene=_)` | Serialize the scene to a portable string. | | `engine.scene.saveToArchive(scene=_)` | Save the scene and loaded assets as an archive. | | `engine.block.create(blockType=_)` | Create a new design block. | | `engine.block.createFill(fillType=_)` | Create a fill block. | | `engine.block.createShape(type=_)` | Create a shape block. | | `engine.block.appendChild(parent=_, child=_)` | Add a block to the scene hierarchy. | | `engine.block.setWidth(block=_, value=_)` | Set a block's width. | | `engine.block.setHeight(block=_, value=_)` | Set a block's 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.setName(block=_, name=_)` | Assign a stable name for later lookup. | | `engine.block.findByName(name=_)` | Find named blocks such as template slots. | | `engine.block.getType(block=_)` | Check the type of an existing block or fill. | | `engine.block.replaceText(block=_, text=_)` | Set text content, including `{{variable}}` tokens. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text size. | | `engine.block.setTextColor(block=_, color=_)` | Set text color. | | `engine.block.setFillSolidColor(block=_, color=_)` | Set a solid fill color. | | `engine.block.setString(block=_, property="fill/image/imageFileURI", value=_)` | Set an image fill URI. | | `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block. | | `engine.block.setFill(block=_, fill=_)` | Assign a fill to a block. | | `engine.block.getFill(block=_)` | Retrieve a block's current fill. | | `engine.block.setContentFillMode(block=_, mode=_)` | Control how media fills the graphic block. | | `engine.block.setPlaceholderEnabled(block=_, enabled=_)` | Mark a block as a placeholder. | | `engine.block.supportsPlaceholderBehavior(block=_)` | Check whether a block supports placeholder behavior. | | `engine.block.setPlaceholderBehaviorEnabled(block=_, enabled=_)` | Enable placeholder replacement behavior. | | `engine.block.findAllPlaceholders()` | Return all placeholder blocks in the current scene. | | `engine.block.referencesAnyVariables(block=_)` | Check whether a block references variables. | | `engine.block.forceLoadResources(blocks=_)` | Load referenced resources before archiving. | | `engine.block.export(block=_, mimeType=_, options=_)` | Export a block to image data. | | `engine.variable.set(key=_, value=_)` | Create or update a text variable. | | `engine.variable.get(key=_)` | Read a variable's current value. | | `engine.variable.findAll()` | List every variable key in the scene. | | `engine.variable.remove(key=_)` | Delete a variable from the scene. | ## Troubleshooting **Template loading failures:** Verify scene strings are correctly encoded and URIs are reachable. Use `try`/`catch` around loading calls to handle network, file, or parsing errors. **Variables not replacing text:** Variable keys inside `{{}}` must exactly match the keys passed to `engine.variable.set(key=_, value=_)`. Keys are case-sensitive. **Placeholders not updating:** Confirm the block is listed by `engine.block.findAllPlaceholders()` and check placeholder behavior support with `engine.block.supportsPlaceholderBehavior(block=_)`. **Export issues:** Confirm all required assets are reachable before exporting or archiving. Missing images or fonts can cause export failures. Check the block hierarchy; orphaned blocks that are not connected to the page tree do not appear in exports. ## Next Steps - [Create From Scratch](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) — Build reusable template structures block by block. - [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) — Define dynamic text elements populated at runtime. - [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Mark editable image, video, and text areas in a locked layout. - [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) — Generate personalized designs by merging external data into 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: "Replace Content" description: "Dynamically replace text, images, and placeholder content within templates using CE.SDK's placeholder and variable systems." platform: android url: "https://img.ly/docs/cesdk/android/use-templates/replace-content-4c482b/" --- > 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/) > [Replace Content](https://img.ly/docs/cesdk/android/use-templates/replace-content-4c482b/) --- ```kotlin file=@cesdk_android_examples/engine-guides-replace-content/ReplaceContent.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 suspend fun replaceContent(engine: Engine): ReplaceContentResult { val previousVariables = engine.variable.findAll().associateWith { key -> engine.variable.get(key) } return try { previousVariables.keys.forEach(engine.variable::remove) val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 1080F) engine.block.setHeight(page, value = 720F) 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.fromHex("#FFF5EDF8"), ) val headline = engine.block.create(DesignBlockType.Text) engine.block.setName(headline, name = "campaign-headline") engine.block.replaceText(headline, text = "Hello, {{first_name}}") engine.block.setPositionX(headline, value = 64F) engine.block.setPositionY(headline, value = 64F) engine.block.setWidth(headline, value = 540F) engine.block.setHeightMode(headline, mode = SizeMode.AUTO) engine.block.setTextFontSize(headline, fontSize = 24F) engine.block.setTextColor(headline, color = Color.fromHex("#FF1E1B2E")) engine.block.appendChild(parent = page, child = headline) val subtitle = engine.block.create(DesignBlockType.Text) engine.block.setName(subtitle, name = "campaign-subtitle") engine.block.replaceText(subtitle, text = "Launching in {{city}}") engine.block.setPositionX(subtitle, value = 64F) engine.block.setPositionY(subtitle, value = 260F) engine.block.setWidth(subtitle, value = 540F) engine.block.setHeightMode(subtitle, mode = SizeMode.AUTO) engine.block.setTextFontSize(subtitle, fontSize = 18F) engine.block.setTextColor(subtitle, color = Color.fromHex("#FF4B445E")) engine.block.appendChild(parent = page, child = subtitle) val imagePlaceholder = engine.block.create(DesignBlockType.Graphic) engine.block.setName(imagePlaceholder, name = "campaign-image") engine.block.setShape(imagePlaceholder, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(imagePlaceholder, value = 620F) engine.block.setPositionY(imagePlaceholder, value = 64F) engine.block.setWidth(imagePlaceholder, value = 396F) engine.block.setHeight(imagePlaceholder, value = 560F) engine.block.setContentFillMode(imagePlaceholder, mode = ContentFillMode.COVER) 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 = imagePlaceholder, fill = imageFill) if (engine.block.supportsPlaceholderBehavior(imageFill)) { engine.block.setPlaceholderBehaviorEnabled(imageFill, enabled = true) } engine.block.setPlaceholderEnabled(imagePlaceholder, enabled = true) engine.block.appendChild(parent = page, child = imagePlaceholder) val templateSceneString = engine.scene.saveToString(scene = scene) engine.scene.load(scene = templateSceneString) val namedPlaceholder = engine.block.findByName(name = "campaign-image").first() val namedPlaceholderName = engine.block.getName(namedPlaceholder) val placeholderNames = engine.block.findAllPlaceholders() .map { placeholder -> engine.block.getName(placeholder) } val placeholderFill = engine.block.getFill(namedPlaceholder) val imagePlaceholderSupportsBehavior = engine.block.supportsPlaceholderBehavior(placeholderFill) val imagePlaceholderEnabled = engine.block.isPlaceholderEnabled(namedPlaceholder) engine.variable.set(key = "first_name", value = "Alex") engine.variable.set(key = "city", value = "Berlin") val firstName = engine.variable.get(key = "first_name") engine.variable.set(key = "campaign_tag", value = "summer-2026") val variableNames = engine.variable.findAll() val campaignTag = engine.variable.get(key = "campaign_tag") engine.variable.remove(key = "campaign_tag") val campaignTagRemoved = "campaign_tag" !in engine.variable.findAll() check(campaignTag == "summer-2026") val replacementImageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg") val replacementFill = engine.block.getFill(namedPlaceholder) engine.block.setUri( block = replacementFill, property = "fill/image/imageFileURI", value = replacementImageUri, ) engine.block.resetCrop(namedPlaceholder) val replacedImageUri = engine.block.getUri( block = replacementFill, property = "fill/image/imageFileURI", ) val subtitleBlock = engine.block.findByName(name = "campaign-subtitle").first() engine.block.replaceText(subtitleBlock, text = "Built for Android audiences") val directSubtitleText = engine.block.getString(subtitleBlock, property = "text/text") val records = listOf( mapOf( "first_name" to "Alex", "city" to "Berlin", "image_uri" to "https://img.ly/static/ubq_samples/sample_2.jpg", ), mapOf( "first_name" to "Jordan", "city" to "Tokyo", "image_uri" to "https://img.ly/static/ubq_samples/sample_3.jpg", ), ) val generatedDesigns = mutableListOf() for (record in records) { engine.scene.load(scene = templateSceneString) engine.variable.set(key = "first_name", value = record.getValue("first_name")) engine.variable.set(key = "city", value = record.getValue("city")) val imageBlock = engine.block.findByName(name = "campaign-image").first() val fill = engine.block.getFill(imageBlock) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = Uri.parse(record.getValue("image_uri")), ) engine.block.resetCrop(imageBlock) val exportPage = engine.scene.getPages().first() // Preload resources so remote image fills and text glyphs are ready before export. engine.block.forceLoadResources(blocks = listOf(exportPage)) val pngBuffer = engine.block.export(exportPage, mimeType = MimeType.PNG) generatedDesigns += GeneratedTemplateDesign( label = record.getValue("first_name"), pngBuffer = pngBuffer, ) } ReplaceContentResult( namedPlaceholder = namedPlaceholderName, placeholderNames = placeholderNames, imagePlaceholderSupportsBehavior = imagePlaceholderSupportsBehavior, imagePlaceholderEnabled = imagePlaceholderEnabled, firstNameVariable = firstName, variableNamesAfterSet = variableNames, campaignTagRemoved = campaignTagRemoved, replacedImageUri = replacedImageUri, directSubtitleText = directSubtitleText, generatedDesigns = generatedDesigns, ) } finally { engine.variable.findAll().forEach { key -> runCatching { engine.variable.remove(key) } } previousVariables.forEach(engine.variable::set) } } ``` ```kotlin file=@cesdk_android_examples/engine-guides-replace-content/ReplaceContentResult.kt reference-only import android.net.Uri import java.nio.ByteBuffer data class ReplaceContentResult( val namedPlaceholder: String, val placeholderNames: List, val imagePlaceholderSupportsBehavior: Boolean, val imagePlaceholderEnabled: Boolean, val firstNameVariable: String, val variableNamesAfterSet: List, val campaignTagRemoved: Boolean, val replacedImageUri: Uri, val directSubtitleText: String, val generatedDesigns: List, ) data class GeneratedTemplateDesign( val label: String, val pngBuffer: ByteBuffer, ) ``` Dynamically replace content within templates using CE.SDK's placeholder and variable systems. Find placeholder blocks by name, update text using variables, and swap image sources programmatically. ![Generated Android template after replacing text variables and the image placeholder.](./assets/android.hero.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-replace-content) Template content replacement enables dynamic designs by swapping placeholder content programmatically. Templates contain blocks marked as placeholders that can be located by name or discovered in bulk for batch processing. Text replacement uses the variable system with `{{variableName}}` syntax, while images are updated by modifying fill properties. This guide covers how to find placeholder blocks, replace text using variables, swap image content, and build data-driven template workflows. It assumes you already have a template loaded — see [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) for the model behind variables and placeholders. ## Finding Placeholder Blocks Locate replaceable content with block discovery APIs. Use `findByName()` when you know the placeholder name, and keep those names stable across template revisions. ```kotlin highlight-android-find-by-name val namedPlaceholder = engine.block.findByName(name = "campaign-image").first() val namedPlaceholderName = engine.block.getName(namedPlaceholder) ``` ### Discover All Placeholders Use `findAllPlaceholders()` to discover every placeholder block in a template and iterate through them programmatically. ```kotlin highlight-android-find-all-placeholders val placeholderNames = engine.block.findAllPlaceholders() .map { placeholder -> engine.block.getName(placeholder) } ``` ### Query Placeholder State Graphic blocks keep their replaceable content in a fill, so placeholder behavior is queried on the fill from `getFill()`. The interactive placeholder flag stays on the containing block. ```kotlin highlight-android-query-placeholder-state val placeholderFill = engine.block.getFill(namedPlaceholder) val imagePlaceholderSupportsBehavior = engine.block.supportsPlaceholderBehavior(placeholderFill) val imagePlaceholderEnabled = engine.block.isPlaceholderEnabled(namedPlaceholder) ``` ## Text Variable Replacement Replace text content through CE.SDK's variable system. A text block containing `{{first_name}}` or another token updates automatically when you set the matching variable with `engine.variable.set()`. ```kotlin highlight-android-text-variables engine.variable.set(key = "first_name", value = "Alex") engine.variable.set(key = "city", value = "Berlin") val firstName = engine.variable.get(key = "first_name") ``` ### Managing Variables List every variable currently stored on the engine with `findAll()`, read a value with `get()`, and remove values that no longer apply with `remove()`. ```kotlin highlight-android-manage-variables engine.variable.set(key = "campaign_tag", value = "summer-2026") val variableNames = engine.variable.findAll() val campaignTag = engine.variable.get(key = "campaign_tag") engine.variable.remove(key = "campaign_tag") val campaignTagRemoved = "campaign_tag" !in engine.variable.findAll() ``` `findAll()` reports variables that have values in the current engine session. It does not scan unresolved `{{...}}` tokens from text blocks. ## Replacing Image Content Update an image placeholder by modifying the fill's image source. Get the fill block with `getFill()`, then set the new URI on the `fill/image/imageFileURI` property with `setUri()`. ```kotlin highlight-android-replace-image val replacementImageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg") val replacementFill = engine.block.getFill(namedPlaceholder) engine.block.setUri( block = replacementFill, property = "fill/image/imageFileURI", value = replacementImageUri, ) engine.block.resetCrop(namedPlaceholder) ``` Resetting the crop after swapping the image keeps the placeholder framing consistent when source images have different aspect ratios. ## Direct Text Replacement Replace the full text of a block without the variable system using `replaceText()`. This is useful when your app owns the exact final string and does not need reusable template tokens. ```kotlin highlight-android-direct-text-replacement val subtitleBlock = engine.block.findByName(name = "campaign-subtitle").first() engine.block.replaceText(subtitleBlock, text = "Built for Android audiences") ``` ## Data-Driven Template Workflows Build automated template population by iterating over data records. Load a fresh copy of the template for each record, update variables and placeholders, then export the page before processing the next record. ```kotlin highlight-android-data-driven val records = listOf( mapOf( "first_name" to "Alex", "city" to "Berlin", "image_uri" to "https://img.ly/static/ubq_samples/sample_2.jpg", ), mapOf( "first_name" to "Jordan", "city" to "Tokyo", "image_uri" to "https://img.ly/static/ubq_samples/sample_3.jpg", ), ) val generatedDesigns = mutableListOf() for (record in records) { engine.scene.load(scene = templateSceneString) engine.variable.set(key = "first_name", value = record.getValue("first_name")) engine.variable.set(key = "city", value = record.getValue("city")) val imageBlock = engine.block.findByName(name = "campaign-image").first() val fill = engine.block.getFill(imageBlock) engine.block.setUri( block = fill, property = "fill/image/imageFileURI", value = Uri.parse(record.getValue("image_uri")), ) engine.block.resetCrop(imageBlock) val exportPage = engine.scene.getPages().first() // Preload resources so remote image fills and text glyphs are ready before export. engine.block.forceLoadResources(blocks = listOf(exportPage)) val pngBuffer = engine.block.export(exportPage, mimeType = MimeType.PNG) generatedDesigns += GeneratedTemplateDesign( label = record.getValue("first_name"), pngBuffer = pngBuffer, ) } ``` Preload the page resources before export so remote image fills and text glyphs resolve before the offscreen render runs. ## Troubleshooting ### Block Not Found by Name Verify the exact name string matches what's set in the template. Names are case-sensitive. Use `getName()` to inspect existing block names. ### Variable Not Replacing Text Ensure the `{{variableName}}` token in the text block matches the key passed to `engine.variable.set()` exactly, including casing. ### Image Not Updating Confirm the block has an image fill by checking that `getFill()` returns a valid fill block. Verify the URI is reachable and properly formatted. ### Placeholder State Queries Return False For a graphic block, query `supportsPlaceholderBehavior()` on its fill from `getFill()`, not on the block. Text blocks are queried on the block directly. ## API Reference | Method | Description | |--------|-------------| | `engine.block.findByName(name=_)` | Find blocks by name identifier. | | `engine.block.getName(block=_)` | Get the name of a block. | | `engine.block.findAllPlaceholders()` | Discover all placeholder blocks in the scene. | | `engine.block.getFill(block=_)` | Get the fill block from a graphic block. | | `engine.block.supportsPlaceholderBehavior(block=_)` | Verify a fill or text block supports placeholder behavior. | | `engine.block.isPlaceholderEnabled(block=_)` | Check whether placeholder interaction is enabled on a block. | | `engine.variable.set(key=_, value=_)` | Set a text variable value for dynamic replacement. | | `engine.variable.get(key=_)` | Get the current value of a variable. | | `engine.variable.findAll()` | List variable names that currently have values in the engine session. | | `engine.variable.remove(key=_)` | Remove a variable value from the engine session. | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Set the image URI on an image fill. | | `engine.block.resetCrop(block=_)` | Reapply placeholder crop framing after swapping the image. | | `engine.block.replaceText(block=_, text=_)` | Replace text content directly. | | `engine.scene.load(scene=_)` | Load a serialized template scene for the next record. | | `engine.scene.getPages()` | Get the page blocks from the current scene. | | `engine.block.forceLoadResources(blocks=_)` | Resolve text and media resources before export. | | `engine.block.export(block=_, mimeType=MimeType.PNG)` | Export the populated page as a PNG. | ## Next Steps - [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) — Automate filling a template from structured data records. - [Product Variations](https://img.ly/docs/cesdk/android/automation/product-variations-f3349f/) — Generate multiple design variations from a single template. - [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) — Learn the template model behind variables and placeholders. - [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Configure placeholder behavior and controls 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: "User Interface" description: "Use CE.SDK’s customizable, production-ready UI or replace it entirely with your own interface." platform: android url: "https://img.ly/docs/cesdk/android/user-interface-5a089a/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/user-interface/overview-41101a/) - Use CE.SDK’s customizable, production-ready UI or replace it entirely with your own interface. - [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) - Customize the visual style of the editor UI, including themes, fonts, labels, and icons. - [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) - Control which features are available and how UI components behave, appear, or are arranged in the editor. - [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) - Extend the editor interface with custom components, panels, actions, and dialogs tailored to your workflow. - [Build Your Own UI](https://img.ly/docs/cesdk/android/user-interface/build-your-own-ui-fe7527/) - Build a completely custom Android editor UI using CE.SDK's direct Engine APIs and your own Compose controls. - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) - Localize the CE.SDK editor UI on Android by overriding Android string resources for the languages your app supports. - [Custom Error Messages](https://img.ly/docs/cesdk/android/user-interface/custom-error-messages-2e8555/) - Learn how to override CE.SDK's engine error dialogs with your own localized, customer-facing copy using the I18n API. - [UI Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) - Listen to UI events and trigger custom logic based on user interactions in the editor interface. --- ## 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 Captions" description: "Integrate automatic caption generation into your CE.SDK application using the Auto Caption plugin with pluggable speech-to-text providers." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ai-integration/auto-captions-73368c/" --- > 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-auto-captions-plugin/AutoCaptionsPluginSolution.kt reference-only import androidx.compose.runtime.Composable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.configuration.video.callback.onCreate 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.autoCaptions.AutoCaptionsPlugin import ly.img.editor.plugin.autoCaptions.TranscriptionOptions import ly.img.editor.plugin.autoCaptions.TranscriptionProvider import ly.img.editor.plugin.autoCaptions.gateway.GatewayTranscriptionProvider import okhttp3.Call import okhttp3.Callback import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.Response import java.io.File import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException // Add this composable to your NavHost. @Composable fun AutoCaptionsPluginSolution( license: String, gatewayApiKey: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration .remember(::VideoConfigurationBuilder) { onCreate = { // Demo scaffolding — not part of the lesson: open a sample clip so the canvas shows // footage behind the captions sheet and Generate Automatically has speech to transcribe. onCreate( createScene = { editorContext.engine.scene.createFromVideo( videoUri = editorContext.baseUri .buildUpon() .appendPath("ly.img.video") .appendPath("videos") .appendPath("pexels-kampus-production-8154913.mp4") .build(), ) }, ) } } .then(::AutoCaptionsPlugin) { provider = GatewayTranscriptionProvider(apiKey = gatewayApiKey) } }, onClose = onClose, ) } @Composable private fun AutoCaptionsTranscriptionOptionsSolution( license: String, gatewayApiKey: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::VideoConfigurationBuilder) .then(::AutoCaptionsPlugin) { provider = GatewayTranscriptionProvider(apiKey = gatewayApiKey) options = TranscriptionOptions( language = "en", maxLineLength = 30, maxLines = 2, ) } }, onClose = onClose, ) } /** * A minimal custom provider: post the audio to any speech-to-text service and return SRT text. */ class CustomTranscriptionProvider( private val httpClient: OkHttpClient = OkHttpClient(), ) : TranscriptionProvider { override val name = "My Speech-to-Text Service" override suspend fun transcribe( audio: File, mimeType: String, options: TranscriptionOptions, ): String { val url = "https://example.com/transcribe".toHttpUrl() .newBuilder() .apply { options.language?.let { addQueryParameter("language", it) } } .build() val request = Request.Builder() .url(url) // The file streams out in segments; reading it into memory first would defeat the point of // receiving one, since a scene's audio has no size limit. .post(audio.asRequestBody(mimeType.toMediaType())) .build() return suspendCancellableCoroutine { continuation -> val call = httpClient.newCall(request) // Tapping Cancel cancels the surrounding coroutine; aborting the call stops the request from // running on to its timeout. continuation.invokeOnCancellation { call.cancel() } call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) = continuation.resumeWithException(e) override fun onResponse( call: Call, response: Response, ) { response.use { if (!it.isSuccessful) { continuation.resumeWithException(IOException("Transcription failed: ${it.code}")) } else { // Convert your service's response to SRT here, and return an empty string // when it reports no speech. continuation.resume(it.body?.string().orEmpty()) } } } }, ) } } } @Composable private fun AutoCaptionsCustomProviderSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::VideoConfigurationBuilder) .then(::AutoCaptionsPlugin) { provider = CustomTranscriptionProvider() } }, onClose = onClose, ) } @Composable private fun AutoCaptionsGenerationCallbackSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration .remember(::VideoConfigurationBuilder) .then(::AutoCaptionsPlugin) { // The plugin still owns the Generate action and its cancellable progress state; only the // transcription is yours. It validates a provider at setup either way, so one is still // required even though a replaced generator never calls it. provider = CustomTranscriptionProvider() captionsGeneration = { // Replace this with your own pipeline: transcribe the scene's audible content — // editorContext.engine reads it — and serialize the cues as SRT or VTT, timed relative // to the page timeline. The fixed cue below stands in for that work. val srt = """ 1 00:00:00,000 --> 00:00:03,000 Captions from a custom pipeline """.trimIndent() // Returning null tells the user no speech was detected; any error thrown shows a // generic message and reaches the integrator's `onError`. if (srt.isBlank()) { null } else { withContext(Dispatchers.IO) { File.createTempFile("captions", ".srt", editorContext.activity.cacheDir) .apply { writeText(srt) } } } } } }, onClose = onClose, ) } ``` Generate captions automatically from spoken audio in video and audio blocks using CE.SDK's Auto Captions plugin. ![The Add Captions sheet in the Android video editor with the Generate Automatically action](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-auto-captions-plugin) The Auto Captions plugin transcribes the scene's audible content and creates styled, time-synced caption blocks from the result. It ships with a built-in provider that runs the ElevenLabs Scribe v2 speech-to-text model through the IMG.LY AI Gateway, and you can plug in any speech-to-text service by implementing the `TranscriptionProvider` interface. For creating and editing captions by hand, see [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/). The example builds on the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/)'s `VideoConfigurationBuilder`, whose dock already carries the captions button. The plugin's action lives in the Add Captions sheet, so an editor configuration of your own needs that button too, or there is no way to reach the action — see [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/). ## Using the Built-in UI Registering the plugin adds a primary **Generate Automatically** action to the Add Captions sheet, which opens from the captions dock button. Like the sheet's other starting points, it shows only while the page has no captions yet, and it stays disabled until the page holds audio or video to transcribe. Tapping **Generate Automatically** replaces the sheet's actions with a **Generating Captions** state and a **Cancel** button. Nothing reaches the scene until the transcription returns, which is what makes **Cancel** leave it untouched. The run belongs to the editor rather than the sheet, so dismissing the sheet to look at the timeline does not throw a long transcription away — reopening finds it still running, and a result that arrives while the sheet is closed still lands. On success the result is imported through the same path as an SRT or VTT file: captions arrive styled with the default preset and time-synced, and the whole run lands as a single undo step. The sheet then switches to the Edit Captions list, where you tap a caption to edit its text and use the keyboard action bar to split, merge, add, and delete — see [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/). The plugin transcribes the current page's audible content as a whole: every audio block and every video with an audio track. Muted blocks, blocks with their volume at zero, and videos without an audio track are skipped silently, and each block's cues are placed at its position on the page timeline — trimming and playback speed included. Where two sources overlap in time, the higher-ranked one wins: voiceover first, then video, then other audio. A source only claims the stretches it actually speaks for, not the whole clip it sits in, so a short voiceover over a long video no longer silences the video underneath it for the clip's full length. There is no per-block picker: the action always covers the whole page. ## Installing the Plugin Add the plugin to the module that hosts CE.SDK. It follows CE.SDK's unified versioning, so install the version that matches your editor version: ```groovy dependencies { implementation("ly.img:plugin-auto-captions:$UBQ_VERSION$") } ``` The plugin reaches its transcription service over the network. It needs no permission of its own — `android.permission.INTERNET` already merges in from the engine. The editor does not depend on the plugin. The plugin publishes its generator into the editor's state under a key of its own, and the Add Captions sheet reads that key and leaves **Generate Automatically** out when nothing is published — which is how the sheet offers the action without the editor carrying a transcription stack nobody asked for. Nothing captions-specific sits in the editor's own modules: the `CaptionsGenerator` alias belongs to the plugin, the sheet spells the function type inline, and the editor never names a class that would be missing whenever the plugin isn't installed. Returning `null` is how a generator reports that there was nothing to transcribe. ## Configuring the Built-in Provider Import the editor configuration APIs, the plugin, and the provider where you build your editor configuration: ```kotlin highlight-android-auto-captions-imports import androidx.compose.runtime.Composable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.configuration.video.callback.onCreate 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.autoCaptions.AutoCaptionsPlugin import ly.img.editor.plugin.autoCaptions.TranscriptionOptions import ly.img.editor.plugin.autoCaptions.TranscriptionProvider import ly.img.editor.plugin.autoCaptions.gateway.GatewayTranscriptionProvider import okhttp3.Call import okhttp3.Callback import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.Response import java.io.File import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException ``` Chain `AutoCaptionsPlugin` onto your editor configuration with `then` and give it a provider. The built-in `GatewayTranscriptionProvider` only needs an IMG.LY API key — the gateway handles provider routing, billing, and asset storage: ```kotlin highlight-android-auto-captions-basic-setup Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration .remember(::VideoConfigurationBuilder) { onCreate = { // Demo scaffolding — not part of the lesson: open a sample clip so the canvas shows // footage behind the captions sheet and Generate Automatically has speech to transcribe. onCreate( createScene = { editorContext.engine.scene.createFromVideo( videoUri = editorContext.baseUri .buildUpon() .appendPath("ly.img.video") .appendPath("videos") .appendPath("pexels-kampus-production-8154913.mp4") .build(), ) }, ) } } .then(::AutoCaptionsPlugin) { provider = GatewayTranscriptionProvider(apiKey = gatewayApiKey) } }, onClose = onClose, ) ``` You create and manage API keys in the IMG.LY Dashboard. Keys use the `sk_` prefix, and you control which AI models a key can access and its credit budget per key in the Dashboard. The provider also accepts a `gatewayUrl` for a custom gateway base URL and an `httpClient`, so you can route requests through your own OkHttp client — an interceptor that injects a header, or a proxy. Registering the plugin is the whole change: the starter kit's dock already opens the Add Captions sheet where the action appears. See [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/) for the full captions surface, including the dock button a configuration of your own has to add and the caption inspector buttons. > **Warning:** A missing provider fails at editor setup rather than the first time somebody taps **Generate Automatically**, so a checkout without a key does not reach the user as a generic error. ## Transcription Options Set `options` to control the transcription language and how the transcript is broken into subtitle cues: ```kotlin highlight-android-auto-captions-transcription-options .then(::AutoCaptionsPlugin) { provider = GatewayTranscriptionProvider(apiKey = gatewayApiKey) options = TranscriptionOptions( language = "en", maxLineLength = 30, maxLines = 2, ) } ``` | Option | Default | Purpose | | --- | --- | --- | | `language` | `null` | BCP-47 code of the spoken language, for example `"en"`, `"de"`, `"pt"`. `null` lets the provider detect the language automatically. | | `maxLineLength` | `37` | Maximum characters per subtitle line before the next word starts a new line. | | `maxLines` | `1` | Maximum lines per subtitle cue before the next words start a new cue. | The built-in provider honors both formatting limits; a custom provider is free to apply or ignore them. ## Implementing a Custom Transcription Provider Use any speech-to-text service by implementing the `TranscriptionProvider` interface. It requires a `name` string and a `transcribe` method that turns an audio file into SRT subtitle text: ```kotlin highlight-android-auto-captions-custom-provider /** * A minimal custom provider: post the audio to any speech-to-text service and return SRT text. */ class CustomTranscriptionProvider( private val httpClient: OkHttpClient = OkHttpClient(), ) : TranscriptionProvider { override val name = "My Speech-to-Text Service" override suspend fun transcribe( audio: File, mimeType: String, options: TranscriptionOptions, ): String { val url = "https://example.com/transcribe".toHttpUrl() .newBuilder() .apply { options.language?.let { addQueryParameter("language", it) } } .build() val request = Request.Builder() .url(url) // The file streams out in segments; reading it into memory first would defeat the point of // receiving one, since a scene's audio has no size limit. .post(audio.asRequestBody(mimeType.toMediaType())) .build() return suspendCancellableCoroutine { continuation -> val call = httpClient.newCall(request) // Tapping Cancel cancels the surrounding coroutine; aborting the call stops the request from // running on to its timeout. continuation.invokeOnCancellation { call.cancel() } call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) = continuation.resumeWithException(e) override fun onResponse( call: Call, response: Response, ) { response.use { if (!it.isSuccessful) { continuation.resumeWithException(IOException("Transcription failed: ${it.code}")) } else { // Convert your service's response to SRT here, and return an empty string // when it reports no speech. continuation.resume(it.body?.string().orEmpty()) } } } }, ) } } } ``` The method receives the audio as a `File` staged in the editor's cache directory, with its MIME type — `audio/mp4` for the AAC track extracted from a video, and for a standalone audio block the source file's own type, commonly `audio/mpeg` or `audio/wav` — plus the `TranscriptionOptions` you configured. Stream the file rather than reading it into memory: a scene's audio has no size limit, and the file is what keeps a long recording off the heap. The editor deletes it once generation ends, so don't hold on to it past the call. Return an SRT-formatted string with timings relative to the start of the audio, or an empty string when no speech was detected. Any exception you throw surfaces as a generation failure in the editor and is passed to your `onError` callback, and `name` identifies the provider in the failure log. The surrounding coroutine is cancelled when the user taps **Cancel**, so keep implementations cooperatively cancellable — the sample aborts its in-flight call from `invokeOnCancellation`. Pass your provider to the plugin in place of the built-in one: ```kotlin highlight-android-auto-captions-use-custom-provider .then(::AutoCaptionsPlugin) { provider = CustomTranscriptionProvider() } ``` ## Full Custom Generation The plugin's transcription step is a `CaptionsGenerator` you can replace. Do that to own the whole pipeline — not just the speech-to-text call — while keeping the editor's Generate action and its cancellable progress state: ```kotlin highlight-android-auto-captions-generation-callback .then(::AutoCaptionsPlugin) { // The plugin still owns the Generate action and its cancellable progress state; only the // transcription is yours. It validates a provider at setup either way, so one is still // required even though a replaced generator never calls it. provider = CustomTranscriptionProvider() captionsGeneration = { // Replace this with your own pipeline: transcribe the scene's audible content — // editorContext.engine reads it — and serialize the cues as SRT or VTT, timed relative // to the page timeline. The fixed cue below stands in for that work. val srt = """ 1 00:00:00,000 --> 00:00:03,000 Captions from a custom pipeline """.trimIndent() // Returning null tells the user no speech was detected; any error thrown shows a // generic message and reaches the integrator's `onError`. if (srt.isBlank()) { null } else { withContext(Dispatchers.IO) { File.createTempFile("captions", ".srt", editorContext.activity.cacheDir) .apply { writeText(srt) } } } } } ``` The generator is a plain `suspend () -> File?` that you assign inside the plugin's configuration block, so it closes over the plugin's `editorContext` — `editorContext.engine` reads the scene's audio and video content, and `editorContext.activity.cacheDir` gives you somewhere to write. The skeleton above returns a fixed cue instead. It must return an SRT or VTT `File` with cue timings relative to the page timeline. The editor owns the UI around it: it shows the same **Generate Automatically** action and busy state, imports the returned file — replacing any existing captions — and deletes the file afterwards. Return `null` when there is nothing to transcribe; anything you throw reaches your `onError` callback behind the generic message. If your pipeline signals that deeper in its call stack, catch it there and return `null` — only what this callback returns reaches the editor. The editor cancels it when the user taps **Cancel**. The plugin checks for a `provider` when the editor is configured, whichever generator is in place, so keep setting one even when a replacement never calls it. ## Error Handling Generation failures reach the user as a toast from the editor rather than a dialog the sheet owns, because a run outlives the sheet and a failure can land while it is closed: - **No speech**: when the page has nothing audible or the transcription comes back empty, the message reads "No speech was detected in the audio." - **Any other error**: transport and service errors show "Something went wrong while generating captions. Please try again." - **Cancellation**: cancelling says nothing — the sheet returns to its Add Captions state and the scene stays untouched. When several blocks are transcribed, one block's failure doesn't sink the others; an error surfaces only when nothing was produced at all, so a partial run succeeds without telling the user which block dropped out. The message the user sees stays generic, so the cause is handed to your `onError` callback — the same channel the rest of the captions sheet reports through, which puts it in your crash reporting rather than only on an attached device. The plugin also logs per-block failures under the `AutoCaptions` tag, naming the provider on transcription errors. ## Troubleshooting - **Generate Automatically doesn't appear**: the editor looks for the plugin at runtime and leaves the action out when it isn't there. Add the `ly.img:plugin-auto-captions` dependency, chain `AutoCaptionsPlugin` onto your editor configuration with `then`, and make sure the dock carries `Dock.Button.rememberCaptions()` so the Add Captions sheet can be opened. - **Generate Automatically is disabled**: the current page has no audio or video content to transcribe — blocks on other pages don't count. Add a video or audio block to the page you're editing. - **No-speech message despite spoken content**: muted blocks, blocks with their volume at zero, and videos without audio tracks are skipped. Check the mute and volume state of the blocks that carry the speech. - **Generation fails with the generic message**: verify the device can reach the gateway and that the API key is valid. Your `onError` callback receives the underlying failure, and the log under the `AutoCaptions` tag names the provider. - **Generated captions appear unstyled**: default styling comes from the `ly.img.caption.presets` asset source. Register it as shown in [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/). ## API Reference | API | Category | Purpose | | --- | --- | --- | | `EditorConfiguration.remember(builder=_)` | Plugin registration | Creates the editor configuration the plugin is chained onto. | | `then(builderFactory=::AutoCaptionsPlugin, builder=_)` | Plugin registration | Adds the Generate Automatically action to the Add Captions sheet, backed by the configured provider. | | `GatewayTranscriptionProvider(apiKey=_, gatewayUrl=_, httpClient=_)` | Provider | Built-in provider running ElevenLabs Scribe v2 through the IMG.LY AI Gateway. | | `TranscriptionOptions(language=_, maxLineLength=_, maxLines=_)` | Provider | Language and subtitle formatting options passed to the provider. | | `TranscriptionProvider.transcribe(audio=_, mimeType=_, options=_)` | Provider contract | Turns a staged audio `File` into SRT subtitle text. | | `AutoCaptionsPlugin.captionsGeneration` | Plugin registration | The `suspend () -> File?` the Generate action runs; replace it to source captions yourself. Return `null` when there is nothing to transcribe. | | `Dock.Button.rememberCaptions()` | Dock | Dock button that opens the Add Captions sheet, already present in the video starter kit's dock. | ### Related types - `TranscriptionProvider` — the interface a speech-to-text backend implements. - `CaptionsGenerator` — the type behind the Generate action; a `typealias` for `suspend () -> File?`, in `ly.img.editor.plugin.autoCaptions`. Return `null` to tell the user no speech was detected. ## Next Steps - [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/) — Create, import, edit, and style captions in the editor - [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) — Build captions programmatically with the Engine APIs - [Update Caption Presets](https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/) — Extend the caption style presets with custom styles using content.json updates - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — Configure the dock area to show or hide tools, panels, or quick access actions --- ## 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: "Appearance" description: "Customize the visual style of the editor UI, including themes, fonts, labels, and icons." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) --- --- ## Related Pages - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) - Customize the Android editor theme mode and keep custom Compose UI aligned with the editor palette. - [Custom Labels](https://img.ly/docs/cesdk/android/user-interface/appearance/custom-labels-7794f7/) - Override CE.SDK editor UI text by replacing Android string resources in your app. - [Icons](https://img.ly/docs/cesdk/android/user-interface/appearance/icons-679e32/) - Replace or customize the editor’s icons to align with your brand or application design. - [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) - Configure custom Android overlays for loading, confirmation, cancellation, and other transient editor 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: "Custom Labels" description: "Override CE.SDK editor UI text by replacing Android string resources in your app." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/appearance/custom-labels-7794f7/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) > [Custom Labels](https://img.ly/docs/cesdk/android/user-interface/appearance/custom-labels-7794f7/) --- Customize CE.SDK editor labels on Android by overriding the string resources that the editor UI reads from your app package. CE.SDK's Android editor UI uses Android string resources for labels, button text, panel titles, dialogs, and accessibility descriptions. To change a label, add a string with the same resource name to your app module's `res/values/strings.xml`. These overrides apply wherever the CE.SDK editor UI uses that resource, including ready-made surfaces such as the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). They are best suited for product terminology, brand wording, and small copy refinements. ## Understanding Custom Labels Custom labels let you change individual pieces of UI text while keeping the rest of the editor language unchanged. For example, you can change visible dock labels such as "Elements" to "Graphics", "Images" to "Photos", or "Add Page" to "New Page" without replacing the full localization file. For icon-only controls, such as the top editor bar, the same resource override changes the accessibility label instead of rendering visible text next to the icon. Android resolves these labels through the normal resource merge step. Your app module's resource value takes precedence over the value shipped by the CE.SDK editor library, so the override is packaged with your APK or AAB. ## Android Resource Key Structure Editor string resource names use a `ly_img_editor_` prefix followed by the UI area and element name: | Pattern | Example | Purpose | | --- | --- | --- | | `ly_img_editor_navigation_bar_button_*` | `ly_img_editor_navigation_bar_button_export` | Navigation bar icon accessibility labels | | `ly_img_editor_dock_button_*` | `ly_img_editor_dock_button_elements` | Dock buttons | | `ly_img_editor_asset_library_title_*` | `ly_img_editor_asset_library_title_images` | Asset library tabs and titles | | `ly_img_editor_canvas_menu_button_*` | `ly_img_editor_canvas_menu_button_delete` | Canvas menu actions | | `ly_img_editor_pages_view_mode_dock_button_*` | `ly_img_editor_pages_view_mode_dock_button_add_page` | Page management actions | Use the public CE.SDK Android source package path as the source of truth for built-in English labels: [sources/editor-core/src/main/res/values/strings.xml](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values/strings.xml). ## Override Labels in App Resources Add only the resources you want to replace to your app module. Keep the same resource names and provide your custom values: ```xml Graphics Graphics Photos Photos Photos ``` You do not need to copy the complete CE.SDK resource file. Missing keys continue to use the defaults from the editor library. ## Override Labels for Localized UIs Android resolves resources from the directory that best matches the current locale. CE.SDK ships locale-specific resources such as `values-de`, so an override in only `res/values/strings.xml` changes the default language but does not replace every localized CE.SDK default. When you customize labels for a localized UI, add the same resource names to each matching locale directory in your app: ```xml Fotos Fotos Fotos ``` Use `res/values/strings.xml` for default wording and locale-specific directories, such as `res/values-de/strings.xml`, for language-specific wording. ## Customize Navigation Accessibility Labels The built-in top editor bar uses icon-only buttons. Navigation bar resources such as `ly_img_editor_navigation_bar_button_export`, `ly_img_editor_navigation_bar_button_close_editor`, `ly_img_editor_navigation_bar_button_undo`, `ly_img_editor_navigation_bar_button_redo`, and `ly_img_editor_navigation_bar_button_toggle_preview_mode` are used as accessibility content descriptions for those icons, not as visible button text. Override them when you want screen readers and UI automation to use product-wide terminology such as "Download" instead of "Export" or "Close" instead of "Back": ```xml Download Close Revert Restore View Mode ``` Keep these labels short because they describe compact icon buttons. They do not visibly appear in the top editor bar unless you build a custom navigation surface that renders text labels. ## Customize Dock and Library Labels Dock labels and asset library titles help users find content categories. Override both the dock button and the matching library title when you want the terminology to stay consistent after users open the library: ```xml Graphics Graphics Photos Photos Photos Typography Typography ``` Some asset labels are resolved from asset IDs. For those labels, CE.SDK looks for resource names such as `ly_img_editor_asset_label_fade`, then falls back to the label provided by the asset source. ## Customize Page and Action Labels Page and action labels appear in page management controls, canvas menus, and inspector actions. Use the same terminology across related actions. The stock canvas menu uses icon-only buttons, so `ly_img_editor_canvas_menu_button_delete` and `ly_img_editor_canvas_menu_button_duplicate` set accessibility content descriptions; page-mode and inspector resources provide visible action text. ```xml New Page Remove Copy Remove Copy Remove Copy ``` If the same concept appears in more than one editor surface, override all related resources so users do not see mixed wording. ## Discovering Available Labels Use the default resource files as the source of truth for available Android labels: - English labels: [sources/editor-core/src/main/res/values/strings.xml](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values/strings.xml) - German labels: [sources/editor-core/src/main/res/values-de/strings.xml](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values-de/strings.xml) - Plugin labels: each plugin can ship its own `res/values/strings.xml`, for example [background removal](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/plugin-background-removal/src/main/res/values/strings.xml) uses `ly_img_plugin_background_removal_dock_button` Search those files for the visible text first, then copy the matching resource name into your app module. The surrounding prefix usually identifies where the label appears, such as `navigation_bar`, `dock`, `asset_library`, `canvas_menu`, `inspector_bar`, or `dialog`. ## Best Practices **Override Only What You Need** Keep your app's resource file small and intentional: ```xml Photos Photos Photos ``` **Keep Related Labels Consistent** When you change a term, check related surfaces for the same concept. For example, if you rename "Images" to "Photos", update the dock button, library title, and image section label together. **Test Labels in Context** Long labels can overflow compact buttons and sheets, and long accessibility descriptions can become noisy for assistive technology. Validate visible labels on the smallest device size you support, and prefer one or two words for dock buttons, page actions, and navigation icon descriptions. **Document Your Overrides** Keep a small comment or team note that explains why each override exists. This makes future CE.SDK updates easier to review when new default resources are added. ## Custom Labels vs. Full Localization Custom labels change selected strings for the language your app is already using. Use them when you want brand-specific wording or a few targeted copy changes. Use [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) when you need complete language coverage. For a new language, copy the default resource file into a locale-specific directory such as `res/values-fr/strings.xml` and translate the full set of strings. ## Runtime Considerations Android resource overrides are resolved from packaged app resources. They are not a runtime translation dictionary, so changing labels for different brands, tenants, or user roles usually means one of these approaches: - Package product-specific resource values per app flavor. - Use locale-specific resource directories for language-specific wording. - Build custom Compose UI for the surface that needs runtime text changes. ## Resource Reference | Resource | Default text | Use when | | --- | --- | --- | | `ly_img_editor_navigation_bar_button_export` | Export | Set the export icon content description | | `ly_img_editor_navigation_bar_button_close_editor` | Back | Set the editor close/back icon content description | | `ly_img_editor_navigation_bar_button_undo` | Undo | Set the undo icon content description | | `ly_img_editor_navigation_bar_button_redo` | Redo | Set the redo icon content description | | `ly_img_editor_navigation_bar_button_toggle_preview_mode` | Toggle Preview | Set the preview mode icon content description | | `ly_img_editor_dock_button_elements` | Elements | Rename the elements dock button | | `ly_img_editor_dock_button_images` | Images | Rename the images dock button | | `ly_img_editor_dock_button_text` | Text | Rename the text dock button | | `ly_img_editor_asset_library_title_elements` | Elements | Rename the elements library title | | `ly_img_editor_asset_library_title_images` | Images | Rename the images library title | | `ly_img_editor_asset_library_section_images` | Images | Rename image sections inside the asset library | | `ly_img_editor_asset_library_title_text` | Text | Rename the text library title | | `ly_img_editor_pages_view_mode_dock_button_add_page` | Add Page | Rename the add-page action | | `ly_img_editor_pages_view_mode_dock_button_delete` | Delete | Rename the page-mode delete action | | `ly_img_editor_pages_view_mode_dock_button_duplicate` | Duplicate | Rename the page-mode duplicate action | | `ly_img_editor_canvas_menu_button_delete` | Delete | Set the canvas-menu delete icon content description | | `ly_img_editor_canvas_menu_button_duplicate` | Duplicate | Set the canvas-menu duplicate icon content description | | `ly_img_editor_inspector_bar_button_delete` | Delete | Rename the inspector delete action | | `ly_img_editor_inspector_bar_button_duplicate` | Duplicate | Rename the inspector duplicate action | ## Next Steps - [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) - Manage complete language resources for the editor UI - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) - Customize the visual appearance of the interface - [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) - Extend the editor with custom panels, actions, and dialogs --- ## 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: "Icons" description: "Replace or customize the editor’s icons to align with your brand or application design." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/appearance/icons-679e32/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) > [Icons](https://img.ly/docs/cesdk/android/user-interface/appearance/icons-679e32/) --- ```kotlin file=@cesdk_android_examples/editor-guides-appearance-icons/IconsEditorSolution.kt reference-only import android.widget.Toast import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberImagesLibrary import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Replace import ly.img.editor.guides.icons.BrandIcons import ly.img.editor.guides.icons.brandicons.BrandSpark import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.ShapeType import ly.img.engine.Color as EngineColor // Add this composable to your NavHost @Composable fun IconsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { if (editorContext.engine.scene.get() == null) { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(page, value = 1080F) editorContext.engine.block.setHeight(page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) val block = editorContext.engine.block.create(DesignBlockType.Graphic) editorContext.engine.block.setShape( block, shape = editorContext.engine.block.createShape(ShapeType.Rect), ) val fill = editorContext.engine.block.createFill(FillType.Color) editorContext.engine.block.setFill(block, fill = fill) editorContext.engine.block.setFillSolidColor( block = block, color = EngineColor.fromRGBA(r = 0.86F, g = 0.9F, b = 1F, a = 1F), ) editorContext.engine.block.setWidth(block, value = 420F) editorContext.engine.block.setHeight(block, value = 420F) editorContext.engine.block.setPositionX(block, value = 330F) editorContext.engine.block.setPositionY(block, value = 330F) editorContext.engine.block.appendChild(parent = page, child = block) editorContext.engine.scene.zoomToBlock(page) editorContext.engine.block.setSelected(block, selected = true) } } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberImagesLibrary { vectorIcon = { BrandIcons.BrandSpark } } } add { Dock.Button.rememberTextLibrary() } } } } } canvasMenu = { CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.guides.icons.canvasMenu.brandAction") } vectorIcon = { BrandIcons.BrandSpark } contentDescription = { "Show brand action" } onClick = { Toast.makeText( editorContext.activity, "Brand action", Toast.LENGTH_SHORT, ).show() } } } add { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.guides.icons.canvasMenu.replace") } vectorIcon = { IconPack.Replace } contentDescription = { "Replace selection" } onClick = { Toast.makeText( editorContext.activity, "Replace selection", Toast.LENGTH_SHORT, ).show() } } } } } } } } }, onClose = onClose, ) } ``` Customize the editor’s icons by passing Compose `ImageVector` values to editor components. ![Icons example showing the Android editor with branded and built-in canvas menu icons](./assets/icons-android.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-appearance-icons) The Android editor UI renders icons through Jetpack Compose. Use built-in `IconPack` vectors for common editor actions, or convert your own SVGs into `ImageVector` properties and pass them to component builders. For a complete product surface, start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). The snippets below use the same `EditorConfiguration` component system so you can apply the pattern to your own editor setup. ## Creating Custom Icon Vectors Android does not register runtime SVG sprite strings. Convert each trusted SVG into a Compose vector and expose it from your app’s icon pack, then import the generated property where you configure the editor UI. Generate vectors from source SVG files during development and commit the generated Kotlin source. Use an SVG-to-Compose generator such as `svg-to-compose` so the result is a Compose `ImageVector` property that works with `vectorIcon`. Use `currentColor`-friendly SVGs when possible so Material tinting can style the icon consistently. ### SVG Source Requirements - Start from trusted SVG files that your team owns or has reviewed. - Include a `viewBox` so the generated vector scales correctly in editor buttons. - Avoid hardcoded width and height values that fight Compose sizing. - Prefer paths that can be tinted by the receiving component. ### Security Considerations Because Android icon vectors are compiled Kotlin source, the editor does not inject arbitrary SVG markup at runtime. Do not add a runtime SVG parser for untrusted content just to mirror web integrations; sanitize and review SVGs before generating Kotlin vectors. ## Replacing Dock Entry Icons Override a predefined dock button’s `vectorIcon` when you want to keep the button behavior but change how it looks. This example keeps the Images library action and swaps only its icon. ```kotlin highlight-android-replace-dock-icon Dock.Button.rememberImagesLibrary { vectorIcon = { BrandIcons.BrandSpark } } ``` The builder override runs inside the existing `Dock.Button.rememberImagesLibrary` helper, so the button still opens the Images library. Only the `ImageVector` changes. ## Using Icons in Custom Components Create a custom editor component when your app owns both the icon and the action. This example adds a branded canvas menu button with a stable app-owned component ID, a generated `ImageVector`, an accessibility label, and a click handler for the app action. ```kotlin highlight-android-custom-canvas-menu-icon CanvasMenu.Button.remember { id = { EditorComponentId("com.example.guides.icons.canvasMenu.brandAction") } vectorIcon = { BrandIcons.BrandSpark } contentDescription = { "Show brand action" } onClick = { Toast.makeText( editorContext.activity, "Brand action", Toast.LENGTH_SHORT, ).show() } } ``` The button uses `BrandIcons.BrandSpark` through `vectorIcon`, so CE.SDK still applies the canvas menu button tint. Use the lower-level `icon` composable only when your component needs custom Compose content instead of a single vector. ## Built-In Icons CE.SDK ships generated Compose vectors in `ly.img.editor.core.iconpack.IconPack`. Import the extension property you need, then pass it anywhere an editor component accepts an `ImageVector`. For example, import `ly.img.editor.core.iconpack.IconPack` and the specific extension property, such as `ly.img.editor.core.iconpack.Replace`, before using `IconPack.Replace` in an editor component: ```kotlin highlight-android-iconpack-component CanvasMenu.Button.remember { id = { EditorComponentId("com.example.guides.icons.canvasMenu.replace") } vectorIcon = { IconPack.Replace } contentDescription = { "Replace selection" } onClick = { Toast.makeText( editorContext.activity, "Replace selection", Toast.LENGTH_SHORT, ).show() } } ``` | Icon | | ---- | | `IconPack.AddAudio` | | `IconPack.AddCameraBackground` | | `IconPack.AddCameraForeground` | | `IconPack.AddGalleryBackground` | | `IconPack.AddGalleryForeground` | | `IconPack.AddImageForeground` | | `IconPack.AddOverlay` | | `IconPack.AddShape` | | `IconPack.AddSticker` | | `IconPack.AddText` | | `IconPack.Adjustments` | | `IconPack.Animation` | | `IconPack.ArrowBack` | | `IconPack.ArrowForward` | | `IconPack.AsClip` | | `IconPack.AsOverlay` | | `IconPack.Blur` | | `IconPack.BringForward` | | `IconPack.Close` | | `IconPack.CloudAlertOutline` | | `IconPack.CropRotate` | | `IconPack.Delete` | | `IconPack.Duplicate` | | `IconPack.Effect` | | `IconPack.Elements` | | `IconPack.ExpandMore` | | `IconPack.Export` | | `IconPack.Filter` | | `IconPack.GroupEnter` | | `IconPack.Image` | | `IconPack.ImageOutline` | | `IconPack.Keyboard` | | `IconPack.LayersOutline` | | `IconPack.LibraryElementsOutline` | | `IconPack.LibraryElements` | | `IconPack.Minus` | | `IconPack.Music` | | `IconPack.Pages` | | `IconPack.PlayBox` | | `IconPack.PlayBoxOutline` | | `IconPack.Plus` | | `IconPack.Preview` | | `IconPack.PreviewToggled` | | `IconPack.Rabbit` | | `IconPack.Redo` | | `IconPack.ReorderHorizontally` | | `IconPack.Replace` | | `IconPack.Resize` | | `IconPack.SelectGroup` | | `IconPack.SendBackward` | | `IconPack.ShapeIcon` | | `IconPack.Shapes` | | `IconPack.ShapesOutline` | | `IconPack.SizeL` | | `IconPack.SizeLCircled` | | `IconPack.SizeM` | | `IconPack.SizeMCircled` | | `IconPack.SizeS` | | `IconPack.SizeSCircled` | | `IconPack.Split` | | `IconPack.StickerEmoji` | | `IconPack.StickerEmojiOutline` | | `IconPack.TextFields` | | `IconPack.Typeface` | | `IconPack.Undo` | | `IconPack.VoiceoverAdd` | | `IconPack.VolumeHigh` | | `IconPack.WifiCancel` | ## Troubleshooting ### Custom Icon Not Appearing - Verify the generated icon property is imported from the package where your vector was generated. - Confirm the editor component uses `vectorIcon` or draws the vector through the `icon` composable. - Keep custom component IDs stable and unique so the component list can manage replacements correctly. ### Icon Not Scaling Correctly - Check the source SVG `viewBox` before generating the Compose vector. - Avoid hardcoded dimensions in custom `Icon` composables unless the surrounding component expects them. - Test the icon in both light and dark UI modes when the vector contains multiple paths. ### Icon Color Not Matching Theme - Prefer the `vectorIcon` builder property so CE.SDK applies the component tint. - If you use the lower-level `icon` composable, pass the desired tint to `Icon`. - Avoid generated vectors with hardcoded brand colors unless the icon must ignore the theme. ## API Reference | API | Description | | --- | ----------- | | `Dock.remember(builder=_)` | Creates the dock component that owns dock items. | | `Dock.ListBuilder.remember(builder=_)` | Defines the dock item list and order. | | `Dock.Button.rememberImagesLibrary(builder=_)` | Creates the predefined Images library dock button and lets you override builder properties such as `vectorIcon`. | | `CanvasMenu.remember(builder=_)` | Creates the canvas menu component that appears for selected blocks. | | `CanvasMenu.ListBuilder.remember(builder=_)` | Defines the canvas menu item list and order. | | `CanvasMenu.Button.remember(builder=_)` | Creates a custom canvas menu button for app-owned actions. | | `CanvasMenu.ButtonBuilder.id` | Assigns a stable component ID to the custom canvas menu button. | | `EditorComponentId(id=_)` | Assigns a stable unique ID to a custom editor component. | | `IconPack.Replace` | Provides the built-in Replace icon vector after importing `ly.img.editor.core.iconpack.Replace`. | | `Dock.ButtonBuilder.vectorIcon` | Supplies the `ImageVector` rendered by a dock button. | | `Dock.ButtonBuilder.icon` | Supplies custom Compose icon content for a dock button when a single `ImageVector` is not enough. | | `CanvasMenu.ButtonBuilder.vectorIcon` | Supplies the `ImageVector` rendered by a canvas menu button. | | `CanvasMenu.ButtonBuilder.icon` | Supplies custom Compose icon content for a canvas menu button when a single `ImageVector` is not enough. | | `CanvasMenu.ButtonBuilder.contentDescription` | Supplies the accessibility label for the canvas menu button icon. | | `CanvasMenu.ButtonBuilder.onClick` | Handles the custom canvas menu button action. | --- ## 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: "Overlay" description: "Configure custom Android overlays for loading, confirmation, cancellation, and other transient editor flows." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) > [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-overlay/OverlayEditorSolution.kt reference-only import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.window.DialogProperties import kotlinx.coroutines.delay 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.engine.DesignBlockType // Add this composable to your NavHost @Composable fun OverlayEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { var isLoading by remember { mutableStateOf(false) } Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { isLoading = true try { 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) // Replace this delay with the startup work that should block editor interaction. delay(3000) } finally { isLoading = false } } overlay = { EditorComponent.remember { decoration = { if (isLoading) { AlertDialog( onDismissRequest = { }, title = { Text(text = "Loading editor") }, text = { Text(text = "The editor is preparing the scene. You can close it if you need to cancel.") }, confirmButton = { TextButton( onClick = { editorContext.eventHandler.send(EditorEvent.CloseEditor()) }, ) { Text(text = "Close Editor") } }, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), ) } } } } } }, onClose = onClose, ) } ``` Render app-controlled Compose UI above the CE.SDK editor to block interaction during loading, confirmation, or other transient flows. ![Custom overlay dialog on Android](./assets/overlay-android.png) > **Reading time:** 3 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-overlay) This guide uses a small Design Editor setup to show a loading dialog above the editor. The same `EditorConfiguration.overlay` hook applies to your CE.SDK editor UI, including the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). ## Configuration Keep overlay visibility in Compose state so callbacks and overlay UI can update the same value. ```kotlin highlight-android-state var isLoading by remember { mutableStateOf(false) } ``` In this example, `onCreate` intentionally owns scene creation. It enables the loading state before the sample scene is prepared and disables it after the startup work finishes. ```kotlin highlight-android-loading-state onCreate = { isLoading = true try { 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) // Replace this delay with the startup work that should block editor interaction. delay(3000) } finally { isLoading = false } } ``` Assign `overlay` to an `EditorComponent` and render your Compose UI from its `decoration` block. The dialog below is non-dismissible while loading and sends `EditorEvent.CloseEditor` when the user cancels. ```kotlin highlight-android-overlay overlay = { EditorComponent.remember { decoration = { if (isLoading) { AlertDialog( onDismissRequest = { }, title = { Text(text = "Loading editor") }, text = { Text(text = "The editor is preparing the scene. You can close it if you need to cancel.") }, confirmButton = { TextButton( onClick = { editorContext.eventHandler.send(EditorEvent.CloseEditor()) }, ) { Text(text = "Close Editor") } }, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), ) } } } } ``` > **Note:** The overlay is edge-to-edge. Draw over or around system bars yourself when your UI needs explicit status-bar or navigation-bar treatment. ## Complete Sample The linked Android sample creates a simple page, simulates startup work, and displays the overlay dialog while the loading state is active. Use it as the starting point for your own loading, confirmation, or export dialogs. --- ## 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: "Theming" description: "Customize the Android editor theme mode and keep custom Compose UI aligned with the editor palette." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) > [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-theming/ThemingEditorSolution.kt reference-only import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.core.component.Dock 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.iconpack.IconPack import ly.img.editor.core.iconpack.Replace // Add this composable to your NavHost @Composable fun ThemingEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { var uiMode by remember { mutableStateOf(EditorUiMode.LIGHT) } Editor( license = license, // pass null or empty for evaluation mode with watermark uiMode = uiMode, configuration = { EditorConfiguration.remember { dock = { Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.remember { textString = { if (uiMode == EditorUiMode.DARK) { "Use Light Theme" } else { "Use Dark Theme" } } vectorIcon = { IconPack.Replace } tint = { MaterialTheme.colorScheme.onSecondaryContainer } containerColor = { MaterialTheme.colorScheme.secondaryContainer } onClick = { uiMode = if (uiMode == EditorUiMode.DARK) { EditorUiMode.LIGHT } else { EditorUiMode.DARK } } } } } } } } } }, onClose = onClose, ) } ``` Configure the Android editor to follow the system appearance or force a light or dark theme. ![Android editor theme-toggle button in light and dark modes](./assets/theming-android.png) > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-theming) The Android editor applies CE.SDK's Compose `EditorTheme` inside the `Editor` composable. Use `uiMode` when your app needs to override the device theme, then style custom editor components from `MaterialTheme.colorScheme` so they stay aligned with the active light or dark palette. For a complete ready-made surface, start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) and apply the same `uiMode` setting where your app renders `Editor`. ## Built-in Themes The `Editor` composable accepts `uiMode` and defaults to `EditorUiMode.SYSTEM`. | Mode | Behavior | | ---- | -------- | | `EditorUiMode.SYSTEM` | Follows the operating system light or dark setting. | | `EditorUiMode.LIGHT` | Always renders the editor with the light palette. | | `EditorUiMode.DARK` | Always renders the editor with the dark palette. | Pass the selected mode directly to `Editor`: ```kotlin highlight-android-editor-uimode uiMode = uiMode, ``` ## Switching Themes at Runtime Hoist the selected mode into Compose state when users should change the theme from your UI. ```kotlin highlight-android-theme-state var uiMode by remember { mutableStateOf(EditorUiMode.LIGHT) } ``` The sample starts with `EditorUiMode.LIGHT` so the theme-toggle button and screenshot show a deterministic light-to-dark switch. In production, initialize this state from your app's theme preference, or use `EditorUiMode.SYSTEM` when the editor should follow the device setting. The sample adds a dock button that toggles between the light and dark palettes. Its label, icon tint, and container color are read from the current Compose theme, so the custom button updates with the editor theme. ```kotlin highlight-android-theme-toggle configuration = { EditorConfiguration.remember { dock = { Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.remember { textString = { if (uiMode == EditorUiMode.DARK) { "Use Light Theme" } else { "Use Dark Theme" } } vectorIcon = { IconPack.Replace } tint = { MaterialTheme.colorScheme.onSecondaryContainer } containerColor = { MaterialTheme.colorScheme.secondaryContainer } onClick = { uiMode = if (uiMode == EditorUiMode.DARK) { EditorUiMode.LIGHT } else { EditorUiMode.DARK } } } } } } } } } }, ``` ## Custom Styling Scope Android theming is implemented through Jetpack Compose. `uiMode` selects the editor's light or dark palette, while app-specific controls you add through editor configuration should read the active Compose theme instead of hard-coding colors. Use component configuration for app-specific UI that you add to the editor: - Read colors and typography from `MaterialTheme` inside configuration lambdas. - Apply brand colors to the custom components you own, such as dock buttons, panels, or actions. - Test any custom tint, container, or text color in both light and dark modes. ## Best Practices - Use `EditorUiMode.SYSTEM` unless your app has its own appearance preference. - Persist a user-selected theme in your app state and pass it back through `uiMode`. - Prefer `MaterialTheme.colorScheme` values for custom editor components so contrast stays tied to the active palette. - Validate custom component colors against real editor content in light and dark modes. ## Troubleshooting - **The editor keeps following the system theme:** pass `EditorUiMode.LIGHT` or `EditorUiMode.DARK` through `uiMode` instead of leaving the default. - **A runtime toggle updates text but not the editor palette:** keep the selected `EditorUiMode` in Compose state above `Editor` so changing it recomposes the editor. - **Custom buttons look wrong in one mode:** avoid hard-coded colors and read `MaterialTheme.colorScheme` from inside the component configuration. ## API Reference | API | Description | | --- | ----------- | | `Editor(uiMode=_)` | Selects whether the editor follows the system theme or renders a fixed light or dark palette. | | `EditorConfiguration.remember(builder=_)` | Provides the editor component configuration used by the sample dock button. | | `Dock.remember(builder=_)` | Configures the editor dock that hosts custom dock items. | | `Dock.ListBuilder.remember(builder=_)` | Builds the ordered list of items shown in the dock. | | `Dock.ListBuilder.add(block=_)` | Adds a custom dock item to the dock list. | | `Dock.Button.remember(builder=_)` | Creates the custom theme-toggle button shown in the dock. | ## Next Steps - [Custom Labels](https://img.ly/docs/cesdk/android/user-interface/appearance/custom-labels-7794f7/) — Override default UI text with your own labels for full control over the editor's wording. - [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) - Extend the editor interface with custom components, panels, actions, and dialogs tailored to your workflow. - [Customize Behaviour](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/customize-behaviour-c09cb2/) — Define how UI components behave when interacting with your custom logic. --- ## 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 Your Own UI" description: "Build a completely custom Android editor UI using CE.SDK's direct Engine APIs and your own Compose controls." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/build-your-own-ui-fe7527/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Build Your Own UI](https://img.ly/docs/cesdk/android/user-interface/build-your-own-ui-fe7527/) --- ```kotlin file=@cesdk_android_examples/engine-guides-user-interface-build-your-own-ui/BuildYourOwnUI.kt reference-only import android.app.Activity import android.view.SurfaceView import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSavedStateRegistryOwner import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import kotlinx.coroutines.flow.collect 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.MimeType import ly.img.engine.ShapeType import java.nio.ByteBuffer @Composable fun BuildYourOwnUIScreen( license: String, onClose: () -> Unit, ) { var page by remember { mutableStateOf(null) } var selectedBlock by remember { mutableStateOf(null) } var selectedType by remember { mutableStateOf("No block selected") } var positionX by remember { mutableStateOf(0F) } var positionY by remember { mutableStateOf(0F) } var width by remember { mutableStateOf(0F) } var height by remember { mutableStateOf(0F) } var rotationDegrees by remember { mutableStateOf(0F) } var exportStatus by remember { mutableStateOf("Ready to export") } fun applySelectionState(selection: BuildYourOwnUISelection) { selectedBlock = selection.block selectedType = selection.type ?: "No block selected" positionX = selection.positionX positionY = selection.positionY width = selection.width height = selection.height rotationDegrees = selection.rotationDegrees } val activity = LocalContext.current as Activity val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current val coroutineScope = rememberCoroutineScope() val engine = remember(activity.application) { Engine.init(activity.application) Engine.getInstance(id = "ly.img.engine.examples.build-your-own-ui") } val surfaceView = remember { SurfaceView(activity) } BackHandler(onBack = onClose) Column(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.weight(1F)) { AndroidView( modifier = Modifier .fillMaxSize() .semantics { contentDescription = "Custom CE.SDK canvas" }, factory = { surfaceView }, ) } Divider() BuildYourOwnUIControls( selectedType = selectedType, positionX = positionX, positionY = positionY, width = width, height = height, rotationDegrees = rotationDegrees, exportStatus = exportStatus, onPositionXChange = { positionX = it }, onPositionYChange = { positionY = it }, onWidthChange = { width = it }, onHeightChange = { height = it }, onRotationChange = { rotationDegrees = it }, onPositionXFinished = { selectedBlock?.let { engine.block.setPositionX(block = it, value = positionX) } }, onPositionYFinished = { selectedBlock?.let { engine.block.setPositionY(block = it, value = positionY) } }, onWidthFinished = { selectedBlock?.let { engine.block.setWidth(block = it, value = width) } }, onHeightFinished = { selectedBlock?.let { engine.block.setHeight(block = it, value = height) } }, onRotationFinished = { selectedBlock?.let { engine.block.setRotation( block = it, radians = Math.toRadians(rotationDegrees.toDouble()).toFloat(), ) } }, onAddText = { page?.let { addTextBlock(engine = engine, page = it) applySelectionState(readSelection(engine)) } }, onAddShape = { page?.let { addShapeBlock(engine = engine, page = it) applySelectionState(readSelection(engine)) } }, onExport = { val pageToExport = page if (pageToExport != null) { coroutineScope.launch { val png = exportPagePng(engine = engine, page = pageToExport) exportStatus = "Exported ${png.remaining()} PNG bytes" } } }, ) } LaunchedEffect(engine, surfaceView, savedStateRegistryOwner) { engine.start( license = license.takeIf { it.isNotBlank() }, userId = "build-your-own-ui", savedStateRegistryOwner = savedStateRegistryOwner, ) engine.bindSurfaceView(surfaceView) val initialPage = findOrCreateInitialPage(engine) page = initialPage engine.scene.zoomToBlock( block = initialPage, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) collectSelectionEvents(engine = engine, onSelectionChanged = ::applySelectionState) } DisposableEffect(activity, engine) { onDispose { if (engine.isEngineRunning()) { engine.unbind() if (!activity.isChangingConfigurations) { engine.stop() } } } } } private fun findOrCreateInitialPage(engine: Engine): DesignBlock { val existingScene = engine.scene.get() if (existingScene != null) { val existingPage = engine.block.findByType(DesignBlockType.Page).firstOrNull { engine.block.isValid(it) } if (existingPage != null) return existingPage } return createInitialContent(engine = engine, scene = existingScene) } private fun createInitialContent( engine: Engine, scene: DesignBlock?, ): DesignBlock { val targetScene = 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 = targetScene, child = page) val textBlock = engine.block.create(DesignBlockType.Text) engine.block.setString(block = textBlock, property = "text/text", value = "Tap to Edit") engine.block.setPositionX(block = textBlock, value = 80F) engine.block.setPositionY(block = textBlock, value = 80F) engine.block.setWidth(block = textBlock, value = 300F) engine.block.setHeight(block = textBlock, value = 80F) engine.block.appendChild(parent = page, child = textBlock) val shapeBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(type = ShapeType.Rect) val fill = engine.block.createFill(fillType = FillType.Color) engine.block.setShape(block = shapeBlock, shape = shape) engine.block.setFill(block = shapeBlock, fill = fill) engine.block.setFillSolidColor( block = shapeBlock, color = Color.fromRGBA(r = 0.2F, g = 0.6F, b = 0.9F, a = 1F), ) engine.block.setPositionX(block = shapeBlock, value = 450F) engine.block.setPositionY(block = shapeBlock, value = 200F) engine.block.setWidth(block = shapeBlock, value = 150F) engine.block.setHeight(block = shapeBlock, value = 150F) engine.block.appendChild(parent = page, child = shapeBlock) engine.block.select(textBlock) return page } private data class BuildYourOwnUISelection( val block: DesignBlock?, val type: String?, val positionX: Float, val positionY: Float, val width: Float, val height: Float, val rotationDegrees: Float, ) private suspend fun collectSelectionEvents( engine: Engine, onSelectionChanged: (BuildYourOwnUISelection) -> Unit, ) { onSelectionChanged(readSelection(engine)) engine.event.subscribe().collect { onSelectionChanged(readSelection(engine)) } } private fun readSelection(engine: Engine): BuildYourOwnUISelection { val selected = engine.block.findAllSelected().firstOrNull() if (selected == null || !engine.block.isValid(selected)) { return BuildYourOwnUISelection( block = null, type = null, positionX = 0F, positionY = 0F, width = 0F, height = 0F, rotationDegrees = 0F, ) } return BuildYourOwnUISelection( block = selected, type = engine.block.getType(selected), positionX = engine.block.getPositionX(selected), positionY = engine.block.getPositionY(selected), width = engine.block.getWidth(selected), height = engine.block.getHeight(selected), rotationDegrees = Math.toDegrees(engine.block.getRotation(selected).toDouble()).toFloat(), ) } @Composable private fun BuildYourOwnUIControls( selectedType: String, positionX: Float, positionY: Float, width: Float, height: Float, rotationDegrees: Float, exportStatus: String, onPositionXChange: (Float) -> Unit, onPositionYChange: (Float) -> Unit, onWidthChange: (Float) -> Unit, onHeightChange: (Float) -> Unit, onRotationChange: (Float) -> Unit, onPositionXFinished: () -> Unit, onPositionYFinished: () -> Unit, onWidthFinished: () -> Unit, onHeightFinished: () -> Unit, onRotationFinished: () -> Unit, onAddText: () -> Unit, onAddShape: () -> Unit, onExport: () -> Unit, ) { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceVariant) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Button(onClick = onAddText) { Text("Add Text") } Button(onClick = onAddShape) { Text("Add Shape") } OutlinedButton(onClick = onExport) { Text("Export PNG") } } Text(text = "Selected: $selectedType", style = MaterialTheme.typography.labelMedium) Text(text = exportStatus, style = MaterialTheme.typography.bodySmall) PropertyPanel( positionX = positionX, positionY = positionY, width = width, height = height, rotationDegrees = rotationDegrees, onPositionXChange = onPositionXChange, onPositionYChange = onPositionYChange, onWidthChange = onWidthChange, onHeightChange = onHeightChange, onRotationChange = onRotationChange, onPositionXFinished = onPositionXFinished, onPositionYFinished = onPositionYFinished, onWidthFinished = onWidthFinished, onHeightFinished = onHeightFinished, onRotationFinished = onRotationFinished, ) } } private fun addTextBlock( engine: Engine, page: DesignBlock, ) { val textBlock = engine.block.create(DesignBlockType.Text) engine.block.setString(block = textBlock, property = "text/text", value = "Custom headline") engine.block.setPositionX(block = textBlock, value = 80F) engine.block.setPositionY(block = textBlock, value = 80F) engine.block.setWidth(block = textBlock, value = 320F) engine.block.setHeight(block = textBlock, value = 100F) engine.block.appendChild(parent = page, child = textBlock) engine.block.select(textBlock) } private fun addShapeBlock( engine: Engine, page: DesignBlock, ) { val shapeBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(type = ShapeType.Rect) val fill = engine.block.createFill(fillType = FillType.Color) engine.block.setShape(block = shapeBlock, shape = shape) engine.block.setFill(block = shapeBlock, fill = fill) engine.block.setFillSolidColor( block = shapeBlock, color = Color.fromRGBA(r = 0.09F, g = 0.7F, b = 0.55F, a = 1F), ) engine.block.setPositionX(block = shapeBlock, value = 120F) engine.block.setPositionY(block = shapeBlock, value = 220F) engine.block.setWidth(block = shapeBlock, value = 180F) engine.block.setHeight(block = shapeBlock, value = 180F) engine.block.appendChild(parent = page, child = shapeBlock) engine.block.select(shapeBlock) } @Composable private fun PropertyPanel( positionX: Float, positionY: Float, width: Float, height: Float, rotationDegrees: Float, onPositionXChange: (Float) -> Unit, onPositionYChange: (Float) -> Unit, onWidthChange: (Float) -> Unit, onHeightChange: (Float) -> Unit, onRotationChange: (Float) -> Unit, onPositionXFinished: () -> Unit, onPositionYFinished: () -> Unit, onWidthFinished: () -> Unit, onHeightFinished: () -> Unit, onRotationFinished: () -> Unit, ) { TransformSlider("X", positionX, 0F..800F, onPositionXChange, onPositionXFinished) TransformSlider("Y", positionY, 0F..600F, onPositionYChange, onPositionYFinished) TransformSlider("W", width, 1F..800F, onWidthChange, onWidthFinished) TransformSlider("H", height, 1F..600F, onHeightChange, onHeightFinished) TransformSlider("Rot", rotationDegrees, -180F..180F, onRotationChange, onRotationFinished) } @Composable private fun TransformSlider( label: String, value: Float, range: ClosedFloatingPointRange, onValueChange: (Float) -> Unit, onValueChangeFinished: () -> Unit, ) { Row(modifier = Modifier.fillMaxWidth()) { Text( modifier = Modifier.width(40.dp), text = label, style = MaterialTheme.typography.bodySmall, ) Spacer(modifier = Modifier.width(8.dp)) Slider( modifier = Modifier.weight(1F), value = value, onValueChange = onValueChange, onValueChangeFinished = onValueChangeFinished, valueRange = range, ) } } private suspend fun exportPagePng( engine: Engine, page: DesignBlock, ): ByteBuffer = engine.block.export(block = page, mimeType = MimeType.PNG) ``` Build a custom Android editing surface by rendering the CE.SDK Engine into your own Compose UI and driving every control through Engine APIs. ![Custom Android editor UI with a canvas, toolbar, and transform controls](./assets/android.hero.png) > **Reading time:** 12 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-user-interface-build-your-own-ui) When you need complete control over the editing experience, you can start and bind the Engine yourself, host its render surface in your app, and build custom tools around it. This guide shows a Compose-based editor surface with a canvas, toolbar, property controls, event synchronization, block creation, and PNG export. For a complete ready-made editing surface, start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). Use the direct Engine approach below when your app needs to own the layout, controls, and interaction model. ## Architecture Overview A custom Android UI separates rendering from controls. The Engine renders into a `SurfaceView`, while Compose state mirrors the selected block and lets your buttons or sliders call Engine APIs such as `engine.block.create(...)`, `engine.block.setPositionX(...)`, and `engine.block.export(...)`. ```kotlin highlight-android-state var page by remember { mutableStateOf(null) } var selectedBlock by remember { mutableStateOf(null) } var selectedType by remember { mutableStateOf("No block selected") } var positionX by remember { mutableStateOf(0F) } var positionY by remember { mutableStateOf(0F) } var width by remember { mutableStateOf(0F) } var height by remember { mutableStateOf(0F) } var rotationDegrees by remember { mutableStateOf(0F) } var exportStatus by remember { mutableStateOf("Ready to export") } fun applySelectionState(selection: BuildYourOwnUISelection) { selectedBlock = selection.block selectedType = selection.type ?: "No block selected" positionX = selection.positionX positionY = selection.positionY width = selection.width height = selection.height rotationDegrees = selection.rotationDegrees } ``` All Engine calls in this sample run from the main thread. The Engine event stream updates the Compose state whenever selection or block properties change, so the UI stays synchronized with canvas interactions and programmatic updates. ## Initialize Engine and Setup Canvas Create an Engine instance, start it with your license, and bind it to a `SurfaceView` that Compose hosts through `AndroidView`. The sample owns the Engine lifecycle because it is not using the prebuilt editor UI. For the complete Android Engine lifecycle and all render-target options, see the [Engine Interface](https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/) guide. Create the Engine and the Android render view first. ```kotlin highlight-android-engine-instance val engine = remember(activity.application) { Engine.init(activity.application) Engine.getInstance(id = "ly.img.engine.examples.build-your-own-ui") } val surfaceView = remember { SurfaceView(activity) } ``` Host the render view in Compose with `AndroidView`. ```kotlin highlight-android-render-target Box(modifier = Modifier.weight(1F)) { AndroidView( modifier = Modifier .fillMaxSize() .semantics { contentDescription = "Custom CE.SDK canvas" }, factory = { surfaceView }, ) } ``` Start the Engine and bind the render view before creating scene content. ```kotlin highlight-android-engine-start engine.start( license = license.takeIf { it.isNotBlank() }, userId = "build-your-own-ui", savedStateRegistryOwner = savedStateRegistryOwner, ) engine.bindSurfaceView(surfaceView) ``` Release the render target and stop the Engine when the custom editor leaves composition. ```kotlin highlight-android-engine-cleanup DisposableEffect(activity, engine) { onDispose { if (engine.isEngineRunning()) { engine.unbind() if (!activity.isChangingConfigurations) { engine.stop() } } } } ``` `Engine.init(application)` prepares the Android runtime. `Engine.getInstance(id=_)` gives you a stable Engine instance, `engine.start(...)` starts it, and `engine.bindSurfaceView(surfaceView=_)` makes the Engine draw into your custom UI. ## Create Initial Content Seed the scene with a page, a text block, and a graphic block so the custom controls have immediate content to operate on. Graphic blocks need a shape and fill before they render. ```kotlin highlight-android-create-initial-content private fun findOrCreateInitialPage(engine: Engine): DesignBlock { val existingScene = engine.scene.get() if (existingScene != null) { val existingPage = engine.block.findByType(DesignBlockType.Page).firstOrNull { engine.block.isValid(it) } if (existingPage != null) return existingPage } return createInitialContent(engine = engine, scene = existingScene) } private fun createInitialContent( engine: Engine, scene: DesignBlock?, ): DesignBlock { val targetScene = 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 = targetScene, child = page) val textBlock = engine.block.create(DesignBlockType.Text) engine.block.setString(block = textBlock, property = "text/text", value = "Tap to Edit") engine.block.setPositionX(block = textBlock, value = 80F) engine.block.setPositionY(block = textBlock, value = 80F) engine.block.setWidth(block = textBlock, value = 300F) engine.block.setHeight(block = textBlock, value = 80F) engine.block.appendChild(parent = page, child = textBlock) val shapeBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(type = ShapeType.Rect) val fill = engine.block.createFill(fillType = FillType.Color) engine.block.setShape(block = shapeBlock, shape = shape) engine.block.setFill(block = shapeBlock, fill = fill) engine.block.setFillSolidColor( block = shapeBlock, color = Color.fromRGBA(r = 0.2F, g = 0.6F, b = 0.9F, a = 1F), ) engine.block.setPositionX(block = shapeBlock, value = 450F) engine.block.setPositionY(block = shapeBlock, value = 200F) engine.block.setWidth(block = shapeBlock, value = 150F) engine.block.setHeight(block = shapeBlock, value = 150F) engine.block.appendChild(parent = page, child = shapeBlock) engine.block.select(textBlock) return page } ``` The sample reuses an existing page when Android saved-state restoration or configuration changes already kept a scene alive. For a newly created scene, the first text block is selected at the end of setup and flows into the property panel through the event handling code below. ## Handle Engine Events Subscribe to Engine events and read the current selection after every event batch. Passing no block filter to `engine.event.subscribe()` receives events for the whole scene. ```kotlin highlight-android-handle-events private data class BuildYourOwnUISelection( val block: DesignBlock?, val type: String?, val positionX: Float, val positionY: Float, val width: Float, val height: Float, val rotationDegrees: Float, ) private suspend fun collectSelectionEvents( engine: Engine, onSelectionChanged: (BuildYourOwnUISelection) -> Unit, ) { onSelectionChanged(readSelection(engine)) engine.event.subscribe().collect { onSelectionChanged(readSelection(engine)) } } private fun readSelection(engine: Engine): BuildYourOwnUISelection { val selected = engine.block.findAllSelected().firstOrNull() if (selected == null || !engine.block.isValid(selected)) { return BuildYourOwnUISelection( block = null, type = null, positionX = 0F, positionY = 0F, width = 0F, height = 0F, rotationDegrees = 0F, ) } return BuildYourOwnUISelection( block = selected, type = engine.block.getType(selected), positionX = engine.block.getPositionX(selected), positionY = engine.block.getPositionY(selected), width = engine.block.getWidth(selected), height = engine.block.getHeight(selected), rotationDegrees = Math.toDegrees(engine.block.getRotation(selected).toDouble()).toFloat(), ) } ``` The sample reads the selected block type, position, size, and rotation after each event. If the selection becomes invalid, the UI resets to an empty state. ## Build Custom UI Controls Compose buttons can control the Engine directly. The toolbar adds text, adds shapes, and triggers export; the same pattern works for icons, menus, segmented controls, or app-specific actions. ```kotlin highlight-android-ui-controls @Composable private fun BuildYourOwnUIControls( selectedType: String, positionX: Float, positionY: Float, width: Float, height: Float, rotationDegrees: Float, exportStatus: String, onPositionXChange: (Float) -> Unit, onPositionYChange: (Float) -> Unit, onWidthChange: (Float) -> Unit, onHeightChange: (Float) -> Unit, onRotationChange: (Float) -> Unit, onPositionXFinished: () -> Unit, onPositionYFinished: () -> Unit, onWidthFinished: () -> Unit, onHeightFinished: () -> Unit, onRotationFinished: () -> Unit, onAddText: () -> Unit, onAddShape: () -> Unit, onExport: () -> Unit, ) { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceVariant) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Button(onClick = onAddText) { Text("Add Text") } Button(onClick = onAddShape) { Text("Add Shape") } OutlinedButton(onClick = onExport) { Text("Export PNG") } } Text(text = "Selected: $selectedType", style = MaterialTheme.typography.labelMedium) Text(text = exportStatus, style = MaterialTheme.typography.bodySmall) PropertyPanel( positionX = positionX, positionY = positionY, width = width, height = height, rotationDegrees = rotationDegrees, onPositionXChange = onPositionXChange, onPositionYChange = onPositionYChange, onWidthChange = onWidthChange, onHeightChange = onHeightChange, onRotationChange = onRotationChange, onPositionXFinished = onPositionXFinished, onPositionYFinished = onPositionYFinished, onWidthFinished = onWidthFinished, onHeightFinished = onHeightFinished, onRotationFinished = onRotationFinished, ) } } ``` Because the controls are regular Compose UI, you can style and arrange them with your app's design system while keeping all creative operations inside the Engine. ## Add Blocks Programmatically Create new blocks with typed block APIs, configure their properties, append them to the page, then select the new block so the property panel updates immediately. ```kotlin highlight-android-add-blocks private fun addTextBlock( engine: Engine, page: DesignBlock, ) { val textBlock = engine.block.create(DesignBlockType.Text) engine.block.setString(block = textBlock, property = "text/text", value = "Custom headline") engine.block.setPositionX(block = textBlock, value = 80F) engine.block.setPositionY(block = textBlock, value = 80F) engine.block.setWidth(block = textBlock, value = 320F) engine.block.setHeight(block = textBlock, value = 100F) engine.block.appendChild(parent = page, child = textBlock) engine.block.select(textBlock) } private fun addShapeBlock( engine: Engine, page: DesignBlock, ) { val shapeBlock = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(type = ShapeType.Rect) val fill = engine.block.createFill(fillType = FillType.Color) engine.block.setShape(block = shapeBlock, shape = shape) engine.block.setFill(block = shapeBlock, fill = fill) engine.block.setFillSolidColor( block = shapeBlock, color = Color.fromRGBA(r = 0.09F, g = 0.7F, b = 0.55F, a = 1F), ) engine.block.setPositionX(block = shapeBlock, value = 120F) engine.block.setPositionY(block = shapeBlock, value = 220F) engine.block.setWidth(block = shapeBlock, value = 180F) engine.block.setHeight(block = shapeBlock, value = 180F) engine.block.appendChild(parent = page, child = shapeBlock) engine.block.select(shapeBlock) } ``` The text and shape flows use the same create-configure-append-select recipe. Media blocks follow the same hierarchy pattern but also need a loaded resource or fill before they render. ## Create Property Panels The property panel binds sliders to the selected block's transform values. Slider movement updates Compose state first, then writes back to the Engine when editing finishes. ```kotlin highlight-android-property-panel @Composable private fun PropertyPanel( positionX: Float, positionY: Float, width: Float, height: Float, rotationDegrees: Float, onPositionXChange: (Float) -> Unit, onPositionYChange: (Float) -> Unit, onWidthChange: (Float) -> Unit, onHeightChange: (Float) -> Unit, onRotationChange: (Float) -> Unit, onPositionXFinished: () -> Unit, onPositionYFinished: () -> Unit, onWidthFinished: () -> Unit, onHeightFinished: () -> Unit, onRotationFinished: () -> Unit, ) { TransformSlider("X", positionX, 0F..800F, onPositionXChange, onPositionXFinished) TransformSlider("Y", positionY, 0F..600F, onPositionYChange, onPositionYFinished) TransformSlider("W", width, 1F..800F, onWidthChange, onWidthFinished) TransformSlider("H", height, 1F..600F, onHeightChange, onHeightFinished) TransformSlider("Rot", rotationDegrees, -180F..180F, onRotationChange, onRotationFinished) } @Composable private fun TransformSlider( label: String, value: Float, range: ClosedFloatingPointRange, onValueChange: (Float) -> Unit, onValueChangeFinished: () -> Unit, ) { Row(modifier = Modifier.fillMaxWidth()) { Text( modifier = Modifier.width(40.dp), text = label, style = MaterialTheme.typography.bodySmall, ) Spacer(modifier = Modifier.width(8.dp)) Slider( modifier = Modifier.weight(1F), value = value, onValueChange = onValueChange, onValueChangeFinished = onValueChangeFinished, valueRange = range, ) } } ``` `engine.block.setRotation(...)` expects radians, so the sample converts the slider's degree value before writing it to the Engine. The event reader converts the value back to degrees for display. ## Export Designs Export the page or any other block with `engine.block.export(...)`. The Android API returns a `ByteBuffer` that your app can save, share, or upload. ```kotlin highlight-android-export private suspend fun exportPagePng( engine: Engine, page: DesignBlock, ): ByteBuffer = engine.block.export(block = page, mimeType = MimeType.PNG) ``` The sample exports the page as PNG. Use another `MimeType` with `engine.block.export(...)` for supported static formats such as JPEG, PDF, or SVG. For MP4 or other video output, use `engine.block.exportVideo(...)` with a `timeOffset`, `duration`, and `progressCallback`. ## Framework Integration Patterns Compose integrations typically use `AndroidView` to host a `SurfaceView` or `TextureView`, then bind the Engine after `engine.start(...)` completes. A View-based Activity or Fragment can use the same Engine APIs by creating the render view in XML or code and calling `engine.bindSurfaceView(surfaceView=_)` or `engine.bindTextureView(textureView=_)`. The rest of the custom UI is framework-agnostic: your controls hold app state, call Engine APIs in response to user intent, and update from `engine.event.subscribe(...)`. ## Troubleshooting ### Canvas Not Rendering Confirm the Engine has started, the `SurfaceView` is attached, and `engine.bindSurfaceView(surfaceView=_)` ran before scene setup. A visible custom UI needs a bound `SurfaceView` or `TextureView`; `bindOffscreen(width=_, height=_)` is for render-only flows without a visible canvas. ### Events Not Firing Start collecting `engine.event.subscribe(...)` after scene setup and keep the collecting coroutine alive for the lifetime of the custom UI. If the composable recreates the Engine or cancels the `LaunchedEffect`, the property panel stops receiving updates. ### Performance Issues Avoid writing to the Engine on every slider tick unless your UI really needs continuous updates. The sample writes values when editing finishes so the Engine does not receive unnecessary transform calls during every drag frame. ### Blocks Not Responding Ensure new blocks are appended to the scene hierarchy with `engine.block.appendChild(parent=_, child=_)`. Detached blocks exist in the Engine but do not render on the page. ### Selection State Out of Sync Use `engine.block.select(block=_)` for single selection after programmatic changes, and refresh UI state from `engine.block.findAllSelected()` inside your event handler. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `Engine.init(application=_)` | Engine | Prepare the Android Engine runtime before creating instances. | | `Engine.getInstance(id=_, audioContext=_)` | Engine | Get or create a named Engine instance. | | `engine.start(license=_, userId=_, savedStateRegistryOwner=_)` | Engine | Start the Engine and optionally enable Android saved-state restoration. | | `engine.bindSurfaceView(surfaceView=_)` | Engine | Render Engine frames into a `SurfaceView`. | | `engine.bindTextureView(textureView=_)` | Engine | Render Engine frames into a `TextureView`. | | `engine.unbind()` | Engine | Detach the current render target. | | `engine.isEngineRunning()` | Engine | Check whether the Engine is currently started. | | `engine.stop()` | Engine | Stop the Engine and release runtime resources. | | `engine.scene.create()` | Scene | Create a new scene. | | `engine.scene.get()` | Scene | Return the active scene, or `null` when no scene exists yet. | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Scene | Zoom the canvas viewport to a block. | | `engine.block.create(blockType=_)` | Block | Create a block of the requested type. | | `engine.block.findByType(type=_)` | Block | Find blocks of a requested type in the active scene. | | `engine.block.createShape(type=_)` | Block | Create a shape block used by a graphic block. | | `engine.block.createFill(fillType=_)` | Block | Create a fill block used by a graphic block. | | `engine.block.setShape(block=_, shape=_)` | Block | Attach a shape to a graphic block. | | `engine.block.setFill(block=_, fill=_)` | Block | Attach a fill to a graphic block. | | `engine.block.appendChild(parent=_, child=_)` | Block | Add a block to the scene hierarchy. | | `engine.block.select(block=_)` | Block | Select a block and clear the previous single selection. | | `engine.block.findAllSelected()` | Block | Read the current selection. | | `engine.block.isValid(block=_)` | Block | Check whether a block ID still refers to an existing block. | | `engine.block.getType(block=_)` | Block | Read a block's type. | | `engine.block.setPositionX(block=_, value=_)` | Block | Set a block's x position. | | `engine.block.setPositionY(block=_, value=_)` | Block | Set a block's y position. | | `engine.block.getPositionX(block=_)` | Block | Read a block's x position. | | `engine.block.getPositionY(block=_)` | Block | Read a block's y position. | | `engine.block.setWidth(block=_, value=_)` | Block | Set a block's width. | | `engine.block.setHeight(block=_, value=_)` | Block | Set a block's height. | | `engine.block.getWidth(block=_)` | Block | Read a block's width. | | `engine.block.getHeight(block=_)` | Block | Read a block's height. | | `engine.block.setRotation(block=_, radians=_)` | Block | Set a block's rotation in radians. | | `engine.block.getRotation(block=_)` | Block | Read a block's rotation in radians. | | `engine.block.setString(block=_, property=_, value=_)` | Block | Write a string property such as `text/text`. | | `engine.block.setFillSolidColor(block=_, color=_)` | Block | Set a solid fill color on a block. | | `engine.block.export(block=_, mimeType=_)` | Block | Export a block or page to bytes. | | `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_)` | Block | Export a page to video bytes with progress updates. | | `engine.event.subscribe(blocks=_)` | Event | Subscribe to block lifecycle events. | ## Next Steps - [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Deep dive into direct Engine concepts. - [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Understand block types and hierarchy. - [Export Designs](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Explore 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: "Custom Error Messages" description: "Learn how to override CE.SDK's engine error dialogs with your own localized, customer-facing copy using the I18n API." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/custom-error-messages-2e8555/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Custom Error Messages](https://img.ly/docs/cesdk/android/user-interface/custom-error-messages-2e8555/) --- When an engine operation fails—an unsupported file, an asset that can't be applied, or an export—CE.SDK surfaces the error to the user. This guide shows you how to replace that text with your own localized, brand-appropriate copy through Android string resources, and how to reuse the same resolved copy in your own error handling. CE.SDK's engine emits **structured errors**. Every recoverable failure carries a stable `code` (for example `SCENE.NOT_VALID`), an English developer-facing message and hint, and typed arguments. The editor surfaces these to the user—a fatal failure such as an invalid scene in a blocking dialog, a recoverable one such as an asset that can't be applied in a transient notice—and you override the customer-facing text by adding a string resource keyed off the error code. When you don't provide one, the editor falls back to the engine's English message—so the message is never blank. ## How CE.SDK Resolves Error Text When the editor surfaces an engine error, it looks up a string resource derived from the error's `code`. It uses your resource when present and falls back to the engine's own English string otherwise: 1. `ly_img_engine_error_` — your custom copy. 2. The engine's English `message` (or `hint`) — the fallback when no resource is authored. Because the lookup always ends at the engine string, you only author copy for the errors you care about; everything else keeps a sensible default. ## Deriving the Resource Name The resource name is the error `code`, lowercased, with each `.` replaced by `_` and prefixed with `ly_img_engine_error_`: | Engine code | String resource name | | --- | --- | | `SCENE.NOT_VALID` | `ly_img_engine_error_scene_not_valid` | | `ASSET.UNSUPPORTED_MIME_TYPE_FOR_BLOCK` | `ly_img_engine_error_asset_unsupported_mime_type_for_block` | | `BLOCK.TEXT_INVALID_FONT_SIZE` | `ly_img_engine_error_block_text_invalid_font_size` | Each code also has an optional **description** companion resource—the same name with a `_description` suffix—for the longer "what to do next" body copy that complements the short headline. It resolves the same way and falls back to the engine's English `hint`: | Engine code | Description resource name | | --- | --- | | `SCENE.NOT_VALID` | `ly_img_engine_error_scene_not_valid_description` | | `ASSET.UNSUPPORTED_MIME_TYPE_FOR_BLOCK` | `ly_img_engine_error_asset_unsupported_mime_type_for_block_description` | For the complete list of error codes you can override, see the [Error Catalog](https://img.ly/docs/cesdk/android/concepts/error-catalog-z3djzn/). ## Adding Custom Error Copy Add the string resources to your app's own resource files. Provide a value for each locale you support; Android selects the right one based on the device language. ```xml Unsupported file type Select an element with a color first ``` ```xml Nicht unterstützter Dateityp Wähle zuerst ein Element mit Farbe aus ``` You only need the resources you want to change; all other text keeps CE.SDK's defaults. ## Interpolating Error Details Structured errors carry typed arguments—an unsupported MIME type, an invalid value, a block id. Reference them in your copy with `{{argument}}` placeholders, and the editor fills them in from the error's `args` when it surfaces the error: ```xml Unsupported file type ({{mimeType}}) ``` The available argument names for each code come from its catalog entry. Inspect `EngineException.args` on the thrown error to confirm which names are available; a placeholder with no matching argument is left untouched. These placeholders use the `{{argName}}` i18next-style syntax, **not** Android's native positional format specifiers (`%1$s`, `%d`). The resolver reads the resource with the no-argument `Context.getString(resId)` and substitutes the `{{name}}` tokens itself—it never calls the `getString(resId, formatArgs)` overload or `getQuantityString`—so Android's native `String.format` arguments and `` quantity strings do not apply to these resources. If you override a resource the Android-native way (`%1$s` plus format arguments), the user sees a literal `%1$s`. This is a deliberate split from the editor's other resources: the `ly_img_editor_*` strings (titles, labels, dialogs) use native `%s`/`%d` specifiers and are read with the `getString(resId, formatArgs)` overload, while the `ly_img_engine_*` error strings use `{{name}}` tokens and are read with no-argument `getString(resId)`. Both conventions live side by side in the same `strings.xml`, so apply `{{name}}` only to the `ly_img_engine_*` overrides. ## Reusing the Resolver in Custom Error Handling When you handle errors yourself through the editor's `onError` callback, resolve the same copy the built-in dialogs show with the `EngineException.getDisplayMessage(context)` extension. Cast the thrown error to `EngineException` first; it returns your authored resource for a structured error, falling back to the engine's English message—so you never show a blank or a raw code. For errors that aren't an `EngineException`, keep your own fallback. Pair it with `getDisplayDescription(context)` for the longer body copy. It resolves your authored `_description` resource, falls back to the engine's English `hint`, and is `null` when neither exists—so you can show a two-line message and omit the body when there's nothing to add. `EditorConfiguration.remember` must be called inside the `Editor` composable's `configuration` lambda, and `LocalContext.current` only resolves in a composable. Read the context in your composable and pass it into the `onError` handler: ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import ly.img.editor.BasicConfigurationBuilder import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.getDisplayDescription import ly.img.editor.core.getDisplayMessage import ly.img.engine.EngineException @Composable fun MyEditor(onClose: (Throwable?) -> Unit) { val context = LocalContext.current Editor( license = "YOUR_CESDK_LICENSE_KEY", configuration = { EditorConfiguration.remember(::BasicConfigurationBuilder) { onError = { error -> val engineError = error as? EngineException val message = engineError?.getDisplayMessage(context) ?: error.message val description = engineError?.getDisplayDescription(context) val docsUrl = engineError?.docsUrl // Show your own snackbar or dialog with `message` and the optional // `description`, optionally linking `docsUrl`. } } }, onClose = onClose, ) } ``` ## Handling Errors Programmatically To branch on a specific failure rather than just display copy, cast to `EngineException` and switch on its stable `code`. Branching on the code rather than the message keeps your handling stable across releases. ```kotlin import ly.img.engine.EngineErrorCode import ly.img.engine.EngineException onError = { error -> when ((error as? EngineException)?.code) { EngineErrorCode.SCENE_NOT_VALID -> showRecoveryPrompt() else -> showGenericError((error as? EngineException)?.getDisplayMessage(context) ?: error.message.orEmpty()) } } ``` `EngineException` also exposes `hint`, `args`, and `docsUrl` so you can enrich your own UI. ## Verify Your Implementation 1. Add a resource for a code you can trigger, such as `ly_img_engine_error_asset_unsupported_mime_type_for_block`. 2. Reproduce the failure—drag in an unsupported file. 3. Confirm the editor shows your copy. 4. Trigger an error you did **not** author and confirm it falls back to the engine's English message instead of going blank. ## Related - [Error Catalog](https://img.ly/docs/cesdk/android/concepts/error-catalog-z3djzn/) — Every engine error code, its message, and its hint. --- ## 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: "Customization" description: "Control which features are available and how UI components behave, appear, or are arranged in the editor." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) --- --- ## Related Pages - [Page Format](https://img.ly/docs/cesdk/android/user-interface/customization/page-format-496315/) - Define default page size, orientation, and other format settings for your design canvas. - [Crop Presets](https://img.ly/docs/cesdk/android/user-interface/customization/crop-presets-f94f26/) - Define crop preset settings for your Android editor. - [Force Crop](https://img.ly/docs/cesdk/android/user-interface/customization/force-crop-c2854e/) - Apply predefined crop presets programmatically in the Android SDK. - [Movement Constraints](https://img.ly/docs/cesdk/android/user-interface/customization/movement-constraints-8f3a2c/) - Restrict how far blocks can be dragged outside the page in the CE.SDK Android editor. - [Color Palette](https://img.ly/docs/cesdk/android/user-interface/customization/color-palette-429fd9/) - Customize the color palette available to users for consistent branding and styling. - [Rearrange Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/) - Reorder UI buttons across editor components to match your application's workflows. - [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) - Show, hide, or customize the editor's top navigation bar to match your app layout. - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Configure the dock area to show or hide tools, panels, or quick access actions. - [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) - Show or hide side panels to focus the user interface on what matters most for your use case. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) - Customize the inspector bar for editing properties like position, color, and size. - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) - Customize the floating toolbar that appears when a design block is selected on the canvas. - [Video Timeline](https://img.ly/docs/cesdk/android/user-interface/customization/video-timeline-7a0400/) - Customize the video timeline's add-content buttons, header, height, and expanded state. - [Hide Elements](https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/) - Hide complete editor components or remove specific buttons from CE.SDK editor UI components 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: "Canvas Menu" description: "Customize the floating toolbar that appears when a design block is selected on the canvas." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-canvas-menu/SimpleCanvasMenuSolution.kt reference-only import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost @Composable fun SimpleCanvasMenuSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { canvasMenu = { CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-canvas-menu/NewListBuilderCanvasMenuSolution.kt reference-only import android.widget.Toast import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberBringForward import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberSelectGroup import ly.img.editor.core.component.rememberSendBackward import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost @Composable fun NewListBuilderCanvasMenuSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { canvasMenu = { CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.review") } onClick = { Toast .makeText(editorContext.activity, "Review action", Toast.LENGTH_SHORT) .show() } vectorIcon = null textString = { "Review" } contentDescription = { "Review selected block" } } } add { CanvasMenu.Button.rememberSelectGroup() } if (editorContext.isSelectionInGroup) { add { CanvasMenu.Divider.remember() } } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberBringForward() } if (editorContext.canSelectionMove) { add { CanvasMenu.Divider.remember() } } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-canvas-menu/ModifyListBuilderCanvasMenuSolution.kt reference-only import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.bringForward import ly.img.editor.core.component.delete import ly.img.editor.core.component.duplicate import ly.img.editor.core.component.modify import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberBringForward import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberSendBackward import ly.img.editor.core.component.sendBackward import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Music // Add this composable to your NavHost @Composable fun ModifyListBuilderCanvasMenuSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { canvasMenu = { CanvasMenu.remember { listBuilder = { val existingListBuilder = CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberBringForward() } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } } existingListBuilder.modify { addFirst { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.first") } vectorIcon = null textString = { "First" } onClick = {} } } addLast { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.last") } vectorIcon = null textString = { "Last" } onClick = {} } } addAfter(id = CanvasMenu.Button.Id.bringForward, failIfNotFound = true) { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.afterBringForward") } vectorIcon = null textString = { "After Forward" } onClick = {} } } addBefore(id = CanvasMenu.Button.Id.sendBackward, failIfNotFound = true) { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.beforeSendBackward") } vectorIcon = null textString = { "Before Backward" } onClick = {} } } replace(id = CanvasMenu.Button.Id.duplicate, failIfNotFound = true) { CanvasMenu.Button.rememberDuplicate { vectorIcon = { IconPack.Music } } } remove(id = CanvasMenu.Button.Id.delete, failIfNotFound = true) } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-canvas-menu/CanvasMenuItems.kt reference-only import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Music import ly.img.engine.DesignBlockType @Composable fun rememberPredefinedCanvasMenuButton() = CanvasMenu.Button.rememberDuplicate() @Composable fun rememberCustomizedDeleteButton() = CanvasMenu.Button.rememberDelete { textString = { "Remove" } contentDescription = { "Remove selected block" } enabled = { editorContext.selection.type != DesignBlockType.Text } } @Composable fun rememberReviewCanvasMenuButton() = CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.review") } onClick = { Toast .makeText(editorContext.activity, "Review action", Toast.LENGTH_SHORT) .show() } vectorIcon = { IconPack.Music } textString = { "Review" } contentDescription = { "Review selected block" } } @Composable fun rememberCanvasMenuDivider() = CanvasMenu.Divider.remember { modifier = { remember(this) { Modifier .padding(horizontal = 8.dp) .size(width = 1.dp, height = 24.dp) } } } @Composable fun rememberCanvasMenuCustomItem() = EditorComponent.remember { id = { EditorComponentId("com.example.canvasMenu.customItem") } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Custom item clicked", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Review", ) } } } ``` The canvas menu is the floating toolbar that appears next to a selected design block. Configure its item list, adjust existing entries, and add custom actions from your Android editor configuration. ![A selected design block with the Android canvas menu floating above it](./assets/android.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-canvas-menu) ## Canvas Menu Architecture The canvas menu is an `EditorComponent` that the editor positions next to the current selection. It appears only when the editor state and selected block make quick actions relevant. You assemble the menu from these component entries and related types: - **`CanvasMenu.Button`** - A button entry. Use predefined factories such as `CanvasMenu.Button.rememberDuplicate()` or create your own button. - **`CanvasMenu.Divider`** - A visual separator between groups of related actions. - **`EditorComponent`** - The base component type for fully custom canvas menu items. The related `CanvasMenu.Scope` exposes the editor context, selected element, and helper state such as group membership or layer movement support to canvas menu configuration callbacks. For a complete product surface, start from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). This guide focuses on the canvas menu configuration that you can apply to your own editor setup. ## Configuration Canvas menu customization lives in `EditorConfiguration`. The `Editor` composable owns Engine startup; these snippets use the existing editor context and configure only the canvas menu surface. ```kotlin highlight-android-canvas-menu-configuration EditorConfiguration.remember { canvasMenu = { CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } } } } } } ``` `CanvasMenu.remember` keeps the default visibility and decoration unless you override them, but it does not add items on its own. Assign `listBuilder` to populate the menu. By default, the menu appears when a design block is selected, touch interaction is idle, no sheet is open, the selection is neither an audio block nor a page, the editor is not in text edit mode, the scene is paused, and the selected block is visible at the current playback time. The default decoration positions the toolbar near the selected block. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers editor setup more broadly; this page covers the canvas menu portion of that setup. ## Declaring the Item List Use `CanvasMenu.ListBuilder.remember` to declare the complete list of canvas menu items. The order of `add` calls is the display order, and the list replaces the menu contents for this configuration. ```kotlin highlight-android-declare-items CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.review") } onClick = { Toast .makeText(editorContext.activity, "Review action", Toast.LENGTH_SHORT) .show() } vectorIcon = null textString = { "Review" } contentDescription = { "Review selected block" } } } add { CanvasMenu.Button.rememberSelectGroup() } if (editorContext.isSelectionInGroup) { add { CanvasMenu.Divider.remember() } } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberBringForward() } if (editorContext.canSelectionMove) { add { CanvasMenu.Divider.remember() } } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } } ``` - Predefined buttons include common actions such as duplicate, delete, layer reordering, and selecting a parent group. - `CanvasMenu.Divider.remember()` separates action groups. - Custom buttons can be placed alongside predefined items when the action belongs directly in the contextual toolbar. ## Modify Canvas Menu Items Use `modify` when you already have a list builder and only need to add, replace, or remove a few entries. Call it on the builder instance you want to adjust. ```kotlin highlight-android-modify-items existingListBuilder.modify { addFirst { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.first") } vectorIcon = null textString = { "First" } onClick = {} } } addLast { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.last") } vectorIcon = null textString = { "Last" } onClick = {} } } addAfter(id = CanvasMenu.Button.Id.bringForward, failIfNotFound = true) { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.afterBringForward") } vectorIcon = null textString = { "After Forward" } onClick = {} } } addBefore(id = CanvasMenu.Button.Id.sendBackward, failIfNotFound = true) { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.beforeSendBackward") } vectorIcon = null textString = { "Before Backward" } onClick = {} } } replace(id = CanvasMenu.Button.Id.duplicate, failIfNotFound = true) { CanvasMenu.Button.rememberDuplicate { vectorIcon = { IconPack.Music } } } remove(id = CanvasMenu.Button.Id.delete, failIfNotFound = true) } ``` | Operation | Purpose | | --- | --- | | `addFirst` | Add an item at the beginning. | | `addLast` | Add an item at the end. | | `addAfter` | Insert an item after a target ID. | | `addBefore` | Insert an item before a target ID. | | `replace` | Replace the item with the target ID. | | `remove` | Remove the item with the target ID. | > **Warning:** Operations that target a specific ID can throw when `failIfNotFound` is `true` and the ID is missing from the source list. Use predefined IDs such as `CanvasMenu.Button.Id.duplicate` when you target built-in items. > **Note:** The default canvas menu list is empty. Use `modify` carefully when your app inherits items from another configuration, plugin, or starter surface: if that source changes item order in a later editor version, positional edits such as `addBefore` and `addAfter` move with it. Declare the complete list with a fresh `CanvasMenu.ListBuilder.remember` when strict ordering matters. ## Canvas Menu Item Configuration Canvas menu entries are editor components. Each entry needs a unique `EditorComponentId` so the list builder can target it safely. ### Predefined Buttons CE.SDK ships factory functions for common canvas actions. Use them when the built-in behavior matches the action you need, and see the full list of available factories below. ```kotlin highlight-android-predefined-button @Composable fun rememberPredefinedCanvasMenuButton() = CanvasMenu.Button.rememberDuplicate() ``` ### Customize Predefined Buttons Pass a builder block to a predefined factory when you want to override selected properties while keeping the rest of the factory behavior. ```kotlin highlight-android-customize-predefined-button @Composable fun rememberCustomizedDeleteButton() = CanvasMenu.Button.rememberDelete { textString = { "Remove" } contentDescription = { "Remove selected block" } enabled = { editorContext.selection.type != DesignBlockType.Text } } ``` The default delete button is icon-only and is visible when the selected block allows `lifecycle/destroy`. This example keeps the delete action and default visibility gate, then adds a visible label, overrides the accessibility description, and sets a separate enabled predicate that disables the button for text blocks. ### Create New Buttons Create a new `CanvasMenu.Button` when the action is specific to your app. Use a stable, unique ID and provide the action, icon, text, and accessibility description. ```kotlin highlight-android-new-button @Composable fun rememberReviewCanvasMenuButton() = CanvasMenu.Button.remember { id = { EditorComponentId("com.example.canvasMenu.button.review") } onClick = { Toast .makeText(editorContext.activity, "Review action", Toast.LENGTH_SHORT) .show() } vectorIcon = { IconPack.Music } textString = { "Review" } contentDescription = { "Review selected block" } } ``` Use `onClick` for your app-specific behavior. The button still participates in the canvas menu's visibility and item decoration like every other menu entry. ### Dividers Use `CanvasMenu.Divider.remember()` to separate related actions. You can keep the default divider or customize its modifier, visibility, and decoration. ```kotlin highlight-android-divider @Composable fun rememberCanvasMenuDivider() = CanvasMenu.Divider.remember { modifier = { remember(this) { Modifier .padding(horizontal = 8.dp) .size(width = 1.dp, height = 24.dp) } } } ``` ### Create Custom Items Use `EditorComponent.remember` when a button or divider is not enough. A custom item owns its rendering through `decoration`, so it is responsible for layout, click handling, and accessibility behavior. ```kotlin highlight-android-custom-item @Composable fun rememberCanvasMenuCustomItem() = EditorComponent.remember { id = { EditorComponentId("com.example.canvasMenu.customItem") } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Custom item clicked", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Review", ) } } } ``` ## List of Available Canvas Menu Buttons The predefined buttons live in the `CanvasMenu.Button` namespace. Most predefined buttons are icon-only with accessibility descriptions, while `CanvasMenu.Button.rememberSelectGroup()` is text-only. Each factory also provides its default visibility and action behavior. | Button | ID constant | Default behavior | | --- | --- | --- | | `CanvasMenu.Button.rememberBringForward()` | `CanvasMenu.Button.Id.bringForward` | Brings the selected block forward when layer movement is possible. Captions always render above the page, so layer movement is never possible for one. | | `CanvasMenu.Button.rememberSendBackward()` | `CanvasMenu.Button.Id.sendBackward` | Sends the selected block backward when layer movement is possible. Captions always render above the page, so layer movement is never possible for one. | | `CanvasMenu.Button.rememberDuplicate()` | `CanvasMenu.Button.Id.duplicate` | Duplicates the selected block when its `lifecycle/duplicate` scope is allowed. Hidden for a caption, whose copy would land outside the caption track. | | `CanvasMenu.Button.rememberDelete()` | `CanvasMenu.Button.Id.delete` | Deletes the selected block when its `lifecycle/destroy` scope is allowed. | | `CanvasMenu.Button.rememberSelectGroup()` | `CanvasMenu.Button.Id.selectGroup` | Selects the group that contains the selected block. | ## API Reference | API | Category | Purpose | | --- | --- | --- | | `EditorConfiguration.remember(builder=_)` | Config | Create the editor configuration that assigns `canvasMenu`. | | `CanvasMenu.remember(builder=_)` | Config | Create and configure the canvas menu component. | | `CanvasMenu.ListBuilder.remember(builder=_)` | Config | Declare a complete canvas menu item list. | | `listBuilder.modify(builder=_)` | Config | Adjust an existing list builder instance. | | `modify.addFirst(block=_)` | List operation | Add an item at the beginning. | | `modify.addLast(block=_)` | List operation | Add an item at the end. | | `modify.addAfter(id=_, failIfNotFound=_, block=_)` | List operation | Insert an item after a target ID. | | `modify.addBefore(id=_, failIfNotFound=_, block=_)` | List operation | Insert an item before a target ID. | | `modify.replace(id=_, failIfNotFound=_, block=_)` | List operation | Replace an item by ID. | | `modify.remove(id=_, failIfNotFound=_)` | List operation | Remove an item by ID. | | `CanvasMenu.Button.rememberBringForward(builder=_)` | Item | Create the predefined bring-forward button. | | `CanvasMenu.Button.rememberSendBackward(builder=_)` | Item | Create the predefined send-backward button. | | `CanvasMenu.Button.rememberDuplicate(builder=_)` | Item | Create the predefined duplicate button. | | `CanvasMenu.Button.rememberDelete(builder=_)` | Item | Create the predefined delete button. | | `CanvasMenu.Button.rememberSelectGroup(builder=_)` | Item | Create the predefined select-group button. | | `CanvasMenu.Button.remember(builder=_)` | Item | Create a custom canvas menu button. | | `CanvasMenu.Divider.remember(builder=_)` | Layout | Create a divider item. | | `EditorComponent.remember(builder=_)` | Item | Create a fully custom menu item. | | `engine.block.isAllowedByScope(block=_, key=_)` | Support check | Check whether the selected block allows a scope before enabling a custom action. | ## Next Steps - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Configure the bottom toolbar that opens asset libraries and sheets. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) - Configure context-sensitive controls for the selected block. - [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) - Configure the top bar with close, undo, and redo. - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Configure sources shared across editor components. --- ## 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 Palette" description: "Customize the color palette available to users for consistent branding and styling." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/color-palette-429fd9/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Color Palette](https://img.ly/docs/cesdk/android/user-interface/customization/color-palette-429fd9/) --- ```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, ) } ``` Replace the editor's default color swatches with brand colors so fills, strokes, and other color-aware Android editor controls start from the same approved palette. ![Editor showing custom brand color swatches in the Fill and Stroke controls](./assets/android.hero.png) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-color-palette) The Android editor reads palette swatches from `EditorConfiguration.colorPalette`. The list replaces the built-in swatches that appear in the CE.SDK editor UI's fill, stroke, and similar color controls; include any default colors you still want to keep. The sample also registers the same brand colors as a local asset source. That part is useful when your app needs reusable color definitions for custom UI, searches, or other engine workflows. For a dedicated asset-source walkthrough, see [Create a Color Palette](https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/). The [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) uses the same editor configuration model, so you can apply this palette configuration to a complete design editor surface as well. ## Define Brand Colors Keep one data model for the reusable color assets and the editor swatches. The `AssetDefinition` stores labels, tags, and RGB, CMYK, or spot color payloads; `paletteColor` stores the Compose `Color` shown in Android's swatch row. ```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 } ``` Android swatches are screen colors, so CMYK and spot color assets still need an sRGB preview value for `paletteColor`. The engine color remains in `AssetPayload.color` for workflows that query or apply the asset directly. ## Register the Color Library Create the local source before adding color assets to it. The sample removes an existing source with the same ID first so repeated launches in the same editor process stay deterministic. ```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) } ``` Call the registration helper from `EditorConfiguration.onCreate`. In Android editor configurations, use `onCreate` when the callback owns scene creation or loading; this sample creates a blank scene and page before registering the library. ```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) } ``` ## Configure the Editor Palette Assign `colorPalette` inside `EditorConfiguration.remember`. The editor shows the Compose colors in list order and replaces the default Android swatches with this list. ```kotlin highlight-android-config-palette colorPalette = { remember { brandPaletteColors() } } ``` Controls that allow disabling a color, such as Fill and Stroke, reserve one slot for the "no color" button and show up to the first six palette entries. Controls that always require a color can use the full configured list. ## Update a Color Library When you change a local color source at runtime, mutate the source and then notify listeners. This updates the reusable asset library; `EditorConfiguration.colorPalette` remains the Compose swatch list configured by your editor state. ```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) } ``` If your app also lets users remove visible editor swatches, back `colorPalette` with your own Compose state and remove the color from that state before the editor recomposes. ## API Reference | Method | Description | |--------|-------------| | `EditorConfiguration.Companion.remember(builder=_)` | Creates the editor configuration used by the `Editor` composable. | | `EditorConfiguration.colorPalette` | Provides the ordered Compose color list used by Android editor color controls. | | `engine.scene.create(sceneLayout=_)` | Creates the scene that hosts the sample page. | | `engine.block.create(blockType=_)` | Creates the page block that receives the configured dimensions. | | `engine.block.setWidth(block=_, value=_, maintainCrop=_)` | Sets the page width used by the sample scene. | | `engine.block.setHeight(block=_, value=_, maintainCrop=_)` | Sets the page height used by the sample scene. | | `engine.block.appendChild(parent=_, child=_)` | Adds the page block to the created scene. | | `engine.asset.findAllSources()` | Lists registered asset source IDs so the sample can replace an existing local source. | | `engine.asset.removeSource(sourceId=_)` | Removes an existing asset source by ID. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Creates a local asset source for reusable color assets. | | `engine.asset.addAsset(sourceId=_, asset=_)` | Adds a color asset definition to the local source. | | `engine.asset.assetSourceContentsChanged(sourceId=_)` | Notifies listeners that local source contents changed. | | `engine.asset.removeAsset(sourceId=_, assetId=_)` | Removes one asset from a local source. | | Type | Description | |------|-------------| | `ComposeColor(red=_, green=_, blue=_)` | Compose color shown as an Android editor swatch. | | `AssetDefinition` | Stores a reusable 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 CE.SDK's color spaces and where they apply - [Apply Colors](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) — Set fills, strokes, and text color programmatically - [Theming](https://img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938/) — Customize the editor's appearance and color tokens - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — Configure the editor's primary navigation - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) — Tailor which controls appear when a block is selected --- ## 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 Presets" description: "Define crop preset settings for your Android editor." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/crop-presets-f94f26/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Crop Presets](https://img.ly/docs/cesdk/android/user-interface/customization/crop-presets-f94f26/) --- ```kotlin file=@cesdk_android_examples/editor-guides-crop-presets/CropPresetsEditorSolution.kt reference-only import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.Composable import kotlinx.coroutines.delay import ly.img.editor.Editor import ly.img.editor.core.component.Dock import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberCrop 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.sheet.SheetType import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.ShapeType private const val CropPresetsImageBlockName = "crop-presets-image" @Composable fun CropPresetsEditorSolution( 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) val imageBlock = editorContext.engine.block.create(DesignBlockType.Graphic) editorContext.engine.block.setName(imageBlock, name = CropPresetsImageBlockName) editorContext.engine.block.setShape( imageBlock, shape = editorContext.engine.block.createShape(ShapeType.Rect), ) editorContext.engine.block.setWidth(imageBlock, value = 520F) editorContext.engine.block.setHeight(imageBlock, value = 360F) editorContext.engine.block.setPositionX(imageBlock, value = 140F) editorContext.engine.block.setPositionY(imageBlock, value = 120F) 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_4.jpg", ) editorContext.engine.block.setFill(imageBlock, fill = imageFill) editorContext.engine.block.appendChild(parent = page, child = imageBlock) val cropPresetSourceId = "ly.img.crop.presets" val cropPresetContent = """ { "version": "7.0.0", "id": "ly.img.crop.presets", "assets": [ { "id": "aspect-ratio-free", "label": { "en": "Free" }, "payload": { "transformPreset": { "type": "FreeAspectRatio" } }, "groups": ["fixed-ratio"] }, { "id": "aspect-ratio-16-9", "label": { "en": "16:9" }, "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 16, "height": 9 } }, "groups": ["fixed-ratio"] } ] } """.trimIndent() if (cropPresetSourceId in editorContext.engine.asset.findAllSources()) { editorContext.engine.asset.removeSource(cropPresetSourceId) } editorContext.engine.asset.addLocalSourceFromJSON( contentJSON = cropPresetContent, ) } onLoaded = { // Wait for the first editor frame so the demo route opens directly in crop mode. delay(500) val imageBlock = editorContext.engine.block.findByName(CropPresetsImageBlockName).first() editorContext.engine.block.select(imageBlock) editorContext.eventHandler.send( event = EditorEvent.Sheet.Open( SheetType.Crop(mode = SheetType.Crop.Mode.ImageCrop), ), ) } dock = { Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberCrop() } } } } } } }, onClose = onClose, ) } ``` Customize the aspect ratio options users see when they open crop mode in the Android editor. ![Crop presets in the Android crop interface](./assets/crop-presets-android.png) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-crop-presets) Crop presets are asset-source entries with a `payload.transformPreset` value. The Android image crop UI reads aspect ratio presets from `ly.img.crop.presets`. Fixed-size page presets use the same transform preset model, but Android reads them from `ly.img.page.presets`. This guide covers how the built-in crop UI uses both sources, which preset types Android supports in `content.json`, and how to replace or merge preset content for your own editor. ## Using the Built-in Crop UI The crop interface displays preset tiles when users enter crop mode. Aspect ratio crop presets from `ly.img.crop.presets` use their localized labels and render built-in icons from `payload.transformPreset`. Fixed-size page presets from `ly.img.page.presets` use the regular asset list. Add thumbnail metadata, such as `meta.thumbUri`, so the page preset tile has an image to render. If `thumbUri` is missing, Android falls back to `meta.uri`, which is usually not a useful page preset thumbnail. The default crop source includes free aspect ratio presets, fixed aspect ratio presets, and the `Original` content aspect ratio preset. The default page source includes fixed-size presets for common social and print dimensions. Use custom JSON when you want to add, replace, reorder, or merge the presets that appear in crop mode. | Source ID | Use | | --- | --- | | `ly.img.crop.presets` | Free, fixed, and content aspect ratio presets for image or video crop frames | | `ly.img.page.presets` | Fixed-size page presets that set page dimensions and DPI | The Android editor does not expose a crop-preset library entry with multiple `sourceIds`. To replace, reorder, remove, or extend presets, change the assets inside the source that owns that preset family. Aspect ratio presets belong in `ly.img.crop.presets`; fixed-size page presets belong in `ly.img.page.presets`. To add custom presets alongside the defaults, copy the default `content.json`, merge your entries into the same source, and load the merged file. ## Loading Default or Custom Sources CE.SDK includes a default crop preset source. For production apps, self-host or bundle those assets and load them during editor setup. See [Serve Assets From Your Server](https://img.ly/docs/cesdk/android/serve-assets-b0827c/) for the default asset layout and `baseUri` setup. To add, replace, reorder, or merge presets, load a `content.json` file whose `id` matches the source you want to update. If that source is already registered, remove it before loading a full replacement. `engine.asset.addLocalSourceFromJSON(...)` reads the source ID from the JSON file and supports all transform preset types shown below. When your JSON uses `{{base_url}}` placeholders for page preset thumbnails, pass a valid base path when loading from a JSON string. For the default asset layout, that base path is the package asset root that contains `ly.img.page.presets`. When you load a `content.json` URI instead, keep thumbnail paths relative to that file's parent directory. Run the replacement from an editor setup callback where the engine is available. The snippet below replaces `ly.img.crop.presets` with the entries shown in `cropPresetContent`; it does not keep the other default crop presets. Use the same remove-and-load pattern with a JSON file whose `id` is `ly.img.page.presets` when you replace fixed-size page presets. To keep defaults plus custom presets, start from the default `content.json`, add your entries to that same file, and load the merged source with the same source ID: ```kotlin highlight-android-replace-crop-presets val cropPresetSourceId = "ly.img.crop.presets" val cropPresetContent = """ { "version": "7.0.0", "id": "ly.img.crop.presets", "assets": [ { "id": "aspect-ratio-free", "label": { "en": "Free" }, "payload": { "transformPreset": { "type": "FreeAspectRatio" } }, "groups": ["fixed-ratio"] }, { "id": "aspect-ratio-16-9", "label": { "en": "16:9" }, "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 16, "height": 9 } }, "groups": ["fixed-ratio"] } ] } """.trimIndent() if (cropPresetSourceId in editorContext.engine.asset.findAllSources()) { editorContext.engine.asset.removeSource(cropPresetSourceId) } editorContext.engine.asset.addLocalSourceFromJSON( contentJSON = cropPresetContent, ) ``` ## Creating Custom Crop Presets Each preset asset needs an `id`, localized `label`, optional `groups`, and a `payload.transformPreset` object. The `groups` array controls how presets are grouped in the crop sheet. ### Free Aspect Ratio A free aspect ratio preset removes constraints from the crop frame so users can drag each side independently. ```json { "id": "aspect-ratio-free", "label": { "en": "Free", "de": "Frei" }, "payload": { "transformPreset": { "type": "FreeAspectRatio" } }, "groups": ["fixed-ratio"] } ``` ### Fixed Aspect Ratio A fixed aspect ratio preset locks the crop frame to a ratio. The `width` and `height` values define the ratio, not absolute dimensions. ```json { "id": "aspect-ratio-16-9", "label": { "en": "16:9", "de": "16:9" }, "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 16, "height": 9 } }, "groups": ["fixed-ratio"] } ``` ### Content Aspect Ratio A content aspect ratio preset snaps the crop frame back to the intrinsic ratio of the selected image or video content. ```json { "id": "aspect-ratio-original", "label": { "en": "Original", "de": "Original" }, "payload": { "transformPreset": { "type": "ContentAspectRatio" } }, "groups": ["fixed-ratio"] } ``` This preset only works for blocks whose image or video fill has resolvable intrinsic dimensions. Applying it to text, an empty placeholder, a page without matching content dimensions, or media that has not resolved dimensions yet returns an error. ### Fixed Size A fixed-size preset sets exact page dimensions. Android applies `FixedSize` presets from `ly.img.page.presets` by updating the page width, page height, and scene DPI. ```json { "id": "square-post", "label": { "en": "Square Post (1:1)", "de": "Quadratischer Post (1:1)" }, "meta": { "thumbUri": "{{base_url}}/ly.img.page.presets/thumbnails/instagram/ig-square.png" }, "payload": { "transformPreset": { "type": "FixedSize", "width": 1080, "height": 1080, "designUnit": "Pixel" } }, "groups": ["fixed-size"] } ``` The `designUnit` value defines the measurement unit for the page dimensions. Use `Pixel`, `Millimeter`, or `Inch`. The `thumbUri` value points to the thumbnail rendered in the Android page preset list. ## Configuring Source Content The crop preset source can contain free, content, and fixed aspect ratio presets: ```json { "version": "7.0.0", "id": "ly.img.crop.presets", "assets": [ { "id": "aspect-ratio-free", "label": { "en": "Free" }, "payload": { "transformPreset": { "type": "FreeAspectRatio" } }, "groups": ["fixed-ratio"] }, { "id": "aspect-ratio-original", "label": { "en": "Original" }, "payload": { "transformPreset": { "type": "ContentAspectRatio" } }, "groups": ["fixed-ratio"] }, { "id": "aspect-ratio-16-9", "label": { "en": "16:9" }, "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 16, "height": 9 } }, "groups": ["fixed-ratio"] } ] } ``` The page preset source contains fixed-size presets: ```json { "version": "7.0.0", "id": "ly.img.page.presets", "assets": [ { "id": "square-post", "label": { "en": "Square Post (1:1)" }, "meta": { "thumbUri": "{{base_url}}/ly.img.page.presets/thumbnails/instagram/ig-square.png" }, "payload": { "transformPreset": { "type": "FixedSize", "width": 1080, "height": 1080, "designUnit": "Pixel" } }, "groups": ["fixed-size"] } ] } ``` Keep labels localized with at least an `en` entry. Add more locale keys when your app supports multiple languages. For fixed-size page presets, keep thumbnail files under the base path used for `{{base_url}}`, or adjust `thumbUri` to match the relative layout used by your `content.json` URI. ## Troubleshooting - **Presets do not appear:** verify the loaded JSON `id` matches the preset family: `ly.img.crop.presets` for aspect ratio presets, or `ly.img.page.presets` for fixed-size page presets. Confirm the source is registered with `engine.asset.findAllSources()`. - **Custom presets replace defaults unexpectedly:** Android reads one source per preset family. Merge default and custom entries into the matching `content.json` when you want both. - **A preset has no label:** add a `label` object with an `en` value, and add locale-specific values such as `de` when needed. - **A content aspect ratio preset fails:** apply it only to image or video blocks whose intrinsic dimensions are available. - **A fixed-size preset tile has no thumbnail:** add `meta.thumbUri` and make sure the URL resolves. For JSON strings, pass `basePath` when `thumbUri` uses `{{base_url}}`; for `content.json` URIs, make the thumbnail path relative to the source file's parent directory. - **A fixed-size preset does not resize the page:** put the asset in `ly.img.page.presets` and include `width`, `height`, and `designUnit` in `payload.transformPreset`. ## API Reference | API | Description | | --- | ----------- | | `engine.asset.addLocalSourceFromJSON(contentUri=_, matcher=_)` | Loads a `content.json` file and registers the source ID declared in that file | | `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_, matcher=_)` | Loads source content from a JSON string and replaces `{{base_url}}` placeholders with `basePath` | | `engine.asset.removeSource(sourceId=_)` | Removes an existing preset source before loading replacement content | | `engine.asset.findAllSources()` | Lists registered source IDs for debugging and validation | ## Preset Types | Type | Source | Effect | | --- | --- | --- | | `FreeAspectRatio` | `ly.img.crop.presets` | Enables unconstrained crop-frame resizing | | `FixedAspectRatio` | `ly.img.crop.presets` | Locks the crop frame to a width-to-height ratio | | `ContentAspectRatio` | `ly.img.crop.presets` | Matches the selected image or video content ratio | | `FixedSize` | `ly.img.page.presets` | Sets fixed page dimensions and scene 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: "Dock" description: "Configure the dock area to show or hide tools, panels, or quick access actions." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-dock/SimpleDockSolution.kt reference-only import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ly.img.editor.Editor import ly.img.editor.core.component.Dock 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.theme.surface1 @Composable fun SimpleDockSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { dock = { Dock.remember { scope = { remember(this) { Dock.Scope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface1.copy(alpha = 0.95f)) .padding(vertical = 10.dp), ) { it() } } listBuilder = { Dock.ListBuilder.remember { /* Add items */ } } horizontalArrangement = { Arrangement.SpaceEvenly } itemDecoration = { Box(modifier = Modifier.padding(2.dp)) { it() } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-dock/NewListBuilderDockSolution.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.EditorComponentId import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberElementsLibrary import ly.img.editor.core.component.rememberImagesLibrary import ly.img.editor.core.component.rememberStickersLibrary import ly.img.editor.core.component.rememberSystemCamera import ly.img.editor.core.component.rememberSystemGallery import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.iconpack.AddShape import ly.img.editor.core.iconpack.IconPack @Composable fun NewListBuilderDockSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.custom") } vectorIcon = { IconPack.AddShape } textString = { "Custom" } onClick = {} } } add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberSystemCamera() } add { Dock.Button.rememberElementsLibrary() } add { Dock.Button.rememberStickersLibrary() } add { Dock.Button.rememberImagesLibrary() } add { Dock.Button.rememberTextLibrary() } } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-dock/ModifyListBuilderDockSolution.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.EditorComponentId import ly.img.editor.core.component.modify import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberShapesLibrary import ly.img.editor.core.component.rememberSystemCamera import ly.img.editor.core.component.rememberSystemGallery import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.component.shapesLibrary import ly.img.editor.core.component.systemCamera import ly.img.editor.core.component.systemGallery import ly.img.editor.core.component.textLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun ModifyListBuilderDockSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { dock = { Dock.remember { listBuilder = { val existingListBuilder = Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberSystemCamera() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberShapesLibrary() } } existingListBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.first") } vectorIcon = null textString = { "First Button" } onClick = {} } } addLast { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.last") } vectorIcon = null textString = { "Last Button" } onClick = {} } } addAfter(id = Dock.Button.Id.systemGallery, failIfNotFound = true) { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.afterSystemGallery") } vectorIcon = null textString = { "After System Gallery" } onClick = {} } } addBefore(id = Dock.Button.Id.systemCamera, failIfNotFound = true) { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.beforeSystemCamera") } vectorIcon = null textString = { "Before System Camera" } onClick = {} } } replace(id = Dock.Button.Id.textLibrary, failIfNotFound = true) { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.replacedTextLibrary") } vectorIcon = null textString = { "Replaced Text Library" } onClick = {} } } remove(id = Dock.Button.Id.shapesLibrary, failIfNotFound = true) } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-dock/DockItems.kt reference-only import android.widget.Toast import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberImagesLibrary import ly.img.editor.core.event.EditorEvent import ly.img.editor.core.iconpack.AddImageForeground import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Music import ly.img.editor.core.sheet.SheetType @Composable fun rememberPredefinedDockButton() = Dock.Button.rememberImagesLibrary() @Composable fun rememberCustomizedDockButton() = Dock.Button.rememberImagesLibrary { textString = { "Brand Images" } vectorIcon = { IconPack.AddImageForeground } contentDescription = { "Open brand images" } enabled = { true } visible = { true } } @Composable fun rememberDockButton() = Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.newButton") } scope = { remember(this) { Dock.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.Volume())) } icon = { Icon( imageVector = IconPack.Music, contentDescription = null, ) } text = { Text( text = "Volume", ) } enabled = { true } } @Composable fun rememberCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.dock.newCustomItem") } scope = { remember(this) { Dock.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Dock action clicked", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Custom", ) } } } ``` The dock is the horizontal toolbar along the bottom of the editor. Declare its buttons from scratch for strict control, or adjust an existing list to add, replace, and remove individual entries. ![The editor with a configured dock of library buttons along the bottom](./assets/android.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-dock) ## Dock Architecture The dock is an `EditorComponent` that the editor renders as the horizontal toolbar at the bottom. It is selection-independent, and each item can decide its own visibility. You assemble the dock from these component entries and related types: - **`Dock.Button`** - The built-in button implementation, with an action plus an icon and optional text. - **`EditorComponent`** - The base component type for fully custom dock items. The related `Dock.Scope` and `Dock.ItemScope` expose the editor context, event handler, and engine to dock configuration callbacks. The dock gives users quick access to content libraries such as images, text, shapes, and stickers, plus editing tools such as crop, resize, filters, and adjustments. For a complete product surface, start from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). This guide focuses on the dock configuration that you can apply to your own editor setup. ## Configuration Dock customization lives in `EditorConfiguration`. The `Editor` composable owns Engine startup; these snippets use the existing editor context and configure only the dock surface. ```kotlin highlight-android-dock-configuration EditorConfiguration.remember { dock = { Dock.remember { scope = { remember(this) { Dock.Scope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface1.copy(alpha = 0.95f)) .padding(vertical = 10.dp), ) { it() } } listBuilder = { Dock.ListBuilder.remember { /* Add items */ } } horizontalArrangement = { Arrangement.SpaceEvenly } itemDecoration = { Box(modifier = Modifier.padding(2.dp)) { it() } } } } } ``` `Dock.remember` keeps the default visibility and decoration unless you override them, but it does not add items on its own. Assign `listBuilder` to populate the dock. Supplying an empty `Dock.ListBuilder.remember { }` hides all dock items, while `visible` can hide or show the whole dock component. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers editor setup more broadly; this page covers the dock portion of that setup. ## Declaring the Item List Use `Dock.ListBuilder.remember` to declare the complete list of dock items. The order of `add` calls is the display order, and the list replaces the dock contents for this configuration. ```kotlin highlight-android-new-list-builder Dock.ListBuilder.remember { add { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.custom") } vectorIcon = { IconPack.AddShape } textString = { "Custom" } onClick = {} } } add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberSystemCamera() } add { Dock.Button.rememberElementsLibrary() } add { Dock.Button.rememberStickersLibrary() } add { Dock.Button.rememberImagesLibrary() } add { Dock.Button.rememberTextLibrary() } } ``` - Predefined buttons open common libraries, sheets, and capture flows. - Custom buttons can sit alongside predefined items when the action belongs directly in the bottom toolbar. - A full list is the most predictable option for product UIs that need stable ordering across editor versions. Common [predefined dock buttons are listed below](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/#list-of-available-dock-buttons). ## Permissions for Device Capabilities Some predefined dock buttons open device capabilities. `Dock.Button.rememberSystemGallery()` opens the configured in-editor gallery-backed asset library, so any media permissions depend on how your asset-library integration reads device media. `Dock.Button.rememberSystemCamera()` uses the editor's camera capture flow; it shows the camera permission flow only when your app declares `android.permission.CAMERA`. The system camera flow writes captures through the SDK-provided `${applicationId}.ly.img.editor.fileprovider` authority. Do not declare a second FileProvider for that dock button; only declare and request the Android permissions your integration actually needs. ## Modify Dock Items Use `modify` when you already have a list builder and only need to add, replace, or remove a few entries. Call it on the builder instance you want to adjust. ```kotlin highlight-android-modify-list-builder val existingListBuilder = Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberSystemCamera() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberShapesLibrary() } } existingListBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.first") } vectorIcon = null textString = { "First Button" } onClick = {} } } addLast { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.last") } vectorIcon = null textString = { "Last Button" } onClick = {} } } addAfter(id = Dock.Button.Id.systemGallery, failIfNotFound = true) { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.afterSystemGallery") } vectorIcon = null textString = { "After System Gallery" } onClick = {} } } addBefore(id = Dock.Button.Id.systemCamera, failIfNotFound = true) { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.beforeSystemCamera") } vectorIcon = null textString = { "Before System Camera" } onClick = {} } } replace(id = Dock.Button.Id.textLibrary, failIfNotFound = true) { Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.replacedTextLibrary") } vectorIcon = null textString = { "Replaced Text Library" } onClick = {} } } remove(id = Dock.Button.Id.shapesLibrary, failIfNotFound = true) } ``` | Operation | Purpose | | --- | --- | | `addFirst` | Add an item at the beginning. | | `addLast` | Add an item at the end. | | `addAfter` | Insert an item after a target ID. | | `addBefore` | Insert an item before a target ID. | | `replace` | Replace the item with the target ID. | | `remove` | Remove the item with the target ID. | > **Warning:** Operations that target a specific ID can throw when `failIfNotFound` is `true` and the ID is missing from the source list. Use predefined IDs such as `Dock.Button.Id.systemGallery` when you target built-in items. > **Note:** The default dock list can change between editor versions, so use `modify` carefully when your app inherits items from another configuration, plugin, or starter surface. Declare the complete list with a fresh `Dock.ListBuilder.remember` when strict ordering matters. ## Dock.Item Configuration Each dock item is an `EditorComponent`, and its `id` must be unique. You can use predefined buttons, customize a predefined button, create a new button, or provide a fully custom item. ### Predefined Buttons Start with a predefined button from `Dock.Button`. The [full list](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/#list-of-available-dock-buttons) is below. ```kotlin highlight-android-predefined-button @Composable fun rememberPredefinedDockButton() = Dock.Button.rememberImagesLibrary() ``` ### Customize Predefined Buttons Most predefined buttons accept a `Dock.ButtonBuilder` block, so override only the properties you need. This example keeps the images-library behavior, changes the label and accessibility description, and shows where optional icon, `enabled`, and `visible` overrides belong. ```kotlin highlight-android-customize-button @Composable fun rememberCustomizedDockButton() = Dock.Button.rememberImagesLibrary { textString = { "Brand Images" } vectorIcon = { IconPack.AddImageForeground } contentDescription = { "Open brand images" } enabled = { true } visible = { true } } ``` - **`textString`** sets the visible label. - **`vectorIcon`** supplies an `ImageVector` when you want to replace or explicitly set the button icon. - **`contentDescription`** is used by accessibility services. - **`enabled`** and **`visible`** can be wired to state predicates; the sample leaves them always active and visible. ### Create New Buttons Create a new `Dock.Button` when no predefined action fits. The button id is required and should be stable across recompositions. ```kotlin highlight-android-new-button @Composable fun rememberDockButton() = Dock.Button.remember { id = { EditorComponentId("my.package.dock.button.newButton") } scope = { remember(this) { Dock.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.Volume())) } icon = { Icon( imageVector = IconPack.Music, contentDescription = null, ) } text = { Text( text = "Volume", ) } enabled = { true } } ``` - **`id`** identifies the item for rendering and later `modify` operations. - **`onClick`** runs when the user taps the button. - **`icon`** and **`text`** render the button content. Use `vectorIcon` and `textString` for the simpler string/vector variants. - **`visible`** and **`enabled`** keep state logic out of the label and icon. ### Create Custom Items For full control over rendering, create an `EditorComponent` and draw your UI in `decoration`. ```kotlin highlight-android-custom-item @Composable fun rememberCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.dock.newCustomItem") } scope = { remember(this) { Dock.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Dock action clicked", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Custom", ) } } } ``` Use this pattern for non-button dock entries. You are responsible for layout, click handling, accessibility, and visual states inside the decoration. ## List of Available Dock Buttons Predefined buttons are composable factory functions in the `Dock.Button` namespace. Most functions return a dock button with default properties that you can customize as shown in [Customize Predefined Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/#customize-predefined-buttons). `Dock.Button.rememberAssetLibrary()` is the exception: it returns an `EditorComponent` and uses `EditorComponentBuilder`, so customize its `id`, `modifier`, `visible`, or `decoration` instead of button-only properties such as `textString` and `vectorIcon`. Target these entries in `modify` operations with the matching `Dock.Button.Id` constant. | Button | ID | Description | | --- | --- | --- | | `Dock.Button.rememberAssetLibrary()` | `Dock.Button.Id.assetLibrary` | Opens the full asset library sheet. | | `Dock.Button.rememberElementsLibrary()` | `Dock.Button.Id.elementsLibrary` | Opens the elements library. The content is sourced from the [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/). | | `Dock.Button.rememberOverlaysLibrary()` | `Dock.Button.Id.overlaysLibrary` | Opens the overlays library. | | `Dock.Button.rememberImagesLibrary()` | `Dock.Button.Id.imagesLibrary` | Opens the images library. | | `Dock.Button.rememberTextLibrary()` | `Dock.Button.Id.textLibrary` | Opens the text library. | | `Dock.Button.rememberShapesLibrary()` | `Dock.Button.Id.shapesLibrary` | Opens the shapes library. | | `Dock.Button.rememberStickersLibrary()` | `Dock.Button.Id.stickersLibrary` | Opens the stickers library. | | `Dock.Button.rememberStickersAndShapesLibrary()` | `Dock.Button.Id.stickersAndShapesLibrary` | Opens the combined stickers and shapes library. | | `Dock.Button.rememberAudiosLibrary()` | `Dock.Button.Id.audiosLibrary` | Opens the audio library. | | `Dock.Button.rememberVoiceoverRecord()` | `Dock.Button.Id.voiceoverRecord` | Opens the voiceover recording sheet. | | `Dock.Button.rememberCaptions()` | `Dock.Button.Id.captions` | Opens the captions sheet. It carries no visibility condition of its own, so add it only to the docks where captions apply; the Video Editor Starter Kit already includes it. See [Edit Captions](https://img.ly/docs/cesdk/android/edit-video/edit-captions-ed9c17/). | | `Dock.Button.rememberSystemGallery()` | `Dock.Button.Id.systemGallery` | Opens system gallery content through the configured asset library. | | `Dock.Button.rememberSystemCamera()` | `Dock.Button.Id.systemCamera` | Captures media with the system camera. | | `Dock.Button.rememberImglyCamera()` | `Dock.Button.Id.imglyCamera` | Captures media with the IMG.LY camera integration. Add the camera dependency with the same version as the editor before using it. | | `Dock.Button.rememberReorder()` | `Dock.Button.Id.reorder` | Opens the reorder sheet. Shown only when the background track contains at least two clips. | | `Dock.Button.rememberAdjustments()` | `Dock.Button.Id.adjustments` | Opens the adjustments sheet when the current page allows adjustments. | | `Dock.Button.rememberFilter()` | `Dock.Button.Id.filter` | Opens the filter sheet when the current page allows filters. | | `Dock.Button.rememberEffect()` | `Dock.Button.Id.effect` | Opens the effect sheet when the current page allows effects. | | `Dock.Button.rememberBlur()` | `Dock.Button.Id.blur` | Opens the blur sheet when the current page allows blur. | | `Dock.Button.rememberCrop()` | `Dock.Button.Id.crop` | Opens the crop sheet when the current page supports crop and the layer scope allows it. | | `Dock.Button.rememberResizeAll()` | `Dock.Button.Id.resizeAll` | Opens the resize sheet. | ## API Reference | API | Category | Purpose | | --- | --- | --- | | `EditorConfiguration.remember { dock = { ... } }` | Config | Configure the dock component for an editor instance. | | `Dock.remember { ... }` | Config | Create and configure a dock component. | | `Dock.ListBuilder.remember { add { ... } }` | Config | Build a complete dock item list in display order. | | `add { ... }` | List operation | Append an item to a new list builder. | | `existingListBuilder.modify { ... }` | Config | Adjust an existing list builder without rewriting the source list. | | `addFirst { ... }` / `addLast { ... }` | List operation | Add items at the beginning or end of the source list. | | `addAfter(id=_, failIfNotFound=_)` / `addBefore(id=_, failIfNotFound=_)` | List operation | Insert items next to a source item id. | | `replace(id=_, failIfNotFound=_)` | List operation | Replace a source item by id. | | `remove(id=_, failIfNotFound=_)` | List operation | Remove a source item by id. | | `Dock.Button.rememberSystemGallery()` | Item | Add the system gallery dock button. | | `Dock.Button.rememberSystemCamera()` | Item | Add the system camera dock button. | | `Dock.Button.rememberElementsLibrary()` | Item | Add the elements library dock button. | | `Dock.Button.rememberStickersLibrary()` | Item | Add the stickers library dock button. | | `Dock.Button.rememberImagesLibrary()` | Item | Add the images library dock button. | | `Dock.Button.rememberTextLibrary()` | Item | Add the text library dock button. | | `Dock.Button.rememberShapesLibrary()` | Item | Add the shapes library dock button. | | `Dock.Button.remember { id; onClick; icon; text }` | Item | Create a custom dock button. | | `EditorComponent.remember { decoration = { ... } }` | Item | Create a fully custom dock item. | | `EditorComponentId("...")` | Item | Define stable ids for custom items and modify targets. | | `editorContext.eventHandler.send(event=_)` | Event | Dispatch editor UI events from custom dock item actions. | | `EditorEvent.Sheet.Open(type=SheetType.Volume())` | Event | Open the volume sheet from a custom dock button action. | ## Next Steps - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) - Context-sensitive editing controls for the selected block. - [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) - Configure the editor's top bar. - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) - The floating toolbar for the current selection. - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - The sources behind the dock's library buttons. --- ## 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 Crop" description: "Apply predefined crop presets programmatically in the Android SDK." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/force-crop-c2854e/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Force Crop](https://img.ly/docs/cesdk/android/user-interface/customization/force-crop-c2854e/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-force-crop/ForceCropEditorSolution.kt reference-only import androidx.compose.foundation.layout.Arrangement 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.component.Dock import ly.img.editor.core.component.data.ForceCropConfiguration import ly.img.editor.core.component.data.ForceCropMode import ly.img.editor.core.component.data.ForceCropPresetCandidate import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberCrop import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.event.EditorEvent @Composable fun ForceCropEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { val context = LocalContext.current Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { editorContext.engine.scene.createFromImage( imageUri = "https://img.ly/static/ubq_samples/sample_4.jpg".toUri(), ) val cropPresetsSourceId = "ly.img.crop.presets" val pagePresetsSourceId = "ly.img.page.presets" if (editorContext.engine.asset.findAllSources().contains(cropPresetsSourceId)) { editorContext.engine.asset.removeSource(cropPresetsSourceId) } if (editorContext.engine.asset.findAllSources().contains(pagePresetsSourceId)) { editorContext.engine.asset.removeSource(pagePresetsSourceId) } editorContext.engine.asset.addLocalSourceFromJSON( contentJSON = context.assets.open("force_crop_content.json").bufferedReader().use { it.readText() }, basePath = editorContext.baseUri.toString(), ) editorContext.engine.asset.addLocalSourceFromJSON( contentJSON = context.assets.open("force_crop_page_content.json").bufferedReader().use { it.readText() }, basePath = editorContext.baseUri.toString(), ) } onLoaded = { val page = requireNotNull(editorContext.engine.scene.getCurrentPage()) val cropPresetsSourceId = "ly.img.crop.presets" val pagePresetsSourceId = "ly.img.page.presets" // Android treats the top-level preset and presetCandidates as one candidate list. val configuration = ForceCropConfiguration( sourceId = cropPresetsSourceId, presetId = "instagram-portrait", presetCandidates = listOf( ForceCropPresetCandidate( sourceId = pagePresetsSourceId, presetId = "profile-photo", ), ForceCropPresetCandidate( sourceId = cropPresetsSourceId, presetId = "aspect-ratio-1-1", ), ), mode = ForceCropMode.IfNeeded(), ) editorContext.eventHandler.send( event = EditorEvent.ApplyForceCrop( designBlock = page, configuration = configuration, ), ) } dock = { Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberCrop() } } } } } } }, onClose = onClose, ) } ``` Apply crop presets programmatically to enforce specific aspect ratios or dimensions on Android editor blocks. ![Force crop applied in the Android editor](./assets/android.hero.png) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-force-crop) The Android force crop flow uses `EditorEvent.ApplyForceCrop` with a `ForceCropConfiguration`. The event targets a page or any other croppable block, resolves the configured preset candidates from asset sources, selects the best match for the block's current dimensions, and applies it according to the selected mode. This guide covers how to apply crop presets programmatically, load custom fixed aspect ratio or fixed size presets, and control whether the crop UI opens after applying the preset. ## Loading Crop Presets CE.SDK ships with default fixed aspect ratio presets in the `ly.img.crop.presets` source and fixed size page presets in the `ly.img.page.presets` source. The sample below replaces both asset sources with guide-local JSON files so the crop sheet only shows the candidate presets used by this guide. For the full crop preset structure, see [Crop Presets](https://img.ly/docs/cesdk/android/user-interface/customization/crop-presets-f94f26/). ```kotlin highlight-android-populate-asset-source val cropPresetsSourceId = "ly.img.crop.presets" val pagePresetsSourceId = "ly.img.page.presets" if (editorContext.engine.asset.findAllSources().contains(cropPresetsSourceId)) { editorContext.engine.asset.removeSource(cropPresetsSourceId) } if (editorContext.engine.asset.findAllSources().contains(pagePresetsSourceId)) { editorContext.engine.asset.removeSource(pagePresetsSourceId) } editorContext.engine.asset.addLocalSourceFromJSON( contentJSON = context.assets.open("force_crop_content.json").bufferedReader().use { it.readText() }, basePath = editorContext.baseUri.toString(), ) editorContext.engine.asset.addLocalSourceFromJSON( contentJSON = context.assets.open("force_crop_page_content.json").bufferedReader().use { it.readText() }, basePath = editorContext.baseUri.toString(), ) ``` The `cropPresetsSourceId` and `pagePresetsSourceId` constants are the `"ly.img.crop.presets"` and `"ly.img.page.presets"` source IDs. `ForceCropConfiguration` uses these source IDs later to resolve the available preset candidates. ## Creating Custom Crop Presets The guide-local asset files define fixed aspect ratio and fixed size presets through `payload.transformPreset`: ```json file=@cesdk_android_examples/editor-guides-configuration-force-crop/assets/force_crop_content.json { "version": "4.0.0", "id": "ly.img.crop.presets", "assets": [ { "id": "instagram-portrait", "label": { "en": "4:5", "de": "4:5" }, "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 4, "height": 5 } }, "groups": ["fixed-ratio"] }, { "id": "aspect-ratio-1-1", "label": { "en": "1:1", "de": "1:1" }, "meta": { "thumbUri": "{{base_url}}/ly.img.crop.presets/thumbnails/ratio-1-1.png" }, "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 1, "height": 1 } }, "groups": ["fixed-ratio"] } ] } ``` ```json file=@cesdk_android_examples/editor-guides-configuration-force-crop/assets/force_crop_page_content.json { "version": "4.0.0", "id": "ly.img.page.presets", "assets": [ { "id": "profile-photo", "label": { "en": "Profile Photo (400x400)", "de": "Profile Photo (400x400)" }, "meta": { "thumbUri": "{{base_url}}/ly.img.page.presets/thumbnails/instagram/ig-square.png" }, "payload": { "transformPreset": { "type": "FixedSize", "width": 400, "height": 400, "designUnit": "Pixel" } }, "groups": ["fixed-size"] } ] } ``` ### Fixed Aspect Ratio Presets Fixed aspect ratio presets keep a ratio while allowing flexible dimensions. The sample includes an `instagram-portrait` preset with a `4:5` ratio. ### Fixed Size Presets Fixed size presets enforce exact pixel dimensions and belong to `ly.img.page.presets` on Android. The sample includes a `profile-photo` preset with a `400x400` fixed size, `Pixel` design unit, and `meta.thumbUri` thumbnail so the preset tile can render in the crop UI. ## Applying a Crop Preset Create a `ForceCropConfiguration` with the preset source, preset ID, additional preset candidates, and mode. Android combines the top-level `sourceId` and `presetId` with `presetCandidates`, skips missing sources or preset IDs, then selects the best match for the target block's current dimensions. Send the configuration through the editor event handler in `onLoaded` or later so the target block already exists. Applying force crop in `onCreate` is too early because the editor has not finished loading the scene. ```kotlin highlight-android-apply-force-crop val cropPresetsSourceId = "ly.img.crop.presets" val pagePresetsSourceId = "ly.img.page.presets" // Android treats the top-level preset and presetCandidates as one candidate list. val configuration = ForceCropConfiguration( sourceId = cropPresetsSourceId, presetId = "instagram-portrait", presetCandidates = listOf( ForceCropPresetCandidate( sourceId = pagePresetsSourceId, presetId = "profile-photo", ), ForceCropPresetCandidate( sourceId = cropPresetsSourceId, presetId = "aspect-ratio-1-1", ), ), mode = ForceCropMode.IfNeeded(), ) editorContext.eventHandler.send( event = EditorEvent.ApplyForceCrop( designBlock = page, configuration = configuration, ), ) ``` ## Understanding Crop Modes The `mode` parameter controls how the editor responds after applying a crop preset. ### Silent Mode `ForceCropMode.Silent` applies the preset without opening the crop UI. Use it when the crop should happen as part of a background workflow. ### Always Mode `ForceCropMode.Always` applies the preset and opens the crop sheet immediately. The crop UI keeps the preset dimensions locked while allowing the user to adjust the crop area. ### If Needed Mode `ForceCropMode.IfNeeded` compares the preset with the current frame dimensions. The preset is applied and the crop sheet opens only when the current block differs from the target by more than the configured threshold. ## Applying Force Crop to Pages When the target block is a page, Android temporarily enables page resize interaction while applying the preset. `Silent` mode restores the previous resize setting after the operation, while `Always` and `IfNeeded` keep the crop sheet open when a crop is applied. ## Troubleshooting - **The target block fails to crop:** verify the block exists and supports cropping before sending the event. Use `engine.block.supportsCrop(block=_)` for the same support check Android runs internally. - **No preset is applied:** Android resolves the configured `sourceId` and `presetId` plus every `ForceCropPresetCandidate`. Missing sources or preset IDs are skipped, so inspect `engine.asset.findAllSources()` and confirm each configured ID matches the loaded JSON assets. - **No crop UI appears:** `ForceCropMode.Silent` intentionally applies the preset without opening the crop sheet. Use `ForceCropMode.Always` or `ForceCropMode.IfNeeded(threshold=_)` when users should adjust the crop after the preset is applied. ## API Reference | API | Description | | --- | ----------- | | `editorContext.eventHandler.send(event=_)` | Dispatches `EditorEvent.ApplyForceCrop` after the editor has loaded the scene | | `engine.asset.addLocalSourceFromJSON(contentJSON=_, basePath=_)` | Loads guide-local crop preset JSON into an asset source | | `engine.asset.findAllSources()` | Lists registered asset sources before replacing the preset sources | | `engine.asset.removeSource(sourceId=_)` | Removes an existing preset source before loading replacement presets | | `engine.block.supportsCrop(block=_)` | Checks whether the target block supports cropping | ## Key Types | Type | Purpose | | ---- | ------- | | `EditorEvent.ApplyForceCrop(designBlock=_, configuration=_)` | Editor event consumed by the Android editor event handler | | `ForceCropConfiguration(sourceId=_, presetId=_, mode=_, presetCandidates=_)` | Configuration object for the target preset, candidate presets, and crop mode | | `ForceCropPresetCandidate(sourceId=_, presetId=_)` | Candidate preset identifier made from an asset source ID and preset ID | | `ForceCropMode.Silent` | Applies the preset without opening the crop UI | | `ForceCropMode.Always` | Applies the preset and opens the crop UI | | `ForceCropMode.IfNeeded(threshold=_)` | Opens the crop UI only when the preset changes the block beyond the threshold | ## Next Steps - [Hide Elements](https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/) - Hide complete editor components or remove specific buttons from CE.SDK editor UI components on Android. - [Rearrange Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/) - Reorder UI buttons across editor components to match your application's 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: "Hide Elements" description: "Hide complete editor components or remove specific buttons from CE.SDK editor UI components on Android." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Hide Elements](https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-hide-elements/HideElementsEditorSolution.kt reference-only import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp import ly.img.editor.Editor import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.Dock import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.closeEditor import ly.img.editor.core.component.crop import ly.img.editor.core.component.delete import ly.img.editor.core.component.duplicate import ly.img.editor.core.component.modify import ly.img.editor.core.component.redo import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberBringForward import ly.img.editor.core.component.rememberCloseEditor import ly.img.editor.core.component.rememberCrop import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberExport import ly.img.editor.core.component.rememberFormatText import ly.img.editor.core.component.rememberLayer import ly.img.editor.core.component.rememberRedo import ly.img.editor.core.component.rememberSendBackward import ly.img.editor.core.component.rememberShapesLibrary import ly.img.editor.core.component.rememberSystemCamera import ly.img.editor.core.component.rememberSystemGallery import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.component.rememberUndo import ly.img.editor.core.component.shapesLibrary import ly.img.editor.core.component.textLibrary import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember @Composable fun HideElementsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { dock = { rememberHiddenDock() } navigationBar = { rememberMinimalNavigationBar() } canvasMenu = { rememberCanvasMenuWithoutDuplicateAndDelete() } inspectorBar = { rememberInspectorBarWithoutCropAndDelete() } } }, onClose = onClose, ) } @Composable private fun rememberHiddenDock() = Dock.remember { visible = { false } } @Composable private fun rememberDockWithoutTextAndShapes() = Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberSystemCamera() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberShapesLibrary() } }.modify { remove(id = Dock.Button.Id.textLibrary, failIfNotFound = true) remove(id = Dock.Button.Id.shapesLibrary, failIfNotFound = true) } } } @Composable private fun rememberNavigationBarWithoutCloseAndRedo() = NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } add { NavigationBar.Button.rememberExport() } } }.modify { remove(id = NavigationBar.Button.Id.closeEditor, failIfNotFound = true) remove(id = NavigationBar.Button.Id.redo, failIfNotFound = true) } } } @Composable private fun rememberCanvasMenuWithoutDuplicateAndDelete() = CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberBringForward() } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } }.modify { remove(id = CanvasMenu.Button.Id.duplicate, failIfNotFound = true) remove(id = CanvasMenu.Button.Id.delete, failIfNotFound = true) } } } @Composable private fun rememberInspectorBarWithoutCropAndDelete() = InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.rememberCrop() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberDelete() } }.modify { remove(id = InspectorBar.Button.Id.crop, failIfNotFound = true) remove(id = InspectorBar.Button.Id.delete, failIfNotFound = true) } } } @Composable private fun rememberMinimalNavigationBar() = NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberExport() } } } } } ``` Hide complete editor components or remove individual buttons to create a more focused editing experience. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-hide-elements) ## Overview CE.SDK editor components are configured through `EditorConfiguration`. You can hide a whole component with its `visible` property, or keep the component visible and remove specific actions from its `listBuilder`. | Approach | Android API | Result | |----------|-------------|--------| | Hide component | `visible = { false }` on the component builder | The component does not render or reserve editor space | | Remove items | `listBuilder.modify { remove(id = ...) }` | The selected buttons are removed while the component remains available | > **Note:** The [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) shows a complete > editor surface. The snippets below stay focused on the editor configuration > APIs so you can apply them to your own editor setup. ## Hiding the Dock Set `visible` to `false` on `Dock.remember` when the dock should be hidden for the whole editing session: ```kotlin highlight-android-hide-dock @Composable private fun rememberHiddenDock() = Dock.remember { visible = { false } } ``` Use this approach when the component itself should disappear. Removing every dock item only removes the actions from the list builder; `visible = { false }` also removes the component container. ## Removing Items from the Dock Start from the dock buttons your editor needs, then call `modify` on the list builder and remove buttons by their typed IDs: ```kotlin highlight-android-remove-dock-items @Composable private fun rememberDockWithoutTextAndShapes() = Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberSystemCamera() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberShapesLibrary() } }.modify { remove(id = Dock.Button.Id.textLibrary, failIfNotFound = true) remove(id = Dock.Button.Id.shapesLibrary, failIfNotFound = true) } } } ``` This example removes the text and shape library buttons while keeping the gallery and camera buttons available. ## Removing Items from the Navigation Bar Navigation bar items are usually grouped with `aligned`. Keep those groups in the source list builder, then remove buttons by ID: ```kotlin highlight-android-remove-navigation-bar-items @Composable private fun rememberNavigationBarWithoutCloseAndRedo() = NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } add { NavigationBar.Button.rememberExport() } } }.modify { remove(id = NavigationBar.Button.Id.closeEditor, failIfNotFound = true) remove(id = NavigationBar.Button.Id.redo, failIfNotFound = true) } } } ``` Here the close and redo actions are removed, while undo and export remain in the end-aligned group. ## Removing Items from the Canvas Menu The canvas menu appears for selected blocks. Remove contextual actions from its list builder when users should not see specific operations: ```kotlin highlight-android-remove-canvas-menu-items @Composable private fun rememberCanvasMenuWithoutDuplicateAndDelete() = CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberBringForward() } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() } }.modify { remove(id = CanvasMenu.Button.Id.duplicate, failIfNotFound = true) remove(id = CanvasMenu.Button.Id.delete, failIfNotFound = true) } } } ``` The snippet keeps layer ordering actions and removes duplicate and delete. ## Removing Items from the Inspector Bar The inspector bar is also selection-dependent. Configure the available actions with `InspectorBar.ListBuilder.remember`, then remove any actions that do not belong in your workflow: ```kotlin highlight-android-remove-inspector-bar-items @Composable private fun rememberInspectorBarWithoutCropAndDelete() = InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.rememberCrop() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberDelete() } }.modify { remove(id = InspectorBar.Button.Id.crop, failIfNotFound = true) remove(id = InspectorBar.Button.Id.delete, failIfNotFound = true) } } } ``` This keeps layer and text formatting actions while removing crop and delete. Each component exposes built-in button IDs through its own namespace: | Component | ID namespace | Example | |-----------|--------------|---------| | Dock | `Dock.Button.Id.*` | `Dock.Button.Id.textLibrary` | | Navigation Bar | `NavigationBar.Button.Id.*` | `NavigationBar.Button.Id.closeEditor` | | Canvas Menu | `CanvasMenu.Button.Id.*` | `CanvasMenu.Button.Id.delete` | | Inspector Bar | `InspectorBar.Button.Id.*` | `InspectorBar.Button.Id.crop` | ## Creating a Minimal UI Combine the same patterns to build a canvas-focused editor. Hide the dock and replace the navigation bar with only the actions you want to keep: ```kotlin highlight-android-minimal-ui dock = { rememberHiddenDock() } navigationBar = { rememberMinimalNavigationBar() } ``` Define the reduced navigation bar as a small list builder: ```kotlin highlight-android-minimal-navigation-bar @Composable private fun rememberMinimalNavigationBar() = NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberExport() } } } } } ``` Use a new list builder when you want full control over the visible actions. Use `modify` when you want to start from an existing component and remove only a few items. ## API Reference | API | Purpose | |-----|---------| | `EditorConfiguration.remember { ... }` | Creates the editor configuration that owns component customization | | `Dock.remember { visible = { false } }` | Configures the dock and hides it completely | | `Dock.ListBuilder.remember { ... }` | Defines dock buttons before removal | | `NavigationBar.ListBuilder.remember { ... }` | Defines navigation bar buttons and alignment groups before removal | | `CanvasMenu.ListBuilder.remember { ... }` | Defines contextual canvas menu actions before removal | | `InspectorBar.ListBuilder.remember { ... }` | Defines inspector bar actions before removal | | `listBuilder.modify { remove(id=_, failIfNotFound=_) }` | Removes an existing component item from a list builder | ## Next Steps - [Customize Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Full dock customization patterns - [Customize Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) - Navigation bar configuration - [Rearrange Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/) - Learn how to reorder buttons and components in CE.SDK's navigation bar, canvas menu, canvas bar, dock, and inspector bar using the Ordering APIs. - [Add a New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) - Add a custom button to the UI to trigger specific actions or 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: "Inspector Bar" description: "Customize the inspector bar for editing properties like position, color, and size." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-inspector-bar/SimpleInspectorBarSolution.kt reference-only import androidx.compose.runtime.Composable 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.rememberDefaultScope import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost @Composable fun SimpleInspectorBarSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { inspectorBar = { InspectorBar.remember { // Reuse the editor selection and edit mode as the inspector bar scope. scope = { InspectorBar.rememberDefaultScope(parentScope = this) } listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberDelete() } } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-inspector-bar/NewListBuilderInspectorBarSolution.kt reference-only import androidx.compose.runtime.Composable 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.rememberAdjustments import ly.img.editor.core.component.rememberAnimations import ly.img.editor.core.component.rememberBlur import ly.img.editor.core.component.rememberClipSpeed import ly.img.editor.core.component.rememberCrop import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberEditText import ly.img.editor.core.component.rememberEffect import ly.img.editor.core.component.rememberFillStroke import ly.img.editor.core.component.rememberFormatText import ly.img.editor.core.component.rememberLayer import ly.img.editor.core.component.rememberMoveAsClip import ly.img.editor.core.component.rememberMoveAsOverlay import ly.img.editor.core.component.rememberReplace import ly.img.editor.core.component.rememberShape import ly.img.editor.core.component.rememberSplit import ly.img.editor.core.component.rememberTextBackground 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 NewListBuilderInspectorBarSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { inspectorBar = { InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberDuplicate() } add { InspectorBar.Button.rememberDelete() } add { InspectorBar.Button.rememberAnimations() } add { InspectorBar.Button.rememberAdjustments() } add { InspectorBar.Button.rememberEffect() } add { InspectorBar.Button.rememberBlur() } add { InspectorBar.Button.rememberClipSpeed() } add { InspectorBar.Button.rememberReplace() } add { InspectorBar.Button.rememberEditText() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberFillStroke() } add { InspectorBar.Button.rememberTextBackground() } add { InspectorBar.Button.rememberVolume() } add { InspectorBar.Button.rememberCrop() } add { InspectorBar.Button.rememberShape() } add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.rememberSplit() } add { InspectorBar.Button.rememberMoveAsClip() } add { InspectorBar.Button.rememberMoveAsOverlay() } } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-inspector-bar/ModifyListBuilderInspectorBarSolution.kt reference-only import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.crop import ly.img.editor.core.component.delete import ly.img.editor.core.component.formatText import ly.img.editor.core.component.layer import ly.img.editor.core.component.modify import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberAnimations import ly.img.editor.core.component.rememberCrop import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberFillStroke import ly.img.editor.core.component.rememberFormatText import ly.img.editor.core.component.rememberLayer import ly.img.editor.core.component.rememberReplace import ly.img.editor.core.component.rememberTextBackground import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost @Composable fun ModifyListBuilderInspectorBarSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { inspectorBar = { InspectorBar.remember { listBuilder = { val existingListBuilder = InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.rememberCrop() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberDelete() } } existingListBuilder.modify { addFirst { InspectorBar.Button.rememberDuplicate() } addLast { InspectorBar.Button.rememberReplace() } addAfter(id = InspectorBar.Button.Id.layer, failIfNotFound = true) { InspectorBar.Button.rememberFillStroke() } addBefore(id = InspectorBar.Button.Id.crop, failIfNotFound = true) { InspectorBar.Button.rememberAnimations() } replace(id = InspectorBar.Button.Id.formatText, failIfNotFound = true) { InspectorBar.Button.rememberTextBackground() } remove(id = InspectorBar.Button.Id.delete, failIfNotFound = true) } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-inspector-bar/InspectorBarItems.kt reference-only import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberFormatText import ly.img.editor.core.event.EditorEvent import ly.img.editor.core.iconpack.Close import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Preview import ly.img.editor.core.iconpack.Typeface import ly.img.editor.core.sheet.SheetType import ly.img.engine.DesignBlockType @Composable fun rememberInspectorBarButton() = InspectorBar.Button.remember { id = { EditorComponentId("my.package.inspectorBar.button.showMessage") } onClick = { Toast .makeText(editorContext.activity, "Inspector action tapped", Toast.LENGTH_SHORT) .show() } icon = { Icon( imageVector = IconPack.Preview, contentDescription = "Show inspector message", ) } text = { Text( text = "Show message", ) } } @Composable fun rememberInspectorBarButtonSimple() = InspectorBar.Button.remember { id = { EditorComponentId("my.package.inspectorBar.button.closeEditor") } onClick = { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } vectorIcon = { IconPack.Close } textString = { "Close editor" } contentDescription = { "Close editor" } } @Composable fun rememberCustomizedFormatTextButton() = InspectorBar.Button.rememberFormatText { onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.FormatText())) } vectorIcon = { IconPack.Typeface } textString = { "Format" } enabled = { editorContext.selection.type == DesignBlockType.Text && editorContext.engine.block.isAllowedByScope( editorContext.selection.designBlock, "text/character", ) } visible = { editorContext.selection.type == DesignBlockType.Text && editorContext.engine.block.isAllowedByScope( editorContext.selection.designBlock, "text/character", ) } } @Composable fun rememberInspectorBarCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.inspectorBar.newCustomItem") } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Custom inspector item clicked", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Show toast", ) } } } @Composable fun rememberCustomInspectorBarList() = InspectorBar.ListBuilder.remember { add { rememberCustomizedFormatTextButton() } add { rememberInspectorBarButton() } add { rememberInspectorBarButtonSimple() } add { rememberInspectorBarCustomItem() } } ``` Customize the Android inspector bar by replacing its item list, modifying an existing list builder, and creating app-specific controls. ![Inspector bar in the Android editor](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-inspector-bar) ## Inspector Bar Architecture The inspector bar is an `EditorComponent` that the editor renders at the bottom when a design element is selected. It shows context-sensitive controls for the current selection, such as text formatting, crop actions, media adjustments, and layer operations. You assemble the inspector bar from these component entries and related types: - **`InspectorBar.Button`** - The built-in button implementation, with an action plus an icon and optional text. - **`EditorComponent`** - The base component type for fully custom inspector bar items. The related `InspectorBar.Scope` exposes the editor context, current selection, and current edit mode to inspector bar configuration callbacks. For a complete product surface, start from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). This guide focuses on the inspector bar configuration that you can apply to your own editor setup. ## Configuration Inspector bar customization lives in `EditorConfiguration`. The `Editor` composable owns Engine startup; these snippets use the existing editor context and configure only the inspector bar surface. ```kotlin highlight-android-inspector-bar-configuration EditorConfiguration.remember { inspectorBar = { InspectorBar.remember { // Reuse the editor selection and edit mode as the inspector bar scope. scope = { InspectorBar.rememberDefaultScope(parentScope = this) } listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberDelete() } } } } } } ``` `InspectorBar.remember` keeps the default visibility and decoration unless you override them, but it does not add items on its own. Assign `listBuilder` to populate the bar. Leaving `visible` unset keeps the default valid-selection, non-Crop edit mode, and Voiceover-sheet guards active. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers editor setup more broadly; this page covers the inspector bar portion of that setup. Key configuration properties: | Property | Purpose | | --- | --- | | `id` | Sets the component ID for the inspector bar. The default is `ly.img.component.inspectorBar`. | | `scope` | Provides the editor state used by the inspector bar and its items. | | `modifier` | Applies a Jetpack Compose modifier to the bar. | | `visible` | Controls whether the bar is shown for the current selection and edit mode. Leave it unset to keep the default behavior. | | `enterTransition` | Animates the bar as it appears. The default is a vertical slide in. | | `exitTransition` | Animates the bar as it disappears. The default is a vertical slide out. | | `decoration` | Wraps the full bar with styling such as background, shadow, or padding. The default is `InspectorBar.DefaultDecoration`. | | `listBuilder` | Defines which `EditorComponent` items are available and in which order. The sample adds the predefined delete button as a minimal item. | | `horizontalArrangement` | Controls the horizontal arrangement when the list does not use aligned groups. The default is `Arrangement.Start`. | | `itemsRowEnterTransition` | Animates the item row while the bar appears. The default is a horizontal slide in. | | `itemsRowExitTransition` | Animates the item row while the bar disappears. The default is no exit transition. | | `itemDecoration` | Applies the same decoration to all items in the bar. Use per-item decoration when only one item needs custom styling. | ## Declaring the Item List Use `InspectorBar.ListBuilder.remember` when you want strict control over the entire inspector bar order. Add predefined buttons in the sequence you want them to appear, then add custom buttons only when built-in behavior does not cover your workflow. ```kotlin highlight-android-new-list-builder InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberDuplicate() } add { InspectorBar.Button.rememberDelete() } add { InspectorBar.Button.rememberAnimations() } add { InspectorBar.Button.rememberAdjustments() } add { InspectorBar.Button.rememberEffect() } add { InspectorBar.Button.rememberBlur() } add { InspectorBar.Button.rememberClipSpeed() } add { InspectorBar.Button.rememberReplace() } add { InspectorBar.Button.rememberEditText() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberFillStroke() } add { InspectorBar.Button.rememberTextBackground() } add { InspectorBar.Button.rememberVolume() } add { InspectorBar.Button.rememberCrop() } add { InspectorBar.Button.rememberShape() } add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.rememberSplit() } add { InspectorBar.Button.rememberMoveAsClip() } add { InspectorBar.Button.rememberMoveAsOverlay() } } ``` Only items whose own `visible` state returns `true` render for the current selection. This lets one list contain buttons for text, image, video, audio, and shape blocks without showing irrelevant controls. Common [predefined buttons are listed below](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/#predefined-inspectorbarbuttons). > **Note:** Use aligned groups when you want separate item groups instead of one continuous row. The same `ListBuilder` alignment pattern is shown in the [Navigation Bar guide](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/#declaring-the-item-list). ## Modify Inspector Bar Items Use `modify` when you already have a list builder and only need to add, replace, or remove a few items. This keeps the source order intact and applies the changes around stable component IDs. ```kotlin highlight-android-modify-list-builder val existingListBuilder = InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.rememberCrop() } add { InspectorBar.Button.rememberFormatText() } add { InspectorBar.Button.rememberDelete() } } existingListBuilder.modify { addFirst { InspectorBar.Button.rememberDuplicate() } addLast { InspectorBar.Button.rememberReplace() } addAfter(id = InspectorBar.Button.Id.layer, failIfNotFound = true) { InspectorBar.Button.rememberFillStroke() } addBefore(id = InspectorBar.Button.Id.crop, failIfNotFound = true) { InspectorBar.Button.rememberAnimations() } replace(id = InspectorBar.Button.Id.formatText, failIfNotFound = true) { InspectorBar.Button.rememberTextBackground() } remove(id = InspectorBar.Button.Id.delete, failIfNotFound = true) } ``` Available modification operations: | Operation | Effect | | --- | --- | | `addFirst` | Prepends a new item at the beginning. | | `addLast` | Appends a new item at the end. | | `addAfter` | Inserts a new item after the item with the provided `id`. | | `addBefore` | Inserts a new item before the item with the provided `id`. | | `replace` | Replaces the item with the provided `id`. | | `remove` | Removes the item with the provided `id`. | When `failIfNotFound` is `true`, `addAfter`, `addBefore`, `replace`, and `remove` throw if the target `id` is missing from the source list. Use this when a missing item should fail loudly during development. > **Note:** The default item set and order can change between editor versions, so use > `existingListBuilder.modify` carefully. Prefer replacing the full list with a > fresh `InspectorBar.ListBuilder.remember` when your app requires strict > ordering across SDK upgrades. ## Inspector Bar Item Configuration Each inspector bar item is an `EditorComponent`, so every custom item should use a stable, unique `EditorComponentId`. Start with predefined buttons when possible, then customize button content or create a custom item when the UI needs a different layout. ### Predefined Buttons Predefined buttons are exposed as composable helper functions, such as `InspectorBar.Button.rememberDelete()` or `InspectorBar.Button.rememberCrop()`. They include default icons, text, visibility, and actions for common editor operations. ### Customize Predefined Buttons Pass a builder block to a predefined helper when you want its default ID and behavior family, but need to override specific properties. The example keeps the predefined format-text button and replaces its click action, icon, label, enabled state, and visibility. ```kotlin highlight-android-customize-predefined-button @Composable fun rememberCustomizedFormatTextButton() = InspectorBar.Button.rememberFormatText { onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.FormatText())) } vectorIcon = { IconPack.Typeface } textString = { "Format" } enabled = { editorContext.selection.type == DesignBlockType.Text && editorContext.engine.block.isAllowedByScope( editorContext.selection.designBlock, "text/character", ) } visible = { editorContext.selection.type == DesignBlockType.Text && editorContext.engine.block.isAllowedByScope( editorContext.selection.designBlock, "text/character", ) } } ``` When you override `enabled` or `visible` on a predefined button, keep the same selection and editing-scope checks that make the default button safe for the current block. ### Create New Buttons Use `InspectorBar.Button.remember` for a fully custom button. The builder controls the button ID, visibility, transitions, decoration, click behavior, content, tint, and enabled state. ```kotlin highlight-android-new-button @Composable fun rememberInspectorBarButton() = InspectorBar.Button.remember { id = { EditorComponentId("my.package.inspectorBar.button.showMessage") } onClick = { Toast .makeText(editorContext.activity, "Inspector action tapped", Toast.LENGTH_SHORT) .show() } icon = { Icon( imageVector = IconPack.Preview, contentDescription = "Show inspector message", ) } text = { Text( text = "Show message", ) } } ``` If your button only needs an icon vector, text, and content description, use the shorter `vectorIcon`, `textString`, and `contentDescription` properties. ```kotlin highlight-android-simple-button @Composable fun rememberInspectorBarButtonSimple() = InspectorBar.Button.remember { id = { EditorComponentId("my.package.inspectorBar.button.closeEditor") } onClick = { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } vectorIcon = { IconPack.Close } textString = { "Close editor" } contentDescription = { "Close editor" } } ``` ### Create Custom Items For custom layouts that do not fit the button component, use `EditorComponent.remember` and render the item's UI inside `decoration`. ```kotlin highlight-android-custom-item @Composable fun rememberInspectorBarCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.inspectorBar.newCustomItem") } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Custom inspector item clicked", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Show toast", ) } } } ``` ### Insert Custom Items Defining an item only creates the component. Add the component inside `InspectorBar.ListBuilder.remember` so the inspector bar can render it with the rest of the item list. ```kotlin highlight-android-insert-custom-items @Composable fun rememberCustomInspectorBarList() = InspectorBar.ListBuilder.remember { add { rememberCustomizedFormatTextButton() } add { rememberInspectorBarButton() } add { rememberInspectorBarButtonSimple() } add { rememberInspectorBarCustomItem() } } ``` ## Predefined InspectorBar.Buttons This table highlights the commonly used public composable helpers on `InspectorBar.Button` with IDs that are safe to target when modifying a list builder. You can use them directly in a list builder or customize their builder properties as shown in [Customize Predefined Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/#customize-predefined-buttons). | Button | ID | Description | Renders For | | --- | --- | --- | --- | | `InspectorBar.Button.rememberReplace` | `InspectorBar.Button.Id.replace` | Opens the asset library sheet with `EditorEvent.Sheet.Open`. The selected asset replaces the content of the selected design block. | Page image/video backgrounds, non-sticker image/video graphics, and non-voiceover audio | | `InspectorBar.Button.rememberEditText` | `InspectorBar.Button.Id.editText` | Enters text editing mode for the selected text block. | Text blocks with `text/edit` scope | | `InspectorBar.Button.rememberFormatText` | `InspectorBar.Button.Id.formatText` | Opens the text formatting sheet with `EditorEvent.Sheet.Open`. | Text and caption blocks with `text/character` scope | | `InspectorBar.Button.rememberFillStroke` | `InspectorBar.Button.Id.fillStroke` | Opens the fill and stroke sheet with `EditorEvent.Sheet.Open`. | Non-sticker selections with fill or stroke controls | | `InspectorBar.Button.rememberTextBackground` | `InspectorBar.Button.Id.textBackground` | Opens the text background sheet with `EditorEvent.Sheet.Open`. | Text and caption blocks with `text/character` scope | | `InspectorBar.Button.rememberTextPresets` | `InspectorBar.Button.Id.textPresets` | Opens a restyle picker with `EditorEvent.Sheet.Open`. The picker offers three buckets — Plain, Styles and Curved — that apply a new look to the selected text block in place. | Text blocks with `text/character` scope when the `ly.img.text.styles` source is registered | | `InspectorBar.Button.rememberTextOnPath` | `InspectorBar.Button.Id.textOnPath` | Opens the text-on-path sheet with `EditorEvent.Sheet.Open`. Reads curve presets from the `ly.img.text.curves` source. | Text blocks with `text/character` scope | | `InspectorBar.Button.rememberEditCaptions` | `InspectorBar.Button.Id.editCaptions` | Opens the captions sheet with `EditorEvent.Sheet.Open`, scrolled to the selected caption. | Caption selections with `text/edit` scope | | `InspectorBar.Button.rememberCaptionStyle` | `InspectorBar.Button.Id.captionStyle` | Opens the caption style preset sheet with `EditorEvent.Sheet.Open`. Reads presets from the `ly.img.caption.presets` source. | Caption selections while the `ly.img.caption.presets` source is registered | | `InspectorBar.Button.rememberVolume` | `InspectorBar.Button.Id.volume` | Opens the volume sheet with `EditorEvent.Sheet.Open`. | Audio selections and video-fill selections with `fill/change` scope | | `InspectorBar.Button.rememberClipSpeed` | `InspectorBar.Button.Id.clipSpeed` | Opens the clip speed sheet with `EditorEvent.Sheet.Open`. | Audio or video-playback selections with `fill/change` scope | | `InspectorBar.Button.rememberCrop` | `InspectorBar.Button.Id.crop` | Opens the crop sheet with `EditorEvent.Sheet.Open`. Pages open page crop mode; other selections open element crop mode. | Page or non-sticker image/video fills that support crop and `layer/crop` scope | | `InspectorBar.Button.rememberAnimations` | `InspectorBar.Button.Id.animations` | Opens the animation sheet with `EditorEvent.Sheet.Open`. | Non-page, non-audio, non-caption selections | | `InspectorBar.Button.rememberTransition` | `InspectorBar.Button.Id.transition` | Opens the transition sheet with `EditorEvent.Sheet.Open`, for the transition between the selected clip and the one after it. | Selections followed by another clip on the same track, where both support transitions and the next clip starts no later than the selected one ends | | `InspectorBar.Button.rememberAdjustments` | `InspectorBar.Button.Id.adjustments` | Opens the adjustments sheet with `EditorEvent.Sheet.Open`. | Non-sticker image/video fills with `appearance/adjustments` scope | | `InspectorBar.Button.rememberFilter` | `InspectorBar.Button.Id.filter` | Opens the filter sheet with `EditorEvent.Sheet.Open`. | Non-sticker image/video fills with `appearance/filter` scope | | `InspectorBar.Button.rememberEffect` | `InspectorBar.Button.Id.effect` | Opens the effect sheet with `EditorEvent.Sheet.Open`. | Non-sticker image/video fills with `appearance/effect` scope | | `InspectorBar.Button.rememberBlur` | `InspectorBar.Button.Id.blur` | Opens the blur sheet with `EditorEvent.Sheet.Open`. | Non-sticker image/video fills with `appearance/blur` scope | | `InspectorBar.Button.rememberShape` | `InspectorBar.Button.Id.shape` | Opens the shape sheet with `EditorEvent.Sheet.Open`. | Non-sticker blocks with Star, Polygon, or Rect shapes and `shape/change` scope | | `InspectorBar.Button.rememberSelectGroup` | `InspectorBar.Button.Id.selectGroup` | Selects the group that contains the current selection. | Selections whose parent block is a group | | `InspectorBar.Button.rememberEnterGroup` | `InspectorBar.Button.Id.enterGroup` | Changes selection from the selected group to a design block inside that group. | Group selections | | `InspectorBar.Button.rememberLayer` | `InspectorBar.Button.Id.layer` | Opens the layer sheet with `EditorEvent.Sheet.Open`. | Non-page, non-audio, non-caption selections with layer, lifecycle, or move scope | | `InspectorBar.Button.rememberSplit` | `InspectorBar.Button.Id.split` | Splits the selected block at the playhead. A caption split divides its time at the playhead and its text at the nearest word gap, so the button disables itself unless the playhead sits at least 0.1 seconds away from both the caption's start and its end and the caption holds at least two characters. | Selections with `lifecycle/duplicate` scope | | `InspectorBar.Button.rememberMoveAsClip` | `InspectorBar.Button.Id.moveAsClip` | Moves the selected block into the background track as a clip. | Non-audio, non-caption selections outside the background track | | `InspectorBar.Button.rememberMoveAsOverlay` | `InspectorBar.Button.Id.moveAsOverlay` | Moves the selected block from the background track to an overlay. | Non-audio selections in the background track | | `InspectorBar.Button.rememberVoiceover` | `InspectorBar.Button.Id.voiceover` | Opens the voiceover recording sheet with `EditorEvent.Sheet.Open` for another take. | Audio selections of kind `voiceover` whose recording is saved | | `InspectorBar.Button.rememberReorder` | `InspectorBar.Button.Id.reorder` | Opens the reorder sheet with `EditorEvent.Sheet.Open`. | Selections in a background track with at least two children | | `InspectorBar.Button.rememberDuplicate` | `InspectorBar.Button.Id.duplicate` | Duplicates the selected design block. | Non-page, non-caption selections with `lifecycle/duplicate` scope | | `InspectorBar.Button.rememberDelete` | `InspectorBar.Button.Id.delete` | Deletes the selected design block. | Non-page selections with `lifecycle/destroy` scope | ## Troubleshooting - If the bar does not appear, check `visible` and confirm the editor has a valid selection. - If a custom insertion does not run, verify the target `EditorComponentId` and the `failIfNotFound` setting. - If a button is missing for a block type, the button's own `visible` logic may hide it for the current selection. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builder=_)` | Creates the editor configuration that owns the inspector bar override. | | `InspectorBar.remember(builder=_)` | Creates an inspector bar component from builder properties. | | `InspectorBar.rememberDefaultScope(parentScope=_)` | Reuses the editor selection and edit mode as the inspector bar scope. | | `InspectorBar.ListBuilder.remember(builder=_)` | Builds a new ordered inspector bar item list. | | `listBuilderScope.add(block=_)` | Adds an `EditorComponent` item to the current inspector bar list. | | `existingListBuilder.modify(builder=_)` | Returns a modified list builder derived from an existing list. | | `modifyScope.addFirst(block=_)` | Prepends an item during list modification. | | `modifyScope.addLast(block=_)` | Appends an item during list modification. | | `modifyScope.addAfter(id=_, failIfNotFound=_, block=_)` | Inserts an item after the matching component ID. | | `modifyScope.addBefore(id=_, failIfNotFound=_, block=_)` | Inserts an item before the matching component ID. | | `modifyScope.replace(id=_, failIfNotFound=_, block=_)` | Replaces the item with the matching component ID. | | `modifyScope.remove(id=_, failIfNotFound=_)` | Removes the item with the matching component ID. | | `InspectorBar.Button.remember(builder=_)` | Creates a custom inspector bar button. | | `InspectorBar.Button.rememberFormatText(builder=_)` | Creates a predefined format-text button that can be customized through its builder. | | `editorContext.engine.block.isAllowedByScope(block=_, key="text/character")` | Checks whether text character formatting is allowed for the current selection before showing or enabling formatting controls. | | `editorContext.eventHandler.send(event=_)` | Dispatches editor events from an inspector bar item. | | `EditorComponent.remember(builder=_)` | Creates a custom inspector bar item with fully custom Compose content. | ## Next Steps - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — Customize the bottom toolbar that opens asset libraries and sheets. - [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) — Configure the top bar. - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) — Customize the floating selection toolbar. - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Configure the sheets that buttons like `replace` open. --- ## 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: "Movement Constraints" description: "Restrict how far blocks can be dragged outside the page in the CE.SDK Android editor." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/movement-constraints-8f3a2c/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Movement Constraints](https://img.ly/docs/cesdk/android/user-interface/customization/movement-constraints-8f3a2c/) --- ```kotlin file=@cesdk_android_examples/engine-guides-movement-constraints/MovementConstraints.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.MovementConstraintRule import ly.img.engine.MovementConstraintScope fun movementConstraints( 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.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.appendChild(parent = page, child = block) // Allow every block in the scene to overshoot by 20% of its own size. engine.editor.setMovementConstraint(MovementConstraintRule(overshoot = 0.2F)) // Pin all text and caption blocks fully inside the page. engine.editor.setMovementConstraint( listOf( MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.BlockType("text")), MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.BlockType("caption")), ), ) // Override the scene-wide default for blocks on this page. engine.editor.setMovementConstraint( MovementConstraintRule(overshoot = 0.1F, scope = MovementConstraintScope.Block(page)), ) // Override every other level for one specific block. engine.editor.setMovementConstraint( MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.Block(block)), ) // Read the resolved constraint, walking the priority chain: // block > parent page > blockType > scene-wide. val active = engine.editor.getMovementConstraint(block) // Clear a scope by passing the matching descriptor. Use no argument to remove // the scene-wide default. engine.editor.removeMovementConstraint(MovementConstraintScope.Block(block)) // per-block engine.editor.removeMovementConstraint(MovementConstraintScope.BlockType("text")) // per-type engine.editor.removeMovementConstraint(MovementConstraintScope.Block(page)) // per-page engine.editor.removeMovementConstraint() // scene-wide default engine.stop() } ``` Limit how far a block may extend past its page during user interactions. The constraints apply to mouse and touch gestures — moving, resizing, and scaling. API calls bypass them. `overshoot` is a non-negative fraction of the **block's own size**: `0f` pins the block fully inside, `0.2f` allows a 20% overshoot. Each rule's scope decides which blocks it applies to: - `MovementConstraintScope.Scene` — scene-wide default. - `MovementConstraintScope.Block(id)` — a specific block (pages count as blocks). - `MovementConstraintScope.BlockType(name)` — every block of the given type. ## Scene-wide default Apply a rule that affects every page in the scene: ```kotlin highlight-movement-constraint-scene-wide // Allow every block in the scene to overshoot by 20% of its own size. engine.editor.setMovementConstraint(MovementConstraintRule(overshoot = 0.2F)) ``` ## Per block type Scope a rule with `BlockType` to restrict all blocks of that type. Call `setMovementConstraint` with a list to apply several rules in one call: ```kotlin highlight-movement-constraint-per-type // Pin all text and caption blocks fully inside the page. engine.editor.setMovementConstraint( listOf( MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.BlockType("text")), MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.BlockType("caption")), ), ) ``` ## Per page Pages are blocks, so you can target a page block to set a default for its children: ```kotlin highlight-movement-constraint-per-page // Override the scene-wide default for blocks on this page. engine.editor.setMovementConstraint( MovementConstraintRule(overshoot = 0.1F, scope = MovementConstraintScope.Block(page)), ) ``` ## Per block Target a specific block ID to override every other level: ```kotlin highlight-movement-constraint-per-block // Override every other level for one specific block. engine.editor.setMovementConstraint( MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.Block(block)), ) ``` ## Read the active value Read the resolved constraint for a block. The lookup walks the priority chain: block, parent page, blockType, then scene-wide. It returns `null` when the block is unconstrained. ```kotlin highlight-movement-constraint-read // Read the resolved constraint, walking the priority chain: // block > parent page > blockType > scene-wide. val active = engine.editor.getMovementConstraint(block) ``` ## Remove a constraint Pass the matching `MovementConstraintScope` to clear any level of the priority chain, or call `removeMovementConstraint()` with no argument to clear the scene-wide default: ```kotlin highlight-movement-constraint-remove // Clear a scope by passing the matching descriptor. Use no argument to remove // the scene-wide default. engine.editor.removeMovementConstraint(MovementConstraintScope.Block(block)) // per-block engine.editor.removeMovementConstraint(MovementConstraintScope.BlockType("text")) // per-type engine.editor.removeMovementConstraint(MovementConstraintScope.Block(page)) // per-page engine.editor.removeMovementConstraint() // scene-wide default ``` ## Full code Here's the full code: ```kotlin highlight-movement-constraints 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.MovementConstraintRule import ly.img.engine.MovementConstraintScope fun movementConstraints( 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.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.appendChild(parent = page, child = block) // Allow every block in the scene to overshoot by 20% of its own size. engine.editor.setMovementConstraint(MovementConstraintRule(overshoot = 0.2F)) // Pin all text and caption blocks fully inside the page. engine.editor.setMovementConstraint( listOf( MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.BlockType("text")), MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.BlockType("caption")), ), ) // Override the scene-wide default for blocks on this page. engine.editor.setMovementConstraint( MovementConstraintRule(overshoot = 0.1F, scope = MovementConstraintScope.Block(page)), ) // Override every other level for one specific block. engine.editor.setMovementConstraint( MovementConstraintRule(overshoot = 0F, scope = MovementConstraintScope.Block(block)), ) // Read the resolved constraint, walking the priority chain: // block > parent page > blockType > scene-wide. val active = engine.editor.getMovementConstraint(block) // Clear a scope by passing the matching descriptor. Use no argument to remove // the scene-wide default. engine.editor.removeMovementConstraint(MovementConstraintScope.Block(block)) // per-block engine.editor.removeMovementConstraint(MovementConstraintScope.BlockType("text")) // per-type engine.editor.removeMovementConstraint(MovementConstraintScope.Block(page)) // per-page engine.editor.removeMovementConstraint() // scene-wide default engine.stop() } ``` --- ## 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: "Navigation Bar" description: "Show, hide, or customize the editor's top navigation bar to match your app layout." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-navigation-bar/SimpleNavigationBarSolution.kt reference-only import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ly.img.editor.Editor import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.remember import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost @Composable fun SimpleNavigationBarSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { navigationBar = { NavigationBar.remember { scope = { remember(this) { NavigationBar.Scope(parentScope = this) } } visible = { true } modifier = { Modifier } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { // Also available via NavigationBar.DefaultDecoration Box( modifier = Modifier .fillMaxWidth() .heightIn(min = 64.dp) .background(MaterialTheme.colorScheme.surface) .padding(PaddingValues(horizontal = 4.dp)), contentAlignment = Alignment.Center, ) { it() } } listBuilder = { NavigationBar.ListBuilder.remember { /* Add items */ } } horizontalArrangement = { Arrangement.SpaceEvenly } // Default value is { it() } itemDecoration = { Box(modifier = Modifier.padding(2.dp)) { it() } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-navigation-bar/NewListBuilderNavigationBarSolution.kt reference-only import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp import ly.img.editor.Editor import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberCloseEditor import ly.img.editor.core.component.rememberExport import ly.img.editor.core.component.rememberRedo import ly.img.editor.core.component.rememberUndo import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember // Add this composable to your NavHost @Composable fun NewListBuilderNavigationBarSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { navigationBar = { NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.CenterHorizontally) { add { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.custom") } vectorIcon = null textString = { "Custom Button" } onClick = {} } } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberExport() } add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } } } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-navigation-bar/ModifyListBuilderNavigationBarSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import ly.img.editor.Editor import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.closeEditor import ly.img.editor.core.component.export import ly.img.editor.core.component.modify import ly.img.editor.core.component.redo import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberCloseEditor import ly.img.editor.core.component.rememberExport import ly.img.editor.core.component.rememberRedo import ly.img.editor.core.component.rememberTogglePagesMode import ly.img.editor.core.component.rememberUndo import ly.img.editor.core.component.undo import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Music // Add this composable to your NavHost @Composable fun ModifyListBuilderNavigationBarSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { navigationBar = { NavigationBar.remember { listBuilder = { // Makes sense to use only with builders that are already available and cannot be modified by you directly. val existingListBuilder = NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.End) { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.End) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } add { NavigationBar.Button.rememberTogglePagesMode() } add { NavigationBar.Button.rememberExport() } } } } existingListBuilder.modify { addFirst(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.first") } vectorIcon = { IconPack.Music } textString = { "First Button" } onClick = {} } } addLast(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.last") } vectorIcon = { IconPack.Music } textString = { "Last Button" } onClick = {} } } addAfter(id = NavigationBar.Button.Id.redo, failIfNotFound = true) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.afterRedo") } vectorIcon = { IconPack.Music } textString = { "After Redo" } onClick = {} } } addBefore(id = NavigationBar.Button.Id.undo, failIfNotFound = true) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.beforeUndo") } vectorIcon = { IconPack.Music } textString = { "Before Undo" } onClick = {} } } replace(id = NavigationBar.Button.Id.export, failIfNotFound = true) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.replacedExport") } vectorIcon = null textString = { "Replaced Export" } onClick = {} } } remove(id = NavigationBar.Button.Id.closeEditor, failIfNotFound = true) } } } } } }, onClose = onClose, ) } ``` ```kotlin file=@cesdk_android_examples/editor-guides-configuration-navigation-bar/NavigationBarItems.kt reference-only import android.widget.Toast import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.remember import ly.img.editor.core.event.EditorEvent import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Music import ly.img.editor.core.sheet.SheetType @Composable fun rememberNavigationBarButton() = NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.newButton") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.Volume())) } // Default value is null icon = { Icon( imageVector = IconPack.Music, contentDescription = null, ) } // Default value is null text = { Text( text = "Hello World", ) } enabled = { true } } @Composable fun rememberNavigationBarButtonSimple() = NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.newButton") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } // Default value is null vectorIcon = { IconPack.Music } // Default value is null textString = { "Hello World" } tint = { MaterialTheme.colorScheme.onSurfaceVariant } enabled = { true } contentDescription = null } @Composable fun rememberNavigationBarCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.navigationBar.newCustomItem") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Hello World Clicked!", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Hello World", ) } } } ``` The navigation bar is the top editor toolbar for session actions, undo and redo, preview modes, page navigation, and export. Replace its item list or modify existing entries from your Android editor configuration. ![Navigation bar in the Android editor](./assets/navigation-bar-android.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-navigation-bar) ## Navigation Bar Architecture The navigation bar is an `EditorComponent` that the editor renders as the top toolbar. It is selection-independent, and each item can decide its own visibility. You assemble the navigation bar from these component entries and related types: - **`NavigationBar.Button`** - The built-in button implementation, with an action plus an icon and optional text. - **`EditorComponent`** - The base component type for fully custom navigation bar items. The related `NavigationBar.Scope` exposes the editor context, event handler, and engine to navigation bar configuration callbacks. Items can be organized into start, center, and end alignment groups when your layout needs multiple regions. For a complete product surface, start from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). This guide focuses on the navigation bar configuration that you can apply to your own editor setup. ## Configuration Navigation bar customization lives in `EditorConfiguration`. The `Editor` composable owns Engine startup; these snippets use the existing editor context and configure only the navigation bar surface. ```kotlin highlight-navigationBarConfiguration EditorConfiguration.remember { navigationBar = { NavigationBar.remember { scope = { remember(this) { NavigationBar.Scope(parentScope = this) } } visible = { true } modifier = { Modifier } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { // Also available via NavigationBar.DefaultDecoration Box( modifier = Modifier .fillMaxWidth() .heightIn(min = 64.dp) .background(MaterialTheme.colorScheme.surface) .padding(PaddingValues(horizontal = 4.dp)), contentAlignment = Alignment.Center, ) { it() } } listBuilder = { NavigationBar.ListBuilder.remember { /* Add items */ } } horizontalArrangement = { Arrangement.SpaceEvenly } // Default value is { it() } itemDecoration = { Box(modifier = Modifier.padding(2.dp)) { it() } } } } } ``` `NavigationBar.remember` keeps the default visibility and decoration unless you override them, but it does not add items on its own. Assign `listBuilder` to populate the bar. Supplying an empty `NavigationBar.ListBuilder.remember { }` hides all navigation bar items, while `visible` can hide or show the whole navigation bar component. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers editor setup more broadly; this page covers the navigation bar portion of that setup. ## Declaring the Item List Use `NavigationBar.ListBuilder.remember` to declare the complete list of navigation bar items. The order of `add` calls inside an alignment group is the display order, and the list replaces the navigation bar contents for this configuration. ```kotlin highlight-newListBuilder NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.CenterHorizontally) { add { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.custom") } vectorIcon = null textString = { "Custom Button" } onClick = {} } } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberExport() } add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } } } ``` - Predefined buttons include session, undo, redo, export, preview-mode, and page-navigation actions. - Use `aligned` groups when the bar needs separate start, center, and end regions. - Custom buttons can sit alongside predefined items when the action belongs directly in the top toolbar. Common [predefined navigation bar buttons are listed below](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/#list-of-available-navigationbarbuttons). > **Note:** **Important:** If you use aligned groups then the value of `horizontalArrangement` in `NavigationBar.remember` is ignored. > **Note:** **Warning:** It is not allowed to add items both inside and outside align > blocks at the same time: either all items should be in aligned groups, or no > items at all. ## Modify Navigation Bar Items Use `modify` when you already have a list builder and only need to add, replace, or remove a few entries. Call it on the builder instance you want to adjust. ```kotlin highlight-modifyListBuilder // Makes sense to use only with builders that are already available and cannot be modified by you directly. val existingListBuilder = NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.End) { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.End) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } add { NavigationBar.Button.rememberTogglePagesMode() } add { NavigationBar.Button.rememberExport() } } } } existingListBuilder.modify { addFirst(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.first") } vectorIcon = { IconPack.Music } textString = { "First Button" } onClick = {} } } addLast(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.last") } vectorIcon = { IconPack.Music } textString = { "Last Button" } onClick = {} } } addAfter(id = NavigationBar.Button.Id.redo, failIfNotFound = true) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.afterRedo") } vectorIcon = { IconPack.Music } textString = { "After Redo" } onClick = {} } } addBefore(id = NavigationBar.Button.Id.undo, failIfNotFound = true) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.beforeUndo") } vectorIcon = { IconPack.Music } textString = { "Before Undo" } onClick = {} } } replace(id = NavigationBar.Button.Id.export, failIfNotFound = true) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.replacedExport") } vectorIcon = null textString = { "Replaced Export" } onClick = {} } } remove(id = NavigationBar.Button.Id.closeEditor, failIfNotFound = true) } ``` | Operation | Purpose | | --- | --- | | `addFirst` | Add an item at the beginning of an alignment group. | | `addLast` | Add an item at the end of an alignment group. | | `addAfter` | Insert an item after a target ID. | | `addBefore` | Insert an item before a target ID. | | `replace` | Replace the item with the target ID. | | `remove` | Remove the item with the target ID. | > **Warning:** Operations that target a specific ID can throw when `failIfNotFound` is `true` and the ID is missing from the source list. Use predefined IDs such as `NavigationBar.Button.Id.export` when you target built-in items. > **Note:** The default navigation bar list can change between editor versions, so use `modify` carefully when your app inherits items from another configuration, plugin, or starter surface. Declare the complete list with a fresh `NavigationBar.ListBuilder.remember` when strict ordering matters. ## NavigationBar.Item Configuration Each `NavigationBar.Item` is an `EditorComponent`. Its `id` must be unique which is a requirement for proper component management. Items must be organized within alignment groups when using the aligned layout system. ### Predefined Buttons Start with predefined buttons which are provided as composable functions. All [available predefined buttons are listed below](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/#list-of-available-navigationbarbuttons). ### Create New Buttons Create custom buttons when predefined options don't meet your needs: ```kotlin highlight-navigationBarItems-newButton @Composable fun rememberNavigationBarButton() = NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.newButton") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.Volume())) } // Default value is null icon = { Icon( imageVector = IconPack.Music, contentDescription = null, ) } // Default value is null text = { Text( text = "Hello World", ) } enabled = { true } } ``` **Required and optional properties:** - `id` - the id of the button. Note that it is highly recommended that every unique `EditorComponent` has a unique id. By default property contains a random value. - `scope` - scope of this component. Every new value will trigger recomposition of all `ScopedProperty`s such as `visible`, `enterTransition`, `exitTransition` etc. Consider using Compose `androidx.compose.runtime.State` objects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the `Engine`. By default the scope is updated only when the parent component scope is updated. - `modifier` - Jetpack Compose modifier of this component. By default empty modifier is applied. - `visible` - whether the button should be visible. Default value is always true. - `enterTransition` - transition of the button when it enters the parent composable. Default value is always no enter transition. - `exitTransition` - transition of the button when it exits the parent composable. Default value is always no exit transition. - `decoration` - decoration of the button. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration. - `onClick` - the callback that is invoked when the button is clicked. By default it is a no-op. - `icon` - the icon content of the button. If null, it will not be rendered. Default value is null. - `text` - the text content of the button. If null, it will not be rendered. Default value is null. - `tint` - the tint color of the content. By default it is `MaterialTheme.colorScheme.onSurfaceVariant`. - `enabled` - whether the button is enabled. Default value is always true. This gives full control over the content of the button. However, there are simpler configuration options if you do not want to fully customize `text` and `icon` composables. Let's have a look at this example: ```kotlin highlight-navigationBarItems-newButton-simple @Composable fun rememberNavigationBarButtonSimple() = NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.newButton") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } // Default value is null vectorIcon = { IconPack.Music } // Default value is null textString = { "Hello World" } tint = { MaterialTheme.colorScheme.onSurfaceVariant } enabled = { true } contentDescription = null } ``` It has three differences: 1. `icon` is replaced with `vectorIcon` lambda, that returns `ImageVector` instead of drawing the icon content. 2. `text` is replaced with `textString` lambda, that returns `String` instead of drawing the text content. 3. `contentDescription` property is added that is used by accessibility services to describe what the button does. Provide it whenever the button does not contain visible text explaining its action. ### Create Custom Items For completely custom implementations, use `EditorComponent.remember` and render your custom UI inside `decoration`. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise: ```kotlin highlight-navigationBarItems-newCustomItem @Composable fun rememberNavigationBarCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.navigationBar.newCustomItem") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Hello World Clicked!", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Hello World", ) } } } ``` **Required and optional properties:** - `id` - the unique id of the custom item. Note that it is highly recommended that every unique `EditorComponent` has a unique id. By default it contains a random value. - `scope` - scope of this component. Every new value will trigger recomposition of all `ScopedProperty`s such as `visible`, `enterTransition`, `exitTransition` etc. Consider using Compose `androidx.compose.runtime.State` objects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the `Engine`. By default it is derived from the parent component scope. - `modifier` - Jetpack Compose modifier of this component. By default empty modifier is applied. - `visible` - whether the custom item should be visible. Default value is always true. - `enterTransition` - transition of the custom item when it enters the parent composable. Default value is always no enter transition. - `exitTransition` - transition of the custom item when it exits the parent composable. Default value is always no exit transition. - `decoration` - render your custom item here. You are responsible for drawing the UI, handling clicks, and applying any custom styling. Default value is always no decoration. ## List of Available NavigationBar.Buttons All predefined buttons are available as composable functions in the `NavigationBar.Button` namespace. Each function returns a `NavigationBar.Button` with default properties that you can customize as shown in the [Create New Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/#create-new-buttons) section. | Button | Id | Description | | ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NavigationBar.Button.rememberCloseEditor` | `NavigationBar.Button.Id.closeEditor` | Triggers [EditorConfiguration.onClose](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) callback via `EditorEvent.OnClose`. | | `NavigationBar.Button.rememberUndo` | `NavigationBar.Button.Id.undo` | Does undo operation in the editor via [EditorApi.undo](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) engine API. | | `NavigationBar.Button.rememberRedo` | `NavigationBar.Button.Id.redo` | Does redo operation in the editor via [EditorApi.redo](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) engine API. | | `NavigationBar.Button.rememberExport` | `NavigationBar.Button.Id.export` | Triggers [EditorConfiguration.onExport](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) callback via `EditorEvent.Export`. | | `NavigationBar.Button.rememberTogglePreviewMode` | `NavigationBar.Button.Id.togglePreviewMode` | Updates editor view mode via `EditorEvent.SetViewMode`: when current view mode is `EditorViewMode.Edit`, then `EditorViewMode.Preview` is set and vice versa. Note that this button is intended to be used in Photo Editor, Apparel Editor and Postcard Editor and may cause unexpected behaviors when used in other solutions. | | `NavigationBar.Button.rememberTogglePagesMode` | `NavigationBar.Button.Id.togglePagesMode` | Updates editor view mode via `EditorEvent.SetViewMode`: when current view mode is `EditorViewMode.Edit`, then `EditorViewMode.Pages` is set and vice versa. Note that this button is intended to be used in Design Editor and may cause unexpected behaviors when used in other solutions. | | `NavigationBar.Button.rememberPreviousPage` | `NavigationBar.Button.Id.previousPage` | Navigates to the previous page via `EditorEvent.Navigation.ToPreviousPage`. | | `NavigationBar.Button.rememberNextPage` | `NavigationBar.Button.Id.nextPage` | Navigates to the next page via `EditorEvent.Navigation.ToNextPage`. | ## API Reference | API | Category | Purpose | | --- | --- | --- | | `EditorConfiguration.remember { navigationBar = { ... } }` | Config | Configure the navigation bar component for an editor instance. | | `NavigationBar.remember { ... }` | Config | Create and configure a navigation bar component. | | `NavigationBar.ListBuilder.remember { aligned { add { ... } } }` | Config | Build a complete navigation bar item list in aligned groups. | | `add { ... }` | List operation | Append an item to an alignment group in a new list builder. | | `existingListBuilder.modify { ... }` | Config | Adjust an existing list builder without rewriting the source list. | | `addFirst { ... }` / `addLast { ... }` | List operation | Add items at the beginning or end of an alignment group. | | `addAfter(id=_, failIfNotFound=_)` / `addBefore(id=_, failIfNotFound=_)` | List operation | Insert items next to a source item id. | | `replace(id=_, failIfNotFound=_)` | List operation | Replace a source item by id. | | `remove(id=_, failIfNotFound=_)` | List operation | Remove a source item by id. | | `NavigationBar.Button.rememberCloseEditor()` | Item | Add the close-editor navigation button. | | `NavigationBar.Button.rememberUndo()` | Item | Add the undo navigation button. | | `NavigationBar.Button.rememberRedo()` | Item | Add the redo navigation button. | | `NavigationBar.Button.rememberExport()` | Item | Add the export navigation button. | | `NavigationBar.Button.remember { id; onClick; icon; text }` | Item | Create a custom navigation bar button. | | `EditorComponent.remember { decoration = { ... } }` | Item | Create a fully custom navigation bar item. | | `EditorComponentId("...")` | Item | Define stable ids for custom items and modify targets. | | `editorContext.eventHandler.send(event=_)` | Event | Dispatch editor UI events from custom navigation bar item actions. | ## Next Steps - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Configure the bottom toolbar that opens asset libraries and sheets. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) - Configure context-sensitive controls for the selected block. - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) - Customize the floating toolbar for the current selection. - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) - Configure sources shared across editor components. --- ## 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: "Page Format" description: "Define default page size, orientation, and other format settings for your design canvas." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/page-format-496315/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Page Format](https://img.ly/docs/cesdk/android/user-interface/customization/page-format-496315/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-page-format/PageFormatEditorSolution.kt reference-only import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.engine.AssetDefinition import ly.img.engine.AssetPayload import ly.img.engine.AssetTransformPreset import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit @Composable fun PageFormatEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder) { onCreate = { val pagePresetsSourceId = "ly.img.page.presets" if (editorContext.engine.asset.findAllSources().contains(pagePresetsSourceId)) { editorContext.engine.asset.removeSource(pagePresetsSourceId) } editorContext.engine.asset.addLocalSource( sourceId = pagePresetsSourceId, supportedMimeTypes = emptyList(), ) editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "a4-portrait", label = mapOf("en" to "A4 Portrait"), groups = listOf("print"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 210F, height = 297F, designUnit = DesignUnit.MILLIMETER, ), ), ), ) editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "a5-landscape", label = mapOf("en" to "A5 Landscape"), groups = listOf("print"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 210F, height = 148F, designUnit = DesignUnit.MILLIMETER, ), ), ), ) editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "square-social", label = mapOf("en" to "Square Social"), groups = listOf("digital"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 1080F, height = 1080F, designUnit = DesignUnit.PIXEL, ), ), ), ) editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "letter", label = mapOf("en" to "US Letter"), groups = listOf("print"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 8.5F, height = 11F, designUnit = DesignUnit.INCH, ), ), ), ) val scene = editorContext.engine.scene.create(designUnit = DesignUnit.MILLIMETER) val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 210F) editorContext.engine.block.setHeight(block = page, value = 297F) editorContext.engine.block.appendChild(parent = scene, child = page) } } }, onClose = onClose, ) } ``` Customize the page format presets that appear in the CE.SDK editor UI so your users can choose only the sizes your workflow supports. ![Page format selector in the CE.SDK editor UI](./assets/page-presets-android.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-page-format) CE.SDK includes page format presets in the resize sheet. The Android editor UI reads these presets from the `ly.img.page.presets` asset source, so you customize the selector by replacing that source with your own fixed-size assets. > **Note:** For a complete product surface, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). The example below focuses on the asset source setup that feeds the CE.SDK editor UI. ## Using the Built-in Page Format UI Users can open the resize sheet from the CE.SDK editor UI and select one of the registered page presets. Selecting a preset resizes the current page, or all pages when the resize-all flow is active. Each Android page format is an asset with an `AssetTransformPreset.FixedSize` payload. The `groups` list controls how formats are grouped in the selector. ## Replacing the Page Format Source Register the source with the default page preset ID, `"ly.img.page.presets"`. If the editor already registered that source, remove it first so the selector reads only your custom formats. ```kotlin highlight-android-register-page-source val pagePresetsSourceId = "ly.img.page.presets" if (editorContext.engine.asset.findAllSources().contains(pagePresetsSourceId)) { editorContext.engine.asset.removeSource(pagePresetsSourceId) } editorContext.engine.asset.addLocalSource( sourceId = pagePresetsSourceId, supportedMimeTypes = emptyList(), ) ``` ## Adding Page Format Assets Each page format asset needs a `FixedSize` transform preset: - `width` and `height` define the page dimensions. - `designUnit` sets the unit for those dimensions: `DesignUnit.PIXEL`, `DesignUnit.MILLIMETER`, or `DesignUnit.INCH`. - `label` is the text shown in the selector. - `groups` lets the selector organize formats into sections such as print or digital sizes. ### Using Millimeter Dimensions Use millimeters for print formats. This example adds A4 portrait and A5 landscape presets to the same page preset source. ```kotlin highlight-android-add-print-formats editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "a4-portrait", label = mapOf("en" to "A4 Portrait"), groups = listOf("print"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 210F, height = 297F, designUnit = DesignUnit.MILLIMETER, ), ), ), ) editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "a5-landscape", label = mapOf("en" to "A5 Landscape"), groups = listOf("print"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 210F, height = 148F, designUnit = DesignUnit.MILLIMETER, ), ), ), ) ``` ### Using Pixel Dimensions Use pixels for screen-first formats. A square preset uses the same width and height so it keeps a 1:1 page shape. ```kotlin highlight-android-add-pixel-format editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "square-social", label = mapOf("en" to "Square Social"), groups = listOf("digital"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 1080F, height = 1080F, designUnit = DesignUnit.PIXEL, ), ), ), ) ``` ### Using Inch Dimensions Use inches for formats common in regions that use imperial measurements. ```kotlin highlight-android-add-inch-format editorContext.engine.asset.addAsset( sourceId = pagePresetsSourceId, asset = AssetDefinition( id = "letter", label = mapOf("en" to "US Letter"), groups = listOf("print"), payload = AssetPayload( transformPreset = AssetTransformPreset.FixedSize( width = 8.5F, height = 11F, designUnit = DesignUnit.INCH, ), ), ), ) ``` ## Creating the Scene Load the page format source before creating the scene. Then create the initial page with the dimensions you want users to see first. ```kotlin highlight-android-create-scene val scene = editorContext.engine.scene.create(designUnit = DesignUnit.MILLIMETER) val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 210F) editorContext.engine.block.setHeight(block = page, value = 297F) editorContext.engine.block.appendChild(parent = scene, child = page) ``` The Android editor UI does not read a `meta.default` field from page format assets during scene creation. If your app needs a default page size, create the first page with that size in your `onCreate` callback. ## Page Orientation Orientation comes from the relationship between `width` and `height`. A wider format appears as landscape, while a taller format appears as portrait. Android represents each orientation as a separate fixed-size asset. Add one portrait asset and one landscape asset when users need both options; the Android resize sheet does not read the web-only `fixedOrientation` metadata flag. ## Troubleshooting - **Custom formats do not appear**: Make sure the source ID is `"ly.img.page.presets"` and that you add the source before opening the resize sheet. - **Default page size is unchanged**: Create the initial page with the desired dimensions in `onCreate`; Android does not apply `meta.default` automatically. - **Formats appear in the wrong unit**: Check that each `AssetTransformPreset.FixedSize` uses the intended `DesignUnit`. ## API Reference | Method | Category | Description | |--------|----------|-------------| | `editorContext.engine.asset.findAllSources()` | Asset | Check whether the page preset source already exists | | `editorContext.engine.asset.removeSource(sourceId=_)` | Asset | Remove the existing page preset source before replacing it | | `editorContext.engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Asset | Register a local source for page format assets | | `editorContext.engine.asset.addAsset(sourceId=_, asset=_)` | Asset | Add one page format asset to the source | | `editorContext.engine.scene.create(designUnit=_)` | Scene | Create the scene with the unit used by the initial page | | `editorContext.engine.block.create(blockType=_)` | Block | Create the initial page block | | `editorContext.engine.block.setWidth(block=_, value=_)` | Block | Set the initial page width | | `editorContext.engine.block.setHeight(block=_, value=_)` | Block | Set the initial page height | | `editorContext.engine.block.appendChild(parent=_, child=_)` | Block | Attach the page to the scene | ## Related Types | Type | Purpose | |------|---------| | `AssetDefinition` | Describes one page format asset | | `AssetPayload` | Stores the `transformPreset` for a page format asset | | `AssetTransformPreset.FixedSize` | Defines fixed page dimensions and their design unit | | `DesignUnit` | Selects pixel, millimeter, or inch 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: "Panel" description: "Show or hide side panels to focus the user interface on what matters most for your use case." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-panel/DefaultPanelSolution.kt reference-only import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ly.img.editor.Editor import ly.img.editor.core.EditorContext import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.data.Height 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.sheet.SheetStyle import ly.img.editor.core.sheet.SheetType @Composable fun DefaultPanelSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { openPanelDockButton } add { openCustomPanelDockButton } } } } } } }, onClose = onClose, ) } val openPanelDockButton @Composable get() = Dock.Button.remember { id = { EditorComponentId("open_library_panel") } text = { Text("Open Library") } icon = { Icon(Icons.Rounded.KeyboardArrowUp, null) } onClick = { editorContext.eventHandler.send( EditorEvent.Sheet.Open( SheetType.LibraryAdd(), ), ) } } val openCustomPanelDockButton @Composable get() = Dock.Button.remember { id = { EditorComponentId("open_custom_panel") } text = { Text("Custom Panel") } icon = { Icon(Icons.Rounded.KeyboardArrowUp, null) } onClick = { editorContext.eventHandler.send( EditorEvent.Sheet.Open( customPanelSheetType(), ), ) } } fun customPanelSheetType(): SheetType { return SheetType.Custom( style = SheetStyle( isFloating = false, minHeight = Height.Exactly(0.dp), maxHeight = Height.Fraction(0.5F), isHalfExpandingEnabled = false, isHalfExpandedInitially = false, animateInitialValue = true, ), content = { SimpleCustomPanelContent(editorContext = editorContext) }, ) } @Composable private fun SimpleCustomPanelContent(editorContext: EditorContext) { Column( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(16.dp), horizontalAlignment = Alignment.Start, ) { Text("Custom panel") Spacer(modifier = Modifier.height(16.dp)) Text("Render native Compose content here.") Spacer(modifier = Modifier.height(24.dp)) Button( modifier = Modifier.align(Alignment.End), onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true)) }, ) { Text("Done") } } } ``` Use sheets to show built-in editor panels or your own Compose content in the editor UI. ![Panel on Android](./assets/panel-android.png) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-panel) In Android integrations, panels are implemented as editor sheets. You open a concrete `SheetType` through `EditorEvent.Sheet.Open`, and you close the currently displayed sheet with `EditorEvent.Sheet.Close`. The same Base Editor sheet system is used by Starter Kits. See the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) for a complete editor surface that uses these components in context. ## Controlling a Panel Send an `EditorEvent.Sheet.Open` event from a component that has access to `editorContext`. This example adds a dock button that opens the built-in asset library sheet. ```kotlin highlight-android-open-panel editorContext.eventHandler.send( EditorEvent.Sheet.Open( SheetType.LibraryAdd(), ), ) ``` To dismiss the active sheet from code, send `EditorEvent.Sheet.Close`. The `animate` argument controls whether the close transition is animated. ```kotlin highlight-android-close-panel editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true)) ``` Android sheets are selected by type instead of string IDs. If your workflow needs a different panel, open the matching `SheetType` rather than searching for or closing panels by ID. ## Creating a Custom Panel Use `SheetType.Custom` when you need app-specific UI inside a sheet. The `content` lambda is regular Compose content in `EditorScope`, so it can render controls and send editor events. ```kotlin highlight-android-open-custom-panel return SheetType.Custom( style = SheetStyle( isFloating = false, minHeight = Height.Exactly(0.dp), maxHeight = Height.Fraction(0.5F), isHalfExpandingEnabled = false, isHalfExpandedInitially = false, animateInitialValue = true, ), content = { SimpleCustomPanelContent(editorContext = editorContext) }, ) ``` Open the custom sheet type with the same event flow as a built-in sheet. ```kotlin highlight-android-open-custom-panel-event editorContext.eventHandler.send( EditorEvent.Sheet.Open( customPanelSheetType(), ), ) ``` The `SheetStyle` controls how the sheet is presented: | Parameter | Default Value | Description | | --- | --- | --- | | `isFloating` | `false` | Renders the sheet over the editor when `true`; adjusts the canvas around the sheet when `false`. | | `minHeight` | `Height.Exactly(0.dp)` | Sets the minimum sheet height. | | `maxHeight` | `Height.Fraction(0.5F)` | Sets the maximum sheet height. Use `null` when the sheet should not cap its height. | | `isHalfExpandingEnabled` | `false` | Adds a half-expanded state between hidden and expanded. | | `isHalfExpandedInitially` | `false` | Opens the sheet half-expanded first when half expansion is enabled. | | `animateInitialValue` | `true` | Animates the initial expanded or half-expanded state. | If custom content needs to scroll after it reaches `maxHeight`, add the scrolling behavior to your Compose content. ## Default Sheet Types The editor provides built-in sheet types for common editing tasks: | Sheet Type | Description | | --- | --- | | `SheetType.LibraryAdd()` | Add assets from the configured asset library. | | `SheetType.LibraryReplace(libraryCategory = category)` | Replace the selected block's asset with assets from a library category. | | `SheetType.Reorder()` | Reorder videos on the background track. | | `SheetType.Adjustments()` | Make adjustments to image and video fills. | | `SheetType.Filter()` | Set filters on image and video fills. | | `SheetType.Effect()` | Set effects on image and video fills. | | `SheetType.Blur()` | Set blur effects on image and video fills. | | `SheetType.Crop(mode = SheetType.Crop.Mode.ImageCrop)` | Crop the selected image or video fill. | | `SheetType.ResizeAll()` | Resize all pages in the design. | | `SheetType.Layer()` | Control the layering of design blocks. | | `SheetType.FormatText()` | Format selected text blocks. | | `SheetType.Shape()` | Change the shape of selected blocks. | | `SheetType.FillStroke()` | Edit fill and stroke properties. | | `SheetType.Speed()` | Control video clip playback speed. | | `SheetType.Volume()` | Control audio and video volume. | | `SheetType.Voiceover()` | Record voice-over audio in video scenes. | | `SheetType.Animation()` | Configure in, out, and loop animations. | | `SheetType.Transition(outgoingBlock = outgoingBlock)` | Configure the transition from a clip to its next eligible clip. | | `SheetType.TextBackground()` | Configure text background properties. | ## API Reference | API | Use | | --- | --- | | `editorContext.eventHandler.send(event=_)` | Sends an editor event from a UI component that has access to `editorContext`. | | `EditorEvent.Sheet.Open(type=_)` | Opens the sheet described by a `SheetType`. | | `EditorEvent.Sheet.Close(animate=_)` | Closes the active sheet, optionally animating the transition. | | `SheetType.Custom(style=_, content=_)` | Defines a sheet with app-specific Compose content. Add scrolling inside `content` when needed. | | `SheetType.LibraryAdd(style=_, libraryCategory=_, addToBackgroundTrack=_)` | Opens the configured asset library for adding assets to the scene. | | `SheetStyle(isFloating=_, minHeight=_, maxHeight=_, isHalfExpandingEnabled=_, isHalfExpandedInitially=_, animateInitialValue=_)` | Controls placement, height limits, half-expanded state, and initial animation. | | `Height.Exactly(size=_)` | Sets an exact sheet height for `SheetStyle.minHeight` or `SheetStyle.maxHeight`. | | `Height.Fraction(fraction=_)` | Sets a sheet height as a fraction of the editor height. | ## Next Steps - [Create a Custom Panel](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/create-custom-panel-d87b83/) - Design a custom sidebar panel to support unique workflows and user needs. - [Asset Library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) - Manage the assets users browse, preview, and insert. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) - Customize the inspector bar for editing 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: "Rearrange Buttons" description: "Reorder UI buttons across editor components to match your application's workflows." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Rearrange Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/) --- ```kotlin file=@cesdk_android_examples/editor-guides-customization-rearrange-buttons/RearrangeButtonsEditorSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.bringForward import ly.img.editor.core.component.duplicate import ly.img.editor.core.component.imglyCamera import ly.img.editor.core.component.layer import ly.img.editor.core.component.modify import ly.img.editor.core.component.redo import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberRedo import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.component.rememberUndo import ly.img.editor.core.component.selectGroup import ly.img.editor.core.component.sendBackward import ly.img.editor.core.component.textLibrary import ly.img.editor.core.component.undo import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.configuration.then @Composable fun RearrangeButtonsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder).then { navigationBar = navigationBarComponent@{ val sourceNavigationBar = parentConfiguration?.navigationBar as? NavigationBar ?: return@navigationBarComponent null val updatedListBuilder = sourceNavigationBar.listBuilder.modify { remove(id = NavigationBar.Button.Id.undo, failIfNotFound = true) remove(id = NavigationBar.Button.Id.redo, failIfNotFound = true) addFirst(alignment = Alignment.Start) { NavigationBar.Button.rememberUndo() } addFirst(alignment = Alignment.Start) { NavigationBar.Button.rememberRedo() } } remember(sourceNavigationBar, updatedListBuilder) { sourceNavigationBar.copy(listBuilder = updatedListBuilder) } } canvasMenu = canvasMenuComponent@{ val sourceCanvasMenu = parentConfiguration?.canvasMenu as? CanvasMenu ?: return@canvasMenuComponent null val updatedListBuilder = sourceCanvasMenu.listBuilder.modify { // These divider IDs come from the Design Editor Starter Kit. // Give custom dividers stable IDs and remove those IDs instead. val designCanvasMenuDivider1 = EditorComponentId("ly.img.component.canvasMenu.divider1") val designCanvasMenuDivider2 = EditorComponentId("ly.img.component.canvasMenu.divider2") remove(id = designCanvasMenuDivider1, failIfNotFound = false) remove(id = CanvasMenu.Button.Id.bringForward, failIfNotFound = false) remove(id = CanvasMenu.Button.Id.selectGroup, failIfNotFound = false) remove(id = CanvasMenu.Button.Id.sendBackward, failIfNotFound = false) remove(id = designCanvasMenuDivider2, failIfNotFound = false) } remember(sourceCanvasMenu, updatedListBuilder) { sourceCanvasMenu.copy(listBuilder = updatedListBuilder) } } dock = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { remove(id = Dock.Button.Id.textLibrary, failIfNotFound = true) addFirst { Dock.Button.rememberTextLibrary() } remove(id = Dock.Button.Id.imglyCamera, failIfNotFound = true) } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } inspectorBar = inspectorBarComponent@{ val sourceInspectorBar = parentConfiguration?.inspectorBar as? InspectorBar ?: return@inspectorBarComponent null val updatedListBuilder = sourceInspectorBar.listBuilder.modify { remove(id = InspectorBar.Button.Id.duplicate, failIfNotFound = true) addBefore(id = InspectorBar.Button.Id.layer, failIfNotFound = true) { InspectorBar.Button.rememberDuplicate() } } remember(sourceInspectorBar, updatedListBuilder) { sourceInspectorBar.copy(listBuilder = updatedListBuilder) } } } }, onClose = onClose, ) } ``` Reorder buttons across the navigation bar, canvas menu, dock, and inspector bar to prioritize actions for your workflows. ![Rearranged Android dock buttons](./assets/android.hero.png) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-customization-rearrange-buttons) ## Overview CE.SDK editor components expose list builders for their button order. Start with the existing component from `parentConfiguration`, call `listBuilder.modify`, then remove, insert, or replace items by stable button IDs. | Operation | Purpose | | --- | --- | | `addFirst` | Insert at the beginning | | `addLast` | Insert at the end | | `addBefore` | Insert before a matching ID | | `addAfter` | Insert after a matching ID | | `replace` | Replace a matching ID | | `remove` | Remove a matching ID | ID-based operations accept `failIfNotFound`. Keep the default `true` when the example expects the item to exist in the current editor configuration. Default item order can change between editor versions. Use `modify` when you want to adapt the current default configuration; if your workflow needs strict ordering, define the complete component list with the component-specific `*.ListBuilder.remember { ... }` APIs instead. > **Note:** This example starts from the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) > with `EditorConfiguration.remember(::DesignConfigurationBuilder).then { ... }`, > so `parentConfiguration` provides the Starter Kit components to rearrange. If > you copy these component lambdas into a fresh `EditorConfiguration.remember` > block, there are no Starter Kit components to inherit; pass in an existing > configuration or component to modify, or build your own list builder. ## Rearranging the Navigation Bar The navigation bar can use aligned item groups. When the source builder uses aligned groups, pass the target `Alignment` to `addFirst` or `addLast`. Here we move undo and redo from the trailing group to the leading group. ```kotlin highlight-android-navigation-bar navigationBar = navigationBarComponent@{ val sourceNavigationBar = parentConfiguration?.navigationBar as? NavigationBar ?: return@navigationBarComponent null val updatedListBuilder = sourceNavigationBar.listBuilder.modify { remove(id = NavigationBar.Button.Id.undo, failIfNotFound = true) remove(id = NavigationBar.Button.Id.redo, failIfNotFound = true) addFirst(alignment = Alignment.Start) { NavigationBar.Button.rememberUndo() } addFirst(alignment = Alignment.Start) { NavigationBar.Button.rememberRedo() } } remember(sourceNavigationBar, updatedListBuilder) { sourceNavigationBar.copy(listBuilder = updatedListBuilder) } } ``` Common navigation bar IDs include `NavigationBar.Button.Id.closeEditor`, `NavigationBar.Button.Id.undo`, `NavigationBar.Button.Id.redo`, and `NavigationBar.Button.Id.export`. ## Rearranging the Canvas Menu The canvas menu appears next to the selected block. Remove contextual actions that should not be prominent in your workflow while leaving the remaining default actions in their existing order. When you remove a grouped action cluster, remove its adjacent dividers as well so the remaining buttons stay visually grouped. The divider IDs shown here belong to the Design Editor Starter Kit; custom configurations should assign stable IDs to their own dividers and remove those IDs instead. `failIfNotFound = false` is useful when extending configurations where those IDs may not be present. ```kotlin highlight-android-canvas-menu canvasMenu = canvasMenuComponent@{ val sourceCanvasMenu = parentConfiguration?.canvasMenu as? CanvasMenu ?: return@canvasMenuComponent null val updatedListBuilder = sourceCanvasMenu.listBuilder.modify { // These divider IDs come from the Design Editor Starter Kit. // Give custom dividers stable IDs and remove those IDs instead. val designCanvasMenuDivider1 = EditorComponentId("ly.img.component.canvasMenu.divider1") val designCanvasMenuDivider2 = EditorComponentId("ly.img.component.canvasMenu.divider2") remove(id = designCanvasMenuDivider1, failIfNotFound = false) remove(id = CanvasMenu.Button.Id.bringForward, failIfNotFound = false) remove(id = CanvasMenu.Button.Id.selectGroup, failIfNotFound = false) remove(id = CanvasMenu.Button.Id.sendBackward, failIfNotFound = false) remove(id = designCanvasMenuDivider2, failIfNotFound = false) } remember(sourceCanvasMenu, updatedListBuilder) { sourceCanvasMenu.copy(listBuilder = updatedListBuilder) } } ``` Common canvas menu IDs include `CanvasMenu.Button.Id.bringForward`, `CanvasMenu.Button.Id.sendBackward`, `CanvasMenu.Button.Id.duplicate`, and `CanvasMenu.Button.Id.delete`. ## Rearranging the Dock The dock provides access to libraries and tools at the bottom of the editor. In this example, we move the text library to the first position and remove the camera button inherited from the Design Editor Starter Kit. ```kotlin highlight-android-dock dock = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { remove(id = Dock.Button.Id.textLibrary, failIfNotFound = true) addFirst { Dock.Button.rememberTextLibrary() } remove(id = Dock.Button.Id.imglyCamera, failIfNotFound = true) } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } ``` Common dock IDs include `Dock.Button.Id.systemGallery`, `Dock.Button.Id.imglyCamera`, `Dock.Button.Id.textLibrary`, and `Dock.Button.Id.shapesLibrary`. ## Rearranging the Inspector Bar The inspector bar shows contextual actions for the selected block. Move the duplicate action before layer options by removing it first, then adding it back before `InspectorBar.Button.Id.layer`. ```kotlin highlight-android-inspector-bar inspectorBar = inspectorBarComponent@{ val sourceInspectorBar = parentConfiguration?.inspectorBar as? InspectorBar ?: return@inspectorBarComponent null val updatedListBuilder = sourceInspectorBar.listBuilder.modify { remove(id = InspectorBar.Button.Id.duplicate, failIfNotFound = true) addBefore(id = InspectorBar.Button.Id.layer, failIfNotFound = true) { InspectorBar.Button.rememberDuplicate() } } remember(sourceInspectorBar, updatedListBuilder) { sourceInspectorBar.copy(listBuilder = updatedListBuilder) } } ``` Common inspector bar IDs include `InspectorBar.Button.Id.layer`, `InspectorBar.Button.Id.crop`, `InspectorBar.Button.Id.formatText`, and `InspectorBar.Button.Id.delete`. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builderFactory=_)` | Creates the base editor configuration | | `EditorConfiguration.then(builder=_)` | Extends an existing editor configuration | | `NavigationBar.ListBuilder.remember(builder=_)` | Builds navigation bar items | | `CanvasMenu.ListBuilder.remember(builder=_)` | Builds canvas menu items | | `Dock.ListBuilder.remember(builder=_)` | Builds dock items | | `InspectorBar.ListBuilder.remember(builder=_)` | Builds inspector bar items | | `listBuilder.modify(builder=_)` | Applies list-builder changes without replacing the whole component | | `addFirst(alignment=_, block=_)` | Inserts an item at the beginning of an aligned group | | `addFirst(block=_)` | Inserts an item at the beginning of an unaligned list | | `addLast(alignment=_, block=_)` | Inserts an item at the end of an aligned group | | `addLast(block=_)` | Inserts an item at the end of an unaligned list | | `addBefore(id=_, failIfNotFound=_, block=_)` | Inserts an item before a matching component ID | | `addAfter(id=_, failIfNotFound=_, block=_)` | Inserts an item after a matching component ID | | `replace(id=_, failIfNotFound=_, block=_)` | Replaces an item with a matching component ID | | `remove(id=_, failIfNotFound=_)` | Removes an item with a matching component ID | ## Next Steps - [Add a New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) - Add a custom button to the UI to trigger specific actions or workflows. - [Hide Elements](https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/) - Learn how to hide and show UI elements in CE.SDK using the Feature API, ordering APIs, and panel management across different platforms. - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) - Full canvas menu customization - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Full dock customization --- ## 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 Timeline" description: "Customize the video timeline's add-content buttons, header, height, and expanded state." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/customization/video-timeline-7a0400/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) > [Video Timeline](https://img.ly/docs/cesdk/android/user-interface/customization/video-timeline-7a0400/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-timeline/TimelineCustomizationSolution.kt reference-only import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState 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.unit.dp import ly.img.editor.Editor import ly.img.editor.core.component.Button import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.HorizontalListBuilder import ly.img.editor.core.component.Timeline import ly.img.editor.core.component.UnalignedListBuilder import ly.img.editor.core.component.data.TimelineHeight import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberAddAudio import ly.img.editor.core.component.rememberAddClip import ly.img.editor.core.component.rememberAudiosLibrary import ly.img.editor.core.component.rememberCamera import ly.img.editor.core.component.rememberGallery import ly.img.editor.core.component.rememberImglyCamera import ly.img.editor.core.component.rememberLibrary import ly.img.editor.core.component.rememberLoop import ly.img.editor.core.component.rememberMusic import ly.img.editor.core.component.rememberOverlaysLibrary import ly.img.editor.core.component.rememberPlayPause import ly.img.editor.core.component.rememberResizeAll import ly.img.editor.core.component.rememberStickersAndShapesLibrary import ly.img.editor.core.component.rememberSystemGallery import ly.img.editor.core.component.rememberTextLibrary import ly.img.editor.core.component.rememberTimecode import ly.img.editor.core.component.rememberToggleExpanded import ly.img.editor.core.component.rememberVoiceoverRecord 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.iconpack.IconPack import ly.img.editor.core.iconpack.LibraryElements import ly.img.editor.core.iconpack.Music import ly.img.editor.core.iconpack.VolumeHigh import ly.img.editor.core.state.EditorViewMode object AddStockFootage : EditorEvent // Add this composable to your NavHost @Composable fun TimelineCustomizationSolution( license: String, onClose: (Throwable?) -> Unit, ) { // Note that the value is reset on configuration changes. // Default implementation uses editorContext.mutableStateOf which survives configuration changes. val isTimelineExpanded = remember { mutableStateOf(false) } Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { bottomPanel = { rememberCustomizedTimeline(expandedState = isTimelineExpanded) } // Demo scaffolding, not part of the lesson: the dock a video editor usually offers, // so there is media to arrange on the timeline. dock = { rememberVideoDock() } } }, onClose = onClose, ) } @Composable private fun rememberVideoDock() = Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemGallery() } add { Dock.Button.rememberImglyCamera() } add { Dock.Button.rememberOverlaysLibrary() } add { Dock.Button.rememberTextLibrary() } add { Dock.Button.rememberStickersAndShapesLibrary() } add { Dock.Button.rememberAudiosLibrary() } add { Dock.Button.rememberVoiceoverRecord() } add { Dock.Button.rememberResizeAll() } } } } @Composable private fun rememberCustomizedTimeline(expandedState: MutableState) = Timeline.remember { // Your own state decides whether the timeline starts expanded. The expand/collapse toggle // writes back to it, so this value keeps matching what is on screen. scope = { remember(this) { Timeline.Scope(parentScope = this, expandedState = expandedState) } } addClipButton = { Timeline.Button.rememberAddClip { optionsBuilder = { UnalignedListBuilder.remember { add { Timeline.AddClipOption.rememberCamera() } add { Timeline.AddClipOption.rememberLibrary() } add { Timeline.AddClipOption.remember { id = { EditorComponentId("my.company.timeline.addClip.stockFootage") } vectorIcon = { IconPack.LibraryElements } textString = { "Stock Footage" } onClick = { editorContext.eventHandler.send(AddStockFootage) } } } } } } } addAudioButton = { Timeline.Button.rememberAddAudio { optionsBuilder = { UnalignedListBuilder.remember { add { Timeline.AddAudioOption.rememberMusic() } } } } } // The timeline renders tracks alone, so the header is declared rather than modified. headerListBuilder = { Timeline.HeaderListBuilder.remember { aligned(alignment = Alignment.Start) { add { Timeline.Label.rememberTimecode() } } aligned(alignment = Alignment.CenterHorizontally) { add { Timeline.Button.rememberPlayPause() } } aligned(alignment = Alignment.End) { add { rememberMuteButton() } add { Timeline.Button.rememberToggleExpanded() } } } } // Grow to at most three overlay tracks. This is the default. height = { TimelineHeight.Dynamic(maximumTracks = 3) } // Or pin the timeline to exactly two overlay tracks: // height = { TimelineHeight.Fixed(tracks = 2) } } @Composable private fun rememberMuteButton(): Button { var muted by remember { mutableStateOf(false) } return Button.remember(::timelineHeaderButtonBuilder) { id = { EditorComponentId("my.company.timeline.button.mute") } vectorIcon = { IconPack.VolumeHigh } contentDescription = { if (muted) "Unmute" else "Mute" } onClick = { muted = muted.not() } } } private fun timelineHeaderButtonBuilder() = Timeline.ButtonBuilder() @Composable fun rememberRestatedAddClipButton() = Timeline.Button.rememberAddClip { optionsBuilder = { UnalignedListBuilder.remember { add { // Reordered, and the gallery source keeps its icon while its label changes. Timeline.AddClipOption.rememberGallery { textString = { "From Device" } } } add { // A built-in source keeps its label and icon while its behavior is replaced. Timeline.AddClipOption.rememberCamera { onClick = { editorContext.eventHandler.send(AddStockFootage) } } } add { Timeline.AddClipOption.rememberLibrary() } } } } @Composable fun rememberConditionalAddClipButton() = Timeline.Button.rememberAddClip { optionsBuilder = { val state by editorContext.state.collectAsState() UnalignedListBuilder.remember { // The options lambda is re-evaluated on recomposition, so read state inside it // rather than capturing a value from the caller. if (state.viewMode is EditorViewMode.Edit) { add { Timeline.AddClipOption.rememberCamera() } } add { Timeline.AddClipOption.rememberLibrary() } } } } @Composable fun rememberSingleSourceAddClipButton() = Timeline.Button.rememberAddClip { optionsBuilder = { UnalignedListBuilder.remember { add { Timeline.AddClipOption.rememberLibrary() } } } } // The lane sizes a replacement to one track row, so drop the header button's square touch target. @Composable fun rememberCustomAddAudioButton() = Button.remember(Timeline::ButtonBuilder) { modifier = { Modifier.size(width = 96.dp, height = 40.dp) } id = { EditorComponentId("my.company.timeline.button.soundtrack") } vectorIcon = { IconPack.Music } textString = { "Soundtrack" } onClick = { editorContext.eventHandler.send(AddStockFootage) } } @Composable fun rememberDeclaredHeader(): HorizontalListBuilder> = Timeline.HeaderListBuilder.remember { aligned(alignment = Alignment.Start) { add { Timeline.Label.rememberTimecode() } } aligned(alignment = Alignment.CenterHorizontally) { add { Timeline.Button.rememberPlayPause() } } aligned(alignment = Alignment.End) { add { Timeline.Button.rememberLoop() } add { Timeline.Button.rememberToggleExpanded() } } } // The defaults are already bare: no header, and no lane buttons. An empty header removes the // player bar with it, and the tracks keep rendering. @Composable fun rememberBareTimeline() = Timeline.remember() ``` Customize the video timeline — the playback and arrangement surface below the canvas — by configuring its add-content buttons, header, height, and expanded state instead of rebuilding it. ![The timeline with a customized header and an Add Clip menu offering a custom source](./assets/android.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-timeline) ## Timeline Architecture `Timeline` is both the component you mount and the namespace for everything that customizes it. It renders three configurable surfaces: - The **header** above the ruler, holding the timecode, playback controls, and the expand/collapse toggle. - The **"Add Clip" button** in the background lane, after the last clip of the main sequence. - The **"Add Audio" button** at the foot of the overlay stack, below the overlay tracks. **Key types:** - **`TimelineBuilder`** — the builder `Timeline.remember` hands you. It exposes `addClipButton`, `addAudioButton`, `headerListBuilder`, `height`, and `scope`. - **`Timeline.Scope`** — the timeline scope, constructed with its parent scope and a `MutableState` for the expanded state. - **`Timeline.ItemScope`** — the scope every header item and lane button is built in. `editorContext` reaches the engine, the event handler, and the editor state from there. - **`Timeline.AddClipOption`** / **`Timeline.AddAudioOption`** — the entries the two lane buttons offer. Each is an ordinary editor component, so it takes the same `vectorIcon`, `textString`, `enabled` and `visible` properties as a dock button. - **`Timeline.AddClipButtonBuilder`** / **`Timeline.AddAudioButtonBuilder`** — configure each lane button, including its `optionsBuilder`. Use `UnalignedListBuilder.remember` and `add { ... }` to declare the menu entries. - **`TimelineHeight`** — `Dynamic` or `Fixed`, counted in track rows. Each property is a lambda evaluated during composition, so what the timeline shows can follow editor state rather than being fixed when the editor is created. ## Configuration Mount the timeline as the editor's bottom panel. `Timeline.remember` builds the component, which renders its tracks and nothing else: no header, and no lane buttons. Each section below turns one of those surfaces on. The video starter kit does the same, so its timeline is a configuration rather than a separate component. ```kotlin highlight-android-bottom-panel bottomPanel = { rememberCustomizedTimeline(expandedState = isTimelineExpanded) } ``` That timeline is built by a `Timeline.remember` block, and every section below sets one property on its builder: ```kotlin highlight-android-timeline-builder @Composable private fun rememberCustomizedTimeline(expandedState: MutableState) = Timeline.remember { ``` The builder block runs once, so avoid reassigning properties conditionally inside it. Read changing values inside the individual property lambdas instead — those re-evaluate on recomposition. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers how `EditorConfiguration` sets up the editor as a whole, and the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) shows the complete editor surface this timeline sits in. ## Device Permissions The default add-content options reach two capabilities Android gates behind a runtime prompt: `Timeline.AddClipOption.rememberCamera()` records through the camera, and `Timeline.AddAudioOption.rememberVoiceover()` records through the microphone. The CE.SDK camera module declares `android.permission.CAMERA` and `android.permission.RECORD_AUDIO` in its own manifest, so manifest merging adds them to your app and you do not declare them yourself. The camera and voiceover flows request the runtime grant when the user first opens them. Device media is your app's responsibility. `Timeline.AddClipOption.rememberGallery()` opens the library's gallery source, which reads the device's media store, so declare the `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO`, and `READ_MEDIA_AUDIO` permissions your minimum SDK requires — plus `READ_MEDIA_VISUAL_USER_SELECTED` to support partial access, where the user grants only selected items rather than the whole library. ## Add Clip Sources The "Add Clip" button opens a menu of sources that add to the background track. `Timeline.Button.rememberAddClip` decides which sources it offers. There are three built-in ones — `rememberCamera()`, `rememberGallery()`, and `rememberLibrary()`. The button offers camera plus library by default. To customize the menu, set `optionsBuilder` to an `UnalignedListBuilder.remember` block and declare each entry with `add { ... }`, in display order. Menu entries do not use the header's `aligned { ... }` groups. This example includes camera, library, and a custom stock-footage source: ```kotlin highlight-android-add-clip addClipButton = { Timeline.Button.rememberAddClip { optionsBuilder = { UnalignedListBuilder.remember { add { Timeline.AddClipOption.rememberCamera() } add { Timeline.AddClipOption.rememberLibrary() } add { Timeline.AddClipOption.remember { id = { EditorComponentId("my.company.timeline.addClip.stockFootage") } vectorIcon = { IconPack.LibraryElements } textString = { "Stock Footage" } onClick = { editorContext.eventHandler.send(AddStockFootage) } } } } } } } ``` A custom source's `onClick` runs in the editor scope, so it can reach the engine, the configured asset library, and the event handler through `editorContext`. Sending your own event keeps the picker logic in your configuration: ```kotlin highlight-android-custom-event object AddStockFootage : EditorEvent ``` Every built-in source carries a stable id — `Timeline.AddClipOption.Id.camera`, `.gallery`, `.library`, and `Timeline.AddAudioOption.Id.music` / `.voiceover` for the audio ones. Use them to find a source in a list, or to give a replacement the same identity. ### Reorder, Relabel, and Replace Behavior List the sources yourself to control the order or drop one. Each built-in factory accepts a builder with `icon`, `vectorIcon`, `text`, `textString`, and `onClick` properties, so a source can keep its behavior while its presentation changes, or keep its presentation while its behavior changes: ```kotlin highlight-android-add-clip-restated @Composable fun rememberRestatedAddClipButton() = Timeline.Button.rememberAddClip { optionsBuilder = { UnalignedListBuilder.remember { add { // Reordered, and the gallery source keeps its icon while its label changes. Timeline.AddClipOption.rememberGallery { textString = { "From Device" } } } add { // A built-in source keeps its label and icon while its behavior is replaced. Timeline.AddClipOption.rememberCamera { onClick = { editorContext.eventHandler.send(AddStockFootage) } } } add { Timeline.AddClipOption.rememberLibrary() } } } } ``` > **Caution:** Assigning `optionsBuilder` replaces the default menu. Leave it unset when you > want the built-in sources and any defaults added in future releases. ### Hide a Source Conditionally Read editor state inside `optionsBuilder` and conditionally call `add { ... }` to include a source: ```kotlin highlight-android-add-clip-conditional @Composable fun rememberConditionalAddClipButton() = Timeline.Button.rememberAddClip { optionsBuilder = { val state by editorContext.state.collectAsState() UnalignedListBuilder.remember { // The options lambda is re-evaluated on recomposition, so read state inside it // rather than capturing a value from the caller. if (state.viewMode is EditorViewMode.Edit) { add { Timeline.AddClipOption.rememberCamera() } } add { Timeline.AddClipOption.rememberLibrary() } } } } ``` ### One Source Instead of a Menu When only one source remains, the button performs it directly on tap instead of opening a menu and uses that source's text as its label. Set the button's `textString` to override this automatic label. This example opens the asset library directly: ```kotlin highlight-android-add-clip-single @Composable fun rememberSingleSourceAddClipButton() = Timeline.Button.rememberAddClip { optionsBuilder = { UnalignedListBuilder.remember { add { Timeline.AddClipOption.rememberLibrary() } } } } ``` An `UnalignedListBuilder.remember` block with no entries hides the button entirely. ## Add Audio Sources The "Add Audio" button works the same way. `Timeline.Button.rememberAddAudio` offers `rememberMusic()` and `rememberVoiceover()` by default. Override `optionsBuilder` to change its sources. This example includes only music, so the button uses the music source's label and opens the audio library directly: ```kotlin highlight-android-add-audio addAudioButton = { Timeline.Button.rememberAddAudio { optionsBuilder = { UnalignedListBuilder.remember { add { Timeline.AddAudioOption.rememberMusic() } } } } } ``` ### Replace the Whole Button Create a custom button with `Button.remember(Timeline::ButtonBuilder)` to give it a `Timeline.ItemScope`. Assign the component returned by `rememberCustomAddAudioButton()` to `addAudioButton`: ```kotlin highlight-android-add-audio-custom // The lane sizes a replacement to one track row, so drop the header button's square touch target. @Composable fun rememberCustomAddAudioButton() = Button.remember(Timeline::ButtonBuilder) { modifier = { Modifier.size(width = 96.dp, height = 40.dp) } id = { EditorComponentId("my.company.timeline.button.soundtrack") } vectorIcon = { IconPack.Music } textString = { "Soundtrack" } onClick = { editorContext.eventHandler.send(AddStockFootage) } } ``` The lane sizes a replacement to one track row and reserves its height by row count, so a taller button overflows the lane. Set `modifier` in the button's builder to give it an explicit size, as the example does with `96.dp` by `40.dp`. ## Timeline Header Header items are grouped by alignment — `Alignment.Start`, `Alignment.CenterHorizontally`, or `Alignment.End`. Each group is positioned independently, so an item's alignment decides which edge it hugs; there is no separate spacer item. The header is empty until you declare it. The ready-made items are `Timeline.Label.rememberTimecode()`, `Timeline.Button.rememberPlayPause()`, `rememberLoop()`, and `rememberToggleExpanded()`. Each is an editor component with a configurable builder. Play/pause and loop use `Timeline.ButtonBuilder`, which exposes `onClick` and icon properties. Timecode and expand/collapse use `Timeline.ItemBuilder`; customize their rendered content through `decoration` or hide them with `visible`. The timecode displays a value and has no action, so it sits under `Timeline.Label` rather than `Timeline.Button`. ### Declare the Header Declare the items you want, grouped by alignment. A centered group is centered as a whole, so keep one item in it — otherwise that item will not sit in the middle of the header: ```kotlin highlight-android-modify-header // The timeline renders tracks alone, so the header is declared rather than modified. headerListBuilder = { Timeline.HeaderListBuilder.remember { aligned(alignment = Alignment.Start) { add { Timeline.Label.rememberTimecode() } } aligned(alignment = Alignment.CenterHorizontally) { add { Timeline.Button.rememberPlayPause() } } aligned(alignment = Alignment.End) { add { rememberMuteButton() } add { Timeline.Button.rememberToggleExpanded() } } } } ``` Prefer the alignment-anchored operations over `addAfter(id=)` and `addBefore(id=)` when the position only has to be "at this edge". The id-anchored operations fail when the item they name is absent, which is what happens once a built-in item you anchored on is removed or renamed. Pass `failIfNotFound = false` to make an anchored operation tolerate that. A custom header item is an ordinary editor component, so it can hold its own state: ```kotlin highlight-android-custom-header-item @Composable private fun rememberMuteButton(): Button { var muted by remember { mutableStateOf(false) } return Button.remember(::timelineHeaderButtonBuilder) { id = { EditorComponentId("my.company.timeline.button.mute") } vectorIcon = { IconPack.VolumeHigh } contentDescription = { if (muted) "Unmute" else "Mute" } onClick = { muted = muted.not() } } } private fun timelineHeaderButtonBuilder() = Timeline.ButtonBuilder() ``` ### Declare the Header Outright Assign a list builder to `headerListBuilder` to declare the header from the groups you want: ```kotlin highlight-android-header @Composable fun rememberDeclaredHeader(): HorizontalListBuilder> = Timeline.HeaderListBuilder.remember { aligned(alignment = Alignment.Start) { add { Timeline.Label.rememberTimecode() } } aligned(alignment = Alignment.CenterHorizontally) { add { Timeline.Button.rememberPlayPause() } } aligned(alignment = Alignment.End) { add { Timeline.Button.rememberLoop() } add { Timeline.Button.rememberToggleExpanded() } } } ``` ## Remove a Surface None of the three surfaces is present until you add it, so the bare component already renders tracks alone. Leaving `addAudioButton` unset also lets the timeline reclaim the row that button would occupy, and an empty header renders no player bar rather than an empty strip: ```kotlin highlight-android-remove // The defaults are already bare: no header, and no lane buttons. An empty header removes the // player bar with it, and the tracks keep rendering. @Composable fun rememberBareTimeline() = Timeline.remember() ``` ## Timeline Height The timeline's height is expressed in track rows rather than pixels. The count covers the rows stacked above the background track — the overlay tracks, plus the caption lane when the scene has one. The background track is always shown and is never counted. `TimelineHeight.Dynamic` is the default. The timeline hugs its rows and grows to at most `maximumTracks`. `TimelineHeight.Fixed` sizes the timeline to show exactly that many rows and stops auto-resizing — `0` still shows the background track, and negative values are clamped to `0`: ```kotlin highlight-android-height // Grow to at most three overlay tracks. This is the default. height = { TimelineHeight.Dynamic(maximumTracks = 3) } // Or pin the timeline to exactly two overlay tracks: // height = { TimelineHeight.Fixed(tracks = 2) } ``` Because `height` is a lambda, it can follow editor state rather than being a constant. ## Expanded State By default the editor owns the expanded state, which starts expanded and survives configuration changes. To provide your own `MutableState`, override the builder's `scope` and construct `Timeline.Scope(parentScope = this, expandedState = expandedState)`. Remember that scope using the parent scope as its key: ```kotlin highlight-android-expanded // Your own state decides whether the timeline starts expanded. The expand/collapse toggle // writes back to it, so this value keeps matching what is on screen. scope = { remember(this) { Timeline.Scope(parentScope = this, expandedState = expandedState) } } ``` The example passes a state initialized to `false`, so the timeline starts collapsed. Within the timeline and its item scopes, `editorContext.expandedState` exposes that same state. It stays in sync in both directions: writing to it expands or collapses the timeline, and the user's own toggle writes back. Collapsing leaves the header visible and hides the tracks below it. The example creates this state with `remember`. Use `rememberSaveable` instead if your custom expanded state should survive configuration changes. ## API Reference ### Methods | Method | Description | | --- | --- | | `Timeline.remember(builder=_)` | Builds the timeline component, mounted as bottom-panel content | | `Timeline.Button.rememberAddClip(builder=_)` | The "Add Clip" button with the given sources | | `Timeline.Button.rememberAddAudio(builder=_)` | The "Add Audio" button with the given sources | | `Timeline.Label.rememberTimecode(builder=_)` | Header label showing the playhead position and total duration | | `Timeline.Button.rememberPlayPause(builder=_)` | Header play/pause button | | `Timeline.Button.rememberLoop(builder=_)` | Header looping toggle | | `Timeline.Button.rememberToggleExpanded(builder=_)` | Header expand/collapse toggle | | `Timeline.HeaderListBuilder.remember(builder=_)` | Declares the header from alignment groups | | `UnalignedListBuilder.remember(builder=_)` | Declares a lane button's menu entries | | `add(block=_)` | Appends a menu entry or header item | | `Timeline.Scope(parentScope=_, expandedState=_)` | Creates a timeline scope using the supplied expanded state | | `Button.remember(builderFactory=_, builder=_)` | Builds a custom header or lane button | | `Timeline.AddClipOption.rememberCamera(builder=_)` | Records a clip with the camera | | `Timeline.AddClipOption.rememberGallery(builder=_)` | Opens the asset library's device gallery | | `Timeline.AddClipOption.rememberLibrary(builder=_)` | Opens the asset library | | `Timeline.AddClipOption.remember(builder=_)` | A custom clip source | | `Timeline.AddAudioOption.rememberMusic(builder=_)` | Opens the audio asset library | | `Timeline.AddAudioOption.rememberVoiceover(builder=_)` | Starts a voiceover recording | | `Timeline.AddAudioOption.remember(builder=_)` | A custom audio source | | `remove(id=_, failIfNotFound=_)` | Removes a header item by id | | `addFirst(alignment=_, block=_)` | Prepends a header item to an alignment group | | `editorContext.eventHandler.send(event=_)` | Sends an editor event from an option or button | ### Properties Builder properties below are assigned through composable lambdas; the types shown are their return values. | Property | Type | Description | | --- | --- | --- | | `addClipButton` | `EditorComponent<*>?` | The "Add Clip" button. `null`, the default, shows none | | `addAudioButton` | `EditorComponent<*>?` | The "Add Audio" button. `null`, the default, shows none | | `headerListBuilder` | `HorizontalListBuilder>` | The header above the ruler. Empty by default | | `height` | `TimelineHeight` | `Dynamic(maximumTracks=3)` by default, or `Fixed(tracks=_)` | | `scope` | `Timeline.Scope` | The timeline scope. Construct it with your own expanded state to drive and observe expansion | The following properties belong to the lane button builders and timeline scopes: | Property | Type | Description | | --- | --- | --- | | `Timeline.AddClipButtonBuilder.optionsBuilder` | `UnalignedListBuilder` | Clip menu entries; camera plus library by default | | `Timeline.AddAudioButtonBuilder.optionsBuilder` | `UnalignedListBuilder` | Audio menu entries; music plus voiceover by default | | `textString` | `String` | Lane button label override; otherwise a single source supplies its own label | | `editorContext.expandedState` | `MutableState` | Expanded state available within `Timeline.Scope` and `Timeline.ItemScope` | Built-in components expose stable ids: | Property | Type | Description | | --- | --- | --- | | `Timeline.Button.Id.loop` | `EditorComponentId` | Id constants for the built-in header items | | `Timeline.AddClipOption.Id.camera` | `EditorComponentId` | Id constants for the built-in clip sources | | `Timeline.AddAudioOption.Id.music` | `EditorComponentId` | Id constants for the built-in audio sources | ## Next Steps - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) — Build and edit timelines programmatically with the engine. - [Record Voiceover](https://img.ly/docs/cesdk/android/create-audio/audio/record-voiceover-07e8e1/) — The recording flow behind the voiceover add-audio source. - [Hide Elements](https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/) — Remove editor items your integration does not need - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — Bottom toolbar that opens asset libraries and sheets. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) — Contextual toolbar for the selected block. - [Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) — Top bar 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: "UI Events" description: "Listen to UI events and trigger custom logic based on user interactions in the editor interface." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/events-514b70/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-ui-events/UiEventsEditorSolution.kt reference-only import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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.state.EditorViewMode data class UiEventsState( val isLoading: Boolean = false, val lastEvent: String = "Waiting for editor events", ) object ShowLoading : EditorEvent object HideLoading : EditorEvent data class WorkflowStepChanged( val label: String, ) : EditorEvent // Add this composable to your NavHost @Composable fun UiEventsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { var state by remember { mutableStateOf(UiEventsState()) } Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { onLoaded = { editorContext.eventHandler.send( WorkflowStepChanged(label = "Editor loaded"), ) } onEvent = { event -> state = when (event) { ShowLoading -> state.copy( isLoading = true, lastEvent = "Started custom loading state", ) HideLoading -> state.copy( isLoading = false, lastEvent = "Finished custom loading state", ) is WorkflowStepChanged -> state.copy(lastEvent = event.label) is EditorEvent.SetViewMode -> { val viewMode = when (event.viewMode) { is EditorViewMode.Edit -> "Edit" is EditorViewMode.Preview -> "Preview" is EditorViewMode.Pages -> "Pages" } state.copy(lastEvent = "View mode changed to $viewMode") } is EditorEvent.Export.Start -> { state.copy(lastEvent = "Export requested") } else -> state } } overlay = { EditorComponent.remember { decoration = { val syncScope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize()) { Text( text = state.lastEvent, modifier = Modifier .align(Alignment.TopCenter) .padding(top = 24.dp), ) Column( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { Button( onClick = { editorContext.eventHandler.send( EditorEvent.SetViewMode(EditorViewMode.Preview()), ) }, ) { Text("Preview") } Button( onClick = { editorContext.eventHandler.send(EditorEvent.Export.Start()) }, ) { Text("Request Export") } Button( enabled = !state.isLoading, onClick = { syncScope.launch { editorContext.eventHandler.send(ShowLoading) editorContext.eventHandler.send( WorkflowStepChanged(label = "Sync requested"), ) // Simulate asynchronous app work before clearing the loading event. delay(1_200) editorContext.eventHandler.send(HideLoading) } }, ) { Text("Run Sync") } } if (state.isLoading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), ) } } } } } } }, onClose = onClose, ) } ``` Listen to editor UI events, send custom events from editor configuration code, and route those events into your Android app state. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-configuration-ui-events) Editor UI events are messages sent through the editor's `EditorEventHandler`. Built-in editor components use them for actions such as changing view mode or starting export, and your configuration can send custom events for app-specific workflows. > **Note:** The [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) shows a complete > Android editor surface that uses the same configuration layer. ## Editor Events vs Engine Events Use UI events when the event belongs to the editor interface: a button tap, sheet transition, export request, or custom workflow step. The event flow is: `editorContext.eventHandler.send(event)` dispatches an `EditorEvent`, the editor handles known internal events when applicable, and `onEvent` receives internal and custom events for your app logic. Use [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) instead when you need CreativeEngine block lifecycle changes such as created, updated, or destroyed blocks. That guide uses `engine.event.subscribe()` and is separate from the editor UI event bus. ## Define Custom Events Custom events are Kotlin objects or data classes that implement `EditorEvent`. Keep them small and specific to the UI workflow you want to observe. ```kotlin highlight-android-custom-events object ShowLoading : EditorEvent object HideLoading : EditorEvent data class WorkflowStepChanged( val label: String, ) : EditorEvent ``` ## Use Events for App State Keep UI state in regular Compose state and update it from `onEvent`. The event handler should decide what happened; composables read the state when rendering event-driven UI. ```kotlin highlight-android-event-state data class UiEventsState( val isLoading: Boolean = false, val lastEvent: String = "Waiting for editor events", ) ``` ```kotlin highlight-android-remember-state var state by remember { mutableStateOf(UiEventsState()) } ``` ## Observe Editor Events Configure `onEvent` inside `EditorConfiguration.remember`. The callback runs for built-in events and for the custom event types defined above. ```kotlin highlight-android-observe-events onEvent = { event -> state = when (event) { ShowLoading -> state.copy( isLoading = true, lastEvent = "Started custom loading state", ) HideLoading -> state.copy( isLoading = false, lastEvent = "Finished custom loading state", ) is WorkflowStepChanged -> state.copy(lastEvent = event.label) is EditorEvent.SetViewMode -> { val viewMode = when (event.viewMode) { is EditorViewMode.Edit -> "Edit" is EditorViewMode.Preview -> "Preview" is EditorViewMode.Pages -> "Pages" } state.copy(lastEvent = "View mode changed to $viewMode") } is EditorEvent.Export.Start -> { state.copy(lastEvent = "Export requested") } else -> state } } ``` This example tracks `EditorEvent.SetViewMode` and `EditorEvent.Export.Start` as built-in UI events. The sample overlay sends those events from buttons so the reducer is reachable in the demo, and it handles custom events in the same reducer so app logic has a single event entry point. ## Send Events from Editor UI Send custom events from editor callbacks or custom UI components through `editorContext.eventHandler.send(...)`. The sample sends a workflow step after the editor finishes loading. ```kotlin highlight-android-send-custom-events onLoaded = { editorContext.eventHandler.send( WorkflowStepChanged(label = "Editor loaded"), ) } ``` The same handler is available from components configured through `EditorConfiguration`, so custom buttons can dispatch app-specific events without bypassing the editor event pipeline. The overlay below sends built-in and custom events from buttons, then renders the last observed event plus a loading spinner from the same state object. ```kotlin highlight-android-overlay-actions overlay = { EditorComponent.remember { decoration = { val syncScope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize()) { Text( text = state.lastEvent, modifier = Modifier .align(Alignment.TopCenter) .padding(top = 24.dp), ) Column( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { Button( onClick = { editorContext.eventHandler.send( EditorEvent.SetViewMode(EditorViewMode.Preview()), ) }, ) { Text("Preview") } Button( onClick = { editorContext.eventHandler.send(EditorEvent.Export.Start()) }, ) { Text("Request Export") } Button( enabled = !state.isLoading, onClick = { syncScope.launch { editorContext.eventHandler.send(ShowLoading) editorContext.eventHandler.send( WorkflowStepChanged(label = "Sync requested"), ) // Simulate asynchronous app work before clearing the loading event. delay(1_200) editorContext.eventHandler.send(HideLoading) } }, ) { Text("Run Sync") } } if (state.isLoading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), ) } } } } } ``` ## API Reference | API | Description | | --- | ----------- | | `EditorConfiguration.remember { onEvent = { ... } }` | Creates an editor configuration and registers the event observer. | | `editorContext.eventHandler.send(event=_)` | Dispatches a built-in or custom editor UI event. | | `EditorComponent.remember { decoration = { ... } }` | Creates a custom UI component that can send editor events from its callbacks. | ## Key Types | Type | Purpose | | ---- | ------- | | `EditorEvent` | Base interface for built-in and custom editor UI events. | | `EditorEvent.SetViewMode(viewMode=_)` | Built-in event type that sets the editor view mode. | | `EditorEvent.Export.Start` | Built-in event sent by export actions before `onExport` runs. | | `ShowLoading`, `HideLoading`, `WorkflowStepChanged` | Custom events used by the sample to update app-side Compose state. | ## Next Steps - [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) - subscribe to CreativeEngine block lifecycle events. - [Overlay](https://img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891/) - render event-driven dialogs and loading states over 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: "Localization" description: "Localize the CE.SDK editor UI on Android by overriding Android string resources for the languages your app supports." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/localization-508e20/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Localization](https://img.ly/docs/cesdk/android/user-interface/localization-508e20/) --- Localize the CE.SDK editor UI on Android by overriding the string resources that ship with the editor library. CE.SDK's Android editor UI uses standard Android string resources for labels, button text, sheet titles, dialogs, and accessibility descriptions. These resources apply wherever the CE.SDK editor UI renders Android strings, including ready-made surfaces such as the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). There is no runtime `setLocale()` or translation dictionary API for the Android editor UI. Android selects the active resources from the app or device locale, and your app overrides CE.SDK defaults by packaging resources with the same names. ## Default Languages and Resource Lookup CE.SDK ships editor resources for English and German: - English defaults live in [`sources/editor-core/src/main/res/values/strings.xml`](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values/strings.xml). - German defaults live in [`sources/editor-core/src/main/res/values-de/strings.xml`](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values-de/strings.xml). Android merges resources from your app module and its library dependencies when it builds the APK or AAB. If your app declares a string resource with the same name as a CE.SDK editor resource, your app's value takes precedence for that resource configuration. Locale-specific directories still matter. An override in `res/values/strings.xml` changes the default value, while CE.SDK's `values-de` value can still render on a German device unless you also provide a matching `res/values-de/strings.xml` override. ## Finding the Resource You Want to Translate Editor resource names use the `ly_img_editor_` prefix followed by the UI area, element type, and specific name. Search the English resource file for the visible text you want to change, then copy the matching resource name into your app. | Visible UI element | Resource name | | --- | --- | | Gallery dock button | `ly_img_editor_dock_button_gallery` | | Images asset library title | `ly_img_editor_asset_library_title_images` | | Text format sheet title | `ly_img_editor_sheet_format_text_title` | Plugin resources can live in plugin modules and usually use a plugin-specific prefix, such as `ly_img_plugin_*`. Check the plugin's own `res/values/strings.xml` file when you want to translate plugin UI. ## Overriding Existing Strings Add only the resources you want to replace to your app module. Keep the resource names unchanged: ```xml Media Typography ``` For each locale your app supports, add the same resource names to the matching locale directory: ```xml Medien Typografie ``` You do not need to copy the complete CE.SDK resource file for small terminology changes. Missing keys continue to use the defaults from the editor library. If you only need selected brand terms or label changes, the [Custom Labels](https://img.ly/docs/cesdk/android/user-interface/appearance/custom-labels-7794f7/) guide covers that narrower workflow in more detail. ## White-labeling and Custom Terminology Resource overrides are also the Android white-labeling mechanism for editor text. Keep related terms consistent across surfaces: if you rename "Images" to "Photos", update the dock button, the asset library title, and the image section label together. ```xml Photos Photos Photos ``` Resource values are packaged at build time. For tenant-specific or brand-specific wording, use Android product flavors, locale-specific resources, or custom Compose UI for the surface that needs runtime text changes. ## Adding Support for a New Language To add a language that CE.SDK does not ship, copy the English editor resource file into a locale-specific directory such as `res/values-fr/strings.xml`, then translate the values. The fragment below shows the file shape: ```xml Galerie Images Images Images Format ``` For complete language coverage, translate the full editor resource set. If a key is missing from your new locale, Android falls back to the default resource value, which can leave parts of the editor in English. When your app uses Android's per-app language picker, make sure the new locale is included in the app's supported locale configuration so users can select it. ## Troubleshooting ### My Override Has No Effect Confirm the resource name matches the CE.SDK resource exactly and that the file is in your app module's `res` directory. Resource names are resolved by exact string equality after Android's resource merge step. ### Some Strings Stay in German or English Check whether the active device or app locale uses a more specific resource directory. For example, a German device can still use CE.SDK's bundled `values-de` value when your app only overrides the default `res/values/strings.xml` value. ### A Formatted Label Renders Incorrectly Preserve Android format placeholders when you override strings that contain values such as `%s` or `%d`. The editor reads regular `ly_img_editor_*` resources with Android's native string formatting, so changing or removing placeholders can break labels that receive runtime values. ## Key Reference | Concern | Android resource location | | --- | --- | | Key naming convention | `ly_img_editor___` | | English editor strings | [`res/values/strings.xml`](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values/strings.xml) | | German editor strings | [`res/values-de/strings.xml`](https://github.com/imgly/cesdk-android/blob/v$UBQ_VERSION$/sources/editor-core/src/main/res/values-de/strings.xml) | | Override location | Your app module's `res/values/strings.xml` | | Locale-specific override | Your app module's `res/values-/strings.xml` | | Locale selection | Android app or device locale and resource qualifiers | ## Next Steps - [Custom Error Messages](https://img.ly/docs/cesdk/android/user-interface/custom-error-messages-2e8555/) — Override engine error dialogs with localized, customer-facing copy - [UI Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) — Customize editor components and layout - [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) — Explore editor configuration options - [Asset Library Customization](https://img.ly/docs/cesdk/android/import-media/asset-library/customize-c9a4de/) — Customize asset sources and 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: "Overview" description: "Use CE.SDK’s customizable, production-ready UI or replace it entirely with your own interface." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/overview-41101a/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [Overview](https://img.ly/docs/cesdk/android/user-interface/overview-41101a/) --- Use the ready-to-use Android editor UI, adapt it to your product, or build a completely custom interface on top of CreativeEngine. CE.SDK gives your Android app a complete editor UI for creating, editing, and exporting scenes. You can ship the default experience, configure its Jetpack Compose components, extend it with custom actions and panels, or bypass it and drive the engine from your own UI. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Architecture The Android editor separates the UI layer from the CreativeEngine. The engine owns scene data, block manipulation, rendering, playback, and export, while the UI layer presents controls and forwards user actions through configuration callbacks and editor events. At a high level, the editor consists of: - **CreativeEngine APIs** — The underlying APIs for scenes, blocks, assets, playback, and rendering. - **Compose UI components** — Bars, menus, sheets, overlays, and buttons configured through `EditorConfiguration`. - **Events and callbacks** — Hooks that let your app react to lifecycle, export, upload, close, and custom UI events. This separation lets you keep the default UI where it fits, replace selected surfaces, or build your own interface without changing how the engine stores and renders content. ## Default Android UI The default Android editor is optimized for touch-first editing. Its component layout is Android-specific, so use the Android component guides when you need exact customization APIs. ![Annotated Android CE.SDK editor UI showing the navigation bar, canvas, selected block, canvas menu, and dock.](./assets/android-editor-ui-overview.png) | Area | Purpose | | ---- | ------- | | Canvas | Shows the engine-rendered scene and handles direct manipulation of selected content. | | Navigation bar | Hosts top-level actions such as close, undo, redo, page navigation, and export. | | Dock | Provides bottom access to asset libraries, capture flows, and common editing tools. | | Canvas menu | Appears near the selected block with immediate contextual actions such as duplicate, delete, reorder, or group. | | Inspector bar | Shows selection-specific editing controls at the bottom of the editor. | | Panels and sheets | Present focused workflows such as asset libraries, crop, filters, layer controls, and custom task UIs. | ## Customizing the UI Use customization when the default editor flow is close to what you need, but your product needs different branding, ordering, or feature availability. - **Appearance** — Match your brand with themes, labels, fonts, icons, and color-related UI. - **Layout** — Reorder, replace, hide, or show component items in the navigation bar, dock, canvas menu, inspector bar, and panels. - **Behavior** — Enable features by context, react to selections, and connect editor callbacks to your app state. Most Android UI surfaces are configured through Compose builders on `EditorConfiguration`. You can start from the default component, modify its item list, or provide a replacement component when you need full control. ## Extending the UI Use extensions when the editor should expose functionality that is specific to your product or workflow. Common examples include: - Adding a custom dock, navigation bar, canvas menu, or inspector bar button. - Opening a custom panel or sheet for a multi-step workflow. - Connecting an action to your backend, content management system, or AI service. - Packaging reusable configuration as a plugin-style `EditorConfigurationBuilder`. Extensions still run inside the editor UI. They can access the editor scope, send editor events, and call engine APIs when the user triggers them. ## Building Your Own UI You can also skip the default editor UI and build a custom Android interface around CreativeEngine. This is useful when your app needs a highly specialized workflow, a unique layout, or a focused editing surface that does not match the default editor structure. In that setup, your app owns the UI and uses the engine for scene loading, block editing, selection state, rendering, playback, and export. You are also responsible for lifecycle, app navigation, and any accessibility or localization behavior in your custom controls. ## Integrating with Custom Workflows The editor UI can work with application state and business logic instead of acting as a closed editing surface. Use callbacks and events to: - Prepare scenes, asset sources, and settings when the editor loads. - Route export, upload, and close actions through your own product flow. - Update external state when selections or editor modes change. - Open custom panels, dialogs, or overlays for app-specific tasks. This makes the default Android UI suitable for embedded editors, template personalization flows, product customizers, and automation-assisted creative tools. ## Next Steps - [Customization](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/) — Control which features are available and how UI components behave, appear, or are arranged in the editor. - [Appearance](https://img.ly/docs/cesdk/android/user-interface/appearance-b155eb/) — Customize the visual style of the editor UI, including themes, fonts, labels, and icons. - [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) — Extend the editor interface with custom components, panels, actions, and dialogs tailored to your workflow. - [UI Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) — Connect editor callbacks and UI events to your app logic. --- ## 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: "UI Extensions" description: "Extend the editor interface with custom components, panels, actions, and dialogs tailored to your workflow." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) --- --- ## Related Pages - [Add a New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) - Add a custom button to the UI to trigger specific actions or workflows. - [Quick Actions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/quick-actions-1e478d/) - Add one-tap editing actions to the canvas menu and inspector bar that edit the selected block instantly. - [Create a Custom Panel](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/create-custom-panel-d87b83/) - Build a custom panel that edits the selected block with native Compose controls, opened from an inspector bar button. - [Asset Library](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/asset-library-f2c082/) - Customize the asset library panel to show user uploads, remote assets, or categories. - [Customize UI Behavior](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/customize-behaviour-c09cb2/) - Define how Android editor UI components behave when interacting with your custom logic. --- ## 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 New Button" description: "Add a custom button to the UI to trigger specific actions or workflows." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) > [Add New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) --- ```kotlin file=@cesdk_android_examples/editor-guides-ui-extensions-add-button/AddButtonEditorSolution.kt reference-only import android.net.Uri import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import ly.img.editor.Editor import ly.img.editor.configuration.design.DesignConfigurationBuilder import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.NavigationBar import ly.img.editor.core.component.duplicate import ly.img.editor.core.component.layer import ly.img.editor.core.component.modify 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.configuration.then import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.Plus import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.ShapeType import ly.img.engine.Color as EngineColor private const val DEMO_BLOCK_NAME = "com.example.guides.addButton.selection" private const val LIFECYCLE_DUPLICATE_SCOPE = "lifecycle/duplicate" @Composable fun AddButtonEditorSolution( license: String, baseUri: Uri, onClose: (Throwable?) -> Unit, ) { Editor( license = license, baseUri = baseUri, configuration = { EditorConfiguration.remember(::DesignConfigurationBuilder).then { onLoaded = onLoaded@{ val engine = editorContext.engine val scene = engine.scene.get() ?: engine.scene.create() val page = engine.scene.getCurrentPage() ?: engine.scene.getPages().firstOrNull() ?: engine.block .create(DesignBlockType.Page) .also { engine.block.setWidth(block = it, value = 1080F) engine.block.setHeight(block = it, value = 1080F) engine.block.appendChild(parent = scene, child = it) } val block = engine.block.findByName(DEMO_BLOCK_NAME).firstOrNull() ?: engine.block .create(DesignBlockType.Graphic) .also { engine.block.setName(block = it, name = DEMO_BLOCK_NAME) engine.block.setShape( block = it, shape = engine.block.createShape(ShapeType.Rect), ) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block = it, fill = fill) engine.block.setFillSolidColor( block = it, color = EngineColor.fromRGBA(r = 0.32F, g = 0.58F, b = 0.94F, a = 1F), ) engine.block.setWidth(block = it, value = 320F) engine.block.setHeight(block = it, value = 220F) engine.block.setPositionX(block = it, value = 380F) engine.block.setPositionY(block = it, value = 360F) engine.block.appendChild(parent = page, child = it) } engine.block.bringToFront(block = block) engine.block.setSelected(block = block, selected = true) engine.scene.zoomToBlock(page) parentConfiguration?.onLoaded?.invoke(this) } dock = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("com.example.guides.addButton.dock.create") } vectorIcon = { IconPack.Plus } textString = { "Create" } contentDescription = { "Create item" } onClick = { Toast .makeText(editorContext.activity, "Create item", Toast.LENGTH_SHORT) .show() } } } } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } canvasMenu = canvasMenuComponent@{ val sourceCanvasMenu = parentConfiguration?.canvasMenu as? CanvasMenu ?: return@canvasMenuComponent null val updatedListBuilder = sourceCanvasMenu.listBuilder.modify { addAfter(id = CanvasMenu.Button.Id.duplicate, failIfNotFound = false) { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.guides.addButton.canvasMenu.note") } vectorIcon = { IconPack.Plus } contentDescription = { "Add note" } visible = { editorContext.safeSelection != null } enabled = { editorContext.safeSelection?.designBlock?.let { editorContext.engine.block.isAllowedByScope(it, LIFECYCLE_DUPLICATE_SCOPE) } == true } onClick = { Toast .makeText(editorContext.activity, "Add note", Toast.LENGTH_SHORT) .show() } } } } remember(sourceCanvasMenu, updatedListBuilder) { sourceCanvasMenu.copy(listBuilder = updatedListBuilder) } } inspectorBar = inspectorBarComponent@{ val sourceInspectorBar = parentConfiguration?.inspectorBar as? InspectorBar ?: return@inspectorBarComponent null val updatedListBuilder = sourceInspectorBar.listBuilder.modify { addBefore(id = InspectorBar.Button.Id.layer, failIfNotFound = false) { InspectorBar.Button.remember { id = { EditorComponentId("com.example.guides.addButton.inspector.review") } vectorIcon = { IconPack.Plus } textString = { "Review" } contentDescription = { "Review selection" } visible = { editorContext.safeSelection != null } enabled = { editorContext.safeSelection?.designBlock?.let { editorContext.engine.block.isAllowedByScope(it, LIFECYCLE_DUPLICATE_SCOPE) } == true } onClick = { Toast .makeText(editorContext.activity, "Review selection", Toast.LENGTH_SHORT) .show() } } } } remember(sourceInspectorBar, updatedListBuilder) { sourceInspectorBar.copy(listBuilder = updatedListBuilder) } } navigationBar = navigationBarComponent@{ val sourceNavigationBar = parentConfiguration?.navigationBar as? NavigationBar ?: return@navigationBarComponent null val updatedListBuilder = sourceNavigationBar.listBuilder.modify { addLast(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("com.example.guides.addButton.navigation.create") } vectorIcon = { IconPack.Plus } contentDescription = { "Create draft" } onClick = { Toast .makeText(editorContext.activity, "Create draft", Toast.LENGTH_SHORT) .show() } } } } remember(sourceNavigationBar, updatedListBuilder) { sourceNavigationBar.copy(listBuilder = updatedListBuilder) } } } }, onClose = onClose, ) } ``` Add app-specific buttons to existing editor UI areas so users can trigger your own actions from the CE.SDK editor. ![Android editor with custom buttons in several UI areas](./assets/android.hero.jpg) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-ui-extensions-add-button) ## Overview Android editor UI extensions use Compose components and list builders. Start with the component from `parentConfiguration`, call `listBuilder.modify`, then insert your custom button with a stable `EditorComponentId`. | Component | Selection Context | Best For | | --- | --- | --- | | Dock | None | Global actions and asset entry points | | Canvas Menu | `editorContext.safeSelection` | Selection-specific actions near the selected block | | Inspector Bar | `editorContext.safeSelection` | Property or selection-specific actions | | Navigation Bar | None | App-level actions | For a complete product surface, start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). The snippets below use the same `EditorConfiguration` component system, so you can apply the pattern to your own editor setup. ## Adding to Dock Add a dock button for global actions that should always be available at the bottom of the editor. Use reverse domain notation for custom IDs so your button does not collide with built-in component IDs. ```kotlin highlight-android-dock dock = dockComponent@{ val sourceDock = parentConfiguration?.dock as? Dock ?: return@dockComponent null val updatedListBuilder = sourceDock.listBuilder.modify { addFirst { Dock.Button.remember { id = { EditorComponentId("com.example.guides.addButton.dock.create") } vectorIcon = { IconPack.Plus } textString = { "Create" } contentDescription = { "Create item" } onClick = { Toast .makeText(editorContext.activity, "Create item", Toast.LENGTH_SHORT) .show() } } } } remember(sourceDock, updatedListBuilder) { sourceDock.copy(listBuilder = updatedListBuilder) } } ``` - `id` gives the custom button a stable identity for later reordering or replacement. - `vectorIcon` and `textString` define the button content. - `onClick` runs the app-specific action when the user taps the button. ## Adding to Canvas Menu Add canvas menu buttons for actions that belong next to the selected block. Use selection-aware `visible` and `enabled` predicates when the action only applies to certain selected elements. ```kotlin highlight-android-canvas-menu canvasMenu = canvasMenuComponent@{ val sourceCanvasMenu = parentConfiguration?.canvasMenu as? CanvasMenu ?: return@canvasMenuComponent null val updatedListBuilder = sourceCanvasMenu.listBuilder.modify { addAfter(id = CanvasMenu.Button.Id.duplicate, failIfNotFound = false) { CanvasMenu.Button.remember { id = { EditorComponentId("com.example.guides.addButton.canvasMenu.note") } vectorIcon = { IconPack.Plus } contentDescription = { "Add note" } visible = { editorContext.safeSelection != null } enabled = { editorContext.safeSelection?.designBlock?.let { editorContext.engine.block.isAllowedByScope(it, LIFECYCLE_DUPLICATE_SCOPE) } == true } onClick = { Toast .makeText(editorContext.activity, "Add note", Toast.LENGTH_SHORT) .show() } } } } remember(sourceCanvasMenu, updatedListBuilder) { sourceCanvasMenu.copy(listBuilder = updatedListBuilder) } } ``` The example inserts the button after the built-in duplicate action when that item exists. `failIfNotFound = false` keeps the customization compatible with editor configurations that do not include that built-in button. ## Adding to Inspector Bar Add inspector bar buttons for actions that change or inspect the selected element. The inspector bar shares the same button builder properties, but its scope follows the current selection at the bottom of the editor. ```kotlin highlight-android-inspector-bar inspectorBar = inspectorBarComponent@{ val sourceInspectorBar = parentConfiguration?.inspectorBar as? InspectorBar ?: return@inspectorBarComponent null val updatedListBuilder = sourceInspectorBar.listBuilder.modify { addBefore(id = InspectorBar.Button.Id.layer, failIfNotFound = false) { InspectorBar.Button.remember { id = { EditorComponentId("com.example.guides.addButton.inspector.review") } vectorIcon = { IconPack.Plus } textString = { "Review" } contentDescription = { "Review selection" } visible = { editorContext.safeSelection != null } enabled = { editorContext.safeSelection?.designBlock?.let { editorContext.engine.block.isAllowedByScope(it, LIFECYCLE_DUPLICATE_SCOPE) } == true } onClick = { Toast .makeText(editorContext.activity, "Review selection", Toast.LENGTH_SHORT) .show() } } } } remember(sourceInspectorBar, updatedListBuilder) { sourceInspectorBar.copy(listBuilder = updatedListBuilder) } } ``` - `visible` hides the button completely when the predicate returns `false`. - `enabled` keeps the button visible but disables interaction when the predicate returns `false`. ## Adding to Navigation Bar Add navigation bar buttons for app-level actions at the top of the editor. When the existing navigation bar uses aligned groups, pass the target alignment to `addFirst` or `addLast`. ```kotlin highlight-android-navigation-bar navigationBar = navigationBarComponent@{ val sourceNavigationBar = parentConfiguration?.navigationBar as? NavigationBar ?: return@navigationBarComponent null val updatedListBuilder = sourceNavigationBar.listBuilder.modify { addLast(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("com.example.guides.addButton.navigation.create") } vectorIcon = { IconPack.Plus } contentDescription = { "Create draft" } onClick = { Toast .makeText(editorContext.activity, "Create draft", Toast.LENGTH_SHORT) .show() } } } } remember(sourceNavigationBar, updatedListBuilder) { sourceNavigationBar.copy(listBuilder = updatedListBuilder) } } ``` | Alignment | Position | | --- | --- | | `Alignment.Start` | Leading side | | `Alignment.CenterHorizontally` | Center | | `Alignment.End` | Trailing side | ## Button Parameters All four button types use the same base builder properties. Assign them as scoped lambdas inside the button builder block. | Parameter | Builder Assignment | Required | Default | Description | | --- | --- | --- | --- | --- | | `id` | `id = { EditorComponentId(...) }` | Recommended | Random ID | Stable component identity for list operations | | `vectorIcon` | `vectorIcon = { IconPack.Plus }` | No | `null` | Icon rendered with the component tint | | `textString` | `textString = { "Create" }` | No | `null` | Text label rendered by the button | | `contentDescription` | `contentDescription = { "Create item" }` | Recommended | `null` | Accessibility label for icon-only buttons | | `onClick` | `onClick = { ... }` | No | No-op | Action invoked when the button is tapped | | `visible` | `visible = { editorContext.safeSelection != null }` | No | `true` | Hides the button when `false` | | `enabled` | `enabled = { ... }` | No | `true` | Disables the button when `false` | ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builderFactory=_)` | Creates the base editor configuration. | | `EditorConfiguration.then(builder=_)` | Extends an existing editor configuration. | | `Dock.remember(builder=_)` | Creates the dock component. | | `Dock.ListBuilder.remember(builder=_)` | Builds dock items. | | `Dock.Button.remember(builder=_)` | Creates a custom dock button. | | `CanvasMenu.remember(builder=_)` | Creates the canvas menu component. | | `CanvasMenu.ListBuilder.remember(builder=_)` | Builds canvas menu items. | | `CanvasMenu.Button.remember(builder=_)` | Creates a custom canvas menu button. | | `InspectorBar.remember(builder=_)` | Creates the inspector bar component. | | `InspectorBar.ListBuilder.remember(builder=_)` | Builds inspector bar items. | | `InspectorBar.Button.remember(builder=_)` | Creates a custom inspector bar button. | | `NavigationBar.remember(builder=_)` | Creates the navigation bar component. | | `NavigationBar.ListBuilder.remember(builder=_)` | Builds navigation bar items. | | `NavigationBar.Button.remember(builder=_)` | Creates a custom navigation bar button. | | `listBuilder.modify(builder=_)` | Applies list-builder changes without replacing the whole component. | | `addFirst(block=_)` | Inserts an item at the beginning of an unaligned list. | | `addAfter(id=_, failIfNotFound=_, block=_)` | Inserts an item after a matching component ID. | | `addBefore(id=_, failIfNotFound=_, block=_)` | Inserts an item before a matching component ID. | | `addLast(alignment=_, block=_)` | Inserts an item at the end of an aligned group. | | `EditorComponentId(id=_)` | Assigns a stable unique ID to a custom editor component. | | `engine.block.isAllowedByScope(block=_, key=_)` | Checks whether the selected block allows the scoped action. | | `ButtonBuilder.id` | Sets the button component ID. | | `ButtonBuilder.vectorIcon` | Supplies the `ImageVector` rendered by the button. | | `ButtonBuilder.textString` | Supplies the visible text label. | | `ButtonBuilder.contentDescription` | Supplies the accessibility label. | | `ButtonBuilder.onClick` | Handles the button tap. | | `ButtonBuilder.visible` | Controls whether the button is rendered. | | `ButtonBuilder.enabled` | Controls whether the rendered button accepts interaction. | ## Next Steps - [Rearrange Buttons](https://img.ly/docs/cesdk/android/user-interface/customization/rearrange-buttons-97022a/) - Customize button order in UI components - [Customize Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) - Full dock customization patterns - [Customize Navigation Bar](https://img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39/) - Navigation bar configuration - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) - Contextual menu customization --- ## 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: "Asset Library" description: "Customize the asset library panel to show user uploads, remote assets, or categories." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ui-extensions/asset-library-f2c082/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) > [Asset Library](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/asset-library-f2c082/) --- ```kotlin file=@cesdk_android_examples/editor-guides-user-interface-ui-extensions-asset-library/UserInterfaceAssetLibraryEditorSolution.kt reference-only import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import ly.img.editor.Editor import ly.img.editor.core.R import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.library.AssetLibrary import ly.img.editor.core.library.AssetType import ly.img.editor.core.library.LibraryCategory import ly.img.editor.core.library.LibraryContent import ly.img.editor.core.library.data.AssetSourceType import ly.img.editor.core.library.data.UploadAssetSourceType import ly.img.engine.Asset import ly.img.engine.AssetContext import ly.img.engine.AssetCredits import ly.img.engine.AssetDefinition import ly.img.engine.AssetFilter import ly.img.engine.AssetLicense import ly.img.engine.AssetSource import ly.img.engine.DesignBlock 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 private const val BRAND_IMAGE_SOURCE_ID = "ly.img.example.brand.images" private const val BRAND_UPLOAD_SOURCE_ID = "ly.img.example.brand.uploads" private const val CAMPAIGN_GROUP = "campaign" private const val PRODUCT_GROUP = "product" private val brandImageSourceType = AssetSourceType(sourceId = BRAND_IMAGE_SOURCE_ID) private val brandUploadSourceType = UploadAssetSourceType( sourceId = BRAND_UPLOAD_SOURCE_ID, mimeTypeFilter = "image/*", ) @Composable fun UserInterfaceAssetLibraryEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { if (editorContext.engine.scene.get() == null) { 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) } val engine = editorContext.engine if (BRAND_IMAGE_SOURCE_ID !in engine.asset.findAllSources()) { engine.asset.addSource( BrandImageAssetSource( engine = engine, assetBaseUri = editorContext.baseUri.buildUpon() .appendPath("ly.img.image") .build(), ), ) } if (BRAND_UPLOAD_SOURCE_ID !in engine.asset.findAllSources()) { engine.asset.addLocalSource( sourceId = BRAND_UPLOAD_SOURCE_ID, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key), ) } } onUpload = { assetDefinition, uploadSource -> uploadTransientResource( assetDefinition = assetDefinition, uploadSource = uploadSource, ) } assetLibrary = { remember { val brandImages = LibraryCategory.Images.copy( content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_images, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(brandImageSourceType), groups = listOf(CAMPAIGN_GROUP), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = brandImageSourceType, groups = listOf(CAMPAIGN_GROUP), assetType = AssetType.Image, ), ), LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_image_uploads, sourceTypes = listOf(brandUploadSourceType), assetType = AssetType.Image, ), ), ), ) AssetLibrary.getDefault( tabs = listOf(AssetLibrary.Tab.IMAGES), images = brandImages, ) } } } }, onClose = onClose, ) } private class BrandImageAssetSource( private val engine: Engine, private val assetBaseUri: Uri, ) : AssetSource(sourceId = BRAND_IMAGE_SOURCE_ID) { override val credits = AssetCredits( name = "Brand Media Team", uri = Uri.parse("https://img.ly/"), ) override val license = AssetLicense( name = "Internal use", uri = null, ) override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) override suspend fun getGroups(): List = assets.flatMap { it.groups }.distinct() override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult { val locale = query.locale ?: "en" val filteredAssets = assets .asSequence() .filter { it.matches(query) } .map { it.toAsset(sourceId = sourceId, locale = locale) } .filter { asset -> query.filter.orEmpty().all { filter -> asset.matches(filter) } } .toList() val page = query.page.coerceAtLeast(0) val perPage = query.perPage.coerceAtLeast(1) val start = page * perPage val pageAssets = filteredAssets .drop(start) .take(perPage) return FindAssetsResult( assets = pageAssets, currentPage = page, nextPage = if (start + perPage < filteredAssets.size) page + 1 else -1, total = filteredAssets.size, ) } override suspend fun fetchAsset( id: String, options: FetchAssetOptions, ): Asset? = assets .firstOrNull { it.id == id } ?.toAsset(sourceId = sourceId, locale = options.locale ?: "en") override suspend fun applyAsset(asset: Asset): DesignBlock? { val block = engine.asset.defaultApplyAsset(asset) ?: return null engine.block.setMetadata(block = block, key = "asset/source", value = sourceId) engine.block.setMetadata(block = block, key = "asset/externalId", value = asset.id) return block } private fun BrandImage.matches(query: FindAssetsQuery): Boolean { val groupMatches = query.groups?.all(groups::contains) ?: true val excludedGroupMatches = query.excludeGroups?.none(groups::contains) ?: true val tagMatches = query.tags?.all { requestedTag -> tags.any { it.equals(requestedTag, ignoreCase = true) } } ?: true val searchTerms = query.query ?.trim() ?.lowercase() ?.split(Regex("\\s+")) ?.filter { it.isNotEmpty() } .orEmpty() val queryMatches = searchTerms.all { term -> label.contains(term, ignoreCase = true) || tags.any { it.contains(term, ignoreCase = true) } } return groupMatches && excludedGroupMatches && tagMatches && queryMatches } private fun Asset.matches(filter: AssetFilter): Boolean = when (filter) { is AssetFilter.Equals -> filterValues(filter.property).any { it.lowercaseAscii() == filter.value.lowercaseAscii() } is AssetFilter.Contains -> filterValues(filter.property).any { filter.value.lowercaseAscii() in it.lowercaseAscii() } is AssetFilter.And -> { require(filter.filters.isNotEmpty()) { "AssetFilter.And must not be empty." } filter.filters.all { child -> matches(child) } } is AssetFilter.Or -> { require(filter.filters.isNotEmpty()) { "AssetFilter.Or must not be empty." } filter.filters.any { child -> matches(child) } } is AssetFilter.Not -> !matches(filter.filter) } private fun Asset.filterValues(property: String): List = when (property) { "label" -> listOfNotNull(label) "id" -> listOf(id) "tags" -> tags.orEmpty() "groups" -> groups.orEmpty() else -> { require(property.startsWith("meta.") && property.length > "meta.".length) { "Unsupported asset filter property: $property" } listOfNotNull(meta?.get(property.removePrefix("meta."))) } } private fun String.lowercaseAscii(): String = buildString(length) { for (character in this@lowercaseAscii) { append( if (character in 'A'..'Z') { (character.code + 32).toChar() } else { character }, ) } } private fun BrandImage.toAsset( sourceId: String, locale: String, ) = Asset( id = id, context = AssetContext(sourceId = sourceId), label = label, locale = locale, tags = tags, groups = groups, meta = mapOf( "uri" to assetUri("images", fileName), "thumbUri" to assetUri("thumbnails", fileName), "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 width.toString(), "height" to height.toString(), ), credits = credits, license = license, ) private data class BrandImage( val id: String, val label: String, val tags: List, val groups: List, val fileName: String, val width: Int, val height: Int, ) private fun assetUri(vararg pathSegments: String): String { val builder = assetBaseUri.buildUpon() pathSegments.forEach { builder.appendPath(it) } return builder.build().toString() } private companion object { private val assets = listOf( BrandImage( id = "brand-campaign-hero", label = "Campaign Hero", tags = listOf("campaign", "beach", "blue"), groups = listOf(CAMPAIGN_GROUP), fileName = "sample_1.jpg", width = 2500, height = 1667, ), BrandImage( id = "brand-product-backdrop", label = "Product Backdrop", tags = listOf("product", "mountain", "green"), groups = listOf(PRODUCT_GROUP), fileName = "sample_10.jpg", width = 2500, height = 1667, ), BrandImage( id = "brand-campaign-texture", label = "Campaign Texture", tags = listOf("campaign", "nature", "warm"), groups = listOf(CAMPAIGN_GROUP), fileName = "sample_12.jpg", width = 2500, height = 1667, ), ) } } 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.parse("https://assets.example.com") .buildUpon() .appendPath(sourceId) .appendQueryParameter("sourceUri", uri) .build() .toString() } ``` Customize the Android asset library by registering custom asset sources, exposing upload sources, and choosing which sections appear in the editor UI. ![Customized Android asset library showing image and upload sections.](./assets/asset-library-android.png) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-user-interface-ui-extensions-asset-library) The Android editor renders the asset library from an `AssetLibrary` configuration. Each visible section points to one or more `AssetSourceType` values, and those source types must match sources that you register with the CreativeEngine. For a complete editor surface that includes the asset library, see the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/). This guide focuses on the asset library configuration that you can add to your own editor setup. ## Creating Custom Asset Sources Start by defining stable source IDs for the assets you load from your backend and for assets users upload during an editing session. The same IDs connect the engine sources to the UI sections. ```kotlin highlight-android-source-types private const val BRAND_IMAGE_SOURCE_ID = "ly.img.example.brand.images" private const val BRAND_UPLOAD_SOURCE_ID = "ly.img.example.brand.uploads" private const val CAMPAIGN_GROUP = "campaign" private const val PRODUCT_GROUP = "product" private val brandImageSourceType = AssetSourceType(sourceId = BRAND_IMAGE_SOURCE_ID) private val brandUploadSourceType = UploadAssetSourceType( sourceId = BRAND_UPLOAD_SOURCE_ID, mimeTypeFilter = "image/*", ) ``` Register the custom source during editor creation after your `onCreate` callback creates or loads a scene. The example also creates a local upload source so the upload section can store user-selected images during the session. ```kotlin highlight-android-register-sources val engine = editorContext.engine if (BRAND_IMAGE_SOURCE_ID !in engine.asset.findAllSources()) { engine.asset.addSource( BrandImageAssetSource( engine = engine, assetBaseUri = editorContext.baseUri.buildUpon() .appendPath("ly.img.image") .build(), ), ) } if (BRAND_UPLOAD_SOURCE_ID !in engine.asset.findAllSources()) { engine.asset.addLocalSource( sourceId = BRAND_UPLOAD_SOURCE_ID, supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key), ) } ``` ## Adding Asset Library Panels Android uses `LibraryCategory` and `LibraryContent` to describe the tabs, sections, and grids shown in the asset library. Here we replace the Images tab with a custom category that contains a curated image section and an upload section. ```kotlin highlight-android-library-category assetLibrary = { remember { val brandImages = LibraryCategory.Images.copy( content = LibraryContent.Sections( titleRes = R.string.ly_img_editor_asset_library_title_images, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceTypes = listOf(brandImageSourceType), groups = listOf(CAMPAIGN_GROUP), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = brandImageSourceType, groups = listOf(CAMPAIGN_GROUP), assetType = AssetType.Image, ), ), LibraryContent.Section( titleRes = R.string.ly_img_editor_asset_library_section_image_uploads, sourceTypes = listOf(brandUploadSourceType), assetType = AssetType.Image, ), ), ), ) AssetLibrary.getDefault( tabs = listOf(AssetLibrary.Tab.IMAGES), images = brandImages, ) } } ``` Use this pattern when you want to reorder tabs, remove default categories, or point a category at custom sources. For more specialized layouts, construct an `AssetLibrary` directly and provide different categories for tabs, replacement sheets, or media-specific entry points. ## Implementing the Source Implement `AssetSource` when assets come from a remote API, a Digital Asset Management system, or another app-managed catalog. Start with source metadata so the editor knows which MIME types the source supports and which credits or license should be attached to its results. ```kotlin highlight-android-source-metadata private class BrandImageAssetSource( private val engine: Engine, private val assetBaseUri: Uri, ) : AssetSource(sourceId = BRAND_IMAGE_SOURCE_ID) { override val credits = AssetCredits( name = "Brand Media Team", uri = Uri.parse("https://img.ly/"), ) override val license = AssetLicense( name = "Internal use", uri = null, ) override val supportedMimeTypes = listOf(MimeType.JPEG.key, MimeType.PNG.key) ``` `findAssets` maps the engine query to your data model, handles group and search filtering, and returns paginated `Asset` results. Replace the in-memory list with your network or storage client while keeping the returned result shape the same. ```kotlin highlight-android-find-assets override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult { val locale = query.locale ?: "en" val filteredAssets = assets .asSequence() .filter { it.matches(query) } .map { it.toAsset(sourceId = sourceId, locale = locale) } .filter { asset -> query.filter.orEmpty().all { filter -> asset.matches(filter) } } .toList() val page = query.page.coerceAtLeast(0) val perPage = query.perPage.coerceAtLeast(1) val start = page * perPage val pageAssets = filteredAssets .drop(start) .take(perPage) return FindAssetsResult( assets = pageAssets, currentPage = page, nextPage = if (start + perPage < filteredAssets.size) page + 1 else -1, total = filteredAssets.size, ) } ``` `FindAssetsQuery.filter` contains structured `AssetFilter` predicates in addition to search text, tags, and groups. Evaluate these predicates against each converted `Asset`; this example supports equality and containment checks for labels, IDs, tags, groups, and `meta.` values, along with nested `And`, `Or`, and `Not` filters. ```kotlin highlight-android-asset-filter private fun Asset.matches(filter: AssetFilter): Boolean = when (filter) { is AssetFilter.Equals -> filterValues(filter.property).any { it.lowercaseAscii() == filter.value.lowercaseAscii() } is AssetFilter.Contains -> filterValues(filter.property).any { filter.value.lowercaseAscii() in it.lowercaseAscii() } is AssetFilter.And -> { require(filter.filters.isNotEmpty()) { "AssetFilter.And must not be empty." } filter.filters.all { child -> matches(child) } } is AssetFilter.Or -> { require(filter.filters.isNotEmpty()) { "AssetFilter.Or must not be empty." } filter.filters.any { child -> matches(child) } } is AssetFilter.Not -> !matches(filter.filter) } private fun Asset.filterValues(property: String): List = when (property) { "label" -> listOfNotNull(label) "id" -> listOf(id) "tags" -> tags.orEmpty() "groups" -> groups.orEmpty() else -> { require(property.startsWith("meta.") && property.length > "meta.".length) { "Unsupported asset filter property: $property" } listOfNotNull(meta?.get(property.removePrefix("meta."))) } } private fun String.lowercaseAscii(): String = buildString(length) { for (character in this@lowercaseAscii) { append( if (character in 'A'..'Z') { (character.code + 32).toChar() } else { character }, ) } } ``` Map each item to an `Asset` with the metadata the default image apply behavior expects. For image assets, include the asset URI, thumbnail URI, MIME type, block type, fill type, shape type, and dimensions when you have them. ```kotlin highlight-android-asset-metadata private fun BrandImage.toAsset( sourceId: String, locale: String, ) = Asset( id = id, context = AssetContext(sourceId = sourceId), label = label, locale = locale, tags = tags, groups = groups, meta = mapOf( "uri" to assetUri("images", fileName), "thumbUri" to assetUri("thumbnails", fileName), "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 width.toString(), "height" to height.toString(), ), credits = credits, license = license, ) ``` The same source can customize how an asset is applied. In the example, `applyAsset` delegates to `engine.asset.defaultApplyAsset()` and then writes metadata to the created block. ```kotlin highlight-android-apply-asset override suspend fun applyAsset(asset: Asset): DesignBlock? { val block = engine.asset.defaultApplyAsset(asset) ?: return null engine.block.setMetadata(block = block, key = "asset/source", value = sourceId) engine.block.setMetadata(block = block, key = "asset/externalId", value = asset.id) return block } ``` ## Enabling User Uploads For uploads, create a local source with `engine.asset.addLocalSource()` and use an `UploadAssetSourceType` with the same source ID in your library section. The editor's upload flow adds selected files to that local source automatically. If your app needs durable uploads, implement `EditorConfiguration.onUpload` and replace the temporary URI in the provided `AssetDefinition` with the permanent URI returned by your storage service before returning the definition. ```kotlin highlight-android-upload-callback onUpload = { assetDefinition, uploadSource -> uploadTransientResource( assetDefinition = assetDefinition, uploadSource = uploadSource, ) } ``` The helper below keeps the uploaded asset's metadata intact and replaces only the local URI fields. Replace the placeholder storage call with your app's upload client. ```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.parse("https://assets.example.com") .buildUpon() .appendPath(sourceId) .appendQueryParameter("sourceUri", uri) .build() .toString() } ``` ## Querying and Organizing Assets `FindAssetsQuery` carries the current search text, locale, tags, groups, and pagination request. Use these values in `findAssets` so the UI can search and page through large catalogs without loading every asset at once. Groups are useful for categories such as campaigns, products, or seasons. Return the complete group list from `getGroups()`, then pass `groups` on a `LibraryContent.Section` or `LibraryContent.Grid` to show a filtered slice of the source. ## Troubleshooting ### Assets Do Not Appear Check that the source ID in `AssetSourceType(sourceId = ...)` exactly matches the ID registered with `engine.asset.addSource()` or `engine.asset.addLocalSource()`. Also confirm that `findAssets` returns a non-empty `assets` list and a matching `total`. ### Uploads Are Hidden Upload buttons only appear for `UploadAssetSourceType` sections. Register a local source with the same ID and keep the section's `sourceTypes` list to that single upload source. ### Applying an Asset Fails For image assets, include `uri`, `thumbUri`, `mimeType`, `blockType`, `fillType`, `shapeType`, `width`, and `height` metadata where possible. If you override `applyAsset`, call `engine.asset.defaultApplyAsset()` unless you intentionally create or update blocks yourself. ## API Reference | API | Purpose | | --- | --- | | `EditorConfiguration.remember(builder=_)` | Configure editor callbacks and the asset library UI. | | `engine.asset.findAllSources()` | Check whether a source ID is already registered. | | `engine.asset.addSource(source=_)` | Register a custom `AssetSource`. | | `engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_)` | Register an engine-managed source for user uploads or runtime assets. | | `EditorConfiguration.onUpload` | Intercept uploaded asset definitions before the editor stores them in an upload source. | | `AssetLibrary.getDefault(tabs=_, images=_)` | Start from the default library and override selected tabs or categories. | | `LibraryContent.Sections(titleRes=_, sections=_)` | Render multiple asset sections inside a category. | | `LibraryContent.Section(titleRes=_, sourceTypes=_, groups=_, assetType=_, expandContent=_)` | Configure a preview section for one or more asset sources. | | `LibraryContent.Grid(titleRes=_, sourceType=_, groups=_, assetType=_)` | Render one asset source as a paginated grid. | | `AssetSourceType(sourceId=_)` | Connect a UI section to a registered source ID. | | `UploadAssetSourceType(sourceId=_, mimeTypeFilter=_)` | Connect a UI section to a source that should show upload controls. | | `AssetSource.findAssets(query=_)` | Return filtered, paginated assets for a source. | | `FindAssetsQuery.filter` | Provide structured `AssetFilter` predicates for an asset query. | | `AssetSource.fetchAsset(id=_, options=_)` | Resolve one asset by ID when the engine requests it directly. | | `AssetSource.getGroups()` | Return the groups available for category filtering. | | `engine.asset.defaultApplyAsset(asset=_)` | Apply an asset using the built-in block creation behavior. | | `engine.block.setMetadata(block=_, key=_, value=_)` | Store source metadata on the block created from an asset. | ## Next Steps - [Create a Custom Panel](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/create-custom-panel-d87b83/) — Design a custom sidebar panel to support unique workflows and user needs. - [Import From Remote Source](https://img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf/) - Fetch assets remotely - [Import Media Overview](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/) - Asset management fundamentals - [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) - Extend editor 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: "Create a Custom Panel" description: "Build a custom panel that edits the selected block with native Compose controls, opened from an inspector bar button." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ui-extensions/create-custom-panel-d87b83/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) > [Create Custom Panel](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/create-custom-panel-d87b83/) --- ```kotlin file=@cesdk_android_examples/editor-guides-ui-extensions-create-custom-panel/CreateCustomPanelSolution.kt reference-only import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Slider import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.unit.dp import ly.img.editor.Editor import ly.img.editor.core.EditorContext import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.data.Height 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.iconpack.IconPack import ly.img.editor.core.iconpack.Preview import ly.img.editor.core.sheet.SheetStyle import ly.img.editor.core.sheet.SheetType import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.ShapeType import ly.img.engine.Color as EngineColor private const val CREATE_CUSTOM_PANEL_DEMO_BLOCK_NAME = "com.example.guides.createCustomPanel.selection" @Composable fun CreateCustomPanelSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { onLoaded = { val engine = editorContext.engine val scene = engine.scene.get() ?: engine.scene.create() val page = engine.scene.getCurrentPage() ?: engine.scene.getPages().firstOrNull() ?: engine.block .create(DesignBlockType.Page) .also { engine.block.setWidth(block = it, value = 1080F) engine.block.setHeight(block = it, value = 1080F) engine.block.appendChild(parent = scene, child = it) } val block = engine.block.findByName(CREATE_CUSTOM_PANEL_DEMO_BLOCK_NAME).firstOrNull() ?: engine.block .create(DesignBlockType.Graphic) .also { engine.block.setName(block = it, name = CREATE_CUSTOM_PANEL_DEMO_BLOCK_NAME) engine.block.setShape( block = it, shape = engine.block.createShape(ShapeType.Rect), ) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block = it, fill = fill) engine.block.setFillSolidColor( block = it, color = EngineColor.fromRGBA(r = 0.32F, g = 0.58F, b = 0.94F, a = 1F), ) engine.block.setWidth(block = it, value = 320F) engine.block.setHeight(block = it, value = 220F) engine.block.setPositionX(block = it, value = 380F) engine.block.setPositionY(block = it, value = 360F) engine.block.appendChild(parent = page, child = it) } engine.block.bringToFront(block = block) engine.block.setSelected(block = block, selected = true) engine.scene.zoomToBlock(page) parentConfiguration?.onLoaded?.invoke(this) } inspectorBar = { InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { openCreateCustomPanelButton } } } } } } }, onClose = onClose, ) } val openCreateCustomPanelButton @Composable get() = InspectorBar.Button.remember { id = { EditorComponentId("open_create_custom_panel") } text = { Text("Properties") } icon = { Icon(IconPack.Preview, contentDescription = "Open properties panel") } onClick = { val selectedBlock = editorContext.selection.designBlock editorContext.eventHandler.send( EditorEvent.Sheet.Open(createCustomPanelSheetType(selectedBlock)), ) } } fun createCustomPanelSheetType(block: DesignBlock): SheetType = SheetType.Custom( style = SheetStyle( isFloating = false, minHeight = Height.Exactly(0.dp), maxHeight = Height.Fraction(0.7F), isHalfExpandingEnabled = false, isHalfExpandedInitially = false, animateInitialValue = true, ), content = { CreateCustomPanelPropertyPanel(editorContext = editorContext, block = block) }, ) @Composable fun CreateCustomPanelPropertyPanel( editorContext: EditorContext, block: DesignBlock, ) { val engine = editorContext.engine var name by remember(block) { mutableStateOf(runCatching { engine.block.getName(block) }.getOrDefault("")) } var opacity by remember(block) { mutableStateOf(runCatching { engine.block.getOpacity(block) }.getOrDefault(1F)) } var isVisible by remember(block) { mutableStateOf(runCatching { engine.block.isVisible(block) }.getOrDefault(true)) } Column( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(16.dp), horizontalAlignment = Alignment.Start, ) { Text("Block properties") Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( modifier = Modifier.fillMaxWidth(), value = name, onValueChange = { name = it }, label = { Text("Name") }, singleLine = true, ) Spacer(modifier = Modifier.height(24.dp)) Text("Opacity") Slider( value = opacity, onValueChange = { opacity = it }, valueRange = 0F..1F, ) Spacer(modifier = Modifier.height(16.dp)) Row(verticalAlignment = Alignment.CenterVertically) { Switch( checked = isVisible, onCheckedChange = { isVisible = it }, ) Spacer(modifier = Modifier.width(12.dp)) Text("Visible") } Spacer(modifier = Modifier.height(24.dp)) Button( modifier = Modifier.align(Alignment.End), onClick = { engine.block.setName(block, name = name) engine.block.setOpacity(block = block, value = opacity) engine.block.setVisible(block = block, visible = isVisible) editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true)) }, ) { Text("Done") } } } ``` Build a functional custom panel — a property editor that opens from an inspector bar button, edits the selected block, and writes changes back to the scene. ![Custom panel on Android](./assets/android.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-ui-extensions-create-custom-panel) ## Overview Custom panels on Android are editor sheets whose content you build with native Jetpack Compose. You present your own composable and wire it to the engine directly — there is no panel registration step and no component catalog. This guide builds a property editor: a button in the inspector bar opens a sheet that edits the selected block's name, opacity, and visibility. The example builds on `EditorConfiguration.remember { ... }`, the standard Android editor configuration entry point. Substitute your own editor configuration as needed — the `inspectorBar` slot is exposed on every configuration. The [Configuration](https://img.ly/docs/cesdk/android/configuration-2c1c3d/) guide covers how editor configuration sets up the editor as a whole, and the [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) guide covers the sheet mechanism — sheet types, presentation styles, and the built-in sheets — that this guide builds on. ## Opening the Panel from the Inspector Bar The inspector bar is the surface the editor shows for the current selection, which makes it the natural home for a panel that edits the selected block. Add an inspector bar button whose action opens the sheet. The button has access to `editorContext`, including the `engine`, the `eventHandler`, and the current `selection`. Read the selected block from `editorContext.selection.designBlock`, then open a `SheetType.Custom` that renders the property panel. ```kotlin highlight-android-inspector-button InspectorBar.Button.remember { id = { EditorComponentId("open_create_custom_panel") } text = { Text("Properties") } icon = { Icon(IconPack.Preview, contentDescription = "Open properties panel") } onClick = { val selectedBlock = editorContext.selection.designBlock editorContext.eventHandler.send( EditorEvent.Sheet.Open(createCustomPanelSheetType(selectedBlock)), ) } } ``` - Replacing `inspectorBar.listBuilder` replaces the inspector bar's contents; include any other items your app needs in the same list builder. - A panel that is not tied to a selection — like the built-in sheets in the Panel guide — can fit a dock button instead. See [Add a New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) for both surfaces. ## Building the Panel Content Panel content is regular Compose UI. Because the `SheetType.Custom` content lambda runs in the editor scope, it can read `editorContext.engine` directly. `SheetStyle` controls whether the sheet floats and how tall it can become, while the `content` lambda supplies the app-provided panel UI. The [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) guide documents the other sheet options. ```kotlin highlight-android-open-custom-panel fun createCustomPanelSheetType(block: DesignBlock): SheetType = SheetType.Custom( style = SheetStyle( isFloating = false, minHeight = Height.Exactly(0.dp), maxHeight = Height.Fraction(0.7F), isHalfExpandingEnabled = false, isHalfExpandedInitially = false, animateInitialValue = true, ), content = { CreateCustomPanelPropertyPanel(editorContext = editorContext, block = block) }, ) ``` Use Compose state with `remember { mutableStateOf(...) }` to seed transient controls from the selected block. The sample reads the current name with `engine.block.getName()`, opacity with `engine.block.getOpacity()`, and visibility with `engine.block.isVisible()`, then renders native controls such as `OutlinedTextField`, `Slider`, and `Switch`. For state that should survive configuration changes, use `editorContext.mutableStateOf(key, initial)` instead. It stores the value for the lifetime of the editor, so give each state value a unique key. Use regular Compose state when the value only needs to live with the panel content. ```kotlin highlight-android-panel-content @Composable fun CreateCustomPanelPropertyPanel( editorContext: EditorContext, block: DesignBlock, ) { val engine = editorContext.engine var name by remember(block) { mutableStateOf(runCatching { engine.block.getName(block) }.getOrDefault("")) } var opacity by remember(block) { mutableStateOf(runCatching { engine.block.getOpacity(block) }.getOrDefault(1F)) } var isVisible by remember(block) { mutableStateOf(runCatching { engine.block.isVisible(block) }.getOrDefault(true)) } Column( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(16.dp), horizontalAlignment = Alignment.Start, ) { Text("Block properties") Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( modifier = Modifier.fillMaxWidth(), value = name, onValueChange = { name = it }, label = { Text("Name") }, singleLine = true, ) Spacer(modifier = Modifier.height(24.dp)) Text("Opacity") Slider( value = opacity, onValueChange = { opacity = it }, valueRange = 0F..1F, ) Spacer(modifier = Modifier.height(16.dp)) Row(verticalAlignment = Alignment.CenterVertically) { Switch( checked = isVisible, onCheckedChange = { isVisible = it }, ) Spacer(modifier = Modifier.width(12.dp)) Text("Visible") } ``` ## Applying Changes and Closing the Panel The Done button writes the edited values back to the scene with `engine.block.setName()`, `engine.block.setOpacity()`, and `engine.block.setVisible()`, then dismisses the sheet by sending `EditorEvent.Sheet.Close`. The user can also drag the sheet down to dismiss it. ```kotlin highlight-android-apply-close Button( modifier = Modifier.align(Alignment.End), onClick = { engine.block.setName(block, name = name) engine.block.setOpacity(block = block, value = opacity) engine.block.setVisible(block = block, visible = isVisible) editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true)) }, ) { Text("Done") } ``` ## API Reference | Method | Description | | --- | --- | | `EditorConfiguration.remember { inspectorBar = { ... } }` | Declare the inspector bar in the editor configuration | | `InspectorBar.Button.remember { ... }` | Create an inspector bar button whose `onClick` can open the panel | | `editorContext.selection.designBlock` | The selected block inside the button action | | `editorContext.engine` | Engine access inside the button action and sheet content | | `editorContext.mutableStateOf(key=_, initial=_)` | Store Compose state in the editor scope so it survives configuration changes | | `editorContext.eventHandler.send(EditorEvent.Sheet.Open(...))` | Present custom Compose content as a sheet | | `editorContext.eventHandler.send(EditorEvent.Sheet.Close(...))` | Dismiss the current sheet | | `SheetType.Custom(style=_, content=_)` | Define a custom editor sheet with app-provided Compose content | | `engine.block.getName(block=_)` | Read a block's name | | `engine.block.setName(block=_, name=_)` | Write a block's name | | `engine.block.getOpacity(block=_)` | Read a block's opacity | | `engine.block.setOpacity(block=_, value=_)` | Write a block's opacity | | `engine.block.isVisible(block=_)` | Read whether a block is visible | | `engine.block.setVisible(block=_, visible=_)` | Write a block's visibility | ## Next Steps - [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) — sheet types, presentation styles, and the built-in sheets this guide builds on. - [Add a New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) — full options for dock, canvas menu, inspector bar, and navigation bar buttons. - [Dock](https://img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c/) — configure the dock area and item order. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) — the built-in surface for editing properties of the selected block. --- ## 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: "Customize UI Behavior" description: "Define how Android editor UI components behave when interacting with your custom logic." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ui-extensions/customize-behaviour-c09cb2/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) > [Customize Behaviour](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/customize-behaviour-c09cb2/) --- ```kotlin file=@cesdk_android_examples/editor-guides-ui-extensions-customize-behaviour/CustomizeBehaviourEditorSolution.kt reference-only 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable 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.unit.dp import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import ly.img.editor.Editor import ly.img.editor.EditorUiMode import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.data.Height 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.iconpack.Export import ly.img.editor.core.iconpack.IconPack import ly.img.editor.core.iconpack.PlayBox import ly.img.editor.core.sheet.SheetStyle import ly.img.editor.core.sheet.SheetType data class CustomizeBehaviourState( val latestEvent: String = "No scene changes observed yet.", val dialogMessage: String? = null, ) object ValidateSelection : EditorEvent object OpenWorkflowSheet : EditorEvent @Composable fun CustomizeBehaviourEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { var uiMode by remember { mutableStateOf(EditorUiMode.LIGHT) } Editor( license = license, uiMode = uiMode, configuration = { EditorConfiguration.remember { var state by editorContext.mutableStateOf( key = "customize_behaviour_state", initial = CustomizeBehaviourState(), ) onLoaded = { editorContext.engine.event .subscribe(blocks = emptyList()) .onEach { events -> if (events.isNotEmpty()) { state = state.copy( latestEvent = "Received ${events.size} block event(s).", ) } } .launchIn(editorContext.coroutineScope) } onEvent = { event -> when (event) { is ValidateSelection -> { val selectedCount = editorContext.engine.block.findAllSelected().size state = state.copy( dialogMessage = if (selectedCount == 0) { "Select a block before running this workflow." } else { "Workflow can run for $selectedCount selected block(s)." }, ) } is OpenWorkflowSheet -> { editorContext.eventHandler.send( EditorEvent.Sheet.Open( type = workflowSheet(latestEvent = state.latestEvent), ), ) } } } dock = { Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.remember { id = { EditorComponentId("customize_behaviour_validate") } textString = { "Validate" } vectorIcon = { IconPack.PlayBox } enabled = { state.dialogMessage == null } onClick = { editorContext.eventHandler.send(ValidateSelection) } } } add { Dock.Button.remember { id = { EditorComponentId("customize_behaviour_panel") } textString = { "Status" } vectorIcon = { IconPack.Export } onClick = { editorContext.eventHandler.send(OpenWorkflowSheet) } } } } } } } overlay = { EditorComponent.remember { decoration = { Box(modifier = Modifier.fillMaxSize()) { Button( modifier = Modifier .align(Alignment.TopEnd) .padding(16.dp), onClick = { uiMode = if (uiMode == EditorUiMode.DARK) { EditorUiMode.LIGHT } else { EditorUiMode.DARK } }, ) { Text("Toggle theme") } } state.dialogMessage?.let { message -> AlertDialog( onDismissRequest = { state = state.copy(dialogMessage = null) }, title = { Text("Workflow update") }, text = { Text(message) }, confirmButton = { TextButton( onClick = { state = state.copy(dialogMessage = null) }, ) { Text("OK") } }, ) } } } } } }, onClose = onClose, ) } private fun workflowSheet(latestEvent: String): SheetType = SheetType.Custom( style = SheetStyle( isFloating = false, minHeight = Height.Exactly(0.dp), maxHeight = Height.Fraction(0.45F), isHalfExpandingEnabled = false, isHalfExpandedInitially = true, ), content = { Column( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text("Latest editor event") Spacer(modifier = Modifier.height(12.dp)) Text(latestEvent) Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true)) }, ) { Text("Close") } } }, ) ``` Connect Android editor UI components to custom application logic with editor events, Compose state, custom sheets, and runtime theme changes. ![Android editor screen with custom behavior controls in the dock](./assets/customize-behaviour-android.png) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-ui-extensions-customize-behaviour) Android behavior customization is built around the Base Editor configuration layer. You keep UI state in Compose, use editor-scoped state for values updated from editor callbacks, send `EditorEvent`s from editor components, handle those events in `EditorConfiguration.onEvent`, and collect engine flows when the editor is loaded. For a complete ready-made surface, start with the [Design Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/design-editor-8unj9u/) and apply the same event and state patterns where your app renders `Editor`. ## Understanding UI Customization Approaches Use static configuration for the UI structure that should exist when the editor starts, such as custom dock buttons or overlays. Use event handling and Compose state for behavior that changes while the editor is running. The sample groups its runtime UI state in one data object. Components read this state, while callbacks update it after user actions or engine events. ```kotlin highlight-android-state data class CustomizeBehaviourState( val latestEvent: String = "No scene changes observed yet.", val dialogMessage: String? = null, ) ``` Because the event subscription runs in the editor coroutine scope, store the state with `editorContext.mutableStateOf(...)`. This keeps callback-updated values alive across Android configuration changes until the editor closes. ```kotlin highlight-android-editor-state var state by editorContext.mutableStateOf( key = "customize_behaviour_state", initial = CustomizeBehaviourState(), ) ``` ## Listening to Editing Events Use `onLoaded` for long-running subscriptions because the editor engine is ready at that point. `engine.event.subscribe(blocks = emptyList())` listens to block lifecycle changes for the whole scene, and `launchIn(editorContext.coroutineScope)` keeps the collection tied to the editor lifetime. ```kotlin highlight-android-listen-events onLoaded = { editorContext.engine.event .subscribe(blocks = emptyList()) .onEach { events -> if (events.isNotEmpty()) { state = state.copy( latestEvent = "Received ${events.size} block event(s).", ) } } .launchIn(editorContext.coroutineScope) } ``` Use this pattern when external UI should react to scene edits, selection changes, or block updates. For deeper block-event patterns, see [Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/). ## Programmatic Sheet Management Android editor panels are represented as sheets. Create a `SheetType.Custom` when your workflow needs a focused UI surface, then open or close it by sending sheet events through the editor event handler. ```kotlin highlight-android-custom-sheet private fun workflowSheet(latestEvent: String): SheetType = SheetType.Custom( style = SheetStyle( isFloating = false, minHeight = Height.Exactly(0.dp), maxHeight = Height.Fraction(0.45F), isHalfExpandingEnabled = false, isHalfExpandedInitially = true, ), content = { Column( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text("Latest editor event") Spacer(modifier = Modifier.height(12.dp)) Text(latestEvent) Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true)) }, ) { Text("Close") } } }, ) ``` The sample opens the sheet from `onEvent` after a custom event is sent from the dock. This keeps panel behavior centralized with the rest of the guide's runtime logic. ## Showing Feedback with Compose On Android, feedback UI is regular Compose content that you render through the editor overlay. Store the message in Compose state, then show a dialog from `overlay`. ```kotlin highlight-android-overlay overlay = { EditorComponent.remember { decoration = { Box(modifier = Modifier.fillMaxSize()) { Button( modifier = Modifier .align(Alignment.TopEnd) .padding(16.dp), onClick = { uiMode = if (uiMode == EditorUiMode.DARK) { EditorUiMode.LIGHT } else { EditorUiMode.DARK } }, ) { Text("Toggle theme") } } state.dialogMessage?.let { message -> AlertDialog( onDismissRequest = { state = state.copy(dialogMessage = null) }, title = { Text("Workflow update") }, text = { Text(message) }, confirmButton = { TextButton( onClick = { state = state.copy(dialogMessage = null) }, ) { Text("OK") } }, ) } } } } ``` This approach lets your app use normal Android accessibility, theming, and lifecycle behavior while still placing the feedback above the editor. ## Registering Custom Actions Define custom actions as `EditorEvent` implementations. Send those events from buttons, sheets, or overlay controls, then handle them in `onEvent`. ```kotlin highlight-android-custom-events object ValidateSelection : EditorEvent object OpenWorkflowSheet : EditorEvent ``` The handler can inspect the engine, update app state, or send built-in editor events. In this sample, one custom event validates the current selection, while another opens a custom status sheet. ```kotlin highlight-android-handle-events onEvent = { event -> when (event) { is ValidateSelection -> { val selectedCount = editorContext.engine.block.findAllSelected().size state = state.copy( dialogMessage = if (selectedCount == 0) { "Select a block before running this workflow." } else { "Workflow can run for $selectedCount selected block(s)." }, ) } is OpenWorkflowSheet -> { editorContext.eventHandler.send( EditorEvent.Sheet.Open( type = workflowSheet(latestEvent = state.latestEvent), ), ) } } } ``` ## Dynamic Theme Control The `Editor` composable accepts `uiMode`, so you can hoist the selected mode into Compose state and change it at runtime. ```kotlin highlight-android-theme uiMode = uiMode, ``` The sample toggles this state from an overlay button. Any custom components that read `MaterialTheme.colorScheme` update with the active editor theme. ## Feature Management at Runtime For custom Android editor components, feature availability is regular component state. Read app permissions, dialog state, or engine state inside `enabled` or `visible` lambdas so controls stay in sync with your app. ```kotlin highlight-android-dock-buttons dock = { Dock.remember { horizontalArrangement = { Arrangement.Center } listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.remember { id = { EditorComponentId("customize_behaviour_validate") } textString = { "Validate" } vectorIcon = { IconPack.PlayBox } enabled = { state.dialogMessage == null } onClick = { editorContext.eventHandler.send(ValidateSelection) } } } add { Dock.Button.remember { id = { EditorComponentId("customize_behaviour_panel") } textString = { "Status" } vectorIcon = { IconPack.Export } onClick = { editorContext.eventHandler.send(OpenWorkflowSheet) } } } } } } } ``` For built-in editing permissions, keep using engine roles and scopes. Use UI component state for the controls you add yourself. ## Integrating with External State The same state object can bridge CE.SDK and your app. Engine subscriptions update `latestEvent`, editor components send custom events, `onEvent` translates those events into state changes, and the overlay renders the result. This keeps the editor integration one-directional: user interactions send events, callbacks update state, and Compose redraws the UI. ## Troubleshooting ### Event Handlers Do Not Run Verify that custom controls call `editorContext.eventHandler.send(...)` and that `onEvent` handles the event type. Built-in and custom events are both forwarded to `onEvent`. ### Sheets Do Not Open Make sure you send `EditorEvent.Sheet.Open(type = ...)` with a concrete `SheetType`. To dismiss a custom sheet from its content, send `EditorEvent.Sheet.Close(animate = true)`. ### UI State Resets Unexpectedly Hoist state above `Editor` when the value should survive recomposition. Use `editorContext.mutableStateOf(...)` for state that must survive editor configuration changes until the editor closes. ### Theme Changes Do Not Apply Pass the current `EditorUiMode` through `Editor(uiMode = ...)`. If custom components use hard-coded colors, replace them with `MaterialTheme.colorScheme` values so they respond to the active theme. ## API Reference | API | Description | | --- | ----------- | | `Editor(uiMode=_)` | Renders the editor with the selected light, dark, or system theme mode. | | `EditorConfiguration.remember(builder=_)` | Creates the Android editor configuration used by callbacks and components. | | `editorContext.mutableStateOf(key=_, initial=_)` | Stores custom state in the editor scope so callback-updated values survive configuration changes. | | `onLoaded = _` | Runs after the editor engine is ready and is the right place to collect long-lived engine flows. | | `engine.event.subscribe(blocks=_)` | Subscribes to block lifecycle events, optionally filtered by block list. | | `onEvent = _` | Receives built-in and custom `EditorEvent`s sent through the editor event handler. | | `editorContext.engine.block.findAllSelected()` | Reads the current editor selection so custom event handlers can validate selected blocks. | | `editorContext.eventHandler.send(event=_)` | Sends a built-in or custom editor event. | | `EditorEvent.Sheet.Open(type=_)` | Opens a built-in or custom editor sheet. | | `EditorEvent.Sheet.Close(animate=_)` | Closes the currently active editor sheet. | | `SheetType.Custom(style=_, content=_)` | Defines a custom Android sheet with Compose content. | | `Dock.remember(builder=_)` | Configures the editor dock. | | `Dock.ListBuilder.remember(builder=_)` | Builds the list of dock items. | | `Dock.ListBuilder.add(block=_)` | Adds a dock item to the list. | | `Dock.Button.remember(builder=_)` | Creates a custom dock button. | | `EditorComponent.remember(builder=_)` | Creates custom overlay or component content. | ## Next Steps - [Create a Custom Panel](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/create-custom-panel-d87b83/) — Design a custom sidebar panel to support unique workflows and user needs. - [Hide Elements](https://img.ly/docs/cesdk/android/user-interface/customization/hide-elements-fe945c/) — Learn how to hide and show UI elements in CE.SDK using the Feature API, ordering APIs, and panel management across different 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: "Quick Actions" description: "Add one-tap editing actions to the canvas menu and inspector bar that edit the selected block instantly." platform: android url: "https://img.ly/docs/cesdk/android/user-interface/ui-extensions/quick-actions-1e478d/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/android/user-interface-5a089a/) > [UI Extensions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1/) > [Quick Actions](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/quick-actions-1e478d/) --- ```kotlin file=@cesdk_android_examples/editor-guides-quick-actions/QuickActionsEditorSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import ly.img.editor.Editor import ly.img.editor.core.component.CanvasMenu import ly.img.editor.core.component.EditorComponentId import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberDelete import ly.img.editor.core.component.rememberDuplicate import ly.img.editor.core.component.rememberLayer 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.Color as EngineColor // Add this composable to your NavHost. @Composable fun QuickActionsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, configuration = { EditorConfiguration.remember { onCreate = { if (editorContext.engine.scene.get() == null) { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(page, value = 1080F) editorContext.engine.block.setHeight(page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) val block = editorContext.engine.block.create(DesignBlockType.Graphic) editorContext.engine.block.setShape( block = block, shape = editorContext.engine.block.createShape(ShapeType.Rect), ) val fill = editorContext.engine.block.createFill(FillType.Color) editorContext.engine.block.setFill(block = block, fill = fill) editorContext.engine.block.setFillSolidColor( block = block, color = EngineColor.fromRGBA(r = 0.96F, g = 0.6F, b = 0.12F, a = 1F), ) editorContext.engine.block.setWidth(block, value = 540F) editorContext.engine.block.setHeight(block, value = 540F) editorContext.engine.block.setPositionX(block, value = 270F) editorContext.engine.block.setPositionY(block, value = 270F) editorContext.engine.block.appendChild(parent = page, child = block) editorContext.engine.scene.zoomToBlock(page) editorContext.engine.block.setSelected(block, selected = true) } } canvasMenu = { rememberQuickActionsCanvasMenu() } inspectorBar = { rememberQuickActionsInspectorBar() } } }, onClose = onClose, ) } @Composable private fun rememberQuickActionsCanvasMenu() = CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberDuplicate() } add { rememberFlipSelectionButton() } add { rememberResetOrientationButton() } add { CanvasMenu.Button.rememberDelete() } } } } @Composable private fun rememberFlipSelectionButton() = CanvasMenu.Button.remember { id = { EditorComponentId("ly.img.guide.quickActions.canvasMenu.flip") } visible = { remember(this) { val selectedBlock = editorContext.safeSelection?.designBlock selectedBlock != null && editorContext.engine.block.supportsFill(selectedBlock) && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/flip") } } textString = { "Flip" } contentDescription = { "Flip selected block horizontally" } onClick = { flipSelectedBlockHorizontally() } } @Composable private fun rememberResetOrientationButton() = CanvasMenu.Button.remember { id = { EditorComponentId("ly.img.guide.quickActions.canvasMenu.resetOrientation") } visible = { remember(this) { val selectedBlock = editorContext.safeSelection?.designBlock selectedBlock != null && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/rotate") && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/flip") } } textString = { "Reset" } contentDescription = { "Reset selected block orientation" } onClick = { val selectedBlock = editorContext.selection.designBlock editorContext.engine.block.setRotation(block = selectedBlock, radians = 0F) editorContext.engine.block.setFlipHorizontal(block = selectedBlock, flip = false) editorContext.engine.editor.addUndoStep() } } @Composable private fun rememberQuickActionsInspectorBar() = InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.remember { id = { EditorComponentId("ly.img.guide.quickActions.inspectorBar.flip") } visible = { remember(this) { val selectedBlock = editorContext.safeSelection?.designBlock selectedBlock != null && editorContext.engine.block.supportsFill(selectedBlock) && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/flip") } } textString = { "Flip" } contentDescription = { "Flip selected block horizontally" } onClick = { val selectedBlock = editorContext.selection.designBlock val flipped = editorContext.engine.block.isFlipHorizontal(selectedBlock) editorContext.engine.block.setFlipHorizontal(block = selectedBlock, flip = !flipped) editorContext.engine.editor.addUndoStep() } } } add { InspectorBar.Button.rememberDelete() } } } } private fun CanvasMenu.ItemScope.flipSelectedBlockHorizontally() { val selectedBlock = editorContext.selection.designBlock val flipped = editorContext.engine.block.isFlipHorizontal(selectedBlock) editorContext.engine.block.setFlipHorizontal(block = selectedBlock, flip = !flipped) editorContext.engine.editor.addUndoStep() } ``` A quick action is a one-tap button that edits the selected block immediately, with no panel in between. Surface the built-in actions, then add your own that run engine edits on tap. ![Quick Actions on Android](./assets/android.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-quick-actions) ## Overview Quick actions live where the user is already working: the canvas menu that floats next to a selected block, and the inspector bar that runs along the bottom of the editor. Each one is a button whose `onClick` handler reaches into the engine and edits the current selection on tap — duplicate, delete, flip, reset — without opening a sheet. CE.SDK ships a set of predefined quick actions and lets you add your own. A custom quick action is a `CanvasMenu.Button` or `InspectorBar.Button` whose handler calls the engine; the difference from an ordinary button is that the handler performs the edit directly. | Symbol | Purpose | | ------ | ------- | | `CanvasMenu.Button.rememberDuplicate()` and siblings | Predefined quick actions | | `CanvasMenu.Button.remember { ... }` | A custom one-tap action | | `editorContext.selection.designBlock` | The selected block the handler edits | | `InspectorBar.Button.remember { ... }` | Reuse an action from a second surface | The example creates and selects a graphic block during `onCreate` only so the quick actions are visible when the guide opens. That setup is demo scaffolding; the reusable lesson starts with the `canvasMenu` and `inspectorBar` builders. ## Built-in Quick Actions CE.SDK ships factory functions for common selected-block edits. Each function returns a button with its default action, label, and visibility rules, so dropping it into the list is enough: ```kotlin highlight-android-canvas-menu @Composable private fun rememberQuickActionsCanvasMenu() = CanvasMenu.remember { listBuilder = { CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberDuplicate() } add { rememberFlipSelectionButton() } add { rememberResetOrientationButton() } add { CanvasMenu.Button.rememberDelete() } } } } ``` The example keeps the predefined duplicate and delete actions, then inserts custom quick actions between them. Other predefined canvas menu actions include selection and layer-order operations such as bring forward, send backward, and select group. ## A Custom One-Tap Action When the predefined buttons do not cover your edit, construct a button with a stable reverse-domain `id`, a label, and an `onClick` handler. This custom action mirrors the selected block horizontally: ```kotlin highlight-android-custom-button @Composable private fun rememberFlipSelectionButton() = CanvasMenu.Button.remember { id = { EditorComponentId("ly.img.guide.quickActions.canvasMenu.flip") } visible = { remember(this) { val selectedBlock = editorContext.safeSelection?.designBlock selectedBlock != null && editorContext.engine.block.supportsFill(selectedBlock) && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/flip") } } textString = { "Flip" } contentDescription = { "Flip selected block horizontally" } onClick = { flipSelectedBlockHorizontally() } } ``` The `visible` predicate gates the action to selected blocks where the operation applies. The `onClick` handler then edits the current `editorContext.selection.designBlock`. Because the handler edits the engine directly, it records an undo step when it finishes. Built-in quick actions do this for you; custom engine edits should call `engine.editor.addUndoStep()` once their mutation is complete. ```kotlin highlight-android-shared-helper private fun CanvasMenu.ItemScope.flipSelectedBlockHorizontally() { val selectedBlock = editorContext.selection.designBlock val flipped = editorContext.engine.block.isFlipHorizontal(selectedBlock) editorContext.engine.block.setFlipHorizontal(block = selectedBlock, flip = !flipped) editorContext.engine.editor.addUndoStep() } ``` ## Gate the Action to the Selection Use `visible` when an action should only appear for compatible selections, and `enabled` when it should stay visible but temporarily inactive. The sample checks the current block before showing the flip action, so the canvas menu does not offer an operation that the selection cannot perform. ## A Compound Quick Action A single tap can run several engine calls. This action resets the block's orientation by clearing its rotation and horizontal flip together: ```kotlin highlight-android-compound-action @Composable private fun rememberResetOrientationButton() = CanvasMenu.Button.remember { id = { EditorComponentId("ly.img.guide.quickActions.canvasMenu.resetOrientation") } visible = { remember(this) { val selectedBlock = editorContext.safeSelection?.designBlock selectedBlock != null && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/rotate") && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/flip") } } textString = { "Reset" } contentDescription = { "Reset selected block orientation" } onClick = { val selectedBlock = editorContext.selection.designBlock editorContext.engine.block.setRotation(block = selectedBlock, radians = 0F) editorContext.engine.block.setFlipHorizontal(block = selectedBlock, flip = false) editorContext.engine.editor.addUndoStep() } } ``` The single `addUndoStep()` after both edits collapses the operation into one history entry, so undo reverts the rotation and flip together. ## Reuse the Action in the Inspector Bar A quick action is not tied to the canvas menu. The inspector bar exposes the same editor context, so the same engine-editing pattern works from a second surface: ```kotlin highlight-android-inspector-bar @Composable private fun rememberQuickActionsInspectorBar() = InspectorBar.remember { listBuilder = { InspectorBar.ListBuilder.remember { add { InspectorBar.Button.rememberLayer() } add { InspectorBar.Button.remember { id = { EditorComponentId("ly.img.guide.quickActions.inspectorBar.flip") } visible = { remember(this) { val selectedBlock = editorContext.safeSelection?.designBlock selectedBlock != null && editorContext.engine.block.supportsFill(selectedBlock) && editorContext.engine.block.isAllowedByScope(selectedBlock, key = "layer/flip") } } textString = { "Flip" } contentDescription = { "Flip selected block horizontally" } onClick = { val selectedBlock = editorContext.selection.designBlock val flipped = editorContext.engine.block.isFlipHorizontal(selectedBlock) editorContext.engine.block.setFlipHorizontal(block = selectedBlock, flip = !flipped) editorContext.engine.editor.addUndoStep() } } } add { InspectorBar.Button.rememberDelete() } } } } ``` Use the inspector bar for selected-block actions that should stay visible beside persistent controls such as layer and delete. Keep the behavior consistent across surfaces by sharing the same engine logic where possible. ## Handling State and Feedback Quick actions should make their state clear before and after they run: - Hide or disable actions when the selected block cannot support the operation. - Keep the handler focused on one immediate edit or one atomic group of edits. - Use a panel, sheet, or app-level message for workflows that need user input, progress, or async failure handling. ## Troubleshooting **Canvas menu button missing** - Select a compatible block and check that the button is added to `CanvasMenu.ListBuilder`. **Inspector bar button missing** - Select a compatible block and check that the button is added to `InspectorBar.ListBuilder`. **Action does not change the block** - Check both the selection predicate and the engine API scope. The sample guards the flip action with `engine.block.supportsFill(block)` and `engine.block.isAllowedByScope(block=_, key="layer/flip")`. **Action cannot be undone** - If a custom handler edits the engine directly, call `engine.editor.addUndoStep()` after the mutation. Built-in quick actions already handle undo history. ## API Reference | Method | Description | | ------ | ----------- | | `EditorConfiguration.remember(builder=_)` | Creates the editor configuration where quick actions are wired | | `CanvasMenu.remember(builder=_)` | Creates a canvas menu component for the selected block | | `CanvasMenu.ListBuilder.remember(builder=_)` | Builds the ordered canvas menu item list | | `CanvasMenu.ListBuilder.add(block=_)` | Adds a button or divider to the canvas menu list | | `CanvasMenu.Button.remember(builder=_)` | Creates a custom canvas menu button | | `CanvasMenu.Button.rememberDuplicate(builder=_)` | Creates the predefined duplicate button | | `CanvasMenu.Button.rememberDelete(builder=_)` | Creates the predefined delete button | | `InspectorBar.remember(builder=_)` | Creates an inspector bar component for the selected block | | `InspectorBar.ListBuilder.remember(builder=_)` | Builds the ordered inspector bar item list | | `InspectorBar.ListBuilder.add(block=_)` | Adds a button to the inspector bar list | | `InspectorBar.Button.remember(builder=_)` | Creates a custom inspector bar button | | `InspectorBar.Button.rememberLayer(builder=_)` | Creates the predefined layer inspector button | | `InspectorBar.Button.rememberDelete(builder=_)` | Creates the predefined delete inspector button | | `engine.block.supportsFill(block=_)` | Checks whether the selected block supports fill-related actions | | `engine.block.isAllowedByScope(block=_, key=_)` | Checks whether a block allows a scoped operation | | `engine.block.isFlipHorizontal(block=_)` | Reads the block's current horizontal flip state | | `engine.block.setFlipHorizontal(block=_, flip=_)` | Toggles the horizontal flip state | | `engine.block.setRotation(block=_, radians=_)` | Sets the selected block's rotation | | `engine.editor.addUndoStep()` | Records a programmatic block change in the undo history | ## Next Steps - [Add a New Button](https://img.ly/docs/cesdk/android/user-interface/ui-extensions/add-new-button-74884d/) - Add buttons to the Dock, Canvas Menu, Inspector Bar, and Navigation Bar. - [Canvas Menu](https://img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b/) - Configure the floating toolbar that hosts quick actions. - [Inspector Bar](https://img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd/) - Customize the inspector bar for editing properties like position, color, and size. - [Remove Background](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) — Remove image backgrounds to isolate subjects or prepare assets for compositing and reuse. --- ## 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 Creative Editor" description: "The Android Mobile Design Editor SDK offers a robust and customizable solution for creating and editing visual designs directly within your Android applications." platform: android url: "https://img.ly/docs/cesdk/android/what-is-cesdk-2e7acd/" --- > 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/) > [What is CE.SDK?](https://img.ly/docs/cesdk/android/what-is-cesdk-2e7acd/) --- The Android Mobile Design Editor SDK offers a robust and customizable solution for creating and editing visual designs directly within your Android applications. ### What is CE.SDK? **CreativeEditor SDK (CE.SDK)** is a powerful design engine that brings fully customizable image, video, and design editing directly into your iOS app. Whether you're enabling AI-powered design workflows, template-based creation, dynamic content generation, or full-featured creative editing, CE.SDK offers the flexibility, performance, and developer control you need — all with minimal integration overhead. [Explore Demos](https://img.ly/showcases/cesdk/?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) Trusted by leading organizations worldwide, CE.SDK powers the creative editors used in best-in-class applications, including those from Shopify, Semrush, HP, Shutterfly, Ticketmaster, and Swiss Post. ## Key Capabilities of the Android Creative Editor SDK ## File Format Support CE.SDK supports a wide range of file types to ensure maximum flexibility for developers: ### Importing Media ### Exporting Media ### Importing Templates For detailed information, see the [full file format support list](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/). ## Integrations CE.SDK supports out-of-the-box integrations with: - **Getty Images** - **Unsplash** - **Pexels** - **Soundstripe** Want to connect your own asset sources? Register a custom provider using our 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