--- title: "Animation" description: "Add motion to designs with support for keyframes, timeline editing, and programmatic animation control." platform: android url: "https://img.ly/docs/cesdk/android/animation-ce900c/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) - Add motion to video scenes with preset animation controls and programmatic animation APIs. - [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) - Explore the types of animations supported by CE.SDK, including object, text, and transition effects. - [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) - Build animations manually or with presets to animate objects, text, and scenes within your design. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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 animations manually or with presets to animate objects, text, and scenes within your design." 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/) --- --- ## Related Pages - [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) - Apply movement, scaling, rotation, or opacity changes to elements using timeline-based keyframes. - [Text Animations](https://img.ly/docs/cesdk/android/animation/create/text-d6f4aa/) - Animate text elements with effects like fade, typewriter, and bounce for dynamic visual presentation. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Base Animations" description: "Apply movement, scaling, rotation, or opacity changes to elements using timeline-based keyframes." platform: android url: "https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) > [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) --- ```kotlin file=@cesdk_android_examples/engine-guides-using-animations/UsingAnimations.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block, fill = fill) if (!engine.block.supportsAnimation(block)) { engine.stop() return@launch } 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, slideInAnimation) engine.block.setLoopAnimation(block, breathingLoopAnimation) engine.block.setOutAnimation(block, fadeOutAnimation) val animation = engine.block.getLoopAnimation(block) val animationType = engine.block.getType(animation) val squeezeLoopAnimation = engine.block.createAnimation(AnimationType.SqueezeLoop) engine.block.destroy(engine.block.getLoopAnimation(block)) engine.block.setLoopAnimation(block, squeezeLoopAnimation) // The following line would also destroy all currently attached animations // engine.block.destroy(block) val allAnimationProperties = engine.block.findAllProperties(slideInAnimation) engine.block.setFloat(slideInAnimation, "animation/slide/direction", 0.5F * Math.PI.toFloat()) engine.block.setDuration(slideInAnimation, 0.6) engine.block.setEnum(slideInAnimation, "animationEasing", "EaseOut") println("Available easing options: ${engine.block.getEnumValues("animationEasing")}") 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() } ``` CreativeEditor SDK supports many different types of configurable animations for animating the appearance of design blocks in video scenes. Similarly to blocks, each animation object has a numeric id which can be used to query and [modify its properties](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/). ## Accessing Animation APIs In order to query whether a block supports animations, you should call the `fun supportsAnimation(block: DesignBlock): Boolean` API. ```kotlin highlight-supportsAnimation if (!engine.block.supportsAnimation(block)) { engine.stop() return@launch } ``` ## Animation Categories There are three different categories of animations: *In*, *Out* and *Loop* animations. ### In Animations *In* animations animate a block for a specified duration after the block first appears in the scene. For example, if a block has a time offset of 4s in the scene and it has an *In* animation with a duration of 1s, then the appearance of the block will be animated between 4s and 5s with the *In* animation. ### Out Animations *Out* animations animate a block for a specified duration before the block disappears from the scene. For example, if a block has a time offset of 4s in the scene and a duration of 5s and it has an *Out* animation with a duration of 1s, then the appearance of the block will be animated between 8s and 9s with the *Out* animation. ### Loop Animations *Loop* animations animate a block for the total duration that the block is visible in the scene. *Loop* animations also run simultaneously with *In* and *Out* animations, if those are present. ## Creating Animations In order to create a new animation, we must call the `fun createAnimation(type: AnimationType): DesignBlock` API. All `AnimationType` implementations below are nested in `AnimationType` sealed class. We currently support the following *In* and *Out* animation types: - `Slide - "//ly.img.ubq/animation/slide"` - `Pan - "//ly.img.ubq/animation/pan"` - `Fade - "//ly.img.ubq/animation/fade"` - `Blur - "//ly.img.ubq/animation/blur"` - `Grow - "//ly.img.ubq/animation/grow"` - `Zoom - "//ly.img.ubq/animation/zoom"` - `Pop - "//ly.img.ubq/animation/pop"` - `Wipe - "//ly.img.ubq/animation/wipe"` - `Baseline - "//ly.img.ubq/animation/baseline"` - `CropZoom - "//ly.img.ubq/animation/crop_zoom"` - `Spin - "//ly.img.ubq/animation/spin"` - `KenBurns - "//ly.img.ubq/animation/ken_burns"` - `TypewriterText - "//ly.img.ubq/animation/typewriter_text"` // text-ony - `BlockSwipeText - "//ly.img.ubq/animation/block_swipe_text"` // text-ony - `SpreadText - "//ly.img.ubq/animation/spread_text"` // text-only - `MergeText - "//ly.img.ubq/animation/merge_text"` // text-only and the following *Loop* animation types: - `SpinLoop - "//ly.img.ubq/animation/spin_loop"` - `FadeLoop - "//ly.img.ubq/animation/fade_loop"` - `BlurLoop - "//ly.img.ubq/animation/blur_loop"` - `PulsatingLoop - "//ly.img.ubq/animation/pulsating_loop"` - `BreathingLoop - "//ly.img.ubq/animation/breathing_loop"` - `JumpLoop - "//ly.img.ubq/animation/jump_loop"` - `SqueezeLoop - "//ly.img.ubq/animation/squeeze_loop"` - `SwayLoop - "//ly.img.ubq/animation/sway_loop"` ```kotlin highlight-createAnimation val slideInAnimation = engine.block.createAnimation(AnimationType.Slide) val breathingLoopAnimation = engine.block.createAnimation(AnimationType.BreathingLoop) val fadeOutAnimation = engine.block.createAnimation(AnimationType.Fade) ``` ## Assigning Animations In order to assign an *In* animation to the block, call the `fun setInAnimation(block: DesignBlock, animation: DesignBlock)` API. ```kotlin highlight-setInAnimation engine.block.setInAnimation(block, slideInAnimation) ``` In order to assign a *Loop* animation to the block, call the `fun setLoopAnimation(block: DesignBlock, animation: DesignBlock)` API. ```kotlin highlight-setLoopAnimation engine.block.setLoopAnimation(block, breathingLoopAnimation) ``` In order to assign an *Out* animation to the block, call the `fun setOutAnimation(block: DesignBlock, animation: DesignBlock)` API. ```kotlin highlight-setOutAnimation engine.block.setOutAnimation(block, fadeOutAnimation) ``` To query the current animation ids of a design block, call the `fun getInAnimation(block: DesignBlock): DesignBlock`, `fun getLoopAnimation(block: DesignBlock): DesignBlock` or `fun getInAnimation(block: DesignBlock): DesignBlock` API. You can now pass the returned animation `DesignBlock` into other APIs in order to query more information about the animation, e.g. its type via the `fun getType(block: DesignBlock): String` API. In case the design block does not have animation, query will return an invalid design block. Make sure to check for `fun isValid(block: DesignBlock): Boolean` before running any API's on the animation design block. ```kotlin highlight-getAnimation val animation = engine.block.getLoopAnimation(block) val animationType = engine.block.getType(animation) ``` When replacing the animation of a design block, remember to destroy the previous animation object if you don't intend to use it any further. Animation objects that are not attached to a design block will never be automatically destroyed. Destroying a design block will also destroy all of its attached animations. ```kotlin highlight-replaceAnimation val squeezeLoopAnimation = engine.block.createAnimation(AnimationType.SqueezeLoop) engine.block.destroy(engine.block.getLoopAnimation(block)) engine.block.setLoopAnimation(block, squeezeLoopAnimation) // The following line would also destroy all currently attached animations // engine.block.destroy(block) ``` ## Animation Properties Just like design blocks, animations with different types have different properties that you can query and modify via the API. Use `fun findAllProperties(block: DesignBlock): List` in order to get a list of all properties of a given animation. For the slide animation in this example, the call would return `["name", "animation/slide/direction", "animationEasing", "includedInExport", "playback/duration", "type", "uuid"]`. Please refer to the [API docs](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) for a complete list of all available properties for each type of animation. ```kotlin highlight-getProperties val allAnimationProperties = engine.block.findAllProperties(slideInAnimation) ``` Once we know the property keys of an animation, we can use the same APIs as for design blocks in order to modify those properties. For example, we can use `fun setFloat(block: DesignBlock, property: String, value: Float)` in order to change the direction of the slide animation to make our block slide in from the top. ```kotlin highlight-modifyProperties engine.block.setFloat(slideInAnimation, "animation/slide/direction", 0.5F * Math.PI.toFloat()) ``` All animations have a duration. For *In* and *Out* animations, the duration defines the total length of the animation as described above. For *Loop* animations, the duration defines the length of each loop cycle. We can use the `fun setDuration(block: DesignBlock, duration: Double)` API in order to change the animation duration. Note that changing the duration of an *In* animation will automatically adjust the duration of the *Out* animation (and vice versa) in order to avoid overlaps between the two animations. ```kotlin highlight-changeDuration engine.block.setDuration(slideInAnimation, 0.6) ``` Some animations allow you to configure their easing behavior by choosing from a list of common easing curves. The easing controls the acceleration throughout the animation. We can use the `fun setEnum(block: DesignBlock, property: String, value: String)` API in order to change the easing curve. Call `engine.block.getEnumValues("animationEasing")` in order to get a list of currently supported easing options. In this example, we set the easing to `EaseOut` so that the animation starts fast and then slows down towards the end. An `EaseIn` easing would start slow and then speed up, while `EaseInOut` starts slow, speeds up towards the middle of the animation and then slows down towards the end again. ```kotlin highlight-changeEasing engine.block.setEnum(slideInAnimation, "animationEasing", "EaseOut") println("Available easing options: ${engine.block.getEnumValues("animationEasing")}") ``` ## Full Code Here's the full code: ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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, 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.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block, fill = fill) if (!engine.block.supportsAnimation(block)) { engine.stop() return@launch } 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, slideInAnimation) engine.block.setLoopAnimation(block, breathingLoopAnimation) engine.block.setOutAnimation(block, fadeOutAnimation) val animation = engine.block.getLoopAnimation(block) val animationType = engine.block.getType(animation) val squeezeLoopAnimation = engine.block.createAnimation(AnimationType.SqueezeLoop) engine.block.destroy(engine.block.getLoopAnimation(block)) engine.block.setLoopAnimation(block, squeezeLoopAnimation) // The following line would also destroy all currently attached animations // engine.block.destroy(block) val allAnimationProperties = engine.block.findAllProperties(slideInAnimation) engine.block.setFloat(slideInAnimation, "animation/slide/direction", 0.5F * Math.PI.toFloat()) engine.block.setDuration(slideInAnimation, 0.6) engine.block.setEnum(slideInAnimation, "animationEasing", "EaseOut") println("Available easing options: ${engine.block.getEnumValues("animationEasing")}") 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: "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-using-animations/UsingAnimations.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block, fill = fill) if (!engine.block.supportsAnimation(block)) { engine.stop() return@launch } 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, slideInAnimation) engine.block.setLoopAnimation(block, breathingLoopAnimation) engine.block.setOutAnimation(block, fadeOutAnimation) val animation = engine.block.getLoopAnimation(block) val animationType = engine.block.getType(animation) val squeezeLoopAnimation = engine.block.createAnimation(AnimationType.SqueezeLoop) engine.block.destroy(engine.block.getLoopAnimation(block)) engine.block.setLoopAnimation(block, squeezeLoopAnimation) // The following line would also destroy all currently attached animations // engine.block.destroy(block) val allAnimationProperties = engine.block.findAllProperties(slideInAnimation) engine.block.setFloat(slideInAnimation, "animation/slide/direction", 0.5F * Math.PI.toFloat()) engine.block.setDuration(slideInAnimation, 0.6) engine.block.setEnum(slideInAnimation, "animationEasing", "EaseOut") println("Available easing options: ${engine.block.getEnumValues("animationEasing")}") 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() } ``` When applied to text blocks, some animations allow you to control whether the animation should be applied to the entire text at once, line by line, word by word or character by character. We can use the `fun setEnum(block: DesignBlock, property: String, value: String)` API in order to change the text writing style. Call `engine.block.getEnumValues("textAnimationWritingStyle")` in order to get a list of currently supported text writing style options. The default writing style is `Line`. In this example, we set the easing to `Word` so that the text animates in one word at a time. ```kotlin highlight-textAnimationWritingStyle 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") ``` Together with the writing style, you can also configure the overlap between the individual segments of a text animation using the `textAnimationOverlap` property. With an overlap value of `0`, the next segment only starts its animation once the previous segment's animation has finished. With an overlap value of `1`, all segments animate at the same time. ```kotlin highlight-textAnimationOverlap 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") ``` ## Full Code Here's the full code: ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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, 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 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() } ``` --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Add motion to video scenes with preset animation controls and programmatic animation APIs." platform: android url: "https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/android/animation-ce900c/) > [Overview](https://img.ly/docs/cesdk/android/animation/overview-6a2ef2/) --- Animations in CreativeEditor SDK (CE.SDK) bring your designs to life by adding motion to images, text, and design elements in video scenes. Whether you're creating a dynamic social media post, a video ad, or an engaging product demo, animations help capture attention and communicate ideas more effectively. The editor UI can expose preset in, out, and loop animations for selected video, image, sticker, shape, and text blocks. You can adjust the properties exposed by each preset in the UI where available, or control animations programmatically with the CreativeEngine API. Android integrations can use the [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) Animations inspector for supported block types. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Timeline and Preset Timing Animations in CE.SDK are time-based presets attached to blocks in video scenes. In, out, and loop animations start relative to block visibility on the page timeline. In animations play when a block appears, out animations play when it leaves, and loop animations repeat while the block is visible. CE.SDK animations are preset effects, so they do not expose custom per-property keyframe editing. Use the editor UI for supported preset selection and property adjustments, or use CreativeEngine for programmatic setup and rendering. Use MP4 export when you need to preserve motion in the final output. ## Next Steps - [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) - Explore the types of animations supported by CE.SDK, including object, text, and transition effects. - [Create Animations](https://img.ly/docs/cesdk/android/animation/create-15cf50/) - Build animations manually or with presets to animate objects, text, and scenes within your design. - [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. - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame. - [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) - Learn how to create and customize videos in CE.SDK using scenes, assets, and time-based editing. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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: "Explore the types of animations supported by CE.SDK, including object, text, and transition effects." 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/) --- ## Animation Categories There are three different categories of animations: *In*, *Out* and *Loop* animations. ### In Animations *In* animations animate a block for a specified duration after the block first appears in the scene. For example, if a block has a time offset of 4s in the scene and it has an *In* animation with a duration of 1s, then the appearance of the block will be animated between 4s and 5s with the *In* animation. ### Out Animations *Out* animations animate a block for a specified duration before the block disappears from the scene. For example, if a block has a time offset of 4s in the scene and a duration of 5s and it has an *Out* animation with a duration of 1s, then the appearance of the block will be animated between 8s and 9s with the *Out* animation. ### Loop Animations *Loop* animations animate a block for the total duration that the block is visible in the scene. *Loop* animations also run simultaneously with *In* and *Out* animations, if those are present. ## Animation Presets We currently support the following *In* and *Out* animation presets: - `'//ly.img.ubq/animation/slide'` - `'//ly.img.ubq/animation/pan'` - `'//ly.img.ubq/animation/fade'` - `'//ly.img.ubq/animation/blur'` - `'//ly.img.ubq/animation/grow'` - `'//ly.img.ubq/animation/zoom'` - `'//ly.img.ubq/animation/pop'` - `'//ly.img.ubq/animation/wipe'` - `'//ly.img.ubq/animation/baseline'` - `'//ly.img.ubq/animation/crop_zoom'` - `'//ly.img.ubq/animation/spin'` - `'//ly.img.ubq/animation/ken_burns'` - `'//ly.img.ubq/animation/typewriter_text'` (text-only) - `'//ly.img.ubq/animation/block_swipe_text'` (text-only) - `'//ly.img.ubq/animation/merge_text'` (text-only) - `'//ly.img.ubq/animation/spread_text'` (text-only) and the following *Loop* animation types: - `'//ly.img.ubq/animation/spin_loop'` - `'//ly.img.ubq/animation/fade_loop'` - `'//ly.img.ubq/animation/blur_loop'` - `'//ly.img.ubq/animation/pulsating_loop'` - `'//ly.img.ubq/animation/breathing_loop'` - `'//ly.img.ubq/animation/jump_loop'` - `'//ly.img.ubq/animation/squeeze_loop'` - `'//ly.img.ubq/animation/sway_loop'` ## Animation Type Properties ## Baseline Type A text animation that slides text in along its baseline. This section describes the properties available for the **Baseline Type** (`//ly.img.ubq/animation/baseline`) block type. | Property | Type | Default | Description | | ------------------------------ | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/baseline/direction` | `Enum` | `"Up"` | The direction of the wipe animation., Possible values: `"Up"`, `"Right"`, `"Down"`, `"Left"` | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Block Swipe Text Type A text animation that reveals text with a colored block swiping across. This section describes the properties available for the **Block Swipe Text Type** (`//ly.img.ubq/animation/block_swipe_text`) block type. | Property | Type | Default | Description | | ----------------------------------------- | -------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `animation/block_swipe_text/blockColor` | `Color` | `{"r":0,"g":0,"b":0,"a":1}` | The overlay block color. | | `animation/block_swipe_text/direction` | `Enum` | `"Right"` | The direction of the block swipe animation., Possible values: `"Up"`, `"Right"`, `"Down"`, `"Left"` | | `animation/block_swipe_text/useTextColor` | `Bool` | `true` | Whether the overlay block should use the text color. | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Blur Type An animation that applies a blur effect over time. This section describes the properties available for the **Blur Type** (`//ly.img.ubq/animation/blur`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/blur/fade` | `Bool` | `true` | Whether an opacity fade animation should be applied during the blur animation. | | `animation/blur/intensity` | `Float` | `1` | The maximum intensity of the blur. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Blur Loop Type A looping animation that continuously applies a blur effect. This section describes the properties available for the **Blur Loop Type** (`//ly.img.ubq/animation/blur_loop`) block type. | Property | Type | Default | Description | | ------------------------------- | -------- | ------- | --------------------------------------------------------------- | | `animation/blur_loop/intensity` | `Float` | `1` | The maximum blur intensity of this effect. | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Breathing Loop Type A looping animation with a slow, breathing-like scale effect. This section describes the properties available for the **Breathing Loop Type** (`//ly.img.ubq/animation/breathing_loop`) block type. | Property | Type | Default | Description | | ------------------------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `animation/breathing_loop/intensity` | `Float` | `0` | Controls the intensity of the scaling. A value of 0 results in a maximum scale of 1.25. A value of 1 results in a maximum scale of 2.5. | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Crop Zoom Type An animation that zooms the content within the block's frame. This section describes the properties available for the **Crop Zoom Type** (`//ly.img.ubq/animation/crop_zoom`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/crop_zoom/fade` | `Bool` | `true` | Whether an opacity fade animation should be applied during the crop zoom animation. | | `animation/crop_zoom/scale` | `Float` | `1.25` | The maximum crop scale value. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Fade Type An animation that fades the block in or out. This section describes the properties available for the **Fade Type** (`//ly.img.ubq/animation/fade`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Fade Loop Type A looping animation that continuously fades the block in and out. This section describes the properties available for the **Fade Loop Type** (`//ly.img.ubq/animation/fade_loop`) block type. | Property | Type | Default | Description | | ------------------- | -------- | ------- | --------------------------------------------------------------- | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Grow Type An animation that scales the block up from a point. This section describes the properties available for the **Grow Type** (`//ly.img.ubq/animation/grow`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/grow/direction` | `Enum` | `"All"` | The direction from which the grow animation originates. Can be horizontal only, vertical only, from the center (all), or from any of the four corners (top-left, top-right, bottom-left, bottom-right)., Possible values: `"Horizontal"`, `"Vertical"`, `"All"` | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Jump Loop Type A looping animation with a jumping motion. This section describes the properties available for the **Jump Loop Type** (`//ly.img.ubq/animation/jump_loop`) block type. | Property | Type | Default | Description | | ------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------- | | `animation/jump_loop/direction` | `Enum` | `"Up"` | The direction of the jump animation., Possible values: `"Up"`, `"Right"`, `"Down"`, `"Left"` | | `animation/jump_loop/intensity` | `Float` | `0.5` | Controls how far the block should move as a percentage of its width or height. | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Ken Burns Type An animation that simulates the Ken Burns effect by panning and zooming on content. This section describes the properties available for the **Ken Burns Type** (`//ly.img.ubq/animation/ken_burns`) block type. | Property | Type | Default | Description | | ----------------------------------------- | -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/ken_burns/direction` | `Enum` | `"Right"` | The direction of the pan travel., Possible values: `"Up"`, `"Right"`, `"Down"`, `"Left"` | | `animation/ken_burns/fade` | `Bool` | `false` | Whether an opacity fade animation should be applied during the animation. | | `animation/ken_burns/travelDistanceRatio` | `Float` | `1` | The movement distance relative to the length of the crop. | | `animation/ken_burns/zoomIntensity` | `Float` | `0.5` | The factor by which to zoom in or out. | | `animationEasing` | `Enum` | `"EaseOutQuint"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `2.4` | The duration in seconds for which this block should be visible. | ## Merge Text Type A text animation where lines of text merge from opposite directions. This section describes the properties available for the **Merge Text Type** (`//ly.img.ubq/animation/merge_text`) block type. | Property | Type | Default | Description | | -------------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/merge_text/direction` | `Enum` | `"Left"` | The in-animation direction of the first line of text., Possible values: `"Right"`, `"Left"` | | `animation/merge_text/intensity` | `Float` | `0.5` | The intensity of the pan. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Pan Type An animation that pans the block across the view. This section describes the properties available for the **Pan Type** (`//ly.img.ubq/animation/pan`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/pan/direction` | `Float` | `0` | The movement direction of the animation in radians. | | `animation/pan/distance` | `Float` | `0.1` | The movement distance relative to the longer side of the page. | | `animation/pan/fade` | `Bool` | `true` | Whether an opacity fade animation should be applied during the pan animation. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Pop Type An animation that quickly scales the block up and down. This section describes the properties available for the **Pop Type** (`//ly.img.ubq/animation/pop`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Pulsating Loop Type A looping animation with a pulsating scale effect. This section describes the properties available for the **Pulsating Loop Type** (`//ly.img.ubq/animation/pulsating_loop`) block type. | Property | Type | Default | Description | | ------------------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `animation/pulsating_loop/intensity` | `Float` | `0` | Controls the intensity of the pulsating effect. A value of 0 results in a maximum scale of 1.25. A value of 1 results in a maximum scale of 2.5. | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | ## Slide Type An animation that slides the block into or out of view. This section describes the properties available for the **Slide Type** (`//ly.img.ubq/animation/slide`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/slide/direction` | `Float` | `0` | The movement direction angle of the slide animation in radians. | | `animation/slide/fade` | `Bool` | `false` | Whether an opacity fade animation should be applied during the slide animation. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Spin Type An animation that rotates the block. This section describes the properties available for the **Spin Type** (`//ly.img.ubq/animation/spin`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/spin/direction` | `Enum` | `"Clockwise"` | The direction of the spin animation., Possible values: `"Clockwise"`, `"CounterClockwise"` | | `animation/spin/fade` | `Bool` | `true` | Whether an opacity fade animation should be applied during the spin animation. | | `animation/spin/intensity` | `Float` | `1` | How far the animation should spin the block. 1.0 is a full rotation (360°). | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Spin Loop Type A looping animation that continuously rotates the block. This section describes the properties available for the **Spin Loop Type** (`//ly.img.ubq/animation/spin_loop`) block type. | Property | Type | Default | Description | | ------------------------------- | -------- | ------------- | ------------------------------------------------------------------------------------------ | | `animation/spin_loop/direction` | `Enum` | `"Clockwise"` | The direction of the spin animation., Possible values: `"Clockwise"`, `"CounterClockwise"` | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Spread Text Type A text animation where letters spread apart or come together. This section describes the properties available for the **Spread Text Type** (`//ly.img.ubq/animation/spread_text`) block type. | Property | Type | Default | Description | | --------------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/spread_text/fade` | `Bool` | `true` | Whether the text should fade in / out during the spread animation. | | `animation/spread_text/intensity` | `Float` | `0.5` | The intensity of the spread. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | ## Squeeze Loop Type A looping animation with a squeezing effect. This section describes the properties available for the **Squeeze Loop Type** (`//ly.img.ubq/animation/squeeze_loop`) block type. | Property | Type | Default | Description | | ------------------- | -------- | ------- | --------------------------------------------------------------- | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Sway Loop Type A looping animation with a swaying rotational motion. This section describes the properties available for the **Sway Loop Type** (`//ly.img.ubq/animation/sway_loop`) block type. | Property | Type | Default | Description | | ------------------------------- | -------- | ------- | ----------------------------------------------------------------------------------- | | `animation/sway_loop/intensity` | `Float` | `1` | The intensity of the animation. Defines the maximum sway angle between 15° and 45°. | | `playback/duration` | `Double` | `1.2` | The duration in seconds for which this block should be visible. | ## Typewriter Text Type A text animation that reveals text as if it's being typed. This section describes the properties available for the **Typewriter Text Type** (`//ly.img.ubq/animation/typewriter_text`) block type. | Property | Type | Default | Description | | ---------------------------------------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------- | | `animation/typewriter_text/writingStyle` | `Enum` | `"Character"` | Whether the text should appear one character or one word at a time., Possible values: `"Character"`, `"Word"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | ## Wipe Type An animation that reveals or hides the block with a wipe transition. This section describes the properties available for the **Wipe Type** (`//ly.img.ubq/animation/wipe`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/wipe/direction` | `Enum` | `"Right"` | The direction of the wipe animation., Possible values: `"Up"`, `"Right"`, `"Down"`, `"Left"` | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | ## Zoom Type An animation that scales the entire block. This section describes the properties available for the **Zoom Type** (`//ly.img.ubq/animation/zoom`) block type. | Property | Type | Default | Description | | --------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `animation/zoom/fade` | `Bool` | `true` | Whether an opacity fade animation should be applied during the zoom animation. | | `animationEasing` | `Enum` | `"Linear"` | The easing function to apply to the animation., Possible values: `"Linear"`, `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, `"EaseInQuart"`, `"EaseOutQuart"`, `"EaseInOutQuart"`, `"EaseInQuint"`, `"EaseOutQuint"`, `"EaseInOutQuint"`, `"EaseInBack"`, `"EaseOutBack"`, `"EaseInOutBack"`, `"EaseInSpring"`, `"EaseOutSpring"`, `"EaseInOutSpring"` | | `playback/duration` | `Double` | `0.6` | The duration in seconds for which this block should be visible. | | `textAnimationOverlap` | `Float` | `0.35` | The overlap factor for text animations. | | `textAnimationWritingStyle` | `Enum` | `"Line"` | The writing style for text animations (e.g., by character, by word)., Possible values: `"Block"`, `"Line"`, `"Character"`, `"Word"` | --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "API Reference" description: "Find out how to use the API of the CESDK." platform: android url: "https://img.ly/docs/cesdk/android/api-reference/overview-8f24e1/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [API Reference](https://img.ly/docs/cesdk/android/api-reference/overview-8f24e1/) --- For Android, the following packages are available: - [ly.img:engine](`$\{props.platform.slug}/api-reference/engine/ly.img[58]engine/ly.img.engine`) - [ly.img:engine-camera](`$\{props.platform.slug}/api-reference/engine-camera/ly.img[58]engine-camera/ly.img.engine.camera`) - [ly.img:editor](`$\{props.platform.slug}/api-reference/editor/ly.img[58]editor/ly.img.editor`) - [ly.img:editor-core](`$\{props.platform.slug}/api-reference/editor-core`) - [ly.img:camera](`$\{props.platform.slug}/api-reference/camera/ly.img[58]camera/ly.img.camera`) - [ly.img:camera-core](`$\{props.platform.slug}/api-reference/camera-core/ly.img[58]camera-core/ly.img.camera.core`) --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Automate Workflows" description: "Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale." platform: android url: "https://img.ly/docs/cesdk/android/automation-715209/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) --- ```kotlin file=@cesdk_android_examples/engine-guides-automate-workflows/AutomateWorkflows.kt reference-only package ly.img.editor.examples import android.app.Application import android.content.Context import android.graphics.BitmapFactory import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.defaultBaseUri import ly.img.engine.DefaultAssetSource 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.populateAssetSource 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, ) 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 { runAutomationWorkflow(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)) } } } } } } suspend fun runAutomationWorkflow( context: Context, license: String, ): AutomationResult = withContext(Dispatchers.Main) { val application = context.applicationContext as Application Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.automateWorkflows") engine.start( license = license, userId = "automation-guide", ) engine.bindOffscreen(width = 1080, height = 1350) val existingAssetSources = engine.asset.findAllSources().toSet() listOf( DefaultAssetSource.COLORS_DEFAULT_PALETTE.key, DefaultAssetSource.TYPEFACE.key, ).filterNot(existingAssetSources::contains) .forEach { assetSource -> engine.populateAssetSource( id = assetSource, jsonUri = defaultBaseUri.buildUpon() .appendPath(assetSource) .appendPath("content.json") .build(), replaceBaseUri = defaultBaseUri, ) } try { 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, ) } AutomationResult( variableKeys = engine.variable.findAll().sorted(), tokenizedBlockNames = tokenizedBlockNames, exportedFiles = exportedFiles, ) } finally { engine.stop() } } 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) 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." } 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, ) ``` ## Start the Headless Engine Once Start the Engine once for the whole workflow, bind an offscreen surface, and reuse that same instance throughout the batch. The example below follows the same asset-loading pattern as the Android Starter Kits: it calls `populateAssetSource(...)` for each required default source instead of the deprecated `addDefaultAssetSources(...)` helper. ```kotlin highlight-android-start engine.start( license = license, userId = "automation-guide", ) engine.bindOffscreen(width = 1080, height = 1350) val existingAssetSources = engine.asset.findAllSources().toSet() listOf( DefaultAssetSource.COLORS_DEFAULT_PALETTE.key, DefaultAssetSource.TYPEFACE.key, ).filterNot(existingAssetSources::contains) .forEach { assetSource -> engine.populateAssetSource( id = assetSource, jsonUri = defaultBaseUri.buildUpon() .appendPath(assetSource) .appendPath("content.json") .build(), replaceBaseUri = defaultBaseUri, ) } ``` - The examples app passes its configured license into this guide screen before starting the Engine. - Initialize the Engine once in your application bootstrap before starting a headless engine instance. - 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 on the shared Engine instance, then calls `populateAssetSource(...)` only for missing sources so revisiting the screen does not add the same default palette and font assets twice. ## Build a Reusable Scene Contract The example creates its template scene in code so the workflow stays self-contained. In production, you would usually load the same structure from a saved `.scene` or archive instead. ```kotlin highlight-android-template val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1080F) engine.block.setHeight(page, value = 1350F) val background = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect)) val backgroundFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = backgroundFill, property = "fill/color/value", value = EngineColor.fromRGBA(r = 0.96F, g = 0.94F, b = 0.90F, a = 1F), ) engine.block.setFill(background, fill = backgroundFill) engine.block.setWidth(background, value = 1080F) engine.block.setHeight(background, value = 1350F) engine.block.appendChild(parent = page, child = background) val heroImage = engine.block.create(DesignBlockType.Graphic) engine.block.setName(heroImage, name = "hero-image") engine.block.setShape(heroImage, shape = engine.block.createShape(ShapeType.Rect)) val heroFill = engine.block.createFill(FillType.Image) engine.block.setString( block = heroFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_2.jpg", ) engine.block.setFill(heroImage, fill = heroFill) engine.block.setWidth(heroImage, value = 860F) engine.block.setHeight(heroImage, value = 720F) engine.block.setPositionX(heroImage, value = 110F) engine.block.setPositionY(heroImage, value = 100F) engine.block.appendChild(parent = page, child = heroImage) val copyPanel = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(copyPanel, shape = engine.block.createShape(ShapeType.Rect)) val copyPanelFill = engine.block.createFill(FillType.Color) engine.block.setColor( block = copyPanelFill, property = "fill/color/value", value = EngineColor.fromRGBA(r = 1F, g = 1F, b = 1F, a = 0.92F), ) engine.block.setFill(copyPanel, fill = copyPanelFill) engine.block.setWidth(copyPanel, value = 860F) engine.block.setHeight(copyPanel, value = 360F) engine.block.setPositionX(copyPanel, value = 110F) engine.block.setPositionY(copyPanel, value = 860F) engine.block.appendChild(parent = page, child = copyPanel) val headline = engine.block.create(DesignBlockType.Text) engine.block.setName(headline, name = "headline-copy") engine.block.setString(headline, property = "text/text", value = "{{headline}}") engine.block.setTextFontSize(headline, fontSize = 14F) engine.block.setTextColor( headline, color = EngineColor.fromRGBA(r = 0.12F, g = 0.10F, b = 0.15F, a = 1F), ) engine.block.setWidth(headline, value = 700F) engine.block.setWidthMode(headline, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(headline, mode = SizeMode.AUTO) engine.block.setBoolean(headline, property = "text/clipLinesOutsideOfFrame", value = false) engine.block.setPositionX(headline, value = 160F) engine.block.setPositionY(headline, value = 915F) engine.block.appendChild(parent = page, child = headline) val subline = engine.block.create(DesignBlockType.Text) engine.block.setName(subline, name = "subline-copy") engine.block.setString(subline, property = "text/text", value = "{{subline}}") engine.block.setTextFontSize(subline, fontSize = 8F) engine.block.setTextColor( subline, color = EngineColor.fromRGBA(r = 0.28F, g = 0.24F, b = 0.32F, a = 1F), ) engine.block.setWidth(subline, value = 700F) engine.block.setWidthMode(subline, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(subline, mode = SizeMode.AUTO) engine.block.setBoolean(subline, property = "text/clipLinesOutsideOfFrame", value = false) engine.block.setPositionX(subline, value = 160F) engine.block.setPositionY(subline, value = 1000F) engine.block.appendChild(parent = page, child = subline) val cta = engine.block.create(DesignBlockType.Text) engine.block.setName(cta, name = "cta-copy") engine.block.setString(cta, property = "text/text", value = "{{cta}}") engine.block.setTextFontSize(cta, fontSize = 9F) engine.block.setTextColor( cta, color = EngineColor.fromRGBA(r = 0.16F, g = 0.29F, b = 0.82F, a = 1F), ) engine.block.setWidth(cta, value = 700F) engine.block.setWidthMode(cta, mode = SizeMode.ABSOLUTE) engine.block.setHeightMode(cta, mode = SizeMode.AUTO) engine.block.setBoolean(cta, property = "text/clipLinesOutsideOfFrame", value = false) engine.block.setPositionX(cta, value = 160F) engine.block.setPositionY(cta, value = 1090F) engine.block.appendChild(parent = page, child = cta) val serializedTemplate = engine.scene.saveToString(scene = scene) ``` This template contract does two important things: - Text blocks contain `{{headline}}`, `{{subline}}`, and `{{cta}}` tokens. Those tokens resolve against the Engine’s variable store at render time. - The hero image block is named `hero-image`, giving the automation step a stable handle for media replacement. - A dedicated footer panel reserves readable copy space below the image, so the exported variants stay legible on-device even in evaluation mode. ## Validate What the Template Exposes On Android, `engine.variable.findAll()` only lists keys that are already present in the variable store. It does not discover `{{token}}` references directly from the scene. Before you run a batch, keep the expected variable keys in your own app contract and use block inspection to verify which named blocks still reference variables. ```kotlin highlight-android-discover-slots return engine.block.findAll() .filter { block -> engine.block.referencesAnyVariables(block) } .map { block -> engine.block.getName(block) } .filter(String::isNotBlank) .sorted() ``` This gives you a lightweight structure check without mutating the scene. It is especially useful when designers iterate on a template and you want a fast sanity check before exporting a larger batch. ## Apply Record Data and Replace Media For each record, reload the reusable template, set the variable values, then update the named media slot. ```kotlin highlight-android-apply-data engine.scene.load( scene = templateScene, waitForResources = true, ) engine.variable.set(key = "headline", value = job.headline) engine.variable.set(key = "subline", value = job.subline) engine.variable.set(key = "cta", value = job.cta) val heroImage = engine.block.findByName(name = "hero-image").first() val heroFill = engine.block.getFill(heroImage) engine.block.setString( block = heroFill, property = "fill/image/imageFileURI", value = job.heroImageUri, ) engine.block.resetCrop(heroImage) ``` - Reloading the serialized template keeps each export isolated from the previous record. - Variable keys are case-sensitive. Treat them like part of your API contract between the template and your app. - `resetCrop()` reapplies the placeholder framing after a new image URI is assigned. ## Export Sequentially on Android Export the current page, write the buffer to disk, and move on to the next record. Keeping the pipeline sequential avoids unnecessary memory pressure on the device. ```kotlin highlight-android-export val exportData = engine.block.export( block = page, mimeType = MimeType.PNG, ) val outputFile = File(outputDirectory, "${job.fileStem}.png") withContext(Dispatchers.IO) { outputFile.outputStream().channel.use { channel -> while (exportData.hasRemaining()) { channel.write(exportData) } } } ``` The sample exports PNG previews because they are easy to inspect in-app. The same pattern works with `MimeType.JPEG` or `MimeType.PDF` when your downstream workflow expects a different output format. To process more than one record, keep the Engine alive and run the same steps in order: ```kotlin highlight-android-batch val exportedFiles = jobs.map { job -> exportAutomationJob( engine = engine, templateScene = templateScene, job = job, outputDirectory = outputDirectory, ) } ``` ## Add a Human Approval Step If a design still needs review, stop after populating the scene instead of exporting immediately. Serialize that populated scene with `engine.scene.saveToString(...)`, then open the saved scene in an editor flow. This keeps one template contract for both automated generation and manual approval. ## Next Steps - [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) – use the Engine directly when no prebuilt UI is needed. - [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) – repeat the same automation flow across many records. - [Create Templates](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) – design the reusable scenes your workflow populates. - [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) – manage the variable store and tokenized text safely. --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/automation/overview-34d971/) - Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale. - [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) - Documentation for Batch Processing - [Auto-Resize Blocks in Android (Kotlin)](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/) - Configure absolute, percent, and auto sizing modes to build responsive, content-driven layouts with the CE.SDK block API on Android. - [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) - Generate personalized designs from a single template by merging external data into CE.SDK scenes with variables and named placeholder blocks. - [Product Variations](https://img.ly/docs/cesdk/android/automation/product-variations-f3349f/) - Generate multiple product variants from a single template by swapping text, images and styles programmatically. - [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) - Create many image variants from structured data by interpolating content into reusable design templates. - [Actions](https://img.ly/docs/cesdk/android/automation/actions-c67fee/) - Trigger editor-driven automation flows on Android with editor callbacks and events. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Actions" description: "Trigger editor-driven automation flows on Android with editor callbacks and events." platform: android url: "https://img.ly/docs/cesdk/android/automation/actions-c67fee/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one 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/) > [Actions](https://img.ly/docs/cesdk/android/automation/actions-c67fee/) --- ```kotlin file=@cesdk_android_examples/editor-guides-automation-actions/AutomationActionsEditorSolution.kt reference-only import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.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 androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.editor.Editor import ly.img.editor.core.component.EditorComponent import ly.img.editor.core.component.remember import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.event.EditorEvent import ly.img.engine.DesignBlockType import ly.img.engine.MimeType import ly.img.engine.SizeMode import java.io.File import java.nio.ByteBuffer import java.util.UUID data class AutomationActionsState( val isExporting: Boolean = false, val exportedFileName: String? = null, val errorMessage: String? = null, ) object ShowExportProgress : EditorEvent data class ExportFinished( val fileName: String, ) : EditorEvent data class ExportFailed( val message: String, ) : EditorEvent @Composable fun AutomationActionsEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { var state by remember { mutableStateOf(AutomationActionsState()) } 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 title = editorContext.engine.block.create(DesignBlockType.Text) editorContext.engine.block.setWidthMode(title, mode = SizeMode.AUTO) editorContext.engine.block.setHeightMode(title, mode = SizeMode.AUTO) editorContext.engine.block.replaceText(title, text = "Quarterly Report") editorContext.engine.block.appendChild(parent = page, child = title) editorContext.engine.scene.zoomToBlock(page) } } onExport = { editorContext.eventHandler.send(ShowExportProgress) runCatching { val buffer = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) writeToTempFile( byteBuffer = buffer, directory = { editorContext.activity.cacheDir }, ) }.onSuccess { file -> editorContext.eventHandler.send(ExportFinished(file.name)) }.onFailure { throwable -> editorContext.eventHandler.send( ExportFailed(throwable.message ?: throwable.toString()), ) } } onEvent = { event -> when (event) { is EditorEvent.Export.Start -> { state = state.copy(exportedFileName = null, errorMessage = null) } is ShowExportProgress -> { state = state.copy( isExporting = true, exportedFileName = null, errorMessage = null, ) } is ExportFinished -> { state = state.copy( isExporting = false, exportedFileName = event.fileName, ) } is ExportFailed -> { state = state.copy( isExporting = false, errorMessage = event.message, ) } } } overlay = { EditorComponent.remember { decoration = { Box( modifier = Modifier.fillMaxSize(), ) { Button( modifier = Modifier .align(Alignment.BottomCenter) .padding(24.dp), enabled = !state.isExporting, onClick = { editorContext.eventHandler.send(EditorEvent.Export.Start()) }, ) { Text("Export Document") } } if (state.isExporting) { Dialog( onDismissRequest = {}, properties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false, ), ) { CircularProgressIndicator() } } state.exportedFileName?.let { fileName -> AlertDialog( onDismissRequest = { state = state.copy(exportedFileName = null) }, title = { Text("Automation complete") }, text = { Text("Created $fileName in the app cache directory.") }, confirmButton = { TextButton( onClick = { state = state.copy(exportedFileName = null) }, ) { Text("OK") } }, ) } state.errorMessage?.let { errorMessage -> AlertDialog( onDismissRequest = { state = state.copy(errorMessage = null) }, title = { Text("Automation failed") }, text = { Text(errorMessage) }, confirmButton = { TextButton( onClick = { state = state.copy(errorMessage = null) }, ) { Text("OK") } }, ) } } } } } }, onClose = onClose, ) } private suspend fun writeToTempFile( byteBuffer: ByteBuffer, directory: () -> File, mimeType: MimeType = MimeType.PDF, ): File = withContext(Dispatchers.IO) { val extension = mimeType.key.substringAfterLast('/') File .createTempFile(UUID.randomUUID().toString(), ".$extension", directory()) .apply { outputStream().use { it.channel.write(byteBuffer) } } } ``` On Android, user-triggered automation in the prebuilt editor is wired through `EditorConfiguration` callbacks and `EditorEvent`s. Use `onExport` to run the automated step, then route progress and results back into the UI through `onEvent`. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-automation-actions) ## What You'll Learn - Prepare a reusable scene in `onCreate` for an editor-driven automation workflow. - Trigger an automated PDF export by sending `EditorEvent.Export.Start()` from your own UI. - Send progress, success, and failure back into the editor overlay with custom `EditorEvent`s. ## Automation Hooks on Android Android's prebuilt editor exposes action-style integration points through `EditorConfiguration`: | Hook | When it runs | Typical automation use | | --- | --- | --- | | `onCreate` | When the editor and engine are created | Load or build the scene, register asset sources, and apply editor settings. | | `onExport` | When an export action dispatches `EditorEvent.Export.Start()` | Validate the design, export it, upload it, or hand it to another workflow. | | `onUpload` | After the user selects a file for an upload asset source | Replace temporary local URIs with permanent server-backed URIs. | | `onEvent` | Whenever an internal or custom `EditorEvent` is sent | Update overlay state, analytics, or follow-up workflow steps. | > **Note:** The default `onExport` implementation only logs a warning. If the editor > should do anything useful when an export action runs, provide your own > callback. ## Prepare the Scene Create or reuse the scene in `onCreate`. The callback can run again after process recreation, so check whether a scene already exists before rebuilding it. ```kotlin highlight-actions-onCreate 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 title = editorContext.engine.block.create(DesignBlockType.Text) editorContext.engine.block.setWidthMode(title, mode = SizeMode.AUTO) editorContext.engine.block.setHeightMode(title, mode = SizeMode.AUTO) editorContext.engine.block.replaceText(title, text = "Quarterly Report") editorContext.engine.block.appendChild(parent = page, child = title) editorContext.engine.scene.zoomToBlock(page) } } ``` The sample creates a single square page and a text block so the export flow has deterministic content. ## Trigger Automation from UI Actions Use a visible overlay action to send `EditorEvent.Export.Start()`. The editor's built-in export button sends the same event, so the pattern stays the same if you later wire the automation flow to the default navigation bar. ```kotlin highlight-actions-trigger-export Button( modifier = Modifier .align(Alignment.BottomCenter) .padding(24.dp), enabled = !state.isExporting, onClick = { editorContext.eventHandler.send(EditorEvent.Export.Start()) }, ) { Text("Export Document") } ``` That event then invokes `EditorConfiguration.onExport`, which is where the automation step should run. ```kotlin highlight-actions-onExport onExport = { editorContext.eventHandler.send(ShowExportProgress) runCatching { val buffer = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) writeToTempFile( byteBuffer = buffer, directory = { editorContext.activity.cacheDir }, ) }.onSuccess { file -> editorContext.eventHandler.send(ExportFinished(file.name)) }.onFailure { throwable -> editorContext.eventHandler.send( ExportFailed(throwable.message ?: throwable.toString()), ) } } ``` This callback: - Runs in response to `EditorEvent.Export.Start()` dispatched from the UI. - Exports the current scene to PDF with `block.export(...)`. - Sends success or failure back through custom events instead of mutating UI state directly inside the export branch. ```kotlin highlight-actions-write-to-temp-file private suspend fun writeToTempFile( byteBuffer: ByteBuffer, directory: () -> File, mimeType: MimeType = MimeType.PDF, ): File = withContext(Dispatchers.IO) { val extension = mimeType.key.substringAfterLast('/') File .createTempFile(UUID.randomUUID().toString(), ".$extension", directory()) .apply { outputStream().use { it.channel.write(byteBuffer) } } } ``` The exported file is written to the app cache directory so your app can upload it, share it, or move it into permanent storage. Like the other editor callbacks, `onExport` runs in a coroutine that survives configuration changes and is cancelled only when the editor closes. ## Reflect Progress in the UI Keep lightweight Compose state for the overlay, define custom events for the automation flow, and reduce those events in `onEvent`. ```kotlin highlight-actions-state-class data class AutomationActionsState( val isExporting: Boolean = false, val exportedFileName: String? = null, val errorMessage: String? = null, ) ``` ```kotlin highlight-actions-state var state by remember { mutableStateOf(AutomationActionsState()) } ``` ```kotlin highlight-actions-custom-events object ShowExportProgress : EditorEvent data class ExportFinished( val fileName: String, ) : EditorEvent data class ExportFailed( val message: String, ) : EditorEvent ``` ```kotlin highlight-actions-onEvent onEvent = { event -> when (event) { is EditorEvent.Export.Start -> { state = state.copy(exportedFileName = null, errorMessage = null) } is ShowExportProgress -> { state = state.copy( isExporting = true, exportedFileName = null, errorMessage = null, ) } is ExportFinished -> { state = state.copy( isExporting = false, exportedFileName = event.fileName, ) } is ExportFailed -> { state = state.copy( isExporting = false, errorMessage = event.message, ) } } } ``` In the sample, the overlay reacts to that state by showing a blocking progress spinner while export runs, then a result or error dialog when the automated step finishes. ```kotlin highlight-actions-overlay overlay = { EditorComponent.remember { decoration = { Box( modifier = Modifier.fillMaxSize(), ) { Button( modifier = Modifier .align(Alignment.BottomCenter) .padding(24.dp), enabled = !state.isExporting, onClick = { editorContext.eventHandler.send(EditorEvent.Export.Start()) }, ) { Text("Export Document") } } if (state.isExporting) { Dialog( onDismissRequest = {}, properties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false, ), ) { CircularProgressIndicator() } } state.exportedFileName?.let { fileName -> AlertDialog( onDismissRequest = { state = state.copy(exportedFileName = null) }, title = { Text("Automation complete") }, text = { Text("Created $fileName in the app cache directory.") }, confirmButton = { TextButton( onClick = { state = state.copy(exportedFileName = null) }, ) { Text("OK") } }, ) } state.errorMessage?.let { errorMessage -> AlertDialog( onDismissRequest = { state = state.copy(errorMessage = null) }, title = { Text("Automation failed") }, text = { Text(errorMessage) }, confirmButton = { TextButton( onClick = { state = state.copy(errorMessage = null) }, ) { Text("OK") } }, ) } } } } ``` ## Next Steps - [Automation Overview](https://img.ly/docs/cesdk/android/automation/overview-34d971/) - map this editor-driven pattern to the rest of CE.SDK's automation workflows. - [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - customize the exported format and add PDF-specific options. - [UI Events](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) - learn the full lifecycle of editor callbacks and event handling 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: "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 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch 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, ) fun autoResize( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): Job = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.autoResize") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) try { runAutoResizeGuide(engine) } finally { engine.stop() } } 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") 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. ## Initialize the engine 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. - [Multi-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 android.app.Application 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.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import ly.img.engine.SizeMode data class MergeRecord( val fullName: String, val jobTitle: String, val email: String, val photoUri: String, ) data class MergedCard( val fileName: String, val pngBytes: ByteArray, ) suspend fun mergeBusinessCards( application: Application, license: String?, userId: String, ): List = withContext(Dispatchers.Main) { Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.data-merge-guide") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1050, height = 600) try { 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) val mergedCards = mutableListOf() for (record in records) { engine.variable.findAll().forEach { key -> engine.variable.remove(key) } 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) val pngBuffer = engine.block.export(exportPage, mimeType = MimeType.PNG) val pngBytes = ByteArray(pngBuffer.remaining()) pngBuffer.get(pngBytes) mergedCards += MergedCard( fileName = record.fullName.lowercase().replace(" ", "-") + ".png", pngBytes = pngBytes, ) } return@withContext mergedCards } finally { engine.stop() } } ``` 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. ## Initialize the Engine We start by initializing the headless Creative Engine. In production, replace the evaluation-mode `license = null` value with your own key. ```kotlin highlight-setup engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1050, height = 600) ``` ## 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-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-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-batch-loop for (record in records) { engine.variable.findAll().forEach { key -> engine.variable.remove(key) } 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-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-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-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-export val pngBuffer = engine.block.export(exportPage, mimeType = MimeType.PNG) val pngBytes = ByteArray(pngBuffer.remaining()) pngBuffer.get(pngBytes) mergedCards += MergedCard( fileName = record.fullName.lowercase().replace(" ", "-") + ".png", pngBytes = pngBytes, ) ``` You can switch the `MimeType` to JPEG, WebP, or PDF if your batch job targets different delivery channels. ## Cleanup Resources Always stop the engine when the batch completes. Wrapping cleanup in a `finally` block ensures the engine shuts down even if one record fails. ```kotlin highlight-cleanup engine.stop() ``` ## 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 | | `engine.stop()` | Release the engine after the batch run finishes | ## Next Steps - [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) — Automate generation of multiple designs from a template in a loop. - [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) — Templates enable dynamic, reusable designs with text variables and placeholder media. Learn to create, load, and personalize templates programmatically. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Multiple Image Generation" description: "Create many image variants from structured data by interpolating content into reusable design templates." platform: android url: "https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/android/automation-715209/) > [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) --- Generate image variants, such as square, portrait, or landscape layouts, from a single data record using the CreativeEditor SDK's Engine API. This pattern lets you populate templates programmatically with text, images, and colors to create consistent, on-brand designs across all formats. ## What You'll Learn - Load multiple templates into CE.SDK and populate them with structured data. - Replace text and image placeholders dynamically using variables and named blocks. - Apply consistent brand color themes across scenes. - Export each variant as PNG, JPEG, or PDF. - Build efficient workflows for generating multiple format variations. ## When to Use It Use multi-image generation when a single record (like a restaurant listing or product) needs to produce multiple layout variants. For larger datasets with many records generating many images, refer to the [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) guide. ## Core Concepts **Templates and Instances**: Templates define reusable layout and placeholders. An instance is a populated version with specific data. Use `engine.scene.saveToString()` to serialize a template and `engine.scene.load(scene =)` to load it for processing. **Variables for Dynamic Text**: Define variables in your templates for fields like `RestaurantName` or `Rating`. Set them at runtime with `engine.variable.set(key =, value =)`. Use `engine.variable.findAll()` to verify available variable names. **Named Blocks for Image Replacement**: Name your image placeholders (for example, `RestaurantImage`, `Logo`). Retrieve them with `engine.block.findByName()`, access the fill with `getFill()`, then update its source URI using `setString(..., property = "fill/image/imageFileURI")`. Always reset the crop after replacing an image fill for proper framing. **Brand and Conditional Styling**: Use predictable block naming for elements such as star ratings. Apply color changes programmatically with `setColor` to visualize rating or brand status. **Sequential Template Processing**: Process each variant one at a time to reduce memory pressure and simplify export tracking. ## Prerequisites - CE.SDK for Android integrated through Gradle. - A valid license key. - Templates saved as `.scene` files in assets or available via URLs. - Template variables and named blocks prepared for population. ## Initialize the Engine ```kotlin import ly.img.engine.Engine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch fun makeEngine(license: String, userId: String) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.multiimage") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) engine.addDefaultAssetSources() } ``` ## 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 [Custom Asset Sources](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/) guide for setup examples. **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](https://img.ly/docs/cesdk/android/import-media/overview-84bb23/). --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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 this table to find the closest Android guide for common automation workflows. Published Android guides are linked below. Draft-only topics are listed without a link. | 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 | | Generate platform-specific size variants | Product Variations | | Resize designs for different aspect ratios | [Auto-Resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/) | | Build scenes programmatically | Automate Design Generation | | Generate multiple images per design | [Multiple Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) | | Trigger automation from the editor UI | 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: "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, ) suspend fun productVariations( context: Context, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): List = withContext(Dispatchers.Main) { val engine = Engine.getInstance(id = "ly.img.engine.product-variations") try { engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1080) 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) 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 } exportedFiles } finally { engine.stop() } } ``` 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 [Multi-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-productVariations-dataModel 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-productVariations-createTemplate 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-productVariations-discoverVariables 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-productVariations-generateLoop 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-productVariations-setVariables 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-productVariations-replaceImage 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-productVariations-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. - [Multi-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. - [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. - [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 solid colors, gradients, and fills to blocks. - [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 Color](https://img.ly/docs/cesdk/android/colors/apply-2211e3/) --- ```kotlin file=@cesdk_android_examples/engine-guides-colors/Colors.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.CMYKColor import ly.img.engine.Color import ly.img.engine.ColorSpace import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.RGBAColor import ly.img.engine.ShapeType fun colors( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) try { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(block, value = 350F) engine.block.setPositionY(block, value = 400F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) engine.block.appendChild(parent = page, child = block) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block, fill = fill) val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F) val cmykRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 1F) val cmykPartialRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 0.5F) val spotPinkFlamingo = Color.fromSpotColor( name = "Pink-Flamingo", tint = 1F, externalReference = "Brand-Colors", ) val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F) engine.editor.setSpotColor( name = "Pink-Flamingo", Color.fromRGBA(r = 0.988F, g = 0.455F, b = 0.992F), ) engine.editor.setSpotColor(name = "Yellow", Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F)) val colorFill = engine.block.getFill(block) // Fill colors use the generic color property path for RGB, CMYK, and spot colors. engine.block.setColor( colorFill, property = "fill/color/value", value = rgbaBlue, ) engine.block.setColor( colorFill, property = "fill/color/value", value = cmykRed, ) engine.block.setColor( colorFill, property = "fill/color/value", value = spotPinkFlamingo, ) val currentFillColor = engine.block.getColor( colorFill, property = "fill/color/value", ) check(currentFillColor == spotPinkFlamingo) engine.block.setStrokeEnabled(block, enabled = true) engine.block.setStrokeWidth(block, width = 8F) engine.block.setStrokeColor(block, color = cmykPartialRed) val currentStrokeColor = engine.block.getStrokeColor(block) check(currentStrokeColor == cmykPartialRed) engine.block.setDropShadowEnabled(block, enabled = true) engine.block.setDropShadowOffsetX(block, offsetX = 12F) engine.block.setDropShadowOffsetY(block, offsetY = 12F) engine.block.setDropShadowColor(block, color = spotPartialYellow) val currentShadowColor = engine.block.getDropShadowColor(block) check(currentShadowColor == spotPartialYellow) val cmykBlueConverted = engine.editor.convertColorToColorSpace( color = rgbaBlue, colorSpace = ColorSpace.CMYK, ) val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace( color = spotPinkFlamingo, colorSpace = ColorSpace.SRGB, ) check(cmykBlueConverted is CMYKColor) check(rgbaPinkFlamingoConverted is RGBAColor) val definedSpotColors = engine.editor.findAllSpotColors() check("Pink-Flamingo" in definedSpotColors) check("Yellow" in definedSpotColors) engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F)) val updatedYellow = engine.editor.getSpotColorCMYK("Yellow") check(updatedYellow.c == 0.2F) engine.editor.removeSpotColor("Yellow") check("Yellow" !in engine.editor.findAllSpotColors()) } finally { engine.stop() } } ``` Apply solid colors to design elements like shapes, text, and backgrounds using CE.SDK's Android Engine API. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors) Colors in CE.SDK are applied to block properties like fill, stroke, and shadow. The Android Engine API supports sRGB for screen display, CMYK for print production, and spot colors for specialized printing workflows. This guide covers how to create Android color objects, apply them to fill, stroke, and shadow properties, manage spot color definitions, and convert colors to sRGB or CMYK. ## Create Color Objects CE.SDK represents each color space with a dedicated Android color type. Create RGB, CMYK, and spot color values that match the target output. ```kotlin highlight-android-create-colors val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F) val cmykRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 1F) val cmykPartialRed = Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F, tint = 0.5F) val spotPinkFlamingo = Color.fromSpotColor( name = "Pink-Flamingo", tint = 1F, externalReference = "Brand-Colors", ) val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F) ``` RGB colors use red, green, blue, and alpha values from `0.0` to `1.0`. CMYK colors use cyan, magenta, yellow, black, and tint values from `0.0` to `1.0`. Spot colors reference a named spot color definition and can include an external reference for the color source. ## Define Spot Colors Before applying a spot color, define its screen preview approximation. The engine needs this approximation because spot colors represent inks that cannot be rendered directly on screen. ```kotlin highlight-android-define-spot engine.editor.setSpotColor( name = "Pink-Flamingo", Color.fromRGBA(r = 0.988F, g = 0.455F, b = 0.992F), ) engine.editor.setSpotColor(name = "Yellow", Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F)) ``` Use `engine.editor.setSpotColor()` with either `Color.fromRGBA()` or `Color.fromCMYK()` to define the approximation. Reuse the same name to update an existing spot color definition. ## Apply Fill Colors To set a block's fill color, get the fill block with `engine.block.getFill()`, then set `"fill/color/value"` on that fill block. ```kotlin highlight-android-apply-fill val colorFill = engine.block.getFill(block) // Fill colors use the generic color property path for RGB, CMYK, and spot colors. engine.block.setColor( colorFill, property = "fill/color/value", value = rgbaBlue, ) engine.block.setColor( colorFill, property = "fill/color/value", value = cmykRed, ) engine.block.setColor( colorFill, property = "fill/color/value", value = spotPinkFlamingo, ) val currentFillColor = engine.block.getColor( colorFill, property = "fill/color/value", ) ``` The fill is a separate block from the graphic block. Use `engine.block.getColor()` with the same property path when you need to read the current fill color. ## Apply Stroke Colors Stroke colors are applied directly to the design block. Enable the stroke first, set its width, and then set the stroke color. ```kotlin highlight-android-apply-stroke engine.block.setStrokeEnabled(block, enabled = true) engine.block.setStrokeWidth(block, width = 8F) engine.block.setStrokeColor(block, color = cmykPartialRed) val currentStrokeColor = engine.block.getStrokeColor(block) ``` The Android API exposes typed helpers for stroke color, so this path does not need a raw property string. ## Apply Shadow Colors Drop shadow colors are also applied directly to the design block. Enable the shadow and configure its offset before setting the color. ```kotlin highlight-android-apply-shadow engine.block.setDropShadowEnabled(block, enabled = true) engine.block.setDropShadowOffsetX(block, offsetX = 12F) engine.block.setDropShadowOffsetY(block, offsetY = 12F) engine.block.setDropShadowColor(block, color = spotPartialYellow) val currentShadowColor = engine.block.getDropShadowColor(block) ``` Spot colors work with shadows the same way they work with fills and strokes. ## Convert Between Color Spaces Use `engine.editor.convertColorToColorSpace()` when you need a color value in sRGB or CMYK. ```kotlin highlight-android-convert-color val cmykBlueConverted = engine.editor.convertColorToColorSpace( color = rgbaBlue, colorSpace = ColorSpace.CMYK, ) val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace( color = spotPinkFlamingo, colorSpace = ColorSpace.SRGB, ) ``` Pass the source color and either `ColorSpace.SRGB` or `ColorSpace.CMYK` as the target. `ColorSpace.SPOT_COLOR` is not a valid conversion target. Spot colors can still be source colors; the engine converts them from their registered approximation to sRGB or CMYK. Color conversions are approximations because RGB, CMYK, and spot colors use different color models and may not represent the same colors exactly. Some vibrant sRGB colors may appear muted when converted to CMYK. ## List Defined Spot Colors Query all spot colors currently registered in the engine with `engine.editor.findAllSpotColors()`. ```kotlin highlight-android-list-spot val definedSpotColors = engine.editor.findAllSpotColors() ``` The returned list contains the spot color names defined through `engine.editor.setSpotColor()`. ## Update Spot Color Definitions Redefine a spot color by calling `engine.editor.setSpotColor()` with the same name and a new approximation. ```kotlin highlight-android-update-spot engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F)) val updatedYellow = engine.editor.getSpotColorCMYK("Yellow") ``` Blocks that reference that spot color use the updated approximation the next time they render. ## Remove Spot Color Definitions Remove a spot color definition with `engine.editor.removeSpotColor()`. ```kotlin highlight-android-remove-spot engine.editor.removeSpotColor("Yellow") ``` Blocks that still reference the removed spot color fall back to the default magenta approximation. ## Troubleshooting ### Spot Color Appears Magenta The spot color was not defined before use. Call `engine.editor.setSpotColor()` with the exact spot color name before applying it to blocks. ### Stroke or Shadow Color Is Not Visible The effect is not enabled. Call `engine.block.setStrokeEnabled(block, true)` or `engine.block.setDropShadowEnabled(block, true)` before setting the color. ### Color Looks Different After Conversion Color space conversions are approximations. CMYK has a smaller gamut than sRGB, so vibrant colors can appear muted after conversion. ### Fill Color Does Not Change Apply colors to the fill block returned by `engine.block.getFill()`, not to the parent design block. ## API Reference | Method | Description | |--------|-------------| | `engine.block.setColor(block=_, property="fill/color/value", value=_)` | Set a fill color that can preserve RGB, CMYK, or spot color values | | `engine.block.getColor(block=_, property="fill/color/value")` | Read a color property from a fill block | | `engine.block.getFill(block=_)` | Get the fill block of a design block | | `engine.block.setStrokeEnabled(block=_, enabled=_)` | Enable or disable stroke on a design block | | `engine.block.setStrokeWidth(block=_, width=_)` | Set the stroke width | | `engine.block.setStrokeColor(block=_, color=_)` | Set the stroke color | | `engine.block.getStrokeColor(block=_)` | Read the stroke color | | `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable or disable the drop shadow | | `engine.block.setDropShadowOffsetX(block=_, offsetX=_)` | Set the drop shadow x offset | | `engine.block.setDropShadowOffsetY(block=_, offsetY=_)` | Set the drop shadow y offset | | `engine.block.setDropShadowColor(block=_, color=_)` | Set the drop shadow color | | `engine.block.getDropShadowColor(block=_)` | Read the drop shadow color | | `engine.editor.setSpotColor(name=_, color=_)` | Define or update a spot color approximation | | `engine.editor.findAllSpotColors()` | List all defined spot color names | | `engine.editor.getSpotColorCMYK(name=_)` | Read a spot color's CMYK approximation | | `engine.editor.removeSpotColor(name=_)` | Remove a spot color definition | | `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Convert a color to `ColorSpace.SRGB` or `ColorSpace.CMYK`; `ColorSpace.SPOT_COLOR` is not supported as a target | --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Color Basics" description: "Learn how color works in CE.SDK, including the three supported color spaces (sRGB, CMYK, and Spot) and when to use each for screen display or print workflows." platform: android url: "https://img.ly/docs/cesdk/android/colors/basics-307115/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [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 - [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Define and manage spot colors for specialized printing --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Color Conversion" description: "Learn how to convert colors between color spaces in CE.SDK. Convert sRGB, CMYK, and spot colors programmatically for screen display or print workflows." platform: android url: "https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Color Conversion](https://img.ly/docs/cesdk/android/colors/conversion-bcd82b/) --- ```kotlin file=@cesdk_android_examples/engine-guides-colors-conversion/ColorConversion.kt reference-only import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.CMYKColor import ly.img.engine.Color import ly.img.engine.ColorSpace import ly.img.engine.Engine import ly.img.engine.RGBAColor import ly.img.engine.SpotColor suspend fun colorConversion(engine: Engine): ColorConversionResult = withContext(Dispatchers.Main) { // Define a spot color with an RGB approximation for screen preview. engine.editor.setSpotColor( name = "Brand Red", color = Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F), ) val srgbColor = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F) val cmykColor = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 1F) val spotColor = Color.fromSpotColor( name = "Brand Red", tint = 1F, externalReference = "", ) val cmykToSrgb = engine.editor.convertColorToColorSpace( color = cmykColor, colorSpace = ColorSpace.SRGB, ) as RGBAColor val spotToSrgb = engine.editor.convertColorToColorSpace( color = spotColor, colorSpace = ColorSpace.SRGB, ) as RGBAColor println("CMYK converted to sRGB: $cmykToSrgb") println("Spot color converted to sRGB: $spotToSrgb") val srgbToCmyk = engine.editor.convertColorToColorSpace( color = srgbColor, colorSpace = ColorSpace.CMYK, ) as CMYKColor // Add a CMYK approximation before converting the spot color for print output. engine.editor.setSpotColor( name = "Brand Red", color = Color.fromCMYK(c = 0F, m = 0.85F, y = 0.9F, k = 0.05F, tint = 1F), ) val spotToCmyk = engine.editor.convertColorToColorSpace( color = spotColor, colorSpace = ColorSpace.CMYK, ) as CMYKColor println("sRGB converted to CMYK: $srgbToCmyk") println("Spot color converted to CMYK: $spotToCmyk") val detectedTypes = listOf(srgbColor, cmykColor, spotColor).map { color -> when (color) { is RGBAColor -> "sRGB" is CMYKColor -> "CMYK" is SpotColor -> "SpotColor" } } val transparentSrgb = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 0.5F) val transparentSrgbToCmyk = engine.editor.convertColorToColorSpace( color = transparentSrgb, colorSpace = ColorSpace.CMYK, ) as CMYKColor val tintedCmyk = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 0.5F) val tintedCmykPreview = engine.editor.convertColorToColorSpace( color = tintedCmyk, colorSpace = ColorSpace.SRGB, ) as RGBAColor val tintedSpotColor = Color.fromSpotColor( name = "Brand Red", tint = 0.4F, externalReference = "", ) val tintedSpotPreview = engine.editor.convertColorToColorSpace( color = tintedSpotColor, colorSpace = ColorSpace.SRGB, ) as RGBAColor val tintedSpotToCmyk = engine.editor.convertColorToColorSpace( color = tintedSpotColor, colorSpace = ColorSpace.CMYK, ) as CMYKColor val colorFromDesign: Color = spotColor val pickerPreviewColor = engine.editor.convertColorToColorSpace( color = colorFromDesign, colorSpace = ColorSpace.SRGB, ) as RGBAColor // Display pickerPreviewColor.r, pickerPreviewColor.g, pickerPreviewColor.b, and pickerPreviewColor.a. val colorForExport: Color = srgbColor val printColor = if (colorForExport is CMYKColor) { colorForExport } else { engine.editor.convertColorToColorSpace( color = colorForExport, colorSpace = ColorSpace.CMYK, ) as CMYKColor } ColorConversionResult( cmykToSrgb = cmykToSrgb, spotToSrgb = spotToSrgb, srgbToCmyk = srgbToCmyk, spotToCmyk = spotToCmyk, transparentSrgbToCmyk = transparentSrgbToCmyk, tintedCmykPreview = tintedCmykPreview, tintedSpotPreview = tintedSpotPreview, tintedSpotToCmyk = tintedSpotToCmyk, detectedTypes = detectedTypes, pickerPreviewColor = pickerPreviewColor, printColor = printColor, ) } ``` ```kotlin file=@cesdk_android_examples/engine-guides-colors-conversion/ColorConversionResult.kt reference-only import ly.img.engine.CMYKColor import ly.img.engine.RGBAColor data class ColorConversionResult( val cmykToSrgb: RGBAColor, val spotToSrgb: RGBAColor, val srgbToCmyk: CMYKColor, val spotToCmyk: CMYKColor, val transparentSrgbToCmyk: CMYKColor, val tintedCmykPreview: RGBAColor, val tintedSpotPreview: RGBAColor, val tintedSpotToCmyk: CMYKColor, val detectedTypes: List, val pickerPreviewColor: RGBAColor, val printColor: CMYKColor, ) ``` Convert colors between sRGB, CMYK, and spot color spaces programmatically in CE.SDK. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-colors-conversion) CE.SDK supports sRGB, CMYK, and SpotColor values. Color conversion is a programmatic Engine API on Android, so use it when you build custom color interfaces, show print values, or prepare colors before export. This guide covers converting colors to sRGB and CMYK, converting spot colors through their approximations, identifying Android color types, and checking how alpha and tint values are preserved. ## Supported Color Spaces CE.SDK works with these Android color value types: | Color Space | Android Type | Use Case | | --- | --- | --- | | **sRGB** | `RGBAColor` with `r`, `g`, `b`, `a` components from `0.0` to `1.0` | Screen display and previews | | **CMYK** | `CMYKColor` with `c`, `m`, `y`, `k`, `tint` components from `0.0` to `1.0` | Print workflows | | **SpotColor** | `SpotColor` with `name`, `tint`, and `externalReference` | Specialized printing with named inks | Use `ColorSpace.SRGB` or `ColorSpace.CMYK` as conversion targets. A `SpotColor` can be converted by using the RGB or CMYK approximation registered for its name. ## Setting Up Colors Before converting a spot color, register an approximation for the spot color name. Android uses the overloaded `setSpotColor(...)` API for both RGB and CMYK approximations. ```kotlin highlight-android-define-spot-color // Define a spot color with an RGB approximation for screen preview. engine.editor.setSpotColor( name = "Brand Red", color = Color.fromRGBA(r = 0.95F, g = 0.25F, b = 0.21F, a = 1F), ) ``` Create color values with the Android `Color` factory methods. The sample uses the same sRGB, CMYK, and spot color values throughout the conversion steps. ```kotlin highlight-android-create-colors val srgbColor = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F) val cmykColor = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 1F) val spotColor = Color.fromSpotColor( name = "Brand Red", tint = 1F, externalReference = "", ) ``` ## Converting to sRGB Use `engine.editor.convertColorToColorSpace(color, ColorSpace.SRGB)` when you need screen-display values. The returned value is a `Color`, so cast it to `RGBAColor` after converting to sRGB. ```kotlin highlight-android-convert-to-srgb val cmykToSrgb = engine.editor.convertColorToColorSpace( color = cmykColor, colorSpace = ColorSpace.SRGB, ) as RGBAColor val spotToSrgb = engine.editor.convertColorToColorSpace( color = spotColor, colorSpace = ColorSpace.SRGB, ) as RGBAColor println("CMYK converted to sRGB: $cmykToSrgb") println("Spot color converted to sRGB: $spotToSrgb") ``` CMYK colors convert to `RGBAColor` components. Spot colors use the registered RGB approximation for their spot color name. When the source color has a tint below `1.0`, the returned sRGB color keeps `a = 1.0` and blends its RGB components toward white. ## Converting to CMYK Use `engine.editor.convertColorToColorSpace(color, ColorSpace.CMYK)` when a print workflow needs CMYK components. For spot colors, register a CMYK approximation before converting to CMYK. ```kotlin highlight-android-convert-to-cmyk val srgbToCmyk = engine.editor.convertColorToColorSpace( color = srgbColor, colorSpace = ColorSpace.CMYK, ) as CMYKColor // Add a CMYK approximation before converting the spot color for print output. engine.editor.setSpotColor( name = "Brand Red", color = Color.fromCMYK(c = 0F, m = 0.85F, y = 0.9F, k = 0.05F, tint = 1F), ) val spotToCmyk = engine.editor.convertColorToColorSpace( color = spotColor, colorSpace = ColorSpace.CMYK, ) as CMYKColor println("sRGB converted to CMYK: $srgbToCmyk") println("Spot color converted to CMYK: $spotToCmyk") ``` > **Note:** Color space conversions may not be perfectly reversible. Some sRGB colors cannot be represented exactly in CMYK because the color gamuts differ. ## Identifying Color Types Android exposes color values as a sealed `Color` hierarchy. Use Kotlin type checks before reading type-specific properties. ```kotlin highlight-android-identify-types val detectedTypes = listOf(srgbColor, cmykColor, spotColor).map { color -> when (color) { is RGBAColor -> "sRGB" is CMYKColor -> "CMYK" is SpotColor -> "SpotColor" } } ``` This lets you branch on `RGBAColor`, `CMYKColor`, and `SpotColor` before reading their component properties. ## Handling Tint and Alpha On Android, alpha and tint are not simply copied between target fields. Non-black sRGB colors convert to CMYK with `tint = 1.0` even if the source has alpha, while pure black sRGB uses the source alpha as the CMYK tint. Tinted CMYK and spot colors convert to full-opacity sRGB previews, but their RGB components are blended toward white based on the tint. For example, pure CMYK red with `tint = 0.5` converts to a pink preview with `r = 1.0`, `g = 0.5`, `b = 0.5`, and `a = 1.0`. | Source | Target | Transformation | | --- | --- | --- | | sRGB alpha (non-black) | CMYK | The converted CMYK color uses `tint = 1.0` | | sRGB alpha (pure black) | CMYK | The converted CMYK color uses `tint = source alpha` | | CMYK tint | sRGB | RGB components blend toward white and `a = 1.0` | | SpotColor tint | sRGB | RGB components blend toward white and `a = 1.0` | | SpotColor tint | CMYK | The converted CMYK color uses `tint = source tint` | The sample below uses a non-black transparent sRGB color, so its converted CMYK color keeps `tint = 1.0`. ```kotlin highlight-android-handle-tint-alpha val transparentSrgb = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 0.5F) val transparentSrgbToCmyk = engine.editor.convertColorToColorSpace( color = transparentSrgb, colorSpace = ColorSpace.CMYK, ) as CMYKColor val tintedCmyk = Color.fromCMYK(c = 0F, m = 0.8F, y = 0.95F, k = 0F, tint = 0.5F) val tintedCmykPreview = engine.editor.convertColorToColorSpace( color = tintedCmyk, colorSpace = ColorSpace.SRGB, ) as RGBAColor val tintedSpotColor = Color.fromSpotColor( name = "Brand Red", tint = 0.4F, externalReference = "", ) val tintedSpotPreview = engine.editor.convertColorToColorSpace( color = tintedSpotColor, colorSpace = ColorSpace.SRGB, ) as RGBAColor val tintedSpotToCmyk = engine.editor.convertColorToColorSpace( color = tintedSpotColor, colorSpace = ColorSpace.CMYK, ) as CMYKColor ``` ## Practical Use Cases ### Building a Color Picker When a custom color picker needs screen preview values, convert the design color to sRGB and display the returned `RGBAColor` components. ```kotlin highlight-android-color-picker val colorFromDesign: Color = spotColor val pickerPreviewColor = engine.editor.convertColorToColorSpace( color = colorFromDesign, colorSpace = ColorSpace.SRGB, ) as RGBAColor // Display pickerPreviewColor.r, pickerPreviewColor.g, pickerPreviewColor.b, and pickerPreviewColor.a. ``` ### Export Preparation Before a print-oriented export, check whether the color is already CMYK. Convert non-CMYK colors to `ColorSpace.CMYK` before showing or storing print values. ```kotlin highlight-android-print-preparation val colorForExport: Color = srgbColor val printColor = if (colorForExport is CMYKColor) { colorForExport } else { engine.editor.convertColorToColorSpace( color = colorForExport, colorSpace = ColorSpace.CMYK, ) as CMYKColor } ``` ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Spot color converts to an unexpected value | The spot color has no approximation for the target color space | Call `setSpotColor(...)` with an `RGBAColor` or `CMYKColor` before conversion | | Colors differ after round-tripping | Color conversion is not always lossless | Avoid assuming that converting sRGB to CMYK and back returns the exact original value | | Type-specific properties are unavailable | `convertColorToColorSpace(...)` returns the base `Color` type | Cast after converting, or check the type with Kotlin `is` checks | ## API Reference | API | Purpose | | --- | --- | | `engine.editor.convertColorToColorSpace(color=_, colorSpace=_)` | Converts a color to `ColorSpace.SRGB` or `ColorSpace.CMYK` | | `engine.editor.setSpotColor(name=_, color=Color.fromRGBA(r=_, g=_, b=_, a=_))` | Defines or updates an RGB approximation for a spot color | | `engine.editor.setSpotColor(name=_, color=Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_))` | Defines or updates a CMYK approximation for a spot color | | `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Creates an sRGB color value | | `Color.fromCMYK(c=_, m=_, y=_, k=_, tint=_)` | Creates a CMYK color value | | `Color.fromSpotColor(name=_, tint=_, externalReference=_)` | Creates a spot color reference that uses the registered approximation for its name | --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create a Color Palette" description: "Build reusable color palettes to maintain consistency and streamline user choices." platform: android url: "https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) > [Create a Color Palette](https://img.ly/docs/cesdk/android/colors/create-color-palette-7012e0/) --- ```kotlin file=@cesdk_android_examples/editor-guides-configuration-color-palette/ColorPaletteEditorSolution.kt reference-only import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import ly.img.editor.Editor import ly.img.editor.core.component.InspectorBar import ly.img.editor.core.component.remember import ly.img.editor.core.component.rememberFillStroke import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.engine.AssetColor import ly.img.engine.AssetDefinition import ly.img.engine.AssetPayload import ly.img.engine.DesignBlockType import ly.img.engine.Engine import 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) } private fun selectFirstPageForDemo(engine: Engine) { // Select the page so the Fill/Stroke inspector button is visible immediately. engine.block.findByType(DesignBlockType.Page) .firstOrNull() ?.let { engine.block.setSelected(it, selected = true) } } // 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 = { selectFirstPageForDemo(editorContext.engine) } 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.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 - [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: "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 - [Spot Colors](https://img.ly/docs/cesdk/android/colors/for-print/spot-c3a150/) - Learn how to define spot colors and set their color approximation in the CreativeEditor 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: "Spot Colors" description: "Learn how to define spot colors and set their color approximation in the CreativeEditor SDK." 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 reference-only // Create a spot color with an RGB color approximation. engine.editor.setSpotColor("Red", Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F)) // Create a spot color with a CMYK color approximation. // Add a CMYK approximation to the already defined 'Red' spot color. engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F)) engine.editor.setSpotColor("Red", Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F)) // List all defined spot colors. engine.editor.findAllSpotColors() // ['Red', 'Yellow'] // Retrieve the RGB color approximation for a defined color. // The alpha value will always be 1.0. val rgbaSpotRed = engine.editor.getSpotColorRGB("Red") // Retrieve the CMYK color approximation for a defined color. val cmykSpotRed = engine.editor.getSpotColorCMYK("Red") // Retrieving the approximation of an undefined spot color returns magenta. val cmykSpotUnknown = engine.editor.getSpotColorCMYK("Unknown") // Returns CMYK values for magenta. // Removes a spot color from the list of defined spot colors. engine.editor.removeSpotColor("Red") ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/creative-sdk)'s CreativeEngine to manage spot colors in the `editor` API. ## Functions ```kotlin fun findAllSpotColors(): List ``` Queries the names of currently set spot colors previously set with \`setSpotColor\`\`. - Returns the names of set spot colors. ```kotlin fun getSpotColorRGB(name: String): RGBAColor ``` Queries the RGB representation set for a spot color. If the value of the queried spot color has not been set yet, returns the default RGB representation (of magenta). The alpha value is always 1.0. - `name`: the name of a spot color. - Returns the RGB representation of a spot color. ```kotlin fun getSpotColorCMYK(name: String): CMYKColor ``` Queries the CMYK representation set for a spot color. If the value of the queried spot color has not been set yet, returns the default RGB representation (of magenta). - `name`: the name of a spot color. - Returns the CMYK representation of a spot color. ```kotlin fun setSpotColor( name: String, color: RGBAColor, ) ``` Sets the RGB representation of a spot color. Use this function to both create a new spot color or update an existing spot color. Note: The alpha value is ignored. - `name`: the name of a spot color. - `color`: the RGB spot color. ```kotlin fun setSpotColor( name: String, color: CMYKColor, ) ``` Sets the CMYK representation of a spot color. Use this function to both create a new spot color or update an existing spot color. - `name`: the name of a spot color. - `color`: the CMYK spot color. ```kotlin fun removeSpotColor(name: String) ``` Removes a spot color from the list of set spot colors. - `name`: the name of a spot color. ## Full Code Here's the full code: ```kotlin // Create a spot color with an RGB color approximation. engine.editor.setSpotColor("Red", Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F)) // Create a spot color with a CMYK color approximation. // Add a CMYK approximation to the already defined 'Red' spot color. engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0F, m = 0F, y = 1F, k = 0F)) engine.editor.setSpotColor("Red", Color.fromCMYK(c = 0F, m = 1F, y = 1F, k = 0F)) // List all defined spot colors. engine.editor.findAllSpotColors() // ['Red', 'Yellow'] // Retrieve the RGB color approximation for a defined color. // The alpha value will always be 1.0. val rgbaSpotRed = engine.editor.getSpotColorRGB("Red") // Retrieve the CMYK color approximation for a defined color. val cmykSpotRed = engine.editor.getSpotColorCMYK("Red") // Retrieving the approximation of an undefined spot color returns magenta. val cmykSpotUnknown = engine.editor.getSpotColorCMYK("Unknown") // Returns CMYK values for magenta. // Removes a spot color from the list of defined spot colors. engine.editor.removeSpotColor("Red") ``` --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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 - [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/) --- This guide explains how to check whether the P3 color space is supported on a given device using the `supportsP3()` function and how to handle scenarios where P3 is unavailable. `supportsP3` returns whether the engine supports displaying and working in the P3 color space on the current device. Otherwise, this function throws an error with a description of why the P3 color space is not supported. If supported, the engine can be switched to a P3 color space using the "features/p3WorkingColorSpace" setting. `checkP3Support` throws an error if the engine does not support working in the P3 color space. ```kotlin // Check whether the current device supports working in the P3 color space val p3IsSupported = engine.editor.supportsP3() try { engine.editor.checkP3Support() } catch (ex: Exception) { // P3 is not supported on the current device } ``` --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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). Whether you're designing for digital screens or printed materials, consistent color management ensures your creations look the way you intend. 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: "System Compatibility" description: "Learn how device performance and hardware limits affect CE.SDK editing, rendering, and export capabilities." platform: android url: "https://img.ly/docs/cesdk/android/compatibility-139ef9/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/) > [System Compatibility](https://img.ly/docs/cesdk/android/compatibility-139ef9/) --- ## Targets On Android, CE.SDK makes use of system-frameworks to benefit from hardware acceleration and platform native performance. The following targets are supported: - Android 7 or later (`minSdk 24`) ## Recommended Hardware Android phones released in the last 5 years, e.g. Asus Zenfone 3, Samsung M31s, or Google Pixel 5. Video capabilities directly depend on the video capabilities of the individual phone. ## Video Playback and exporting is **supported for all codecs** mentioned in the general section. However, mobile devices have stricter limits around the number of parallel encoders and decoders compared to fully fledged desktop machines. This means, that very large scenes with more than 10 videos shown in parallel may fail to play all videos at the same time. ## Export Limitations The export size is limited by the hardware capabilities of the device, e.g., due to the maximum texture size that can be allocated. The maximum possible export size can be queried via API, see [export guide](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/). --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Compatibility & Security" description: "Learn about CE.SDK's compatibility and security features." platform: android url: "https://img.ly/docs/cesdk/android/compatibility-fef719/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/) --- CE.SDK provides robust compatibility and security features across platforms. Learn about supported browsers, frameworks, file formats, language support, and how CE.SDK ensures secure operation in your applications. --- ## Related Pages - [Bundle Size](https://img.ly/docs/cesdk/android/bundle-size-df9210/) - Understand CE.SDK’s engine and editor bundle sizes and how they affect your mobile app’s download footprint. - [System Compatibility](https://img.ly/docs/cesdk/android/compatibility-139ef9/) - Learn how device performance and hardware limits affect CE.SDK editing, rendering, and export capabilities. - [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) - See which image, video, audio, font, and template formats CE.SDK supports for import and export. - [Security](https://img.ly/docs/cesdk/android/security-777bfd/) - Learn how CE.SDK keeps your data private with client-side processing, secure licensing, and GDPR-compliant practices. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Concepts" description: "Key concepts and principles of CE.SDK" platform: android url: "https://img.ly/docs/cesdk/android/concepts-c9ff51/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) --- Key Concepts and principles of CE.SDK. --- ## Related Pages - [Key Concepts](https://img.ly/docs/cesdk/android/key-concepts-21a270/) - Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control. - [Key Capabilities](https://img.ly/docs/cesdk/android/key-capabilities-dbb5b1/) - Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control. - [Architecture](https://img.ly/docs/cesdk/android/concepts/architecture-6ea9b2/) - Understand how CE.SDK is structured around the CreativeEngine and its six interconnected APIs. - [Terminology](https://img.ly/docs/cesdk/android/concepts/terminology-99e82d/) - Definitions for the core terms and concepts used throughout CE.SDK documentation, including Engine, Scene, Block, Fill, Shape, Effect, and more. - [Editing Workflow](https://img.ly/docs/cesdk/android/concepts/editing-workflow-032d27/) - Control editing access with Creator, Adopter, Viewer, and Presenter roles using global and block-level scopes for tailored permissions. - [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) - Learn how blocks define elements in a scene and how to structure them for rendering in CE.SDK. - [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) - Create, configure, save, and load scenes—the root container for all design elements in CE.SDK. - [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) - Structure Android scenes with consistent pages, shared dimensions, and page-level properties in CE.SDK. - [Assets](https://img.ly/docs/cesdk/android/concepts/assets-a84fdd/) - Understand the Android asset system in CE.SDK, including asset definitions, custom asset sources, queries, and apply flows. - [Editor State](https://img.ly/docs/cesdk/android/concepts/edit-modes-1f5b6c/) - Control how users interact with content by switching between edit modes like transform, crop, and text. - [Templating](https://img.ly/docs/cesdk/android/concepts/templating-f94385/) - Understand how templates work in CE.SDK—reusable designs with variables for dynamic text and placeholders for swappable media. - [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) - Subscribe to block creation, update, and deletion events to track changes in your CE.SDK scene. - [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) - Use buffers to store temporary, non-serializable data in CE.SDK via the CreativeEngine API. - [Working With Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/) - Preload resources, find transient data, detect MIME types, and relocate URLs in CE.SDK for Android. - [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) - Manage undo and redo stacks in CE.SDK using multiple histories, callbacks, and API-based controls. - [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/) - Configure design units (pixels, millimeters, inches) and DPI settings for print-ready output in CE.SDK. - [Font Size Unit](https://img.ly/docs/cesdk/android/concepts/font-size-unit-3b2d60/) - Configure how font sizes are interpreted (Pixel vs Point) per scene in the CE.SDK Android engine. - [Headless](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Use the engine directly, without any prebuilt UI. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType @Suppress("UNUSED_VARIABLE") fun architecture( license: String? = null, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") var subscription: Job? = null var engineStarted = false try { engine.start(license = license, userId = userId) engineStarted = true engine.bindOffscreen(width = 1080, height = 1920) // 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 CoroutineScope from the surrounding launch block. .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 (engineStarted) { engine.stop() } } } ``` 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-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-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-architecture-sceneModes // 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-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 CoroutineScope from the surrounding launch block. .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-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.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch 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.ShapeType fun conceptsAssets( license: String?, // Pass null for evaluation mode or your production key in app code. 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 sourceEventJobs = 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) val source = BrandedAssetSource() engine.asset.addSource(source) 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) } engine.asset.addLocalSource( sourceId = "my-local-images", supportedMimeTypes = listOf("image/jpeg"), ) 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 "image/jpeg", "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 = "my-local-images", asset = localAsset) engine.asset.assetSourceContentsChanged(sourceId = "my-local-images") engine.asset.removeSource(sourceId = "my-local-images") engine.asset.removeSource(sourceId = source.sourceId) } finally { sourceEventJobs.forEach { it.cancelAndJoin() } engine.stop() } } private class BrandedAssetSource : AssetSource(sourceId = SOURCE_ID) { override val supportedMimeTypes = listOf("image/jpeg") 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 "image/jpeg", "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 "image/jpeg", "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 = "ly.img.asset.source.branded" } } ``` 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. The example accepts `license: String?`. Pass `null` to run in evaluation mode during development, then replace it with your production key when you wire the code into your app. 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-conceptsAssets-assetDefinition 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 "image/jpeg", "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-conceptsAssets-assetSource 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. Your source responds with a `FindAssetsResult` that contains the assets for the requested page, the total match count, and `nextPage`, which is `-1` when there are no more results. 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-conceptsAssets-queryAssets 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. ## 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-conceptsAssets-applyAsset 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-conceptsAssets-localSource engine.asset.addLocalSource( sourceId = "my-local-images", supportedMimeTypes = listOf("image/jpeg"), ) 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 "image/jpeg", "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 = "my-local-images", asset = localAsset) engine.asset.assetSourceContentsChanged(sourceId = "my-local-images") ``` `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-conceptsAssets-sourceEvents 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 - [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.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.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 fun conceptsBlocks( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): Job = CoroutineScope(Dispatchers.Main).launch { runConceptsBlocks(license, userId) } suspend fun runConceptsBlocks( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = coroutineScope { val engine = Engine.getInstance(id = "ly.img.engine.example") var selectionObserver: Job? = null var stateObserver: Job? = null try { 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) 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) } println("Blocks guide initialized successfully.") println("Created graphic and text blocks, then exercised hierarchy and state APIs.") } finally { selectionObserver?.cancel() stateObserver?.cancel() engine.stop() } } ``` 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-serialization val savedString = engine.block.saveToString(blocks = listOf(graphic, textBlock)) println("Blocks saved to string, length: ${savedString.length}") // Alternatively, blocks can be saved with their assets in an archive: // val savedArchive = engine.block.saveToArchive(blocks = listOf(graphic, textBlock)) val loadedBlocks = engine.block.loadFromString(savedString) println("Loaded blocks from string: ${loadedBlocks.size}") // Alternatively, blocks can be loaded from an archive or an extracted archive directory: // val loadedArchiveBlocks = engine.block.loadFromArchive(Uri.parse("file:///path/to/blocks.zip")) // val loadedUrlBlocks = engine.block.loadFromURL(Uri.parse("file:///path/to/blocks.blocks")) loadedBlocks.forEach { loadedBlock -> engine.block.destroy(loadedBlock) } ``` Use `saveToString()` for lightweight serialization or `saveToArchive()` to include referenced assets. Load archived data back with `loadFromArchive()` or point `loadFromURL()` at a `blocks.blocks` file inside an extracted archive directory. Loaded blocks are not attached to the scene automatically. Parent them with `appendChild()` if you want them to render. ## Troubleshooting - Block is not visible: ensure it is appended to a page, and that the page is appended to the scene. - Property writes fail: verify the property name with `findAllProperties()` and the accessor with `getPropertyType()`. - Selection or state callbacks never fire: collect the returned `Flow` from a coroutine that stays active while the observer is needed. - Loaded blocks do not appear after deserialization: append them back into the scene hierarchy after `loadFromString()`, `loadFromArchive()`, or `loadFromURL()`. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Buffers" description: "Use buffers to store temporary, non-serializable data in CE.SDK via the CreativeEngine API." platform: android url: "https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) --- ```kotlin file=@cesdk_android_examples/engine-guides-buffers/Buffers.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 fun buffers( 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.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() 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) wavData.put("RIFF".toByteArray()) wavData.putInt(wavFileSize - 8) wavData.put("WAVE".toByteArray()) wavData.put("fmt ".toByteArray()) 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()) wavData.put("data".toByteArray()) 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 = ByteArray(size = 4) header.get(riff) check(String(riff) == "RIFF") val bufferLength = engine.editor.getBufferLength(uri = bufferUri) check(bufferLength == wavFileSize) val demoBuffer = engine.editor.createBuffer() val demoData = ByteBuffer.allocateDirect(8).apply { put(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8)) flip() } engine.editor.setBufferData(uri = demoBuffer, offset = 0, data = demoData) engine.editor.setBufferLength(uri = demoBuffer, length = 4) check(engine.editor.getBufferLength(uri = demoBuffer) == 4) 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) engine.editor.destroyBuffer(uri = bufferUri) } finally { engine.stop() } } ``` 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. The example starts the engine in evaluation mode by passing `license = null`. Replace that with your production license key when you integrate the same flow into an app. 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-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-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-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-write-buffer val bytesPerSample = 2 val wavDataSize = sampleCount * bytesPerSample val wavFileSize = 44 + wavDataSize val wavData = ByteBuffer.allocateDirect(wavFileSize).order(ByteOrder.LITTLE_ENDIAN) wavData.put("RIFF".toByteArray()) wavData.putInt(wavFileSize - 8) wavData.put("WAVE".toByteArray()) wavData.put("fmt ".toByteArray()) 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()) wavData.put("data".toByteArray()) 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-read-buffer val header = engine.editor.getBufferData(uri = bufferUri, offset = 0, length = 44) val riff = ByteArray(size = 4) header.get(riff) check(String(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-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-resize-buffer val demoBuffer = engine.editor.createBuffer() val demoData = ByteBuffer.allocateDirect(8).apply { put(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8)) flip() } engine.editor.setBufferData(uri = demoBuffer, offset = 0, data = demoData) engine.editor.setBufferLength(uri = demoBuffer, length = 4) check(engine.editor.getBufferLength(uri = demoBuffer) == 4) 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-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-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-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) engine.editor.destroyBuffer(uri = bufferUri) ``` 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.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit import ly.img.engine.Engine import kotlin.math.roundToInt fun designUnits( 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 page = configureDesignUnits(engine) engine.scene.zoomToBlock(page) engine.stop() } 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-designUnits-getDesignUnit // 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-designUnits-setDesignUnit // 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-designUnits-configureDpi // 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-designUnits-setPageDimensions // 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-designUnits-createTextBlock // 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-designUnits-compareUnits // 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.DefaultAssetSource 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 import ly.img.engine.addDefaultAssetSources @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 requiredDefaultSources = setOf( DefaultAssetSource.CROP_PRESETS, DefaultAssetSource.PAGE_PRESETS, ) coroutineScope { val missingDefaultSources = requiredDefaultSources .filterNot { source -> engine.asset.findAllSources().contains(source.key) } .toSet() if (missingDefaultSources.isNotEmpty()) { engine.addDefaultAssetSources( exclude = DefaultAssetSource.values().toSet() - missingDefaultSources, ) } 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-editorState-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-editorState-getEditMode 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-editorState-setEditMode 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-editorState-customEditMode 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-editorState-onStateChanged 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-editorState-textCursorPosition 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-editorState-interactionHappening 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.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.collect 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.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" fun editingWorkflow( license: String?, userId: String, ): Job = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example.editing-workflow") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 720, height = 1080) val roleCustomization = customizeEditingWorkflowRoles(engine, this) try { createEditingWorkflowTemplate(engine) } finally { roleCustomization.cancelAndJoin() engine.stop() } } 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.setColor( block = engine.block.getFill(background), property = "fill/color/value", value = 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.setColor( block = engine.block.getFill(brandBanner), property = "fill/color/value", value = 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.setColor( block = engine.block.getFill(brandBanner), property = "fill/color/value", value = 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.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 --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch 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 fun events( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): Job = 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 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) 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) 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) allBlocksSubscription.cancel() specificBlocksSubscription.cancel() processedEventsSubscription.cancel() engine.stop() } ``` 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 Use evaluation mode by passing `license = null`, then create a scene and page before subscribing. Engine calls stay on the main thread, while `bindOffscreen()` gives the engine an offscreen surface for this headless demo. ```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.cancel() specificBlocksSubscription.cancel() processedEventsSubscription.cancel() ``` Cancel subscriptions when your screen leaves composition, the editor closes, or you stop tracking a block. Leaving collectors active forces the engine to keep preparing event lists on every update. ## Event Batching and Deduplication Events are collected during an engine update and delivered together at the end. The engine deduplicates `UPDATED` events, so you receive at most one `UPDATED` event per block per update cycle. This batching behavior means: - Multiple rapid changes to a single block result in one `UPDATED` event. - The array order does not reflect the chronological order of changes inside one update cycle. - If you need to know which property changed, compare against cached values in your app. ## Use Cases Events support several reactive patterns in Android apps: - **Syncing external state**: Keep ViewModels, Redux stores, or persistence layers aligned with scene changes. - **Building reactive UI**: Update Compose state when blocks change without polling the engine. - **Tracking changes for undo/redo**: Monitor block changes for custom history or analytics pipelines. - **Validating scene constraints**: React to new or updated blocks when you enforce design rules in code. ## Troubleshooting **Events not firing**: Make sure you have not cancelled the collection job too early, and confirm the filtered block IDs are still valid. **Exception on `DESTROYED` events**: Treat destroyed block IDs as invalid, and prune stale cached IDs with `engine.block.isValid()` before calling other Block API methods. **Missing `UPDATED` events**: The engine deduplicates updates, so multiple rapid property changes become one `UPDATED` event per block. **Leaking collectors**: Store the `Job` returned by `launchIn()` and cancel it during cleanup. ## Next Steps [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) — Learn about block types, properties, and lifecycle. [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) — Implement undo/redo functionality. [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) — Understand scene structure and management. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Font Size Unit" description: "Configure how font sizes are interpreted (Pixel vs Point) per scene in the CE.SDK Android engine." platform: android url: "https://img.ly/docs/cesdk/android/concepts/font-size-unit-3b2d60/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Font Size Unit](https://img.ly/docs/cesdk/android/concepts/font-size-unit-3b2d60/) --- ```kotlin file=@cesdk_android_examples/engine-guides-concepts-font-size-unit/FontSizeUnit.kt reference-only import ly.img.engine.DesignBlockType import ly.img.engine.DesignUnit import ly.img.engine.Engine import ly.img.engine.FontUnit import ly.img.engine.SceneLayout import ly.img.engine.SizeMode import kotlin.math.abs private const val FONT_SIZE_EPSILON = 0.001F fun fontSizeUnit(engine: Engine) { // `scene.create(designUnit, fontSizeUnit)` lets you pair both units // explicitly. When `fontSizeUnit` is null, CE.SDK pairs it with // `designUnit`: `Pixel` design -> `Pixel` fonts, `Millimeter` and // `Inch` -> `Point` fonts. val scene = engine.scene.create( designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.POINT, sceneLayout = SceneLayout.FREE, ) val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = text) engine.block.replaceText(text, text = "Font Size Unit") engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) // Read the scene's current font-size unit. This scene passes `Point` // explicitly even though the design unit is `Pixel`. val initialUnit = engine.scene.getFontSizeUnit() println("Initial font-size unit: $initialUnit") // POINT check(initialUnit == FontUnit.POINT) // Switch the scene-wide default. Existing text keeps its visual size; // only future `setTextFontSize` / `getTextFontSizes` calls use the new // unit. `setDesignUnit` does not overwrite this setting, so the choice // survives changes to the design coordinate system. engine.scene.setFontSizeUnit(FontUnit.PIXEL) val switchedUnit = engine.scene.getFontSizeUnit() println("After switch: $switchedUnit") // PIXEL check(switchedUnit == FontUnit.PIXEL) // The value 24f is interpreted in the scene's `fontSizeUnit`, so the // engine reads it as 24 px. engine.block.setTextFontSize(text, fontSize = 24F) // Font-size float properties use the same font-size unit. engine.block.setFloat(block = text, property = "text/fontSize", value = 24F) // `getTextFontSizes` returns values in the scene's `fontSizeUnit`, // mirroring how `setTextFontSize` interpreted them. val sizesInPixels = engine.block.getTextFontSizes(text) val propertyFontSizeInPixels = engine.block.getFloat(block = text, property = "text/fontSize") println("Sizes (px): $sizesInPixels") println("Property size (px): $propertyFontSizeInPixels") check(sizesInPixels.size == 1 && abs(sizesInPixels.first() - 24F) < FONT_SIZE_EPSILON) check(abs(propertyFontSizeInPixels - 24F) < FONT_SIZE_EPSILON) } ``` Pick the unit your scene uses for `setTextFontSize` and `getTextFontSizes`. The engine continues to store font sizes in points internally; this setting only changes how values are interpreted at the API boundary. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-concepts-font-size-unit) A scene's `fontSizeUnit` is the unit `BlockApi.setTextFontSize` expects when setting a value and `BlockApi.getTextFontSizes` returns the value. CE.SDK supports two units: `FontUnit.POINT` (the typographic default) and `FontUnit.PIXEL` (to match a pixel-based design unit). This guide covers reading and changing the scene's font-size unit, how that default flows through Android text APIs, why Android font-size calls use the scene-level font unit, and how to pair the font unit with the design unit at scene creation. ## Reading the Current Font-Size Unit Use `engine.scene.getFontSizeUnit()` to retrieve the font unit the current scene uses for the font size APIs. In this sample, the scene passes `FontUnit.POINT` explicitly even though the design unit is `DesignUnit.PIXEL`. When the unit-aware `engine.scene.create(designUnit, fontSizeUnit, sceneLayout)` overload receives `fontSizeUnit=null`, CE.SDK pairs the font-size unit with the design unit: `DesignUnit.PIXEL` uses `FontUnit.PIXEL`, while `DesignUnit.MILLIMETER` and `DesignUnit.INCH` use `FontUnit.POINT`. Loaded scenes saved before `fontSizeUnit` existed return `FontUnit.POINT` for compatibility. ```kotlin highlight-android-get-font-size-unit // Read the scene's current font-size unit. This scene passes `Point` // explicitly even though the design unit is `Pixel`. val initialUnit = engine.scene.getFontSizeUnit() println("Initial font-size unit: $initialUnit") // POINT ``` ## Setting the Font-Size Unit `engine.scene.setFontSizeUnit(FontUnit)` switches the scene-wide default. Existing text retains its visual size—the engine still stores values in points and converts on the way in and out. Only subsequent `setTextFontSize` and `getTextFontSizes` calls use the new unit. ```kotlin highlight-android-set-font-size-unit // Switch the scene-wide default. Existing text keeps its visual size; // only future `setTextFontSize` / `getTextFontSizes` calls use the new // unit. `setDesignUnit` does not overwrite this setting, so the choice // survives changes to the design coordinate system. engine.scene.setFontSizeUnit(FontUnit.PIXEL) val switchedUnit = engine.scene.getFontSizeUnit() println("After switch: $switchedUnit") // PIXEL ``` `setDesignUnit` does not change `fontSizeUnit`, so a deliberate font-unit choice survives changes to the design coordinate system. ## Setting Font Sizes in the Scene Unit When you call `BlockApi.setTextFontSize(block, fontSize, from, to)`, the value is interpreted in the scene's `fontSizeUnit`. The optional `from` and `to` parameters limit the write to a UTF-16 code unit range; keep the default `-1` values to target the whole text block, or the current editing selection when the block is being edited. The same unit applies to `BlockApi.setFloat` and `BlockApi.getFloat` for `text/fontSize`, `text/minAutomaticFontSize`, `text/maxAutomaticFontSize`, `caption/fontSize`, `caption/minAutomaticFontSize`, and `caption/maxAutomaticFontSize`. ```kotlin highlight-android-implicit-set // The value 24f is interpreted in the scene's `fontSizeUnit`, so the // engine reads it as 24 px. engine.block.setTextFontSize(text, fontSize = 24F) // Font-size float properties use the same font-size unit. engine.block.setFloat(block = text, property = "text/fontSize", value = 24F) ``` ## No Per-Call Unit Override on Android Android's `BlockApi.setTextFontSize` and `BlockApi.getTextFontSizes` APIs use the scene's `fontSizeUnit` directly. They do not expose a per-call unit option, so set the scene unit that matches your workflow before writing or reading font sizes. If your app stores typography in another unit, convert those values in your application layer before passing them to CE.SDK, or switch the scene's `fontSizeUnit` for the workflow that owns those text edits. ## Reading Font Sizes `BlockApi.getTextFontSizes(block, from, to)` returns values in the scene's `fontSizeUnit`. Use `from` and `to` UTF-16 code unit offsets to read a substring range; keep the default `-1` values to read the current cursor or selection while the block is being edited, or the whole text block otherwise. The same conversion applies to `BlockApi.getFloat(block, "text/fontSize")`, so reads match how Android interpreted the previous write. ```kotlin highlight-android-read-sizes // `getTextFontSizes` returns values in the scene's `fontSizeUnit`, // mirroring how `setTextFontSize` interpreted them. val sizesInPixels = engine.block.getTextFontSizes(text) val propertyFontSizeInPixels = engine.block.getFloat(block = text, property = "text/fontSize") println("Sizes (px): $sizesInPixels") println("Property size (px): $propertyFontSizeInPixels") ``` ## Pairing Units at Scene Creation The unit-aware `engine.scene.create(designUnit, fontSizeUnit, sceneLayout)` overload accepts both options. When `fontSizeUnit` is `null`, CE.SDK pairs it with `designUnit` (`DesignUnit.PIXEL` ⇒ `FontUnit.PIXEL`, `DesignUnit.MILLIMETER` and `DesignUnit.INCH` ⇒ `FontUnit.POINT`). Pass both explicitly when you want to mix them—for example, a Pixel design with Point-based typography. ```kotlin highlight-android-create-with-units // `scene.create(designUnit, fontSizeUnit)` lets you pair both units // explicitly. When `fontSizeUnit` is null, CE.SDK pairs it with // `designUnit`: `Pixel` design -> `Pixel` fonts, `Millimeter` and // `Inch` -> `Point` fonts. val scene = engine.scene.create( designUnit = DesignUnit.PIXEL, fontSizeUnit = FontUnit.POINT, sceneLayout = SceneLayout.FREE, ) ``` Auto-pairing only applies to the unit-aware overload above. The layout-only `engine.scene.create(sceneLayout)` overload creates a Pixel scene with `FontUnit.POINT`. `engine.scene.createForVideo()`, `engine.scene.createFromImage()`, and `engine.scene.createFromVideo()` also keep `FontUnit.POINT` for compatibility. Call `engine.scene.setFontSizeUnit(FontUnit.PIXEL)` after creation if you want font sizes to match Pixel-based coordinates. ## API Reference | API | Purpose | | --- | --- | | `engine.scene.getFontSizeUnit()` | Get the current scene's font-size unit. | | `engine.scene.setFontSizeUnit(fontSizeUnit=_)` | Set the current scene's font-size unit. | | `engine.scene.create(designUnit=_, fontSizeUnit=null, sceneLayout=SceneLayout.FREE)` | Create a scene whose font-size unit is paired automatically with the design unit. | | `engine.scene.create(sceneLayout=SceneLayout.FREE)` | Create a Pixel scene with the compatibility font-size unit `FontUnit.POINT`. | | `engine.scene.createForVideo()` | Create a video scene with the compatibility font-size unit `FontUnit.POINT`. | | `engine.scene.createFromImage(imageUri=_, dpi=300F, pixelScaleFactor=1F, sceneLayout=SceneLayout.FREE)` | Create an image scene with the compatibility font-size unit `FontUnit.POINT`. | | `engine.scene.createFromVideo(videoUri=_)` | Create a video scene from a video URI with the compatibility font-size unit `FontUnit.POINT`. | | `engine.block.setTextFontSize(block=_, fontSize=_, from=-1, to=-1)` | Set a text block's font size, or a UTF-16 text range, in the scene unit. | | `engine.block.getTextFontSizes(block=_, from=-1, to=-1)` | Read a text block's font sizes, or a UTF-16 text range, in the scene unit. | | `engine.block.setFloat(block=_, property="text/fontSize", value=_)`
`engine.block.getFloat(block=_, property="text/fontSize")` | Set or read the main text font-size property in the scene unit. | | `engine.block.setFloat(block=_, property="text/minAutomaticFontSize", value=_)`
`engine.block.getFloat(block=_, property="text/minAutomaticFontSize")` | Set or read the text auto-resize minimum in the scene unit. | | `engine.block.setFloat(block=_, property="text/maxAutomaticFontSize", value=_)`
`engine.block.getFloat(block=_, property="text/maxAutomaticFontSize")` | Set or read the text auto-resize maximum in the scene unit. | | `engine.block.setFloat(block=_, property="caption/fontSize", value=_)`
`engine.block.getFloat(block=_, property="caption/fontSize")` | Set or read the caption font-size property in the scene unit. | | `engine.block.setFloat(block=_, property="caption/minAutomaticFontSize", value=_)`
`engine.block.getFloat(block=_, property="caption/minAutomaticFontSize")` | Set or read the caption auto-resize minimum in the scene unit. | | `engine.block.setFloat(block=_, property="caption/maxAutomaticFontSize", value=_)`
`engine.block.getFloat(block=_, property="caption/maxAutomaticFontSize")` | Set or read the caption auto-resize maximum in the scene unit. | ## Next Steps - [Design Units](https://img.ly/docs/cesdk/android/concepts/design-units-cc6597/) - Understand the broader unit system that determines layout coordinates and DPI. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Headless" description: "Use the engine directly, without any prebuilt UI." platform: android url: "https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-scene-from-scratch/CreateSceneFromScratch.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType fun createSceneFromScratch( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope( Dispatchers.Main, ).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Star)) engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color)) engine.block.appendChild(parent = page, child = block) engine.stop() } ``` Headless Mode lets you use the CreativeEditor SDK's Engine directly. No prebuilt editor UI required. You initialize the Engine, load or build scenes programmatically, and export to images/PDF/video entirely in code. This is ideal for custom UIs, automation, server-triggered rendering, or batch exports. ## What You'll Learn - What "Headless / Engine-only" means and how it differs from the UI editor. - When to choose Headless Mode (and when not to). - How to initialize the Engine for headless use in Kotlin. - How to load/build a scene and modify blocks programmatically. - How to export (PNG/JPEG/PDF, and notes on video) without launching the UI. ## When to Use It **Pick Headless Mode when you need:** - A fully custom UI: You're building your own editing interface or integrating into an existing app layout. - Programmatic rendering: Generate images/PDFs from templates or data—no user interaction. - Automation & batch work: Merge data at scale, pre-render previews, or run background exports. - Export-only flows: Quickly render a scene without ever opening the editor. **Avoid Headless Mode when:** - You want turnkey editing UX out of the box (use the standard Editor for that). - You don't want to create selection, gestures, or tool panels yourself. ### Quick Comparison |Scenario | Headless (Engine-only) | Standard UI Editor | |---|---|---| |Automate design generation from code|✅|❌| |Export scenes without user interaction|✅|❌| |Let users visually edit with ready-made panels|❌|✅| |Build a custom editor interface|✅|⭘ (extend via config)| ### How Headless Mode Works With the prebuilt editors, user actions call the **Engine** API through the UI. In Headless Mode, you start the Engine and work entirely in Kotlin to: - Scene management: create/load scenes; add pages; read/write properties - Blocks: create text/graphics/shapes; set fills, sizes, transforms; append to parents - Assets: register sources, resolve URIs, add media programmatically - Templates & data: load scene archives/JSON, update text variables, swap images - Export: render blocks or pages to PNG/JPEG/PDF (and trigger video exports where appropriate) ### Initialize the Engine in Headless Mode (Kotlin) ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Engine class HeadlessRenderer { private lateinit var engine: Engine fun startEngine(license: String, userId: String) = CoroutineScope(Dispatchers.Main).launch { engine = Engine.getInstance(id = "ly.img.engine.example") engine.start( license = license, userId = userId ) // Bind offscreen for headless rendering (no UI needed) engine.bindOffscreen(width = 1080, height = 1920) } } ``` This is "headless" because you never instantiate or present the editor UI. You only create an Engine and use its APIs. The `bindOffscreen` method creates an offscreen rendering surface—perfect for headless scenarios where no visible View is needed. ### Create and Export a Scene (Kotlin) Below is a minimal, end-to-end example that works with the preceding class to: - Create a scene with a single page. - Add a rectangle filled with a remote image. - Add a text block. - Export the page as PNG data. ```kotlin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlockType import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import ly.img.engine.SizeMode import java.nio.ByteBuffer data class ExportResult( val pngData: ByteBuffer, val suggestedFilename: String ) suspend fun HeadlessRenderer.buildAndExport(): ExportResult = withContext(Dispatchers.Main) { // 1) Create an empty scene and a page val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) // Set page size engine.block.setWidth(page, value = 800f) engine.block.setHeight(page, value = 600f) // Attach page to scene root engine.block.appendChild(parent = scene, child = page) // 2) Add an image rectangle val rect = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(rect, shape = shape) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", // Use your own asset URL or registered source URI value = "https://img.ly/static/ubq_samples/sample_1.jpg" ) engine.block.setFill(rect, fill = imageFill) // Position & size the rect engine.block.setPositionX(rect, value = 100f) engine.block.setPositionY(rect, value = 100f) engine.block.setWidth(rect, value = 400f) engine.block.setHeight(rect, value = 300f) engine.block.appendChild(parent = page, child = rect) // 3) Add text val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, text = "Hello, From Headless Mode!") engine.block.setPositionX(text, value = 100f) engine.block.setPositionY(text, value = 450f) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.appendChild(parent = page, child = text) // 4) Export the page to PNG val pngData = engine.block.export(page, mimeType = MimeType.PNG) ExportResult(pngData = pngData, suggestedFilename = "headless-output.png") } ``` ### Saving the File (optional) After creating the image, you may want to save it. Here is a minimal code example to save the file to the app's files directory and return the file path. ```kotlin import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File suspend fun savePNG(context: Context, result: ExportResult): File = withContext(Dispatchers.IO) { val outputDir = context.filesDir val destinationFile = File(outputDir, result.suggestedFilename) destinationFile.outputStream().channel.use { channel -> channel.write(result.pngData) } destinationFile } ``` ![The 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: `engine.addDefaultAssetSources()` 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. - Check CORS/network security configuration if using custom domains. **❌ Text looks wrong or uses fallback fonts**: - The Engine needs the exact font used by the text style. Register/bundle the font and ensure it's discoverable before export. **❌ Memory spikes on big batches**: - Reuse a single Engine instance when possible. - Export, write to disk, and release large buffers before rendering the next item. - Call `engine.stop()` when completely done to free resources. ## Next Steps Now that you understand the basics of headless mode, below are some topics to help you expand your knowledge: - [Templates & Variables](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) – Design tokenized templates and drive them from data. - [Exporting](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) – PNG/JPEG/PDF exports, plus format options and best practices. - [Standard Editor vs Headless](https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/) – If you need turnkey UI, start here and decide whether to drop to headless for specific flows. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Pages" description: "Structure Android scenes with consistent pages, shared dimensions, and page-level properties in CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Pages](https://img.ly/docs/cesdk/android/concepts/pages-7b6bae/) --- ```kotlin file=@cesdk_android_examples/engine-guides-concepts-pages/Pages.kt reference-only import kotlinx.coroutines.Dispatchers 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( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): PagesGuideSummary = withContext(Dispatchers.Main.immediate) { val engine = Engine.getInstance(id = "ly.img.engine.example.pages") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) try { // 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) 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, ) } finally { engine.stop() } } ``` 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-pages-createScene // 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-pages-createPages 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-pages-addContent 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-pages-setDimensions // 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-pages-findPages 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-pages-pageMargins engine.block.setBoolean( block = firstPage, property = "page/marginEnabled", value = true, ) engine.block.setFloat(block = firstPage, property = "page/margin/top", value = 10F) engine.block.setFloat(block = firstPage, property = "page/margin/bottom", value = 10F) engine.block.setFloat(block = firstPage, property = "page/margin/left", value = 10F) engine.block.setFloat(block = firstPage, property = "page/margin/right", value = 10F) ``` Set `page/marginEnabled` to `true`, then adjust `page/margin/top`, `page/margin/bottom`, `page/margin/left`, and `page/margin/right` in design units. ### Title Template The `page/titleTemplate` property controls the label shown for a page. It supports template tokens such as `{{ubq.page_index}}` for numbered labels. ```kotlin highlight-pages-titleTemplate 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-pages-pageBackground 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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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.Dispatchers 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.ShapeType import java.io.ByteArrayOutputStream import java.nio.ByteBuffer suspend fun resources( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): List = withContext(Dispatchers.Main) { val logLines = mutableListOf() var runningEngine: Engine? = null try { val engine = Engine.getInstance(id = "ly.img.engine.resources.example") runningEngine = engine engine.start(license = license, userId = userId) engine.bindOffscreen(width = 680, height = 260) 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") 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 transientImageBytes = ByteArrayOutputStream().use { output -> check(transientBitmap.compress(Bitmap.CompressFormat.PNG, 100, output)) output.toByteArray() } transientBitmap.recycle() val transientImageBuffer = ByteBuffer .allocateDirect(transientImageBytes.size) .apply { put(transientImageBytes) flip() } val transientImageBufferUri = engine.editor.createBuffer() engine.editor.setBufferData( uri = transientImageBufferUri, offset = 0, data = transientImageBuffer, ) check(engine.editor.getBufferLength(uri = transientImageBufferUri) == transientImageBytes.size) val transientImageBlock = engine.block.create(DesignBlockType.Graphic) val transientImageFill = engine.block.createFill(FillType.Image) 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)) // Pass an empty list to preload all resources currently known to the engine. engine.block.forceLoadResources(blocks = emptyList()) // Or preload only the blocks whose resources are needed next. val graphics = engine.block.findByType(DesignBlockType.Graphic) engine.block.forceLoadResources(blocks = graphics) // No preload call is required; this inspects resource references that cannot be serialized. val transientResources = engine.editor.findAllTransientResources() 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) unusedBlocks.forEach { engine.block.destroy(it) } val mimeType = engine.editor.getMimeType(uri = imageUri) val relocatedImageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg") engine.editor.relocateResource( currentUri = imageUri, relocatedUri = relocatedImageUri, ) val relocatedResources = transientResources.map { (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 = uploadTransientResourceToPermanentStorage( sourceUri = transientUri, data = resourceBytes.toByteArray(), ) engine.editor.relocateResource( currentUri = transientUri, relocatedUri = permanentUri, ) transientUri to permanentUri } val remainingTransientResources = engine.editor.findAllTransientResources() 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, all known resources, and ${graphics.size} 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 += "Destroyed ${unusedBlocks.size} unused 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 { runningEngine?.stop() } } private suspend fun uploadTransientResourceToPermanentStorage( sourceUri: Uri, data: ByteArray, ): Uri { check(data.isNotEmpty()) { "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.size}" return Uri.parse("https://your-storage.example/uploads/$fileName") } ``` 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") 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, pass an empty list to load every resource currently known to the engine, or pass a smaller set of blocks to control the load order. ```kotlin highlight-android-preload-resources // Preload every resource referenced by the scene and its children. engine.block.forceLoadResources(blocks = listOf(scene)) // Pass an empty list to preload all resources currently known to the engine. engine.block.forceLoadResources(blocks = emptyList()) // Or preload only the blocks whose resources are needed next. val graphics = engine.block.findByType(DesignBlockType.Graphic) 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() ``` 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) unusedBlocks.forEach { engine.block.destroy(it) } ``` Pair this with `findAllMediaURIs()` to skip relocating resources for blocks that are no longer reachable, or call `destroy()` on each block to free memory before saving. ## 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") 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. ## 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, _) -> 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 = uploadTransientResourceToPermanentStorage( sourceUri = transientUri, data = resourceBytes.toByteArray(), ) engine.editor.relocateResource( currentUri = transientUri, relocatedUri = permanentUri, ) transientUri to permanentUri } val remainingTransientResources = engine.editor.findAllTransientResources() 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: ByteArray, ): Uri { check(data.isNotEmpty()) { "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.size}" 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.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch 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 fun modifyingScenes( 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(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) engine.scene.setZoomLevel(2F) engine.scene.load(scene = savedScene) zoomEvents.cancel() activeSceneEvents.cancel() engine.stop() } ``` 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. The sample runs in evaluation mode when `license` is `null`; replace it with your production key before shipping. ## 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-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-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-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-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-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-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-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-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-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-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-load-scene val loadedScene = engine.scene.load(scene = savedScene) println("Scene loaded: $loadedScene") ``` Loading a new scene replaces any existing scene. The engine only keeps one active scene at a time. ### Loading from URL Use `engine.scene.load(sceneUri = Uri.parse(...))` to load a scene from a local or remote location. For scene bundles that include referenced assets, use `engine.scene.loadArchive(archiveUri = Uri.parse(...))`. ### Applying Templates Apply template content to the current scene using `engine.scene.applyTemplate(template = ...)` or `engine.scene.applyTemplate(templateUri = ...)`. Template content is scaled automatically to the current page dimensions while keeping the current scene's design unit and page size. ## Event Subscriptions Subscribe to scene-related events with Kotlin `Flow` to react to changes in real time. The example below keeps both collectors active long enough to observe a zoom change and a scene reload before cleaning them up. ```kotlin highlight-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) engine.scene.setZoomLevel(2F) engine.scene.load(scene = savedScene) 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(), ) ``` ## 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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Terminology" description: "Definitions for the core terms and concepts used throughout CE.SDK documentation, including Engine, Scene, Block, Fill, Shape, Effect, and more." platform: android url: "https://img.ly/docs/cesdk/android/concepts/terminology-99e82d/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Terminology](https://img.ly/docs/cesdk/android/concepts/terminology-99e82d/) --- A reference guide to the core terms and concepts used throughout CE.SDK documentation. CE.SDK uses consistent terminology across all platforms. Understanding what we call things helps you navigate the API, read documentation efficiently, and communicate effectively with other developers working on CE.SDK integration. ## Core Architecture ### Engine All operations, like creating scenes, manipulating blocks, rendering, and exporting, go through the *Engine*. Initialize it once and use it throughout your application's lifecycle. ### Scene The root container for all design content. A *Scene* contains *Pages*, which contain *Blocks*. Only one *Scene* can be active per *Engine* instance. You can create a *Scene* programmatically or load one from a file. *Scenes* support both static designs (social posts, print materials, graphics) and time-based content (duration, playback time, animation). See [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) for details. ### Page *Pages* are containers within a *Scene* that hold content *Blocks* and define working area dimensions. For static designs, pages are individual artboards. For video editing, pages are time-based compositions where *Blocks* are arranged across time. ### Block The fundamental building unit in CE.SDK. Everything visible in a design is a *Block*: images, text, shapes, graphics, audio, video, and even *Pages* themselves. *Blocks* form a parent-child hierarchy. Each *Block* has two identifiers: - **DesignBlock**: A Kotlin `Int` handle that references a block in API calls - **UUID**: A stable string identifier that persists across save and load operations In this guide, *Block* refers to the CE.SDK design element concept, while `DesignBlock` refers to the Android handle type. See [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for details. ## Block Anatomy Modify a *Block's* appearance and behavior by attaching *Fills*, *Shapes*, and *Effects*. Most of these modifiers must be created separately and then attached to a *Block*. ### Fill *Fills* cover the surface of a *Block's* shape: - **Color Fill**: Solid color - **Gradient Fill**: Linear, radial, or conical gradients - **Image Fill**: Image content - **Video Fill**: Video content ### Shape *Shapes* define a *Block's* outline and dimensions, determining the silhouette and how the *Fill* is clipped. *Shape* types include: - **Rect**: Rectangles and squares - **Ellipse**: Circles and ovals - **Polygon**: Multi-sided shapes - **Star**: Star shapes with configurable points - **Line**: Straight lines - **Vector Path**: Custom vector shapes Like *Fills*, *Shapes* are created separately and attached to *Blocks*. See [Shapes](https://img.ly/docs/cesdk/android/shapes-9f1b2c/) for details. ### Effect *Effects* are non-destructive visual modifications applied to a *Block*. Multiple *Effects* can be stacked. *Effect* categories include: - **Adjustments**: Brightness, contrast, saturation, and other image corrections - **Filters**: LUT-based color grading, duotone - **Stylization**: Pixelize, posterize, half-tone, dot pattern, linocut, outliner - **Distortion**: Liquid, mirror, shifter, cross-cut, extrude blur - **Focus**: Tilt-shift, vignette - **Color**: Recolor, green screen (chroma key) - **Other**: Glow, TV glitch The order determines how multiple effects attached to a single block interact. See [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) for details. ### Blur A modifier that reduces sharpness. *Blur* types include: - **Uniform Blur**: Even blur across the entire block - **Radial Blur**: Circular blur from a center point - **Mirrored Blur**: Blur with reflection > **Note:** **Blur has a dedicated API because it composites differently than other effects.** While most effects like brightness or saturation operate only on a block's own pixels, blur needs to sample pixels from the surrounding area to calculate the blurred result. This means blur interacts with the scene's layering and transparency in ways other effects do not. When you blur a partially transparent block, the engine must handle how that blur blends with whatever content sits behind it. See [Blur](https://img.ly/docs/cesdk/android/filters-and-effects/blur-71d642/) for details. ### Drop Shadow A built-in block property, not an *Effect*, that renders a shadow beneath blocks. *Drop Shadow* has dedicated API methods for enabling, color, offset, and blur radius. > **Warning:** Unlike effects, drop shadow is configured directly on the block rather than created and attached separately. ## Block Handling These terms describe how *Blocks* are categorized and identified. ### Type The built-in *Type* defines a *Block's* core behavior and available properties. *Type* is immutable: you choose it when creating the *Block*. - `//ly.img.ubq/graphic` — Visual block for images, shapes, and graphics - `//ly.img.ubq/text` — Text content - `//ly.img.ubq/audio` — Audio content - `//ly.img.ubq/page` — Page container - `//ly.img.ubq/scene` — Root scene container - `//ly.img.ubq/track` — Video timeline track - `//ly.img.ubq/stack` — Stack container for layering - `//ly.img.ubq/group` — Group container for organizing blocks - `//ly.img.ubq/camera` — Camera for scene viewing - `//ly.img.ubq/cutout` — Cutout or mask block - `//ly.img.ubq/caption` — Caption or subtitle block - `//ly.img.ubq/captionTrack` — Track for captions The *Type* determines which properties and capabilities a *Block* has. ### Kind A custom string label you assign to categorize *Blocks* for your application. Unlike *Type*, *Kind* is mutable and application-defined. Changing the *Kind* has no effect on appearance or behavior at the engine level. You can query and search for *Blocks* by *Kind*. Common uses: - Categorizing template elements ("logo", "headline", "background") - Filtering blocks for custom UI - Automation workflows that process blocks by purpose ### Property A configurable attribute of a *Block*. *Properties* have types (`Bool`, `Int`, `Float`, `String`, `Color`, `Enum`) and paths like `text/fontSize` or `fill/image/imageFileURI`. Access *Properties* using type-specific getter and setter methods. Each *Block* type exposes different properties, which you can discover programmatically. See [Blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for details. ## Assets and Resources ### Asset Think of *Assets* as media items that you can provide to your users: images, videos, audio files, fonts, stickers, or templates, meaning anything that can be added to a design. *Assets* have metadata including: - **ID**: Unique identifier within an asset source - **Label**: Display name - **Meta**: Custom metadata (URI, dimensions, format) - **Thumbnail URI**: Preview image URL *Assets* are provided by *Asset Sources* and added through the UI or programmatically. ### Asset Source A provider of *Assets*. *Asset Sources* can be built-in, like the default sticker library, or custom. *Asset Sources* implement a query interface returning paginated results with search and filtering. - **Local Asset Source**: Assets defined in JSON, loaded at initialization - **Remote Asset Source**: Custom implementation fetching from external APIs Register *Asset Sources* with the *Engine* to make *Assets* available throughout your application. ### Resource Loaded data from an *Asset* URI. When you reference an image or video URL in a *Block*, the *Engine* fetches and caches the *Resource*. *Resources* include binary data and metadata for rendering. See [Resources](https://img.ly/docs/cesdk/android/concepts/resources-a58d71/) for details. ### Buffer A resizable container for arbitrary binary data. *Buffers* are useful for dynamically generated content that does not come from a URL, such as synthesized audio or programmatically created images. Create a *Buffer*, write data to it, and reference it by URI in *Block* properties. *Buffer* data is not serialized with scenes and changes cannot be undone. See [Buffers](https://img.ly/docs/cesdk/android/concepts/buffers-9c565b/) for details. ## Templating and Automation These terms describe dynamic content and reusable designs. ### Template A reusable design with predefined structure and styling. *Templates* typically contain *Placeholders* and *Variables* that users customize while maintaining overall layout and branding. *Templates* are scenes saved in a format that can be loaded and modified. ### Placeholder A *Block* marked for content replacement. When a *Block's* placeholder property is enabled, it signals that the *Block* expects user-provided content, like an image drop zone or editable text field. *Placeholders* indicate which parts of a design should be customized versus fixed. See [Placeholders](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a/) for details. ### Variable A named value referenced in text blocks using `{{variableName}}` syntax. *Variables* enable data-driven design generation by populating templates with dynamic content. Define *Variables* at the scene level and reference them in text blocks. When a *Variable* value changes, all referencing text blocks update automatically. See [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) for details. ## Permissions and Scopes These terms relate to controlling what operations are allowed. ### Scope A permission setting controlling whether specific operations are allowed on a *Block*. *Scopes* enable fine-grained control over what users can modify, which is essential for template workflows where some elements should be editable and others locked. Common scopes: - `layer/move` — Allow or prevent moving - `layer/resize` — Allow or prevent resizing - `layer/rotate` — Allow or prevent rotation - `layer/visibility` — Allow or prevent hiding - `lifecycle/destroy` — Allow or prevent deletion - `editor/select` — Allow or prevent selection Enable or disable *Scopes* per *Block* to create controlled editing experiences. See [Lock Design Elements](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) for details. ### Role A preset collection of *Scope* settings. CE.SDK defines four built-in *Roles*. On Android, set the role with `editor.setRole(...)`: - **Creator**: Full access to all operations, for template authors - **Adopter**: Restricted access for end users customizing templates - **Viewer**: Read-only access without editing capabilities - **Presenter**: Presentation-focused, non-editing access *Roles* provide a convenient way to apply consistent permission sets. ## Layout and Units These terms relate to positioning and measurement. ### Design Unit The measurement unit for dimensions in a *Scene*. The choice affects how positions, sizes, and exports are interpreted. Options: - **Pixel**: Screen pixels, default for digital designs - **Millimeter**: Metric measurement for print - **Inch**: Imperial measurement for print Set the design unit at the scene level. All dimension values are interpreted in that unit. ### DPI (Dots Per Inch) Resolution setting affecting export quality and unit conversion. Higher DPI produces larger exports with more detail. The default is 300 DPI, suitable for print-quality output. DPI matters when working with physical units, like millimeters or inches, as it determines how measurements translate to pixel dimensions during export. ## Operating Modes These terms describe how CE.SDK runs. ### Scene Capabilities Every *Scene* supports the full range of features: - **Static designs**: Content arranged spatially on pages. - **Video editing**: *Blocks* can have duration, time offset, playback time, and animation properties. On Android, choose the starting configuration when creating the scene: `engine.scene.create()` for a static design layout, or `engine.scene.createForVideo()` for video editing. See [Scenes](https://img.ly/docs/cesdk/android/concepts/scenes-e8596d/) for details. ### Headless Mode Running CE.SDK without the built-in UI. Used for: - Offscreen rendering and export - Automation pipelines - Custom UI implementations - Batch processing In *Headless Mode*, you work directly with *Engine* APIs without the visual editor. See [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) for setup. ## Events and State These terms relate to monitoring changes. ### Event / Subscription A callback mechanism for reacting to changes in the *Engine*. Subscribe to events and receive notifications when state changes. Common events: - Selection changes - Block state changes - History (undo and redo) changes On Android, block lifecycle subscriptions are exposed as Kotlin `Flow`s. Cancel the coroutine collecting the `Flow` when you no longer need notifications. See [Events](https://img.ly/docs/cesdk/android/concepts/events-353f97/) for details. ### Block State The current status of a *Block* indicating readiness or issues: - **Ready**: Normal state, no pending operations - **Pending**: Operation in progress, with optional progress value from 0 to 1 - **Error**: Operation failed, with error type (`IMAGE_DECODING`, `VIDEO_DECODING`, `FILE_FETCH`, `AUDIO_DECODING`, or `UNKNOWN`) *Block State* reflects the combined status of the *Block* and its attached *Fill*, *Shape*, and *Effects*. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Undo and History" description: "Manage undo and redo stacks in CE.SDK using multiple histories, callbacks, and API-based controls." platform: android url: "https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/android/concepts-c9ff51/) > [Undo and History](https://img.ly/docs/cesdk/android/concepts/undo-and-history-99479d/) --- ```kotlin file=@cesdk_android_examples/engine-guides-undo-and-history/UndoAndHistory.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel 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.HistoryUpdate import ly.img.engine.ShapeType fun undoAndHistory( 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 page = createDemoPage(engine) val historyUpdates = subscribeToHistoryUpdates(scope = this, engine = engine) try { val primaryBlock = createPrimaryBlock(engine, page) undoLatestChange(engine) redoLatestChange(engine) applyManualUndoStep(engine, primaryBlock) createSecondaryHistoryDemo(engine, page) } finally { historyUpdates.cancel() engine.stop() } } 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() 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() engine.editor.setActiveHistory(primaryHistory) 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. The standalone sample creates a small offscreen demo page only so the snippets can run in isolation; 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() 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() engine.editor.setActiveHistory(primaryHistory) ``` ### Cleaning Up History Stacks Destroy temporary stacks once the isolated workflow is complete. ```kotlin highlight-android-destroy-history engine.editor.destroyHistory(secondaryHistory) ``` 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 androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.core.engine.EngineRenderTarget // Add this composable to your NavHost @Composable fun BasicEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark userId = "", baseUri = "file:///android_asset/".toUri(), engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, onClose = onClose, ) } ``` In this example, we will show you how to make basic configurations for the mobile editor. ## Configuration The basic configuration settings shown here are passed directly to `Editor`. - `license` - the license to activate the [Engine](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) with. ```kotlin highlight-configuration-license license = license, // pass null or empty for evaluation mode with watermark ``` - `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. The default value is `null`. ```kotlin highlight-configuration-userId userId = "", ``` - `baseUri` - is used to initialize the engine's [setting](https://img.ly/docs/cesdk/android/settings-970c98/) before the editor's [callback](https://img.ly/docs/cesdk/android/user-interface/events-514b70/) is run. It is the foundational URI for constructing absolute paths from relative ones. For example, setting it to the Android assets directory allows loading resources directly from there: `file:///android_asset/`. This URI enables the loading of specific scenes or assets using their relative paths. The default value is pointing at the versioned IMG.LY CDN but it should be changed in production environments. ```kotlin highlight-configuration-baseUri baseUri = "file:///android_asset/".toUri(), ``` - `engineRenderTarget` - the target which should be used by the [Engine](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) to render. The engine is able to render on both [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView) and [TextureView](https://developer.android.com/reference/android/view/TextureView). The default value is `EngineRenderTarget.SURFACE_VIEW`. ```kotlin highlight-configuration-renderTarget engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, ``` ## Full Code Here's the full code: ```kotlin file=@cesdk_android_examples/editor-guides-configuration-basics/BasicEditorSolution.kt import androidx.compose.runtime.Composable import androidx.core.net.toUri import ly.img.editor.Editor import ly.img.editor.core.engine.EngineRenderTarget // Add this composable to your NavHost @Composable fun BasicEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark userId = "", baseUri = "file:///android_asset/".toUri(), engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, 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: "Conversion" description: "Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools." platform: android url: "https://img.ly/docs/cesdk/android/conversion-c3fbb3/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) - Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools. - [To Base64](https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/) - Convert CE.SDK exports to Base64-encoded strings for embedding in URLs, storing in databases, or transmitting via APIs. - [To Blob](https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/) - Export CE.SDK design blocks to Android ByteBuffer data for saving, uploading, or sharing. - [To PNG](https://img.ly/docs/cesdk/android/conversion/to-png-f1660c/) - Export designs and images to PNG format with compression settings and target dimensions using CE.SDK. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools." platform: android url: "https://img.ly/docs/cesdk/android/conversion/overview-44dc58/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) --- CreativeEditor SDK (CE.SDK) exports Android designs to formats such as PNG, PDF, SVG, and MP4 so your app can prepare assets for sharing, printing, storage, or publishing workflows. You can trigger conversions programmatically with the Android Engine API or let users start exports through the editor UI. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Supported Input and Output Formats CE.SDK accepts a range of input formats when working with designs, including: When it comes to exporting or converting designs, the SDK supports the following output formats: | Category | Supported Formats | | ------------ | ---------------------------------------------------- | | **Images** | `.png`, `.jpeg`, `.tga` | | **Vector** | `.svg` with text exported as paths | | **Print** | `.pdf` with compatibility and underlayer options | | **Video** | `.mp4` | | **Scene** | serialized scene strings for `.scene` workflows | | **Blocks** | proprietary block strings or block archive entries such as `blocks.blocks` | | **Archive** | `.zip` archives with scenes or blocks and their assets | | **Raw Data** | binary RGBA8888 image data through `MimeType.BINARY` | Each format serves different use cases, giving you the flexibility to adapt designs for your application’s needs. ## Conversion Methods Use the conversion path that matches how much control your Android app needs over the export workflow. | Method | Android API | Use it for | | ------ | ----------- | ---------- | | Programmatic image, vector, PDF, or raw binary export | `engine.block.export(...)` | Exporting a scene, page, group, or block from Kotlin with a selected `MimeType` and optional `ExportOptions`. | | Programmatic video export | `engine.block.exportVideo(...)` | Exporting a page timeline to MP4 while receiving progress updates. | | Scene and block serialization | `engine.scene.saveToString(scene=_)`, `engine.scene.saveToArchive(scene=_)`, `engine.block.saveToString(blocks=_)`, `engine.block.saveToArchive(blocks=_)` | Persisting editable CE.SDK content either as a serialized scene string or archive, or as proprietary block data or a block archive. | | Color-mask export | `engine.block.exportWithColorMask(...)` | Creating image data plus a mask for workflows that need a separate color mask. | | Editor UI export | `onExport` / built-in export action | Letting users export from the Android editor UI while your app controls the final export handling. | Programmatic exports return binary data that your app can write to storage, upload, share, or pass to another workflow. UI-driven exports are useful when the editor should remain user-facing and the app only needs to customize what happens after the user taps export. ## Customization Options Android exports combine scene-level settings with `ExportOptions` for static formats and `ExportVideoOptions` for MP4 output. | Option | Applies to | Purpose | | ------ | ---------- | ------- | | `targetWidth` / `targetHeight` | Static and video exports | Render output at a specific size while preserving the block aspect ratio. | | `scene/dpi` | Scene exports and design units | Set print metadata and the pixel-to-inch or millimeter conversion, either while creating an image scene with `engine.scene.createFromImage(imageUri = imageUri, dpi = 300F)` or later with `engine.block.setFloat(block = scene, property = "scene/dpi", value = 300F)`. | | `pngCompressionLevel` | PNG | Balance file size and encode time without changing visual quality. | | `jpegQuality` | JPEG | Control compression quality for JPEG exports. | | `exportPdfWithHighCompatibility` | PDF | Rasterize effects and images for broader PDF viewer compatibility. | | `exportPdfWithUnderlayer` and underlayer settings | PDF | Generate print underlayers for production workflows that require them. | | `frameRate`, bitrate, and H.264 settings | MP4 | Tune video export quality, size, and encoding behavior. | Check `engine.editor.getMaxExportSize()` before large raster exports so the requested dimensions stay within the device-supported export limit. Use `targetWidth` and `targetHeight` for explicit pixel dimensions; changing `scene/dpi` does not replace those pixel-size controls. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To Base64" description: "Convert CE.SDK exports to Base64-encoded strings for embedding in URLs, storing in databases, or transmitting via APIs." platform: android url: "https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [To Base64](https://img.ly/docs/cesdk/android/conversion/to-base64-39ff25/) --- ```kotlin file=@cesdk_android_examples/engine-guides-conversion-to-base64/ToBase64.kt reference-only import android.app.Application import android.util.Base64 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Color import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import java.nio.ByteBuffer suspend fun exportDesignToBase64( application: Application, license: String?, userId: String, ): List = withContext(Dispatchers.Main) { Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.to-base64-guide") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1080) try { val scene = engine.scene.create() val page = createSamplePage(engine) engine.block.appendChild(parent = scene, child = page) val mimeType = MimeType.PNG val buffer = engine.block.export(block = page, mimeType = mimeType) val base64 = buffer.toBase64() val dataUri = "data:${mimeType.key};base64,$base64" val inlineImageSource = "data:${mimeType.key};base64,$base64" // Use inlineImageSource wherever your app expects a URI string, // for example in a WebView, JSON payload, or HTML email template. val jpegOptions = ExportOptions(jpegQuality = 0.7F, targetWidth = 720F) val jpegBuffer = engine.block.export( block = page, mimeType = MimeType.JPEG, options = jpegOptions, ) val jpegDataUri = jpegBuffer.toDataUri(MimeType.JPEG) val compressedPngOptions = ExportOptions(pngCompressionLevel = 9, targetWidth = 720F) val compressedPngBuffer = engine.block.export( block = page, mimeType = MimeType.PNG, options = compressedPngOptions, ) val compressedPngDataUri = compressedPngBuffer.toDataUri(MimeType.PNG) val secondPage = createSamplePage(engine) engine.block.appendChild(parent = scene, child = secondPage) val pages = engine.scene.getPages() val pageBuffers = engine.block.export(blocks = pages, mimeType = MimeType.PNG) val pageDataUris = pageBuffers.map { pageBuffer -> pageBuffer.toDataUri(MimeType.PNG) } check(inlineImageSource.startsWith("data:image/png;base64,")) check(jpegDataUri.startsWith("data:image/jpeg;base64,")) check(compressedPngDataUri.startsWith("data:image/png;base64,")) check(pageDataUris.size == pages.size) check(pageDataUris.all { it.startsWith("data:image/png;base64,") }) listOf(dataUri, jpegDataUri, compressedPngDataUri) + pageDataUris } finally { engine.stop() } } private fun ByteBuffer.toBase64(): String { val copy = asReadOnlyBuffer() copy.rewind() val bytes = ByteArray(copy.remaining()) copy.get(bytes) return Base64.encodeToString(bytes, Base64.NO_WRAP) } private fun ByteBuffer.toDataUri(mimeType: MimeType): String = "data:${mimeType.key};base64,${toBase64()}" private fun createSamplePage(engine: Engine): DesignBlock { val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 1080F) engine.block.setHeight(page, value = 1080F) val background = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(background, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(background, color = Color.fromHex("#F4F0EA")) engine.block.appendChild(parent = page, child = background) engine.block.fillParent(background) val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, text = "Base64 export") engine.block.setPositionX(text, value = 96F) engine.block.setPositionY(text, value = 456F) engine.block.setWidth(text, value = 888F) engine.block.setTextFontSize(text, fontSize = 96F) engine.block.setTextColor(text, color = Color.fromHex("#23201D")) engine.block.appendChild(parent = page, child = text) return page } ``` Convert CE.SDK exports to Base64-encoded strings for embedding in HTML, storing in databases, or transmitting via JSON APIs. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-conversion-to-base64) Base64 encoding transforms binary image data into ASCII text. On Android, CE.SDK's `engine.block.export()` returns a `ByteBuffer`; convert that buffer to a Base64 string with Android's `Base64` API, then prepend the MIME type when you need a data URI. ## Export a Block to Base64 Export a design block as PNG and convert the resulting buffer to a Base64 string. ```kotlin highlight-android-export-base64 val mimeType = MimeType.PNG val buffer = engine.block.export(block = page, mimeType = mimeType) val base64 = buffer.toBase64() val dataUri = "data:${mimeType.key};base64,$base64" ``` The export buffer contains the rendered image. `Base64.NO_WRAP` keeps the encoded output on one line, which is required for data URIs and JSON string values. ## Convert Buffer to Base64 Copy the readable bytes out of the `ByteBuffer`, then encode them as Base64 text. ```kotlin highlight-android-convert-buffer private fun ByteBuffer.toBase64(): String { val copy = asReadOnlyBuffer() copy.rewind() val bytes = ByteArray(copy.remaining()) copy.get(bytes) return Base64.encodeToString(bytes, Base64.NO_WRAP) } private fun ByteBuffer.toDataUri(mimeType: MimeType): String = "data:${mimeType.key};base64,${toBase64()}" ``` The helper uses a read-only copy so converting the buffer does not consume the original buffer's position for later file writes or checks. ## Create a Data URI Construct a data URI by combining the MIME type with the Base64 string. Data URIs embed image data directly in HTML, CSS, or WebView content without separate file references. ```kotlin highlight-android-data-uri val inlineImageSource = "data:${mimeType.key};base64,$base64" // Use inlineImageSource wherever your app expects a URI string, // for example in a WebView, JSON payload, or HTML email template. ``` The resulting string follows the format `data:image/png;base64,...`. Use `MimeType.key` so the URI prefix always matches the export format. ## Work with Different MIME Types CE.SDK supports multiple image formats, each with format-specific quality options through `ExportOptions`. ```kotlin highlight-android-mime-types val jpegOptions = ExportOptions(jpegQuality = 0.7F, targetWidth = 720F) val jpegBuffer = engine.block.export( block = page, mimeType = MimeType.JPEG, options = jpegOptions, ) val jpegDataUri = jpegBuffer.toDataUri(MimeType.JPEG) val compressedPngOptions = ExportOptions(pngCompressionLevel = 9, targetWidth = 720F) val compressedPngBuffer = engine.block.export( block = page, mimeType = MimeType.PNG, options = compressedPngOptions, ) val compressedPngDataUri = compressedPngBuffer.toDataUri(MimeType.PNG) ``` | Format | Option | Default | Notes | |--------|--------|---------|-------| | PNG | `pngCompressionLevel` | `5` | Lossless, supports transparency | | JPEG | `jpegQuality` | `0.9` | Lossy, smaller file size, no transparency | > **Note:** Base64 increases data size by approximately 33%. For images larger than 100KB, consider storing the raw bytes or a file URI instead. ## Batch Process Multiple Pages Export all pages in a scene to Base64 strings with the batch export API. Pass the full page list to `engine.block.export(blocks = pages, mimeType = MimeType.PNG)`, then encode each returned buffer. ```kotlin highlight-android-batch val pages = engine.scene.getPages() val pageBuffers = engine.block.export(blocks = pages, mimeType = MimeType.PNG) val pageDataUris = pageBuffers.map { pageBuffer -> pageBuffer.toDataUri(MimeType.PNG) } ``` The batch API returns one buffer per input block in the same order, so keep the page list and encoded strings aligned when you persist metadata. ## When to Use Base64 Base64 encoding is useful for: - Embedding images in HTML email templates or WebView content - Storing image data in text-only databases or `SharedPreferences` - Transmitting images through JSON APIs that don't support binary data - Creating inline data URIs for CSS backgrounds For large images or file storage, write the buffer bytes directly to disk instead of expanding them into Base64 text. ## Troubleshooting **Base64 string too long** — Use JPEG with a lower `jpegQuality`, reduce dimensions with `targetWidth` and `targetHeight`, or store the raw bytes when your API accepts binary data. **Image not displaying** — Verify the data URI starts with `data:${mimeType.key};base64,` and that the Base64 payload was not truncated during storage or transport. **Memory pressure with batch exports** — Keep batch exports scoped to the pages or blocks you need. Convert each `ByteBuffer` through a read-only copy so checks or later writes do not depend on a consumed buffer position. ## API Reference | API | Purpose | |-----|---------| | `engine.block.export(block=_, mimeType=_, options=_)` | Export one `DesignBlock` to a `ByteBuffer` with image format and size options | | `engine.block.export(blocks=_, mimeType=_, options=_)` | Export multiple blocks and receive one `ByteBuffer` per block in input order | | `engine.scene.getPages()` | Get the scene pages for page-by-page batch exports | | `ExportOptions(jpegQuality=_, pngCompressionLevel=_, targetWidth=_, targetHeight=_)` | Configure compression quality and output dimensions for supported image exports | | `Base64.encodeToString(input=_, flags=Base64.NO_WRAP)` | Encode exported bytes as a single-line Base64 string | ## Next Steps - [Export Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) — Explore all available export formats and configuration - [Export to PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) — Generate PDFs for print and document workflows --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To Blob" description: "Export CE.SDK design blocks to Android ByteBuffer data for saving, uploading, or sharing." platform: android url: "https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [To Blob](https://img.ly/docs/cesdk/android/conversion/to-blob-4e6493/) --- ```kotlin file=@cesdk_android_examples/engine-guides-to-blob/ToBlob.kt reference-only import android.app.Application import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import ly.img.engine.Color import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import java.io.File import java.io.FileOutputStream import java.nio.ByteBuffer data class ToBlobResult( val pngData: ByteBuffer, val jpegData: ByteBuffer, val pageExports: List, val savedPngFile: File, ) fun toBlob( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, outputDir: File, ): Deferred = CoroutineScope(Dispatchers.Main).async { val engine = startToBlobEngine( application = application, license = license, userId = userId, ) try { val pages = createExportScene(engine) val page = pages.first() val pngData = exportBlockToBinaryData(engine, page).copyForVerification() val jpegData = exportWithOptions(engine, page).copyForVerification() val pageExports = exportMultipleBlocks(engine).map(ByteBuffer::copyForVerification) val savedPngFile = saveByteBufferToFile( buffer = pngData, outputFile = File(outputDir, "to-blob-page.png"), ) ToBlobResult( pngData = pngData, jpegData = jpegData, pageExports = pageExports, savedPngFile = savedPngFile, ) } finally { engine.stop() } } suspend fun startToBlobEngine( application: Application, license: String?, userId: String, ): Engine { Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.toBlob") try { engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) return engine } catch (error: Throwable) { engine.stop() throw error } } private fun ByteBuffer.copyForVerification(): ByteBuffer { val duplicate = asReadOnlyBuffer() val bytes = ByteArray(duplicate.remaining()) duplicate.get(bytes) return ByteBuffer.wrap(bytes).asReadOnlyBuffer() } private fun createExportScene(engine: Engine): List { val scene = engine.scene.create() return List(2) { pageIndex -> val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.appendChild(parent = scene, child = page) addPageContent(engine, page, pageIndex) page } } private fun addPageContent( engine: Engine, page: DesignBlock, pageIndex: Int, ) { val background = engine.block.create(DesignBlockType.Graphic) engine.block.setName(background, "To Blob background") engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(background, value = 1280F) engine.block.setHeight(background, value = 720F) engine.block.setFill(background, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = background, color = Color.fromHex("#FFF8FAFC"), ) engine.block.appendChild(parent = page, child = background) val panel = engine.block.create(DesignBlockType.Graphic) engine.block.setName(panel, "To Blob export panel") engine.block.setShape(panel, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(panel, value = 820F) engine.block.setHeight(panel, value = 420F) engine.block.setPositionX(panel, value = 230F) engine.block.setPositionY(panel, value = 150F) engine.block.setFill(panel, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = panel, color = if (pageIndex == 0) Color.fromHex("#FF1F6FEB") else Color.fromHex("#FFCF3E53"), ) engine.block.appendChild(parent = page, child = panel) val stripe = engine.block.create(DesignBlockType.Graphic) engine.block.setName(stripe, "To Blob accent stripe") engine.block.setShape(stripe, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(stripe, value = 820F) engine.block.setHeight(stripe, value = 72F) engine.block.setPositionX(stripe, value = 230F) engine.block.setPositionY(stripe, value = 498F) engine.block.setFill(stripe, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = stripe, color = Color.fromHex("#FF111827"), ) engine.block.appendChild(parent = page, child = stripe) } suspend fun exportBlockToBinaryData( engine: Engine, page: DesignBlock, ): ByteBuffer { val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, ) check(pngData.hasRemaining()) { "PNG export is empty" } return pngData } suspend fun exportWithOptions( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions( jpegQuality = 0.8F, targetWidth = 1920F, targetHeight = 1080F, ) val jpegData = engine.block.export( block = page, mimeType = MimeType.JPEG, options = options, ) check(jpegData.hasRemaining()) { "JPEG export is empty" } return jpegData } suspend fun exportMultipleBlocks(engine: Engine): List { val pages = engine.scene.getPages() val pngBuffers = engine.block.export( blocks = pages, mimeType = MimeType.PNG, ) check(pngBuffers.size == pages.size) pngBuffers.forEachIndexed { index, pngData -> check(pngData.hasRemaining()) { "PNG export ${index + 1} is empty" } } return pngBuffers } suspend fun saveByteBufferToFile( buffer: ByteBuffer, outputFile: File, ): File = withContext(Dispatchers.IO) { outputFile.parentFile?.mkdirs() val readableBuffer = buffer.asReadOnlyBuffer() FileOutputStream(outputFile).channel.use { channel -> while (readableBuffer.hasRemaining()) { channel.write(readableBuffer) } } check(outputFile.length() > 0L) { "Saved export is empty" } outputFile } ``` Export design blocks to binary `ByteBuffer` data for saving to disk, uploading to a server, sharing, or converting to platform images. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-to-blob) CE.SDK's `engine.block.export()` method renders a page, scene, or individual design block into binary data. Android returns a `ByteBuffer`, so your app can write the result to app-controlled storage, upload it, share it, or decode image exports with Android platform APIs. ## Export a Block to Binary Data Call `engine.block.export(...)` with the block ID and target MIME type. This example exports an existing page to PNG and checks that the returned `ByteBuffer` contains data. ```kotlin highlight-android-export-png suspend fun exportBlockToBinaryData( engine: Engine, page: DesignBlock, ): ByteBuffer { val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, ) check(pngData.hasRemaining()) { "PNG export is empty" } return pngData } ``` Supported export MIME type constants include `MimeType.PNG`, `MimeType.JPEG`, `MimeType.TGA`, `MimeType.SVG`, `MimeType.PDF`, and `MimeType.BINARY`. Use `engine.block.exportVideo(...)` for MP4 video output. ## Configure Export Options Pass `ExportOptions` when you need to control format-specific quality or output dimensions. This JPEG example uses `jpegQuality = 0.8F` and scales the rendered output to `1920 x 1080`. ```kotlin highlight-android-export-options suspend fun exportWithOptions( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions( jpegQuality = 0.8F, targetWidth = 1920F, targetHeight = 1080F, ) val jpegData = engine.block.export( block = page, mimeType = MimeType.JPEG, options = options, ) check(jpegData.hasRemaining()) { "JPEG export is empty" } return jpegData } ``` Format-specific options are ignored by other formats. For example, `jpegQuality` only affects JPEG exports, while `targetWidth` and `targetHeight` resize image output when both dimensions are set. ## Export Multiple Blocks Use the batch overload when you need one binary export per page or block. The returned list follows the input order and the export reuses one worker engine for the batch. ```kotlin highlight-android-export-multiple suspend fun exportMultipleBlocks(engine: Engine): List { val pages = engine.scene.getPages() val pngBuffers = engine.block.export( blocks = pages, mimeType = MimeType.PNG, ) check(pngBuffers.size == pages.size) pngBuffers.forEachIndexed { index, pngData -> check(pngData.hasRemaining()) { "PNG export ${index + 1} is empty" } } return pngBuffers } ``` ## Save to Disk Use a duplicate or read-only view before writing a `ByteBuffer` if the same buffer is also needed for later validation, upload, or decoding. ```kotlin highlight-android-save-to-file suspend fun saveByteBufferToFile( buffer: ByteBuffer, outputFile: File, ): File = withContext(Dispatchers.IO) { outputFile.parentFile?.mkdirs() val readableBuffer = buffer.asReadOnlyBuffer() FileOutputStream(outputFile).channel.use { channel -> while (readableBuffer.hasRemaining()) { channel.write(readableBuffer) } } check(outputFile.length() > 0L) { "Saved export is empty" } outputFile } ``` The sample writes into an app-controlled `File`. Use the same binary data with your own upload or share pipeline when the exported file should leave local storage. ## API Reference | API | Purpose | | --- | --- | | `engine.block.export(block=_, mimeType=_, options=_)` | Export one block as a `ByteBuffer` | | `engine.block.export(blocks=_, mimeType=_, options=_)` | Export several blocks as a `List` | | `ExportOptions(jpegQuality=_, targetWidth=_, targetHeight=_)` | Configure output quality and dimensions | | `engine.scene.getPages()` | Get pages for batch export examples | ## Next Steps - [Conversion Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) - See all supported export formats. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To PNG" description: "Export designs and images to PNG format with compression settings and target dimensions using CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/conversion/to-png-f1660c/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/android/conversion-c3fbb3/) > [To PNG](https://img.ly/docs/cesdk/android/conversion/to-png-f1660c/) --- ```kotlin file=@cesdk_android_examples/engine-guides-conversion-to-png/ConversionToPng.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import ly.img.engine.Color import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import ly.img.engine.SizeMode import java.nio.ByteBuffer data class PngExport( val label: String, val pngData: ByteBuffer, ) data class ConversionToPngResult( val singlePage: PngExport, val allPages: List, val compressed: PngExport, val targetDimensions: PngExport, val textOverhang: PngExport, ) { val allExports: List get() = listOf(singlePage) + allPages + listOf(compressed, targetDimensions, textOverhang) } fun conversionToPng( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): Deferred = CoroutineScope(Dispatchers.Main).async { val engine = Engine.getInstance(id = "ly.img.engine.example") try { engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) createSceneWithPages(engine) val currentPage = engine.scene.getCurrentPage() ?: engine.scene.getPages().first() ConversionToPngResult( singlePage = PngExport("single page", exportSinglePage(engine, currentPage).copyForVerification()), allPages = exportAllPages(engine).mapIndexed { index, pngData -> PngExport("page ${index + 1}", pngData.copyForVerification()) }, compressed = PngExport("compressed", exportWithCompression(engine, currentPage).copyForVerification()), targetDimensions = PngExport( "target dimensions", exportWithTargetDimensions(engine, currentPage).copyForVerification(), ), textOverhang = PngExport( "text overhang", exportWithTextOverhang(engine, currentPage).copyForVerification(), ), ) } finally { engine.stop() } } private fun ByteBuffer.copyForVerification(): ByteBuffer { val duplicate = asReadOnlyBuffer() val bytes = ByteArray(duplicate.remaining()) duplicate.get(bytes) return ByteBuffer.wrap(bytes).asReadOnlyBuffer() } fun createSceneWithPages(engine: Engine): DesignBlock { val scene = engine.scene.create() repeat(2) { pageIndex -> val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) addVisiblePageContent(engine, page, pageIndex) } return scene } private fun addVisiblePageContent( engine: Engine, page: DesignBlock, pageIndex: Int, ) { val background = engine.block.create(DesignBlockType.Graphic) engine.block.setName(background, "PNG export background") engine.block.setShape(background, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(background, value = 800F) engine.block.setHeight(background, value = 600F) engine.block.setFill(background, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = background, color = Color.fromHex("#FFFFFBF1"), ) engine.block.appendChild(parent = page, child = background) val accent = engine.block.create(DesignBlockType.Graphic) engine.block.setName(accent, "PNG export accent") engine.block.setShape(accent, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(accent, value = 520F) engine.block.setHeight(accent, value = 280F) engine.block.setPositionX(accent, value = 140F) engine.block.setPositionY(accent, value = 150F) engine.block.setFill(accent, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = accent, color = if (pageIndex == 0) Color.fromHex("#FF2457D6") else Color.fromHex("#FFE15D2A"), ) engine.block.appendChild(parent = page, child = accent) val label = engine.block.create(DesignBlockType.Text) engine.block.setName(label, "PNG export text") engine.block.setPositionX(label, value = 190F) engine.block.setPositionY(label, value = 245F) engine.block.setWidthMode(label, mode = SizeMode.AUTO) engine.block.setHeightMode(label, mode = SizeMode.AUTO) engine.block.replaceText(label, text = "PNG ${pageIndex + 1}") engine.block.setTextFontSize(label, fontSize = 88F) engine.block.setTextColor(label, color = Color.fromHex("#FFFFFFFF")) engine.block.appendChild(parent = page, child = label) // This fixed frame gives the allowTextOverhang export option real text glyphs to preserve. val overhangText = engine.block.create(DesignBlockType.Text) engine.block.setName(overhangText, "Text overhang sample") engine.block.setPositionX(overhangText, value = 84F) engine.block.setPositionY(overhangText, value = 438F) engine.block.setWidth(overhangText, value = 260F) engine.block.setHeight(overhangText, value = 56F) engine.block.replaceText(overhangText, text = "Jolly glyphs") engine.block.setTextFontSize(overhangText, fontSize = 72F) engine.block.setTextColor(overhangText, color = Color.fromHex("#FF0B1220")) engine.block.appendChild(parent = page, child = overhangText) } suspend fun exportSinglePage( engine: Engine, page: DesignBlock, ): ByteBuffer { val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, ) check(pngData.hasRemaining()) { "single page PNG export is empty" } return pngData } suspend fun exportAllPages(engine: Engine): List { val pages = engine.scene.getPages() val pngFiles = engine.block.export( blocks = pages, mimeType = MimeType.PNG, ) check(pngFiles.size == pages.size) pngFiles.forEachIndexed { index, pngData -> check(pngData.hasRemaining()) { "page ${index + 1} PNG export is empty" } } return pngFiles } suspend fun exportWithCompression( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions(pngCompressionLevel = 9) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = options, ) check(pngData.hasRemaining()) { "compressed PNG export is empty" } return pngData } suspend fun exportWithTargetDimensions( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions( targetWidth = 1200F, targetHeight = 900F, ) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = options, ) check(pngData.hasRemaining()) { "target dimensions PNG export is empty" } return pngData } suspend fun exportWithTextOverhang( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions(allowTextOverhang = true) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = options, ) check(pngData.hasRemaining()) { "text overhang PNG export is empty" } return pngData } ``` Export designs to PNG format with lossless quality and optional transparency support. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-conversion-to-png) PNG is a lossless image format that preserves image quality and supports transparency. It's ideal for designs requiring pixel-perfect fidelity, logos, graphics with transparent backgrounds, and any content where quality cannot be compromised. This guide covers how to export designs to PNG and configure export options using the Android Engine API. ## Export to PNG Use `engine.block.export(...)` with `MimeType.PNG` to export a design block to PNG. The method returns a `ByteBuffer` containing the image data. ```kotlin highlight-android-export-single-page suspend fun exportSinglePage( engine: Engine, page: DesignBlock, ): ByteBuffer { val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, ) check(pngData.hasRemaining()) { "single page PNG export is empty" } return pngData } ``` ## Export All Pages Export all pages in a scene using the batch export API. Pass the page list to `engine.block.export(...)`, which returns one `ByteBuffer` for each page in the same order. ```kotlin highlight-android-export-all-pages suspend fun exportAllPages(engine: Engine): List { val pages = engine.scene.getPages() val pngFiles = engine.block.export( blocks = pages, mimeType = MimeType.PNG, ) check(pngFiles.size == pages.size) pngFiles.forEachIndexed { index, pngData -> check(pngData.hasRemaining()) { "page ${index + 1} PNG export is empty" } } return pngFiles } ``` The batch API reuses a single worker engine for all exports, making it more memory efficient than exporting pages individually. ## Compression Level Control the file size versus export speed tradeoff using `pngCompressionLevel` in `ExportOptions`. Valid values are 0-9, where higher values produce smaller files but take longer to export. Since PNG is lossless, image quality remains unchanged. ```kotlin highlight-android-compression-level suspend fun exportWithCompression( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions(pngCompressionLevel = 9) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = options, ) check(pngData.hasRemaining()) { "compressed PNG export is empty" } return pngData } ``` The default compression level is 5, providing a good balance between file size and export speed. ## Target Dimensions Resize the output by setting `targetWidth` and `targetHeight`. The block scales to fill the target dimensions while maintaining its aspect ratio. ```kotlin highlight-android-target-dimensions suspend fun exportWithTargetDimensions( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions( targetWidth = 1200F, targetHeight = 900F, ) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = options, ) check(pngData.hasRemaining()) { "target dimensions PNG export is empty" } return pngData } ``` Set both values when you need predictable output dimensions. Leave both values `null` to use the block's native size. ## Text Overhang Decorative fonts sometimes have glyphs that extend beyond their frame. Set `allowTextOverhang` to `true` to prevent clipping these glyphs during export. ```kotlin highlight-android-text-overhang suspend fun exportWithTextOverhang( engine: Engine, page: DesignBlock, ): ByteBuffer { val options = ExportOptions(allowTextOverhang = true) val pngData = engine.block.export( block = page, mimeType = MimeType.PNG, options = options, ) check(pngData.hasRemaining()) { "text overhang PNG export is empty" } return pngData } ``` ## Troubleshooting | Issue | Fix | | --- | --- | | Large PNG files | Increase `pngCompressionLevel` toward `9` to reduce file size. Higher compression can take longer, but PNG quality remains lossless. | | Wrong output dimensions | Set `targetWidth` and `targetHeight` together when you need predictable output dimensions. Leave both values `null` only when you want the block's native size. | | Decorative text appears clipped | Set `allowTextOverhang` to `true` when fonts extend outside their text frame. | ## API Reference | API | Description | | --- | --- | | `engine.block.export(block=_, mimeType=MimeType.PNG, options=_)` | Exports a single block to `ByteBuffer` with the specified options | | `engine.block.export(blocks=_, mimeType=MimeType.PNG, options=_)` | Exports multiple blocks, returning one `ByteBuffer` per block | | `engine.scene.getCurrentPage()` | Returns the current page block ID or `null` | | `engine.scene.getPages()` | Returns all page block IDs in the scene | ### Key Options | Option | Description | | --- | --- | | `pngCompressionLevel` | Controls PNG compression from `0` to `9` | | `targetWidth` and `targetHeight` | Resize the export when both dimensions are set | | `allowTextOverhang` | Allows glyphs to extend beyond text frames during export | ## Next Steps - [Conversion Overview](https://img.ly/docs/cesdk/android/conversion/overview-44dc58/) - Learn about other export formats - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Understand the full export workflow --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Audio" description: "Create audio blocks, extract tracks from video, control playback, generate waveforms, and manage audio timing in CE.SDK for Android." platform: android url: "https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/android/create-audio/audio-2f700b/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-audio-audio/Audio.kt reference-only import android.net.Uri import android.util.Log import kotlinx.coroutines.flow.toList import ly.img.engine.AudioFromVideoOptions import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType import java.io.ByteArrayOutputStream import kotlin.math.abs private const val TAG = "AudioGuide" suspend fun audio(engine: Engine): String { val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 12.0) val audioBlock = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = audioBlock) engine.block.setUri( block = audioBlock, property = "audio/fileURI", value = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a"), ) engine.block.forceLoadAVResource(audioBlock) val resourceDuration = engine.block.getAVResourceTotalDuration(audioBlock) val videoBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) engine.block.setFill(videoBlock, fill = videoFill) engine.block.appendChild(parent = page, child = videoBlock) engine.block.forceLoadAVResource(videoFill) val trackCountBeforeExtraction = engine.block.getAudioTrackCountFromVideo(videoFill) check(trackCountBeforeExtraction > 0) { "Video source must contain an audio track." } val extractedAudioBlock = engine.block.createAudioFromVideo( videoFill = videoFill, trackIndex = 0, options = AudioFromVideoOptions( keepTrimSettings = true, muteOriginalVideo = true, ), ) engine.block.appendChild(parent = page, child = extractedAudioBlock) val allExtractedAudioBlocks = engine.block.createAudiosFromVideo( videoFill = videoFill, options = AudioFromVideoOptions( keepTrimSettings = true, muteOriginalVideo = true, ), ) allExtractedAudioBlocks.forEach { audio -> engine.block.appendChild(parent = page, child = audio) } val audioTrackCount = engine.block.getAudioTrackCountFromVideo(videoFill) Log.i(TAG, "Video has $audioTrackCount audio track(s).") engine.block.setPlaybackTime(page, time = 3.0) val playbackTime = engine.block.getPlaybackTime(page) engine.block.setPlaying(page, enabled = true) val playing = engine.block.isPlaying(page) engine.block.setPlaying(page, enabled = false) val paused = !engine.block.isPlaying(page) engine.block.setVolume(audioBlock, volume = 0.7F) val volume = engine.block.getVolume(audioBlock) engine.block.setMuted(audioBlock, muted = true) val muted = engine.block.isMuted(audioBlock) engine.block.setPlaybackSpeed(audioBlock, speed = 1.25F) val playbackSpeed = engine.block.getPlaybackSpeed(audioBlock) engine.block.setSoloPlaybackEnabled(audioBlock, enabled = true) val soloPlaybackEnabled = engine.block.isSoloPlaybackEnabled(audioBlock) engine.block.setSoloPlaybackEnabled(audioBlock, enabled = false) val soloPlaybackDisabled = !engine.block.isSoloPlaybackEnabled(audioBlock) engine.block.setPlaybackSpeed(audioBlock, speed = 1.0F) engine.block.setTimeOffset(audioBlock, offset = 2.0) val timeOffset = engine.block.getTimeOffset(audioBlock) engine.block.setDuration(audioBlock, duration = 8.0) engine.block.setTrimOffset(audioBlock, offset = 1.0) val trimOffset = engine.block.getTrimOffset(audioBlock) engine.block.setLooping(audioBlock, looping = true) val looping = engine.block.isLooping(audioBlock) engine.block.setTrimLength(audioBlock, length = 6.0) val trimLength = engine.block.getTrimLength(audioBlock) val blockDuration = engine.block.getDuration(audioBlock) val waveformChunks = engine.block.generateAudioThumbnailSequence( block = audioBlock, samplesPerChunk = 4, timeBegin = 0.0, timeEnd = 4.0, numberOfSamples = 16, numberOfChannels = 1, ).toList() val waveformSampleCount = waveformChunks.sumOf { chunk -> chunk.samples.size } val transientAudioResources = engine.editor.findAllTransientResources() transientAudioResources.forEach { (transientUri, _) -> val resourceBytes = ByteArrayOutputStream() engine.editor.getResourceData( uri = transientUri, chunkSize = 64 * 1024, ) { chunk -> val copy = chunk.duplicate() val bytes = ByteArray(copy.remaining()) copy.get(bytes) resourceBytes.write(bytes) true } val permanentUri = uploadTransientAudioResource( sourceUri = transientUri, data = resourceBytes.toByteArray(), ) engine.editor.relocateResource( currentUri = transientUri, relocatedUri = permanentUri, ) } val remainingTransientAudioResources = engine.editor.findAllTransientResources() val savedScene = engine.scene.saveToString( scene = scene, allowedResourceSchemes = listOf("http", "https"), ) check(audioBlock != extractedAudioBlock) check(audioTrackCount > 0) check(allExtractedAudioBlocks.size == audioTrackCount) check(abs(playbackTime - 3.0) < 0.001) check(playing) check(paused) check(abs(volume - 0.7F) < 0.001F) check(muted) check(abs(playbackSpeed - 1.25F) < 0.001F) check(soloPlaybackEnabled) check(soloPlaybackDisabled) check(abs(timeOffset - 2.0) < 0.001) check(abs(blockDuration - 8.0) < 0.001) check(abs(trimOffset - 1.0) < 0.001) check(abs(trimLength - 6.0) < 0.001) check(looping) check(resourceDuration > 0.0) check(waveformChunks.isNotEmpty()) check(waveformSampleCount > 0) check(transientAudioResources.isNotEmpty()) check(remainingTransientAudioResources.isEmpty()) check(savedScene.isNotBlank()) return savedScene } private fun uploadTransientAudioResource( sourceUri: Uri, data: ByteArray, ): Uri { check(data.isNotEmpty()) { "Cannot persist an empty audio resource." } // Replace this with your app's storage client and return its permanent URI. // Transient buffer URIs do not carry stable file names, so the app owns the storage key. val sourceId = sourceUri.toString().hashCode().toString(radix = 16) val fileName = "extracted-audio-$sourceId-${data.size}.m4a" return Uri.parse("https://your-storage.example/audio/$fileName") } ``` Add audio to video scenes, extract audio from video fills, control playback, and generate waveform data with CE.SDK for Android. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-audio-audio) Audio blocks let you add background music, voice-overs, sound effects, and other standalone audio to video scenes. CE.SDK also exposes video audio track counts, playback controls, trim controls, and waveform generation through the Engine block API. Playback examples require an Engine instance created with `AudioContext.AUTO`, which is the default. Engines created with `AudioContext.NONE` can still edit and export scenes, but they do not play audio. ## Use Cases Use CE.SDK audio APIs when you need to add or manage: - Background music - Voice-overs - Sound effects - Podcast or narration tracks ## How Audio Works in CE.SDK CE.SDK represents standalone audio as `DesignBlockType.Audio` blocks. Audio blocks are attached to a page, reference their media through `audio/fileURI`, and use the same timeline properties as other time-based blocks. Each audio block can have: - A source URI for standalone audio files - Playback properties such as volume, mute state, playback time, and speed - Timeline properties such as offset, duration, trim offset, and trim length - Waveform sample data for custom timeline UIs Extraction APIs create separate audio blocks from video fill tracks instead of changing an existing audio block's source. ### What Are the Time-Based Properties Audio timing is expressed in seconds: - **Offset**: when the audio block starts relative to its parent. - **Duration**: how long the block stays active on the timeline. - **Trim offset**: where playback starts inside the source audio. - **Trim length**: how much of the source audio is used before it loops or stops. Use the looping APIs to choose that behavior. ### What Are Waveforms Waveforms are sampled audio amplitudes that you can render in a custom UI. On Android, `generateAudioThumbnailSequence()` returns a `Flow` of `AudioThumbnailResult` chunks. Each chunk contains normalized samples in the range from 0 to 1. Stereo requests interleave left and right samples. ### When to Create vs. Extract Audio Create an audio block when the source is an external audio file such as background music or a voice-over. Extract audio when the sound already exists inside a video fill and you need a separate audio block for editing, trimming, or muting the original video. ## Examples The snippets below use a video scene with an existing page. Audio APIs run on the main thread through the same Engine instance as the rest of your scene edits. ### Create Audio Create a standalone audio block, attach it to the page, set its source URI, and load the resource before reading media metadata. ```kotlin highlight-android-create-audio val audioBlock = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = audioBlock) engine.block.setUri( block = audioBlock, property = "audio/fileURI", value = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a"), ) engine.block.forceLoadAVResource(audioBlock) val resourceDuration = engine.block.getAVResourceTotalDuration(audioBlock) ``` ### Extract or Count Video Audio Create a video fill and wait for the suspend `forceLoadAVResource()` API to complete before extracting or counting video audio. The tabs below use this loaded `videoFill`. ```kotlin highlight-android-video-fill-setup val videoBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(videoBlock, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) engine.block.setFill(videoBlock, fill = videoFill) engine.block.appendChild(parent = page, child = videoBlock) engine.block.forceLoadAVResource(videoFill) ``` Check that the loaded source contains audio, and extract the first track into a new audio block. `AudioFromVideoOptions` keeps the trim settings and mutes the source video fill. ```kotlin highlight-android-extract-audio val trackCountBeforeExtraction = engine.block.getAudioTrackCountFromVideo(videoFill) check(trackCountBeforeExtraction > 0) { "Video source must contain an audio track." } val extractedAudioBlock = engine.block.createAudioFromVideo( videoFill = videoFill, trackIndex = 0, options = AudioFromVideoOptions( keepTrimSettings = true, muteOriginalVideo = true, ), ) engine.block.appendChild(parent = page, child = extractedAudioBlock) ``` Use `createAudiosFromVideo()` when the source may contain multiple audio tracks and each track should become its own audio block. ```kotlin highlight-android-extract-all-audio val allExtractedAudioBlocks = engine.block.createAudiosFromVideo( videoFill = videoFill, options = AudioFromVideoOptions( keepTrimSettings = true, muteOriginalVideo = true, ), ) allExtractedAudioBlocks.forEach { audio -> engine.block.appendChild(parent = page, child = audio) } ``` Use `getAudioTrackCountFromVideo()` before extraction when your loaded source may be silent or contain multiple audio tracks. When your app needs to choose a track by metadata, call `getAudioInfoFromVideo()` after the same load step to read each track's `AudioTrackInfo`, including audio codec, channel count, sample rate, audio duration, packet and frame counts, track name, container track index, and ISO 639-2T language. Pass the returned list position to `createAudioFromVideo(trackIndex=...)`; this parameter is the zero-based audio-track ordinal. Do not pass `AudioTrackInfo.trackIndex` directly, because that value is the original container track index and may not match the list position. ```kotlin highlight-android-track-info val audioTrackCount = engine.block.getAudioTrackCountFromVideo(videoFill) Log.i(TAG, "Video has $audioTrackCount audio track(s).") ``` ### Control Audio Playback Playback time and play or pause state are usually controlled on the page so all time-based blocks stay synchronized. Volume, mute state, and playback speed are set on the audio block itself. ```kotlin highlight-android-playback-control engine.block.setPlaybackTime(page, time = 3.0) val playbackTime = engine.block.getPlaybackTime(page) engine.block.setPlaying(page, enabled = true) val playing = engine.block.isPlaying(page) engine.block.setPlaying(page, enabled = false) val paused = !engine.block.isPlaying(page) engine.block.setVolume(audioBlock, volume = 0.7F) val volume = engine.block.getVolume(audioBlock) engine.block.setMuted(audioBlock, muted = true) val muted = engine.block.isMuted(audioBlock) engine.block.setPlaybackSpeed(audioBlock, speed = 1.25F) val playbackSpeed = engine.block.getPlaybackSpeed(audioBlock) engine.block.setSoloPlaybackEnabled(audioBlock, enabled = true) val soloPlaybackEnabled = engine.block.isSoloPlaybackEnabled(audioBlock) engine.block.setSoloPlaybackEnabled(audioBlock, enabled = false) val soloPlaybackDisabled = !engine.block.isSoloPlaybackEnabled(audioBlock) ``` Audio speed supports values from 0.25 to 3.0 for audio blocks. Changing speed also changes how long the block takes on the timeline. ### Manage Audio Timing Use offset and duration to place the audio block on the scene timeline. Use trim offset and trim length to choose which part of the source file plays, and set looping when the trimmed source should repeat while the block remains active. ```kotlin highlight-android-timing engine.block.setPlaybackSpeed(audioBlock, speed = 1.0F) engine.block.setTimeOffset(audioBlock, offset = 2.0) val timeOffset = engine.block.getTimeOffset(audioBlock) engine.block.setDuration(audioBlock, duration = 8.0) engine.block.setTrimOffset(audioBlock, offset = 1.0) val trimOffset = engine.block.getTrimOffset(audioBlock) engine.block.setLooping(audioBlock, looping = true) val looping = engine.block.isLooping(audioBlock) engine.block.setTrimLength(audioBlock, length = 6.0) val trimLength = engine.block.getTrimLength(audioBlock) val blockDuration = engine.block.getDuration(audioBlock) ``` Load the audio resource before trimming so CE.SDK can read the source duration and metadata. ### Generate Audio Thumbnails Waveform generation emits a `Flow` of chunks. Choose `samplesPerChunk`, a time range, the total number of samples, and the number of channels you want to render. ```kotlin highlight-android-waveform val waveformChunks = engine.block.generateAudioThumbnailSequence( block = audioBlock, samplesPerChunk = 4, timeBegin = 0.0, timeEnd = 4.0, numberOfSamples = 16, numberOfChannels = 1, ).toList() val waveformSampleCount = waveformChunks.sumOf { chunk -> chunk.samples.size } ``` Render the returned sample values in your own timeline or waveform component. ### Save Audio Scenes The current Android binding does not expose an audio-only export method. Persist audio edits by saving the scene, or export a video page with audio through the video export APIs when you need an MP4 result. Extracted audio can reference a transient `buffer://` resource. Before calling `saveToString()`, read each transient resource, store it with your app's storage client, then call `relocateResource()` so the scene contains durable URIs only. The default `allowedResourceSchemes` list is `blob`, `bundle`, `file`, `http`, and `https`; `buffer://` is always transient. The sample narrows the list to `http` and `https`, so saving fails if any extracted `buffer://` resource or local `blob`, `bundle`, or `file` resource remains instead of being relocated. ```kotlin highlight-android-save-scene val transientAudioResources = engine.editor.findAllTransientResources() transientAudioResources.forEach { (transientUri, _) -> val resourceBytes = ByteArrayOutputStream() engine.editor.getResourceData( uri = transientUri, chunkSize = 64 * 1024, ) { chunk -> val copy = chunk.duplicate() val bytes = ByteArray(copy.remaining()) copy.get(bytes) resourceBytes.write(bytes) true } val permanentUri = uploadTransientAudioResource( sourceUri = transientUri, data = resourceBytes.toByteArray(), ) engine.editor.relocateResource( currentUri = transientUri, relocatedUri = permanentUri, ) } val remainingTransientAudioResources = engine.editor.findAllTransientResources() val savedScene = engine.scene.saveToString( scene = scene, allowedResourceSchemes = listOf("http", "https"), ) ``` The upload helper represents your app storage layer. Replace it with a real upload or local persistence implementation that returns a URI your app can load later. ```kotlin highlight-android-upload-helper private fun uploadTransientAudioResource( sourceUri: Uri, data: ByteArray, ): Uri { check(data.isNotEmpty()) { "Cannot persist an empty audio resource." } // Replace this with your app's storage client and return its permanent URI. // Transient buffer URIs do not carry stable file names, so the app owns the storage key. val sourceId = sourceUri.toString().hashCode().toString(radix = 16) val fileName = "extracted-audio-$sourceId-${data.size}.m4a" return Uri.parse("https://your-storage.example/audio/$fileName") } ``` ## API Reference The table below lists the Android APIs used in the examples above. | Category | API | Purpose | | --- | --- | --- | | Engine audio context | `Engine.getInstance(id=_, audioContext=AudioContext.AUTO)` | Create an Engine instance that can play audio | | Create blocks | `engine.block.create(blockType=DesignBlockType.Audio)` | Create a standalone audio block | | Create blocks | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a block that can display the video fill | | Create blocks | `engine.block.createShape(type=_)` | Create the video block shape | | Create blocks | `engine.block.createFill(fillType=_)` | Create a video fill for extraction | | Scene hierarchy | `engine.block.appendChild(parent=_, child=_)` | Attach audio, video, or extracted blocks to the scene hierarchy | | Assign sources | `engine.block.setUri(block=_, property="audio/fileURI", value=_)` | Attach an audio file URI to an audio block | | Assign sources | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Attach a video file URI to a video fill | | Video fill setup | `engine.block.setShape(block=_, shape=_)` | Assign a shape to the video block | | Video fill setup | `engine.block.setFill(block=_, fill=_)` | Assign the loaded video fill to the video block | | Extract video audio | `engine.block.createAudioFromVideo(videoFill=_, trackIndex=_, options=_)` | Extract one audio track by zero-based audio-track ordinal from a video fill | | Extract video audio | `engine.block.createAudiosFromVideo(videoFill=_, options=_)` | Extract every audio track from a video fill | | Count video audio | `engine.block.getAudioTrackCountFromVideo(videoFill=_)` | Count audio tracks in a video fill | | Inspect video audio | `engine.block.getAudioInfoFromVideo(videoFill=_)` | Read `AudioTrackInfo` metadata; use the returned list position for extraction because `AudioTrackInfo.trackIndex` is the container track index | | Playback | `engine.block.setPlaying(block=_, enabled=_)` | Start or stop playback for a page or playable block | | Playback | `engine.block.isPlaying(block=_)` | Read the current play or pause state | | Playback | `engine.block.supportsPlaybackControl(block=_)` | Check whether playback control APIs are supported for a block | | Playback | `engine.block.setPlaybackTime(block=_, time=_)` | Move playback to a timeline position | | Playback | `engine.block.getPlaybackTime(block=_)` | Read the current playback time | | Playback | `engine.block.supportsPlaybackTime(block=_)` | Check whether a block exposes a playback time cursor | | Playback | `engine.block.setVolume(block=_, volume=_)` | Set volume from 0.0 to 1.0 | | Playback | `engine.block.getVolume(block=_)` | Read the current volume | | Playback | `engine.block.setMuted(block=_, muted=_)` | Mute or unmute audio | | Playback | `engine.block.isMuted(block=_)` | Read whether audio is muted | | Playback | `engine.block.setPlaybackSpeed(block=_, speed=_)` | Set audio speed from 0.25x to 3.0x | | Playback | `engine.block.getPlaybackSpeed(block=_)` | Read the current playback speed | | Playback | `engine.block.setSoloPlaybackEnabled(block=_, enabled=_)` | Preview one block while the rest of the scene stays paused | | Playback | `engine.block.isSoloPlaybackEnabled(block=_)` | Read whether solo playback is enabled for a block | | Timing | `engine.block.supportsTimeOffset(block=_)` | Check whether a block can be positioned on its parent's timeline | | Timing | `engine.block.setTimeOffset(block=_, offset=_)` | Move the audio block on the timeline | | Timing | `engine.block.getTimeOffset(block=_)` | Read where the audio block starts on the timeline | | Timing | `engine.block.supportsDuration(block=_)` | Check whether a block exposes an active timeline duration | | Timing | `engine.block.setDuration(block=_, duration=_)` | Set the active block duration | | Timing | `engine.block.getDuration(block=_)` | Read the active block duration | | Timing | `engine.block.supportsTrim(block=_)` | Check whether a block or fill exposes trim controls | | Timing | `engine.block.setTrimOffset(block=_, offset=_)` | Start inside the source audio | | Timing | `engine.block.getTrimOffset(block=_)` | Read the source trim start | | Timing | `engine.block.setTrimLength(block=_, length=_)` | Limit the source range used for playback | | Timing | `engine.block.getTrimLength(block=_)` | Read the source trim length | | Timing | `engine.block.setLooping(block=_, looping=_)` | Loop the trimmed source while the block is active | | Timing | `engine.block.isLooping(block=_)` | Read whether the source loops or stops | | Resources | `engine.block.forceLoadAVResource(block=_)` | Load audio or video metadata before querying it | | Resources | `engine.block.getAVResourceTotalDuration(block=_)` | Read the loaded audio or video source duration | | Waveforms | `engine.block.generateAudioThumbnailSequence(block=_, samplesPerChunk=_, timeBegin=_, timeEnd=_, numberOfSamples=_, numberOfChannels=_)` | Generate waveform sample chunks | | Persistence | `engine.editor.findAllTransientResources()` | Find extracted `buffer://` resources that must be persisted before saving | | Persistence | `engine.editor.getResourceData(uri=_, chunkSize=_, onData=_)` | Read transient resource bytes for app storage | | Persistence | `engine.editor.relocateResource(currentUri=_, relocatedUri=_)` | Replace a transient URI with a durable URI | | Persistence | `engine.scene.saveToString(scene=_, allowedResourceSchemes=_)` | Serialize only scenes whose resources use allowed durable schemes | ## Next Steps - [CE.SDK API Reference](https://img.ly/docs/cesdk/android/api-reference/overview-8f24e1/) - Review the full API surface. - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-video/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 - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-video/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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Compositions" description: "Combine and arrange multiple elements to create complex, multi-page, or layered design compositions." platform: android url: "https://img.ly/docs/cesdk/android/create-composition-db709c/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/create-composition/overview-5b19c5/) - Combine and arrange multiple elements to create complex, multi-page, or layered design compositions. - [Multi-Page Layouts](https://img.ly/docs/cesdk/android/create-composition/multi-page-4d2b50/) - Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene. - [Create a Collage](https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/) - Create collages on Android by loading layout templates and transferring content between pages. - [Design a Layout](https://img.ly/docs/cesdk/android/create-composition/layout-b66311/) - Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing. - [Add a Background](https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/) - Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks. - [Positioning and Alignment](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) - Precisely position, align, distribute, and snap objects using CE.SDK's layout APIs. - [Group and Ungroup Objects](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually. - [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Organize design elements using a layer stack for precise control over stacking and visibility. - [Lock Design](https://img.ly/docs/cesdk/android/create-composition/lock-design-0a81de/) - Protect design elements from unwanted modifications using CE.SDK's scope-based permission system. - [Blend Modes](https://img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519/) - Apply blend modes to elements to control how colors and layers interact visually. - [Programmatic Creation](https://img.ly/docs/cesdk/android/create-composition/programmatic-a688bf/) - Build compositions entirely through code with the CE.SDK Engine for automation, batch processing, and headless rendering. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add a Background" description: "Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Add a Background](https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/) --- ```kotlin file=@cesdk_android_examples/engine-guides-add-background/AddBackground.kt reference-only import kotlinx.coroutines.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.GradientColorStop import ly.img.engine.ShapeType import ly.img.engine.SizeMode fun addBackground( 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) try { 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) 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) } } finally { engine.stop() } } ``` 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.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 fun blendModes( 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) 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) engine.stop() } ``` Control how design blocks visually blend with underlying layers using CE.SDK's blend mode system for professional layered compositions. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-blend-modes) Blend modes control how a block's colors combine with underlying layers, similar to blend modes in Photoshop or other design tools. CE.SDK provides 27 blend modes organized into categories: Normal, Darken, Lighten, Contrast, Inversion, and Component. This guide covers how to check blend mode support, apply blend modes programmatically, understand the available blend mode options, and combine blend modes with opacity for fine control over layer compositing. ## Checking Blend Mode Support Before applying a blend mode, verify that the block supports the property and allows writing it. `supportsBlendMode()` checks whether the block has a blend mode, while `isAllowedByScope(block, "layer/blendMode")` checks whether scoped content permits the setter. ```kotlin highlight-android-check-support // Scope checks use engine scope key strings. val canSetBlendMode = engine.block.supportsBlendMode(topBlock) && engine.block.isAllowedByScope(topBlock, key = "layer/blendMode") println("Can set blend mode: $canSetBlendMode") ``` Blend mode support is available for pages, groups, text blocks, and graphic blocks such as graphics with image, video, color, or shape content. Android represents shapes as `DesignBlockType.Graphic` blocks with a `ShapeType`, so check the exact block with `supportsBlendMode()` before setting a mode. ## Setting and Getting Blend Modes Apply a blend mode with `setBlendMode()` and retrieve the current mode with `getBlendMode()`. Most blocks, including the graphic block in this sample, default to `BlendMode.NORMAL`, which displays the block without any blending effect. Groups default to `BlendMode.PASS_THROUGH` so their children can blend with layers below the group. ```kotlin highlight-android-set-blend-mode if (canSetBlendMode) { engine.block.setBlendMode(topBlock, blendMode = BlendMode.MULTIPLY) } ``` After setting a blend mode, confirm the change by reading it back: ```kotlin highlight-android-get-blend-mode val currentBlendMode = engine.block.getBlendMode(topBlock) println("Current blend mode: $currentBlendMode") check(currentBlendMode == BlendMode.MULTIPLY) ``` ## Available Blend Modes CE.SDK provides 27 blend modes organized into categories, each producing different visual results: ### Normal Modes - **`BlendMode.PASS_THROUGH`** - Allows children of a group to blend with layers below the group - **`BlendMode.NORMAL`** - Default mode with no blending effect ### Darken Modes These modes darken the result by comparing the base and blend colors: - **`BlendMode.DARKEN`** - Selects the darker of the base and blend colors - **`BlendMode.MULTIPLY`** - Multiplies colors, producing darker results (great for shadows) - **`BlendMode.COLOR_BURN`** - Darkens base color by increasing contrast - **`BlendMode.LINEAR_BURN`** - Darkens base color by decreasing brightness - **`BlendMode.DARKEN_COLOR`** - Selects the darker color based on luminosity ### Lighten Modes These modes lighten the result by comparing colors: - **`BlendMode.LIGHTEN`** - Selects the lighter of the base and blend colors - **`BlendMode.SCREEN`** - Multiplies the inverse of colors, producing lighter results (great for highlights) - **`BlendMode.COLOR_DODGE`** - Lightens base color by decreasing contrast - **`BlendMode.LINEAR_DODGE`** - Lightens base color by increasing brightness - **`BlendMode.LIGHTEN_COLOR`** - Selects the lighter color based on luminosity ### Contrast Modes These modes increase midtone contrast: - **`BlendMode.OVERLAY`** - Combines Multiply and Screen based on the base color - **`BlendMode.SOFT_LIGHT`** - Similar to Overlay but with a softer effect - **`BlendMode.HARD_LIGHT`** - Similar to Overlay but based on the blend color - **`BlendMode.VIVID_LIGHT`** - Burns or dodges colors based on the blend color - **`BlendMode.LINEAR_LIGHT`** - Increases or decreases brightness based on blend color - **`BlendMode.PIN_LIGHT`** - Replaces colors based on the blend color - **`BlendMode.HARD_MIX`** - Reduces colors to white, black, or primary colors ### Inversion Modes These modes create inverted or subtracted effects: - **`BlendMode.DIFFERENCE`** - Subtracts the darker from the lighter color - **`BlendMode.EXCLUSION`** - Similar to Difference with lower contrast - **`BlendMode.SUBTRACT`** - Subtracts blend color from base color - **`BlendMode.DIVIDE`** - Divides base color by blend color ### Component Modes These modes affect specific color components: - **`BlendMode.HUE`** - Uses the hue of the blend color with base saturation and luminosity - **`BlendMode.SATURATION`** - Uses the saturation of the blend color - **`BlendMode.COLOR`** - Uses the hue and saturation of the blend color - **`BlendMode.LUMINOSITY`** - Uses the luminosity of the blend color ## Combining Blend Modes with Opacity For finer control over compositing, combine blend modes with opacity. Opacity reduces overall visibility while the blend mode affects color interaction with underlying layers. Check `supportsOpacity()` and the `layer/opacity` scope before calling `setOpacity()`, because support only confirms that the block has an opacity property. ```kotlin highlight-android-set-opacity val canSetOpacity = engine.block.supportsOpacity(topBlock) && engine.block.isAllowedByScope(topBlock, key = "layer/opacity") if (canSetOpacity) { engine.block.setOpacity(topBlock, value = 0.7F) } ``` Read back the current opacity value to confirm changes or inspect existing state: ```kotlin highlight-android-get-opacity val currentOpacity = engine.block.getOpacity(topBlock) println("Current opacity: $currentOpacity") check(currentOpacity == 0.7F) ``` > **Tip:** Start with full opacity (1.0) when experimenting with blend modes, then reduce > opacity to soften the effect. Common values are 0.5-0.7 for subtle blending > effects. ## Troubleshooting ### Blend Mode Has No Visible Effect - Ensure the block has visible content, such as a color or image fill. - Place visible blocks below the blended block; blend modes composite with underlying content. - Read back the active mode with `getBlendMode()` to confirm it was applied to the expected block. ### Cannot Set Blend Mode - Check `supportsBlendMode()` before calling `setBlendMode()`. - Confirm `isAllowedByScope(block, "layer/blendMode")` returns `true`; locked template or editor content can support blend modes but deny writes. - Make sure the `DesignBlock` still exists in the scene when you set the mode. - Pass one of the Android `BlendMode` enum values listed above. ### Cannot Set Opacity - Check `supportsOpacity()` before calling `setOpacity()`. - Confirm `isAllowedByScope(block, "layer/opacity")` returns `true`; scoped content can expose opacity but block the setter. ### Unexpected Blending Results - Verify the block order: only content below the block contributes to the blend result. - Match the mode category to the intended effect, such as Darken, Lighten, or Contrast. - Adjust opacity after setting the blend mode to soften strong results. ## API Reference | Method | Description | | --- | --- | | `engine.block.supportsBlendMode(block=_)` | Check if a block supports blend modes | | `engine.block.isAllowedByScope(block=_, key="layer/blendMode")` | Check if the current scopes allow `setBlendMode()` | | `engine.block.setBlendMode(block=_, blendMode=_)` | Set the blend mode for a block | | `engine.block.getBlendMode(block=_)` | Get the current blend mode of a block | | `engine.block.supportsOpacity(block=_)` | Check if a block supports opacity | | `engine.block.isAllowedByScope(block=_, key="layer/opacity")` | Check if the current scopes allow `setOpacity()` | | `engine.block.setOpacity(block=_, value=_)` | Set the opacity for a block (0-1) | | `engine.block.getOpacity(block=_)` | Get the current opacity of a block | ## Next Steps - [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control z-order and visibility of blocks - [Add a Background](https://img.ly/docs/cesdk/android/create-composition/add-background-375a47/) - Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks. - [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Combine blocks to apply blend modes to groups --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create a Collage" description: "Create collages on Android by loading layout templates and transferring content between pages." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Create a Collage](https://img.ly/docs/cesdk/android/create-composition/collage-f7d28d/) --- ```kotlin file=@cesdk_android_examples/engine-guides-collage/Collage.kt reference-only import android.app.Application import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Color import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.GlobalScope import ly.img.engine.ShapeType import kotlin.math.roundToInt fun collage( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope(Dispatchers.Main).launch { Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.example") var engineStarted = false try { engineStarted = engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) createAndApplyCollage(engine = engine) } finally { if (engineStarted) { engine.stop() } } } private suspend fun createAndApplyCollage(engine: Engine) { val scene = engine.scene.create() val page = createCollagePage(engine = engine, width = 1080F, height = 1080F) engine.block.appendChild(parent = scene, child = page) addImageSlot( engine = engine, page = page, uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1.jpg"), x = 32F, y = 32F, width = 480F, height = 480F, ) addImageSlot( engine = engine, page = page, uri = Uri.parse("https://img.ly/static/ubq_samples/sample_2.jpg"), x = 568F, y = 32F, width = 480F, height = 480F, ) addTextBlock( engine = engine, page = page, text = "Weekend trip", x = 64F, y = 830F, ) val layoutPage = createCollagePage(engine = engine, width = 1080F, height = 1080F) addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 32F, width = 1016F, height = 496F) addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 560F, width = 492F, height = 360F) addImageSlot(engine = engine, page = layoutPage, x = 556F, y = 560F, width = 492F, height = 360F) addTextBlock(engine = engine, page = layoutPage, text = "Title", x = 64F, y = 952F) val layoutBlocksString = engine.block.saveToString(blocks = listOf(layoutPage)) engine.block.destroy(layoutPage) val collagePage = applyCollageLayout( engine = engine, currentPage = page, layoutBlocksString = layoutBlocksString, addUndoStep = true, ) engine.scene.zoomToBlock( block = collagePage, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) } suspend fun applyCollageLayout( engine: Engine, currentPage: DesignBlock, layoutBlocksString: String, addUndoStep: Boolean = true, ): DesignBlock { val previousDestroyScope = engine.editor.getGlobalScope("lifecycle/destroy") engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.ALLOW) return try { engine.block.findAllSelected().forEach { selectedBlock -> engine.block.setSelected(block = selectedBlock, selected = false) } var oldPage: DesignBlock? = null var loadedLayoutPage: DesignBlock? = null try { oldPage = engine.block.duplicate(block = currentPage, attachToParent = false) loadedLayoutPage = engine.block.loadFromString(layoutBlocksString).first() engine.block.getChildren(currentPage).forEach(engine.block::destroy) engine.block.getChildren(loadedLayoutPage).forEach { child -> engine.block.insertChild(parent = currentPage, child = child, index = engine.block.getChildren(currentPage).size) } transferCollageContent(engine = engine, fromPage = oldPage, toPage = currentPage) } finally { loadedLayoutPage?.let { engine.block.destroy(it) } oldPage?.let { engine.block.destroy(it) } } if (addUndoStep) { engine.editor.addUndoStep() } currentPage } finally { engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = previousDestroyScope) } } private fun transferCollageContent( engine: Engine, fromPage: DesignBlock, toPage: DesignBlock, ) { val sourceBlocks = visuallySortedBlocks( engine = engine, rootPage = fromPage, blocks = collectChildrenTree(engine = engine, parent = fromPage), ) val targetBlocks = visuallySortedBlocks( engine = engine, rootPage = toPage, blocks = collectChildrenTree(engine = engine, parent = toPage), ) val sourceImages = sourceBlocks.filter { isImageBlock(engine = engine, designBlock = it) } val targetImages = targetBlocks.filter { isImageBlock(engine = engine, designBlock = it) } sourceImages.zip(targetImages).forEach { (sourceImage, targetImage) -> copyImageContent(engine = engine, sourceImage = sourceImage, targetImage = targetImage) } val sourceTexts = sourceBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key } val targetTexts = targetBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key } sourceTexts.zip(targetTexts).forEach { (sourceText, targetText) -> copyTextContent(engine = engine, sourceText = sourceText, targetText = targetText) } } private fun collectChildrenTree( engine: Engine, parent: DesignBlock, ): List = engine.block.getChildren(parent).flatMap { child -> listOf(child) + collectChildrenTree(engine = engine, parent = child) } private data class UntransformedPagePosition( val x: Float, val y: Float, ) private data class PositionedBlock( val block: DesignBlock, val position: UntransformedPagePosition, ) private fun visuallySortedBlocks( engine: Engine, rootPage: DesignBlock, blocks: List, ): List = blocks .map { designBlock -> PositionedBlock( block = designBlock, position = untransformedPagePosition(engine = engine, rootPage = rootPage, designBlock = designBlock), ) } .sortedWith( compareBy { it.position.y.roundToInt() } .thenBy { it.position.x.roundToInt() }, ) .map { it.block } private fun untransformedPagePosition( engine: Engine, rootPage: DesignBlock, designBlock: DesignBlock, ): UntransformedPagePosition { var x = engine.block.getPositionX(designBlock) var y = engine.block.getPositionY(designBlock) var parent = engine.block.getParent(designBlock) // This local-offset sort is for unrotated and unscaled layout slots. while (parent != null && parent != rootPage) { x += engine.block.getPositionX(parent) y += engine.block.getPositionY(parent) parent = engine.block.getParent(parent) } return UntransformedPagePosition(x = x, y = y) } private fun copyImageContent( engine: Engine, sourceImage: DesignBlock, targetImage: DesignBlock, ) { val sourceFill = engine.block.getFill(sourceImage) val targetFill = engine.block.getFill(targetImage) // Image fills use a generic property key, but Android keeps the value typed as Uri. engine.block.setUri( block = targetFill, property = "fill/image/imageFileURI", value = engine.block.getUri(sourceFill, property = "fill/image/imageFileURI"), ) engine.block.setSourceSet( block = targetFill, property = "fill/image/sourceSet", sourceSet = engine.block.getSourceSet(sourceFill, property = "fill/image/sourceSet"), ) if (engine.block.supportsPlaceholderBehavior(sourceImage)) { engine.block.setPlaceholderBehaviorEnabled( block = targetImage, enabled = engine.block.isPlaceholderBehaviorEnabled(sourceImage), ) } engine.block.resetCrop(targetImage) } private fun copyTextContent( engine: Engine, sourceText: DesignBlock, targetText: DesignBlock, ) { // Reading plain text still uses property access; replaceText keeps the write type-safe. engine.block.replaceText( block = targetText, text = engine.block.getString(sourceText, property = "text/text"), ) runCatching { engine.block.setFont( block = targetText, fontFileUri = engine.block.getUri(sourceText, property = "text/fontFileUri"), typeface = engine.block.getTypeface(sourceText), ) } engine.block.getTextColors(sourceText).firstOrNull()?.let { color -> engine.block.setTextColor(block = targetText, color = color) } } private fun isImageBlock( engine: Engine, designBlock: DesignBlock, ): Boolean = engine.block.supportsFill(designBlock) && engine.block.getType(engine.block.getFill(designBlock)) == FillType.Image.key private fun createCollagePage( engine: Engine, width: Float, height: Float, ): DesignBlock { val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = width) engine.block.setHeight(page, value = height) return page } private fun addImageSlot( engine: Engine, page: DesignBlock, uri: Uri? = null, x: Float, y: Float, width: Float, height: Float, ): DesignBlock { val image = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(image, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(image, value = x) engine.block.setPositionY(image, value = y) engine.block.setWidth(image, value = width) engine.block.setHeight(image, value = height) val fill = engine.block.createFill(FillType.Image) if (uri != null) { // Image fills use a generic property key, but Android keeps the value typed as Uri. engine.block.setUri(block = fill, property = "fill/image/imageFileURI", value = uri) } engine.block.setFill(block = image, fill = fill) engine.block.appendChild(parent = page, child = image) return image } private fun addTextBlock( engine: Engine, page: DesignBlock, text: String, x: Float, y: Float, ): DesignBlock { val textBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = textBlock, text = text) engine.block.setPositionX(textBlock, value = x) engine.block.setPositionY(textBlock, value = y) engine.block.setWidth(textBlock, value = 640F) engine.block.setHeight(textBlock, value = 80F) engine.block.setTextColor( block = textBlock, color = Color.fromRGBA(r = 0.08F, g = 0.08F, b = 0.08F, a = 1F), ) engine.block.appendChild(parent = page, child = textBlock) return textBlock } ``` Create a collage on Android by loading a layout page and transferring existing images and text into the new structure. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-collage) Layouts are predefined page structures that arrange images and text in a composition. Unlike templates, which usually replace the whole scene, this workflow keeps the user's content and maps it into a new layout. The Android example uses the Engine directly. You can call the same layout application function from your own Compose UI, an asset source callback, or any other Android workflow that lets users choose a layout. ## What You'll Learn In this guide, you will learn how to: - Define a layout page that contains image slots and text placeholders. - Load the layout from a saved block string. - Replace the current page structure while preserving existing image and text content. - Sort blocks visually so content maps top-to-bottom and left-to-right. ## When to Use Layouts Use layout-based collages when your app needs to keep the current content but change its arrangement. Common examples include: - Photo collages - Grid layouts - Magazine spreads - Social media posts ## Difference Between Layouts and Templates Layouts can be represented as [custom assets](https://img.ly/docs/cesdk/android/import-media/concepts-5e6197/), but the apply behavior is different from a full template load: - **Templates** load a complete design and replace the current scene. - **Layouts** provide a new page structure while your code transfers the current content into matching slots. Preserving assets while changing the layout is app logic. CE.SDK provides the block, fill, text, and scene APIs you need to implement that logic. ## How Collages Work When a user chooses a collage layout, your app performs this sequence: 1. Load a layout page from a saved block string. 2. Duplicate the current page as a temporary content source. 3. Replace the current page's children with the layout's children. 4. Copy images and text from the old page into the new layout in visual order. 5. Clean up temporary blocks and add an undo step. Visual order is important. The sample sorts simple, untransformed layout slots by their accumulated page coordinates so content maps predictably between the old page and the new layout. ## Create Page Helpers The sample uses small helpers to create pages, image slots, and text blocks. The page helper only sets page dimensions. ```kotlin highlight-android-create-page-helper private fun createCollagePage( engine: Engine, width: Float, height: Float, ): DesignBlock { val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = width) engine.block.setHeight(page, value = height) return page } ``` Image slots use graphic blocks with rectangular shapes and image fills. In your app, the `Uri` values can come from local resources, remote media, or user-selected files. ```kotlin highlight-android-image-slot-helper private fun addImageSlot( engine: Engine, page: DesignBlock, uri: Uri? = null, x: Float, y: Float, width: Float, height: Float, ): DesignBlock { val image = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(image, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(image, value = x) engine.block.setPositionY(image, value = y) engine.block.setWidth(image, value = width) engine.block.setHeight(image, value = height) val fill = engine.block.createFill(FillType.Image) if (uri != null) { // Image fills use a generic property key, but Android keeps the value typed as Uri. engine.block.setUri(block = fill, property = "fill/image/imageFileURI", value = uri) } engine.block.setFill(block = image, fill = fill) engine.block.appendChild(parent = page, child = image) return image } ``` Text blocks keep the layout example readable by isolating the text creation and initial color setup. ```kotlin highlight-android-text-block-helper private fun addTextBlock( engine: Engine, page: DesignBlock, text: String, x: Float, y: Float, ): DesignBlock { val textBlock = engine.block.create(DesignBlockType.Text) engine.block.replaceText(block = textBlock, text = text) engine.block.setPositionX(textBlock, value = x) engine.block.setPositionY(textBlock, value = y) engine.block.setWidth(textBlock, value = 640F) engine.block.setHeight(textBlock, value = 80F) engine.block.setTextColor( block = textBlock, color = Color.fromRGBA(r = 0.08F, g = 0.08F, b = 0.08F, a = 1F), ) engine.block.appendChild(parent = page, child = textBlock) return textBlock } ``` ## Define a Layout A layout is a page with positioned image slots and optional text blocks. In production, save these pages as scene or block files and load them from your app bundle, backend, or asset source. ```kotlin highlight-android-define-layout val layoutPage = createCollagePage(engine = engine, width = 1080F, height = 1080F) addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 32F, width = 1016F, height = 496F) addImageSlot(engine = engine, page = layoutPage, x = 32F, y = 560F, width = 492F, height = 360F) addImageSlot(engine = engine, page = layoutPage, x = 556F, y = 560F, width = 492F, height = 360F) addTextBlock(engine = engine, page = layoutPage, text = "Title", x = 64F, y = 952F) val layoutBlocksString = engine.block.saveToString(blocks = listOf(layoutPage)) engine.block.destroy(layoutPage) ``` The example saves the layout page with `block.saveToString()` and later restores it with `block.loadFromString()`. The same pattern works when the string comes from a remote layout file. ## Apply the Collage Call the app-owned layout helper with the current page, the Engine instance, and the saved layout data. Pass `addUndoStep = true` when the action should become a single undoable editor operation. ```kotlin highlight-android-apply-layout val collagePage = applyCollageLayout( engine = engine, currentPage = page, layoutBlocksString = layoutBlocksString, addUndoStep = true, ) ``` The function returns the page that now contains the collage structure and transferred content. ## Replace the Page Structure Applying a layout temporarily allows block deletion, clears the current selection, duplicates the old page, loads the layout page, and moves the layout children into the current page. ```kotlin highlight-android-layout-workflow suspend fun applyCollageLayout( engine: Engine, currentPage: DesignBlock, layoutBlocksString: String, addUndoStep: Boolean = true, ): DesignBlock { val previousDestroyScope = engine.editor.getGlobalScope("lifecycle/destroy") engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.ALLOW) return try { engine.block.findAllSelected().forEach { selectedBlock -> engine.block.setSelected(block = selectedBlock, selected = false) } var oldPage: DesignBlock? = null var loadedLayoutPage: DesignBlock? = null try { oldPage = engine.block.duplicate(block = currentPage, attachToParent = false) loadedLayoutPage = engine.block.loadFromString(layoutBlocksString).first() engine.block.getChildren(currentPage).forEach(engine.block::destroy) engine.block.getChildren(loadedLayoutPage).forEach { child -> engine.block.insertChild(parent = currentPage, child = child, index = engine.block.getChildren(currentPage).size) } transferCollageContent(engine = engine, fromPage = oldPage, toPage = currentPage) } finally { loadedLayoutPage?.let { engine.block.destroy(it) } oldPage?.let { engine.block.destroy(it) } } if (addUndoStep) { engine.editor.addUndoStep() } currentPage } finally { engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = previousDestroyScope) } } ``` Key details: - Store and restore the previous `lifecycle/destroy` scope so the surrounding editor state remains unchanged. - Keep the duplicate backup unattached so it cannot leak into the scene if transfer fails. - Destroy the temporary old and layout pages in cleanup after transfer. ## Transfer Content Collect all descendants from the old page and the new page, sort both lists by visual position, then pair source and target blocks by type. ```kotlin highlight-android-transfer-content private fun transferCollageContent( engine: Engine, fromPage: DesignBlock, toPage: DesignBlock, ) { val sourceBlocks = visuallySortedBlocks( engine = engine, rootPage = fromPage, blocks = collectChildrenTree(engine = engine, parent = fromPage), ) val targetBlocks = visuallySortedBlocks( engine = engine, rootPage = toPage, blocks = collectChildrenTree(engine = engine, parent = toPage), ) val sourceImages = sourceBlocks.filter { isImageBlock(engine = engine, designBlock = it) } val targetImages = targetBlocks.filter { isImageBlock(engine = engine, designBlock = it) } sourceImages.zip(targetImages).forEach { (sourceImage, targetImage) -> copyImageContent(engine = engine, sourceImage = sourceImage, targetImage = targetImage) } val sourceTexts = sourceBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key } val targetTexts = targetBlocks.filter { engine.block.getType(it) == DesignBlockType.Text.key } sourceTexts.zip(targetTexts).forEach { (sourceText, targetText) -> copyTextContent(engine = engine, sourceText = sourceText, targetText = targetText) } } ``` If the source has more images than the layout has slots, extra images are ignored. If the layout has more slots, the remaining slots keep their placeholder content. ## Sort Blocks Visually Block positions are local to their parent. For unrotated and unscaled layout slots, the example accumulates each block's ancestor offsets, rounds those coordinates, then sorts by Y before X. If your layout slots live under rotated or scaled parents, use the Engine's global bounding-box APIs for the sort instead of this local-offset helper. ```kotlin highlight-android-visual-sort private fun collectChildrenTree( engine: Engine, parent: DesignBlock, ): List = engine.block.getChildren(parent).flatMap { child -> listOf(child) + collectChildrenTree(engine = engine, parent = child) } private data class UntransformedPagePosition( val x: Float, val y: Float, ) private data class PositionedBlock( val block: DesignBlock, val position: UntransformedPagePosition, ) private fun visuallySortedBlocks( engine: Engine, rootPage: DesignBlock, blocks: List, ): List = blocks .map { designBlock -> PositionedBlock( block = designBlock, position = untransformedPagePosition(engine = engine, rootPage = rootPage, designBlock = designBlock), ) } .sortedWith( compareBy { it.position.y.roundToInt() } .thenBy { it.position.x.roundToInt() }, ) .map { it.block } private fun untransformedPagePosition( engine: Engine, rootPage: DesignBlock, designBlock: DesignBlock, ): UntransformedPagePosition { var x = engine.block.getPositionX(designBlock) var y = engine.block.getPositionY(designBlock) var parent = engine.block.getParent(designBlock) // This local-offset sort is for unrotated and unscaled layout slots. while (parent != null && parent != rootPage) { x += engine.block.getPositionX(parent) y += engine.block.getPositionY(parent) parent = engine.block.getParent(parent) } return UntransformedPagePosition(x = x, y = y) } ``` Keep layout slots in distinct positions when possible. Blocks with the same accumulated Y position map left-to-right. ## Copy Images Copy the image URI with Android's `Uri` property APIs and copy the source set from the old image fill to the new image fill. Resetting the crop lets the image fit the new slot dimensions. ```kotlin highlight-android-copy-images private fun copyImageContent( engine: Engine, sourceImage: DesignBlock, targetImage: DesignBlock, ) { val sourceFill = engine.block.getFill(sourceImage) val targetFill = engine.block.getFill(targetImage) // Image fills use a generic property key, but Android keeps the value typed as Uri. engine.block.setUri( block = targetFill, property = "fill/image/imageFileURI", value = engine.block.getUri(sourceFill, property = "fill/image/imageFileURI"), ) engine.block.setSourceSet( block = targetFill, property = "fill/image/sourceSet", sourceSet = engine.block.getSourceSet(sourceFill, property = "fill/image/sourceSet"), ) if (engine.block.supportsPlaceholderBehavior(sourceImage)) { engine.block.setPlaceholderBehaviorEnabled( block = targetImage, enabled = engine.block.isPlaceholderBehaviorEnabled(sourceImage), ) } engine.block.resetCrop(targetImage) } ``` The placeholder behavior calls preserve placeholder state when the source block supports it. ## Identify Image Slots The transfer code treats a graphic block with an image fill as an image slot. This keeps the mapping independent from custom metadata or asset-source IDs. ```kotlin highlight-android-image-slot-check private fun isImageBlock( engine: Engine, designBlock: DesignBlock, ): Boolean = engine.block.supportsFill(designBlock) && engine.block.getType(engine.block.getFill(designBlock)) == FillType.Image.key ``` ## Copy Text Text transfer reads the source string, writes it with `replaceText()`, and copies the first text color with the typed text color APIs. Reading plain text still uses property access because Android does not expose a dedicated text getter. Typeface and font file URI preservation is best-effort so unresolved fonts do not block the text content transfer. ```kotlin highlight-android-copy-text private fun copyTextContent( engine: Engine, sourceText: DesignBlock, targetText: DesignBlock, ) { // Reading plain text still uses property access; replaceText keeps the write type-safe. engine.block.replaceText( block = targetText, text = engine.block.getString(sourceText, property = "text/text"), ) runCatching { engine.block.setFont( block = targetText, fontFileUri = engine.block.getUri(sourceText, property = "text/fontFileUri"), typeface = engine.block.getTypeface(sourceText), ) } engine.block.getTextColors(sourceText).firstOrNull()?.let { color -> engine.block.setTextColor(block = targetText, color = color) } } ``` Use the same visual pairing rule for text as for images so captions and titles stay in their expected order. ## Connect It to Your UI The Engine workflow is UI-agnostic. A typical Android integration stores each layout with: | Field | Purpose | | --- | --- | | `id` | Stable layout identifier for your app | | `label` | Display name in your own layout picker | | `uri` | Scene or block file containing the layout page | | `thumbnailUri` | Preview image shown in your UI | When a user selects a layout, load its file, pass the saved block string into your app's `applyCollageLayout(engine = ...)` helper, and keep the UI code separate from the content transfer logic. ## Troubleshooting | Issue | What to Check | | --- | --- | | Layout does not apply | Verify the saved layout string loads with `engine.block.loadFromString(block=_)` and returns a page block before moving children into the current page. | | Content maps to the wrong slots | Keep the sortable image and text slots unrotated and unscaled, and check the accumulated coordinates used by visual sorting. Blocks with the same accumulated Y coordinate map left-to-right, so keep slots distinct enough for predictable ordering. | | Images stay empty or lose variants | Ensure both source and target blocks use image fills, then copy the image URI with `getUri()` / `setUri()` and copy `fill/image/sourceSet` before calling `engine.block.resetCrop(block=_)`. | | Text copies without its expected font | Treat font transfer as best-effort. Copy the text string first, then wrap `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` so unresolved font URIs do not block the collage update. | | Undo or cleanup behaves unexpectedly | Restore the previous `lifecycle/destroy` scope in a `finally` block, destroy temporary duplicate/layout pages, and add the undo step only after transfer completes. | | Slot counts do not match | Pair source and target blocks with `zip`. Extra source content is ignored, and extra layout slots keep their placeholder content. | ## API Reference | API | Category | Purpose | | --- | --- | --- | | `engine.block.saveToString(blocks=_)` | Layout data | Serialize a layout page so it can be stored as a layout asset or file. | | `engine.block.loadFromString(block=_)` | Layout data | Load a saved layout page before moving its children into the current page. | | `engine.block.duplicate(block=_, attachToParent=_)` | Backup | Copy the current page without attaching the duplicate to the scene. | | `engine.block.getChildren(block=_)` | Hierarchy | Read child blocks before clearing or moving a page structure. | | `engine.block.insertChild(parent=_, child=_, index=_)` | Hierarchy | Move layout children into the current page in order. | | `engine.block.destroy(block=_)` | Lifecycle | Remove old children and temporary pages during cleanup. | | `engine.editor.getGlobalScope(key="lifecycle/destroy")` | Lifecycle | Store the current deletion scope before the layout swap. | | `engine.editor.setGlobalScope(key="lifecycle/destroy", globalScope=_)` | Lifecycle | Temporarily allow block deletion, then restore the previous scope. | | `engine.block.findAllSelected()` | Selection | Find selected blocks so the layout change can clear selection first. | | `engine.block.setSelected(block=_, selected=_)` | Selection | Deselect blocks before replacing the page structure. | | `engine.block.getFill(block=_)` | Images | Access the image fill that stores URI and source-set properties. | | `engine.block.getUri(block=_, property="fill/image/imageFileURI")` | Images | Read the source image URI from an image fill. | | `engine.block.setUri(block=_, property="fill/image/imageFileURI", value=_)` | Images | Copy the image URI to the target fill. | | `engine.block.getSourceSet(block=_, property="fill/image/sourceSet")` | Images | Read responsive image variants from the source fill. | | `engine.block.setSourceSet(block=_, property="fill/image/sourceSet", sourceSet=_)` | Images | Preserve responsive image variants on the target fill. | | `engine.block.resetCrop(block=_)` | Images | Refit the transferred image inside the new slot. | | `engine.block.supportsPlaceholderBehavior(block=_)` | Placeholders | Check whether placeholder state can be copied. | | `engine.block.setPlaceholderBehaviorEnabled(block=_, enabled=_)` | Placeholders | Apply the source placeholder behavior to the target image block. | | `engine.block.getString(block=_, property="text/text")` | Text | Read text content from the source block. Android exposes typed text write and style APIs, but not a dedicated plain-text getter. | | `engine.block.replaceText(block=_, text=_)` | Text | Copy text content into the target block. | | `engine.block.setFont(block=_, fontFileUri=_, typeface=_)` | Text | Preserve the source font when the URI and typeface resolve. | | `engine.block.getTextColors(block=_)` | Text | Read the source text colors. | | `engine.block.setTextColor(block=_, color=_)` | Text | Apply the source text color to the target block. | | `engine.block.getParent(block=_)` | Sorting | Walk ancestors while accumulating untransformed page coordinates. | | `engine.block.getPositionX(block=_)` | Sorting | Read local X positions while calculating left-to-right ordering. | | `engine.block.getPositionY(block=_)` | Sorting | Read local Y positions while calculating top-to-bottom ordering. | | `engine.editor.addUndoStep()` | Undo | Commit the completed layout swap as one undoable operation. | ## Next Steps Now that you can create collages with layouts, explore these related guides: - [Templates Overview](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) - Work with templates instead of layouts - [Panel](https://img.ly/docs/cesdk/android/user-interface/customization/panel-7ce1ee/) - Show or hide side panels - [Insert Images](https://img.ly/docs/cesdk/android/insert-media/images-63848a/) - Manage image blocks and fills - [Load a Scene](https://img.ly/docs/cesdk/android/open-the-editor/load-scene-478833/) - Load and save scenes --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Group and Ungroup Objects" description: "Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Group and Ungroup Objects](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) --- ```kotlin file=@cesdk_android_examples/engine-guides-grouping/Grouping.kt reference-only import ly.img.engine.Color import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType suspend fun grouping(engine: Engine) { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) val block1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block1, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block1, value = 120F) engine.block.setHeight(block1, value = 120F) engine.block.setPositionX(block1, value = 200F) engine.block.setPositionY(block1, value = 240F) engine.block.setFill(block1, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block1, color = Color.fromRGBA(r = 0.4F, g = 0.6F, b = 0.9F, a = 1.0F), ) engine.block.appendChild(parent = page, child = block1) val block2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block2, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block2, value = 120F) engine.block.setHeight(block2, value = 120F) engine.block.setPositionX(block2, value = 340F) engine.block.setPositionY(block2, value = 240F) engine.block.setFill(block2, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block2, color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.4F, a = 1.0F), ) engine.block.appendChild(parent = page, child = block2) val block3 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block3, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block3, value = 120F) engine.block.setHeight(block3, value = 120F) engine.block.setPositionX(block3, value = 480F) engine.block.setPositionY(block3, value = 240F) engine.block.setFill(block3, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block3, color = Color.fromRGBA(r = 0.5F, g = 0.8F, b = 0.5F, a = 1.0F), ) engine.block.appendChild(parent = page, child = block3) val blocks = listOf(block1, block2, block3) val canGroup = engine.block.isGroupable(blocks) val group = if (canGroup) { engine.block.group(blocks) } else { error("Select blocks that can be grouped before calling group(...).") } engine.block.setSelected(group, selected = true) engine.block.enterGroup(group) // enterGroup selects the first member by default; select another member when needed. engine.block.select(block2) engine.block.exitGroup(block2) val allGroups = engine.block.findByType(DesignBlockType.Group) val groupType = engine.block.getType(group) val members = engine.block.getChildren(group) engine.block.ungroup(group) val groupsAfterUngroup = engine.block.findByType(DesignBlockType.Group) check(allGroups.isNotEmpty()) check(groupType == DesignBlockType.Group.key) check(members.size == blocks.size) check(groupsAfterUngroup.isEmpty()) } ``` Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-grouping) Use groups when related blocks should stay linked through later edits. Nested groups help model larger composition units, such as cards, badges, or reusable layout sections. This guide covers how to check if blocks can be grouped, create and dissolve groups, navigate into groups to select individual members, and find existing groups in a scene. ## Understanding Groups Groups are blocks with type `DesignBlockType.Group`, and their child blocks are the group members. Android exposes groups through the same block APIs used for other containers, so you can inspect a group's type and children after creation. Use nested groups only after each candidate selection passes `engine.block.isGroupable(...)`; blocks that already belong to another group need to be ungrouped first. ## Create the Blocks We first create several graphic blocks that we'll group together. Each block has a different color fill to make them visually distinct. ```kotlin highlight-android-create-blocks val block1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block1, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block1, value = 120F) engine.block.setHeight(block1, value = 120F) engine.block.setPositionX(block1, value = 200F) engine.block.setPositionY(block1, value = 240F) engine.block.setFill(block1, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block1, color = Color.fromRGBA(r = 0.4F, g = 0.6F, b = 0.9F, a = 1.0F), ) engine.block.appendChild(parent = page, child = block1) ``` The remaining two blocks are created with the same pattern and appended to the page. ## Check If Blocks Can Be Grouped Before grouping, verify that the selected blocks can be grouped using `engine.block.isGroupable(...)`. For valid block IDs, this method returns `true` only when the list is non-empty, no block is a scene or page, no block already belongs to another group, and all blocks are attached to the same page or all are detached from pages. Invalid block IDs fail instead of returning `false`. ```kotlin highlight-android-check-groupable val blocks = listOf(block1, block2, block3) val canGroup = engine.block.isGroupable(blocks) ``` ## Create a Group Use `engine.block.group(...)` to combine multiple blocks into a new group. Guard the call with the `isGroupable(...)` result so invalid selections do not reach `group(...)`. When the guard passes, the method returns the ID of the newly created group block, and the group inherits the combined bounding box of its members. The later snippets use that returned group. ```kotlin highlight-android-create-group val group = if (canGroup) { engine.block.group(blocks) } else { error("Select blocks that can be grouped before calling group(...).") } engine.block.setSelected(group, selected = true) ``` ## Navigate Group Selection CE.SDK provides methods to navigate into and out of groups while editing. ### Enter a Group When a group is selected, use `engine.block.enterGroup(...)` to enter editing mode for that group. This allows you to select and modify individual members within the group. ```kotlin highlight-android-enter-group engine.block.enterGroup(group) // enterGroup selects the first member by default; select another member when needed. engine.block.select(block2) ``` ### Exit a Group When editing a member inside a group, use `engine.block.exitGroup(...)` to return selection to the parent group. This method takes a member block ID and selects its parent group. ```kotlin highlight-android-exit-group engine.block.exitGroup(block2) ``` ## Find and Inspect Groups Discover groups in a scene and inspect their contents using `engine.block.findByType(...)`, `engine.block.getType(...)`, and `engine.block.getChildren(...)`. ```kotlin highlight-android-find-groups val allGroups = engine.block.findByType(DesignBlockType.Group) val groupType = engine.block.getType(group) val members = engine.block.getChildren(group) ``` Use `engine.block.findByType(DesignBlockType.Group)` to get all group blocks in the current scene. Use `engine.block.getType(...)` to check if a specific block is a group by comparing the result to `DesignBlockType.Group.key`. Use `engine.block.getChildren(...)` to get the member blocks of a group. ## Ungroup Blocks Use `engine.block.ungroup(...)` to dissolve a group and release its children back to the parent container. The children maintain their current positions in the scene. ```kotlin highlight-android-ungroup engine.block.ungroup(group) val groupsAfterUngroup = engine.block.findByType(DesignBlockType.Group) ``` ## Troubleshooting ### Blocks Cannot Be Grouped If `engine.block.isGroupable(...)` returns `false`: - Check that none of the blocks is a scene or page block, because scenes and pages cannot be grouped - Check that all blocks are attached to the same page, or that all are detached from pages - Check whether a block already belongs to a group by calling `engine.block.getParent(...)`, then checking that parent with `engine.block.getType(...) == DesignBlockType.Group.key` If the list contains an invalid block ID, Android reports an error instead of returning `false`. Recreate the selection from current blocks before calling `isGroupable(...)`. ### Enter Group Has No Effect If `engine.block.enterGroup(...)` does not change selection: - Verify that the selected block is a group with `engine.block.getType(...)` - Ensure the `editor/select` scope is enabled for the current editing context ### Group Not Visible After Creation If a newly created group is not visible: - Check that each member block was visible before grouping - Verify the group's opacity with `engine.block.getOpacity(...)` ## API Reference | Method | Description | | ----------------------------------------------------- | ----------------------------------------- | | `engine.block.create(blockType=_)` | Create a block | | `engine.block.createShape(type=_)` | Create a shape block | | `engine.block.setShape(block=_, shape=_)` | Assign a shape to a graphic block | | `engine.block.setWidth(block=_, value=_)` | Set the block width | | `engine.block.setHeight(block=_, value=_)` | Set the block height | | `engine.block.setPositionX(block=_, value=_)` | Set the block's x position | | `engine.block.setPositionY(block=_, value=_)` | Set the block's y position | | `engine.block.createFill(fillType=_)` | Create a fill block | | `engine.block.setFill(block=_, fill=_)` | Assign a fill to a block | | `engine.block.setFillSolidColor(block=_, color=_)` | Set a color fill | | `Color.fromRGBA(r=_, g=_, b=_, a=_)` | Create an RGBA color value | | `engine.block.appendChild(parent=_, child=_)` | Add a block to a parent | | `engine.block.isGroupable(blocks=_)` | Check if blocks can be grouped together | | `engine.block.group(blocks=_)` | Create a group from multiple blocks | | `engine.block.setSelected(block=_, selected=_)` | Add a block to the selection | | `engine.block.enterGroup(block=_)` | Enter group editing mode | | `engine.block.select(block=_)` | Select one block | | `engine.block.exitGroup(block=_)` | Exit group editing mode | | `engine.block.findByType(type=DesignBlockType.Group)` | Find all blocks of a specific type | | `engine.block.getType(block=_)` | Get the type string of a block | | `engine.block.getParent(block=_)` | Get the parent block | | `engine.block.getChildren(block=_)` | Get child blocks of a container | | `engine.block.ungroup(block=_)` | Dissolve a group and release its children | | `engine.block.getOpacity(block=_)` | Get a block's opacity | ## Next Steps - [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control z-order and visibility of blocks - [Position and Align](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) - Arrange blocks precisely on the canvas - [Lock Design](https://img.ly/docs/cesdk/android/create-composition/lock-design-0a81de/) - Protect design elements from unwanted modifications using CE.SDK's scope-based permission system. Control which properties users can edit at both global and block levels. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Layer Management" description: "Organize design elements using a layer stack for precise control over stacking and visibility." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Layers](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) --- ```kotlin file=@cesdk_android_examples/engine-guides-layer-management/LayerManagement.kt reference-only import kotlinx.coroutines.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.RGBAColor import ly.img.engine.ShapeType fun layerManagement( 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 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, ) engine.stop() } private fun createLayerBlock( engine: Engine, x: Float, y: Float, color: RGBAColor, ): DesignBlock { val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block, value = 180F) engine.block.setHeight(block, value = 180F) engine.block.setPositionX(block, value = x) engine.block.setPositionY(block, value = y) engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(block = block, color = color) return block } ``` Organize design elements in CE.SDK using a hierarchical layer stack to control stacking order, visibility, and element relationships. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-layer-management) Design elements in CE.SDK are organized in a hierarchical parent-child structure. Children of a block render in order, with the last child appearing on top. This guide covers how to navigate the block hierarchy, reorder elements, toggle visibility, duplicate blocks, and remove blocks. ## Using the Built-in Layer Panel UI The Android editor exposes layer controls for the selected block in the layer sheet. The sheet includes z-order actions, duplicate, delete, opacity, and blend mode controls when the selected block supports them. For a custom layer panel or automation workflow, use the CreativeEngine block APIs shown below. They operate on the same scene hierarchy that the editor UI manipulates. ## Creating Visual Blocks To demonstrate layer ordering, we create colored rectangle blocks that overlap on the page. Each block is a graphic with a rectangle shape, color fill, size, and position. ```kotlin highlight-android-create-block val backBlock = createLayerBlock( engine = engine, x = 120F, y = 120F, color = Color.fromRGBA(r = 0.10F, g = 0.40F, b = 0.95F, a = 1F), ) val middleBlock = createLayerBlock( engine = engine, x = 190F, y = 190F, color = Color.fromRGBA(r = 0.20F, g = 0.75F, b = 0.45F, a = 1F), ) val frontBlock = createLayerBlock( engine = engine, x = 260F, y = 260F, color = Color.fromRGBA(r = 0.95F, g = 0.30F, b = 0.25F, a = 1F), ) ``` The sample uses one helper to keep block creation consistent: ```kotlin highlight-android-create-helper private fun createLayerBlock( engine: Engine, x: Float, y: Float, color: RGBAColor, ): DesignBlock { val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(block, value = 180F) engine.block.setHeight(block, value = 180F) engine.block.setPositionX(block, value = x) engine.block.setPositionY(block, value = y) engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor(block = block, color = color) return block } ``` ## Navigating the Block Hierarchy Programmatically CE.SDK organizes blocks in a parent-child tree. Every block can have one parent and multiple children. ### Getting a Block's Parent Retrieve the parent of any block using `engine.block.getParent()`. This returns the parent's block ID, or `null` if the block has no parent. ```kotlin highlight-android-get-parent val parent = engine.block.getParent(middleBlock) ``` ### Listing Child Blocks Get all direct children of a block using `engine.block.getChildren()`. Children are returned sorted in rendering order, where the last child renders in front of other children. ```kotlin highlight-android-get-children val children = engine.block.getChildren(page) ``` This is useful when iterating over all elements on a page or within a group. ## Adding and Positioning Blocks When you create a new block, it exists independently until you add it to the hierarchy. Attach blocks by appending them to the end or inserting them at a specific index. ### Appending a Block Add a block as the last child of a parent using `engine.block.appendChild()`. Since the last child renders on top, the appended block becomes the topmost element. ```kotlin highlight-android-append-child engine.block.appendChild(parent = page, child = backBlock) engine.block.appendChild(parent = page, child = middleBlock) engine.block.appendChild(parent = page, child = frontBlock) ``` ### Inserting at a Specific Position Insert a block at a specific index in the layer stack using `engine.block.insertChild()`. Index `0` places the block at the back, behind all other children. ```kotlin highlight-android-insert-child val insertedBlock = createLayerBlock( engine = engine, x = 330F, y = 330F, color = Color.fromRGBA(r = 0.98F, g = 0.78F, b = 0.20F, a = 1F), ) engine.block.insertChild(parent = page, child = insertedBlock, index = 0) ``` ### Reparenting Blocks When you add a block to a new parent with `appendChild()` or `insertChild()`, it is automatically removed from its previous parent. ## Changing Z-Order Once blocks are in the hierarchy, you can change their stacking order without removing and re-adding them. CE.SDK provides four methods for z-order manipulation. ### Bring to Front Move an element to the top of its siblings using `engine.block.bringToFront()`. This gives the block the highest stacking order among its siblings. ```kotlin highlight-android-bring-to-front engine.block.bringToFront(backBlock) ``` ### Send to Back Move an element behind all its siblings using `engine.block.sendToBack()`. This gives the block the lowest stacking order among its siblings. ```kotlin highlight-android-send-to-back engine.block.sendToBack(frontBlock) ``` ### Move Forward One Layer Move an element one position forward using `engine.block.bringForward()`. This swaps the block with its immediate sibling in front. ```kotlin highlight-android-bring-forward engine.block.bringForward(insertedBlock) ``` ### Move Backward One Layer Move an element one position backward using `engine.block.sendBackward()`. This swaps the block with its immediate sibling behind. ```kotlin highlight-android-send-backward engine.block.sendBackward(middleBlock) ``` These incremental operations are useful for fine-tuning the layer order without jumping to extremes. ## Controlling Visibility Visibility lets you hide elements without removing them from the scene. Hidden elements remain in the hierarchy and preserve their properties, but they are not rendered. ```kotlin highlight-android-visibility val isVisible = engine.block.isVisible(insertedBlock) engine.block.setVisible(block = insertedBlock, visible = !isVisible) engine.block.setVisible(block = insertedBlock, visible = true) ``` ## Managing Block Lifecycle CE.SDK provides methods for duplicating blocks to create copies and destroying blocks to remove them permanently. ### Duplicating Blocks Create a copy of a block and its children using `engine.block.duplicate()`. By default, the duplicate is attached to the same parent as the original. ```kotlin highlight-android-duplicate val duplicate = engine.block.duplicate(middleBlock) engine.block.setPositionX(duplicate, value = 430F) engine.block.setPositionY(duplicate, value = 140F) ``` The duplicated block starts at the same position as the original. Reposition it to make it visible as a separate element. ### Checking Block Validity Before performing operations on a block, verify it still exists using `engine.block.isValid()`. A block becomes invalid after it has been destroyed. ```kotlin highlight-android-is-valid val duplicateIsValid = engine.block.isValid(duplicate) ``` ### Removing Blocks Permanently remove a block and all its children from the scene using `engine.block.destroy()`. ```kotlin highlight-android-destroy engine.block.destroy(frontBlock) val frontBlockIsValid = engine.block.isValid(frontBlock) ``` After destruction, any reference to the block becomes invalid. Attempting to use an invalid block ID results in errors. ## Framing the Result After making layer changes, zoom to fit the page in the viewport so the composition is clearly visible. ```kotlin highlight-android-zoom engine.scene.zoomToBlock( page, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) ``` ## Troubleshooting **Block not visible after appendChild**: The block may be behind other elements. Use `engine.block.bringToFront()` or adjust the insert index. **getParent returns null**: The block is not attached to any parent. Attach it with `engine.block.appendChild()` or `engine.block.insertChild()`. **Changes not reflected**: The block handle may be invalid. Check with `engine.block.isValid()` before operations. **Duplicate not appearing**: If `attachToParent` is `false`, the duplicate is not attached automatically. Set it to `true` or manually attach the duplicate. ## API Reference | Method | Category | Description | | --- | --- | --- | | `engine.block.getParent(block=_)` | Hierarchy | Get the parent block of a block | | `engine.block.getChildren(block=_)` | Hierarchy | Get all child blocks in rendering order | | `engine.block.appendChild(parent=_, child=_)` | Hierarchy | Append a block as the last child | | `engine.block.insertChild(parent=_, child=_, index=_)` | Hierarchy | Insert a block at a specific position | | `engine.block.bringToFront(block=_)` | Z-Order | Bring a block to the front of its siblings | | `engine.block.sendToBack(block=_)` | Z-Order | Send a block to the back of its siblings | | `engine.block.bringForward(block=_)` | Z-Order | Move a block one position forward | | `engine.block.sendBackward(block=_)` | Z-Order | Move a block one position backward | | `engine.block.isVisible(block=_)` | Visibility | Check if a block is visible | | `engine.block.setVisible(block=_, visible=_)` | Visibility | Set the visibility of a block | | `engine.block.duplicate(block=_, attachToParent=_)` | Lifecycle | Duplicate a block and its children | | `engine.block.destroy(block=_)` | Lifecycle | Remove a block and its children | | `engine.block.isValid(block=_)` | Lifecycle | Check if a block handle is valid | ## Next Steps - [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) — Group multiple blocks to move or transform them together - [Position and Align](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) — Precisely position elements on the canvas - [Multi-Page Layouts](https://img.ly/docs/cesdk/android/create-composition/multi-page-4d2b50/) — Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Design a Layout" description: "Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/layout-b66311/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Design a Layout](https://img.ly/docs/cesdk/android/create-composition/layout-b66311/) --- ```kotlin file=@cesdk_android_examples/engine-guides-layout/Layout.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Color import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.SceneLayout import ly.img.engine.ShapeType fun layout( license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example.layout") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) // Create a scene with VerticalStack layout. Pages appended to the stack // container arrange top-to-bottom automatically. engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK) // Get the stack container that was created with the scene. val stack = engine.block.findByType(DesignBlockType.Stack).first() // Create two pages that will stack vertically. val page1 = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page1, value = 400F) engine.block.setHeight(page1, value = 300F) engine.block.appendChild(parent = stack, child = page1) val page2 = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page2, value = 400F) engine.block.setHeight(page2, value = 300F) engine.block.appendChild(parent = stack, child = page2) // Configure spacing between stacked pages. engine.block.setFloat(stack, property = "stack/spacing", value = 20F) engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true) // Add an image block to the first page. val block1 = engine.block.create(DesignBlockType.Graphic) val shape1 = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block1, shape = shape1) engine.block.setWidth(block1, value = 350F) engine.block.setHeight(block1, value = 250F) engine.block.setPositionX(block1, value = 25F) engine.block.setPositionY(block1, value = 25F) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block1, fill = imageFill) engine.block.appendChild(parent = page1, child = block1) // Add a colored rectangle to the second page. val block2 = engine.block.create(DesignBlockType.Graphic) val shape2 = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block2, shape = shape2) engine.block.setWidth(block2, value = 350F) engine.block.setHeight(block2, value = 250F) engine.block.setPositionX(block2, value = 25F) engine.block.setPositionY(block2, value = 25F) engine.block.setFill(block2, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block2, color = Color.fromRGBA(r = 0.3F, g = 0.6F, b = 0.9F, a = 1F), ) engine.block.appendChild(parent = page2, child = block2) // Switch to a horizontal stack. Existing pages reposition left-to-right. engine.scene.setLayout(SceneLayout.HORIZONTAL_STACK) // Verify the layout type. val currentLayout = engine.scene.getLayout() println("Current layout: $currentLayout") // Append a new page to the existing stack. It snaps to the end with the // configured spacing. val page3 = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page3, value = 400F) engine.block.setHeight(page3, value = 300F) engine.block.appendChild(parent = stack, child = page3) // Add content to the new page. val block3 = engine.block.create(DesignBlockType.Graphic) val shape3 = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block3, shape = shape3) engine.block.setWidth(block3, value = 350F) engine.block.setHeight(block3, value = 250F) engine.block.setPositionX(block3, value = 25F) engine.block.setPositionY(block3, value = 25F) engine.block.setFill(block3, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block3, color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.3F, a = 1F), ) engine.block.appendChild(parent = page3, child = block3) // Move page3 to the first position using insertChild. engine.block.insertChild(parent = stack, child = page3, index = 0) // Verify the new order. val pageOrder = engine.block.getChildren(stack) println("Page order after reordering: $pageOrder") // Update the spacing between stacked pages. engine.block.setFloat(stack, property = "stack/spacing", value = 40F) // Verify the spacing value. val updatedSpacing = engine.block.getFloat(stack, property = "stack/spacing") println("Updated spacing: $updatedSpacing") // Switch back to a free layout to position pages manually. engine.scene.setLayout(SceneLayout.FREE) // Position a page directly; stacks no longer manage placement. val page = engine.block.findByType(DesignBlockType.Page).first() engine.block.setPositionX(page, value = 100F) engine.block.setPositionY(page, value = 200F) engine.stop() } ``` Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-layout) Stack layouts arrange pages automatically with consistent spacing. Vertical stacks arrange pages top-to-bottom, while horizontal stacks arrange them left-to-right. This eliminates manual positioning for compositions like photo collages, product catalogs, or social media carousels. This guide covers how to: - Create vertical and horizontal stack layouts - Add pages and blocks to stacks - Configure spacing between stacked pages - Reorder pages within a stack - Switch between stack and free layouts ## Create a Vertical Stack Layout Vertical stacks arrange pages from top to bottom. Create a scene with `SceneLayout.VERTICAL_STACK`, then append pages to the stack container. ```kotlin highlight-android-vertical-stack // Create a scene with VerticalStack layout. Pages appended to the stack // container arrange top-to-bottom automatically. engine.scene.create(sceneLayout = SceneLayout.VERTICAL_STACK) // Get the stack container that was created with the scene. val stack = engine.block.findByType(DesignBlockType.Stack).first() // Create two pages that will stack vertically. val page1 = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page1, value = 400F) engine.block.setHeight(page1, value = 300F) engine.block.appendChild(parent = stack, child = page1) val page2 = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page2, value = 400F) engine.block.setHeight(page2, value = 300F) engine.block.appendChild(parent = stack, child = page2) // Configure spacing between stacked pages. engine.block.setFloat(stack, property = "stack/spacing", value = 20F) engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true) ``` When you create a scene with `SceneLayout.VERTICAL_STACK`, CE.SDK automatically adds a stack container. Pages appended to this container position themselves with the configured spacing. The `stack/spacingInScreenspace` property keeps spacing visually consistent at any zoom level. ## Add Blocks to Pages Each page can contain multiple blocks. Create blocks with a shape and fill, position them inside the page, then append them as children. ```kotlin highlight-android-add-blocks // Add an image block to the first page. val block1 = engine.block.create(DesignBlockType.Graphic) val shape1 = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block1, shape = shape1) engine.block.setWidth(block1, value = 350F) engine.block.setHeight(block1, value = 250F) engine.block.setPositionX(block1, value = 25F) engine.block.setPositionY(block1, value = 25F) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block1, fill = imageFill) engine.block.appendChild(parent = page1, child = block1) // Add a colored rectangle to the second page. val block2 = engine.block.create(DesignBlockType.Graphic) val shape2 = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block2, shape = shape2) engine.block.setWidth(block2, value = 350F) engine.block.setHeight(block2, value = 250F) engine.block.setPositionX(block2, value = 25F) engine.block.setPositionY(block2, value = 25F) engine.block.setFill(block2, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block2, color = Color.fromRGBA(r = 0.3F, g = 0.6F, b = 0.9F, a = 1F), ) engine.block.appendChild(parent = page2, child = block2) ``` Graphic blocks require both a shape and a fill to be visible. Use an image fill with `fill/image/imageFileURI` for image content, or a color fill for solid colors. Position blocks inside their parent page with `setPositionX` and `setPositionY`. ## Switch to Horizontal Layout Change the layout direction at any time with `setLayout`. Horizontal stacks arrange pages left-to-right instead of top-to-bottom. ```kotlin highlight-android-horizontal-stack // Switch to a horizontal stack. Existing pages reposition left-to-right. engine.scene.setLayout(SceneLayout.HORIZONTAL_STACK) // Verify the layout type. val currentLayout = engine.scene.getLayout() println("Current layout: $currentLayout") ``` Horizontal layouts suit carousels, timelines, and horizontal galleries. Existing pages reposition automatically when you change the layout type. ## Add Pages to Existing Stacks Append new pages to an existing stack at any time. Pages snap to the end of the stack with the configured spacing. ```kotlin highlight-android-add-page // Append a new page to the existing stack. It snaps to the end with the // configured spacing. val page3 = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page3, value = 400F) engine.block.setHeight(page3, value = 300F) engine.block.appendChild(parent = stack, child = page3) // Add content to the new page. val block3 = engine.block.create(DesignBlockType.Graphic) val shape3 = engine.block.createShape(ShapeType.Rect) engine.block.setShape(block3, shape = shape3) engine.block.setWidth(block3, value = 350F) engine.block.setHeight(block3, value = 250F) engine.block.setPositionX(block3, value = 25F) engine.block.setPositionY(block3, value = 25F) engine.block.setFill(block3, fill = engine.block.createFill(FillType.Color)) engine.block.setFillSolidColor( block = block3, color = Color.fromRGBA(r = 0.9F, g = 0.5F, b = 0.3F, a = 1F), ) engine.block.appendChild(parent = page3, child = block3) ``` The stack container handles positioning automatically. You can populate the new page with content before or after appending it. ## Reorder Pages Change page order with `insertChild` to place a page at a specific index inside the stack. ```kotlin highlight-android-reorder // Move page3 to the first position using insertChild. engine.block.insertChild(parent = stack, child = page3, index = 0) // Verify the new order. val pageOrder = engine.block.getChildren(stack) println("Page order after reordering: $pageOrder") ``` Removing a page from its current slot and reinserting it at index 0 moves it to the first position. The remaining pages shift to make room. ## Change Stack Spacing Adjust spacing between pages by setting the `stack/spacing` property on the stack block. ```kotlin highlight-android-spacing // Update the spacing between stacked pages. engine.block.setFloat(stack, property = "stack/spacing", value = 40F) // Verify the spacing value. val updatedSpacing = engine.block.getFloat(stack, property = "stack/spacing") println("Updated spacing: $updatedSpacing") ``` Spacing updates take effect immediately and pages reposition automatically. Read the current value back with `getFloat`. ## Switch to Free Layout For manual positioning, switch to `SceneLayout.FREE`. Pages keep their current positions but stop auto-arranging. ```kotlin highlight-android-free-layout // Switch back to a free layout to position pages manually. engine.scene.setLayout(SceneLayout.FREE) // Position a page directly; stacks no longer manage placement. val page = engine.block.findByType(DesignBlockType.Page).first() engine.block.setPositionX(page, value = 100F) engine.block.setPositionY(page, value = 200F) ``` Free layout gives full control over page positions. Use this when you need precise placement that stack layouts cannot provide. ## Troubleshooting **Pages not arranging automatically** — Verify the scene layout is `SceneLayout.VERTICAL_STACK` or `SceneLayout.HORIZONTAL_STACK` with `getLayout()`. **Spacing not applying** — Set `stack/spacing` on the stack block, not the scene. Use `findByType(DesignBlockType.Stack)` to locate the container. **Pages overlapping** — Ensure pages are direct children of the stack container. Nested pages do not auto-arrange. **Can't position manually** — Stack layouts override manual positions. Switch to `SceneLayout.FREE` for manual control. **Wrong stacking order** — Child order determines position. Use `insertChild(parent = ..., child = ..., index = ...)` to move pages to a specific slot. ## API Reference | Method | Description | |--------|-------------| | `engine.scene.create(sceneLayout=_)` | Create a scene with the specified layout (`SceneLayout.FREE`, `SceneLayout.VERTICAL_STACK`, `SceneLayout.HORIZONTAL_STACK`, `SceneLayout.DEPTH_STACK`). | | `engine.scene.setLayout(layout=_)` | Change the layout of the current scene. | | `engine.scene.getLayout()` | Get the current scene layout. | | `engine.block.findByType(type=DesignBlockType.Stack)` | Find the stack container block. | | `engine.block.setFloat(block=_, property="stack/spacing", value=_)` | Set spacing between stacked pages. | | `engine.block.getFloat(block=_, property="stack/spacing")` | Get the current spacing value. | | `engine.block.setBoolean(block=_, property="stack/spacingInScreenspace", value=_)` | Set whether spacing is measured in screen pixels. | | `engine.block.appendChild(parent=_, child=_)` | Append a page to the stack. | | `engine.block.insertChild(parent=_, child=_, index=_)` | Insert a page at a specific position. | | `engine.block.getChildren(block=_)` | Get child blocks in order. | ## Next Steps - [Auto-resize](https://img.ly/docs/cesdk/android/automation/auto-resize-4c2d58/) — Make blocks fit parent containers - [Manual Positioning](https://img.ly/docs/cesdk/android/edit-image/transform/move-818dd9/) — Position blocks in free layouts - [Layer Hierarchies](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) — Organize blocks in hierarchical structures - [Grouping](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) — Create nested layout hierarchies --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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 android.app.Application import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.GlobalScope import ly.img.engine.ShapeType import ly.img.engine.SizeMode suspend fun lockDesign( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = withContext(Dispatchers.Main) { Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) withEngineCleanup(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 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) 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) } } private suspend fun withEngineCleanup( engine: Engine, block: suspend () -> Unit, ) { try { block() } finally { engine.stop() } } ``` 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. ## Initialize the Engine Standalone headless Android samples must initialize the engine once for the app process before creating or starting an `Engine`. In app code, this typically belongs in `Application.onCreate()`; this sample accepts an `Application` so the required setup is visible. ```kotlin highlight-android-initialize-engine Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) ``` ## 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) ``` ## Cleanup Resources Stop the engine in a `finally` block when a headless sample owns the engine lifecycle. This releases engine resources even if one of the locking steps fails. ```kotlin highlight-android-cleanup-resources try { block() } finally { engine.stop() } ``` ## 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 | | `engine.stop()` | Stop the headless engine and release resources | ## Next Steps - [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.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.SceneLayout import ly.img.engine.ShapeType fun multiPage( 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) // 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.stop() } ``` Create multi-page designs in CE.SDK for brochures, presentations, catalogs, and other documents requiring multiple pages within a single scene. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-multi-page) Multi-page layouts allow you to create documents with multiple pages within a single scene. Each page is an independent canvas that can contain different content while sharing the same scene context. CE.SDK provides scene layout modes that arrange pages vertically, horizontally, or in a free-form canvas. This guide covers how to create multi-page scenes, add and manage pages, configure spacing between pages, and focus the viewport on a page. ## Using the Built-in Page Management UI The CE.SDK Android editor includes a pages mode that displays page thumbnails in a grid. Users can add pages, move the selected page up or down, duplicate supported pages, delete pages when more than one page remains, resize pages, and switch back to editing a selected page. Page thumbnails make the document structure visible at a glance. Selecting a page updates the current page, and opening it in edit mode focuses the editor on that page. ## Creating Multi-Page Scenes Programmatically We can create scenes with multiple pages using the engine API. The scene acts as the document container, and each page can hold independent content blocks. ### Creating a Scene with Pages We create a new scene with `engine.scene.create(sceneLayout = SceneLayout.HORIZONTAL_STACK)`. Stack layouts create a stack block that owns the pages, so we append page blocks to that stack. ```kotlin highlight-android-create-scene // Create a scene with HorizontalStack layout. engine.scene.create(sceneLayout = SceneLayout.HORIZONTAL_STACK) // Get the stack container that owns pages in stack layouts. val stack = engine.block.findByType(DesignBlockType.Stack).first() // Create the first page. val firstPage = engine.block.create(DesignBlockType.Page) engine.block.setWidth(firstPage, value = 800F) engine.block.setHeight(firstPage, value = 600F) engine.block.appendChild(parent = stack, child = firstPage) ``` The scene uses `SceneLayout.HORIZONTAL_STACK`, so pages are arranged side by side from left to right. The first page is created with 800 x 600 dimensions and appended to the stack container. ### Configuring Page Spacing We can add spacing between pages in a stack layout with the `stack/spacing` property. This creates visual separation between adjacent pages. ```kotlin highlight-android-stack-spacing // Add spacing between pages (20 pixels in screen space). engine.block.setFloat(stack, property = "stack/spacing", value = 20F) engine.block.setBoolean(stack, property = "stack/spacingInScreenspace", value = true) ``` Setting `stack/spacingInScreenspace` to `true` interprets the spacing value as screen pixels, so the visual spacing stays consistent while zooming. ### Adding More Pages To add another page, we create a new page block, set its dimensions, and append it to the same stack container. ```kotlin highlight-android-add-page // Create a second page with different content. val secondPage = engine.block.create(DesignBlockType.Page) engine.block.setWidth(secondPage, value = 800F) engine.block.setHeight(secondPage, value = 600F) engine.block.appendChild(parent = stack, child = secondPage) // Add a different image to the second page. val imageBlock2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(imageBlock2, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(imageBlock2, value = 300F) engine.block.setHeight(imageBlock2, value = 200F) engine.block.setPositionX(imageBlock2, value = 250F) engine.block.setPositionY(imageBlock2, value = 200F) val imageFill2 = engine.block.createFill(FillType.Image) engine.block.setString( block = imageFill2, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_2.jpg", ) engine.block.setFill(imageBlock2, fill = imageFill2) engine.block.appendChild(parent = secondPage, child = imageBlock2) ``` Each page can contain different content. Here the second page receives a separate image block, showing that page contents are independent. ### Managing Pages Existing pages are available through `engine.scene.getPages()`. Use this list for page counts, page pickers, and operations that need a stable page order. ```kotlin highlight-android-list-pages val pages = engine.scene.getPages() println("Pages: ${pages.size}") ``` The current page comes from the selected content when that page is visible enough; otherwise CE.SDK returns the page nearest to the viewport center. ```kotlin highlight-android-current-page val currentPage = engine.scene.getCurrentPage() println("Current page: $currentPage") ``` `engine.block.duplicate()` copies a page and its children. ```kotlin highlight-android-duplicate-page val duplicatedPage = engine.block.duplicate(firstPage) ``` `engine.block.insertChild()` moves a page to a specific index in the stack, and `engine.block.destroy()` removes a page. Keep at least one page in the scene. ```kotlin highlight-android-reorder-delete-pages engine.block.insertChild(parent = stack, child = secondPage, index = 0) if (engine.scene.getPages().size > 1) { engine.block.destroy(duplicatedPage) } ``` ## Scene Layout Types CE.SDK supports different layout modes that control how pages are arranged on the canvas. You specify the layout type when creating the scene with `engine.scene.create(sceneLayout = ...)` or change it later with `engine.scene.setLayout(...)`. **Free Layout** (`SceneLayout.FREE`) is the default where pages can be positioned anywhere on the canvas. This provides complete control over page placement. **VerticalStack Layout** (`SceneLayout.VERTICAL_STACK`) arranges pages automatically in a vertical stack from top to bottom. This is useful for scroll-based document previews. **HorizontalStack Layout** (`SceneLayout.HORIZONTAL_STACK`) arranges pages side by side from left to right. This is useful for carousel-style presentations or side-by-side comparisons. ## Navigating Between Pages We can focus the viewport on a specific page with the suspending `engine.scene.zoomToBlock(...)` API. Padding values are interpreted in screen pixels. ```kotlin highlight-android-zoom-to-page engine.scene.zoomToBlock( block = firstPage, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F, ) ``` To derive navigation state from the viewport, find pages sorted by distance to the viewport center and use the first result. ```kotlin highlight-android-nearest-page val nearestPage = engine.scene.findNearestToViewPortCenterByType( DesignBlockType.Page, ).firstOrNull() println("Nearest page: $nearestPage") ``` ## Troubleshooting **Page not visible after creation**: Ensure the page is attached to the stack with `appendChild(...)` and has valid dimensions set with `setWidth(...)` and `setHeight(...)`. **Cannot add content to page**: Verify you're appending blocks to the page block, not the scene directly. Content blocks should be children of pages. **Pages overlapping**: When using stack layouts, make sure pages are appended to the stack container, found with `findByType(DesignBlockType.Stack)`, not directly to the scene. **Spacing not visible**: Check that `stack/spacing` is set to a positive value and that you're using a stack layout (`SceneLayout.HORIZONTAL_STACK` or `SceneLayout.VERTICAL_STACK`). ## API Reference | Method | Purpose | | ------ | ------- | | `engine.scene.create(sceneLayout=_)` | Create a scene with a free, vertical stack, or horizontal stack layout. | | `engine.scene.setLayout(layout=_)` | Change the current scene layout. | | `engine.scene.getPages()` | Return the sorted list of pages in the current scene. | | `engine.scene.getCurrentPage()` | Return the selected or viewport-centered current page. | | `engine.scene.zoomToBlock(block=_, paddingLeft=_, paddingTop=_, paddingRight=_, paddingBottom=_)` | Focus the viewport on a page or another block. | | `engine.scene.findNearestToViewPortCenterByType(blockType=DesignBlockType.Page)` | Find pages sorted by distance to the viewport center. | | `engine.block.create(blockType=DesignBlockType.Page)` | Create a page block. | | `engine.block.appendChild(parent=_, child=_)` | Attach a page to the scene or stack container. | | `engine.block.insertChild(parent=_, child=_, index=_)` | Move a page to a specific child index. | | `engine.block.duplicate(block=_)` | Copy a page and its children. | | `engine.block.destroy(block=_)` | Remove a page or block from the scene. | | `engine.block.setWidth(block=_, value=_)` | Set a page width. | | `engine.block.setHeight(block=_, value=_)` | Set a page height. | | `engine.block.setFloat(block=_, property="stack/spacing", value=_)` | Configure spacing between pages in stack layouts. | | `engine.block.setBoolean(block=_, property="stack/spacingInScreenspace", value=_)` | Keep stack spacing fixed in screen pixels. | ## Next Steps - [Options](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) - Explore export options, supported formats, and configuration features for sharing or rendering output. - [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Organize design elements using a layer stack for precise control over stacking and visibility. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Combine and arrange multiple elements to create complex, multi-page, or layered design compositions." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/overview-5b19c5/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Overview](https://img.ly/docs/cesdk/android/create-composition/overview-5b19c5/) --- In CreativeEditor SDK (CE.SDK), a *composition* is an arrangement of multiple design elements—such as images, text, shapes, graphics, and effects—combined into a single, cohesive visual layout. Unlike working with isolated elements, compositions allow you to design complex, multi-element visuals that tell a richer story or support more advanced use cases. On Android, composition processing runs on the device, keeping editing responsive without requiring server infrastructure. You can use compositions to create a wide variety of projects, including social media posts, marketing materials, collages, and multi-page exports like PDFs. Whether you build layouts manually through the Android editor UI or generate them with CreativeEngine APIs, compositions give you the flexibility and control to design at scale. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) ## Exporting Compositions CE.SDK compositions can be exported in several formats: --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Programmatic Creation" description: "Build compositions entirely through code with the CE.SDK Engine for automation, batch processing, and headless rendering." platform: android url: "https://img.ly/docs/cesdk/android/create-composition/programmatic-a688bf/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/android/create-composition-db709c/) > [Programmatic Creation](https://img.ly/docs/cesdk/android/create-composition/programmatic-a688bf/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-composition-programmatic/CreateCompositionProgrammatic.kt reference-only import android.app.Application import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Color import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.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 fun createCompositionProgrammatic( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope(Dispatchers.Main).launch { exportProgrammaticComposition(application = application, license = license, userId = userId) } suspend fun exportProgrammaticComposition( application: Application, license: String?, userId: String, ): File = withContext(Dispatchers.Main) { var engine: Engine? = null var engineStarted = false try { Engine.init(application) val currentEngine = Engine.getInstance(id = "ly.img.engine.example") engine = currentEngine engineStarted = currentEngine.start(license = license, userId = userId) currentEngine.bindOffscreen(width = 1080, height = 1080) buildProgrammaticComposition(currentEngine) } finally { if (engineStarted) { engine?.stop() } } } 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 bytes = ByteArray(blob.remaining()) blob.get(bytes) outputFile.outputStream().use { output -> output.write(bytes) } 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. ## Initialize the Engine The runnable sample owns an `Engine` instance because it renders the composition without opening an editor UI. Inside the main-dispatcher export function, initialize CE.SDK with your `Application`, start the engine with your license and user ID, and bind an offscreen render surface for headless rendering. The page dimensions and `ExportOptions(targetWidth = 1080F, targetHeight = 1080F)` shown later control the exported PNG dimensions. ```kotlin highlight-android-setup Engine.init(application) val currentEngine = Engine.getInstance(id = "ly.img.engine.example") engine = currentEngine engineStarted = currentEngine.start(license = license, userId = userId) currentEngine.bindOffscreen(width = 1080, height = 1080) ``` ## 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 copies the remaining bytes into a `ByteArray`, 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 bytes = ByteArray(blob.remaining()) blob.get(bytes) outputFile.outputStream().use { output -> output.write(bytes) } outputFile } ``` ## Clean Up Resources The sample tracks whether `Engine.start()` started the named Engine instance. Inside `finally`, it only stops the Engine when this call owns that running instance: ```kotlin highlight-android-cleanup if (engineStarted) { engine?.stop() } ``` This ensures resources started by the sample are released even if scene creation, export, or file writing fails. ## 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. | | `Engine.init(application=_)` | Engine | Initialize CE.SDK before creating an Engine instance. | | `engine.stop()` | Engine | Release resources owned by the Engine instance. | ## Troubleshooting - **Blocks not appearing**: Verify that `appendChild()` attaches blocks to the page. Blocks must be part of the scene hierarchy to render. - **Engine not initialized**: Call `Engine.init(application)` before `Engine.getInstance(...)`, typically from `Application.onCreate()`. - **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. - **Resources remain active**: Call `engine.stop()` in `finally` when the sample starts its own Engine instance. ## Next Steps - [Layer Management](https://img.ly/docs/cesdk/android/create-composition/layer-management-18f07a/) - Control block stacking and organization - [Positioning and Alignment](https://img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a/) - Precise block placement - [Group and Ungroup](https://img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a/) - Group blocks for unified transforms - [Blend Modes](https://img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519/) - Control how blocks interact visually - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Export options and formats --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Templates" description: "Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK." platform: android url: "https://img.ly/docs/cesdk/android/create-templates-3aef79/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/android/create-templates/overview-4ebe30/) - Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK. - [Create Templates From Scratch in Android (Kotlin)](https://img.ly/docs/cesdk/android/create-templates/from-scratch-663cda/) - Build and save reusable CE.SDK templates programmatically in Android using Kotlin. - [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. - [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. - [Apply a Template](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) - Learn how to apply template scenes via API in the CreativeEditor SDK. - [Generate From Templates](https://img.ly/docs/cesdk/android/use-templates/generate-334e15/) - Learn how to load and populate CE.SDK templates in Kotlin for Android applications. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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/) > [Insert Dynamic Content](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7/) --- --- ## 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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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/) > [Insert 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 reference-only // Check if block supports placeholder behavior if (engine.block.supportsPlaceholderBehavior(block)) { // Enable the placeholder behavior engine.block.setPlaceholderBehaviorEnabled(block, enabled = true) val placeholderBehaviorIsEnabled = engine.block.isPlaceholderBehaviorEnabled(block) // Enable the placeholder capabilities (interaction in Adopter mode) engine.block.setPlaceholderEnabled(block, enabled = true) val placeholderIsEnabled = engine.block.isPlaceholderEnabled(block) // Check if block supports placeholder controls if (engine.block.supportsPlaceholderControls(block)) { // Enable the visibility of the placeholder overlay pattern engine.block.setPlaceholderControlsOverlayEnabled(block, enabled = true) val overlayEnabled = engine.block.isPlaceholderControlsOverlayEnabled(block) // Enable the visibility of the placeholder button engine.block.setPlaceholderControlsButtonEnabled(block, enabled = true) val buttonEnabled = engine.block.isPlaceholderControlsButtonEnabled(block) } } ``` In this example, we will demonstrate how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)'s CreativeEngine to manage placeholder behavior and controls through the block Api. ## Placeholder Behavior and Controls ```kotlin fun supportsPlaceholderBehavior(block: DesignBlock): Boolean ``` Query whether the block supports placeholder behavior. - `block`: the block to query. - Returns whether the block supports placeholder behavior. ```kotlin fun setPlaceholderBehaviorEnabled( block: DesignBlock, enabled: Boolean, ) ``` Enable or disable the placeholder behavior for a block. - `block`: the block whose placeholder behavior should be enabled or disabled. - `enabled`: Whether the placeholder behavior should be enabled or disabled. ```kotlin fun isPlaceholderBehaviorEnabled(block: DesignBlock): Boolean ``` Query whether the placeholder behavior for a block is enabled. - `block`: the block whose placeholder behavior state should be queried. - Returns the enabled state of the block's placeholder behavior. ```kotlin fun setPlaceholderEnabled( block: DesignBlock, enabled: Boolean, ) ``` Enable or disable the placeholder function for a block. - `block`: the block whose placeholder function should be enabled or disabled. - `enabled`: whether the function should be enabled or disabled. ```kotlin fun isPlaceholderEnabled(block: DesignBlock): Boolean ``` Query whether the placeholder function for a block is enabled. - `block`: the block whose placeholder function state should be queried. - Returns the enabled state of the placeholder function. ```kotlin fun supportsPlaceholderControls(block: DesignBlock): Boolean ``` Checks whether the block supports placeholder controls. - `block`: The block to query. - Returns whether the block supports placeholder controls. ```kotlin fun setPlaceholderControlsOverlayEnabled( block: DesignBlock, enabled: Boolean, ) ``` Enable or disable the visibility of the placeholder overlay pattern for a block. - `block`: The block whose placeholder overlay should be enabled or disabled. - `enabled`: Whether the placeholder overlay should be shown or not. ```kotlin fun isPlaceholderControlsOverlayEnabled(block: DesignBlock): Boolean ``` Query whether the placeholder overlay pattern for a block is shown. - `block`: The block whose placeholder overlay visibility state should be queried. - Returns the visibility state of the block's placeholder overlay pattern. ```kotlin fun setPlaceholderControlsButtonEnabled( block: DesignBlock, enabled: Boolean, ) ``` Enable or disable the visibility of the placeholder button for a block. - `block`: The block whose placeholder button should be shown or not. - `enabled`: Whether the placeholder button should be shown or not. ```kotlin fun isPlaceholderControlsButtonEnabled(block: DesignBlock): Boolean ``` Query whether the placeholder button for a block is shown. - `block`: The block whose placeholder button visibility state should be queried. - Returns the visibility state of the block's placeholder button. ## Full Code Here's the full code: ```kotlin // Check if block supports placeholder behavior if (engine.block.supportsPlaceholderBehavior(block)) { // Enable the placeholder behavior engine.block.setPlaceholderBehaviorEnabled(block, enabled = true) val placeholderBehaviorIsEnabled = engine.block.isPlaceholderBehaviorEnabled(block) // Enable the placeholder capabilities (interaction in Adopter mode) engine.block.setPlaceholderEnabled(block, enabled = true) val placeholderIsEnabled = engine.block.isPlaceholderEnabled(block) // Check if block supports placeholder controls if (engine.block.supportsPlaceholderControls(block)) { // Enable the visibility of the placeholder overlay pattern engine.block.setPlaceholderControlsOverlayEnabled(block, enabled = true) val overlayEnabled = engine.block.isPlaceholderControlsOverlayEnabled(block) // Enable the visibility of the placeholder button engine.block.setPlaceholderControlsButtonEnabled(block, enabled = true) val buttonEnabled = engine.block.isPlaceholderControlsButtonEnabled(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: "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/) > [Insert 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 reference-only // Query all variables val variableNames = engine.variable.findAll() // Set, get and remove a variable engine.variable.set(key = "name", value = "Chris") val name = engine.variable.get(key = "name") // Chris engine.variable.remove(key = "name") val block = engine.block.create(DesignBlockType.Graphic) engine.block.referencesAnyVariables(block) ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)'s CreativeEngine to modify variables through the `variable` API. The `variable` API lets you set or get the contents of variables that exist in your scene. ## Functions ```kotlin fun findAll(): List ``` Get all text variables currently stored in the engine. - Returns a list of variable names. ```kotlin fun set( key: String, value: String, ) ``` Set a text variable. - `key`: the variable's key. - `value`: the text to replace the variable with. ```kotlin fun get(key: String): String ``` Get a text variable. - `key`: the variable's key. - Returns the text value of the variable. ```kotlin fun remove(key: String) ``` Destroy a text variable. - `key`: the variable's key. ```kotlin fun referencesAnyVariables(block: DesignBlock): Boolean ``` Checks whether the given block references any variables. Doesn't check the block's children. - `block`: the block to query. - Returns true if the block references variables, false otherwise. ## Localizing Variable Keys (CE.SDK only) You can show localized labels for the registered variables to your users by adding a corresponding label property to the object stored at `i18n..variables..label` in the configuration. Otherwise, the name used in `variable.setString()` will be shown. ![](./assets/variables-dark.png) ## Full Code Here's the full code: ```kotlin // Query all variables val variableNames = engine.variable.findAll() // Set, get and remove a variable engine.variable.set(key = "name", value = "Chris") val name = engine.variable.get(key = "name") // Chris engine.variable.remove(key = "name") val block = engine.block.create(DesignBlockType.Graphic) engine.block.referencesAnyVariables(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: "Create Templates From Scratch in Android (Kotlin)" description: "Build and save reusable CE.SDK templates programmatically in Android using Kotlin." 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/) --- Templates define a reusable design pattern—text regions, image placeholders, and locked brand elements that your app can populate at runtime. This guide walks you through creating a template **from scratch** in Android using Kotlin, enabling variable bindings, and saving the result as a string or archive for reuse. ## What You'll Learn - Differences between **templates** and **scenes**. - Programmatically build a template scene. - Enable **variable** bindings for dynamic text. - Save templates to **string** or **archive**. - Store basic **metadata** for library use. ## When to Use It Choose this guide when you need to **author** templates programmatically for things such as: - Automation pipelines - Unit tests - Code‑generated layouts. Prefer the web-based CE.SDK editors if your goal is to let designers craft rich templates visually including: - Marking placeholders. - Locking styles. - Setting edit permissions. ## Templates vs Scenes - **Scene**: a complete document (pages, blocks, assets). Edit and export it directly. - **Template**: a reusable pattern applied to scenes; often includes placeholders and variables to control what's editable versus locked. ## Create Templates Programmatically The web-based CE.SDK editors include built-in template logic and UI. You can use them to: - Mark blocks as placeholders - Bind variables - Assign granular edit permissions. For most teams, this is the recommended path to author templates. This guide shows how to achieve similar results **in Android/Kotlin**, which is useful for code‑driven generation, CI pipelines, or dynamic authoring. In the code below: - You'll create a scene. - Add a page. - Insert a text block bound to a variable - Add an image block. ```kotlin import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) // Text block bound to a variable (e.g., {{name}}) val text = engine.block.create(DesignBlockType.Text) engine.block.setString(text, property = "text/text", value = "{{name}}") engine.block.setPositionX(text, value = 0.1F) engine.block.setPositionY(text, value = 0.1F) engine.block.appendChild(parent = page, child = text) // Image block for dynamic content val image = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(image, shape = shape) val imageFill = engine.block.createFill(FillType.Image) engine.block.setFill(image, fill = imageFill) engine.block.setWidth(image, value = 300F) engine.block.setHeight(image, value = 200F) engine.block.setPositionX(image, value = 0.1F) engine.block.setPositionY(image, value = 0.3F) engine.block.appendChild(parent = page, child = image) ``` ## Binding Variables - Use variables for [text substitution](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/). - Use named blocks or image fills for media that users swap at runtime. Define a variable in text using curly brackets. The variable can be the entire string or part of a string, such as `"Hello, {{guest_name}}"`. To populate variables at runtime: ```kotlin import ly.img.engine.Engine // Populate the template with actual data engine.variable.set(key = "name", value = "John Smith") engine.variable.set(key = "guest_name", value = "Alice") ``` For image replacement in templates, use named blocks: ```kotlin import ly.img.engine.Engine // Give the image block a name for easy lookup engine.block.setString(imageBlock, property = "name", value = "profile-photo") // Later, find and replace the image fill val blocks = engine.block.findByType(DesignBlockType.Graphic) for (block in blocks) { val name = engine.block.getString(block, property = "name") if (name == "profile-photo") { val fill = engine.block.getFill(block) engine.block.setString(fill, property = "fill/image/imageFileURI", value = "https://example.com/photo.jpg") } } ``` ## Saving Templates Templates are scenes with some extra settings. Save templates: - Use the same logic as for scenes. - Save as a **string** for a lightweight file: the template needs to be able to resolve all asset URLs at runtime. - Save as an **archive** for a self-contained, portable file: bundles the assets into the file. ### Save as String ```kotlin import ly.img.engine.Engine val sceneAsString = engine.scene.saveToString(scene = scene) // Persist to your DB or send to a backend ``` ### Save as Archive ```kotlin import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import java.io.File suspend fun saveTemplateAsArchive( engine: Engine, context: Context, scene: Int ): File { val blob = engine.scene.saveToArchive(scene = scene) // Save to file val file = File(context.filesDir, "template_${System.currentTimeMillis()}.cesdk") withContext(Dispatchers.IO) { file.outputStream().channel.use { channel -> channel.write(blob) } } return file } ``` Once you've created the string or data blob, use standard methods to persist it. ## Complete Example Here's a complete example that creates a template from scratch: ```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.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 import java.io.File fun createTemplateFromScratch( context: Context, license: String, userId: String ) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.template") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) try { // Create scene and page 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) // Background (brand color that stays locked) val background = engine.block.create(DesignBlockType.Graphic) val bgShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(background, shape = bgShape) val bgFill = engine.block.createFill(FillType.Color) val bgColor = Color.fromRGBA(r = 0.95F, g = 0.95F, b = 0.98F, a = 1.0F) engine.block.setColor(bgFill, property = "fill/color/value", color = bgColor) engine.block.setFill(background, fill = bgFill) engine.block.appendChild(parent = page, child = background) engine.block.fillParent(background) engine.block.sendToBack(background) // Title text with variable val title = engine.block.create(DesignBlockType.Text) engine.block.setString(title, property = "text/text", value = "{{product_name}}") engine.block.setTextFontSize(title, fontSize = 48F) engine.block.setWidthMode(title, mode = SizeMode.AUTO) engine.block.setHeightMode(title, mode = SizeMode.AUTO) engine.block.setPositionX(title, value = 100F) engine.block.setPositionY(title, value = 200F) engine.block.appendChild(parent = page, child = title) // Description text with variable val description = engine.block.create(DesignBlockType.Text) engine.block.setString(description, property = "text/text", value = "{{description}}") engine.block.setTextFontSize(description, fontSize = 24F) engine.block.setWidthMode(description, mode = SizeMode.AUTO) engine.block.setHeightMode(description, mode = SizeMode.AUTO) engine.block.setPositionX(description, value = 100F) engine.block.setPositionY(description, value = 300F) engine.block.appendChild(parent = page, child = description) // Product image placeholder (named for easy replacement) val productImage = engine.block.create(DesignBlockType.Graphic) val imageShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(productImage, shape = imageShape) val imageFill = engine.block.createFill(FillType.Image) // Set a placeholder image URL engine.block.setString( imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg" ) engine.block.setFill(productImage, fill = imageFill) // Name it for easy lookup later engine.block.setString(productImage, property = "name", value = "product-image") engine.block.setWidth(productImage, value = 600F) engine.block.setHeight(productImage, value = 600F) engine.block.setPositionX(productImage, value = 240F) engine.block.setPositionY(productImage, value = 600F) engine.block.appendChild(parent = page, child = productImage) // Price text with variable val price = engine.block.create(DesignBlockType.Text) engine.block.setString(price, property = "text/text", value = "${{price}}") engine.block.setTextFontSize(price, fontSize = 36F) engine.block.setTextColor( price, color = Color.fromRGBA(r = 0.2F, g = 0.6F, b = 0.2F, a = 1.0F) ) engine.block.setWidthMode(price, mode = SizeMode.AUTO) engine.block.setHeightMode(price, mode = SizeMode.AUTO) engine.block.setPositionX(price, value = 100F) engine.block.setPositionY(price, value = 1350F) engine.block.appendChild(parent = page, child = price) // Save as string val templateString = engine.scene.saveToString(scene = scene) println("Template saved as string (${templateString.length} characters)") // Save to file (for demonstration) val stringFile = File(context.filesDir, "template_string.scene") withContext(Dispatchers.IO) { stringFile.writeText(templateString) } // Save as archive (includes all assets) val archiveBlob = engine.scene.saveToArchive(scene = scene) val archiveFile = File(context.filesDir, "template_archive.cesdk") withContext(Dispatchers.IO) { archiveFile.outputStream().channel.use { channel -> channel.write(archiveBlob) } } println("Template saved as archive: ${archiveFile.absolutePath}") // Example: Populate the template with actual data engine.variable.set(key = "product_name", value = "Premium Coffee Mug") engine.variable.set(key = "description", value = "Hand-crafted ceramic, dishwasher safe") engine.variable.set(key = "price", value = "24.99") println("Template created and saved successfully!") } finally { // Note: Don't stop the engine here if you want to keep using it // engine.stop() } } ``` ## Add Template Metadata Like other assets, you can: - Load templates into the [asset library](https://img.ly/docs/cesdk/android/import-media/asset-library-65d6c4/). - Store metadata in your CMS or local database. - Use the saved metadata later when you register the template as an `AssetDefinition` in an `AssetSource`. That way the UI can display names, thumbnails, and categories. Example metadata structure: ```kotlin data class TemplateMetadata( val id: String, val name: String, val description: String, val thumbnailUrl: String, val category: String, val tags: List, val variables: List, val createdAt: Long, val updatedAt: Long ) ``` ## Lock Template Properties Templates can restrict editing at runtime so that users don't edit any part of the design that should remain static. To protect integrity, you can lock properties such as: - Position - Size - Color - Fill The guide for [locking templates](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) provides details on which properties are lockable and how to set up editor and adopter rules. Example of locking a block: ```kotlin import ly.img.engine.Engine // Lock the background so users can't move or resize it engine.block.setScopeEnabled(background, key = "layer/move", enabled = false) engine.block.setScopeEnabled(background, key = "layer/resize", enabled = false) engine.block.setScopeEnabled(background, key = "fill/change", enabled = false) ``` ## Load and Use Templates Once you've created and saved a template, load it back and populate with data: ### Load from String ```kotlin import android.net.Uri import ly.img.engine.Engine // Load template from string val scene = engine.scene.load(scene = templateString) // Populate with data engine.variable.set(key = "product_name", value = "Wireless Headphones") engine.variable.set(key = "description", value = "Premium sound quality") engine.variable.set(key = "price", value = "149.99") // Find and replace the product image val blocks = engine.block.findByType(DesignBlockType.Graphic) for (block in blocks) { val name = engine.block.getString(block, property = "name") if (name == "product-image") { val fill = engine.block.getFill(block) engine.block.setString( fill, property = "fill/image/imageFileURI", value = "https://example.com/headphones.jpg" ) } } ``` ### Load from Archive Archives need to be unzipped before loading. Here's how to load a template from an archive file: ```kotlin import android.content.Context import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import java.io.File import java.io.FileOutputStream import java.util.zip.ZipInputStream suspend fun loadTemplateFromArchive( engine: Engine, context: Context, archiveFile: File ): Int { // Unzip archive to cache directory val extractDir = File(context.cacheDir, "template_${System.currentTimeMillis()}") extractDir.mkdirs() withContext(Dispatchers.IO) { ZipInputStream(archiveFile.inputStream()).use { zipInputStream -> var entry = zipInputStream.nextEntry while (entry != null) { val file = File(extractDir, entry.name) if (entry.isDirectory) { file.mkdirs() } else { file.parentFile?.mkdirs() FileOutputStream(file).use { outputStream -> zipInputStream.copyTo(outputStream) } } zipInputStream.closeEntry() entry = zipInputStream.nextEntry } } } // Load the scene.scene file from the extracted archive val sceneFile = File(extractDir, "scene.scene") val sceneUri = Uri.fromFile(sceneFile) return engine.scene.load(sceneUri = sceneUri) } ``` ## Troubleshooting **❌ Variables not populating**: - Confirm the variable syntax uses double curly brackets: `{{variable_name}}` - Verify that `engine.variable.set()` is called with the correct key. - Check that the scene is loaded before setting variables. **❌ Named blocks not found**: - Use `engine.block.getString(block, property = "name")` to verify block names. - Make sure the name was set during template creation. - Search within the correct block type using `engine.block.findByType()`. **❌ Missing fonts/images at runtime**: - Use an archive save to embed assets into a template for portability. - Ensure that the asset URIs are reachable and stable. - For local files, use `file:///android_asset/` for assets in the app's assets folder. **❌ Template won't load**: - Verify the template string or archive file is not corrupted. - Check that all required assets are accessible at the specified URIs. - Ensure the CE.SDK version used to create the template matches the version loading it. ## Next Steps Now that you can create templates, some related topics you may find helpful are: - [Generate scenes](https://img.ly/docs/cesdk/android/use-templates/generate-334e15/) with templates as the source. - [Apply templates](https://img.ly/docs/cesdk/android/use-templates/apply-template-35c73e/) to existing scenes. - [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) — automate template population at scale. - [Multi-Image Generation](https://img.ly/docs/cesdk/android/automation/multi-image-generation-2a0de4/) — generate multiple variants from a single template. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Lock the Template" description: "Restrict editing access to specific elements or properties in a template to enforce design rules." platform: android url: "https://img.ly/docs/cesdk/android/create-templates/lock-131489/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/android/create-templates-3aef79/) > [Lock the Template](https://img.ly/docs/cesdk/android/create-templates/lock-131489/) --- ```kotlin file=@cesdk_android_examples/engine-guides-scopes/Scopes.kt reference-only import android.net.Uri 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.GlobalScope fun scopes( 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) engine.scene.createFromImage(Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg")) val block = engine.block.findByType(DesignBlockType.Graphic).first() val scopes = engine.editor.findAllScopes() // Let the global scope defer to the block-level. engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) // Manipulation of layout properties of any block will fail at this point. try { engine.block.setPositionX(block, value = 100F) // Not allowed } catch (exception: Exception) { exception.printStackTrace() } // This will return `GlobalScope.DEFER`. engine.editor.getGlobalScope(key = "layer/move") // Allow the user to control the layout properties of the image block. engine.block.setScopeEnabled(block, key = "layer/move", enabled = true) // Manipulation of layout properties of any block is now allowed. try { engine.block.setPositionX(block, value = 100F) // Allowed } catch (exception: Exception) { exception.printStackTrace() } // Verify that the "layer/move" scope is now enabled for the image block. engine.block.isScopeEnabled(block, key = "layer/move") // This will return true as well since the global scope is set to `GlobalScope.DEFER`. engine.block.isAllowedByScope(block, key = "layer/move") engine.stop() } ``` CE.SDK allows you to control which parts of a block can be manipulated. Scopes describe different aspects of a block, e.g. layout or style and can be enabled or disabled for every single block. There's also the option to control a scope globally. When configuring a scope globally you can set an override to always allow or deny a certain type of manipulation for every block. Or you can configure the global scope to defer to the individual block scopes. Initially, the block-level scopes are all disabled while at the global level all scopes are set to `"Allow"`. This overrides the block-level and allows for any kind of manipulation. If you want to implement a limited editing mode in your software you can set the desired scopes on the blocks you want the user to manipulate and then restrict the available actions by globally setting the scopes to `"Defer"`. In the same way you can prevent any manipulation of properties covered by a scope by setting the respective global scope to `"Deny"`. ## Available Scopes You can retrieve all available scopes by calling `engine.editor.findAllScopes()`. ```kotlin highlight-findAllScopes val scopes = engine.editor.findAllScopes() ``` We currently support the following scopes: | Scope | Explanation | | -------------------------- | -------------------------------------------------- | | `"layer/move"` | Whether the block's position can be changed | | `"layer/resize"` | Whether the block can be resized | | `"layer/rotate"` | Whether the block's rotation can be changed | | `"layer/flip"` | Whether the block can be flipped | | `"layer/crop"` | Whether the block's content can be cropped | | `"layer/clipping"` | Whether the block's clipping can be changed | | `"layer/opacity"` | Whether the block's opacity can be changed | | `"layer/blendMode"` | Whether the block's blend mode can be changed | | `"layer/visibility"` | Whether the block's visibility can be changed | | `"appearance/adjustments"` | Whether the block's adjustments can be changed | | `"appearance/filter"` | Whether the block's filter can be changed | | `"appearance/effect"` | Whether the block's effect can be changed | | `"appearance/blur"` | Whether the block's blur can be changed | | `"appearance/shadow"` | Whether the block's shadow can be changed | | `"lifecycle/destroy"` | Whether the block can be deleted | | `"lifecycle/duplicate"` | Whether the block can be duplicated | | `"editor/add"` | Whether new blocks can be added | | `"editor/select"` | Whether a block can be selected or not | | `"fill/change"` | Whether the block's fill can be changed | | `"fill/changeType"` | Whether the block's fill type can be changed | | `"stroke/change"` | Whether the block's stroke can be changed | | `"shape/change"` | Whether the block's shape can be changed | | `"text/edit"` | Whether the block's text can be changed | | `"text/character"` | Whether the block's text properties can be changed | ## Managing Scopes First, we globally defer the `"layer/move"` scope to the block-level using `engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)`. Since all blocks default to having their scopes set to `false` initially, modifying the layout properties of any block will fail at this point. | Value | Explanation | | -------- | ----------------------------------------------------------------- | | `.allow` | Manipulation of properties covered by the scope is always allowed | | `.deny` | Manipulation of properties covered by the scope is always denied | | `.defer` | Permission is deferred to the scope of the individual blocks | ```kotlin highlight-setGlobalScope // Let the global scope defer to the block-level. engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) // Manipulation of layout properties of any block will fail at this point. try { engine.block.setPositionX(block, value = 100F) // Not allowed } catch (exception: Exception) { exception.printStackTrace() } ``` We can verify the current state of the global `"layer/move"` scope using `engine.editor.getGlobalScope(key = "layer/move")`. ```kotlin highlight-getGlobalScope // This will return `GlobalScope.DEFER`. engine.editor.getGlobalScope(key = "layer/move") ``` Now we can allow the `"layer/move"` scope for a single block by setting it to `true` using `fun setScopeEnabled(block: DesignBlock, key: String, enabled: Boolean)`. ```kotlin highlight-setScopeEnabled // Allow the user to control the layout properties of the image block. engine.block.setScopeEnabled(block, key = "layer/move", enabled = true) // Manipulation of layout properties of any block is now allowed. try { engine.block.setPositionX(block, value = 100F) // Allowed } catch (exception: Exception) { exception.printStackTrace() } ``` Again we can verify this change by calling `fun isScopeEnabled(block: DesignBlock, key: String): Boolean`. ```kotlin highlight-isScopeEnabled // Verify that the "layer/move" scope is now enabled for the image block. engine.block.isScopeEnabled(block, key = "layer/move") ``` Finally, `fun isAllowedByScope(block: DesignBlock, key: String): Boolean` will allow us to verify a block's final scope state by taking both the global state as well as block-level state into account. ```kotlin highlight-isAllowedByScope // This will return true as well since the global scope is set to `GlobalScope.DEFER`. engine.block.isAllowedByScope(block, key = "layer/move") ``` ## Full Code Here's the full code: ```kotlin import android.net.Uri 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.GlobalScope fun scopes( 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) engine.scene.createFromImage(Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg")) val block = engine.block.findByType(DesignBlockType.Graphic).first() val scopes = engine.editor.findAllScopes() // Let the global scope defer to the block-level. engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) // Manipulation of layout properties of any block will fail at this point. try { engine.block.setPositionX(block, value = 100F) // Not allowed } catch (exception: Exception) { exception.printStackTrace() } // This will return `GlobalScope.DEFER`. engine.editor.getGlobalScope(key = "layer/move") // Allow the user to control the layout properties of the image block. engine.block.setScopeEnabled(block, key = "layer/move", enabled = true) // Manipulation of layout properties of any block is now allowed. try { engine.block.setPositionX(block, value = 100F) // Allowed } catch (exception: Exception) { exception.printStackTrace() } // Verify that the "layer/move" scope is now enabled for the image block. engine.block.isScopeEnabled(block, key = "layer/move") // This will return true as well since the global scope is set to `GlobalScope.DEFER`. engine.block.isAllowedByScope(block, key = "layer/move") 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: "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/) --- In CE.SDK, a *template* is a reusable, structured design that defines editable areas and constraints for end users. Templates can be based on static visuals or video compositions and are used to guide content creation, enable mass personalization, and enforce design consistency. Unlike a regular editable design, a template introduces structure through placeholders and constraints, allowing you to define which elements users can change and how. Templates support both static output formats (like PNG, PDF) and videos (like MP4), and can be created or applied using either the CE.SDK UI or API. Templates are a core part of enabling design automation, personalization, and streamlined workflows in any app that includes creative functionality. [Explore Demos](https://img.ly/showcases/cesdk?tags=android) [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) These imported designs can then be adapted into editable, structured templates inside 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: "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. - [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/) - Add entrance and exit transitions to Android video compositions with CE.SDK block animations. - [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Add synchronized captions to Android video scenes with CE.SDK. - [Update Caption Presets](https://img.ly/docs/cesdk/android/create-video/update-caption-presets-e9c385/) - Extend video captions with custom caption preset files and asset source manifests on Android. - [Add Watermark](https://img.ly/docs/cesdk/android/edit-video/add-watermark-762ce6/) - Add text and image watermarks to videos with timeline duration, positioning, opacity, and visibility controls in Android. - [Annotation](https://img.ly/docs/cesdk/android/edit-video/annotation-e9cbad/) - Add timed text, shapes, and highlights to video scenes on Android. - [Redact Sensitive Content in Videos](https://img.ly/docs/cesdk/android/edit-video/redaction-cf6d03/) - Redact sensitive video content on Android using blur, pixelization, solid overlays, and timeline controls. - [Record Reaction](https://img.ly/docs/cesdk/android/create-video/record-reaction-502c3b/) - Record reactions to a base video and compose them into an editable Android video scene. - [Lock Design](https://img.ly/docs/cesdk/android/create-video/lock-design-e92ce4/) - Protect video designs from unwanted modifications using CE.SDK's scope-based permission system. - [Programmatic Creation](https://img.ly/docs/cesdk/android/create-video/programmatic-2b243c/) - Create and export video scenes entirely through code with the CE.SDK Engine on Android. - [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) - Edit video scenes with CE.SDK Engine APIs on Android. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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: "Add entrance and exit transitions to Android video compositions with CE.SDK block animations." 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/) --- ```kotlin file=@cesdk_android_examples/engine-guides-apply-transitions/ApplyTransitions.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.yield 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 fun applyTransitions( 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 = 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) engine.block.setDuration(page, duration = 8.0) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = 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 source assignment uses CE.SDK's URI-valued fill property key. engine.block.setUri( block = firstVideoFill, 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 = firstClip, fill = firstVideoFill) 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/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) engine.block.setFill(block = secondClip, fill = secondVideoFill) engine.block.appendChild(parent = track, child = firstClip) engine.block.appendChild(parent = track, child = secondClip) engine.block.fillParent(track) if (!engine.block.supportsAnimation(firstClip) || !engine.block.supportsAnimation(secondClip)) { engine.stop() return@launch } engine.block.setDuration(block = firstClip, duration = 4.0) engine.block.setDuration(block = secondClip, duration = 4.0) val firstClipOut = engine.block.createAnimation(AnimationType.Fade) engine.block.setDuration(block = firstClipOut, duration = 0.8) engine.block.setOutAnimation(block = firstClip, animation = firstClipOut) val secondClipIn = engine.block.createAnimation(AnimationType.Slide) engine.block.setDuration(block = secondClipIn, duration = 0.8) engine.block.setInAnimation(block = secondClip, animation = secondClipIn) val slideProperties = engine.block.findAllProperties(secondClipIn) // Animation properties use engine property keys; inspect type-specific keys first. if ("animation/slide/direction" in slideProperties) { engine.block.setFloat( block = secondClipIn, property = "animation/slide/direction", value = Math.PI.toFloat(), ) } engine.block.setEnum( block = secondClipIn, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key, ) val currentOut = engine.block.getOutAnimation(firstClip) if (engine.block.isValid(currentOut)) { engine.block.destroy(currentOut) } val replacementOut = engine.block.createAnimation(AnimationType.Fade) engine.block.setDuration(block = replacementOut, duration = 0.6) engine.block.setOutAnimation(block = firstClip, animation = replacementOut) val attachedIn = engine.block.getInAnimation(secondClip) val attachedOut = engine.block.getOutAnimation(firstClip) // Let the scheduled engine update lay out track children before reading derived offsets. yield() check(engine.block.isValid(attachedIn)) check(engine.block.isValid(attachedOut)) check(engine.block.getType(attachedIn) == AnimationType.Slide.key) check(engine.block.getType(attachedOut) == AnimationType.Fade.key) check(engine.block.getDuration(attachedIn) == 0.8) check(engine.block.getDuration(attachedOut) == 0.6) check(engine.block.getEnum(attachedIn, "animationEasing") == AnimationEasingType.EASE_OUT.key) check(engine.block.getTimeOffset(secondClip) == 4.0) engine.stop() } ``` Add visual transitions to video clips by attaching CE.SDK In and Out animations to timeline blocks. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-apply-transitions) Transitions in Android video compositions are block animations. An In animation runs after a clip becomes visible, and an Out animation runs before that clip leaves the page timeline. Android uses these animation APIs for this flow rather than a separate clip-to-clip transition object. ## Create a Video Timeline Start with a video scene, a page, a track, and two graphic blocks that use video fills. The blocks act as clips on the track timeline. ```kotlin highlight-android-create-clips 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 track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = 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 source assignment uses CE.SDK's URI-valued fill property key. engine.block.setUri( block = firstVideoFill, 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 = firstClip, fill = firstVideoFill) 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/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) engine.block.setFill(block = secondClip, fill = secondVideoFill) engine.block.appendChild(parent = track, child = firstClip) engine.block.appendChild(parent = track, child = secondClip) engine.block.fillParent(track) ``` The sample adds both clips to the track in playback order and keeps them full-screen with `fillParent`. ## Check Animation Support Check each clip before creating transition animations. If a block does not support animations, skip transition setup instead of failing later. ```kotlin highlight-android-check-support if (!engine.block.supportsAnimation(firstClip) || !engine.block.supportsAnimation(secondClip)) { engine.stop() return@launch } ``` ## Coordinate Clip Timing Set the clip durations in seconds. The track uses child order and each clip's duration to place the second clip after the first. ```kotlin highlight-android-coordinate-timing engine.block.setDuration(block = firstClip, duration = 4.0) engine.block.setDuration(block = secondClip, duration = 4.0) ``` The Out animation on the first clip plays during the final part of its duration. The In animation on the second clip plays after that clip reaches its time offset. ## Apply an Exit Transition Create a fade animation, set its duration, and attach it as the first clip's Out animation. ```kotlin highlight-android-exit-transition val firstClipOut = engine.block.createAnimation(AnimationType.Fade) engine.block.setDuration(block = firstClipOut, duration = 0.8) engine.block.setOutAnimation(block = firstClip, animation = firstClipOut) ``` ## Apply an Entrance Transition Create a slide animation, set its duration, and attach it as the second clip's In animation. ```kotlin highlight-android-entrance-transition val secondClipIn = engine.block.createAnimation(AnimationType.Slide) engine.block.setDuration(block = secondClipIn, duration = 0.8) engine.block.setInAnimation(block = secondClip, animation = secondClipIn) ``` The animation duration controls how long the transition effect lasts. It does not change the clip's own timeline duration. ## Configure Properties Use `findAllProperties` before setting type-specific animation properties. Common easing values are exposed through `AnimationEasingType`. ```kotlin highlight-android-configure-properties val slideProperties = engine.block.findAllProperties(secondClipIn) // Animation properties use engine property keys; inspect type-specific keys first. if ("animation/slide/direction" in slideProperties) { engine.block.setFloat( block = secondClipIn, property = "animation/slide/direction", value = Math.PI.toFloat(), ) } engine.block.setEnum( block = secondClipIn, property = "animationEasing", value = AnimationEasingType.EASE_OUT.key, ) ``` The slide direction uses radians. Easing changes how quickly the transition accelerates and decelerates during its duration. ## Replace a Transition When replacing the first clip's Out transition, destroy the old animation block before assigning the replacement. Detached animation blocks are not destroyed automatically. ```kotlin highlight-android-replace-transition val currentOut = engine.block.getOutAnimation(firstClip) if (engine.block.isValid(currentOut)) { engine.block.destroy(currentOut) } val replacementOut = engine.block.createAnimation(AnimationType.Fade) engine.block.setDuration(block = replacementOut, duration = 0.6) engine.block.setOutAnimation(block = firstClip, animation = replacementOut) ``` ## Verify Attached Animations Read back the In and Out animation handles when your workflow needs to validate or update existing transitions. ```kotlin highlight-android-readback val attachedIn = engine.block.getInAnimation(secondClip) val attachedOut = engine.block.getOutAnimation(firstClip) // Let the scheduled engine update lay out track children before reading derived offsets. yield() check(engine.block.isValid(attachedIn)) check(engine.block.isValid(attachedOut)) check(engine.block.getType(attachedIn) == AnimationType.Slide.key) check(engine.block.getType(attachedOut) == AnimationType.Fade.key) check(engine.block.getDuration(attachedIn) == 0.8) check(engine.block.getDuration(attachedOut) == 0.6) check(engine.block.getEnum(attachedIn, "animationEasing") == AnimationEasingType.EASE_OUT.key) check(engine.block.getTimeOffset(secondClip) == 4.0) ``` Invalid handles mean that no animation is attached for that slot. ## Troubleshooting - **Animation does not play**: Verify `supportsAnimation`, clip visibility timing, and animation duration. - **Transition appears at the wrong time**: Check the clip's `timeOffset`, the clip's `duration`, and the transition animation's own duration. - **Property update fails**: Inspect `findAllProperties(animation)` before setting type-specific keys like `animation/slide/direction`. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.appendChild(parent=_, child=_)` | Add the page, track, and clip blocks in timeline order. | | `engine.block.fillParent(block=_)` | Size the track clips to fill the page. | | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Assign a video URI to a video fill. | | `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 transition. | | `engine.block.setOutAnimation(block=_, animation=_)` | Attach an exit transition. | | `engine.block.getInAnimation(block=_)` | Read the current entrance animation handle. | | `engine.block.getOutAnimation(block=_)` | Read the current exit animation handle. | | `engine.block.setDuration(block=_, duration=_)` | Set clip or animation duration in seconds. | | `engine.block.getTimeOffset(block=_)` | Read the offset assigned by the track. | | `engine.block.setEnum(block=_, property="animationEasing", value=_)` | Configure an animation easing curve. | | `engine.block.setFloat(block=_, property="animation/slide/direction", value=_)` | Configure slide direction in radians. | | `engine.block.findAllProperties(block=_)` | Discover properties supported by a specific animation block. | | `engine.block.destroy(block=_)` | Destroy a replaced animation block. | ## Next Steps - [Base Animations](https://img.ly/docs/cesdk/android/animation/create/base-0fc5c4/) — Learn how to create, attach, configure, and replace animation blocks. - [Supported Animation Types](https://img.ly/docs/cesdk/android/animation/types-4e5f41/) — Explore available In, Out, and Loop animation presets and their properties. - [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) — Review video scenes, fills, tracks, durations, and exports. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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-video/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-video/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. | --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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 - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/android/create-video/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: "Lock Design" description: "Protect video designs from unwanted modifications using CE.SDK's scope-based permission system." platform: android url: "https://img.ly/docs/cesdk/android/create-video/lock-design-e92ce4/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Lock Design](https://img.ly/docs/cesdk/android/create-video/lock-design-e92ce4/) --- ```kotlin file=@cesdk_android_examples/engine-guides-lock-video-design/LockVideoDesign.kt reference-only import android.app.Application import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.GlobalScope import ly.img.engine.ShapeType import ly.img.engine.SizeMode suspend fun lockVideoDesign( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = withContext(Dispatchers.Main) { Engine.init(application) val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1280, height = 720) try { val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 12.0) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) val videoClip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(videoClip, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setDuration(videoClip, duration = 12.0) val videoFill = engine.block.createFill(FillType.Video) // Video source URIs are currently set through the fill property key. engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = Uri.parse( "https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4", ), ) engine.block.setFill(videoClip, fill = videoFill) engine.block.appendChild(parent = track, child = videoClip) engine.block.fillParent(track) val titleOverlay = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = titleOverlay) engine.block.setWidthMode(titleOverlay, mode = SizeMode.AUTO) engine.block.setHeightMode(titleOverlay, mode = SizeMode.AUTO) engine.block.setPositionX(titleOverlay, value = 80F) engine.block.setPositionY(titleOverlay, value = 80F) engine.block.setDuration(titleOverlay, duration = 12.0) engine.block.replaceText(titleOverlay, text = "Editable title") val watermarkOverlay = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = page, child = watermarkOverlay) engine.block.setWidthMode(watermarkOverlay, mode = SizeMode.AUTO) engine.block.setHeightMode(watermarkOverlay, mode = SizeMode.AUTO) engine.block.setPositionX(watermarkOverlay, value = 980F) engine.block.setPositionY(watermarkOverlay, value = 640F) engine.block.setDuration(watermarkOverlay, duration = 12.0) engine.block.replaceText(watermarkOverlay, text = "LOCKED") val scopes = engine.editor.findAllScopes() scopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY) } engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(videoClip, key = "editor/select", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "editor/select", enabled = true) engine.block.setScopeEnabled(watermarkOverlay, key = "editor/select", enabled = false) engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(titleOverlay, key = "text/edit", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "text/character", enabled = true) engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(videoClip, key = "fill/change", enabled = true) engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "layer/rotate", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(titleOverlay, key = "layer/move", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "layer/resize", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "layer/rotate", enabled = true) val lockedOverlayScopes = listOf( "editor/select", "text/edit", "text/character", "fill/change", "layer/move", "layer/resize", "layer/rotate", "lifecycle/destroy", ) lockedOverlayScopes.forEach { scope -> engine.block.setScopeEnabled(watermarkOverlay, key = scope, enabled = false) } val canSelectVideoClip = engine.block.isAllowedByScope(videoClip, key = "editor/select") val canReplaceVideoClip = engine.block.isAllowedByScope(videoClip, key = "fill/change") val canMoveVideoClip = engine.block.isAllowedByScope(videoClip, key = "layer/move") val canEditTitle = engine.block.isAllowedByScope(titleOverlay, key = "text/edit") val canMoveTitle = engine.block.isAllowedByScope(titleOverlay, key = "layer/move") val canSelectWatermark = engine.block.isAllowedByScope(watermarkOverlay, key = "editor/select") val titleTextScopeEnabled = engine.block.isScopeEnabled(titleOverlay, key = "text/edit") val textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit") require(canSelectVideoClip) require(canReplaceVideoClip) require(!canMoveVideoClip) require(canEditTitle) require(canMoveTitle) require(!canSelectWatermark) require(titleTextScopeEnabled) require(textEditGlobalScope == GlobalScope.DEFER) val availableScopes = engine.editor.findAllScopes() val currentScopeSettings = availableScopes.associateWith { scope -> engine.editor.getGlobalScope(key = scope) } require("editor/select" in availableScopes) require("fill/change" in availableScopes) require(currentScopeSettings["editor/select"] == GlobalScope.DEFER) } finally { engine.stop() } } ``` Protect video clips, overlays, and placeholders from unwanted edits using CE.SDK's scope-based permission system. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-lock-video-design) CE.SDK uses global scopes and block-level scopes to decide which editing operations are allowed. For video designs, the same model can lock the whole scene, keep watermarks protected, and make only selected clips or overlays editable. The backing sample creates a small headless video scene with one clip, one editable title overlay, and one locked watermark. Those blocks provide context for the scope calls below. ## Understanding Scope Permissions Scopes control what operations users can perform on video clips, text overlays, watermarks, and other design blocks. CE.SDK combines global scope settings with block-level settings to determine the effective permission. | Global Scope | Block Scope | Result | | ------------------- | ----------- | --------- | | `GlobalScope.ALLOW` | any | Permitted | | `GlobalScope.DENY` | any | Blocked | | `GlobalScope.DEFER` | enabled | Permitted | | `GlobalScope.DEFER` | disabled | Blocked | Global scopes have three possible values: - **`GlobalScope.ALLOW`**: The operation is always permitted, regardless of block-level settings - **`GlobalScope.DENY`**: The operation is always blocked, regardless of block-level settings - **`GlobalScope.DEFER`**: The permission depends on the block-level scope setting Block-level scopes are binary. They only take effect when the matching global scope is set to `GlobalScope.DEFER`. ## Lock the Entire Video Design To lock all editing operations, discover the current scope keys with `engine.editor.findAllScopes()` and set each global scope to `GlobalScope.DENY`. ```kotlin highlight-android-lock-video-design val scopes = engine.editor.findAllScopes() scopes.forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DENY) } ``` When all scopes are denied, users cannot select, move, edit text, replace fills, or delete blocks. This also prevents changes to video clips and overlays until you explicitly defer a scope. ## Enable Selection for Editable Video Blocks Before users can interact with any block, enable `editor/select`. Setting the global scope to `GlobalScope.DEFER` delegates the decision to each block, so only selected clips or overlays become interactive. ```kotlin highlight-android-enable-selection engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(videoClip, key = "editor/select", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "editor/select", enabled = true) engine.block.setScopeEnabled(watermarkOverlay, key = "editor/select", enabled = false) ``` ## Selective Video Locking Patterns Lock everything first, then selectively enable the capabilities that each video block needs. This keeps the default state restrictive while allowing controlled editing. ### Text Overlay Editing Enable `text/edit` for text changes and `text/character` when users should also adjust text styling. The sample applies both scopes only to the title overlay. ```kotlin highlight-android-text-overlay-editing engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(titleOverlay, key = "text/edit", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "text/character", enabled = true) ``` The title text can now be edited while unrelated layout, fill, and lifecycle operations stay locked unless another section enables them. ### Video Clip Replacement Enable `fill/change` on a video placeholder when users may replace the media source but should not change layout. The sample keeps movement, resize, and rotation denied on the video clip. ```kotlin highlight-android-video-replacement engine.editor.setGlobalScope(key = "fill/change", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(videoClip, key = "fill/change", enabled = true) ``` ### Overlay Layout Adjustments Enable layout scopes only for blocks that users may reposition. The sample allows moving, resizing, and rotating the title overlay while keeping the video clip layout fixed. ```kotlin highlight-android-layout-adjustments engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "layer/resize", globalScope = GlobalScope.DEFER) engine.editor.setGlobalScope(key = "layer/rotate", globalScope = GlobalScope.DEFER) engine.block.setScopeEnabled(titleOverlay, key = "layer/move", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "layer/resize", enabled = true) engine.block.setScopeEnabled(titleOverlay, key = "layer/rotate", enabled = true) ``` ### Protected Overlays Keep scopes disabled for watermarks, legal text, brand marks, or other protected overlays. Explicitly disabling the relevant block-level scopes makes the intent clear when the matching global scopes are deferred elsewhere. ```kotlin highlight-android-protect-overlay val lockedOverlayScopes = listOf( "editor/select", "text/edit", "text/character", "fill/change", "layer/move", "layer/resize", "layer/rotate", "lifecycle/destroy", ) lockedOverlayScopes.forEach { scope -> engine.block.setScopeEnabled(watermarkOverlay, key = scope, enabled = false) } ``` ## Check Effective Permissions Use `engine.block.isAllowedByScope()` to verify what the current scope configuration actually permits. This method evaluates both global and block-level settings. ```kotlin highlight-android-check-permissions val canSelectVideoClip = engine.block.isAllowedByScope(videoClip, key = "editor/select") val canReplaceVideoClip = engine.block.isAllowedByScope(videoClip, key = "fill/change") val canMoveVideoClip = engine.block.isAllowedByScope(videoClip, key = "layer/move") val canEditTitle = engine.block.isAllowedByScope(titleOverlay, key = "text/edit") val canMoveTitle = engine.block.isAllowedByScope(titleOverlay, key = "layer/move") val canSelectWatermark = engine.block.isAllowedByScope(watermarkOverlay, key = "editor/select") val titleTextScopeEnabled = engine.block.isScopeEnabled(titleOverlay, key = "text/edit") val textEditGlobalScope = engine.editor.getGlobalScope(key = "text/edit") require(canSelectVideoClip) require(canReplaceVideoClip) require(!canMoveVideoClip) require(canEditTitle) require(canMoveTitle) require(!canSelectWatermark) require(titleTextScopeEnabled) require(textEditGlobalScope == GlobalScope.DEFER) ``` The distinction between checking methods is: - `isAllowedByScope()` returns the **effective permission** after evaluating both levels - `isScopeEnabled()` returns only the **block-level setting** - `getGlobalScope()` returns only the **global setting** ## Discover Available Scopes Use `engine.editor.findAllScopes()` instead of hardcoding a complete scope list. This keeps locking code aligned with the scopes available in the current engine. ```kotlin highlight-android-discover-scopes val availableScopes = engine.editor.findAllScopes() val currentScopeSettings = availableScopes.associateWith { scope -> engine.editor.getGlobalScope(key = scope) } require("editor/select" in availableScopes) require("fill/change" in availableScopes) require(currentScopeSettings["editor/select"] == GlobalScope.DEFER) ``` ## Available Scopes Reference | Scope | Description | | ------------------------ | ---------------------------------------- | | `layer/move` | Move block position | | `layer/resize` | Resize block dimensions | | `layer/rotate` | Rotate block | | `layer/flip` | Flip block horizontally or vertically | | `layer/crop` | Crop block content | | `layer/opacity` | Change block opacity | | `layer/blendMode` | Change blend mode | | `layer/visibility` | Toggle block visibility | | `layer/clipping` | Change clipping behavior | | `fill/change` | Change fill content or text color | | `fill/changeType` | Change fill type | | `stroke/change` | Change stroke properties | | `shape/change` | Change shape type | | `text/edit` | Edit text content | | `text/character` | Change text styling such as font or size | | `appearance/adjustments` | Change color adjustments | | `appearance/filter` | Apply or change filters | | `appearance/effect` | Apply or change effects | | `appearance/blur` | Apply or change blur | | `appearance/shadow` | Apply or change shadows | | `appearance/animation` | Apply or change animations | | `lifecycle/destroy` | Delete the block | | `lifecycle/duplicate` | Duplicate the block | | `editor/add` | Add new blocks | | `editor/select` | Select blocks | ## Troubleshooting | Issue | Cause | Solution | | ---------------------------------- | --------------------------------------- | ----------------------------------------------------------- | | Block is still editable | The global scope is `GlobalScope.ALLOW` | Set the global scope to `GlobalScope.DENY` or `GlobalScope.DEFER` | | Block is unexpectedly locked | The global scope is `GlobalScope.DENY` | Set the global scope to `GlobalScope.DEFER` and enable the block-level scope | | Users cannot select a block | `editor/select` is still locked | Enable `editor/select` for blocks users should select | | Permission check returns `false` | The code checks the wrong scope level | Use `isAllowedByScope()` for the effective permission | | New scopes are not locked | The code uses a hardcoded scope list | Use `findAllScopes()` to discover scopes dynamically | ## API Reference | Method | Purpose | | ------ | ------- | | `engine.editor.findAllScopes()` | Get all available scope names | | `engine.editor.setGlobalScope(key=_, globalScope=_)` | Set a global scope to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER` | | `engine.editor.getGlobalScope(key=_)` | Get the current global setting for one scope | | `engine.block.setScopeEnabled(block=_, key=_, enabled=_)` | Enable or disable a scope on one block | | `engine.block.isScopeEnabled(block=_, key=_)` | Check only the block-level scope setting | | `engine.block.isAllowedByScope(block=_, key=_)` | Check the effective permission after global and block-level scopes are evaluated | ## Next Steps - [Lock 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. - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Play, pause, seek, and preview audio or video content. - [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Add synchronized captions to Android video scenes. - [Add Watermark](https://img.ly/docs/cesdk/android/edit-video/add-watermark-762ce6/) - Add text or image watermarks to exported videos. - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Render output for sharing or publishing. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Programmatic Creation" description: "Create and export video scenes entirely through code with the CE.SDK Engine on Android." platform: android url: "https://img.ly/docs/cesdk/android/create-video/programmatic-2b243c/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Programmatic Creation](https://img.ly/docs/cesdk/android/create-video/programmatic-2b243c/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-video-programmatic/CreateVideoProgrammatic.kt reference-only import android.net.Uri import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportVideoOptions import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import java.io.File suspend fun createVideoProgrammatic(engine: Engine): File = withContext(Dispatchers.Main) { val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.appendChild(parent = scene, child = page) val introClip = createVideoClip( engine, Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"), ) val detailClip = createVideoClip( engine, Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.appendChild(parent = track, child = introClip.block) engine.block.appendChild(parent = track, child = detailClip.block) engine.block.fillParent(track) // Keep the guide export short; use the clip length your app needs. val sampleClipDurationSeconds = 2.0 engine.block.forceLoadAVResource(introClip.fill) val introDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(introClip.fill)) check(introDuration > 0.0) { "The intro video must contain playable media." } engine.block.setDuration(introClip.block, duration = introDuration) engine.block.forceLoadAVResource(detailClip.fill) val detailDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(detailClip.fill)) check(detailDuration > 0.0) { "The detail video must contain playable media." } engine.block.setDuration(detailClip.block, duration = detailDuration) val pageDuration = introDuration + detailDuration engine.block.setDuration(page, duration = pageDuration) val logTag = "CreateVideoGuide" // Export a compact preview file; use your delivery size and frame rate in production. val previewExportWidth = 640F val previewExportHeight = 360F val previewFrameRate = 15F val videoBytes = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { progress -> Log.i( logTag, "Rendered ${progress.renderedFrames} frames and encoded ${progress.encodedFrames} " + "frames out of ${progress.totalFrames} frames", ) }, options = ExportVideoOptions( targetWidth = previewExportWidth, targetHeight = previewExportHeight, frameRate = previewFrameRate, ), ) val outputFile = withContext(Dispatchers.IO) { val outputFile = File.createTempFile("programmatic-video-", ".mp4") val bytes = ByteArray(videoBytes.remaining()) videoBytes.get(bytes) outputFile.outputStream().use { output -> output.write(bytes) } outputFile } check(outputFile.length() > 0L) { "The exported MP4 file must not be empty." } outputFile } private data class VideoClip( val block: DesignBlock, val fill: DesignBlock, ) private fun createVideoClip( engine: Engine, videoUri: Uri, ): VideoClip { val clip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, // Video fills read their media source from this Engine property key. property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(clip, fill = videoFill) return VideoClip(block = clip, fill = videoFill) } suspend fun createSingleSourceVideoScene(engine: Engine): DesignBlock { val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4") val scene = engine.scene.createFromVideo(videoUri) return scene } ``` Create a video scene entirely through code, add clips to a track, set durations, and export the result as an MP4 file. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-video-programmatic) CE.SDK video scenes can be built without opening the editor UI. This is useful for automation, template-driven rendering, and app flows that create media from known inputs. This guide uses the Android Engine API to create a video scene, arrange clips on a track, load media metadata, set durations, and export the page as MP4. Use [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) when you need trim, split, timed overlay, or other edit recipes after the initial scene is created. For Engine initialization and offscreen rendering setup, see [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/). ## Create a Video Scene Create a timeline-enabled scene with `engine.scene.createForVideo()`. A page holds the video composition and defines the canvas dimensions. ```kotlin highlight-android-create-scene val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.appendChild(parent = scene, child = page) ``` For a one-source video scene, use `engine.scene.createFromVideo(videoUri)` as a shortcut. The main sample uses `createForVideo()` because it shows tracks, multiple clips, and export timing. ```kotlin highlight-android-create-from-video suspend fun createSingleSourceVideoScene(engine: Engine): DesignBlock { val videoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4") val scene = engine.scene.createFromVideo(videoUri) return scene } ``` ## Add Video Clips Each clip is a graphic block with a rectangular shape and a video fill. The helper returns both handles because later timing APIs operate on the graphic block, while media metadata APIs operate on the video fill. ```kotlin highlight-android-create-video-clip-helper private data class VideoClip( val block: DesignBlock, val fill: DesignBlock, ) private fun createVideoClip( engine: Engine, videoUri: Uri, ): VideoClip { val clip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, // Video fills read their media source from this Engine property key. property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(clip, fill = videoFill) return VideoClip(block = clip, fill = videoFill) } ``` Create the clips from source URLs: ```kotlin highlight-android-add-video-clips val introClip = createVideoClip( engine, Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"), ) val detailClip = createVideoClip( engine, Uri.parse("https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) ``` ## Arrange Clips on a Track Append the clips to a `DesignBlockType.Track` in playback order. The track sequences its children from their durations, and `fillParent(track)` sizes the track block to its parent page. For groups and tracks, `fillParent(...)` also fills their child blocks against the nearest parent that is not a group or track, so the unsized clip graphics in this sample fill the page frame before the track is sized. ```kotlin highlight-android-arrange-track val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.appendChild(parent = track, child = introClip.block) engine.block.appendChild(parent = track, child = detailClip.block) engine.block.fillParent(track) ``` ## Load Media and Set Durations Load each video resource before reading metadata. Duration values use seconds. ```kotlin highlight-android-load-media-and-timing // Keep the guide export short; use the clip length your app needs. val sampleClipDurationSeconds = 2.0 engine.block.forceLoadAVResource(introClip.fill) val introDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(introClip.fill)) check(introDuration > 0.0) { "The intro video must contain playable media." } engine.block.setDuration(introClip.block, duration = introDuration) engine.block.forceLoadAVResource(detailClip.fill) val detailDuration = sampleClipDurationSeconds.coerceAtMost(engine.block.getAVResourceTotalDuration(detailClip.fill)) check(detailDuration > 0.0) { "The detail video must contain playable media." } engine.block.setDuration(detailClip.block, duration = detailDuration) val pageDuration = introDuration + detailDuration engine.block.setDuration(page, duration = pageDuration) ``` The sample derives safe clip durations from the source media duration and sets the page duration to the combined clip length. ## Export the Video Export the page with `engine.block.exportVideo(...)`. The sample exports MP4, reports encoding progress, and uses export options to produce a smaller verification file while keeping the page at 1280x720. ```kotlin highlight-android-export-video val logTag = "CreateVideoGuide" // Export a compact preview file; use your delivery size and frame rate in production. val previewExportWidth = 640F val previewExportHeight = 360F val previewFrameRate = 15F val videoBytes = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { progress -> Log.i( logTag, "Rendered ${progress.renderedFrames} frames and encoded ${progress.encodedFrames} " + "frames out of ${progress.totalFrames} frames", ) }, options = ExportVideoOptions( targetWidth = previewExportWidth, targetHeight = previewExportHeight, frameRate = previewFrameRate, ), ) ``` Write the returned `ByteBuffer` to an MP4 file and check that the export is non-empty before returning it. ```kotlin highlight-android-write-file val outputFile = withContext(Dispatchers.IO) { val outputFile = File.createTempFile("programmatic-video-", ".mp4") val bytes = ByteArray(videoBytes.remaining()) videoBytes.get(bytes) outputFile.outputStream().use { output -> output.write(bytes) } outputFile } check(outputFile.length() > 0L) { "The exported MP4 file must not be empty." } ``` ## API Reference | API | Category | Purpose | | --- | --- | --- | | `engine.scene.createForVideo()` | Scene | Create an empty video scene with timeline support. | | `engine.scene.createFromVideo(videoUri=_)` | Scene | Create a one-source video scene from a `Uri`. | | `engine.block.create(blockType=_)` | Block | Create pages, tracks, graphics, and other blocks. | | `engine.block.createShape(type=_)` | Shape | Create a shape for a graphic block. | | `engine.block.setShape(block=_, shape=_)` | Shape | Assign a shape to a graphic block. | | `engine.block.createFill(fillType=_)` | Fill | Create a video fill. | | `engine.block.setFill(block=_, fill=_)` | Fill | Assign a fill to a block. | | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Fill | Set the source URI on a video fill. | | `engine.block.appendChild(parent=_, child=_)` | Hierarchy | Attach scene, page, track, and clip blocks. | | `engine.block.fillParent(block=_)` | Layout | Resize and position the passed block to fill its parent; for groups and tracks, fill child blocks against the nearest non-group/non-track parent first. | | `engine.block.setWidth(block=_, value=_)` | Layout | Set a block width. | | `engine.block.setHeight(block=_, value=_)` | Layout | Set a block height. | | `engine.block.setDuration(block=_, duration=_)` | Timing | Set clip or page duration in seconds. | | `engine.block.getDuration(block=_)` | Timing | Read the page duration passed to the video export. | | `engine.block.forceLoadAVResource(block=_)` | Media | Load a video fill before reading metadata. | | `engine.block.getAVResourceTotalDuration(block=_)` | Media | Read the source media duration in seconds. | | `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_, options=_, onPreExport=_, uriResolver=_)` | Export | Export a page timeline as video bytes. | ## Troubleshooting - **Engine reference is unavailable**: Initialize CE.SDK and obtain an `Engine` instance before running the scene creation code; the Engine reference note above links to the setup flow. - **Clip not visible**: Verify that the graphic block has a shape and fill, and that it is appended to a track or page. - **Source duration is zero**: Call `forceLoadAVResource(...)` on the video fill before metadata APIs. - **Export is empty**: Set a positive page duration and pass the same duration to `exportVideo(...)`. - **Remote media does not load**: Check that the URL is reachable and that the device runtime supports the media format. ## Next Steps - [Create Videos Overview](https://img.ly/docs/cesdk/android/create-video/overview-b06512/) - Understand video scenes and time-based editing - [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) - Modify timelines with trim, split, and timed overlay recipes - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Build interactive video timelines - [Join and Arrange Video Clips](https://img.ly/docs/cesdk/android/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK. - [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.CaptureVideo 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) is CameraResult.Record -> Unit } } @Composable fun rememberRecordReactionLauncher( baseVideoUri: Uri, license: String?, userId: String?, onReactionReady: (CameraResult.Reaction) -> Unit, onDismissed: () -> Unit = {}, ): () -> Unit { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> handleReactionCameraResult(result, onReactionReady, onDismissed) } return { val input = CaptureVideo.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) // Video fills currently use a generic property key for their URI. 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 = CaptureVideo()) { result -> handleReactionCameraResult(result, onReactionReady, onDismissed) } return { val input = CaptureVideo.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) is CameraResult.Record -> 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) // Video fills currently use a generic property key for their URI. 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. - [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 - [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 ly.img.engine.populateAssetSource 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.populateAssetSource( id = CaptionPresetSourceId, jsonUri = contentJsonUri, replaceBaseUri = assetsBaseUri, ) 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.populateAssetSource()`. Pass the folder that contains `ly.img.caption.presets/` as `replaceBaseUri`; the loader replaces `{{base_url}}` in the manifest with that value. ```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.populateAssetSource( id = CaptionPresetSourceId, jsonUri = contentJsonUri, replaceBaseUri = assetsBaseUri, ) 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.populateAssetSource(id=_, jsonUri=_, replaceBaseUri=_)` | 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 - [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 - [Integrating Background Removal in Android (Kotlin)](https://img.ly/docs/cesdk/android/edit-image/remove-bg-9dfcf7/) - Learn how to implement custom background removal using Google ML Kit's Selfie Segmentation in Android with CE.SDK. - [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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Integrating Background Removal in Android (Kotlin)" description: "Learn how to implement custom background removal using Google ML Kit's Selfie Segmentation in Android with CE.SDK." 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/) --- The CE.SDK provides a flexible architecture that allows you to extend its capability to meet your specific needs. This guide demonstrates how to integrate a custom background removal feature using Google ML Kit. You can apply the same approach to implement any custom image processing functionality in your Android app. > **Note:** **ML Kit Selfie Segmentation** works best with images containing people. For more general subject segmentation, you may need to use third-party libraries or cloud-based APIs. ## What You'll Learn - How to extract the current image from the CE.SDK engine. - How to implement background removal using ML Kit Selfie Segmentation. - How to process the image and apply the segmentation mask. - How to update the image in the scene with the processed result. - How to keep operations async and handle errors gracefully. ## When To Use It - Want programmatic "Remove BG" functionality in your app. - Prefer on-device processing (no uploads) for latency, privacy, or offline use. - Need to integrate custom image processing logic (ML Kit, third-party libraries, or your own API). - Building custom UI or automation workflows. ## Setup ML Kit First, add the ML Kit Selfie Segmentation dependency to your `build.gradle`: ```kotlin dependencies { implementation("com.google.mlkit:segmentation-selfie:16.0.0-beta5") } ``` ## Extracting the Image A block that displays an image has an `imageFill` which contains the URI of the underlying image. The first step is to extract the image data from the fill. ```kotlin import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory 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.net.URL suspend fun extractImageFromScene( engine: Engine, context: Context ): Bitmap? { // Get the current page from the scene val page = engine.scene.getCurrentPage() ?: return null // Validate that the page contains an image val imageFill = engine.block.getFill(page) val fillType = engine.block.getType(imageFill) if (fillType != "//ly.img.ubq/fill/image") { return null } // Extract the image URI val imageFileURI = engine.block.getString(imageFill, property = "fill/image/imageFileURI") // Download and decode the image return withContext(Dispatchers.IO) { try { val imageData = if (imageFileURI.startsWith("http")) { // Download from URL val url = URL(imageFileURI) val outputStream = ByteArrayOutputStream() url.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } else { // Load from local file or content URI val uri = Uri.parse(imageFileURI) val inputStream = context.contentResolver.openInputStream(uri) inputStream?.readBytes() ?: byteArrayOf() } BitmapFactory.decodeByteArray(imageData, 0, imageData.size) } catch (e: Exception) { null } } } ``` ## Processing the Image with ML Kit With a `Bitmap`, you can now process the image using ML Kit Selfie Segmentation. Here's a complete implementation: ```kotlin import android.graphics.Bitmap import android.graphics.Color import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.segmentation.Segmentation import com.google.mlkit.vision.segmentation.SegmentationMask import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions import kotlinx.coroutines.tasks.await /** * Removes the background from an image using ML Kit Selfie Segmentation. * @param original The source bitmap to process. * @return A new bitmap with the background removed (transparent), or null if processing fails. */ suspend fun removeBackgroundWithMLKit(original: Bitmap): Bitmap? { try { // Configure ML Kit Selfie Segmentation val options = SelfieSegmenterOptions.Builder() .setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE) .enableRawSizeMask() // Get mask at original image resolution .build() val segmenter = Segmentation.getClient(options) // Create InputImage from bitmap val inputImage = InputImage.fromBitmap(original, 0) // Process the image and get the segmentation mask val segmentationMask: SegmentationMask = segmenter.process(inputImage).await() // Create output bitmap with transparency val width = original.width val height = original.height val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) // Get mask data val mask = segmentationMask.buffer val maskWidth = segmentationMask.width val maskHeight = segmentationMask.height // Apply the mask to create transparent background for (y in 0 until height) { for (x in 0 until width) { // Map image coordinates to mask coordinates val maskX = (x * maskWidth / width).coerceIn(0, maskWidth - 1) val maskY = (y * maskHeight / height).coerceIn(0, maskHeight - 1) // Get mask confidence (0.0 = background, 1.0 = foreground) val maskIndex = maskY * maskWidth + maskX val confidence = mask.getFloat(maskIndex * 4) // 4 bytes per float // Get original pixel color val originalPixel = original.getPixel(x, y) if (confidence > 0.5f) { // Foreground - keep original pixel result.setPixel(x, y, originalPixel) } else { // Background - make transparent result.setPixel(x, y, Color.TRANSPARENT) } } } // Clean up segmenter.close() return result } catch (e: Exception) { e.printStackTrace() return null } } ``` ## Replace the Image in the Scene With a processed image, the last step is to update the fill with the new image: 1. Save the processed bitmap to a file. 2. Get the file URI. 3. Update the image fill's source set with the new URI. ```kotlin import android.content.Context import android.graphics.Bitmap import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import ly.img.engine.Source import java.io.File import java.io.FileOutputStream suspend fun replaceImageInScene( engine: Engine, context: Context, processedBitmap: Bitmap ) { // Save the bitmap to a file val imageFile = withContext(Dispatchers.IO) { val file = File(context.cacheDir, "bg_removed_${System.currentTimeMillis()}.png") FileOutputStream(file).use { outputStream -> processedBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) } file } val processedImageUri = Uri.fromFile(imageFile) // Get the current page and its fill val page = engine.scene.getCurrentPage() ?: return val imageFill = engine.block.getFill(page) // Update the source set with the new image engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = processedImageUri, width = processedBitmap.width.toUInt(), height = processedBitmap.height.toUInt() ) ) ) } ``` ## Complete Function Here's the complete function that combines all the steps: ```kotlin import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Color import android.net.Uri import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.segmentation.Segmentation import com.google.mlkit.vision.segmentation.SegmentationMask import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.Source import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.net.URL suspend fun performBackgroundRemoval( engine: Engine, context: Context ): Boolean { return withContext(Dispatchers.Main) { try { // Step 1: Get the current page val page = engine.scene.getCurrentPage() ?: return@withContext false // Step 2: Validate it's an image fill val imageFill = engine.block.getFill(page) val fillType = engine.block.getType(imageFill) if (fillType != "//ly.img.ubq/fill/image") { return@withContext false } // Step 3: Extract image data val imageFileURI = engine.block.getString(imageFill, property = "fill/image/imageFileURI") val originalBitmap = withContext(Dispatchers.IO) { val imageData = if (imageFileURI.startsWith("http")) { val url = URL(imageFileURI) val outputStream = ByteArrayOutputStream() url.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } else { val uri = Uri.parse(imageFileURI) context.contentResolver.openInputStream(uri)?.readBytes() ?: byteArrayOf() } BitmapFactory.decodeByteArray(imageData, 0, imageData.size) } ?: return@withContext false // Step 4: Process with ML Kit val options = SelfieSegmenterOptions.Builder() .setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE) .enableRawSizeMask() .build() val segmenter = Segmentation.getClient(options) val inputImage = InputImage.fromBitmap(originalBitmap, 0) val segmentationMask: SegmentationMask = segmenter.process(inputImage).await() // Step 5: Apply mask to create transparent background val width = originalBitmap.width val height = originalBitmap.height val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val mask = segmentationMask.buffer val maskWidth = segmentationMask.width val maskHeight = segmentationMask.height for (y in 0 until height) { for (x in 0 until width) { val maskX = (x * maskWidth / width).coerceIn(0, maskWidth - 1) val maskY = (y * maskHeight / height).coerceIn(0, maskHeight - 1) val maskIndex = maskY * maskWidth + maskX val confidence = mask.getFloat(maskIndex * 4) val originalPixel = originalBitmap.getPixel(x, y) result.setPixel( x, y, if (confidence > 0.5f) originalPixel else Color.TRANSPARENT ) } } segmenter.close() // Step 6: Save processed image val imageFile = withContext(Dispatchers.IO) { val file = File(context.cacheDir, "bg_removed_${System.currentTimeMillis()}.png") FileOutputStream(file).use { outputStream -> result.compress(Bitmap.CompressFormat.PNG, 100, outputStream) } file } val processedImageUri = Uri.fromFile(imageFile) // Step 7: Update the scene engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = processedImageUri, width = result.width.toUInt(), height = result.height.toUInt() ) ) ) return@withContext true } catch (e: Exception) { e.printStackTrace() return@withContext false } } } ``` ## Usage Example Here's how to use the background removal function in your app: ```kotlin import android.content.Context import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Engine fun removeBackgroundFromCurrentImage( engine: Engine, context: Context ) = CoroutineScope(Dispatchers.Main).launch { val success = performBackgroundRemoval(engine, context) if (success) { println("✅ Background removed successfully!") } else { println("❌ Failed to remove background") } } ``` ## Optimizations ### 1. Performance for Large Images For better performance with large images, downscale before processing: ```kotlin fun downscaleIfNeeded(bitmap: Bitmap, maxSize: Int = 1024): Bitmap { val maxDimension = maxOf(bitmap.width, bitmap.height) if (maxDimension <= maxSize) return bitmap val scale = maxSize.toFloat() / maxDimension val newWidth = (bitmap.width * scale).toInt() val newHeight = (bitmap.height * scale).toInt() return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) } ``` ### 2. Improve Mask Quality Apply smoothing to the mask for better edge quality: ```kotlin /** * Apply feathering to the mask for softer edges. */ fun applyFeathering(confidence: Float, threshold: Float = 0.5f, feather: Float = 0.1f): Float { return when { confidence >= threshold + feather -> 1.0f confidence <= threshold - feather -> 0.0f else -> { // Linear interpolation in the feather range (confidence - (threshold - feather)) / (2 * feather) } } } ``` Then use it when applying the mask: ```kotlin val alpha = applyFeathering(confidence) if (alpha > 0) { val r = Color.red(originalPixel) val g = Color.green(originalPixel) val b = Color.blue(originalPixel) val newAlpha = (alpha * 255).toInt() result.setPixel(x, y, Color.argb(newAlpha, r, g, b)) } else { result.setPixel(x, y, Color.TRANSPARENT) } ``` ## Troubleshooting **❌ Fill is not an image**: Always check the fill type before processing: ```kotlin val fillType = engine.block.getType(imageFill) if (fillType != "//ly.img.ubq/fill/image") { // Not an image fill, cannot process return } ``` **❌ Mask quality is poor**: - Use `enableRawSizeMask()` for full-resolution masks. - Apply feathering (see optimization section above). - Consider pre-processing the image for better lighting/contrast. **❌ Performance is slow on large images**: - Downscale images before processing (see optimization section). - Use `STREAM_MODE` instead of `SINGLE_IMAGE_MODE` for video. - Process on a background thread (already handled with coroutines in the example). **❌ Segmentation only works for people**: ML Kit Selfie Segmentation is optimized for human subjects. For general object segmentation, consider: - Using TensorFlow Lite models for object detection. - Cloud-based APIs (Google Cloud Vision, etc.). - Third-party segmentation libraries. **❌ App crashes or ML Kit errors**: - Ensure ML Kit dependencies are properly included. - Check that Google Play Services are available on the device. - Handle exceptions gracefully and provide user feedback. ## Alternative: Custom Segmentation Models For more advanced segmentation beyond people, you can use TensorFlow Lite models. Here's a basic structure: ```kotlin import org.tensorflow.lite.Interpreter import java.nio.ByteBuffer class CustomSegmentation(modelPath: String) { private val interpreter: Interpreter = Interpreter(loadModelFile(modelPath)) fun segment(bitmap: Bitmap): Bitmap? { // 1. Preprocess bitmap to model input format // 2. Run inference // 3. Post-process output to mask // 4. Apply mask to create transparent background // Implementation depends on your specific model return null } private fun loadModelFile(modelPath: String): ByteBuffer { // Load TFLite model file // Implementation omitted for brevity TODO("Load your .tflite model file") } } ``` ## Next Steps Now that you can remove backgrounds, explore related guides: - [Scale & Transform](https://img.ly/docs/cesdk/android/edit-image/transform/scale-ebe367/) images after background removal. - [Chroma Key](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/) for green screen effects. - [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) for processing multiple images. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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](https://img.ly/docs/cesdk/android/edit-image/transform/resize-407242/) - Change the size of individual elements or groups. - [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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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" description: "Change the size of individual elements or groups." 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/) --- The CreativeEditor SDK (CE.SDK) provides a **resize** feature for **Android** apps. Precise image sizing is crucial for **fitting content** into specific layouts, or exporting to various formats. CE.SDK offers both **interactive** resize controls and powerful **Kotlin** APIs for dimensional control. Whether you're building **social media templates** or **responsive interfaces**, this guide covers all resizing scenarios. ## Resizing features CE.SDK provides comprehensive tools for adjusting image dimensions in your Android app. The resizing feature enables the following actions: - Interactive resize handles for user control - Programmatic dimension setting with Kotlin - Group resizing for maintaining element relationships - Resize restrictions for layout protection ## Common resizing scenarios Apply resizing for: - Fitting images to Android layout constraints - Creating content for different screen densities - Implementing template-based designs with fixed dimensions ### Resize a block using the UI When a block is selected: - **Handles** appear on the four sides to allow the user to resize either the width or the height of the block. - A setting in the `editor` controls the **visibility** of the handles. - When the handles are **invisible**, a user can't resize using tapping or a mouse. ```kotlin engine.editor.setSettingBoolean("controlGizmo/showResizeHandles", false) ``` ![The image on the left has resize handles, the one on the right does not](../mobile-assets/resize-example-1.png) The image on the left displays all resize handles, while the right image has them disabled. Even with resize handles hidden, users can still access scale and rotation controls. ### Resize a block programmatically Each block contains the following **properties** that you can update to resize the block: - `width` - `height` The values of each property can be: - `SizeMode.AUTO`: automatic sizing. - `SizeMode.PERCENT`: the relationship between a block and its parent. For example, a block containing the following properties: - `SizeMode.PERCENT` mode - `width` set to `1.0f` will make its size to 100% of its parent's width. The following code sets a block to be 400 × 400 px: ```kotlin engine.block.setWidth(imageBlock, 400.0f) engine.block.setHeight(imageBlock, 400.0f) ``` ![Image resized to 400px by 400px](../mobile-assets/resize-example-2.png) There's also a convenience function for setting both width and height at once: ```kotlin engine.block.resize(imageBlock, 400.0f, 400.0f) ``` ![Two blocks sized by percentage](../mobile-assets/resize-example-3.png) ```kotlin import ly.img.engine.SizeMode engine.block.setWidthMode(imageBlock, SizeMode.PERCENT) engine.block.setWidth(imageBlock, 0.5f) // 50% of parent width engine.block.setHeightMode(imageBlock, SizeMode.PERCENT) engine.block.setHeight(imageBlock, 0.5f) // 50% of parent height ``` In this code: - The block on the **left** takes up 50% of the width and height of its parent. - The block on the **right** has been set to 25% of the width and height of its parent. ### Resize blocks as a group Group blocks to resize them together: ```kotlin val groupId = engine.block.group(listOf(imageBlock, textBlock)) engine.block.resize(groupId, 600.0f, 400.0f) ``` ### Lock resizing When working with templates, you can lock a block from being resized by setting its scope: ```kotlin engine.block.setScopeEnabled(imageBlock, "layer/resize", false) ``` To prevent users from transforming an element at all: ```kotlin engine.block.setTransformLocked(imageBlock, 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: "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. If your Android app needs caption-specific controls, wire your own UI to these APIs and keep the scene hierarchy shown below. ## Creating a Video Scene Create a video scene, add a page, set the page dimensions, and enable video captions before creating caption blocks. ```kotlin highlight-android-setup-scene val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.editor.setSettingBoolean(keypath = "features/videoCaptionsEnabled", value = true) ``` ## Setting Page Duration The page duration defines the time range where captions can appear. This sample uses a 20 second page. ```kotlin highlight-android-set-page-duration engine.block.setDuration(page, duration = 20.0) ``` ## Adding a Video Clip Create a graphic block with a video fill, give it the page duration, place it on a normal video track, and fill the page. This gives the captions actual video content to overlay during preview and export. ```kotlin highlight-android-add-video val video = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = Uri.parse( "https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) engine.block.setFill(video, fill = videoFill) engine.block.setDuration(video, duration = 20.0) val videoTrack = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = videoTrack) engine.block.appendChild(parent = videoTrack, child = video) engine.block.fillParent(videoTrack) ``` ## Creating a Caption Track A caption track groups caption blocks under the page. Create it before adding manual or imported captions so every caption has the correct parent. ```kotlin highlight-android-create-caption-track val captionTrack = engine.block.create(DesignBlockType.CaptionTrack) engine.block.appendChild(parent = page, child = captionTrack) ``` Caption tracks can either keep each caption's explicit time offset or manage offsets automatically from child durations. Keep `track/automaticallyManageBlockOffsets` set to `false` when you import SRT/VTT captions or set custom offsets yourself; set it to `true` when captions should play back sequentially without gaps. ```kotlin highlight-android-manage-caption-offsets val manageOffsetsAutomatically = false engine.block.setBoolean( block = captionTrack, property = "track/automaticallyManageBlockOffsets", value = manageOffsetsAutomatically, ) ``` ## Adding Captions ### Creating Caption Blocks Create caption blocks with `DesignBlockType.Caption`, write their text with the `caption/text` property, and append them to the caption track. ```kotlin highlight-android-create-captions val caption1 = engine.block.create(DesignBlockType.Caption) engine.block.setString(caption1, property = "caption/text", value = "Caption text 1") val caption2 = engine.block.create(DesignBlockType.Caption) engine.block.setString(caption2, property = "caption/text", value = "Caption text 2") engine.block.appendChild(parent = captionTrack, child = caption1) engine.block.appendChild(parent = captionTrack, child = caption2) ``` ### Importing Captions from Subtitle Files Use `createCaptionsFromURI` to parse SRT or VTT files. CE.SDK creates caption blocks with the parsed text and timing values. ```kotlin highlight-android-import-captions // Captions can also be loaded from a caption file, i.e., from SRT and VTT files. // The text and timing of the captions are read from the file. val captions = engine.block.createCaptionsFromURI("https://img.ly/static/examples/captions.srt") for (caption in captions) { engine.block.appendChild(parent = captionTrack, child = caption) } ``` Imported captions are still normal caption blocks, so you can append them to a caption track and style them with the same APIs as manually created captions. ## Modifying Captions ### Timing With manual offsets, set the duration and time offset on each caption block. Time values are in seconds. ```kotlin highlight-android-set-timing engine.block.setDuration(caption1, duration = 3.0) engine.block.setDuration(caption2, duration = 5.0) engine.block.setTimeOffset(caption1, offset = 0.0) engine.block.setTimeOffset(caption2, offset = 3.0) ``` ### Position and Size Caption position and size are synchronized between caption blocks that share the same caption track, so set the shared layout on one caption per track. ```kotlin highlight-android-position-size // Position and size sync only with caption blocks under the same caption track. engine.block.setPositionX(caption1, 0.05F) engine.block.setPositionXMode(caption1, PositionMode.PERCENT) engine.block.setPositionY(caption1, 0.8F) engine.block.setPositionYMode(caption1, PositionMode.PERCENT) engine.block.setHeight(caption1, 0.15F) engine.block.setHeightMode(caption1, SizeMode.PERCENT) engine.block.setWidth(caption1, 0.9F) engine.block.setWidthMode(caption1, SizeMode.PERCENT) ``` Percentage modes keep the caption box proportional when the output resolution changes. ### Styling Caption styling properties are also synchronized between caption blocks that share the same caption track. The sample changes text color, background, and drop shadow with dedicated styling setters, then uses property-keyed setters for caption automatic font sizing properties. ```kotlin highlight-android-style-captions // Style properties sync only with caption blocks under the same caption track. engine.block.setTextColor(caption1, color = Color.fromRGBA(0.9F, 0.9F, 0F, 1F)) engine.block.setDropShadowEnabled(caption1, enabled = true) engine.block.setDropShadowColor(caption1, color = Color.fromRGBA(0F, 0F, 0F, 0.8F)) engine.block.setBackgroundColorEnabled(caption1, enabled = true) engine.block.setBackgroundColor(caption1, color = Color.fromRGBA(0F, 0F, 0F, 0.7F)) // Use property-keyed setters for caption automatic font sizing properties. engine.block.setBoolean(caption1, property = "caption/automaticFontSizeEnabled", value = true) engine.block.setFloat(caption1, property = "caption/minAutomaticFontSize", value = 24F) engine.block.setFloat(caption1, property = "caption/maxAutomaticFontSize", value = 72F) ``` If your app registers a caption-preset asset source, you can query it with `engine.asset.findAssets(...)` and apply a returned asset with `engine.asset.applyAssetSourceAsset(...)`. The compiled sample uses direct block properties so it does not depend on a particular preset source being registered. ## Caption Animations Caption blocks support the same animation APIs as other animatable blocks. Create an animation and assign it as an in, loop, or out animation. ```kotlin highlight-android-add-animation val fadeInAnimation = engine.block.createAnimation(AnimationType.Fade) engine.block.setDuration(fadeInAnimation, duration = 0.3) engine.block.setInAnimation(caption1, animation = fadeInAnimation) ``` Use entry animations sparingly for captions; timing and readability usually matter more than motion. ## Exporting Videos with Captions Exporting the page as MP4 burns captions into the rendered video frames. The returned `ByteBuffer` contains the encoded MP4 data with caption pixels at the time offsets defined on each caption block. ```kotlin highlight-android-export-video // Export page as mp4 video. val videoBytes = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { Log.i( TAG, "Rendered ${it.renderedFrames} frames and encoded ${it.encodedFrames} frames out of ${it.totalFrames} frames", ) }, ) ``` The export callback reports render and encode progress. Changes made after export starts are not reflected in the exported file because CE.SDK freezes the scene state for that export. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Captions are not visible | The caption is not under a caption track on the page | Check the hierarchy: page, caption track, caption block | | Caption appears at the wrong time | The time offset or duration is wrong, or automatic offset management is enabled when custom offsets are expected | Read back `getTimeOffset(...)` and `getDuration(...)`, then check `track/automaticallyManageBlockOffsets` on the caption track | | Subtitle import fails | The URI does not resolve to a valid SRT or VTT file | Verify the URI is reachable from the Android app and that the file format is valid | | Styling is not applied | The property key is not a caption or text property | Use caption properties such as `caption/text`, `caption/automaticFontSizeEnabled`, and text styling properties | ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.createForVideo()` | Create a video scene | | `engine.editor.setSettingBoolean(keypath="features/videoCaptionsEnabled", value=_)` | Enable caption editing features | | `engine.block.create(blockType=DesignBlockType.Page)` | Create the page that contains the video and caption tracks | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create a block for the video clip | | `engine.block.createShape(type=ShapeType.Rect)` | Create a rectangular shape for the video block | | `engine.block.setShape(block=_, shape=_)` | Assign the shape to the video block | | `engine.block.createFill(fillType=FillType.Video)` | Create a video fill | | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Set the video file URI | | `engine.block.setFill(block=_, fill=_)` | Assign the video fill to the video block | | `engine.block.create(blockType=DesignBlockType.Track)` | Create a video track | | `engine.block.create(blockType=DesignBlockType.CaptionTrack)` | Create a caption track | | `engine.block.setBoolean(block=_, property="track/automaticallyManageBlockOffsets", value=_)` | Enable or disable automatic caption offset management | | `engine.block.create(blockType=DesignBlockType.Caption)` | Create a caption block | | `engine.block.createCaptionsFromURI(uri=_)` | Import SRT or VTT captions | | `engine.block.appendChild(parent=_, child=_)` | Add tracks and blocks to the hierarchy | | `engine.block.fillParent(block=_)` | Size a track to the page | | `engine.block.setString(block=_, property="caption/text", value=_)` | Set caption text | | `engine.block.setTimeOffset(block=_, offset=_)` | Set when a caption appears | | `engine.block.setDuration(block=_, duration=_)` | Set page, video, caption, or animation duration | | `engine.block.getTimeOffset(block=_)` | Read when a caption appears | | `engine.block.getDuration(block=_)` | Read a page, video, caption, or animation duration | | `engine.block.setPositionX(block=_, value=_)` | Set the caption box x position | | `engine.block.setPositionXMode(block=_, mode=_)` | Set the caption box x position mode | | `engine.block.setPositionY(block=_, value=_)` | Set the caption box y position | | `engine.block.setPositionYMode(block=_, mode=_)` | Set the caption box y position mode | | `engine.block.setWidth(block=_, value=_)` | Set the page width or caption box width | | `engine.block.setWidthMode(block=_, mode=_)` | Set the caption box width mode | | `engine.block.setHeight(block=_, value=_)` | Set the page height or caption box height | | `engine.block.setHeightMode(block=_, mode=_)` | Set the caption box height mode | | `engine.block.setTextColor(block=_, color=_)` | Set caption text color | | `engine.block.setDropShadowEnabled(block=_, enabled=_)` | Enable the caption drop shadow | | `engine.block.setDropShadowColor(block=_, color=_)` | Set caption drop shadow color | | `engine.block.setBackgroundColorEnabled(block=_, enabled=_)` | Enable the caption background | | `engine.block.setBackgroundColor(block=_, color=_)` | Set caption background color | | `engine.block.setBoolean(block=_, property="caption/automaticFontSizeEnabled", value=_)` | Enable automatic caption font sizing | | `engine.block.setFloat(block=_, property="caption/minAutomaticFontSize", value=_)` | Set the minimum automatic font size | | `engine.block.setFloat(block=_, property="caption/maxAutomaticFontSize", value=_)` | Set the maximum automatic font size | | `engine.asset.findAssets(sourceId=_, query=_)` | Query registered caption preset assets | | `engine.asset.applyAssetSourceAsset(sourceId=_, asset=_, block=_)` | Apply a preset asset to an existing caption block | | `engine.block.createAnimation(type=_)` | Create an animation block | | `engine.block.setInAnimation(block=_, animation=_)` | Assign an entry animation | | `engine.block.setLoopAnimation(block=_, animation=_)` | Assign a looping animation | | `engine.block.setOutAnimation(block=_, animation=_)` | Assign an exit animation | | `engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_)` | Export the page with burned-in captions | ## Next Steps - [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 - [Export Overview](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 - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Arrange and edit video clips, audio, and animations frame by frame - [Text Styling](https://img.ly/docs/cesdk/android/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Annotation" description: "Add timed text, shapes, and highlights to video scenes on Android." platform: android url: "https://img.ly/docs/cesdk/android/edit-video/annotation-e9cbad/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Annotation](https://img.ly/docs/cesdk/android/edit-video/annotation-e9cbad/) --- ```kotlin file=@cesdk_android_examples/engine-guides-annotation/Annotation.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import ly.img.engine.Color import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType import ly.img.engine.SizeMode fun annotation(engine: Engine) { val page = createAnnotationScene(engine) val text = addTextAnnotation(engine = engine, page = page) val highlight = addShapeAnnotation(engine = engine, page = page) val annotations = listOf(text, highlight) val sync = TimelineSync(engine = engine, page = page) sync.refresh(annotations) seekToAnnotation(engine = engine, page = page, annotation = highlight) setAnnotationPlayback(engine = engine, page = page, playing = true, looping = true) updateAnnotationText(engine = engine, annotation = text, text = "Replay this part") moveAnnotation(engine = engine, annotation = highlight, x = 780F, y = 260F) updateAnnotationTiming(engine = engine, annotation = highlight, start = 13.0, duration = 3.0) removeAnnotation(engine = engine, annotation = text) } fun createAnnotationScene(engine: Engine): DesignBlock { val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 20.0) val video = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) val videoUri = Uri.parse( "https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) // Video fills expose a URI-valued file property; set it with the typed URI API. engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(video, fill = videoFill) val videoTrack = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = videoTrack) engine.block.appendChild(parent = videoTrack, child = video) engine.block.fillParent(videoTrack) return page } fun addTextAnnotation( engine: Engine, page: DesignBlock, ): DesignBlock { val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, text = "Watch this part!") engine.block.setTextFontSize(text, fontSize = 32F) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.setPositionX(text, value = 160F) engine.block.setPositionY(text, value = 560F) engine.block.setTimeOffset(text, offset = 5.0) engine.block.setDuration(text, duration = 5.0) engine.block.appendChild(parent = page, child = text) return text } fun addShapeAnnotation( engine: Engine, page: DesignBlock, ): DesignBlock { val highlight = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(highlight, shape = engine.block.createShape(ShapeType.Star)) engine.block.setWidth(highlight, value = 140F) engine.block.setHeight(highlight, value = 140F) engine.block.setPositionX(highlight, value = 700F) engine.block.setPositionY(highlight, value = 240F) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(highlight, fill = fill) engine.block.setFillSolidColor( block = highlight, color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ) engine.block.setTimeOffset(highlight, offset = 12.0) engine.block.setDuration(highlight, duration = 4.0) engine.block.appendChild(parent = page, child = highlight) return highlight } data class AnnotationTimelineState( val currentTime: Double, val activeAnnotation: DesignBlock?, ) class TimelineSync( private val engine: Engine, private val page: DesignBlock, ) { private val mutableState = MutableStateFlow( AnnotationTimelineState( currentTime = 0.0, activeAnnotation = null, ), ) val state: StateFlow = mutableState.asStateFlow() private var pollingJob: Job? = null // Call this from UI code that owns a lifecycle scope. fun start( annotations: List, scope: CoroutineScope, ) { pollingJob?.cancel() pollingJob = scope.launch(Dispatchers.Main.immediate) { while (isActive) { refresh(annotations) delay(200) } } } fun refresh(annotations: List) { val currentTime = engine.block.getPlaybackTime(page) val active = annotations.firstOrNull { annotation -> engine.block.isValid(annotation) && engine.block.isVisibleAtCurrentPlaybackTime(annotation) } mutableState.value = AnnotationTimelineState( currentTime = currentTime, activeAnnotation = active, ) } fun stop() { pollingJob?.cancel() pollingJob = null } } fun seekToAnnotation( engine: Engine, page: DesignBlock, annotation: DesignBlock, ) { if (!engine.block.supportsPlaybackTime(page)) return val start = engine.block.getTimeOffset(annotation) engine.block.setPlaybackTime(block = page, time = start) } fun updateAnnotationText( engine: Engine, annotation: DesignBlock, text: String, ) { engine.block.replaceText(annotation, text = text) } fun moveAnnotation( engine: Engine, annotation: DesignBlock, x: Float, y: Float, ) { engine.block.setPositionX(annotation, value = x) engine.block.setPositionY(annotation, value = y) } fun updateAnnotationTiming( engine: Engine, annotation: DesignBlock, start: Double, duration: Double, ) { engine.block.setTimeOffset(annotation, offset = start) engine.block.setDuration(annotation, duration = duration) } fun removeAnnotation( engine: Engine, annotation: DesignBlock, ) { engine.block.destroy(annotation) } fun setAnnotationPlayback( engine: Engine, page: DesignBlock, playing: Boolean, looping: Boolean, ): Pair { engine.block.setPlaying(block = page, enabled = playing) val isPlaying = engine.block.isPlaying(page) engine.block.setLooping(block = page, looping = looping) val isLooping = engine.block.isLooping(page) return isPlaying to isLooping } ``` Annotations are timed visual overlays such as text labels, shapes, highlights, stickers, or images. On Android, they are ordinary blocks placed above video content and made visible for a specific timeline range. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-annotation) ## When to Use Annotations Use annotations for tutorials, sports analysis, education, product demos, and other video workflows where viewers should notice a specific moment. Use captions instead when the content is synchronized spoken text. Annotations use the same Block API as other visual content. Timing is controlled in seconds with `setTimeOffset` and `setDuration`, and playback sync reads the page's current playback time. ## Annotation Blocks and Timeline Placement For a standalone video scene, create a video page with explicit dimensions and duration, add the media to a track, and append annotation blocks to the page after the media. Page children added later render above earlier content. ```kotlin highlight-android-timeline-placement fun createAnnotationScene(engine: Engine): DesignBlock { val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 20.0) val video = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) val videoUri = Uri.parse( "https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) // Video fills expose a URI-valued file property; set it with the typed URI API. engine.block.setUri( block = videoFill, property = "fill/video/fileURI", value = videoUri, ) engine.block.setFill(video, fill = videoFill) val videoTrack = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = videoTrack) engine.block.appendChild(parent = videoTrack, child = video) engine.block.fillParent(videoTrack) return page } ``` ## Add a Text Annotation A text annotation is a `DesignBlockType.Text` block. Position it like any other text block, then set the timeline range before appending it to the page. ```kotlin highlight-android-text-annotation fun addTextAnnotation( engine: Engine, page: DesignBlock, ): DesignBlock { val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, text = "Watch this part!") engine.block.setTextFontSize(text, fontSize = 32F) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.setPositionX(text, value = 160F) engine.block.setPositionY(text, value = 560F) engine.block.setTimeOffset(text, offset = 5.0) engine.block.setDuration(text, duration = 5.0) engine.block.appendChild(parent = page, child = text) return text } ``` The example starts at `5.0` seconds and lasts `5.0` seconds, so it is visible from 5s to 10s on the page timeline. ## Add a Shape Annotation A shape annotation uses a graphic block with a vector shape and fill. This example creates a red star that appears after the text annotation. ```kotlin highlight-android-shape-annotation fun addShapeAnnotation( engine: Engine, page: DesignBlock, ): DesignBlock { val highlight = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(highlight, shape = engine.block.createShape(ShapeType.Star)) engine.block.setWidth(highlight, value = 140F) engine.block.setHeight(highlight, value = 140F) engine.block.setPositionX(highlight, value = 700F) engine.block.setPositionY(highlight, value = 240F) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(highlight, fill = fill) engine.block.setFillSolidColor( block = highlight, color = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F), ) engine.block.setTimeOffset(highlight, offset = 12.0) engine.block.setDuration(highlight, duration = 4.0) engine.block.appendChild(parent = page, child = highlight) return highlight } ``` Any visual block can serve as an annotation. Use text for labels, graphics for highlights, and image or sticker blocks for branded markers. ## Synchronize Annotation UI with Playback Custom UI can poll the page playback time and mark whichever annotation is visible at that moment. Keep the polling interval modest, for example 100-200 ms, so the UI stays responsive. ```kotlin highlight-android-playback-sync data class AnnotationTimelineState( val currentTime: Double, val activeAnnotation: DesignBlock?, ) class TimelineSync( private val engine: Engine, private val page: DesignBlock, ) { private val mutableState = MutableStateFlow( AnnotationTimelineState( currentTime = 0.0, activeAnnotation = null, ), ) val state: StateFlow = mutableState.asStateFlow() private var pollingJob: Job? = null // Call this from UI code that owns a lifecycle scope. fun start( annotations: List, scope: CoroutineScope, ) { pollingJob?.cancel() pollingJob = scope.launch(Dispatchers.Main.immediate) { while (isActive) { refresh(annotations) delay(200) } } } fun refresh(annotations: List) { val currentTime = engine.block.getPlaybackTime(page) val active = annotations.firstOrNull { annotation -> engine.block.isValid(annotation) && engine.block.isVisibleAtCurrentPlaybackTime(annotation) } mutableState.value = AnnotationTimelineState( currentTime = currentTime, activeAnnotation = active, ) } fun stop() { pollingJob?.cancel() pollingJob = null } } ``` > **Note:** * Engine calls must stay on the main thread. > * Store the active annotation ID in UI state and render your own list, marker, or toolbar state from that value. ## Seek to an Annotation Seek on the page, not on the annotation block itself. Read the annotation start with `getTimeOffset`, then set the page playback time. ```kotlin highlight-android-seek-to-annotation fun seekToAnnotation( engine: Engine, page: DesignBlock, annotation: DesignBlock, ) { if (!engine.block.supportsPlaybackTime(page)) return val start = engine.block.getTimeOffset(annotation) engine.block.setPlaybackTime(block = page, time = start) } ``` ## Controlling Playback (Play/Pause, Loop) Use page playback controls as supporting APIs for preview UI. For broader audio and video playback controls, see [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/). ```kotlin highlight-android-playback-controls fun setAnnotationPlayback( engine: Engine, page: DesignBlock, playing: Boolean, looping: Boolean, ): Pair { engine.block.setPlaying(block = page, enabled = playing) val isPlaying = engine.block.isPlaying(page) engine.block.setLooping(block = page, looping = looping) val isLooping = engine.block.isLooping(page) return isPlaying to isLooping } ``` ## Edit & Remove Annotations Text, position, timing, and deletion use the same block APIs after an annotation exists. Keep these operations focused so your UI can call them from list actions or inspector controls. ```kotlin highlight-android-edit-annotation fun updateAnnotationText( engine: Engine, annotation: DesignBlock, text: String, ) { engine.block.replaceText(annotation, text = text) } ``` ```kotlin highlight-android-move-annotation fun moveAnnotation( engine: Engine, annotation: DesignBlock, x: Float, y: Float, ) { engine.block.setPositionX(annotation, value = x) engine.block.setPositionY(annotation, value = y) } ``` ```kotlin highlight-android-retime-annotation fun updateAnnotationTiming( engine: Engine, annotation: DesignBlock, start: Double, duration: Double, ) { engine.block.setTimeOffset(annotation, offset = start) engine.block.setDuration(annotation, duration = duration) } ``` ```kotlin highlight-android-remove-annotation fun removeAnnotation( engine: Engine, annotation: DesignBlock, ) { engine.block.destroy(annotation) } ``` ## API Reference | API | Purpose | | --- | --- | | `engine.scene.createForVideo()` | Create a video scene that supports timeline playback. | | `engine.block.create(blockType=_)` | Create text, graphic, page, and track blocks. | | `engine.block.setWidth(block=_, value=_)` / `engine.block.setHeight(block=_, value=_)` | Size pages and visual annotation blocks. | | `engine.block.setWidthMode(block=_, mode=_)` / `engine.block.setHeightMode(block=_, mode=_)` | Auto-size text annotations to their content. | | `engine.block.replaceText(block=_, text=_)` | Set or update text annotation content. | | `engine.block.setTextFontSize(block=_, fontSize=_)` | Set text annotation size. | | `engine.block.createShape(type=_)` / `engine.block.setShape(block=_, shape=_)` | Create a shape annotation. | | `engine.block.createFill(fillType=_)` / `engine.block.setFill(block=_, fill=_)` / `engine.block.setFillSolidColor(block=_, color=_)` | Style graphic annotations. | | `engine.block.setUri(block=_, property="fill/video/fileURI", value=_)` | Attach video media to a video fill. | | `engine.block.setPositionX(block=_, value=_)` / `engine.block.setPositionY(block=_, value=_)` | Place annotations on the page. | | `engine.block.setTimeOffset(block=_, offset=_)` / `engine.block.getTimeOffset(block=_)` | Set or read the annotation start time in seconds. | | `engine.block.setDuration(block=_, duration=_)` / `engine.block.getDuration(block=_)` | Set or read the annotation duration in seconds. | | `engine.block.appendChild(parent=_, child=_)` | Add annotations to the page hierarchy. | | `engine.block.fillParent(block=_)` | Make a track fill its parent page. | | `engine.block.isValid(block=_)` | Ignore annotations that were removed before a UI refresh. | | `engine.block.supportsPlaybackTime(block=_)` | Check whether a page can be seeked. | | `engine.block.setPlaybackTime(block=_, time=_)` / `engine.block.getPlaybackTime(block=_)` | Seek or read timeline playback time. | | `engine.block.isVisibleAtCurrentPlaybackTime(block=_)` | Determine whether an annotation is active at the current page time. | | `engine.block.setPlaying(block=_, enabled=_)` / `engine.block.isPlaying(block=_)` | Start or query playback. | | `engine.block.setLooping(block=_, looping=_)` / `engine.block.isLooping(block=_)` | Control loop behavior. | | `engine.block.destroy(block=_)` | Remove an annotation. | ## Troubleshooting - **Annotation does not show up:** append it to the page after the media track, and keep its `timeOffset` plus `duration` inside the page duration. - **Seek jumps do nothing:** call `setPlaybackTime` on the page block, not on the annotation block. - **UI feels sluggish:** poll at 5-10 Hz and keep engine calls on the main thread. - **Export differs from preview:** verify the video scene duration and test very small text or heavy effects in exported MP4 output. ## Next Steps - [Add Captions](https://img.ly/docs/cesdk/android/edit-video/add-captions-f67565/) - Use caption blocks and tracks for synchronized spoken text. - [Text Variables](https://img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50/) - Populate labels from dynamic values such as usernames or scores. - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Control timeline playback, trim ranges, looping, and resources. - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Build timeline interfaces for arranging clips and overlays. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Force Trim" description: "Enforce minimum and maximum video durations in the editor UI." platform: android url: "https://img.ly/docs/cesdk/android/edit-video/force-trim-3c1e8a/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Force Trim](https://img.ly/docs/cesdk/android/edit-video/force-trim-3c1e8a/) --- ```kotlin file=@cesdk_android_examples/editor-guides-video-force-trim/ForceTrimVideoSolution.kt reference-only import androidx.compose.runtime.Composable import ly.img.editor.Editor import ly.img.editor.configuration.video.VideoConfigurationBuilder import ly.img.editor.configuration.video.callback.onLoaded import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.editor.core.event.EditorEvent import kotlin.time.Duration.Companion.seconds // Add this composable to your NavHost @Composable fun ForceTrimVideoSolution( license: String, onClose: (Throwable?) -> Unit, ) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) { onLoaded = { val event = EditorEvent.ApplyVideoDurationConstraints( minDuration = 1.seconds, maxDuration = 5.seconds, ) editorContext.eventHandler.send(event) onLoaded() } } }, onClose = onClose, ) } ``` Force trim lets you enforce minimum and maximum video durations in the timeline UI. The editor clamps export to the maximum duration and shows labels to communicate the limits. > **Reading time:** 2 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/editor-guides-video-force-trim) ## Configure duration constraints We apply constraints in `EngineConfiguration.onLoaded` after the scene has loaded. Keep `minimumVideoDuration` and `maximumVideoDuration` in seconds and ensure the max is not smaller than the min. ```kotlin highlight-android-constraints val event = EditorEvent.ApplyVideoDurationConstraints( minDuration = 1.seconds, maxDuration = 5.seconds, ) editorContext.eventHandler.send(event) ``` ## Launch the video editor Use the default video scene and the standard video UI. You can call the setter again later to switch presets at runtime. ```kotlin highlight-android-editor Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::VideoConfigurationBuilder) { onLoaded = { val event = EditorEvent.ApplyVideoDurationConstraints( minDuration = 1.seconds, maxDuration = 5.seconds, ) editorContext.eventHandler.send(event) onLoaded() } } }, onClose = onClose, ) ``` ![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.app.Application import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlock import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType data class TrackClipState( val name: String, val timeOffset: Double, val duration: Double, ) data class JoinAndArrangeVideoResult( val initialTrackClips: List, val reorderedTrackClips: List, val pageDuration: Double, val mainTrackDuration: Double, val overlayTrackOffset: Double, val overlayTrackDuration: Double, val overlayClipCount: Int, ) suspend fun joinAndArrangeVideoClips( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ): JoinAndArrangeVideoResult = withContext(Dispatchers.Main) { var engine: Engine? = null var engineStarted = false try { Engine.init(application) val currentEngine = Engine.getInstance(id = "ly.img.engine.join-and-arrange-video.example") engine = currentEngine engineStarted = currentEngine.start(license = license, userId = userId) currentEngine.bindOffscreen(width = 1920, height = 1080) val scene = currentEngine.scene.createForVideo() val page = currentEngine.block.create(DesignBlockType.Page) currentEngine.block.appendChild(parent = scene, child = page) currentEngine.block.setWidth(page, value = 1920F) currentEngine.block.setHeight(page, value = 1080F) currentEngine.block.setDuration(page, duration = 15.0) val videoUri = Uri.parse( "https://cdn.img.ly/assets/demo/v3/ly.img.video/videos/" + "pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) val clipA = createVideoClip( engine = currentEngine, name = "Clip A", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipB = createVideoClip( engine = currentEngine, name = "Clip B", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipC = createVideoClip( engine = currentEngine, name = "Clip C", videoUri = videoUri, width = 1920F, height = 1080F, ) val track = currentEngine.block.create(DesignBlockType.Track) currentEngine.block.appendChild(parent = page, child = track) currentEngine.block.setBoolean( block = track, property = "track/automaticallyManageBlockOffsets", value = false, ) currentEngine.block.appendChild(parent = track, child = clipA) currentEngine.block.appendChild(parent = track, child = clipB) currentEngine.block.appendChild(parent = track, child = clipC) currentEngine.block.fillParent(track) val initialTrackChildren = currentEngine.block.getChildren(track) check(initialTrackChildren == listOf(clipA, clipB, clipC)) currentEngine.block.setDuration(clipA, duration = 5.0) currentEngine.block.setDuration(clipB, duration = 5.0) currentEngine.block.setDuration(clipC, duration = 5.0) currentEngine.block.setDuration(track, duration = 15.0) currentEngine.block.setTimeOffset(clipA, offset = 0.0) currentEngine.block.setTimeOffset(clipB, offset = 5.0) currentEngine.block.setTimeOffset(clipC, offset = 10.0) val initialTrackDuration = currentEngine.block.getDuration(track) check(initialTrackDuration == 15.0) val initialClipStates = currentEngine.block.getChildren(track).map { clip -> TrackClipState( name = currentEngine.block.getName(clip), timeOffset = currentEngine.block.getTimeOffset(clip), duration = currentEngine.block.getDuration(clip), ) } currentEngine.block.insertChild(parent = track, child = clipC, index = 0) currentEngine.block.setTimeOffset(clipC, offset = 0.0) currentEngine.block.setTimeOffset(clipA, offset = 5.0) currentEngine.block.setTimeOffset(clipB, offset = 10.0) val reorderedTrackDuration = currentEngine.block.getDuration(track) check(reorderedTrackDuration == 15.0) val reorderedClipStates = currentEngine.block.getChildren(track).map { clip -> TrackClipState( name = currentEngine.block.getName(clip), timeOffset = currentEngine.block.getTimeOffset(clip), duration = currentEngine.block.getDuration(clip), ) } val finalClipOrder = currentEngine.block.getChildren(track).map { clip -> currentEngine.block.getName(clip) } val finalClipOffsets = currentEngine.block.getChildren(track).map { clip -> currentEngine.block.getTimeOffset(clip) } check(finalClipOrder == listOf("Clip C", "Clip A", "Clip B")) check(finalClipOffsets == listOf(0.0, 5.0, 10.0)) val overlayTrack = currentEngine.block.create(DesignBlockType.Track) currentEngine.block.appendChild(parent = page, child = overlayTrack) currentEngine.block.setTimeOffset(overlayTrack, offset = 2.0) val overlayClip = createVideoClip( engine = currentEngine, name = "Overlay Clip", videoUri = videoUri, width = 1920F / 4F, height = 1080F / 4F, ) currentEngine.block.setDuration(overlayClip, duration = 5.0) currentEngine.block.appendChild(parent = overlayTrack, child = overlayClip) currentEngine.block.setPositionX(overlayClip, value = 1920F - 1920F / 4F - 40F) currentEngine.block.setPositionY(overlayClip, value = 1080F - 1080F / 4F - 40F) JoinAndArrangeVideoResult( initialTrackClips = initialClipStates, reorderedTrackClips = reorderedClipStates, pageDuration = currentEngine.block.getDuration(page), mainTrackDuration = reorderedTrackDuration, overlayTrackOffset = currentEngine.block.getTimeOffset(overlayTrack), overlayTrackDuration = currentEngine.block.getDuration(overlayTrack), overlayClipCount = currentEngine.block.getChildren(overlayTrack).size, ) } finally { if (engineStarted) { engine?.stop() } } } private suspend fun createVideoClip( engine: Engine, name: String, videoUri: Uri, width: Float, height: Float, ): DesignBlock { val clip = engine.block.create(DesignBlockType.Graphic) engine.block.setName(clip, name) engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(clip, value = width) engine.block.setHeight(clip, value = height) val videoFill = engine.block.createFill(FillType.Video) // The Android binding has no typed property helper for video fill URIs yet. engine.block.setUri(block = videoFill, property = "fill/video/fileURI", value = videoUri) engine.block.setFill(block = clip, fill = videoFill) engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER) engine.block.forceLoadAVResource(block = videoFill) return clip } ``` Combine multiple video clips into a sequence and organize them in the composition using CE.SDK tracks, durations, and time offsets on Android. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-join-and-arrange-video) Video compositions in CE.SDK use a **Scene > Page > Track > Clip** hierarchy. Tracks group clips for timed playback. This guide sets clip durations and time offsets explicitly so the sequence is deterministic and mirrors the Node.js guide flow. This guide covers the built-in timeline behavior at a high level, then uses the CreativeEngine API to build a three-clip montage, reorder it, and add an overlay track for a picture-in-picture composition. ## Joining Clips via UI CE.SDK's Android video editor includes timeline controls for arranging clips. Start from the [Video Editor starter kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) when you need an interactive timeline UI. Use the Engine API sections below when you need to prepare scenes programmatically. ### Adding Clips to Timeline Users add clips from the asset library to the timeline. Adding a clip to an existing track joins it to that sequence; adding it to an empty area creates a separate track. The timeline displays clip duration visually, so longer clips take more horizontal space and the sequence is easy to scan. ### Reordering Clips Users can drag clips within a track to reorder them. The timeline updates the sequence and its time offsets so clips remain packed without gaps. ### Creating Additional Tracks Additional tracks create layered compositions. Tracks later in the page's child order render on top, which enables overlays, titles, and picture-in-picture layouts. ## Programmatic Clip Joining ### Creating the Scene Create a video scene, add a page, set a 16:9 frame, and make the page long enough for three 5-second clips. ```kotlin highlight-android-create-scene val scene = currentEngine.scene.createForVideo() val page = currentEngine.block.create(DesignBlockType.Page) currentEngine.block.appendChild(parent = scene, child = page) currentEngine.block.setWidth(page, value = 1920F) currentEngine.block.setHeight(page, value = 1080F) currentEngine.block.setDuration(page, duration = 15.0) ``` ### Creating Video Clips Create each video clip by building the block structure directly: a graphic block, a rectangle shape, and a video fill. The helper loads the video resource before returning the clip. ```kotlin highlight-android-create-video-helper private suspend fun createVideoClip( engine: Engine, name: String, videoUri: Uri, width: Float, height: Float, ): DesignBlock { val clip = engine.block.create(DesignBlockType.Graphic) engine.block.setName(clip, name) engine.block.setShape(clip, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(clip, value = width) engine.block.setHeight(clip, value = height) val videoFill = engine.block.createFill(FillType.Video) // The Android binding has no typed property helper for video fill URIs yet. engine.block.setUri(block = videoFill, property = "fill/video/fileURI", value = videoUri) engine.block.setFill(block = clip, fill = videoFill) engine.block.setContentFillMode(block = clip, mode = ContentFillMode.COVER) engine.block.forceLoadAVResource(block = videoFill) return clip } ``` Then create the three clips with the same 1920 x 1080 size used by the montage. The next section assigns their playback duration. ```kotlin highlight-android-create-clips val clipA = createVideoClip( engine = currentEngine, name = "Clip A", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipB = createVideoClip( engine = currentEngine, name = "Clip B", videoUri = videoUri, width = 1920F, height = 1080F, ) val clipC = createVideoClip( engine = currentEngine, name = "Clip C", videoUri = videoUri, width = 1920F, height = 1080F, ) ``` ### Creating Tracks Create a track, attach it to the page, and disable automatic offset management because this sample writes the clip offsets directly. ```kotlin highlight-android-create-track val track = currentEngine.block.create(DesignBlockType.Track) currentEngine.block.appendChild(parent = page, child = track) currentEngine.block.setBoolean( block = track, property = "track/automaticallyManageBlockOffsets", value = false, ) ``` ### Adding Clips to Track Append the clips to the track so they share a timeline container. Calling `fillParent()` on the track after appending the clips fills the child clips against the page frame first, then resizes and positions the track itself. ```kotlin highlight-android-add-clips-to-track currentEngine.block.appendChild(parent = track, child = clipA) currentEngine.block.appendChild(parent = track, child = clipB) currentEngine.block.appendChild(parent = track, child = clipC) currentEngine.block.fillParent(track) val initialTrackChildren = currentEngine.block.getChildren(track) check(initialTrackChildren == listOf(clipA, clipB, clipC)) ``` After appending, `getChildren()` verifies that the clips are attached. The next section sets their playback offsets. ### Setting Clip Durations Set each clip duration in seconds. The track duration covers the full 15-second sequence. ```kotlin highlight-android-set-clip-durations currentEngine.block.setDuration(clipA, duration = 5.0) currentEngine.block.setDuration(clipB, duration = 5.0) currentEngine.block.setDuration(clipC, duration = 5.0) currentEngine.block.setDuration(track, duration = 15.0) ``` ## Arranging Clips ### Time Offsets Time offsets control when each block becomes active relative to its parent. In a manual sequence, set each child clip's offset from the cumulative duration of the preceding clips. ```kotlin highlight-android-time-offsets currentEngine.block.setTimeOffset(clipA, offset = 0.0) currentEngine.block.setTimeOffset(clipB, offset = 5.0) currentEngine.block.setTimeOffset(clipC, offset = 10.0) val initialTrackDuration = currentEngine.block.getDuration(track) check(initialTrackDuration == 15.0) val initialClipStates = currentEngine.block.getChildren(track).map { clip -> TrackClipState( name = currentEngine.block.getName(clip), timeOffset = currentEngine.block.getTimeOffset(clip), duration = currentEngine.block.getDuration(clip), ) } ``` Clip A starts at 0 seconds, Clip B at 5 seconds, and Clip C at 10 seconds. With 5-second durations, this creates a continuous 15-second sequence. ### Reordering Clips Use `insertChild()` to move an existing clip to a specific index. The track child order changes immediately. Update the time offsets after the move so playback follows the new order without gaps. ```kotlin highlight-android-reorder-clips currentEngine.block.insertChild(parent = track, child = clipC, index = 0) currentEngine.block.setTimeOffset(clipC, offset = 0.0) currentEngine.block.setTimeOffset(clipA, offset = 5.0) currentEngine.block.setTimeOffset(clipB, offset = 10.0) val reorderedTrackDuration = currentEngine.block.getDuration(track) check(reorderedTrackDuration == 15.0) val reorderedClipStates = currentEngine.block.getChildren(track).map { clip -> TrackClipState( name = currentEngine.block.getName(clip), timeOffset = currentEngine.block.getTimeOffset(clip), duration = currentEngine.block.getDuration(clip), ) } ``` Moving Clip C to index 0 changes the order from A-B-C to C-A-B. The resulting offsets are C at 0 seconds, A at 5 seconds, and B at 10 seconds. ### Querying Track Children Use `getChildren()` to inspect track hierarchy and rendering order while debugging a custom timeline. Pair that readback with `getTimeOffset()` to persist the playback sequence. ```kotlin highlight-android-query-track-children val finalClipOrder = currentEngine.block.getChildren(track).map { clip -> currentEngine.block.getName(clip) } val finalClipOffsets = currentEngine.block.getChildren(track).map { clip -> currentEngine.block.getTimeOffset(clip) } check(finalClipOrder == listOf("Clip C", "Clip A", "Clip B")) check(finalClipOffsets == listOf(0.0, 5.0, 10.0)) ``` ## Multi-Track Compositions ### Adding Multiple Tracks Create layered compositions by adding more tracks to the page. This sample adds an overlay track that starts at 2 seconds and contains a smaller clip in the bottom-right corner. ```kotlin highlight-android-multi-track val overlayTrack = currentEngine.block.create(DesignBlockType.Track) currentEngine.block.appendChild(parent = page, child = overlayTrack) currentEngine.block.setTimeOffset(overlayTrack, offset = 2.0) val overlayClip = createVideoClip( engine = currentEngine, name = "Overlay Clip", videoUri = videoUri, width = 1920F / 4F, height = 1080F / 4F, ) currentEngine.block.setDuration(overlayClip, duration = 5.0) currentEngine.block.appendChild(parent = overlayTrack, child = overlayClip) currentEngine.block.setPositionX(overlayClip, value = 1920F - 1920F / 4F - 40F) currentEngine.block.setPositionY(overlayClip, value = 1080F - 1080F / 4F - 40F) ``` ### Track Rendering Order CE.SDK renders page children in order. The first track appears behind later tracks, so add background video first and overlays or titles later. - **Background layers**: Full-frame clips on the first track. - **Overlays**: Smaller clips positioned on later tracks. - **Titles**: Text or graphics added above the video tracks. ## Troubleshooting ### Clips Not Appearing Verify that every clip is attached to a track and that the track is attached to the page. `engine.block.getParent()` and `engine.block.getChildren()` are the quickest checks for hierarchy issues. ### Wrong Playback Order Check the track child order first. Default Android tracks automatically pack child offsets from that order and each clip's duration. When you manage timing manually, disable `track/automaticallyManageBlockOffsets` and write the offsets with `setTimeOffset()`. CE.SDK still prevents overlaps inside a single track; use separate tracks for overlapping or layered clips. ### Video Not Loading Check that the video URL is reachable and uses a supported format. For Android samples that use explicit video fills, call `engine.block.forceLoadAVResource()` on the video fill before you depend on media metadata. ## API Reference | Method | Description | | --- | --- | | `engine.scene.createForVideo()` | Create a scene configured for video playback. | | `engine.block.create(blockType=DesignBlockType.Page)` | Create the page that holds the video composition. | | `engine.block.create(blockType=DesignBlockType.Track)` | Create a track for sequential or layered clips. | | `engine.block.create(blockType=DesignBlockType.Graphic)` | Create the graphic block used as a video clip. | | `engine.block.createFill(fillType=FillType.Video)` | Create the video fill attached to a clip block. | | `engine.block.appendChild(parent=_, child=_)` | Add a clip to a track or a track to a page hierarchy. | | `engine.block.insertChild(parent=_, child=_, index=_)` | Move or insert a child at a specific rendering-order index. | | `engine.block.getChildren(block=_)` | Read child blocks in rendering order. | | `engine.block.setDuration(block=_, duration=_)` | Set a page, track, or clip duration in seconds. | | `engine.block.getDuration(block=_)` | Read a block duration in seconds. | | `engine.block.setTimeOffset(block=_, offset=_)` | Set when a block starts relative to its parent. | | `engine.block.getTimeOffset(block=_)` | Read a block's time offset in seconds. | | `engine.block.setBoolean(block=_, property="track/automaticallyManageBlockOffsets", value=_)` | Switch a track between automatic and manual child offset management. | | `engine.block.forceLoadAVResource(block=_)` | Load audio or video metadata for a video fill or audio block. | | `engine.block.fillParent(block=_)` | Resize and position a block to fill its parent frame; when the block is a group or track, child blocks are filled against the enclosing frame first. | ## Next Steps Now that you can join and arrange clips, continue with related video editing features: - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Master playback timing and audio mixing - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Understand the complete timeline editing system --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Programmatic Editing" description: "Edit video scenes with CE.SDK Engine APIs on Android." platform: android url: "https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Programmatic Editing](https://img.ly/docs/cesdk/android/edit-video/programmatic-8429af/) --- ```kotlin file=@cesdk_android_examples/engine-guides-create-video-edit-programmatic/EditVideoProgrammatically.kt reference-only import android.app.Application import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Color import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.ExportVideoOptions import ly.img.engine.FillType import ly.img.engine.MimeType import ly.img.engine.ShapeType import ly.img.engine.SplitOptions import java.nio.ByteBuffer fun editVideoProgrammatically( application: Application, license: String?, // pass null or empty for evaluation mode with watermark userId: String, ) = CoroutineScope(Dispatchers.Main).launch { exportProgrammaticVideoEdit(application = application, license = license, userId = userId) } suspend fun exportProgrammaticVideoEdit( application: Application, license: String?, userId: String, ): ProgrammaticVideoEditResult = withContext(Dispatchers.Main) { var engine: Engine? = null var engineStarted = false try { Engine.init(application) val currentEngine = Engine.getInstance(id = "ly.img.engine.programmatic-video-editing") engine = currentEngine engineStarted = currentEngine.start(license = license, userId = userId) currentEngine.bindOffscreen(width = 1280, height = 720) buildProgrammaticVideoEdit(currentEngine) } finally { if (engineStarted) { engine?.stop() } } } private suspend fun buildProgrammaticVideoEdit(engine: Engine): ProgrammaticVideoEditResult { val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 4.0) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.fillParent(track) val firstClip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(firstClip, shape = engine.block.createShape(ShapeType.Rect)) val firstVideoFill = engine.block.createFill(FillType.Video) // Video fill sources use the generic property-keyed URI setter. engine.block.setUri( block = firstVideoFill, property = "fill/video/fileURI", value = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"), ) engine.block.setFill(block = firstClip, fill = firstVideoFill) engine.block.setContentFillMode(firstClip, mode = ContentFillMode.COVER) val secondClip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(secondClip, shape = engine.block.createShape(ShapeType.Rect)) val secondVideoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = secondVideoFill, property = "fill/video/fileURI", value = Uri.parse( "https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) engine.block.setFill(block = secondClip, fill = secondVideoFill) engine.block.setContentFillMode(secondClip, mode = ContentFillMode.COVER) engine.block.appendChild(parent = track, child = firstClip) engine.block.appendChild(parent = track, child = secondClip) engine.block.forceLoadAVResource(block = firstVideoFill) engine.block.forceLoadAVResource(block = secondVideoFill) check(engine.block.getAVResourceTotalDuration(firstVideoFill) >= 3.0) engine.block.setDuration(block = firstClip, duration = 2.0) engine.block.setDuration(block = secondClip, duration = 2.0) engine.block.setTrimOffset(block = firstVideoFill, offset = 1.0) engine.block.setTrimLength(block = firstVideoFill, length = 2.0) val secondSegment = engine.block.split( block = secondClip, atTime = 1.0, options = SplitOptions(selectNewBlock = false), ) val overlay = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(overlay, shape = engine.block.createShape(ShapeType.Rect)) val overlayFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = overlay, fill = overlayFill) engine.block.setFillSolidColor( block = overlay, color = Color.fromRGBA(r = 1F, g = 0.82F, b = 0.1F, a = 0.85F), ) engine.block.setWidth(overlay, value = 1280F) engine.block.setHeight(overlay, value = 72F) engine.block.setPositionY(overlay, value = 648F) engine.block.setTimeOffset(block = overlay, offset = 1.25) engine.block.setDuration(block = overlay, duration = 1.5) engine.block.appendChild(parent = page, child = overlay) val editedVideo = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { progress -> println( "Rendered ${progress.renderedFrames} of ${progress.totalFrames} frames", ) }, options = ExportVideoOptions( videoBitrate = 8_000_000, audioBitrate = 128_000, frameRate = 30F, targetWidth = 1280F, targetHeight = 720F, ), ) check(editedVideo.remaining() > 0) return ProgrammaticVideoEditResult( exportedVideo = editedVideo, pageDuration = engine.block.getDuration(page), firstClipTrimOffset = engine.block.getTrimOffset(firstVideoFill), firstClipTrimLength = engine.block.getTrimLength(firstVideoFill), splitSegmentDuration = engine.block.getDuration(secondSegment), overlayTimeOffset = engine.block.getTimeOffset(overlay), ) } data class ProgrammaticVideoEditResult( val exportedVideo: ByteBuffer, val pageDuration: Double, val firstClipTrimOffset: Double, val firstClipTrimLength: Double, val splitSegmentDuration: Double, val overlayTimeOffset: Double, ) ``` Edit video scenes with CE.SDK Engine APIs when your app needs automation, custom controls, or template-driven video output. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-create-video-edit-programmatic) Programmatic editing works directly on the Engine scene graph instead of the built-in timeline UI. The Android [Video Editor Starter Kit](https://img.ly/docs/cesdk/android/starterkits/video-editor-e1nlor/) already includes a timeline UI, and the [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) guide covers timeline-focused UI and Engine concepts. Use the programmatic approach when your app needs to create or modify video timelines from code, for example when generating variants, applying a known edit recipe, or exporting a scene without opening the editor. This guide builds a compact two-clip montage, trims the first clip, splits the second clip, adds a timed graphic overlay, and exports the edited page as MP4. ## Understand the Video Timeline A video scene contains one or more pages. A page owns tracks and timed blocks, and each track arranges its child clips in sequence. In a typical video timeline: - The scene is created in video mode with `engine.scene.createForVideo()`. - A page defines the canvas size and the exported playback duration. - A track contains graphic clip blocks with video fills. - Audio blocks or overlay blocks can sit on the page timeline beside the track. - Audio-only media uses `DesignBlockType.Audio`; video fills and audio blocks can both be muted or mixed with volume controls. Timeline values are measured in seconds. `setDuration()` controls how long a block is active, `setTimeOffset()` controls when a block starts within its parent timeline, and trim values control which source-media segment plays. ## Create a Video Scene Create a video scene, add a page, set the page size, and set the page duration. The page is the block exported later. ```kotlin highlight-android-create-video-scene val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 4.0) ``` `createForVideo()` enables timeline behavior. The page duration here is `4.0` seconds, so the export renders the first four seconds of the edited page. ## Add and Arrange Clips Create a track, attach it to the page, and append two graphic blocks with video fills. The track uses the child order to play clips sequentially. ```kotlin highlight-android-add-clips val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.fillParent(track) val firstClip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(firstClip, shape = engine.block.createShape(ShapeType.Rect)) val firstVideoFill = engine.block.createFill(FillType.Video) // Video fill sources use the generic property-keyed URI setter. engine.block.setUri( block = firstVideoFill, property = "fill/video/fileURI", value = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4"), ) engine.block.setFill(block = firstClip, fill = firstVideoFill) engine.block.setContentFillMode(firstClip, mode = ContentFillMode.COVER) val secondClip = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(secondClip, shape = engine.block.createShape(ShapeType.Rect)) val secondVideoFill = engine.block.createFill(FillType.Video) engine.block.setUri( block = secondVideoFill, property = "fill/video/fileURI", value = Uri.parse( "https://cdn.img.ly/assets/demo/v1/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) engine.block.setFill(block = secondClip, fill = secondVideoFill) engine.block.setContentFillMode(secondClip, mode = ContentFillMode.COVER) engine.block.appendChild(parent = track, child = firstClip) engine.block.appendChild(parent = track, child = secondClip) ``` Video fill sources use the generic property-keyed `setUri()` API because there is no convenience method dedicated to `fill/video/fileURI`. The rest of the sample uses typed constants such as `DesignBlockType.Track`, `DesignBlockType.Graphic`, `FillType.Video`, and `ShapeType.Rect`. The sample sets `ContentFillMode.COVER` so each clip fills the page frame. Pick the mode that matches how the source video should fit into the graphic block: | Mode | Effect | | --- | --- | | `ContentFillMode.CROP` | Uses manual crop positioning. | | `ContentFillMode.COVER` | Scales content to cover the full block frame and can crop the edges. | | `ContentFillMode.CONTAIN` | Scales content to fit inside the block frame and can leave empty space. | ## Change Timing and Trim Load AV metadata before changing trim values or reading duration metadata. Then set each clip's timeline duration and trim the first fill to start one second into its source media. ```kotlin highlight-android-change-timing-trim engine.block.forceLoadAVResource(block = firstVideoFill) engine.block.forceLoadAVResource(block = secondVideoFill) check(engine.block.getAVResourceTotalDuration(firstVideoFill) >= 3.0) engine.block.setDuration(block = firstClip, duration = 2.0) engine.block.setDuration(block = secondClip, duration = 2.0) engine.block.setTrimOffset(block = firstVideoFill, offset = 1.0) engine.block.setTrimLength(block = firstVideoFill, length = 2.0) ``` Use these APIs for different timing layers: | API | Effect | | --- | --- | | `engine.block.setDuration(block=_, duration=_)` | Sets how long the block is active on the timeline. | | `engine.block.setTimeOffset(block=_, offset=_)` | Sets when the block starts within its parent timeline. | | `engine.block.setTrimOffset(block=_, offset=_)` | Sets where source media playback starts. | | `engine.block.setTrimLength(block=_, length=_)` | Sets how much source media is used for playback. | ## Split a Clip Split the second clip at one second. The original block becomes the first segment, and `split()` returns the new second segment. ```kotlin highlight-android-split-clip val secondSegment = engine.block.split( block = secondClip, atTime = 1.0, options = SplitOptions(selectNewBlock = false), ) ``` The sample passes `SplitOptions(selectNewBlock = false)` because a headless workflow does not need to change editor selection after the split. `SplitOptions` controls how CE.SDK attaches and selects the new segment: | Field | Default | Effect | | --- | --- | --- | | `attachToParent` | `true` | Attaches the returned segment to the same parent as the original block. | | `createParentTrackIfNeeded` | `false` | Creates a parent track when the split block needs one and `attachToParent` is enabled. | | `selectNewBlock` | `true` | Selects the returned segment after splitting. | ## Apply a Timed Overlay Add a short graphic overlay directly to the page timeline. Its `timeOffset` and `duration` make it appear only for part of the exported video. ```kotlin highlight-android-timed-overlay val overlay = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(overlay, shape = engine.block.createShape(ShapeType.Rect)) val overlayFill = engine.block.createFill(FillType.Color) engine.block.setFill(block = overlay, fill = overlayFill) engine.block.setFillSolidColor( block = overlay, color = Color.fromRGBA(r = 1F, g = 0.82F, b = 0.1F, a = 0.85F), ) engine.block.setWidth(overlay, value = 1280F) engine.block.setHeight(overlay, value = 72F) engine.block.setPositionY(overlay, value = 648F) engine.block.setTimeOffset(block = overlay, offset = 1.25) engine.block.setDuration(block = overlay, duration = 1.5) engine.block.appendChild(parent = page, child = overlay) ``` This same pattern works for other programmatic edits: create or find the target block, set its timing, then apply the specific block or fill properties your workflow needs. ## Export the Edited Video Export the page as MP4 with `exportVideo()`. The progress callback reports rendered and total frame counts while the export runs, and `ExportVideoOptions` controls output size, frame rate, and bitrate. ```kotlin highlight-android-export-video val editedVideo = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { progress -> println( "Rendered ${progress.renderedFrames} of ${progress.totalFrames} frames", ) }, options = ExportVideoOptions( videoBitrate = 8_000_000, audioBitrate = 128_000, frameRate = 30F, targetWidth = 1280F, targetHeight = 720F, ), ) check(editedVideo.remaining() > 0) ``` The backing sample asserts that the returned `ByteBuffer` is non-empty so the automated check verifies a real export result. `ExportVideoOptions` exposes these public fields: | Field | Purpose | | --- | --- | | `h264Profile` | Selects the H.264 encoder profile. | | `h264Level` | Selects the H.264 encoder level, for example `52` for level 5.2. | | `videoBitrate` | Sets video bitrate in bits per second, or `0` for automatic selection. | | `audioBitrate` | Sets audio bitrate in bits per second, or `0` for automatic selection. | | `frameRate` | Sets the target export frame rate in Hz. | | `targetWidth` | Sets the target output width when used with `targetHeight`. | | `targetHeight` | Sets the target output height when used with `targetWidth`. | | `allowTextOverhang` | Includes glyph overhang bounds to avoid clipping text during export. | ## API Reference | Android API | Purpose | | --- | --- | | `engine.scene.createForVideo()` | Create a scene in video mode. | | `engine.block.create(blockType=_)` | Create pages, tracks, graphics, audio-only timeline blocks, and other blocks. | | `engine.block.createFill(fillType=_)` | Create video or color fills. | | `engine.block.createShape(type=_)` | Create a shape for a graphic block. | | `engine.block.appendChild(parent=_, child=_)` | Add pages, tracks, clips, and overlays to the hierarchy. | | `engine.block.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.setDuration(block=_, duration=_)` | Set page or block 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 - [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 - [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 - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Learn export workflows before baking redactions into final media - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Control timing, duration, and playback for media 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: "Transform Videos" description: "Learn how Android video transforms use block geometry, crop transforms, groups, animations, and transform permissions." platform: android url: "https://img.ly/docs/cesdk/android/edit-video/transform-369f28/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) --- ```kotlin file=@cesdk_android_examples/engine-guides-video-transform/VideoTransform.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import ly.img.engine.AnimationType import ly.img.engine.ContentFillMode import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.GlobalScope import ly.img.engine.PositionMode import ly.img.engine.ShapeType import kotlin.math.PI import kotlin.math.abs fun transformVideo( license: String?, userId: String, ): Job = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example.video-transform") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1280, height = 720) try { val sampleVideoUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4") val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) engine.block.setWidth(page, value = 1280F) engine.block.setHeight(page, value = 720F) engine.block.setDuration(page, duration = 8.0) val positionedVideo = engine.block.create(DesignBlockType.Graphic) val positionedVideoFill = engine.block.createFill(FillType.Video) engine.block.setName(positionedVideo, "Positioned video") engine.block.setShape(positionedVideo, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(positionedVideo, value = 80F) engine.block.setPositionY(positionedVideo, value = 80F) engine.block.setWidth(positionedVideo, value = 300F) engine.block.setHeight(positionedVideo, value = 170F) // Android does not expose a typed setter for the video fill URI property. engine.block.setUri(block = positionedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri) engine.block.setFill(block = positionedVideo, fill = positionedVideoFill) engine.block.setContentFillMode(block = positionedVideo, mode = ContentFillMode.COVER) engine.block.setDuration(block = positionedVideo, duration = 8.0) engine.block.appendChild(parent = page, child = positionedVideo) val rotatedVideo = engine.block.create(DesignBlockType.Graphic) val rotatedVideoFill = engine.block.createFill(FillType.Video) engine.block.setName(rotatedVideo, "Rotated video") engine.block.setShape(rotatedVideo, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(rotatedVideo, value = 460F) engine.block.setPositionY(rotatedVideo, value = 80F) engine.block.setWidth(rotatedVideo, value = 300F) engine.block.setHeight(rotatedVideo, value = 170F) // Android does not expose a typed setter for the video fill URI property. engine.block.setUri(block = rotatedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri) engine.block.setFill(block = rotatedVideo, fill = rotatedVideoFill) engine.block.setContentFillMode(block = rotatedVideo, mode = ContentFillMode.COVER) engine.block.setDuration(block = rotatedVideo, duration = 8.0) engine.block.appendChild(parent = page, child = rotatedVideo) val croppedVideo = engine.block.create(DesignBlockType.Graphic) val croppedVideoFill = engine.block.createFill(FillType.Video) engine.block.setName(croppedVideo, "Cropped video") engine.block.setShape(croppedVideo, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(croppedVideo, value = 80F) engine.block.setPositionY(croppedVideo, value = 360F) engine.block.setWidth(croppedVideo, value = 300F) engine.block.setHeight(croppedVideo, value = 170F) // Android does not expose a typed setter for the video fill URI property. engine.block.setUri(block = croppedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri) engine.block.setFill(block = croppedVideo, fill = croppedVideoFill) engine.block.setContentFillMode(block = croppedVideo, mode = ContentFillMode.COVER) engine.block.setDuration(block = croppedVideo, duration = 8.0) engine.block.appendChild(parent = page, child = croppedVideo) val lockedVideo = engine.block.create(DesignBlockType.Graphic) val lockedVideoFill = engine.block.createFill(FillType.Video) engine.block.setName(lockedVideo, "Locked video") engine.block.setShape(lockedVideo, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(lockedVideo, value = 460F) engine.block.setPositionY(lockedVideo, value = 360F) engine.block.setWidth(lockedVideo, value = 300F) engine.block.setHeight(lockedVideo, value = 170F) // Android does not expose a typed setter for the video fill URI property. engine.block.setUri(block = lockedVideoFill, property = "fill/video/fileURI", value = sampleVideoUri) engine.block.setFill(block = lockedVideo, fill = lockedVideoFill) engine.block.setContentFillMode(block = lockedVideo, mode = ContentFillMode.COVER) engine.block.setDuration(block = lockedVideo, duration = 8.0) engine.block.appendChild(parent = page, child = lockedVideo) engine.block.setPositionXMode(positionedVideo, mode = PositionMode.ABSOLUTE) engine.block.setPositionYMode(positionedVideo, mode = PositionMode.ABSOLUTE) engine.block.setPositionX(positionedVideo, value = 120F) engine.block.setPositionY(positionedVideo, value = 104F) engine.block.setFlipHorizontal(rotatedVideo, flip = true) engine.block.scale(rotatedVideo, scale = 1.15F, anchorX = 0.5F, anchorY = 0.5F) engine.block.setRotation(rotatedVideo, radians = (PI / 8.0).toFloat()) engine.block.setWidth(lockedVideo, value = 280F, maintainCrop = true) engine.block.setHeight(lockedVideo, value = 158F, maintainCrop = true) if (engine.block.supportsCrop(croppedVideo)) { engine.block.setContentFillMode(croppedVideo, mode = ContentFillMode.CROP) engine.block.setCropScaleRatio(croppedVideo, scaleRatio = 1.35F) // Crop translations are relative to the block frame dimensions. engine.block.setCropTranslationX(croppedVideo, translationX = -0.12F) engine.block.setCropTranslationY(croppedVideo, translationY = 0.08F) engine.block.setCropRotation(croppedVideo, rotation = (PI / 18.0).toFloat()) engine.block.adjustCropToFillFrame(croppedVideo, minScaleRatio = 1.0F) } engine.editor.setSettingBoolean("controlGizmo/showMoveHandles", true) engine.editor.setSettingBoolean("controlGizmo/showResizeHandles", true) engine.editor.setSettingBoolean("controlGizmo/showScaleHandles", true) engine.editor.setSettingBoolean("controlGizmo/showRotateHandles", true) engine.editor.setSettingBoolean("controlGizmo/showCropHandles", true) engine.editor.setSettingFloat("controlGizmo/blockScaleDownLimit", 12F) engine.editor.setSettingEnum("touch/rotateAction", "Rotate") engine.editor.setSettingEnum("touch/pinchAction", "Scale") if (engine.block.isGroupable(listOf(positionedVideo, croppedVideo))) { val group = engine.block.group(listOf(positionedVideo, croppedVideo)) engine.block.setPositionX(group, value = 180F) engine.block.setRotation(group, radians = (PI / 16.0).toFloat()) } if (engine.block.supportsAnimation(rotatedVideo)) { val loopAnimation = engine.block.createAnimation(AnimationType.SpinLoop) engine.block.setLoopAnimation(rotatedVideo, loopAnimation) engine.block.setDuration(loopAnimation, duration = 2.0) engine.block.setTimeOffset(rotatedVideo, offset = 1.0) } val blockFrameScopes = listOf("layer/move", "layer/rotate", "layer/resize", "layer/flip") (blockFrameScopes + "layer/crop").forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DEFER) } blockFrameScopes.forEach { scope -> engine.block.setScopeEnabled(lockedVideo, key = scope, enabled = false) } engine.block.setScopeEnabled(lockedVideo, key = "layer/crop", enabled = false) engine.block.setTransformLocked(lockedVideo, locked = true) val transformsLocked = engine.block.isTransformLocked(lockedVideo) val moveScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/move") val moveAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/move") val cropScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/crop") val cropAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/crop") check(abs(engine.block.getRotation(rotatedVideo) - (PI / 8.0).toFloat()) < 0.001F) check(engine.block.isFlipHorizontal(rotatedVideo)) check(transformsLocked) check(!moveScopeEnabled) check(!moveAllowed) check(!cropScopeEnabled) check(!cropAllowed) check(engine.editor.getSettingBoolean("controlGizmo/showRotateHandles")) check(abs(engine.editor.getSettingFloat("controlGizmo/blockScaleDownLimit") - 12F) < 0.001F) check(engine.editor.getSettingEnum("touch/pinchAction") == "Scale") } finally { engine.stop() } } ``` Transform video blocks by moving, rotating, scaling, cropping, grouping, and locking them in CE.SDK for Android. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-video-transform) Video transformations affect either the block frame or the media inside that frame. Block-level transforms change the graphic block's position, rotation, flip state, size, and group placement. Content-level crop transforms reframe the video fill without moving the block itself. | Transform layer | Use it for | Main APIs | | --- | --- | --- | | Block | Move, rotate, flip, scale, and resize the video block in the scene. | `setPositionX()`, `setRotation()`, `setFlipHorizontal()`, `scale()`, `setWidth()` | | Content | Pan, zoom, or rotate the video inside the block frame. | `setCropScaleRatio()`, `setCropTranslationX()`, `setCropRotation()`, `adjustCropToFillFrame()` | | Permissions | Restrict end-user edits, crop reframing, or block-frame geometry changes. | `setScopeEnabled()`, `setTransformLocked()` | The Kotlin snippets below assume the engine is running on the main thread and that each named variable refers to a video graphic block in the current scene. ## Apply Block Transforms Block transforms affect the whole graphic block. Use absolute or percentage position modes for placement, radians for rotation, booleans for flip states, and an anchor point when scaling. ```kotlin highlight-android-block-transforms engine.block.setPositionXMode(positionedVideo, mode = PositionMode.ABSOLUTE) engine.block.setPositionYMode(positionedVideo, mode = PositionMode.ABSOLUTE) engine.block.setPositionX(positionedVideo, value = 120F) engine.block.setPositionY(positionedVideo, value = 104F) engine.block.setFlipHorizontal(rotatedVideo, flip = true) engine.block.scale(rotatedVideo, scale = 1.15F, anchorX = 0.5F, anchorY = 0.5F) engine.block.setRotation(rotatedVideo, radians = (PI / 8.0).toFloat()) engine.block.setWidth(lockedVideo, value = 280F, maintainCrop = true) engine.block.setHeight(lockedVideo, value = 158F, maintainCrop = true) ``` Use `maintainCrop = true` when resizing a video block and you want CE.SDK to adjust crop values so the visible content remains framed. ## Adjust the Video Inside the Frame Crop transforms change how the video fill appears inside the block frame. They do not change the block's own position or dimensions. ```kotlin highlight-android-content-transforms if (engine.block.supportsCrop(croppedVideo)) { engine.block.setContentFillMode(croppedVideo, mode = ContentFillMode.CROP) engine.block.setCropScaleRatio(croppedVideo, scaleRatio = 1.35F) // Crop translations are relative to the block frame dimensions. engine.block.setCropTranslationX(croppedVideo, translationX = -0.12F) engine.block.setCropTranslationY(croppedVideo, translationY = 0.08F) engine.block.setCropRotation(croppedVideo, rotation = (PI / 18.0).toFloat()) engine.block.adjustCropToFillFrame(croppedVideo, minScaleRatio = 1.0F) } ``` Crop translations are scaled offsets relative to the block frame, not pixel or design-unit positions. A `translationX` value of `-0.12` shifts the video content left by 12% of the frame width, while a `translationY` value of `0.08` shifts it down by 8% of the frame height. Positive X moves content right, and positive Y moves content down. Check `supportsCrop()` before applying crop transforms if your code handles mixed block types. `adjustCropToFillFrame()` prevents empty frame areas after crop scale, translation, or rotation changes. ## Configure Transform Controls The built-in editor UI reads transform settings from the engine. Configure visible handles and touch behavior when your app needs to expose or hide direct manipulation. ```kotlin highlight-android-transform-controls engine.editor.setSettingBoolean("controlGizmo/showMoveHandles", true) engine.editor.setSettingBoolean("controlGizmo/showResizeHandles", true) engine.editor.setSettingBoolean("controlGizmo/showScaleHandles", true) engine.editor.setSettingBoolean("controlGizmo/showRotateHandles", true) engine.editor.setSettingBoolean("controlGizmo/showCropHandles", true) engine.editor.setSettingFloat("controlGizmo/blockScaleDownLimit", 12F) engine.editor.setSettingEnum("touch/rotateAction", "Rotate") engine.editor.setSettingEnum("touch/pinchAction", "Scale") ``` These settings affect the built-in editor interaction layer. `controlGizmo/blockScaleDownLimit` uses screen pixels; the value above keeps scaled blocks at least 12 screen pixels wide and high. Programmatic transform calls use the block APIs. Scope checks for those APIs are disabled by default unless `debug/enforceScopesInAPIs` is enabled, while block-frame transform locks are enforced separately by block-frame transform APIs. ## Transform Groups Group multiple blocks when they should move or rotate together while preserving their relative placement. ```kotlin highlight-android-group-transforms if (engine.block.isGroupable(listOf(positionedVideo, croppedVideo))) { val group = engine.block.group(listOf(positionedVideo, croppedVideo)) engine.block.setPositionX(group, value = 180F) engine.block.setRotation(group, radians = (PI / 16.0).toFloat()) } ``` Call `isGroupable()` before grouping arbitrary selections. A group receives its own block ID, so you can transform the group with the same block APIs used for single blocks. ## Animate and Time Transforms Video scenes can combine static transforms with block animations and timeline placement. Attach animation blocks to the transformed video block and set the block's time offset when it should appear later in the page timeline. ```kotlin highlight-android-animated-transforms if (engine.block.supportsAnimation(rotatedVideo)) { val loopAnimation = engine.block.createAnimation(AnimationType.SpinLoop) engine.block.setLoopAnimation(rotatedVideo, loopAnimation) engine.block.setDuration(loopAnimation, duration = 2.0) engine.block.setTimeOffset(rotatedVideo, offset = 1.0) } ``` Use the dedicated animation guides when you need keyframe-style motion, easing, or detailed animation property configuration. ## Restrict Transform Changes For templates, disable individual transform scopes and use the transform lock for block-frame geometry. Block-level scope flags affect editor interactions when the matching global scope is set to `GlobalScope.DEFER`; use `isAllowedByScope()` to validate the effective editor permission. ```kotlin highlight-android-lock-transforms val blockFrameScopes = listOf("layer/move", "layer/rotate", "layer/resize", "layer/flip") (blockFrameScopes + "layer/crop").forEach { scope -> engine.editor.setGlobalScope(key = scope, globalScope = GlobalScope.DEFER) } blockFrameScopes.forEach { scope -> engine.block.setScopeEnabled(lockedVideo, key = scope, enabled = false) } engine.block.setScopeEnabled(lockedVideo, key = "layer/crop", enabled = false) engine.block.setTransformLocked(lockedVideo, locked = true) val transformsLocked = engine.block.isTransformLocked(lockedVideo) val moveScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/move") val moveAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/move") val cropScopeEnabled = engine.block.isScopeEnabled(lockedVideo, key = "layer/crop") val cropAllowed = engine.block.isAllowedByScope(lockedVideo, key = "layer/crop") ``` Use individual scopes when one operation should remain available, such as allowing crop but preventing movement. Disable `layer/crop` when crop or content reframing must stay fixed. `setTransformLocked()` protects block-frame geometry such as moving, rotating, flipping, scaling, and resizing; crop setters use the `layer/crop` permission path instead. `isScopeEnabled()` reads only the block-level flag, while `isAllowedByScope()` combines the global and block-level scope state. Android API calls enforce scope checks only when `debug/enforceScopesInAPIs` is enabled. ## Troubleshooting | Issue | Check | | --- | --- | | A block does not move or rotate | For editor interactions, confirm the block is valid and `isAllowedByScope()` returns true for the needed scope. For Block API calls, also check whether `debug/enforceScopesInAPIs` is enabled and `isTransformLocked()` is false. | | Position values look wrong | Verify whether the block uses `PositionMode.ABSOLUTE` or `PositionMode.PERCENT`. | | Rotation uses the wrong angle | Pass radians, not degrees. | | Crop leaves empty frame areas | Call `adjustCropToFillFrame()` after changing crop scale, translation, or rotation. | | UI handles are missing | Check `controlGizmo/*` settings and ensure the selected block supports the requested operation. | ## API Reference | API | Purpose | | --- | --- | | `engine.block.setPositionX(block=_,value=_)` | Set the block's x position relative to its parent. | | `engine.block.setPositionY(block=_,value=_)` | Set the block's y position relative to its parent. | | `engine.block.setPositionXMode(block=_,mode=_)` | Choose absolute or percentage x positioning. | | `engine.block.setPositionYMode(block=_,mode=_)` | Choose absolute or percentage y positioning. | | `engine.block.setRotation(block=_,radians=_)` | Rotate the block around its center. | | `engine.block.setFlipHorizontal(block=_,flip=_)` | Mirror the block horizontally. | | `engine.block.setFlipVertical(block=_,flip=_)` | Mirror the block vertically. | | `engine.block.scale(block=_,scale=_,anchorX=_,anchorY=_)` | Scale the block around a normalized anchor point. | | `engine.block.setWidth(block=_,value=_,maintainCrop=_)` | Resize the block width and optionally preserve crop framing. | | `engine.block.setHeight(block=_,value=_,maintainCrop=_)` | Resize the block height and optionally preserve crop framing. | | `engine.block.setDuration(block=_,duration=_)` | Set how long a page, video block, or animation block participates in the video timeline. | | `engine.block.setContentFillMode(block=_,mode=_)` | Choose `ContentFillMode.CROP`, `ContentFillMode.COVER`, or `ContentFillMode.CONTAIN` for the block's content. | | `engine.block.getContentFillMode(block=_)` | Read the block's current content fill mode. | | `engine.block.supportsCrop(block=_)` | Check whether the block supports crop transforms before applying crop values. | | `engine.block.setCropScaleX(block=_,scaleX=_)` | Scale the video content horizontally inside the frame. | | `engine.block.setCropScaleY(block=_,scaleY=_)` | Scale the video content vertically inside the frame. | | `engine.block.setCropScaleRatio(block=_,scaleRatio=_)` | Uniformly scale the video content inside the frame. | | `engine.block.setCropTranslationX(block=_,translationX=_)` | Set the relative horizontal crop offset; `1.0` equals one frame width, positive values move content right. | | `engine.block.setCropTranslationY(block=_,translationY=_)` | Set the relative vertical crop offset; `1.0` equals one frame height, positive values move content down. | | `engine.block.setCropRotation(block=_,rotation=_)` | Rotate the video content inside the block frame. | | `engine.block.adjustCropToFillFrame(block=_,minScaleRatio=_)` | Adjust crop values so the content fills the block frame. | | `engine.block.flipCropHorizontal(block=_)` | Flip the cropped content along its horizontal axis. | | `engine.block.flipCropVertical(block=_)` | Flip the cropped content along its vertical axis. | | `engine.block.setCropAspectRatioLocked(block=_,locked=_)` | Keep crop handles constrained to the current aspect ratio. | | `engine.block.resetCrop(block=_)` | Reset manual crop values and return the content fill mode to cover. | | `engine.block.isGroupable(blocks=_)` | Check whether selected blocks can be grouped. | | `engine.block.group(blocks=_)` | Create a group that can be transformed as one block. | | `engine.block.supportsAnimation(block=_)` | Check whether a block can receive animations. | | `engine.block.createAnimation(type=_)` | Create an animation block such as `AnimationType.SpinLoop`. | | `engine.block.setInAnimation(block=_,animation=_)` | Attach an entry animation to a block. | | `engine.block.setLoopAnimation(block=_,animation=_)` | Attach a looping animation to a block. | | `engine.block.setOutAnimation(block=_,animation=_)` | Attach an exit animation to a block. | | `engine.block.setTimeOffset(block=_,offset=_)` | Place the block later in its parent timeline, in seconds. | | `engine.editor.setGlobalScope(key=_,globalScope=_)` | Set a global scope to `GlobalScope.ALLOW`, `GlobalScope.DENY`, or `GlobalScope.DEFER`. | | `engine.block.setScopeEnabled(block=_,key=_,enabled=_)` | Enable or disable block-level transform scopes such as `"layer/move"`, `"layer/rotate"`, `"layer/flip"`, `"layer/resize"`, and `"layer/crop"`. | | `engine.block.isScopeEnabled(block=_,key=_)` | Check only the block-level scope flag. | | `engine.block.isAllowedByScope(block=_,key=_)` | Check the effective permission after global scope and block-level scope state are combined. | | `engine.editor.setSettingBoolean(keypath="debug/enforceScopesInAPIs",value=_)` | Enable or disable scope checks inside editing APIs; disabled by default. | | `engine.block.setTransformLocked(block=_,locked=_)` | Lock or unlock block-frame geometry transforms such as movement, rotation, flip, scale, and resize. | | `engine.block.isTransformLocked(block=_)` | Check whether block-frame geometry transforms are locked for a block. | | `engine.block.getPositionXMode(block=_)` | Read whether x positioning uses absolute or percentage values. | | `engine.block.getPositionYMode(block=_)` | Read whether y positioning uses absolute or percentage values. | | `engine.block.getRotation(block=_)` | Read the block's rotation in radians. | | `engine.block.isFlipHorizontal(block=_)` | Check whether the block is mirrored horizontally. | | `engine.block.isFlipVertical(block=_)` | Check whether the block is mirrored vertically. | | `engine.block.getCropScaleRatio(block=_)` | Read the uniform crop scale ratio. | | `engine.block.getCropTranslationX(block=_)` | Read the relative horizontal crop offset. | | `engine.block.getCropTranslationY(block=_)` | Read the relative vertical crop offset. | | `engine.block.getCropRotation(block=_)` | Read the crop rotation in radians. | | `engine.editor.setSettingBoolean(keypath="controlGizmo/showMoveHandles",value=_)` | Show or hide the move handles in the editor UI. | | `engine.editor.setSettingBoolean(keypath="controlGizmo/showResizeHandles",value=_)` | Show or hide the edge resize handles in the editor UI. | | `engine.editor.setSettingBoolean(keypath="controlGizmo/showScaleHandles",value=_)` | Show or hide the corner scale handles in the editor UI. | | `engine.editor.setSettingBoolean(keypath="controlGizmo/showRotateHandles",value=_)` | Show or hide the rotation handles in the editor UI. | | `engine.editor.setSettingBoolean(keypath="controlGizmo/showCropHandles",value=_)` | Show or hide the crop handles in the editor UI. | | `engine.editor.setSettingFloat(keypath="controlGizmo/blockScaleDownLimit",value=_)` | Set the minimum on-screen block size while users scale blocks in the editor UI. | | `engine.editor.setSettingEnum(keypath="touch/rotateAction",value=_)` | Choose how rotation touch gestures map to transform behavior. | | `engine.editor.setSettingEnum(keypath="touch/pinchAction",value=_)` | Choose how a touch gesture maps to transform behavior. | ## Next Steps - [Move](https://img.ly/docs/cesdk/android/edit-video/transform/move-aa9d89/) - Position video blocks with absolute or percentage coordinates. - [Rotate](https://img.ly/docs/cesdk/android/edit-video/transform/rotate-eaf662/) - Rotate video blocks with radians. - [Flip](https://img.ly/docs/cesdk/android/edit-video/transform/flip-a603b0/) - Mirror video blocks horizontally or vertically. - [Scale](https://img.ly/docs/cesdk/android/edit-video/transform/scale-f75c8a/) - Scale video blocks around an anchor point. - [Crop](https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/) - Reframe video content inside a block. - [Resize](https://img.ly/docs/cesdk/android/edit-video/transform/resize-b1ce14/) - Change video block dimensions while managing crop behavior. --- ## Related Pages - [Move](https://img.ly/docs/cesdk/android/edit-video/transform/move-aa9d89/) - Position a video relative to its parent using either percentage or units - [Crop Video in Android](https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/) - Cut out specific areas of a video to focus on key content or change aspect ratio - [Rotate](https://img.ly/docs/cesdk/android/edit-video/transform/rotate-eaf662/) - Documentation for Rotate - [Resize](https://img.ly/docs/cesdk/android/edit-video/transform/resize-b1ce14/) - Change the size of individual elements or groups. - [Scale](https://img.ly/docs/cesdk/android/edit-video/transform/scale-f75c8a/) - Scale videos uniformly in your Android app. - [Flip Videos](https://img.ly/docs/cesdk/android/edit-video/transform/flip-a603b0/) - Flip videos horizontally or vertically. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Crop Video in Android" description: "Cut out specific areas of a video to focus on key content or change aspect ratio" platform: android url: "https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/android/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/android/edit-video/transform-369f28/) > [Crop](https://img.ly/docs/cesdk/android/edit-video/transform/crop-8b1741/) --- Video cropping is essential for Android video editing apps, enabling users to focus on important content and optimize videos for different platforms. The CreativeEditor SDK provides both intuitive crop interfaces and powerful Kotlin APIs for video manipulation. Whether you're targeting TikTok, Instagram, or YouTube formats, this guide covers all video cropping scenarios for your Android application. ## Interactive video crop tools The SDK includes specialized video crop controls designed for Android touch interfaces. These components handle real-time video preview, aspect ratio selection, and provide smooth crop adjustments that maintain video quality during editing. ![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 - [Control Audio and Video](https://img.ly/docs/cesdk/android/create-video/control-daba54/) - Master playback controls, volume, and muting. - [Timeline Editor](https://img.ly/docs/cesdk/android/create-video/timeline-editor-912252/) - Understand the complete timeline editing model. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Engine Interface" description: "Initialize and manage the Android Engine lifecycle for headless and UI-based integrations." platform: android url: "https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Engine](https://img.ly/docs/cesdk/android/engine-interface-6fb7cf/) --- ```kotlin file=@cesdk_android_examples/engine-guides-setup/MyApplication.kt reference-only import android.app.Application import ly.img.engine.Engine class MyApplication : Application() { override fun onCreate() { super.onCreate() Engine.init(application = this) } } ``` ```kotlin file=@cesdk_android_examples/engine-guides-setup/MyActivity.kt reference-only import android.net.Uri import android.os.Bundle import android.view.SurfaceHolder import android.view.SurfaceView import android.view.TextureView import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import ly.img.engine.Engine class MyActivity : ComponentActivity() { private val engine = Engine.getInstance(id = "ly.img.engine.example") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textureView = TextureView(this) setContentView(textureView) lifecycleScope.launch { engine.start( license = null, // pass null or empty for evaluation mode with watermark userId = "", savedStateRegistryOwner = this@MyActivity, ) bindTextureView(textureView) loadScene() } } override fun onDestroy() { engine.stop() super.onDestroy() } private fun bindTextureView(textureView: TextureView) { engine.bindTextureView(textureView) } private suspend fun loadScene() { // Check whether a scene already exists before loading it again as it might have been restored in engine.start. engine.scene.get() ?: run { val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") engine.scene.load(sceneUri) } } private fun bindSurfaceView() { val surfaceView = SurfaceView(this) setContentView(surfaceView) engine.bindSurfaceView(surfaceView) } private fun bindSurfaceHolder(surfaceHolder: SurfaceHolder) { engine.bindSurfaceHolder(surfaceHolder) } private fun bindOffscreen() { engine.bindOffscreen(width = 100, height = 100) } } ``` ```kotlin file=@cesdk_android_examples/engine-guides-setup/MyComposable.kt reference-only import android.net.Uri import android.view.SurfaceView import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSavedStateRegistryOwner import androidx.compose.ui.viewinterop.AndroidView import ly.img.engine.Engine @Composable fun MyComposable() { val engine = remember { Engine.getInstance(id = "ly.img.engine.example") } val context = LocalContext.current val surfaceView = remember { SurfaceView(context) } val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current AndroidView(factory = { surfaceView }) LaunchedEffect(Unit) { engine.start( license = null, // pass null or empty for evaluation mode with watermark userId = "", savedStateRegistryOwner = savedStateRegistryOwner, ) engine.bindSurfaceView(surfaceView) engine.scene.get() ?: run { val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") engine.scene.load(sceneUri) } } DisposableEffect(Unit) { onDispose { engine.stop() } } } ``` The Android Engine is the programmatic entry point behind CE.SDK. You can use it through the prebuilt editor UI, where the editor setup manages initialization, or create and bind an Engine yourself for headless rendering, custom UI, and automation. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-android-examples/tree/v$UBQ_VERSION$/engine-guides-setup) ## Choose the Integration Mode Most Android integrations fall into one of two modes: | Mode | Who initializes the Engine? | When to use it | | --- | --- | --- | | Prebuilt editor UI | The editor setup initializes the Engine for you. | You use the CE.SDK editor UI or a Starter Kit and access the Engine from editor callbacks or configuration hooks. | | Headless or custom Engine | Your app initializes and starts the Engine. | You render offscreen, generate assets, validate scenes, build custom UI, or run automation without showing the editor. | The lifecycle below applies only when you create and start an Engine yourself. If you use the prebuilt editor or a Starter Kit, access the Engine through the editor's callbacks instead because it handles initialization for you. ## Initialize the Engine Before any Engine instance can start, call `Engine.init(application)` from your `Application.onCreate()` method. ```kotlin highlight-android-application-init class MyApplication : Application() { override fun onCreate() { super.onCreate() Engine.init(application = this) } } ``` This call stores the application context and prepares the Engine's internal directories. It must run on the main thread and should happen once during app startup. ## Create and Start an Engine Create an Engine instance with a stable ID, then start it on the main thread with your license and user ID. The sample uses an Activity lifecycle coroutine, but any main-thread coroutine works. Render binding and scene work should run after `engine.start(...)` completes. ```kotlin highlight-android-activity-engine private val engine = Engine.getInstance(id = "ly.img.engine.example") ``` ```kotlin highlight-android-activity-start lifecycleScope.launch { engine.start( license = null, // pass null or empty for evaluation mode with watermark userId = "", savedStateRegistryOwner = this@MyActivity, ) bindTextureView(textureView) loadScene() } ``` The `id` controls Engine instance reuse: calling `Engine.getInstance(id=_)` with the same ID returns the same instance. `engine.start(...)` returns `false` if that instance is already running, so a new `savedStateRegistryOwner` is registered only when the Engine starts again. To avoid loading the same scene repeatedly after state restoration, check `engine.scene.get()` after `engine.start(...)` and only load a scene when none was restored. ## Bind a Render Target After `engine.start(...)` completes, bind the Engine to the render target that matches your integration: ```kotlin highlight-android-activity-bind-texture-view engine.bindTextureView(textureView) ``` ```kotlin highlight-android-activity-bind-surface-view val surfaceView = SurfaceView(this) setContentView(surfaceView) engine.bindSurfaceView(surfaceView) ``` ```kotlin highlight-android-activity-bind-surface-holder engine.bindSurfaceHolder(surfaceHolder) ``` ```kotlin highlight-android-activity-bind-offscreen engine.bindOffscreen(width = 100, height = 100) ``` Use a visible `TextureView` or `SurfaceView` for a custom Android UI. Use `bindSurfaceHolder(surfaceHolder=_)` when your UI already owns a raw `SurfaceHolder`, such as a custom player view or third-party wrapper. Use `bindOffscreen(width=_, height=_)` for headless rendering where no view is displayed. Only one Engine can be bound to a render target at a time. Binding one Engine unbinds other running UI Engines, so keep background and foreground rendering flows explicit. ## Work With the Scene After the Engine is started and bound, load or create a scene before calling APIs that need scene content. When you pass a `SavedStateRegistryOwner` to `engine.start(...)`, the Engine can restore a saved scene after process recreation. Check whether a scene already exists before loading a new one. ```kotlin highlight-android-activity-load-scene // Check whether a scene already exists before loading it again as it might have been restored in engine.start. engine.scene.get() ?: run { val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") engine.scene.load(sceneUri) } ``` ## Clean Up the Engine Call `engine.stop()` when the standalone host that owns the `savedStateRegistryOwner` is destroyed. In this sample, that includes Activity recreation so the next Activity can start the named Engine and register its new saved-state owner. App architectures that keep one Engine running across configuration changes should manage restoration outside this snippet instead of passing a new owner to an already-running instance. ```kotlin highlight-android-activity-cleanup override fun onDestroy() { engine.stop() super.onDestroy() } ``` Use `engine.stop()` for lifecycle cleanup: it unbinds the current render target, releases runtime resources, unregisters saved-state handling, and clears the current scene. Use `engine.unbind()` only when the Engine should keep running while you detach it from one render target and bind it to another. ## Compose Lifecycle Compose integrations follow the same lifecycle: remember the Engine instance, start it in a `LaunchedEffect`, bind a `SurfaceView`, work with the scene after the start call completes, and stop it from `DisposableEffect.onDispose` when this Composable owns the Engine lifecycle. ```kotlin highlight-android-compose-engine val engine = remember { Engine.getInstance(id = "ly.img.engine.example") } ``` Set up the `SurfaceView` and saved-state owner before the start coroutine uses them. ```kotlin highlight-android-compose-render-target val context = LocalContext.current val surfaceView = remember { SurfaceView(context) } val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current AndroidView(factory = { surfaceView }) ``` ```kotlin highlight-android-compose-start LaunchedEffect(Unit) { engine.start( license = null, // pass null or empty for evaluation mode with watermark userId = "", savedStateRegistryOwner = savedStateRegistryOwner, ) engine.bindSurfaceView(surfaceView) engine.scene.get() ?: run { val sceneUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") engine.scene.load(sceneUri) } } ``` ```kotlin highlight-android-compose-cleanup DisposableEffect(Unit) { onDispose { engine.stop() } } ``` Use this pattern when you build a custom UI around the Engine. If you use the prebuilt editor UI, prefer the editor or Starter Kit lifecycle hooks instead of creating a separate Engine in the Composable. ## Troubleshooting **Cannot start the Engine before calling Engine.init(applicationContext)**: Call `Engine.init(application)` once from `Application.onCreate()` before calling `Engine.getInstance(...)` or `engine.start(...)`. **Engine APIs fail from the wrong thread**: Call Engine APIs from the Engine's own dispatcher. Public Engine instances created with `Engine.getInstance(...)` run on the main thread. Export callbacks such as `onPreExport` run on a separate background Engine thread, so configure that Engine inside the provided callback. **No frames render in a custom render loop**: Ensure the Engine is bound before rendering frames. Headless frame rendering needs `engine.bindOffscreen(width=_, height=_)`; custom UI flows need a visible `SurfaceView` or `TextureView`. **The export output is empty**: Ensure the scene contains renderable blocks, required resources are reachable, and export options target the block you expect. `engine.block.export(...)` prepares its own background export Engine, so `bindOffscreen(...)` on the main Engine is not the fix for empty export output. **A scene loads twice after rotation or process recreation**: If you pass a `SavedStateRegistryOwner` to `engine.start(...)`, check `engine.scene.get()` before loading a new scene. ## API Reference | Method | Purpose | | --- | --- | | `Engine.init(application=_)` | Initialize the Engine before any instance starts. | | `Engine.getInstance(id=_, audioContext=_)` | Get or create a named Engine instance. Pass `AudioContext.NONE` to disable audio playback for the first instance with that ID, or use `AudioContext.AUTO` for the default audio-capable context. | | `engine.start(license=_, userId=_, savedStateRegistryOwner=_)` | Start the Engine and optionally wire scene state restoration. | | `engine.bindTextureView(textureView=_)` | Render into a `TextureView`. | | `engine.bindSurfaceView(surfaceView=_)` | Render into a `SurfaceView`. | | `engine.bindSurfaceHolder(surfaceHolder=_)` | Render into a raw `SurfaceHolder`. | | `engine.bindOffscreen(width=_, height=_)` | Render without a visible UI surface. | | `engine.unbind()` | Detach the current render target. | | `engine.isEngineRunning()` | Check whether `start()` has completed and `stop()` has not been called. | | `engine.stop()` | Stop the Engine and release its runtime resources. | ## Next Steps - [Headless Mode](https://img.ly/docs/cesdk/android/concepts/headless-mode-24ab98/) - Use the Engine directly without the prebuilt UI. - [Batch Processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/) - Process multiple designs. - [Data Merge](https://img.ly/docs/cesdk/android/automation/data-merge-ae087c/) - Personalize templates with external data. - [Export](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) - Explore export options and formats. - [Node.js SDK](https://img.ly/docs/cesdk/android/what-is-cesdk-2e7acd/) - Use the server-side Engine package for backend processing. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Thumbnail in Android (Kotlin)" description: "Generate small preview images for scenes and pages using CE.SDK export options in Kotlin." 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/) --- Thumbnails are scaled down previews of your designs. They let you show galleries or document picker previews without loading the full editor. CE.SDK generates thumbnails using the same API you use for final output, just with smaller target dimensions and often lower quality settings. This guide focuses on **image thumbnails**: small PNG, JPEG or WebP previews for use in grids, lists and document icons. It **doesn't cover audio waveforms** or arrays of **preview frames** for scrubbers. ## What You'll Learn - How to export a scene or page as a small preview image. - How to control thumbnail dimensions while preserving aspect ratio. - When to choose PNG, JPEG, or WebP for thumbnails. - How to tune quality and file size using `ExportOptions`. - How to batch-generate different thumbnail sizes safely. ## When You'll Use It - Showing a "My Designs" or "Recent Files" gallery. - Rendering previews for templates or drafts. - Generating document icons or share sheet previews. - Creating thumbnail sizes for different UI contexts. ## How Thumbnail Export Works In CE.SDK, `export` means *rendering bitmap image data from the engine*. When you call `export`, the SDK: 1. Renders the current visual state of a block (for example, a page). 2. Composites all **visible** child blocks (images, text, shapes, effects). 3. Produces raw bitmap image data in the requested format (PNG, JPEG, or WebP). Exporting **doesn't** imply writing a file to disk. The result of the export call is an in-memory `ByteBuffer` that you can: - Convert to a `Bitmap`. - Cache in memory. - Write to disk if needed. - Upload elsewhere. CE.SDK doesn't provide a separate "thumbnail API". If you build your own UI, you call `engine.block.export(...)` directly whenever you want. If you use the **prebuilt editor UI** (the default CE.SDK editors), there *is* a convenient hook for the built-in Export/Share button: the editor exposes an `DesignEditor` configuration. The default export: 1. Renders (PDF for design scenes, MP4 for video scenes). 2. Writes the result to a temporary file 3. Opens the system share sheet. That hook is great for customizing what happens when the user taps Export in the prebuilt UI, but under the hood it still uses the same engine export APIs that you use for thumbnails. ## Export a Scene Thumbnail You generate thumbnails by exporting a design block. In most cases this should be either: - The **page block**, which represents the full visible canvas. - The scene, if your design is single-page. Exporting the page block is the safest choice when you want a thumbnail that matches what the user sees on screen. ```kotlin import android.content.Context import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.MimeType import java.io.File suspend fun exportSceneThumbnail( engine: Engine, context: Context, scene: Int ): File { // Define thumbnail size val options = ExportOptions( targetWidth = 400, targetHeight = 300, jpegQuality = 0.8f ) // Export the scene as JPEG val blob = engine.block.export( block = scene, mimeType = MimeType.JPEG, options = options ) // Save to temporary file val thumbnailFile = File(context.cacheDir, "thumbnail_${System.currentTimeMillis()}.jpg") withContext(Dispatchers.IO) { thumbnailFile.outputStream().channel.use { channel -> channel.write(blob) } } return thumbnailFile } ``` The preceding code exports a scene at 400×300 resolution with JPEG quality set to 0.8 for efficient file size. ## Control Thumbnail Size and Aspect Ratio ### How `targetWidth` and `targetHeight` behave When both `targetWidth` and `targetHeight` have values, CE.SDK renders the block large enough to **fill the target box while maintaining its aspect ratio**. Important implications: - You don't need to calculate aspect-fit or aspect-fill yourself. - The exported image may exceed one of the target dimensions internally to preserve aspect ratio. - Consider `targetWidth` and `targetHeight` as a *desired bounding box*, not a hard crop. ### Typical Thumbnail Sizes Common choices include: - 150 × 150 for dense grids - 161 × 161 for Instagram Video Feeds - 55 × 55 or 222 × 150 for Pinterest - 400 × 300 for list previews - 800 × 600 for high-quality previews ## Choose the Right Thumbnail Format CE.SDK supports PNG, JPEG, and WebP for image export. It provides a `MimeType` enum including `JPEG`, `PNG` and `WEBP`. ### PNG - Preserves transparency - Lossless quality - Compression affects speed, not quality - Best for stickers, cutouts, or UI elements ### JPEG - Smaller and faster for photographic content - No transparency - Control quality via `jpegQuality` ### WebP - Efficient compression - Supports lossless and lossy modes - Requires WebP support everywhere you display thumbnails Switching formats only requires changing the `mimeType` and relevant quality option. ```kotlin import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.MimeType import java.io.File suspend fun exportJPEGThumbnail( engine: Engine, context: Context, page: Int ): File { val options = ExportOptions( jpegQuality = 0.8f, targetWidth = 400, targetHeight = 300 ) val blob = engine.block.export( block = page, mimeType = MimeType.JPEG, options = options ) val file = File(context.cacheDir, "thumbnail.jpg") withContext(Dispatchers.IO) { file.outputStream().channel.use { channel -> channel.write(blob) } } return file } ``` When you need **different thumbnails of different sizes or image formats**, call `export` for each permutation. Pass in the correct mime type and an `ExportOptions` configuration. ```kotlin import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.MimeType import java.io.File suspend fun exportMultipleThumbnails( engine: Engine, context: Context, page: Int ): Map { val results = mutableMapOf() // Small thumbnail (22x22) val smallOptions = ExportOptions( jpegQuality = 0.8f, targetWidth = 22, targetHeight = 22 ) val smallBlob = engine.block.export(page, MimeType.JPEG, smallOptions) val smallFile = File(context.cacheDir, "thumbnail_small.jpg") withContext(Dispatchers.IO) { smallFile.outputStream().channel.use { it.write(smallBlob) } } results["small"] = smallFile // Medium thumbnail (150x150) val mediumOptions = ExportOptions( jpegQuality = 0.8f, targetWidth = 150, targetHeight = 150 ) val mediumBlob = engine.block.export(page, MimeType.JPEG, mediumOptions) val mediumFile = File(context.cacheDir, "thumbnail_medium.jpg") withContext(Dispatchers.IO) { mediumFile.outputStream().channel.use { it.write(mediumBlob) } } results["medium"] = mediumFile return results } ``` > **Note:** ## Caching ThumbnailsThumbnail export is expensive compared to image display.Even a basic in-memory cache (for example, `LruCache`) can dramatically improve scrolling performance in galleries and `RecyclerView` lists. ## Tune Quality and File Size with `ExportOptions` `ExportOptions` lets you balance visual quality, file size, and export speed. Key fields for thumbnails: - `pngCompressionLevel` (0–9, default 5) - `jpegQuality` (0.0–1.0, default 0.9) - `webpQuality` (0.0–1.0, default 1.0) - `targetWidth` / `targetHeight` CE.SDK applies only the options relevant to the chosen MIME type. Others are ignored. ```kotlin import ly.img.engine.ExportOptions import ly.img.engine.MimeType // PNG with maximum compression (slower, smaller file) val pngOptions = ExportOptions( pngCompressionLevel = 9, targetWidth = 400, targetHeight = 300 ) val pngBlob = engine.block.export(page, MimeType.PNG, pngOptions) // JPEG with lower quality (faster, much smaller file) val jpegOptions = ExportOptions( jpegQuality = 0.6f, targetWidth = 400, targetHeight = 300 ) val jpegBlob = engine.block.export(page, MimeType.JPEG, jpegOptions) // WebP with lossless mode val webpOptions = ExportOptions( webpQuality = 1.0f, targetWidth = 400, targetHeight = 300 ) val webpBlob = engine.block.export(page, MimeType.WEBP, webpOptions) ``` ## Headless and Background Thumbnail Generation CE.SDK offers two common ways to export without blocking your UI: ### Use Your Existing Engine For occasional thumbnail creation (for example, when a user saves a draft), it's often fine to export from the same `Engine` instance that powers the editor. ### Use a Separate Headless Engine Instance For batch thumbnail generation (for example, populating a large gallery), you can create a separate `Engine` instance, load the same scene data into it, and export thumbnails there. ```kotlin import android.content.Context import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.MimeType import java.io.File suspend fun generateThumbnailsHeadless( context: Context, license: String, userId: String, sceneUris: List ): List { val thumbnails = mutableListOf() // Create a headless engine instance val engine = Engine.getInstance(id = "ly.img.engine.thumbnail") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 1080, height = 1920) try { for (sceneUri in sceneUris) { // Load scene val scene = engine.scene.load(sceneUri = sceneUri) // Export thumbnail val options = ExportOptions( jpegQuality = 0.8f, targetWidth = 400, targetHeight = 300 ) val blob = engine.block.export(scene, MimeType.JPEG, options) // Save to file val file = File(context.cacheDir, "thumb_${System.currentTimeMillis()}.jpg") withContext(Dispatchers.IO) { file.outputStream().channel.use { it.write(blob) } } thumbnails.add(file) } } finally { engine.stop() } return thumbnails } ``` > **Note:** When you're using the **prebuilt editor UI** in Android, you can also customize what happens when the user taps the Export button via the editor's export configuration. The default callback writes the exported data to a temporary file and triggers the share sheet. You could generate thumbnails here and control the export instead. ## Thumbnails from Video Blocks (Single Frame) Although this guide focuses on static image thumbnails, it's worth calling out an important edge case that often surprises developers: If you export a **paused video fill block**, the result is a **single image thumbnail**, just like exporting a graphic or page. This is *not* the same as generating a stream of video thumbnails or scrubbing previews. ### How This Works - Video blocks render their current frame when exported. - You can control *which* frame becomes the thumbnail by setting the playhead time before calling `export`. Conceptually: 1. Seek the video to the desired time. 2. Pause playback. 3. Export the block using the thumbnail export flow shown earlier. This produces a single static image suitable for: - Gallery previews - Document icons - Poster-frame–style thumbnails ## Troubleshooting | Symptom | Likely Cause | Solution | |---|---|---| | Thumbnail only shows part of the design | Exported a child block instead of the page | Export the page block to capture the full visible canvas | | Thumbnail size looks wrong | Missing or zero target dimension | Set both `targetWidth` and `targetHeight` | | Export is slow | Large target size or high PNG compression | Reduce dimensions or compression level | | File size too large | Quality settings too high | Lower JPEG/WebP quality or size | | Thumbnail looks blurry | Target size too small | Increase target dimensions | | Export fails | Scene not loaded | Ensure `engine.scene.get()` returns a valid scene | ## 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. - [For Audio Processing](https://img.ly/docs/cesdk/android/guides/export-save-publish/export/audio-68de25/) - Learn how to export audio in WAV or MP4 format from any block type in CE.SDK for Android. - [To PDF](https://img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b/) - Learn how to export pages to PDF. - [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. - [Create Thumbnail in Android (Kotlin)](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/) - Generate small preview images for scenes and pages using CE.SDK export options 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: "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, JPEG, and WebP 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 | | WebP | `webpQuality` | 0.0–1.0 | 1.0 = lossless, \<1.0 = lossy | 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. - **WebP** can serve **both** roles: it supports transparency like PNG and delivers smaller files like JPEG. ## 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 and WebP 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 | | **WebP** | Quality 95 | ~510 | ~27 | 44 | Smaller than JPEG | | | Quality 80 | ~350 | ~23 | 39 | Excellent web balance | | | Quality 60 | ~240 | ~20 | 35 | Mild artifacts | | | Quality 40 | ~160 | ~18 | 31 | Compact, noticeable loss | | | Lossless | ~830 | ~33 | ∞ | Smaller than PNG, keeps alpha | \*PSNR > 40 dB ≈ visually lossless; 30–35 dB shows mild artifacts. **Key Takeaways**: - **WebP** achieves 70–85 % smaller files than uncompressed PNG with high quality around `webpQuality = 0.8f`. - **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 / WebP | `jpegQuality: 0.8f` or `webpQuality: 0.8f` | ~60–70 % smaller than PNG | Balanced quality and size | | **UI Graphics / Transparent Assets** | PNG / WebP | `pngCompressionLevel: 6–8` or `webpQuality: 1.0f (lossless)` | ~25 % smaller than default PNG | Maintains transparency | | **High-Quality Print or Archival** | PNG / WebP Lossless | `pngCompressionLevel: 9` or `webpQuality: 1.0f` | 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`, `webpQuality`. - Check that values are floats (e.g., `0.8f` not `0.8`). **❌ JPEG Quality too low**: - Increase quality to `0.9f` or use PNG/WebP lossless. **❌ Export slow**: - Check for excessive compression level. - Lower PNG level to 5–6. - Use `withContext(Dispatchers.IO)` for file operations. **❌ Video export issues**: - Ensure video scene is properly configured. - Check available memory with `engine.editor.getMaxExportSize()`. - Monitor progress callback for encoding status. **❌ Out of memory errors**: - Reduce target resolution with `targetWidth` and `targetHeight`. - Export smaller blocks instead of full scene. - Call `engine.stop()` and restart for fresh memory state. ## Next Steps Compression is one of the most practical tools for optimizing export workflows. By adjusting the `ExportOptions` structure in Kotlin, you can deliver high-quality results efficiently—whether your users are exporting social media posts, UI assets, or professional-grade print layouts. - [Export Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) to learn about all available export formats. - Apply compression consistently in automated exports using [batch processing](https://img.ly/docs/cesdk/android/automation/batch-processing-ab2d18/). - Combine scaling and compression for [thumbnails](https://img.ly/docs/cesdk/android/export-save-publish/create-thumbnail-749be1/). - Learn about [export formats](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) and their capabilities. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Options" description: "Explore export options, supported formats, and configuration features for sharing or rendering output." platform: android url: "https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/android/export-save-publish/export-82f968/) > [Overview](https://img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8/) --- ```kotlin file=@cesdk_android_examples/engine-guides-exporting-blocks/ExportingBlocks.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Engine import ly.img.engine.ExportOptions import ly.img.engine.MimeType import java.io.File import java.util.UUID fun exportingBlocks( 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) engine.scene.create() val scene = engine.scene.createFromImage( imageUri = Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg"), ) // Export scene as PNG image. val mimeType = MimeType.PNG // Optionally, the maximum supported export size can be checked before exporting. val maxExportSizeInPixels = engine.editor.getMaxExportSize() // Optionally, the compression level and the target size can be specified. val options = ExportOptions(pngCompressionLevel = 9, targetWidth = null, targetHeight = null) val blob = engine.block.export(scene, mimeType = mimeType, options = options) // Save the blob to file withContext(Dispatchers.IO) { File.createTempFile(UUID.randomUUID().toString(), ".png").apply { outputStream().channel.write(blob) } } engine.stop() } ``` Exporting via the `block.export` endpoint allows fine-grained control of the target format. CE.SDK currently supports exporting scenes, pages, groups, or individual blocks in the MP4, PNG, TGA, JPEG, WEBP, SVG, BINARY and PDF formats. To specify the desired type, just pass in the corresponding `mimeType`. Pass additional options in a mime-type specific object: | Format | MimeType | Options (Default) | | ------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PNG | `image/png` | `pngCompressionLevel (5)` - The compression level is a trade-off between file size and encoding/decoding speed, but doesn't affect quality. Valid values are `[0-9]` ranging from no to maximum compression. | | JPEG | `image/jpeg` | `jpegQuality (0.9)` - Directly influences the resulting files visual quality. Smaller = worse quality, but lower file size. Valid values are `(0-1]` | | WEBP | `image/webp` | `webpQuality (1.0)` - Controls the desired output quality. 1.0 results in a special lossless encoding that usually produces smaller file sizes than PNG. Valid values are (0-1], higher means better quality. | | SVG | `image/svg+xml` | No additional options. Text is exported as vector paths. Drop shadows, blur, effects, and raster images are rasterized and embedded as images within the SVG. | | BINARY | `application/octet-stream` | No additional options. This type returns the raw image data in RGBA8888 order in a blob. | | PDF | `application/pdf` | `exportPdfWithHighCompatibility (true)` - Increase compatibility with different PDF viewers. Images and effects will be rasterized with regard to the scene's DPI value instead of simply being embedded. | | PDF | `application/pdf` | `exportPdfWithUnderlayer (false)` - An underlayer is generated by adding a graphics block behind the existing elements of the shape of the elements on the page. | | PDF | `application/pdf` | `underlayerSpotColorName ("")` - The name of the spot color to be used for the underlayer's fill. | | PDF | `application/pdf` | `underlayerOffset (0.0)` - The adjustment in size of the shape of the underlayer. | Certain formats allow additional configuration, e.g. `options.jpegQuality` controls the output quality level when exporting to JPEG. These format-specific options are ignored when exporting to other formats. You can choose which part of the scene to export by passing in the respective block as the first parameter. For all formats, an optional `targetWidth` and `targetHeight` in pixels can be specified. If used, the block will be rendered large enough, that it fills the target size entirely while maintaining its aspect ratio. The supported export size limit can be queried with `editor.getMaxExportSize()`, the width and height should not exceed this value. ### PDF Export Performance The `exportPdfWithHighCompatibility` option controls how images and gradients are embedded in PDFs. When set to `false`, PDF exports can be **6-15x faster** for high-DPI content by embedding images directly instead of rasterizing them. However, this may cause rendering issues in Safari and macOS Preview with gradients that use transparency. Export details: - Exporting automatically performs an internal update to resolve the final layout for all blocks. - Only blocks that belong to the scene hierarchy can be exported. - The export will include the block and all child elements in the block hierarchy. - If the exported block itself is rotated it will be exported without rotation. - If a margin is set on the block it will be included. - If an outside stroke is set on the block it will be included except for pages. - Exporting a scene with more than one page may result in transparent areas between the pages, it is recommended to export the individual pages instead. - Exporting as JPEG drops any transparency on the final image and may lead to unexpected results. ```kotlin reference-only val scene = engine.scene.get()!! val page = engine.scene.getCurrentPage()!! val exportOptions = options = ExportOptions( /** * The PNG compression level to use, when exporting to PNG. * Valid values are 0 to 9, higher means smaller, but slower. * Quality is not affected. * Ignored for other encodings. * The default value is 5. */ pngCompressionLevel = 5, /** * The JPEG quality to use when encoding to JPEG. * Valid values are (0F-1F], higher means better quality. * Ignored for other encodings. * The default value is 0.9F. */ jpegQuality = 0.9F, /** * The WebP quality to use when encoding to WebP. Valid values are (0-1], higher means better quality. * WebP uses a special lossless encoding that usually produces smaller file sizes than PNG. * Ignored for other encodings. Defaults to 1.0. */ webpQuality = 1.0F, /** * An optional target width used in conjunction with target height. * If used, the block will be rendered large enough, that it fills the target * size entirely while maintaining its aspect ratio. * The default value is null. */ targetWidth = null, /** * An optional target height used in conjunction with target with. * If used, the block will be rendered large enough, that it fills the target * size entirely while maintaining its aspect ratio. * The default value is null. */ targetHeight = null, /** * Export the PDF document with a higher compatibility to different PDF viewers. * Bitmap images and some effects like gradients will be rasterized with the DPI * setting instead of embedding them directly. * The default value is true. */ exportPdfWithHighCompatibility = true, /** * Export the PDF document with an underlayer. * An underlayer is generated by adding a graphics block behind the existing elements of the shape of the elements on * the page. */ exportPdfWithUnderlayer = false, /** * The name of the spot color to be used for the underlayer's fill. */ underlayerSpotColorName = "" /** * The adjustment in size of the shape of the underlayer. */ underlayerOffset = 0.0F ) val blob = engine.block.export( block = scene, mimeType = MimeType.PNG, options = exportOptions ) val colorMaskedBlob = engine.block.exportWithColorMask( block = scene, mimeType = MimeType.PNG, maskColor = Color.fromRGBA(r = 1F, g = 0F, b = 0F) options = exportOptions ) val videoExportOptions = ExportVideoOptions( /** * Determines the encoder feature set and in turn the quality, size and speed of the encoding process. * The default value is 77 (Main Profile). */ h264Profile = 77, /** * Controls the H.264 encoding level. This relates to parameters used by the encoder such as bit rate, * timings and motion vectors. Defined by the spec are levels 1.0 up to 6.2. To arrive at an integer value, * the level is multiplied by ten. E.g. to get level 5.2, pass a value of 52. * The default value is 52. */ h264Level = 52, /** * The video bitrate in bits per second. The maximum bitrate is determined by h264Profile and h264Level. * If the value is 0, the bitrate is automatically determined by the engine. */ videoBitrate = 0, /** * The audio bitrate in bits per second. If the value is 0, the bitrate is automatically determined by the engine (128kbps for stereo AAC stream). */ audioBitrate = 0, /** * The target frame rate of the exported video in Hz. * The default value is 30. */ frameRate = 30.0F, /** * An optional target width used in conjunction with target height. * If used, the block will be rendered large enough, that it fills the target * size entirely while maintaining its aspect ratio. */ targetWidth = 1280, /** * An optional target height used in conjunction with target width. * If used, the block will be rendered large enough, that it fills the target * size entirely while maintaining its aspect ratio. */ targetHeight = 720 ) val videoBlob = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { println("Rendered ${it.renderedFrames} frames and encoded ${it.encodedFrames} frames out of ${it.totalFrames} frames") } ) val maxExportSizeInPixels = engine.editor.getMaxExportSize() val availableMemoryInBytes = engine.editor.getAvailableMemory() ``` ## Export a Static Design ```kotlin suspend fun export( block: DesignBlock, mimeType: MimeType, options: ExportOptions? = null, onPreExport: suspend Engine.() -> Unit = {}, ): ByteBuffer ``` Exports a design block element as a file of the given mime type. Performs an internal update to resolve the final layout for the blocks. Note: The export happens in a background thread and the `Engine` instance in the `onPreExport` lambda is a separate instance and is alive until the suspending function resumes. Use this lambda to configure the background engine for export. - `block`: the design block element to export. - `mimeType`: the mime type of the output file. - `options`: the options for exporting the block type - `onPreExport`: the lambda to configure the engine before export. This lambda is called on a background thread, and the `Engine` parameter of this lambda is a separate engine instance running in that background thread. - Returns the exported data. ## Export with a Color Mask ```kotlin suspend fun exportWithColorMask( block: DesignBlock, mimeType: MimeType, maskColor: RGBAColor, options: ExportOptions? = null, onPreExport: suspend Engine.() -> Unit = {}, ): Pair ``` Exports a design block element as a file of the given mime type. Performs an internal update to resolve the final layout for the blocks. Note: The export happens in a background thread and the `Engine` instance in the `onPreExport` lambda is a separate instance and is alive until the suspending function resumes. Use this lambda to configure the background engine for export. - `block`: the design block element to export. - `mimeType`: the mime type of the output file. - `maskColor`: the mask color. - `options`: the options for exporting the block type - `onPreExport`: the lambda to configure the engine before export. This lambda is called on a background thread, and the `Engine` parameter of this lambda is a separate engine instance running in that background thread. - Returns a pair where first is the exported image data and second is the mask data. ## Export a Video Export a page as a video file of the given mime type. ```kotlin suspend fun exportVideo( block: DesignBlock, timeOffset: Double, duration: Double, mimeType: MimeType, progressCallback: (ExportVideoProgress) -> Unit, options: ExportVideoOptions? = null, onPreExport: suspend Engine.() -> Unit = {}, ): ByteBuffer ``` Exports a design block as a video file of the given mime type. Note: The export will run across multiple iterations of the update loop. In each iteration a frame is scheduled for encoding. Note: The export happens in a background thread and the `Engine` instance in the `onPreExport` lambda is a separate instance and is alive until the suspending function resumes. Use this lambda to configure the background engine for export. - `block`: the design block to export. Currently, only page blocks are supported. - `timeOffset`: the time offset in seconds of the page's timeline from which the video will start. - `duration`: the duration in seconds of the final video. - `mimeType`: the mime type of the output video file. - `progressCallback`: a callback that reports on the progress of the export. It informs the receiver of the number of frames rendered by the engine, the number of encoded frames, and the total number of frames to encode. - `options`: the options used for the export of the block. - `onPreExport`: the lambda to configure the engine before export. This lambda is called on a background thread, and the `Engine` parameter of this lambda is a separate engine instance running in that background thread. - Returns the exported video as a file in filesDir. Note that you are responsible for deleting the file after it is used. - Returns the exported data. ## Export Information Before exporting, the maximum export size and available memory can be queried. ```kotlin fun getMaxExportSize(): Int ``` Get the export size limit in pixels on the current device. An export is only possible when both the width and height of the output are below or equal this limit. However, this is only an upper limit as the export might also not be possible due to other reasons, e.g., memory constraints. - Returns the upper export size limit in pixels or an unlimited size, i.e, the maximum signed 32-bit integer value, if the limit is unknown. ```kotlin fun getAvailableMemory(): Long ``` Get the currently available memory in bytes. - Returns the currently available memory in bytes. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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: "Learn how to export pages to PDF." 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/editor-guides-configuration-callbacks/CallbacksEditorSolution.kt reference-only import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement 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.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.editor.BasicConfigurationBuilder import ly.img.editor.Editor import ly.img.editor.core.component.Dock import ly.img.editor.core.component.EditorComponent 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.rememberSystemCamera 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.engine.DesignBlockType import ly.img.engine.MimeType import java.io.File import java.io.IOException import java.nio.ByteBuffer import java.util.UUID data class State( val isLoading: Boolean = false, val isCloseConfirmationDialogVisible: Boolean = false, val error: Throwable? = null, ) // Add this composable to your NavHost @Composable fun CallbacksEditorSolution( license: String, onClose: (Throwable?) -> Unit, ) { var state by remember { mutableStateOf(State()) } Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::BasicConfigurationBuilder) { onCreate = { state = state.copy(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) editorContext.engine.editor.setSettingEnum(keypath = "touch/pinchAction", value = "Scale") // Add ImageUploads asset source to demonstrate onUpload editorContext.engine.asset.addLocalSource( sourceId = AssetSourceType.ImageUploads.sourceId, supportedMimeTypes = listOf(AssetSourceType.ImageUploads.mimeTypeFilter), ) } finally { state = state.copy(isLoading = false) } } onLoaded = { coroutineScope { launch { editorContext.engine.editor .onStateChanged() .map { editorContext.engine.editor.getEditMode() } .distinctUntilChanged() .collect { Toast.makeText(editorContext.activity, "Edit mode is updated to $it!", Toast.LENGTH_SHORT).show() } } } } onExport = { val engine = editorContext.engine state = state.copy(isLoading = true) runCatching { val buffer = engine.block.export( block = requireNotNull(engine.scene.get()), mimeType = MimeType.PDF, onPreExport = { scene.getPages().forEach { page -> block.setScopeEnabled(page, key = "layer/visibility", enabled = true) block.setVisible(page, visible = true) } }, ) writeToTempFile(buffer, MimeType.PDF) }.onSuccess { file -> Toast.makeText(editorContext.activity, "Exported to $file!", Toast.LENGTH_SHORT).show() state = state.copy(isLoading = false) // Do something with the file }.onFailure { state = state.copy(isLoading = false, error = it) } } onUpload = onUpload@{ assetDefinition, _ -> val meta = assetDefinition.meta ?: return@onUpload assetDefinition val sourceUri = meta["uri"]?.toUri() val uploadedUri = sourceUri // todo upload the asset here and return remote uri val newMeta = meta + listOf( "uri" to uploadedUri.toString(), "thumbUri" to uploadedUri.toString(), ) Toast.makeText(editorContext.activity, "onUpload invoked!", Toast.LENGTH_SHORT).show() assetDefinition.copy(meta = newMeta) } this.onClose = { if (editorContext.engine.editor.canUndo()) { state = state.copy(isCloseConfirmationDialogVisible = true) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } } onError = { error -> state = state.copy(error = error) } overlay = { EditorComponent.remember { decoration = { // Capture system back button tap BackHandler(true) { editorContext.eventHandler.send(EditorEvent.OnClose()) } if (state.isLoading) { Loading() } if (state.isCloseConfirmationDialogVisible) { CloseConfirmationDialog( onDismissRequest = { state = state.copy(isCloseConfirmationDialogVisible = false) }, ) } val error = state.error when { error is IOException -> NoInternetDialog() error != null -> ErrorDialog(throwable = error) } } } } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemCamera() } } } horizontalArrangement = { Arrangement.Center } } } navigationBar = { NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.End) { add { NavigationBar.Button.rememberExport() } } } } } } } }, onClose = onClose, ) } private suspend fun writeToTempFile( byteBuffer: ByteBuffer, mimeType: MimeType = MimeType.PDF, ): File = withContext(Dispatchers.IO) { val extension = mimeType.key.split("/").last() File .createTempFile(UUID.randomUUID().toString(), ".$extension") .apply { outputStream().channel.write(byteBuffer) } } ``` In this guide, you'll learn how to configure the prebuilt editor's export button to produce PDF output. In the new configuration architecture, the export action is routed through `onExport`, and your callback owns the full flow: export the buffer, write it to disk, then decide whether to share or persist the file. ## What You'll Learn - Switch the prebuilt editor's export to PDF via `onExport`. - Implement your own save/share flow for the exported file. - Add export options (DPI, high-compat, underlayer) for design scenes. - Keep MP4 export for video scenes while using PDF for design scenes. ## When to Use It - You want editor UI out of the box, with export customized to PDF. - You want to control how the exported file is written, stored, or shared. - You need optional export tweaks but still want to keep the prebuilt editor UI. ## Export Control The prebuilt editors have an export control in the navigation bar. In the new architecture, tapping it invokes `EditorConfiguration.onExport`. The editor does not provide a built-in share-sheet fallback, so your callback needs to export the buffer, write it to a file, and hand it off to your app's save/share flow. ![Location of export button in prebuilt editors](assets/create-pdf-android-1.png) If your export to PDF is straightforward, you can override `onExport` and keep the logic focused on the export itself. ```kotlin highlight-configuration-onExport onExport = { val engine = editorContext.engine state = state.copy(isLoading = true) runCatching { val buffer = engine.block.export( block = requireNotNull(engine.scene.get()), mimeType = MimeType.PDF, onPreExport = { scene.getPages().forEach { page -> block.setScopeEnabled(page, key = "layer/visibility", enabled = true) block.setVisible(page, visible = true) } }, ) writeToTempFile(buffer, MimeType.PDF) }.onSuccess { file -> Toast.makeText(editorContext.activity, "Exported to $file!", Toast.LENGTH_SHORT).show() state = state.copy(isLoading = false) // Do something with the file }.onFailure { state = state.copy(isLoading = false, error = it) } } ``` The preceding code: - Exports the currently loaded scene as a PDF. - Writes the PDF buffer to a temporary file that your app can then share or persist. - Updates local UI state while the export is running and when it fails. ```kotlin highlight-write-to-temp-file private suspend fun writeToTempFile( byteBuffer: ByteBuffer, mimeType: MimeType = MimeType.PDF, ): File = withContext(Dispatchers.IO) { val extension = mimeType.key.split("/").last() File .createTempFile(UUID.randomUUID().toString(), ".$extension") .apply { outputStream().channel.write(byteBuffer) } } ``` ## Adding Options to Export For more complex configurations, you can use `ExportOptions` to customize the PDF output. If the same UI can open both static and timeline-based scenes, branch on whether the current page supports playback time and keep MP4 export for pages that behave like video timelines. ```kotlin Editor( license = null, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { onExport = { val engine = editorContext.engine val page = engine.scene.getCurrentPage() ?: engine.scene.getPages()[0] if (engine.block.supportsPlaybackTime(page)) { val buffer = engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = {}, ) val file = writeToTempFile(buffer, MimeType.MP4) // Share or persist the video file here. } else { val options = ExportOptions( exportPdfWithHighCompatibility = true, exportPdfWithUnderlayer = true, underlayerSpotColorName = "White", underlayerOffset = -2.0f, ) val buffer = engine.block.export( block = requireNotNull(engine.scene.get()), mimeType = MimeType.PDF, options = options, ) val file = writeToTempFile(buffer, MimeType.PDF) // Share or persist the PDF file here. } } } }, ) ``` The preceding code creates an `ExportOptions` object and passes it to the PDF export function. You can configure additional options like underlayer generation and DPI settings. Once the file has been written to disk, hand it off to your app's share or save flow. For video scenes, keep using `exportVideo`. ## Export Options The `ExportOptions` class provides these parameters for PDF customization: ```kotlin val options = ExportOptions( exportPdfWithHighCompatibility = true, // Flatten complex elements exportPdfWithUnderlayer = true, // Generate print underlayer underlayerSpotColorName = "White", // Spot color for underlayer ink underlayerOffset = -2.0f, // Offset in design units ) ``` **Available options:** - `exportPdfWithHighCompatibility`: Flatten PDF for better compatibility with older viewers. - `exportPdfWithUnderlayer`: Generate an underlayer shape for specialized printing (fabric, glass). - `underlayerSpotColorName`: Specify the spot color name for the underlayer ink. - `underlayerOffset`: Adjust underlayer size with positive (larger) or negative (smaller) offset. > **Note:** When using underlayer, do not flatten the resulting PDF or you will remove the underlayer. The underlayer sits behind your design as a separate layer. ## Troubleshooting **❌ Export still uses PNG/MP4 logic**: - The SDK no longer has a built-in PNG/MP4 export fallback. If you still see those formats, they are coming from your own `onExport` implementation. - Ensure non-timeline pages call `engine.block.export(..., mimeType = MimeType.PDF)`. - If your editor can open timeline-based pages, verify that `exportVideo(...)` only runs for pages that actually support playback time. - If you removed your custom `onExport` callback entirely, note that the default implementation only logs a warning and does not export anything. **❌ PDF looks soft**: - Check the `"scene/dpi"` property of the scene and increase it if necessary. - Ensure placed images have enough pixels to cover the frame. **❌ Underlayer absent in output**: - Verify the spot color name is correct and matches what the printer expects. - Ensure `exportPdfWithUnderlayer = true` is set in `ExportOptions`. - Do not flatten the PDF or the underlayer will be lost. **❌ Video scene needs a different flow**: - This is expected. Check whether the current page supports playback time and call `exportVideo` for timeline-based pages instead of `export`. ## Next Steps Now that you know how to configure the editor to export to PDF, here are some topics to explore: - [Spot Colors](https://img.ly/docs/cesdk/android/colors-a9b79c/) – use spot colors for specialized printing. - [Asset Sources & Upload](https://img.ly/docs/cesdk/android/import-media-4e3703/) – bring user images/videos in, then export as PDF. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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/) --- The CreativeEngine allows you to save scenes in a binary format to share them between editors or store them for later editing. Saving a scene can be done as a either scene file or as an archive file. A scene file does not include any fonts or images. Only the source URIs of assets, the general layout, and element properties are stored. When loading scenes in a new environment, ensure previously used asset URIs are available. Conversely, an archive file contains within it the scene's assets and references them as relative URIs. > **Note:** **Warning** A scene file does not include any fonts or images. Only the source > URIs of assets, the general layout, and element properties are stored. When > loading scenes in a new environment, ensure previously used asset URIs are > available. ```kotlin file=@cesdk_android_examples/engine-guides-save-scene-to-archive/SaveSceneToArchive.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Engine import java.net.HttpURLConnection import java.net.URL import java.nio.channels.Channels fun saveSceneToArchive( 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 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) val blob = engine.scene.saveToArchive(scene = scene) try { withContext(Dispatchers.IO) { val connection = URL("https://example.com/upload/").openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.outputStream.use { Channels.newChannel(it).write(blob) } connection.connect() } } catch (exception: Exception) { } engine.stop() } ``` ## Save Scenes to an Archive In this example, we will show you how to save scenes as an archive with the [CreativeEditor SDK](https://img.ly/products/creative-sdk). As an archive, the resulting `Blob` includes all pages and any hidden elements and all the asset data. To get hold of such a `Blob`, you need to use `engine.scene.saveToArchive()`. This is an asynchronous method. After waiting for the coroutine to finish, we receive a `Blob` holding the entire scene currently loaded in the editor including its assets' data. ```kotlin highlight-saveToArchive val blob = engine.scene.saveToArchive(scene = scene) ``` That `Blob` can then be treated as a form file parameter and sent to a remote location. ```kotlin highlight-create-form-data-archive try { withContext(Dispatchers.IO) { val connection = URL("https://example.com/upload/").openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.outputStream.use { Channels.newChannel(it).write(blob) } connection.connect() } } catch (exception: Exception) { } ``` ### Full Code Here's the full code: ```kotlin import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Engine import java.net.HttpURLConnection import java.net.URL import java.nio.channels.Channels fun saveSceneToArchive( 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 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) val blob = engine.scene.saveToArchive(scene = scene) withContext(Dispatchers.IO) { val connection = URL("https://example.com/upload/").openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.outputStream.use { Channels.newChannel(it).write(blob) } connection.connect() } engine.stop() } ``` ```kotlin file=@cesdk_android_examples/engine-guides-save-scene-to-blob/SaveSceneToBlob.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Engine import java.net.HttpURLConnection import java.net.URL fun saveSceneToBlob( license: String?, // pass null or empty for evaluation mode with watermark userId: String, uploadUrl: 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 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) val savedSceneString = engine.scene.saveToString(scene = scene) val blob = savedSceneString.toByteArray(Charsets.UTF_8) runCatching { withContext(Dispatchers.IO) { val connection = URL(uploadUrl).openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.outputStream.use { it.write(blob) } connection.connect() } } engine.stop() } ``` ## Save Scenes to a Blob In this example, we will show you how to save scenes as a `Blob` with the [CreativeEditor SDK](https://img.ly/products/creative-sdk). This is done by converting the contents of a scene to a string, which can then be stored or transferred. For sending these to a remote location, we wrap them in a `Blob` and treat it as a file object. To get hold of the scene contents as string, you need to use `engine.scene.saveToString()`. This is an asynchronous method. After waiting for the coroutine to finish, we receive a plain string holding the entire scene currently loaded in the editor. This includes all pages and any hidden elements but none of the actual asset data. ```kotlin highlight-saveToBlob val savedSceneString = engine.scene.saveToString(scene = scene) ``` The returned string consists solely of ASCII characters and can safely be used further or written to a database. ```kotlin highlight-create-blob val blob = savedSceneString.toByteArray(Charsets.UTF_8) ``` That object can then be treated as a form file parameter and sent to a remote location. ```kotlin highlight-create-form-data-blob runCatching { withContext(Dispatchers.IO) { val connection = URL(uploadUrl).openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.outputStream.use { it.write(blob) } connection.connect() } } ``` ### Full Code Here's the full code: ```kotlin import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ly.img.engine.Engine import java.net.HttpURLConnection import java.net.URL fun saveSceneToBlob( license: String, userId: String, uploadUrl: 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 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) val savedSceneString = engine.scene.saveToString(scene = scene) val blob = savedSceneString.toByteArray(Charsets.UTF_8) runCatching { withContext(Dispatchers.IO) { val connection = URL(uploadUrl).openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.outputStream.use { it.write(blob) } connection.connect() } } engine.stop() } ``` ```kotlin file=@cesdk_android_examples/engine-guides-save-scene-to-string/SaveSceneToString.kt reference-only import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Engine fun saveSceneToString( 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 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) val savedSceneString = engine.scene.saveToString(scene = scene) println(savedSceneString) engine.stop() } ``` ## Save Scenes to a String In this example, we will show you how to save scenes as a string with the [CreativeEditor SDK](https://img.ly/products/creative-sdk). This is done by converting the contents of a scene to a single string, which can then be stored or transferred. To get hold of such a string, you need to use `engine.scene.saveToString()`. This is an asynchronous method. After waiting for the coroutine to finish, we receive a plain string holding the entire scene currently loaded in the editor. This includes all pages and any hidden elements, but none of the actual asset data. ```kotlin highlight-saveToString val savedSceneString = engine.scene.saveToString(scene = scene) ``` The returned string consists solely of ASCII characters and can safely be used further or written to a database. ```kotlin highlight-result-string println(savedSceneString) ``` ## Full Code ```kotlin import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Engine fun saveSceneToString( 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 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) val savedSceneString = engine.scene.saveToString(scene = scene) println(savedSceneString) engine.stop() } ``` ## Compression Options CE.SDK supports optional compression for saved scenes to reduce file size. Compression is particularly useful for large scenes or when storage space is limited. ```kotlin // Save with Zstd compression (recommended) val compressed = engine.scene.saveToString( scene = scene, options = SaveToStringOptions( compression = CompressionOptions( format = CompressionFormat.ZSTD, level = CompressionLevel.DEFAULT ) ) ) ``` **Compression Formats:** - `CompressionFormat.NONE` - No compression (default) - `CompressionFormat.ZSTD` - Zstandard compression (recommended for best performance) **Compression Levels:** - `CompressionLevel.FASTEST` - Fastest compression, larger output - `CompressionLevel.DEFAULT` - Balanced speed and size (recommended) - `CompressionLevel.BEST` - Best compression, slower **Performance:** Compression adds minimal overhead while reducing scene size by approximately 64%. The default level provides the best balance of speed and compression ratio. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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 and persist metadata alongside your design, such as tags, version info, or creator details." 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 android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.DesignBlockType import ly.img.engine.Engine fun storeMetadata( 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) var scene = engine.scene.createFromImage( Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg"), ) val block = engine.block.findByType(DesignBlockType.Page).first() engine.block.setMetadata(scene, key = "author", value = "img.ly") engine.block.setMetadata(block, key = "customer_id", value = "1234567890") engine.block.setMetadata(block, key = "customer_name", value = "Name") // This will return "img.ly" engine.block.getMetadata(scene, key = "author") // This will return "1000000" engine.block.getMetadata(block, key = "customer_id") // This will return ["customer_id", "customer_name"] engine.block.findAllMetadata(block) engine.block.removeMetadata(block, key = "customer_id") // This will return false engine.block.hasMetadata(block, key = "customer_id") // We save our scene and reload it from scratch val sceneString = engine.scene.saveToString(scene) scene = engine.scene.load(scene = sceneString) // This still returns "img.ly" engine.block.getMetadata(scene, key = "author") // And this still returns "Name" engine.block.getMetadata(block, key = "customer_name") engine.stop() } ``` CE.SDK allows you to store custom metadata in your scenes. You can attach metadata to your scene or directly to your individual design blocks within the scene. This metadata is persistent across saving and loading of scenes. It simply consists of key value pairs of strings. Using any string-based serialization format such as JSON will allow you to store even complex objects. Please note that when duplicating blocks their metadata will also be duplicated. ## Working with Metadata We can add metadata to any design block using `fun setMetadata(block: DesignBlock, key: String, value: String)`. This also includes the scene block. ```kotlin highlight-setMetadata engine.block.setMetadata(scene, key = "author", value = "img.ly") engine.block.setMetadata(block, key = "customer_id", value = "1234567890") engine.block.setMetadata(block, key = "customer_name", value = "Name") ``` We can retrieve metadata from any design block or scene using `fun getMetadata(block: DesignBlock, key: String): String`. Before accessing the metadata you check for its existence using `fun hasMetadata(block: DesignBlock, key: String): Boolean`. ```kotlin highlight-getMetadata // This will return "img.ly" engine.block.getMetadata(scene, key = "author") // This will return "1000000" engine.block.getMetadata(block, key = "customer_id") ``` We can query all metadata keys from any design block or scene using `fun findAllMetadata(block: DesignBlock): List`. For blocks without any metadata, this will return an empty list. ```kotlin highlight-findAllMetadata // This will return ["customer_id", "customer_name"] engine.block.findAllMetadata(block) ``` If you want to get rid of any metadata, you can use `fun removeMetadata(block: DesignBlock, key: String)`. ```kotlin highlight-removeMetadata engine.block.removeMetadata(block, key = "customer_id") // This will return false engine.block.hasMetadata(block, key = "customer_id") ``` Metadata will automatically be saved and loaded as part the scene. So you don't have to worry about it getting lost or having to save it separately. ```kotlin highlight-persistence // We save our scene and reload it from scratch val sceneString = engine.scene.saveToString(scene) scene = engine.scene.load(scene = sceneString) // This still returns "img.ly" engine.block.getMetadata(scene, key = "author") // And this still returns "Name" engine.block.getMetadata(block, key = "customer_name") ``` ## Full Code Here's the full code: ```kotlin import android.net.Uri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.DesignBlockType import ly.img.engine.Engine fun storeMetadata( 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) var scene = engine.scene.createFromImage( Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg"), ) val block = engine.block.findByType(DesignBlockType.Graphic).first() engine.block.setMetadata(scene, key = "author", value = "img.ly") engine.block.setMetadata(block, key = "customer_id", value = "1234567890") engine.block.setMetadata(block, key = "customer_name", value = "Name") // This will return "img.ly" engine.block.getMetadata(scene, key = "author") // This will return "1000000" engine.block.getMetadata(block, key = "customer_id") // This will return ["customer_id", "customer_name"] engine.block.findAllMetadata(block) engine.block.removeMetadata(block, key = "customer_id") // This will return false engine.block.hasMetadata(block, key = "customer_id") // We save our scene and reload it from scratch val sceneString = engine.scene.saveToString(scene) scene = engine.scene.load(scene = sceneString) // This still returns "img.ly" engine.block.getMetadata(scene, key = "author") // And this still returns "Name" engine.block.getMetadata(block, key = "customer_name") 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: "File Format Support" description: "See which image, video, audio, font, and template formats CE.SDK supports for import and export." platform: android url: "https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/android/compatibility-fef719/) > [File Format Support](https://img.ly/docs/cesdk/android/file-format-support-3c4b2a/) --- ## Importing Media ### SVG Limitations ## Exporting Media ## Importing Templates ## Font Formats ## Video & Audio Codecs CE.SDK supports the most widely adopted video and audio codecs to ensure compatibility across platforms: ## Size Limits ### Image Resolution Limits ### Video Resolution & Duration Limits --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Fills" description: "Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements." platform: android url: "https://img.ly/docs/cesdk/android/fills-402ddc/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) --- --- ## Related Pages - [Fills](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) - Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Fills" description: "Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements." platform: android url: "https://img.ly/docs/cesdk/android/fills/overview-3895ee/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Fills](https://img.ly/docs/cesdk/android/fills-402ddc/) > [Overview](https://img.ly/docs/cesdk/android/fills/overview-3895ee/) --- ```kotlin file=@cesdk_android_examples/engine-guides-using-fills/UsingFills.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.ShapeType fun usingFills( 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) 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.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) engine.block.setFill(block, fill = engine.block.createFill(FillType.Color)) engine.block.appendChild(parent = page, child = block) engine.block.supportsFill(scene) // Returns false engine.block.supportsFill(block) // Returns true val colorFill = engine.block.getFill(block) val defaultRectFillType = engine.block.getType(colorFill) val allFillProperties = engine.block.findAllProperties(colorFill) engine.block.setColor( block = colorFill, property = "fill/color/value", value = Color.fromRGBA(r = 1.0F, g = 0.0F, b = 0.0F, a = 1.0F), ) engine.block.setFillEnabled(block, enabled = false) engine.block.setFillEnabled(block, enabled = !engine.block.isFillEnabled(block)) 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.destroy(colorFill) engine.block.setFill(block, fill = imageFill) /* // The following line would also destroy imageFill engine.block.destroy(circle) */ val duplicateBlock = engine.block.duplicate(block) engine.block.setPositionX(duplicateBlock, value = 450F) val autoDuplicateFill = engine.block.getFill(duplicateBlock) engine.block.setString( block = autoDuplicateFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_2.jpg", ) /* // We could now assign this fill to another block. val manualDuplicateFill = engine.block.duplicate(autoDuplicateFill) engine.block.destroy(manualDuplicateFill) */ val sharedFillBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(sharedFillBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(sharedFillBlock, value = 350F) engine.block.setPositionY(sharedFillBlock, value = 400F) engine.block.setWidth(sharedFillBlock, value = 100F) engine.block.setHeight(sharedFillBlock, value = 100F) engine.block.appendChild(parent = page, child = sharedFillBlock) engine.block.setFill(sharedFillBlock, fill = engine.block.getFill(block)) engine.stop() } ``` Some [design blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) in CE.SDK allow you to modify or replace their fill. The fill is an object that defines the contents within the shape of a block. CreativeEditor SDK supports many different types of fills, such as images, solid colors, gradients and videos. Similarly to blocks, each fill has a numeric id which can be used to query and [modify its properties](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/). We currently support the following fill types: - `FillType.Color` - `FillType.LinearGradient` - `FillType.RadialGradient` - `FillType.ConicalGradient` - `FillType.Image` - `FillType.Video` - `FillType.PixelStream` ## Accessing Fills Not all types of design blocks support fills, so you should always first call the `fun supportsFill(block: DesignBlock): Boolean` API before accessing any of the following APIs. ```kotlin highlight-supportsFill engine.block.supportsFill(scene) // Returns false engine.block.supportsFill(block) // Returns true ``` In order to receive the fill id of a design block, call the `fun getFill(block: DesignBlock): DesignBlock` API. You can now pass this id into other APIs in order to query more information about the fill, e.g. its type via the `fun getType(block: DesignBlock): String` API. ```kotlin highlight-getFill val colorFill = engine.block.getFill(block) val defaultRectFillType = engine.block.getType(colorFill) ``` ## Fill Properties Just like design blocks, fills with different types have different properties that you can query and modify via the API. Use `fun findAllProperties(block: DesignBlock): List` in order to get a list of all properties of a given fill. For the solid color fill in this example, the call would return `["fill/color/value", "type"]`. Please refer to the [design blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for a complete list of all available properties for each type of fill. ```kotlin highlight-getProperties val allFillProperties = engine.block.findAllProperties(colorFill) ``` Once we know the property keys of a fill, we can use the same APIs as for design blocks in order to modify those properties. For example, we can use `fun setColor(block: DesignBlock, property: String, value: Color)` in order to change the color of the fill to red. Once we do this, our graphic block with rect shape will be filled with solid red. ```kotlin highlight-modifyProperties engine.block.setColor( block = colorFill, property = "fill/color/value", value = Color.fromRGBA(r = 1.0F, g = 0.0F, b = 0.0F, a = 1.0F), ) ``` ## Disabling Fills You can disable and enable a fill using the `fun setFillEnabled(block: DesignBlock, enabled: Boolean)` API, for example in cases where the design block should only have a stroke but no fill. Notice that you have to pass the id of the design block and not of the fill to the API. ```kotlin highlight-disableFill engine.block.setFillEnabled(block, enabled = false) engine.block.setFillEnabled(block, enabled = !engine.block.isFillEnabled(block)) ``` ## Changing Fill Types All design blocks that support fills allow you to also exchange their current fill for any other type of fill. In order to do this, you need to first create a new fill object using `fun createFill(fillType: FillType): DesignBlock`. ```kotlin highlight-createFill 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", ) ``` In order to assign a fill to a design block, simply call `fun setFill(block: DesignBlock, fill: DesignBlock)`. Make sure to delete the previous fill of the design block first if you don't need it any more, otherwise we will have leaked it into the scene and won't be able to access it any more, because we don't know its id. Notice that we don't use the `appendChild` API here, which only works with design blocks and not fills. When a fill is attached to one design block, it will be automatically destroyed when the block itself gets destroyed. ```kotlin highlight-replaceFill engine.block.destroy(colorFill) engine.block.setFill(block, fill = imageFill) /* // The following line would also destroy imageFill engine.block.destroy(circle) */ ``` ## Duplicating Fills If we duplicate a design block with a fill that is only attached to this block, the fill will automatically be duplicated as well. In order to modify the properties of the duplicate fill, we have to query its id from the duplicate block. ```kotlin highlight-duplicateFill val duplicateBlock = engine.block.duplicate(block) engine.block.setPositionX(duplicateBlock, value = 450F) val autoDuplicateFill = engine.block.getFill(duplicateBlock) engine.block.setString( block = autoDuplicateFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_2.jpg", ) /* // We could now assign this fill to another block. val manualDuplicateFill = engine.block.duplicate(autoDuplicateFill) engine.block.destroy(manualDuplicateFill) */ ``` ## Sharing Fills It is also possible to share a single fill instance between multiple design blocks. In that case, changing the properties of the fill will apply to all of the blocks that it's attached to at once. Destroying a block with a shared fill will not destroy the fill until there are no other design blocks left that still use that fill. ```kotlin highlight-sharedFill val sharedFillBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(sharedFillBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(sharedFillBlock, value = 350F) engine.block.setPositionY(sharedFillBlock, value = 400F) engine.block.setWidth(sharedFillBlock, value = 100F) engine.block.setHeight(sharedFillBlock, value = 100F) engine.block.appendChild(parent = page, child = sharedFillBlock) engine.block.setFill(sharedFillBlock, fill = engine.block.getFill(block)) ``` ## Full Code Here is the full code for working with fills: ```kotlin import kotlinx.coroutines.* import ly.img.engine.* fun usingFills( 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 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.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) engine.block.setFill(block, fill = engine.block.createFill(FillType.Color)) engine.block.appendChild(parent = page, child = block) engine.block.supportsFill(scene) // Returns false engine.block.supportsFill(block) // Returns true val colorFill = engine.block.getFill(block) val defaultRectFillType = engine.block.getType(colorFill) val allFillProperties = engine.block.findAllProperties(colorFill) engine.block.setColor( block = colorFill, property = "fill/color/value", value = Color.fromRGBA(r = 1.0F, g = 0.0F, b = 0.0F, a = 1.0F), ) engine.block.setFillEnabled(block, enabled = false) engine.block.setFillEnabled(block, enabled = !engine.block.isFillEnabled(block)) 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.destroy(colorFill) engine.block.setFill(block, fill = imageFill) /* // The following line would also destroy imageFill engine.block.destroy(circle) */ val duplicateBlock = engine.block.duplicate(block) engine.block.setPositionX(duplicateBlock, value = 450F) val autoDuplicateFill = engine.block.getFill(duplicateBlock) engine.block.setString( block = autoDuplicateFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_2.jpg", ) /* // We could now assign this fill to another block. val manualDuplicateFill = engine.block.duplicate(autoDuplicateFill) engine.block.destroy(manualDuplicateFill) */ val sharedFillBlock = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(sharedFillBlock, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setPositionX(sharedFillBlock, value = 350F) engine.block.setPositionY(sharedFillBlock, value = 400F) engine.block.setWidth(sharedFillBlock, value = 100F) engine.block.setHeight(sharedFillBlock, value = 100F) engine.block.appendChild(parent = page, child = sharedFillBlock) engine.block.setFill(sharedFillBlock, fill = engine.block.getFill(block)) 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: "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 - [Android Filters & Effects SDK](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. - [Add a Filter or Effect](https://img.ly/docs/cesdk/android/filters-and-effects/create-custom-filters-c796ba/) - Programmatically or manually add effects to design elements to modify their visual style. - [Chroma Key (Green Screen) in Android (Kotlin)](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/) - Use CE.SDK's green/blue screen keyer to replace backgrounds, tune edges & spill, and composite subjects over virtual scenes. - [Blur](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. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (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" 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 reference-only // Create and configure a radial blur val radialBlur = engine.block.createBlur(type = BlurType.Radial) engine.block.setFloat(radialBlur, property = "radial/radius", value = 100F) engine.block.setBlur(block, blurBlock = radialBlur) engine.block.setBlurEnabled(block, enabled = true) val isBlurEnabled = engine.block.isBlurEnabled(block) // Access an existing blur if (engine.block.supportsBlur(block)) { val existingBlur = engine.block.getBlur(block) } ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)'s CreativeEngine to modify a block's blur through the `block` API. Blurs can be added to any shape or page. You can create and configure individual blurs and apply them to blocks. Blocks support at most one blur at a time. The same blur may be used for different blocks at the same time. ## Creating a Blur To create a blur simply use `fun createBlur(type: BlurType): DesignBlock`. ```kotlin fun createBlur(type: BlurType): DesignBlock ``` Create a new blur, fails if type is unknown or not a valid blur type. - `type`: the type id of the block. - Returns the handle of the newly created blur. We currently support the following blur types: - `BlurType.Uniform` - `BlurType.Linear` - `BlurType.Mirrored` - `BlurType.Radial` ```kotlin highlight-BlockApi-createBlur // Create and configure a radial blur val radialBlur = engine.block.createBlur(type = BlurType.Radial) ``` ## Configuring a Blur You can configure blurs just like you configure design blocks. See [Modify Properties](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for more detail. ```kotlin highlight-configureBlur engine.block.setFloat(radialBlur, property = "radial/radius", value = 100F) engine.block.setBlur(block, blurBlock = radialBlur) engine.block.setBlurEnabled(block, enabled = true) val isBlurEnabled = engine.block.isBlurEnabled(block) ``` ## Functions ```kotlin fun setBlur( block: DesignBlock, blurBlock: DesignBlock, ) ``` Connects `block`'s blur to the given `blurBlock`. Required scope: "appearance/blur" - `block`: the block to update. - `blurBlock`: a blur block. ```kotlin fun setBlurEnabled( block: DesignBlock, enabled: Boolean, ) ``` Enable or disable the blur of the given design block. - `block`: the block to update. - `enabled`: the new enabled value. ```kotlin fun isBlurEnabled(block: DesignBlock): Boolean ``` Query if blur is enabled for the given block. - `block`: the block to query. - Returns true if the blur is enabled, false otherwise. ```kotlin fun supportsBlur(block: DesignBlock): Boolean ``` Checks whether the block supports blur. - `block`: the block to query. - Returns true if the block supports blur, false otherwise. ```kotlin fun getBlur(block: DesignBlock): DesignBlock ``` Get the blur block of the given design block. - `block`: the block to query. - Returns the blur block. ## Full Code Here's the full code: ```kotlin // Create and configure a radial blur val radialBlur = engine.block.createBlur(type = BlurType.Radial) engine.block.setFloat(radialBlur, property = "radial/radius", value = 100F) engine.block.setBlur(block, blurBlock = radialBlur) engine.block.setBlurEnabled(block, enabled = true) val isBlurEnabled = engine.block.isBlurEnabled(block) // Access an existing blur if (engine.block.supportsBlur(block)) { val existingBlur = engine.block.getBlur(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: "Chroma Key (Green Screen) in Android (Kotlin)" description: "Use CE.SDK's green/blue screen keyer to replace backgrounds, tune edges & spill, and composite subjects over virtual scenes." platform: android url: "https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/android/guides-8d8b00/) > [Filters and Effects](https://img.ly/docs/cesdk/android/filters-and-effects-6f88ac/) > [Apply Chroma Key (Green Screen)](https://img.ly/docs/cesdk/android/filters-and-effects/chroma-key-green-screen-1e3e99/) --- Chroma keying removes a uniform background color (often green or blue) from a video or image so you can composite the foreground over a new scene. In CE.SDK for Android, chroma keying is an **effect** you attach to an image or video block, with parameters for **color selection**, **similarity threshold**, **edge smoothing**, and **spill suppression**. This guide walks you through applying the effect in Kotlin, dialing it in for clean edges, and composing the keyed result with a replacement background. ## What You'll Learn - How to add the **Green Screen** effect to **image** and **video** blocks. - How to set the key color (green by default, but any color works). - How to tune **colorMatch** (similarity), **smoothness** (edge falloff), and **spill** (desaturating color cast). - How to layer a new background behind the keyed subject. - How to persist, export, and protect templates that include chroma key. ## When to Use It Use chroma key when your source contains a uniform backdrop (green, blue, or a solid brand color) and you want to: - Replace the background with a **virtual set**, branded plate, or blurred depth backdrop. - Place talent over **slides** or **product footage**. - Standardize a team's talking‑head videos with consistent backgrounds. - Composite when using an asset formats such as MP4, H.264 or, JPEG that don't support transparency. Avoid chroma key if the subject's clothing, props, or lighting contains the same hue as your key color, or if the background is highly textured. > **Chroma Key vs. Background Removal:** The `effect/green_screen` shader operates on color similarity directly on the GPU. Unlike AI-based background removal, chroma keying provides predictable, real‑time control for studio footage where lighting and backdrop color are controlled. ## Apply the Green Screen Effect In a Prebuilt Editor Chroma key is one of the standard effects available for images and video clips in the prebuilt editors, such as the Design Editor and the Video Editor. Use it as follows: 1. Select a key image or video clip. 2. Look for the `Effects` button in the inspector and tap it. ![Location of the Effect button in the Inspector](assets/chroma-key-ios-159-0.png) Scroll through the effects until you find "Green Screen". Once you tap it, the effect implements immediately. ![Arrow pointing to the Green Screen effect button](assets/chroma-key-ios-159-1.png) An options indicator appears for the effect. Tap it to show the options. ![Green screen effect button showing options indicator](assets/chroma-key-ios-159-2.png) Use the sliders and the color wheel, to change the settings for: - key color - color match - smoothness - spill ![Effect controls for key color, color match, smoothness and, spill](assets/chroma-key-ios-159-3.png) The "Tuning the Effect" section below explains each of these in detail. ## Apply the Green Screen Effect In Code CE.SDK exposes chroma key as the `EffectType.GreenScreen` effect type with the following key properties: - `effect/green_screen/fromColor` the color to key out (default green). - `effect/green_screen/colorMatch` similarity threshold \[0…1]. - `effect/green_screen/smoothness` edge falloff \[0…1]. - `effect/green_screen/spill` desaturates remaining color spill \[0…1]. ### Key an Image Block ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Color import ly.img.engine.EffectType import ly.img.engine.Engine fun applyGreenScreenToImage( engine: Engine, imageBlock: Int ) = CoroutineScope(Dispatchers.Main).launch { // 1) Create the effect and attach it to the block val keyer = engine.block.createEffect(type = EffectType.GreenScreen) engine.block.appendEffect(imageBlock, effectBlock = keyer) // 2) Choose the key color (here: pure green); any color works engine.block.setColor( keyer, property = "effect/green_screen/fromColor", color = Color.fromRGBA(r = 0.0f, g = 1.0f, b = 0.0f, a = 1.0f) ) // 3) Tune similarity, smoothness, and spill engine.block.setFloat(keyer, property = "effect/green_screen/colorMatch", value = 0.40f) engine.block.setFloat(keyer, property = "effect/green_screen/smoothness", value = 0.08f) engine.block.setFloat(keyer, property = "effect/green_screen/spill", value = 0.15f) } ``` ### Key a Video Block Video blocks use video fills instead of image fills, but the rest of the workflow is identical. ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ly.img.engine.Color import ly.img.engine.EffectType import ly.img.engine.Engine fun applyGreenScreenToVideo( engine: Engine, videoBlock: Int ) = CoroutineScope(Dispatchers.Main).launch { val keyer = engine.block.createEffect(type = EffectType.GreenScreen) engine.block.appendEffect(videoBlock, effectBlock = keyer) // Blue screen example engine.block.setColor( keyer, property = "effect/green_screen/fromColor", color = Color.fromRGBA(r = 0.0f, g = 0.25f, b = 1.0f, a = 1.0f) ) engine.block.setFloat(keyer, property = "effect/green_screen/colorMatch", value = 0.35f) engine.block.setFloat(keyer, property = "effect/green_screen/smoothness", value = 0.10f) engine.block.setFloat(keyer, property = "effect/green_screen/spill", value = 0.25f) } ``` Order matters: if you add other effects, like color adjustments, place the **keyer first** in the stack so later effects operate on the premultiplied result. ### Pick the Key Color from the Image Hard‑coding `fromColor` works for controlled shoots. In general, sample the background color under the user's tap. > **Note:** CE.SDK doesn't provide a built-in API to read a pixel at a screen coordinate. In your Android app, map the tap location to the image/video buffer you control and sample the pixel using APIs such as `Bitmap.getPixel()` or `Canvas`. Convert the sampled RGBA to `Color.fromRGBA()` and set `effect/green_screen/fromColor`. If you embed the `DesignEditor`, keep an app-level copy of the media to sample from, since the editor's preview is GPU-rendered. Tie the sampled color back to the effect: ```kotlin import ly.img.engine.Color import ly.img.engine.Engine fun setKeyColor(engine: Engine, keyer: Int, r: Float, g: Float, b: Float) { engine.block.setColor( keyer, property = "effect/green_screen/fromColor", color = Color.fromRGBA(r = r, g = g, b = b, a = 1.0f) ) } ``` For polished UIs, show a zoomed loupe and a live matte preview as the user drags. ### Composite over a Replacement Background A keyed subject is transparent where the background was, so you **layer a background block beneath** the keyed block. ```kotlin import ly.img.engine.DesignBlockType import ly.img.engine.Engine import ly.img.engine.FillType import ly.img.engine.ShapeType fun addBackgroundBehind(engine: Engine, page: Int, subject: Int, imageUrl: String) { val bg = engine.block.create(DesignBlockType.Graphic) val shape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(bg, shape = shape) val fill = engine.block.createFill(FillType.Image) engine.block.setString(fill, property = "fill/image/imageFileURI", value = imageUrl) engine.block.setFill(bg, fill = fill) // Make background full‑bleed on the page // Place background **behind** subject engine.block.insertChild(parent = page, child = bg, index = 0) engine.block.fillParent(bg) engine.block.sendToBack(bg) } ``` For video, create a video fill instead of an image fill and align durations in your export. ## Tuning the Effect The three parameters for tuning chroma key composition are: - color match - spill - smoothness Knowing what they impact can help decide your strategy when the composition doesn't look correct. The examples below all show how these values can change this chroma key image. ![Example composited image.](assets/chroma-key-ios-159-4.png) > **Recommended Starting Values:** | Background | colorMatch | smoothness | spill | > |-------------|-------------|------------|--------| > | **Green Screen** | 0.35–0.45 | 0.08–0.12 | 0.15–0.25 | > | **Blue Screen** | 0.30–0.40 | 0.10–0.15 | 0.25–0.35 | > | **Custom Color** | 0.40–0.50 | 0.08–0.12 | 0.10–0.20 |Tune `colorMatch` first for coverage, then refine edge softness with `smoothness`, and finally correct color tint with `spill`. ### Color Match Color Match determines how close a pixel's color has to be to the key color to be considered *background*. When the value is low, only exact matches are removed. When the value is high, a larger range of colors similar to the key color get removed. What to watch for when the value is wrong: - Too low: you may see patches of the green screen still visible around edges, especially if lighting is uneven or shadows present. - Too high: you risk keying out part of the subject (hair strands, clothing edges, reflective items) creating holes or transparency because the effect is too aggressive. ![Color match range examples.](assets/color-match-range-ios.jpg) The preceding image shows color match values of 0.0, 0.5 and 1.0. ### Smoothness Smoothness controls how gradually or sharply the transitions occur, how soft the matte edges of the gradients are. A low value produces sharp transition between keyed and un-keyed areas. When the value is high, there is softer transition. What to watch for when the value is wrong: - Too low: harsh edges, visible fringes around hair or "hard cutouts" that look unnatural. - Too high: a halo effect or the subject blends into the background. ![Smoothness range examples.](assets/smoothness-range-ios.jpg) The preceding image shows smoothness values of 0.0, 0.5 and 1.0. ### Spill Spill impacts the unwanted "color spill", when your key color reflects or bleeds onto the subject. This is especially noticeable around edges, hair and, shiny objects. What to watch for when the value is wrong: - Too low: you may see green reflection on the subject (especially edges/hair/shoulders) that doesn't get cleaned up, making it look unnatural or floating. - Too high: the subject's actual color edges are desaturated, making hair or detail look gray, faded or too soft. ![Spill range examples.](assets/spill-range-ios.jpg) The preceding image shows spill values of 0.0, 0.5 and 1.0. ## Lighting & Capture Tips - Keep your backdrop evenly lit and 1–2 stops brighter than your subject. - Avoid shadows or wrinkles. Uneven color creates transparency artifacts. - Separate your subject from the background by at least 1 m to reduce spill. ## Template & Scope Considerations If you ship templates that include a keyer, you might want to lock down parameters to protect quality: - Use **Scopes/Permissions** to limit which effect properties the end‑user can change. - Store platform‑tested defaults (match, smoothness, spill) in the template. - Provide preset chips like **"Green Screen"**, **"Blue Screen"**, **"Brand Cyan"** to switch `fromColor` quickly. ## Performance and Rendering Pipeline CE.SDK runs chroma keying directly on the graphics card for smooth, real-time results. Place the keyer near the start of your effect list so that later effects, like color or tone adjustments, apply correctly to the transparent areas. To keep playback fast, avoid heavy effects such as blur or LUTs before the keyer. ## Export Tips - Prefer **ProRes 4444** (or other alpha‑carrying formats) when exporting an intermediate keyed asset to reuse elsewhere. - For final composites, export with the background enabled and a standard delivery codec/format. ## Testing Checklist - Verify background color is uniform and well lit. - Check for reflective surfaces that might cause spill. - Test both **720p** and **4K** previews to compare performance. - Try different wardrobe colors. Avoid those close to the key color. - Examine edges on hair or fine detail under motion. - Validate output formats (e.g., MP4 with solid background vs. ProRes with alpha). ## Troubleshooting **❌ Holes in the matte (background not fully removed)**: - Increase `colorMatch` slightly. If edges get harsh, bump `smoothness` too. **❌ Foreground punched out (you lose subject detail)**: - Lower `colorMatch` until detail returns; then reduce `spill` if the subject appears tinted. **❌ Green/blue color cast on edges**: - Raise `spill` (try 0.2–0.4). If it looks gray, back it down. **❌ Jagged edges**: - Increase `smoothness` in small steps (0.05–0.15). - Consider adding a light `effect/blur` **after** the keyer for video. **❌ Uneven backgrounds / shadows**: - Sample a darker patch of the backdrop or increase `colorMatch` and compensate with `spill`. **❌ Nothing turns transparent:** - Verify the effect is attached to the **right block** and not to the page. - Check `fromColor` is close to the actual backdrop hue (sample it!). - Ensure your block type supports effects (graphic, video are supported). **❌ Performance drops with 4K video**: - Avoid stacking extra heavy effects **before** the keyer. - Render proxies or downscale the preview while tuning; export at full res. **❌ Skin tones look dull**: - Reduce `spill` and re‑tune `colorMatch`. **❌ Hair/fur looks crunchy:** - Raise `smoothness` incrementally (and consider light post‑blur). ## Next Steps With the core of chroma key compositing mastered, here are some other topics that may be interesting: - Learn about other [Filters & Effects](https://img.ly/docs/cesdk/android/filters-and-effects/overview-299b15/) and try combining the keyer with adjustments for color matching. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add a Filter or Effect" description: "Programmatically or manually add effects to design elements to modify their visual style." 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-using-effects/UsingEffects.kt reference-only import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 usingEffects( 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) 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.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block, fill = fill) engine.block.supportsEffects(scene) // Returns false engine.block.supportsEffects(block) // Returns true val pixelize = engine.block.createEffect(type = EffectType.Pixelize) val adjustments = engine.block.createEffect(type = EffectType.Adjustments) engine.block.appendEffect(block, effectBlock = pixelize) engine.block.insertEffect(block, effectBlock = adjustments, index = 0) // engine.block.removeEffect(rect, index = 0) // This will return [adjustments, pixelize] val effectsList = engine.block.getEffects(block) val unusedEffect = engine.block.createEffect(type = EffectType.HalfTone) engine.block.destroy(unusedEffect) val allPixelizeProperties = engine.block.findAllProperties(pixelize) val allAdjustmentProperties = engine.block.findAllProperties(adjustments) engine.block.setInt(pixelize, property = "pixelize/horizontalPixelSize", value = 20) engine.block.setFloat(adjustments, property = "effect/adjustments/brightness", value = 0.2F) engine.block.setEffectEnabled(pixelize, enabled = false) engine.block.setEffectEnabled(pixelize, !engine.block.isEffectEnabled(pixelize)) engine.stop() } ``` Some [design blocks](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) in CE.SDK such as pages and graphic blocks allow you to add effects to them. An effect can modify the visual output of a block's [fill](https://img.ly/docs/cesdk/android/fills-402ddc/). CreativeEditor SDK supports many different types of effects, such as adjustments, LUT filters, pixelization, glow, vignette and more. Similarly to blocks, each effect instance has a numeric id which can be used to query and [modify its properties](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/). We create a scene containing a graphic block with an image fill and want to apply effects to this image. ```kotlin highlight-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) 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.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block, fill = fill) ``` ## Accessing Effects Not all types of design blocks support effects, so you should always first call the `fun supportsEffects(block: DesignBlock): Boolean` API before accessing any of the following APIs. ```kotlin highlight-supportsEffects engine.block.supportsEffects(scene) // Returns false engine.block.supportsEffects(block) // Returns true ``` ## Creating an Effect In order to add effects to our block, we first have to create a new effect instance, which we can do by calling `fun createEffect(type: EffectType): DesignBlock` and passing it the type of effect that we want. In this example, we create a pixelization and an adjustment effect. Please refer to [API Docs](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for a complete list of supported effect types. ```kotlin highlight-createEffect val pixelize = engine.block.createEffect(type = EffectType.Pixelize) val adjustments = engine.block.createEffect(type = EffectType.Adjustments) ``` ## Adding Effects Now we have two effects but the output of our scene looks exactly the same as before. That is because we still need to append these effects to the graphic design block's list of effects, which we can do by calling `fun appendEffect(block: DesignBlock, effectBlock: DesignBlock)`. We can also insert or remove effects from specific indices of a block's effect list using the `fun insertEffect(block: DesignBlock, effectBlock: DesignBlock, index: Int)` and `fun removeEffect(block: DesignBlock, index: Int)` APIs. Effects will be applied to the block in the order they are placed in the block's effects list. If the same effect appears multiple times in the list, it will also be applied multiple times. In our case, the adjustments effect will be applied to the image first, before the result of that is then pixelated. ```kotlin highlight-addEffect engine.block.appendEffect(block, effectBlock = pixelize) engine.block.insertEffect(block, effectBlock = adjustments, index = 0) // engine.block.removeEffect(rect, index = 0) ``` ## Querying Effects Use the `fun getEffects(block: DesignBlock): List` API to query the ordered list of effect ids of a block. ```kotlin highlight-getEffects // This will return [adjustments, pixelize] val effectsList = engine.block.getEffects(block) ``` ## Destroying Effects If we created an effect that we don't want anymore, we have to make sure to destroy it using the same `fun destroy(block: DesignBlock)` API that we also call for design blocks. Effects that are attached to a design block will be automatically destroyed when the design block is destroyed. ```kotlin highlight-destroyEffect val unusedEffect = engine.block.createEffect(type = EffectType.HalfTone) engine.block.destroy(unusedEffect) ``` ## Effect Properties Just like design blocks, effects with different types have different properties that you can query and modify via the API. Use `fun findAllProperties(block: DesignBlock): List` in order to get a list of all properties of a given effect. Please refer to the [API Docs](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/) for a complete list of all available properties for each type of effect. ```kotlin highlight-getProperties val allPixelizeProperties = engine.block.findAllProperties(pixelize) val allAdjustmentProperties = engine.block.findAllProperties(adjustments) ``` Once we know the property keys of an effect, we can use the same APIs as for design blocks in order to [modify those properties](https://img.ly/docs/cesdk/android/concepts/blocks-90241e/). Our adjustment effect here for example will not modify the output unless we at least change one of its adjustment properties - such as the brightness - to not be zero. ```kotlin highlight-modifyProperties engine.block.setInt(pixelize, property = "pixelize/horizontalPixelSize", value = 20) engine.block.setFloat(adjustments, property = "effect/adjustments/brightness", value = 0.2F) ``` ## Disabling Effects You can temporarily disable and enable the individual effects using the `fun setEffectEnabled(effectBlock: DesignBlock, enabled: Boolean)` API. When the effects are applied to a block, all disabled effects are simply skipped. Whether an effect is currently enabled or disabled can be queried with `fun isEffectEnabled(effectBlock: DesignBlock): Boolean`. ```kotlin highlight-disableEffect engine.block.setEffectEnabled(pixelize, enabled = false) engine.block.setEffectEnabled(pixelize, !engine.block.isEffectEnabled(pixelize)) ``` ## Full Code Here's the full code: ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 usingEffects( 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 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.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.setFill(block, fill = fill) engine.block.supportsEffects(scene) // Returns false engine.block.supportsEffects(block) // Returns true val pixelize = engine.block.createEffect(type = EffectType.Pixelize) val adjustments = engine.block.createEffect(type = EffectType.Adjustments) engine.block.appendEffect(block, effectBlock = pixelize) engine.block.insertEffect(block, effectBlock = adjustments, index = 0) // engine.block.removeEffect(rect, index = 0) // This will return [adjustments, pixelize] val effectsList = engine.block.getEffects(block) val unusedEffect = engine.block.createEffect(type = EffectType.HalfTone) engine.block.destroy(unusedEffect) val allPixelizeProperties = engine.block.findAllProperties(pixelize) val allAdjustmentProperties = engine.block.findAllProperties(adjustments) engine.block.setInt(pixelize, property = "pixelize/horizontalPixelSize", value = 20) engine.block.setFloat(adjustments, property = "effect/adjustments/brightness", value = 0.2F) engine.block.setEffectEnabled(pixelize, enabled = false) engine.block.setEffectEnabled(pixelize, !engine.block.isEffectEnabled(pixelize)) 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: "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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 customLUTFilter( 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 = 100F) engine.block.setHeight(page, value = 100F) engine.block.appendChild(parent = scene, child = page) engine.scene.zoomToBlock( scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) val rect = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(rect, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(rect, value = 100F) engine.block.setHeight(rect, value = 100F) engine.block.appendChild(parent = page, child = rect) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) val lutFilter = engine.block.createEffect(EffectType.LutFilter) engine.block.setBoolean(lutFilter, property = "effect/enabled", value = true) engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.9F) @Suppress("ktlint:standard:max-line-length") val lutUri = "https://cdn.img.ly/packages/imgly/cesdk-js/1.76.0/assets/extensions/ly.img.cesdk.filters.lut/LUTs/imgly_lut_ad1920_5_5_128.png" engine.block.setString( 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.appendEffect(rect, effectBlock = lutFilter) engine.block.setFill(rect, fill = imageFill) engine.stop() } ``` We use a technology called Lookup Tables (LUTs) in order to add new filters to our SDK. The main idea is that colors respond to operations that are carried out during the filtering process. We 'record' that very response by applying the filter to the identity image shown below. Identity LUT The resulting image can be used within our SDK and the recorded changes can then be applied to any image by looking up the transformed colors in the modified LUT. If you want to create a new filter, you'll need to [download](content-assets/6e3f49/imgly_lut_ad1920_5_5_128.png) the identity LUT shown above, load it into an image editing software of your choice, apply your operations, save it and add it to your app. > **WARNING:** As any compression artifacts in the edited LUT could lead to distorted results when applying the filter, you need to save your LUT as a PNG file. ## Using Custom Filters In this example, we will use a hosted CDN LUT filter file. First we will load one of our demo scenes and change the first image to use LUT filter we will provide. We will also configure the necessary setting based on the file. LUT file we will use: Color grading LUT showcasing a grid of color variations used for applying a specific visual style to images. ## Load Scene After the setup, we create a new scene. Within this scene, we create a page, set its dimensions, and append it to the scene. Lastly, we adjust the zoom level to properly fit the page into the view. ```kotlin highlight-load-scene val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 100F) engine.block.setHeight(page, value = 100F) engine.block.appendChild(parent = scene, child = page) engine.scene.zoomToBlock( scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) ``` ## Create Rectangle Next, we create a rectangle with defined dimensions and append it to the page. We will apply our LUT filter to this rectangle. ```kotlin highlight-create-rect val rect = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(rect, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(rect, value = 100F) engine.block.setHeight(rect, value = 100F) engine.block.appendChild(parent = page, child = rect) ``` ## Load Image After creating the rectangle, we create an image fill with a specified URL. This will load the image as a fill for the rectangle to which we will apply the LUT filter. ```kotlin highlight-create-image-fill val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) ``` ## Create LUT Filter Now, we create a Look-Up Table (LUT) filter effect. We enable the filter, set its intensity, and provide a URL for the LUT file. We also define the tile count for the filter. The LUT filter effect is then applied to the rectangle and image should appear black and white. ```kotlin highlight-create-lut-filter val lutFilter = engine.block.createEffect(EffectType.LutFilter) engine.block.setBoolean(lutFilter, property = "effect/enabled", value = true) engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.9F) @Suppress("ktlint:standard:max-line-length") val lutUri = "https://cdn.img.ly/packages/imgly/cesdk-js/1.76.0/assets/extensions/ly.img.cesdk.filters.lut/LUTs/imgly_lut_ad1920_5_5_128.png" engine.block.setString( 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) ``` ## Apply LUT Filter Finally, we apply the LUT filter effect to the rectangle, and set the image fill to the rectangle. Before setting an image fill, we destroy the default rectangle fill. ```kotlin highlight-apply-lut-filter engine.block.appendEffect(rect, effectBlock = lutFilter) engine.block.setFill(rect, fill = imageFill) ``` ## Full Code Here's the full code: ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 customLUTFilter( 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 page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 100F) engine.block.setHeight(page, value = 100F) engine.block.appendChild(parent = scene, child = page) engine.scene.zoomToBlock( scene, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) val rect = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(rect, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setWidth(rect, value = 100F) engine.block.setHeight(rect, value = 100F) engine.block.appendChild(parent = page, child = rect) val imageFill = engine.block.createFill(FillType.Image) engine.block.setString( imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) val lutFilter = engine.block.createEffect(EffectType.LutFilter) engine.block.setBoolean(lutFilter, property = "effect/enabled", value = true) engine.block.setFloat(lutFilter, property = "effect/lut_filter/intensity", value = 0.9F) @Suppress("ktlint:standard:max-line-length") val lutUri = "https://cdn.img.ly/packages/imgly/cesdk-js/$UBQ_VERSION$/assets/extensions/ly.img.cesdk.filters.lut/LUTs/imgly_lut_ad1920_5_5_128.png" engine.block.setString( 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.appendEffect(rect, effectBlock = lutFilter) engine.block.setFill(rect, fill = imageFill) 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: "Android Filters & Effects SDK" 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/) --- 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. You can apply both filters and effects through the user interface or programmatically using the CE.SDK API. They allow you to refine the look of images, videos, and graphic elements in your designs with precision and flexibility. [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: "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:** [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) > [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 ### 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/) ### 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/) --- ## Related Pages - [MCP Server](https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/) - Connect AI assistants to CE.SDK documentation using the Model Context Protocol (MCP) server. - [LLMs.txt](https://img.ly/docs/cesdk/android/llms-txt-eb9cc5/) - Our documentation is available in LLMs.txt format --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Clone GitHub Project" description: "Using CE.SDK with a cloned Android GitHub project" platform: android url: "https://img.ly/docs/cesdk/android/get-started/clone-github-project-f9890l/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). --- This guide will walk you through cloning an existing sample project with the CE.SDK editor already set up ## Pre-requisites - Android Studio installed on your machine - A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)), use `null` or an empty string to run in evaluation mode with watermark. ## Clone the GitHub Repository Launch Android Studio and select `File -> New -> Project from version control` Next, add the following URL: ``` https://github.com/imgly/cesdk-android-examples.git ``` ![Android Studio Clone dialog](assets/CloneDialog.png) Click "Clone" and wait for the project to be downloaded and set up. ## Run the Project In the `local.properties` file, add your CE.SDK license key (or leave it empty to run in evaluation mode with watermark) ``` license=MY_LICENSE_GOES_HERE ``` You may have to create this file, if Android Studio did not generate it for you. Finally, run the app on your device or android emulator. ## Common Errors Here are some common errors you may encounter through this guide, and how to solve them. #### Invalid License | ![Invalid license dialog](assets/InvalidLicense.png) | ![Missing license dialog](assets/MissingLicense.png) | | ---------------------------------------------------- | ---------------------------------------------------- | | | | **Solution** -> Check whether you have supplied a valid license #### No Internet ![No internet dialog](assets/NoInternet.png) **Solution** -> Check your internet connection --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Existing Project Setup" description: "Learn how to integrate the CreativeEditor SDK into your existing Android Jetpack Compose project" platform: android url: "https://img.ly/docs/cesdk/android/get-started/existing-project-g0901m/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). --- ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/settings.gradle.kts reference-only pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name = "IMG.LY Artifactory" url = uri("https://artifactory.img.ly/artifactory/maven") mavenContent { includeGroup("ly.img") } } } } rootProject.name = "My App" include(":app") ``` ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/build.gradle.kts reference-only plugins { id("com.android.application") id("kotlin-android") } android { namespace = "com.example.cesdkapp" compileSdk = 36 defaultConfig { applicationId = "com.example.cesdkapp" minSdk = 24 targetSdk = 36 versionCode = 1 versionName = "1.0" ndk { abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } } dependencies { // This dependency makes main compose and coroutine APIs available in your project implementation("ly.img:editor:1.76.0") // Other dependencies here } ``` This guide shows you how to integrate the CreativeEditor SDK into your existing Android Jetpack Compose project. Follow these steps to learn how to: - **add** the necessary dependencies. - **configure** the editor. - **test** the integration. ## Who Is This Guide For? This guide is for developers who: - Have an existing Android Jetpack Compose project - Want to add CreativeEditor SDK features to their app - Need to understand common integration patterns - Want to test available editing capabilities and workflows ## What You'll Achieve By following this guide, you'll perform the following tasks: - **Project Integration**: Add CreativeEditor SDK to your existing Android project - **Editor Implementation**: Implement the editor with proper lifecycle management - **Testing**: Verify the integration works correctly - **Customization**: Learn how to customize the editor for your use case [View Android Examples](https://github.com/imgly/cesdk-android-examples) [Android Documentation](https://img.ly/docs/cesdk/android) ## Prerequisites ### Development Environment - Android Studio (latest version) - Android SDK (API level 24 or higher) - Kotlin 1.9.10 or higher - Gradle 8.4 or later - Jetpack Compose BOM 2023.05.01 or higher ### Platform Requirements - **Android**: API level 24 (Android 7.0) or higher - **Supported ABIs**: arm64-v8a, armeabi-v7a, x86\_64, x86 ### License - A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)), use `null` or an empty string to run in evaluation mode with watermark. ## Verify Your Setup Before starting, verify your Android development environment: ```bash gradle --version ``` This command checks your Gradle installation and reports any issues to resolve before proceeding. > **Note:** You can customize the CreativeEditor SDK for Android exclusively through > native code (Kotlin), as described in the > [configuration overview section](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/). ## Step 1: Add the CreativeEditor SDK Dependency Add the CreativeEditor SDK to your project by updating the build configuration: ### 1.1 Add IMG.LY Repository Update your `settings.gradle.kts` to include the IMG.LY repository: ```kotlin highlight-maven-dependency dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name = "IMG.LY Artifactory" url = uri("https://artifactory.img.ly/artifactory/maven") mavenContent { includeGroup("ly.img") } } } } ``` ### 1.2 Add Editor Dependency Update your `app/build.gradle.kts` to include the editor dependency: ```kotlin highlight-dependency dependencies { // This dependency makes main compose and coroutine APIs available in your project implementation("ly.img:editor:1.76.0") // Other dependencies here } ``` This adds the latest version of the CreativeEditor SDK editor to your `build.gradle.kts` file. ### 1.3 Configure Android Settings Update your `app/build.gradle.kts` to ensure proper configuration: ```kotlin highlight-build-android android { namespace = "com.example.cesdkapp" compileSdk = 36 defaultConfig { applicationId = "com.example.cesdkapp" minSdk = 24 targetSdk = 36 versionCode = 1 versionName = "1.0" ndk { abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } } ``` ### 1.4 Sync Project After adding the dependency, sync your project to download the CreativeEditor SDK: In Android Studio, click **Sync Project with Gradle Files** to download and configure all dependencies. This downloads and installs the CreativeEditor SDK and its dependencies automatically through Gradle. ## Step 2: Implement the Editor Integration Now let's implement the CreativeEditor SDK editor in your Android Jetpack Compose application: ### 2.1 Create EditorComposable Create a new file `EditorComposable.kt` in your project: ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/EditorComposable.kt import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.engine.DesignBlockType // Add this composable to your Activity, Fragment, NavHost etc. @Composable fun EditorComposable(onClose: (Throwable?) -> Unit) { val context = LocalContext.current Editor( // Get your license from https://img.ly/forms/free-trial // Keep this null for evaluation mode with watermark. // Replace it with your license key for production use. license = null, configuration = { EditorConfiguration.remember { onCreate = { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) } onError = { Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show() } } }, onClose = onClose, ) } ``` In this example, we create a scene with a single square page. ### 2.2 Include EditorComposable in your navigation system Update your navigation system to use the editor composable. The example below shows how to integrate it in `NavHost`: ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/MainActivity.kt import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val navController = rememberNavController() NavHost( navController = navController, startDestination = "main", ) { composable(route = "main") { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Button( onClick = { navController.navigate("editor") }, ) { Text("Launch Creative Editor SDK") } } } composable(route = "editor") { EditorComposable { navController.popBackStack() } } } } } } ``` ### 2.3 Add the Internet Permission Add the network permission that CE.SDK needs when it loads its default assets from the CDN: ```xml ``` ## Step 3: Test Your Editor Integration Now let's test your CreativeEditor SDK editor integration: ### 3.1 Build and Run Build your Android project using Gradle: ### 3.2 Test on Android Device Connect your Android device and run: ```bash # Build and install the app ./gradlew installDebug # Or run from Android Studio # Click the "Run" button (green play icon) ``` ### 3.3 Verify Features After launching your app, verify these features work correctly: - **App Launch**: The main activity opens without errors - **Main Screen**: A clean Android Jetpack Compose interface - **Button Interaction**: The "Launch Creative Editor SDK" button responds to taps - **Editor Launch**: The CreativeEditor SDK opens without errors - **Canvas Interaction**: The blank page can be panned and zoomed. ## Step 4: Customize for Your Use Case > **Note:** Choose your integration approach based on your development needs.* **[Starter Kits](/android/starter-kits-d0ed07/)** - Use one of our pre-built editors (photo, video, design) and configure them to match your needs. > * **[Custom Editor](/android/guides/starter-kits/custom-editor-3a5c9b/)** - Build your editor from the ground up as flexibly as possible using CE.SDK's powerful APIs. ## Step 5: Troubleshooting ### Common Issues and Solutions #### Gradle Sync Issues - **Problem**: Repository not found - **Solution**: Verify the IMG.LY repository URL in `settings.gradle.kts` #### Compilation Errors - **Problem**: Missing dependencies - **Solution**: Ensure you've installed all Jetpack Compose dependencies. #### Runtime Errors - **Problem**: Invalid license - **Solution**: Verify your license key is correct and valid (or pass `null` for evaluation mode with watermark) #### Performance Issues - **Problem**: Slow editor loading - **Solution**: Check device specifications and memory usage #### Integration Issues - **Problem**: Editor not displaying - **Solution**: Verify the composable is properly called in your activity ## Step 6: Next Steps ### Advanced Features - Implement custom asset sources - Add custom filters and effects - Integrate with your backend services - Implement user authentication ### Other Integrations - Explore camera integration - Add video editing capabilities - Implement batch processing - Add cloud storage integration ### Production Considerations - Optimize for performance - Implement proper error handling - Add analytics and monitoring - Test on different device configurations ## Additional Resources - [CreativeEditor SDK Documentation](https://img.ly/docs/cesdk/android) - [Android Examples Repository](https://github.com/imgly/cesdk-android-examples) - [Jetpack Compose Documentation](https://developer.android.com/jetpack/compose) - [Android Development Guide](https://developer.android.com/guide) --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "MCP Server" description: "Connect AI assistants to CE.SDK documentation using the Model Context Protocol (MCP) server." platform: android url: "https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). **Navigation:** [Get Started](https://img.ly/docs/cesdk/android/get-started/overview-e18f40/) > [Build with AI](https://img.ly/docs/cesdk/android/get-started/build-with-ai-k7m9p2/) > [MCP Server](https://img.ly/docs/cesdk/android/get-started/mcp-server-fde71c/) --- The CE.SDK MCP server provides a standardized interface that allows any compatible AI assistant to search and access our documentation. This enables AI tools like Claude, Cursor, and VS Code Copilot to provide more accurate, context-aware help when working with CE.SDK. ## What is MCP? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to securely connect to external data sources. By connecting your AI tools to our MCP server, you get: - **Accurate answers**: AI assistants can search and retrieve the latest CE.SDK documentation - **Context-aware help**: Get platform-specific guidance for your development environment - **Up-to-date information**: Always access current documentation without relying on training data ## Available Tools The MCP server exposes two tools: | Tool | Description | | -------- | --------------------------------------------- | | `search` | Search documentation by query string | | `fetch` | Retrieve the full content of a document by ID | ## Server Endpoint | URL | Transport | | ------------------------ | --------------- | | `https://mcp.img.ly/mcp` | Streamable HTTP | No authentication is required. ## Setup Instructions ### Claude Code Add the MCP server with a single command: ```bash claude mcp add --transport http imgly_docs https://mcp.img.ly/mcp ``` ### Claude Desktop 1. Open Claude Desktop and go to **Settings** (click your profile icon) 2. Navigate to **Connectors** in the sidebar 3. Click **Add custom connector** 4. Enter the URL: `https://mcp.img.ly/mcp` 5. Click **Add** to connect ### Cursor Add the following to your Cursor MCP configuration. You can use either: - **Project-specific**: `.cursor/mcp.json` in your project root - **Global**: `~/.cursor/mcp.json` ```json { "mcpServers": { "imgly_docs": { "url": "https://mcp.img.ly/mcp" } } } ``` ### VS Code Add to your workspace configuration at `.vscode/mcp.json`: ```json { "servers": { "imgly_docs": { "type": "http", "url": "https://mcp.img.ly/mcp" } } } ``` ### Windsurf Add the following to your Windsurf MCP configuration at `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "imgly_docs": { "serverUrl": "https://mcp.img.ly/mcp" } } } ``` ### Other Clients For other MCP-compatible clients, use the endpoint `https://mcp.img.ly/mcp` with HTTP transport. Refer to your client's documentation for the specific configuration format. ## Usage Once configured, your AI assistant will automatically have access to CE.SDK documentation. You can ask questions like: - "How do I add a text block in CE.SDK?" - "Show me how to export a design as PNG" - "What are the available blend modes?" The AI will search our documentation and provide answers based on the latest CE.SDK guides and API references. --- ## More Resources - **[Android Documentation Index](https://img.ly/docs/cesdk/android.md)** - Browse all Android documentation - **[Complete Documentation](https://img.ly/docs/cesdk/android/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/android/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "New Project Setup" description: "Learn how to integrate the CreativeEditor SDK into a new Android Activity-based project" platform: android url: "https://img.ly/docs/cesdk/android/get-started/new-activity-based-ui-project-d7678j/" --- > This is one page of the CE.SDK Android documentation. For a complete overview, see the [Android Documentation Index](https://img.ly/docs/cesdk/android.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/android/llms-full.txt). --- ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/settings.gradle.kts reference-only pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name = "IMG.LY Artifactory" url = uri("https://artifactory.img.ly/artifactory/maven") mavenContent { includeGroup("ly.img") } } } } rootProject.name = "My App" include(":app") ``` ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/build.gradle.kts reference-only plugins { id("com.android.application") id("kotlin-android") } android { namespace = "com.example.cesdkapp" compileSdk = 36 defaultConfig { applicationId = "com.example.cesdkapp" minSdk = 24 targetSdk = 36 versionCode = 1 versionName = "1.0" ndk { abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } } dependencies { // This dependency makes main compose and coroutine APIs available in your project implementation("ly.img:editor:1.76.0") // Other dependencies here } ``` This guide shows you how to integrate the CreativeEditor SDK into a **new Android Activity-based project**. Learn how to: - **create** a new project. - **add** the necessary dependencies. - **configure** the editor. - **test** the integration. ## Who Is This Guide For? This guide is for developers who: - Have experience with Android development and Kotlin - Want to create a new Android Activity-based project with integrated creative editing capabilities - Need to implement user-friendly editing interfaces - Want to add professional-grade image editing, design creation, and video editing to their Android apps - Prefer using traditional Android Views with Activity-based architecture ## What You'll Achieve By following this guide, you: - Create a new Android Activity-based project with CreativeEditor SDK integration - Configure platform-specific requirements for Android - Implement a functional editor that you can launch from your app - Test and verify the integration works correctly [Explore Android Demos](https://img.ly/showcases/cesdk/?tags=android) [View on GitHub](https://github.com/imgly/cesdk-android-examples) ## Prerequisites Before you begin, ensure you have the following requirements: ### Development Environment - **Android Studio**: Latest version (Hedgehog or later) - **Kotlin**: 1.9.10 or later - **Gradle**: 8.4 or later - **Git CLI** for version control ### Platform Requirements - **Android**: 7.0+ (API level 24+) - **Minimum SDK**: 24 - **Target SDK**: Latest stable version ### License - A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)), use `null` or an empty string to run in evaluation mode with watermark. ### Verify Your Setup Run the following command to verify your Android development environment: ```bash gradle --version ``` This command checks your Gradle installation and reports any issues to resolve before proceeding. > **Note:** You can customize the CreativeEditor SDK for Android through **native code > (Kotlin) only**, as described in the > [configuration overview section](https://img.ly/docs/cesdk/android/user-interface/customization-72b2f8/). ## Step 1: Create a New Android Activity-Based Project First, verify your Android Studio installation and create a new project: 1. **Open Android Studio** and select "New Project" 2. **Choose "Empty Views Activity"** template 3. **Configure your project**: - Name: `cesdk_android_activity_app` - Package name: `com.example.cesdkactivityapp` - Language: **Kotlin** - Minimum SDK: **API 24 (Android 7.0)** - Build configuration language: **Kotlin DSL** ### Project Structure Your new project should have this structure: ``` cesdk_android_activity_app/ ├── app/ # Main application module │ ├── src/main/ │ │ ├── java/ # Kotlin source files │ │ ├── res/ # Resources │ │ └── AndroidManifest.xml │ ├── build.gradle.kts # App-level build configuration │ └── proguard-rules.pro # ProGuard rules ├── gradle/ # Gradle wrapper ├── build.gradle.kts # Project-level build configuration ├── settings.gradle.kts # Project settings └── gradle.properties # Gradle properties ``` ## Step 2: Add the CreativeEditor SDK Dependency Add the CreativeEditor SDK to your project by updating the build configuration: ### 2.1 Add IMG.LY Repository Update your `settings.gradle.kts` to include the IMG.LY repository: ```kotlin highlight-maven-dependency dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name = "IMG.LY Artifactory" url = uri("https://artifactory.img.ly/artifactory/maven") mavenContent { includeGroup("ly.img") } } } } ``` ### 2.2 Add Editor Dependency Update your `app/build.gradle.kts` to include the editor dependency: ```kotlin highlight-dependency dependencies { // This dependency makes main compose and coroutine APIs available in your project implementation("ly.img:editor:1.76.0") // Other dependencies here } ``` This adds the latest version of the CreativeEditor SDK editor to your `build.gradle.kts` file. ### 2.3 Configure Android Settings Update your `app/build.gradle.kts` to ensure proper configuration: ```kotlin highlight-build-android android { namespace = "com.example.cesdkapp" compileSdk = 36 defaultConfig { applicationId = "com.example.cesdkapp" minSdk = 24 targetSdk = 36 versionCode = 1 versionName = "1.0" ndk { abiFilters += arrayOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } } ``` ### 2.4 Sync Project After adding the dependency, sync your project to download the CreativeEditor SDK: In Android Studio, click **Sync Project with Gradle Files** to download and configure all dependencies. This downloads and installs the CreativeEditor SDK and its dependencies automatically through Gradle. ## Step 3: Implement the Editor Integration Now let's implement the CreativeEditor SDK editor in your Android application: ### 3.1 Create EditorComposable Create a new file `EditorComposable.kt` in your app module's main source set (typically `app/src/main/java/com/yourpackage/`): ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/EditorComposable.kt import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import ly.img.editor.Editor import ly.img.editor.core.configuration.EditorConfiguration import ly.img.editor.core.configuration.remember import ly.img.engine.DesignBlockType // Add this composable to your Activity, Fragment, NavHost etc. @Composable fun EditorComposable(onClose: (Throwable?) -> Unit) { val context = LocalContext.current Editor( // Get your license from https://img.ly/forms/free-trial // Keep this null for evaluation mode with watermark. // Replace it with your license key for production use. license = null, configuration = { EditorConfiguration.remember { onCreate = { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) } onError = { Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show() } } }, onClose = onClose, ) } ``` In this example, we create a scene with a single square page. ### 3.2 Create EditorActivity Create a new Kotlin file called `EditorActivity.kt` in your app module's main source set (typically `app/src/main/java/com/yourpackage/`): ```kotlin file=@cesdk_android_examples/editor-guides-quickstart/EditorActivity.kt import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent // Launch this activity via intent class EditorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { EditorComposable { throwable -> // You can set result here finish() } } } } ``` ### 3.3 Update MainActivity Modify your existing `MainActivity.kt` file (located in `app/src/main/java/com/yourpackage/`) to launch the editor: ```kotlin title="MainActivity.kt" import android.content.Intent import android.os.Bundle import android.widget.Button import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById