[Source](https:/img.ly/docs/cesdk/android/what-is-cesdk-2e7acd) # Android Creative Editor The Android Mobile Design Editor SDK offers a robust and customizable solution for creating and editing visual designs directly within your Android applications. ### What is CE.SDK? **CreativeEditor SDK (CE.SDK)** is a powerful design engine that brings fully customizable image, video, and design editing directly into your iOS app. Whether you’re enabling AI-powered design workflows, template-based creation, dynamic content generation, or full-featured creative editing, CE.SDK offers the flexibility, performance, and developer control you need — all with minimal integration overhead. [Launch Web Demo](https://img.ly/showcases/cesdk/?tags=android) [Get Started](android/get-started/overview-e18f40/) Trusted by leading organizations worldwide, CE.SDK powers the creative editors used in best-in-class applications, including those from Shopify, Semrush, HP, Shutterfly, Ticketmaster, and Swiss Post. ## Key Capabilities of the Android Creative Editor SDK ![Transform](/docs/cesdk/_astro/Transform.By5kJRew_2acCrV.webp) ### Transform Perform operations like cropping, rotating, and resizing design elements. ![Templating](/docs/cesdk/_astro/Templating.eMNm9_jD_ycnVt.webp) ### Templating Create and apply design templates with placeholders and text variables for dynamic content. ![Placeholders & Lockable Design](/docs/cesdk/_astro/Placeholders.DzG3E33B_bmQxQ.webp) ### Placeholders & Lockable Design Constrain templates to guide your users’ design and ensure brand consistency. ![Asset Management](/docs/cesdk/_astro/AssetLibraries.Ce9MfYvX_HmsaC.webp) ### Asset Management Import and manage images, shapes, and other assets to build your designs. ![Design Collage](/docs/cesdk/_astro/VideoCollage.23LDUE8e_1VDFAj.webp) ### Design Collage Arrange multiple elements on a single canvas to create complex layouts. ![Text Editing](/docs/cesdk/_astro/TextEditing.B8Ra1KOE_2lGC8C.webp) ### Text Editing Add and style text blocks with various fonts, colors, and effects. ![Client-Side Processing](/docs/cesdk/_astro/ClientSide.CECpQO_1_c6mBh.webp) ### Client-Side Processing All design editing operations are executed directly on the device, with no need for server dependencies. ![Extendible](/docs/cesdk/_astro/Extendible.CRYmRihj_BmNTE.webp) ### Extendible Hook into the engine API and editor events to implement custom features. ![Customizable UI](/docs/cesdk/_astro/CustomizableUI.DtHv9rY-_2fNrB2.webp) ### Customizable UI Build and integrate custom UIs tailored to your application’s design needs. ## File Format Support CE.SDK supports a wide range of file types to ensure maximum flexibility for developers: ### Importing Media Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. ### Exporting Media Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ### Importing Templates Format Description `.idml` InDesign `.psd` Photoshop `.scene` CE.SDK Native Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs to generate scenes programmatically. For detailed information, see the [full file format support list](android/file-format-support-3c4b2a/). ## Integrations CE.SDK supports out-of-the-box integrations with: * **Getty Images** * **Unsplash** * **Pexels** * **Airtable** * **Soundstripe** Want to connect your own asset sources? Register a custom provider using our API. --- [Source](https:/img.ly/docs/cesdk/android/user-interface-5a089a) # User Interface --- [Source](https:/img.ly/docs/cesdk/android/upgrade-4f8715) # Upgrade --- [Source](https:/img.ly/docs/cesdk/android/to-v1-32-1b6ae8) # To v1.32 Version v1.32 introduced powerful new APIs for customizing the CE.SDK web editor. These new APIs render some of the existing configurations obsolete, requiring code migration to leverage the more flexible new options. This guide will help you navigate these changes and explain what you need to do to update your integration. ## Configuring the Dock Until version 1.32, the dock was configured by two configuration options (although most users only used one of them and kept the others default): * `config.ui.elements.libraries.insert.entries` and * `config.ui.elements.dock` If your configuration adapted one of these two (mostly `config.ui.elements.libraries.insert.entries`), you are affected by this change. For now, it is only deprecated and we will try to do an internal migration for you, but this still might include a breaking change depending on how you used the configuration options before. ### Breaking Change `config.ui.elements.libraries.insert.entries` was called repeatedly with a context of currently selected blocks. Most users and configurations have not used this behavior and just returned the same static list of entries for every call. In this case, your configuration should work as before, but if you have relied on this fact, you have to migrate your configuration to the new API, listen to selection changes, and update the asset library entries accordingly. ### Migration to new APIs Defining the dock is now done by our new APIs in a consistent way to all other customizable locations of the editor. With the [Dock API](android/user-interface/customization/dock-cb916c/), you now have much greater control of how and what is displayed in the dock. This does not only include dock buttons to open asset libraries but also arbitrary buttons and other elements. Please take a look at the [Dock API](android/user-interface/customization/dock-cb916c/) or learn about the general concept [here](android/user-interface/overview-41101a/). If you aren’t affected by the breaking change mentioned above, the easiest way to migrate is to first copy your current dock order after the editor has been inialized. This can be done by calling the new `cesdk.ui.getDockOrder()` method. Now you can take this order and set it during the initialization of the editor by using `cesdk.ui.setDockOrder(copiedDockOrder)`. The old configuration (`config.ui.elements.libraries.insert.entries` and `config.ui.elements.dock`) can be removed afterwards. Of course, you could also just remove the old configuration and use the new API to define the dock order from scratch. Please note, that changes to the asset library entries are now done by the Asset Library Entry API and the dock order is just referring to these. So if you, for instance, want to add an asset source id to be shown in a library, you have to add this asset source id to the asset library entry and not to the dock order. ``` // Before// ======const config: Configuration = { ui: { elements: { libraries: { insert: { entries: (defaultEntries) => { return [ // Changing some of the default entries ...defaultEntries.map((entry) => { if (entry.id === 'ly.img.image') { entry.sourceIds.push('my-own-image-source'); } return entry; }), // Adding a new entry { id: 'my-own-entry', sourceIds: ['my-own-source'] } ]; } } } } }}; // After// ======cesdk.ui.setDockOrder([ ...cesdk.ui.getDockOrder(), // Add a new button referencing your new entry { id: 'ly.img.assetLibrary.dock', label: 'My Own Entry', icon: [...], entries: ['my-own-entry'] }]); // Adding your custom entrycesdk.ui.addAssetLibraryEntry({ id: 'my-own-entry', sourceIds: ['my-own-source']}); // Updating an existing default entryconst imageEntry = cesdk.ui.getAssetLibraryEntry('ly.img.image');cesdk.ui.updateAssetLibraryEntry('ly.img.image',{ sourceIds: [...imageEntry.sourceIds, 'my-own-image-source']}) ``` ## Configuring the Asset Replacement Panel Similar to the definition of the dock, we deprecate the configuration `config.ui.elements.libraries.replace.entries` of the asset library entries for the replacement panel. This method is deprecated but we will try to migrate your configuration internally until it is removed. We recommend you to migrate to the new API as soon as possible. The new API is similar with subtle differences. With `cesdk.ui.setReplaceAssetLibraryEntries` you register a function that is called with the current context (of selected blocks) but only returns ids of entries. ### Breaking Change With the `config.ui.elements.libraries.replace.entries` it was possible to take the default entries and modify them. In theory, you could change the entries and have different “default” entries for insert or replace. Now a change to a default entry provided by the editor via the Asset Library Entry API will be reflected in both the insert and replace entries. To solve this you can just copy one entry for replacement, modify it, and return its id instead. ### Migration to new APIs Take the function from `config.ui.elements.libraries.replace.entries` and use it in `cesdk.ui.setReplaceAssetLibraryEntries` by replacing the entries with their ids. If you have made changes to the default entries or added new custom ones you need to add or change them via the Asset Library Entry API on initialization of the editor. --- [Source](https:/img.ly/docs/cesdk/android/use-templates-a88fd4) # Use Templates --- [Source](https:/img.ly/docs/cesdk/android/to-v1-10-1ff469) # To v1.10 Version v1.10 introduced major changes to how and where engine and the UI store assets. This guide helps you navigate those changes and explains what you need to do to bring your integration up to speed. ## 1\. Scene Uploads are no longer serialized Image uploads are no longer stored in the scene and will not reappear upon scene load. To offer specific assets in your editor, configure and [add an asset source](android/serve-assets-b0827c/) containing the desired assets. ## 2\. Deprecating Extensions Starting with `v1.10` we fully embrace [Asset Sources](android/import-media/from-remote-source/unsplash-8f31f0/) as our standard interface for asset management. We’re deprecating extension packs, previously stored in `/assets/extensions` and indexed via `manifest.json` files. **Fonts are not affected by this deprecation yet, but will receive the same treatment in an upcoming version.** We’ll deprecate the `config.extensions` field for `CreativeEditorSDK`. As part of this deprecation, we’ll **no longer ship** the following packs in the `/assets/extensions` directory in our `npm` packages: * `ly.img.cesdk.images.samples` * `ly.img.cesdk.shapes.default` * `ly.img.cesdk.stickers.doodle` * `ly.img.cesdk.stickers.emoji` * `ly.img.cesdk.stickers.emoticons` * `ly.img.cesdk.stickers.hand` * `ly.img.cesdk.vectorpaths` * `ly.img.cesdk.vectorpaths.abstract` To keep offering the contained assets in your deployment, use our new [convenience functions](#making-use-of-default-and-demo-asset-sources) to instantiate asset sources holding these assets. If you have existing scenes where an asset from an extension pack might be included, you must make sure you’re still serving the corresponding files from your baseURL, so that `/extensions/…` paths still resolve properly. You can acquire a copy of the extension packs shipped in `v1.9.2` [from our CDN](https://cdn.img.ly/packages/imgly/cesdk-engine/1.9.2/assets/extensions.zip). Otherwise your scenes will **render missing asset alerts**. ### 2.1 Making use of Default and Demo Asset Sources We still want to offer a package, that has all batteries included and quickly gets you up to speed. To do so, we introduced two new convenience functions, that can be used to add a set of predefined asset sources to your integration: #### `addDefaultAssetSources` Adds a set of asset sources containing our default assets. These assets may be used in production and [served from your own servers](android/serve-assets-b0827c/). The assets are parsed from the IMG.LY CDN at `{{base_url}}//content.json`, where `base_url` defaults to `https://cdn.img.ly/assets/v1`. Each source is created via `addLocalSource` and populated with the parsed assets. You can specify your own `base_url` or exclude certain source IDs. The following sources are added: ID Description `'ly.img.sticker'` Various stickers `'ly.img.vectorpath'` Shapes and arrows `'ly.img.filter.lut'` LUT effects of various kinds. `'ly.img.filter.duotone'` Color effects of various kinds. #### `addDemoAssetSources` Registers a set of demo asset sources containing our example assets. These are not to meant to be used in your production code. The assets are parsed from the IMG.LY CDN at `https://cdn.img.ly/assets/demo/v1`. The `sceneMode` and `withUploadAssetSources` parameters control whether audio/video and upload sources are added. The following sources are added: ID Description `'ly.img.image'` Sample images `'ly.img.image.upload'` Demo source to upload image assets `'ly.img.audio'` Sample audios `'ly.img.audio.upload'` Demo source to upload audio assets `'ly.img.video'` Sample audios `'ly.img.video.upload'` Demo source to upload video assets #### Modifying Default & Demo Sources After registration you can freely modify the contained assets using the Asset APIs. You can add or remove entire asset sources or individual assets. #### Upload Asset Sources The upload asset sources and library entries for video and audio were added to the default configuration from `addDefaultAssetSources`. If you have added these sources manually (like mentioned in our video docs) you can remove them now. ## 3\. AssetAPI Changes To further streamline interaction, the following breaking changes were made to the AssetAPI: * The `applyAsset` callbacks and `defaultApplyAsset` API now return an optional design block id in their callback if they created a new block. * `thumbUri` and `size` properties in `AssetDefinition` and `AssetResult` are now part of the `meta` property dictionary. * Values of the `blockType` asset meta property must now be design block type ids (e.g. `//ly.img.ubq/image`) ## 4\. A New Way to Add Images Instead of specifying additional images for the `CreativeEditorSDK` in `config.presets.images`, you should make use of `asset.addAsset` and add your images into the `ly.img.image` asset source. ## 5\. General API Changes The `blockType` `meta` property for assets changed from `ly.img.` to fully qualified block types: E.g. `'ly.img.image'` now needs to be `'//ly.img.ubq/image'`. As we’re starting to apply the ‘fill’ concept to more parts of the interface, we deprecated various fill color related APIs: * Deprecated `hasFillColor`, use `hasFill` and query `block.getEnum(id, 'fill/type')` for `Solid` type instead. * Deprecated `get/setFillColorRGBA`, use `setFillSolidColor`instead.. * Deprecated `isFillColorEnabled`, use `isFillEnabled` instead. * Deprecated `setFillType` and `setFillGradientType`, use `createFill`, e.g., with type ‘color’ and then apply the fill block with `setFill` to the block instead. If the block has a fill, it should be removed with `getFill` and `destroy`. * Deprecated `getFillType` and `getFillGradientType`, query `block.getEnum(id, 'fill/type')` and `block.getEnum(id, 'fill/gradient/type')` instead instead * Deprecated `add/removeFillGradientColorStop` and `get/setFillGradientColorStops`. * Deprecated `get/setFillGradientControlPointX/Y`, use `block.getFloat(fill, keypath)` and `block.setFloat(fill, keypath, value)` with key paths ‘fill/gradient/linear/startPointX/Y’, ‘fill/gradient/radial/centerPointX/Y’, and ‘fill/gradient/conical/centerPointX/Y’ instead. * Deprecated `get/setFillGradientRadius`, use `block.getFloat(fill, 'fill/gradient/radial/radius')` and `block.setFloat(fill, 'fill/gradient/radial/radius', value)` instead.” `camera/clearColor` property was replaced it with a global `clearColor` setting to allow controlling the clear color before loading a scene. Properties affecting playback that existed on both `Audio` and `VideoFill` where moved to a common `playback/` namespace: * `'fill/video/looping'` and `'audio/looping'` are now `'playback/looping'` * `'fill/video/volume'` and `'audio/volume'` are now `'playback/volume'` * `'fill/video/muted'` and `'audio/muted'` are now `'playback/muted'` --- [Source](https:/img.ly/docs/cesdk/android/to-v1-19-55bcad) # To v1.19 Version v1.19 of CreativeEngineSDK and CreativeEditorSDK introduces structural changes to many of the current design blocks, making them more composable and more powerful. Along with this update, there are mandatory license changes that require attention. This comes with a number of breaking changes. This document will explain the changes and describe the steps you need to take to adapt them to your setup. ## **Initialization** The initialization of the `Engine` has changed. Now `start` is a suspending function and it requires a new parameter `license` which is the API key you received from our dashboard. There is also a new optional parameter `userId` an optional unique ID tied to your application’s user. This helps us accurately calculate monthly active users (MAU). Especially useful when one person uses the app on multiple devices with a sign-in feature, ensuring they’re counted once. Providing this aids in better data accuracy. ``` engine.start(license = "", userId = "") ``` ## **DesignBlockType** This class is not an enum class anymore, but a sealed class. These are the transformations of all `DesignBlockType` types: Removed: * `DesignBlockType.IMAGE` * `DesignBlockType.VIDEO` * `DesignBlockType.STICKER` * `DesignBlockType.VECTOR_PATH` * `DesignBlockType.RECT_SHAPE` * `DesignBlockType.LINE_SHAPE` * `DesignBlockType.STAR_SHAPE` * `DesignBlockType.POLYGON_SHAPE` * `DesignBlockType.ELLIPSE_SHAPE` * `DesignBlockType.COLOR_FILL` * `DesignBlockType.IMAGE_FILL` * `DesignBlockType.VIDEO_FILL` * `DesignBlockType.LINEAR_GRADIENT_FILL` * `DesignBlockType.RADIAL_GRADIENT_FILL` * `DesignBlockType.CONICAL_GRADIENT_FILL` Renamed: * `DesignBlockType.SCENE` -> `DesignBlockType.Scene` * `DesignBlockType.STACK` -> `DesignBlockType.Stack` * `DesignBlockType.CAMERA` -> `DesignBlockType.Camera` * `DesignBlockType.PAGE` -> `DesignBlockType.Page` * `DesignBlockType.AUDIO` -> `DesignBlockType.Audio` * `DesignBlockType.TEXT` -> `DesignBlockType.Text` * `DesignBlockType.GROUP` -> `DesignBlockType.Group` Added: * `DesignBlockType.Graphic` * `DesignBlockType.Cutout` Note that `DesignBlockType.values()` can be used to get the list of all instances mentioned above. ## **Graphic Design Block** A new generic `DesignBlockType.Graphic` type has been introduced, that forms the basis of the new unified block structure. ## **Shapes** Similar to how the fill of a block is a separate object which can be attached to and replaced on a design block, we have now introduced a similar concept for the shape of a block. You use the new `createShape`, `getShape` and `setShape` APIs in order to define the shape of a design block. Only the new `DesignBlockType.Graphic` block allows to change its shape with these APIs. The new available shape types are: * `ShapeType.Rect` * `ShapeType.Line` * `ShapeType.Ellipse` * `ShapeType.Polygon` * `ShapeType.Star` * `ShapeType.VectorPath` Note that `ShapeType.values()` can be used to get the list of all instances mentioned above. The following design block types are now removed in favor of using a `DesignBlockType.Graphic` block with one of the above mentioned shape instances: * `DesignBlockType.RECT_SHAPE` * `DesignBlockType.LINE_SHAPE` * `DesignBlockType.ELLIPSE_SHAPE` * `DesignBlockType.POLYGON_SHAPE` * `DesignBlockType.STAR_SHAPE` * `DesignBlockType.VECTOR_PATH` This structural change means that the shape-specific properties (e.g. the number of sides of a polygon) are not available on the design block anymore but on the shape instances instead. You will have to add calls to `getShape` to get the instance id of the shape instance and then pass that to the property getter and setter APIs. Also, remember to change property key strings in the getter and setter calls from plural `shapes/…` to singular `shape/…` to match the new type identifiers. ## **Image and Sticker** Previously, `DesignBlockType.IMAGE` and `DesignBlockType.STICKER` were their own high-level design block types. They neither support the fill APIs nor the effects APIs. Both of these blocks are now removed in favor of using a `DesignBlockType.Graphic` block with an image fill (`FillType.Image`) and using the effects APIs instead of the legacy image block’s numerous effects properties. At its core, the sticker block has always just been an image block that is heavily limited in its capabilities. You can neither crop it, nor apply any effects to it. In order to replicate the difference as closely as possible in the new unified structure, more fine-grained scopes have been added. You can now limit the adopter’s ability to crop a block and to edit its appearance. Note that since these scopes only apply to a user of the editor with the “Adopter” role, a “Creator” user will now have all of the same editing options for both images and for blocks that used to be stickers. ## **Scopes** The following is the list of changes to the design block scopes: * (Breaking) The permission to crop a block was split from `content/replace` and `design/style` into a separate scope: `layer/crop`. * Deprecated the `design/arrange` scope and renamed `design/arrange/move` → `layer/move` `design/arrange/resize` → `layer/resize` `design/arrange/rotate` → `layer/rotate` `design/arrange/flip` → `layer/flip` * Deprecated the `content/replace` scope. For `DesignBlockType.Text` blocks, it is replaced with the new `text/edit` scope. For other blocks it is replaced with `fill/change`. * Deprecated the `design/style` scope and replaced it with the following fine-grained scopes: `text/character`, `stroke/change`, `layer/opacity`, `layer/blendMode`, `layer/visibility`, `layer/clipping`, `appearance/adjustments`, `appearance/filter`, `appearance/effect`, `appearance/blur`, `appearance/shadow` * Introduced `fill/change`, `stroke/change`, and `shape/change` scopes that control whether the fill, stroke or shape of a block may be edited by a user with an “Adopter” role. * The deprecated scopes are automatically mapped to their new corresponding scopes by the scope APIs for now until they will be removed completely in a future update. ## **Kind** While the new unified block structure both simplifies a lot of code and makes design blocks more powerful, it also means that many of the design blocks that used to have unique type ids now all have the same generic `DesignBlockType.Graphic` type, which means that calls to the `findByType` cannot be used to filter blocks based on their legacy type ids any more. Simultaneously, there are many instances in which different blocks in the scene which might have the same type and underlying technical structure have different semantic roles in the document and should therefore be treated differently by the user interface. To solve both of these problems, we have introduced the concept of a block “kind”. This is a mutable string that can be used to tag different blocks with a semantic label. You can get the kind of a block using the `getKind` API and you can query blocks with a specific kind using the `findByKind` API. CreativeEngine provides the following default kind values: * `image` * `video` * `sticker` * `scene` * `camera` * `stack` * `page` * `audio` * `text` * `shape` * `group` Unlike the immutable design block type id, you can change the kind of a block with the new `setKind` API. It is important to remember that the underlying structure and properties of a design block are not strictly defined by its kind, since the kind, shape, fill and effects of a block can be changed independent of each other. Therefore, a user-interface should not make assumptions about available properties of a block purely based on its kind. **Note** Due to legacy reasons, blocks with the kind “sticker” will continue to not allow their contents to be cropped. This special behavior will be addressed and replaced with a more general-purpose implementation in a future update. ​ ## **Asset Definitions** The asset definitions have been updated to reflect the deprecation of legacy block type ids and the introduction of the “kind” property. In addition to the “blockType” meta property, you can now also define the `“shapeType”` ,`“fillType”` and `“kind”` of the block that should be created by the default implementation of the applyAsset function. * `“blockType”` defaults to `DesignBlockType.Graphic.key (“//ly.img.ubq/graphic”)` if left unspecified. * `“shapeType”` defaults to `ShapeType.Rect.key (“//ly.img.ubq/shape/rect”)` if left unspecified * `“fillType”` defaults to `FillType.Color.key (“//ly.img.ubq/fill/color”)` if left unspecified Video block asset definitions used to specify the `“blockType”` as `“//ly.img.ubq/fill/video“ (FillType.Video.key)`. The `“fillType”` meta asset property should now be used instead for such fill type ids. ## **Automatic Migration** CreativeEngine will always continue to support scene files that contain the now removed legacy block types. Those design blocks will be automatically replaced by the equivalent new unified block structure when the scene is loaded, which means that the types of all legacy blocks will change to `DesignBlockType.Graphic`. Note that this can mean that a block gains new capabilities that it did not have before. For example, the line shape block did not have any stroke properties, so the `hasStroke` API used to return `false`. However, after the automatic migration its `DesignBlockType.Graphic` design block replacement supports both strokes and fills, so the `hasStroke` API now returns `true` . Similarly, the image block did not support fills or effects, but the `DesignBlockType.Graphic` block does. ## **Types and API Signatures** To improve the type safety of our APIs, we have moved away from using the `DesignBlockType` enum and replaced with a set of types. Those changes have affected the following APIs: * `BlockApi.create()` * `BlockApi.createFill()` * `BlockApi.createEffect()` * `BlockApi.createBlur()` * `BlockApi.findByType()` **Note** All the functions above still support the string overload variants, however, their usage will cause lint warnings in favor of type safe overloads. ## **Code Examples** This section will show some code examples of the breaking changes and how it would look like after migrating. ``` /** Block Creation */ // Creating an Image before migrationval image = engine.block.create(DesignBlockType.IMAGE)engine.block.setString( block = image, property = "image/imageFileURI", value = "https://domain.com/link-to-image.jpg") // Creating an Image after migrationval block = engine.block.create(DesignBlockType.Graphic)val rectShape = engine.block.createShape(ShapeType.Rect)val imageFill = engine.block.createFill(FillType.Image)engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://domain.com/link-to-image.jpg")engine.block.setShape(block, shape = rectShape)engine.block.setFill(block, fill = imageFill)engine.block.setKind(block, kind = "image") // Creating a star shape before migrationval star = engine.block.create(DesignBlockType.STAR_SHAPE)engine.block.setInt(star, property = "shapes/star/points", value = 8) // Creating a star shape after migrationval block = engine.block.create(DesignBlockType.Graphic)val starShape = engine.block.createShape(ShapeType.Star)val colorFill = engine.block.createFill(FillType.Color)engine.block.setInt(block = starShape, property = "shape/star/points", value = 8)engine.block.setShape(block, shape = starShape)engine.block.setFill(block, fill = colorFill)engine.block.setKind(block, kind = "shape") // Creating a sticker before migrationval sticker = engine.block.create(DesignBlockType.STICKER)engine.block.setString( block = sticker, property = "sticker/imageFileURI", value = "https://domain.com/link-to-sticker.png") // Creating a sticker after migrationval block = engine.block.create(DesignBlockType.Graphic)val rectShape = engine.block.createShape(ShapeType.Rect)val imageFill = engine.block.createFill(FillType.Image)engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://domain.com/link-to-sticker.png")engine.block.setShape(block, shape = rectShape)engine.block.setFill(block, fill = imageFill)engine.block.setKind(block, kind = "sticker") /** Block Creation */ ``` ``` /** Block Exploration */ // Query all images in the scene before migrationval images = engine.block.findByType(DesignBlockType.IMAGE) // Query all images in the scene after migrationval images = engine.block.findByType(DesignBlockType.Graphic).filter { block -> val fill = engine.block.getFill(block) engine.block.isValid(fill) && engine.block.getType(fill) == FillType.Image.key} // Query all stickers in the scene before migrationval stickers = engine.block.findByType(DesignBlockType.STICKER) // Query all stickers in the scene after migrationval stickers = engine.block.findByKind("sticker") // Query all Polygon shapes in the scene before migrationval polygons = engine.block.findByType(DesignBlockType.POLYGON_SHAPE) // Query all Polygon shapes in the scene after migrationval polygons = engine.block.findByType(DesignBlockType.Graphic).filter { block -> val shape = engine.block.getShape(block) engine.block.isValid(shape) && engine.block.getType(shape) == ShapeType.Polygon.key} /** Block Exploration */ ``` --- [Source](https:/img.ly/docs/cesdk/android/to-v1-13-d1ac5d) # To v1.13 In version v1.13, the way the CreativeEngine and CreativeEditor SDK are configured has changed. Several configuration options that were previously passed to `CreativeEngine.init()` or `CreativeEditor SDK.init()` have been removed or replaced. This document will explain the changes and describe the steps you can take to adapt them to your setup. #### CreativeEditorSDK initialization We are also introducing a new way of instantiating the CreativeEditorSDK, that provides more precise control over the initialization. Using `CreativeEditorSDK.create()`, you have the chance to configure the SDK before loading or creating a scene. This was not possible before. When using `CreativeEditorSDK.init()`, the SDK would create the initial scene before giving you the option to perform additional configuration via the API. The `create()` method will not create an initial scene for you. You need to do that yourself, using either the Scene API at `cesdk.editor.scene`, or one of the methods on the CreativeEditorSDK instance itself (`createDesignScene`, `createVideoScene`, `loadFromUrl`, `loadFromString`). ### Rationale Over time the number options you could pass into the call to `CreativeEngine.init({...config})` has grown quite a bit. Initially this was the only place where you could configure the behavior and settings of the CreativeEngine, but over the past year we introduced several new APIs. One of those APIs is the EditorAPI, which lets you [adjust](android/settings-970c98/) many [settings](android/settings-970c98/) at runtime, not just at the launch of the app. To improve consistency of our APIs, we decided to scale back the options available in the configuration object in favor of changing settings via the EditorAPI. The only options that remain are those that are strictly necessary for the initialization of the CreativeEngine, such as the `baseUrl` and `license`. These changes were performed with the Creative Engine in mind, but since the CreativeEditor SDK shares a lot of the same code, the changes described in this document also apply to the configuration for the CE.SDK. ### Changed configuration options The following is a list of all configuration options that have been changed or removed, along with instructions on how to migrate the use of these options in your codebase: * `scene` options (`dpi`, `pixelScaleFactor`) have been removed. `scene/dpi` and `scene/pixelScaleFactor` can now be found as [properties on the scene in the BlockAPI](android/concepts/blocks-90241e/). * `page` options have been removed. * `page.title.show` has been replaced with `cesdk.engine.editor.setSettingBool('page/title/show', enabled)` * `page.title.fontFileUri` has been replaced with `cesdk.engine.editor.setSettingString('page/title/fontFileUri', uri)` * `page.dimOutOfPageAreas` has been replaced with `cesdk.engine.editor.setSettingBool('page/dimOutOfPageAreas', dimEnabled)` * `assetSources` have been removed. To add asset sources, use the AssetAPI at `cesdk.engine.asset`. * `preset.colors` has been removed as it was never used, previously. * `presets.colorPalettes` has been removed from CreativeEngine as it was not used. It has been moved to `ui.colorPalette` in the CESDK. * `presets.images` has been removed. To add assets and asset sources, use the AssetAPI at `cesdk.engine.asset`. * `presets.pageFormats` has been removed from the CreativeEngine as it was not used. Is has been moved to `ui.pageFormats` in the CESDK. Previously it was possible to mark a page format as default by setting `meta: {default: true}` in it. In `ui.pageFormats`, this has been simplified, to just `default: true`. * `variables` has been removed. Use the [VariableAPI](android/create-templates/add-dynamic-content/text-variables-7ecb50/) instead. * `callbacks.log` has been moved to `logger`. Previously the logger callback would take a `Loglevel` enum as a second parameter. This enum has been removed. Instead you can define the loglevel with plain strings `'Warning' | 'Error' | 'Info'` ### Change initialization code To ensure your users perceive a consistent UI experience, settings that have been moved to api calls should be made immediately after initializing the CreativeEngine. For the Creative Editor SDK, use the `create()` method, instead of `init()` #### CreativeEngine ``` const engine = await CreativeEngine.init(config);// 1. Configure Engineengine.editor.setSettingEnum('doubleClickSelectionMode', 'Direct');// ... other settings // 2. Create/load sceneconst sceneId = await engine.scene.create(); // 3. Append Engine canvas element to the DOMdocument.getElementById('my-engine-container').append(engine.element); // ... your application code ``` #### CreativeEngine SDK ``` const cesdk = await CreativeEditorSDK.create('my-engine-container', config);// 1. Configure SDKcesdk.engine.asset.addSource(myAssetSource);// ... other settings // 2. Create/load sceneconst sceneId = await cesdk.createDesignScene(myPageFormats[pageFormatId]); // ... your application code ``` ### Fallback and warnings The CreativeEngine and CreativeEditor SDK still interpret the config object with its previous settings. If removed configuration options are detected during intialization, a warning is printed to the console, with individual instructions on how to migrate them. It is recommended to adjust your configuration as described above for the best compatibility with future developments and to get rid of these warnings. The fallback mechanism is not enabled for the new `CreativeEditorSDK.create()` method! Passing removed configuration options to `create()` will cause that option to be ignored and an error will be printed to the console. ### CreativeEngine Typescript definitions The more complex parts of our configuration (such as the page format definitions) were previously exporting type definitions under the `ConfigTypes` namespace in the CreativeEngine package. This namespace and all the types in it have been removed or moved elsewhere. For now we still export `ConfigTypes`, but have marked it and its members as `@deprecated`. Most of them are not used at all anymore, the rest have been moved elsewhere: * `ConfigTypes.Color` can now be imported directly as `PaletteColor` * `ConfigTypes.TypefaceDefinition` can be imported directly, as `TypefaceDefinition`. * `ConfigTypes.PageFormatDefinition` can be imported directly as `PageFormatDefinition` (CE.SDK only). * `ConfigTypes.Logger` can be imported directly as `Logger` The `LogLevel` enum that was previously used by `Logger` has been replaced with a string union (`'Warning' | 'Error' | 'Info'`). ### CreativeEditorSDK Typescript definitions The CreativeEditor SDK package still _does_ export a `ConfigTypes` namespace. For use with the new `CreativeEditorSDK.create()`, we are offering a new type `CreateConfiguration`, which is lacking all of the removed keys instead of marking them as deprecated. ### Demo asset sources When adding demo asset sources using `cesdk.addDemoAssetSources()`, or `engine.addDemoAssetSources()`, make sure to specify the correct scene mode. The installed demo asset sources vary between Design and Video modes. If you don’t specify a scene mode, `addDemoAssetSources()` will try to add the correct sources based on the current scene, and default to `'Design'`. If you call `addDemoAssetSources()` _without_ a scene mode, and _before_ loading or creating a video scene, the audio and video asset sources will not be added. --- [Source](https:/img.ly/docs/cesdk/android/text-8a993a) # Text --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes-a000fe) # Stickers and Shapes --- [Source](https:/img.ly/docs/cesdk/android/settings-970c98) # Settings All keys listed below can be modified through the Editor API. The nested settings inside `UBQSettings` can be reached via key paths, e.g. `page/title/show`. ## Settings ### `BlockAnimationSettings` Member Type Default Description enabled `bool` `true` Whether animations should be enabled or not. ### `CameraClampingSettings` Member Type Default Description overshootMode `CameraClampingOvershootMode` `Reverse` Controls what happens when the clamp area is smaller than the viewport. Center: the clamp area is centered in the viewport. Reverse: the clamp area can move inside the viewport until it hits the edges. ### `CameraSettings` Member Type Default Description clamping `CameraClampingSettings: CameraClampingOvershootMode overshootMode` `{}` Clamping settings for the camera. ### `ControlGizmoSettings` Member Type Default Description blockScaleDownLimit `float` `8.0` Scale-down limit for blocks in screen pixels when scaling them with the gizmos or with touch gestures. The limit is ensured to be at least 0.1 to prevent scaling to size zero. showCropHandles `bool` `{true}` Whether or not to show the handles to adjust the crop area during crop mode. showCropScaleHandles `bool` `{true}` Whether or not to display the outer handles that scale the full image during crop. showMoveHandles `bool` `{true}` Whether or not to show the move handles. showResizeHandles `bool` `{true}` Whether or not to display the non-proportional resize handles (edge handles) showRotateHandles `bool` `{true}` Whether or not to show the rotation handles. showScaleHandles `bool` `{true}` Whether or not to display the proportional scale handles (corner handles) ### `DebugFlags` Flags that control debug outputs. Member Type Default Description enforceScopesInAPIs `bool` `false` Whether APIs calls that perform edits should throw errors if the corresponding scope does not allow the edit. showHandlesInteractionArea `bool` `{false}` Display the interaction area around the handles. useDebugMipmaps `bool` `false` Enable the use of colored mipmaps to see which mipmap is used. ### `MouseSettings` Member Type Default Description enableScroll `bool` `true` Whether the engine processes mouse scroll events. enableZoom `bool` `true` Whether the engine processes mouse zoom events. ### `PageSettings` Member Type Default Description allowCropInteraction `bool` `true` If crop interaction (by handles and gestures) should be possible when the enabled arrangements allow resizing. allowMoveInteraction `bool` `false` If move interaction (by handles and gestures) should be possible when the enabled arrangements allow moving and if the page layout is not controlled by the scene, e.g., in a ‘VerticalStack’. allowResizeInteraction `bool` `false` If a resize interaction (by handles and gestures) should be possible when the enabled arrangements allow resizing. allowRotateInteraction `bool` `false` If rotation interaction (by handles and gestures) should be possible when the enabled arrangements allow rotation and if the page layout is not controlled by the scene, e.g., in a ‘VerticalStack’. dimOutOfPageAreas `bool` `true` Whether the opacity of the region outside of all pages should be reduced. innerBorderColor `Color` `createRGBColor(0.0, 0.0, 0.0, 0.0)` Color of the inner frame around the page. marginFillColor `Color` `createRGBColor(0.79, 0.12, 0.40, 0.1)` Color of frame around the bleed margin area of the pages. marginFrameColor `Color` `createRGBColor(0.79, 0.12, 0.40, 0.0)` Color filled into the bleed margins of pages. moveChildrenWhenCroppingFill `bool` `false` Whether the children of the page should be transformed to match their old position relative to the page fill when a page fill is cropped. outerBorderColor `Color` `createRGBColor(1.0, 1.0, 1.0, 0.0)` Color of the outer frame around the page. restrictResizeInteractionToFixedAspectRatio `bool` `false` If the resize interaction should be restricted to fixed aspect ratio resizing. title `PageTitleSettings(bool show, bool showOnSinglePage, bool showPageTitleTemplate, bool appendPageName, string separator, Color color, string fontFileUri)` “ Page title settings. ### `PageTitleSettings` Member Type Default Description appendPageName `bool` `true` Whether to append the page name to the title if a page name is set even if the name is not specified in the template or the template is not shown color `Color` `createRGBColor(1., 1., 1.)` Color of page titles visible in preview mode, can change with different themes. fontFileUri `string` `DEFAULT_FONT` Font of page titles. separator `string` `"-"` Title label separator between the page number and the page name. show `bool` `true` Whether to show titles above each page. showOnSinglePage `bool` `true` Whether to hide the the page title when only a single page is given. showPageTitleTemplate `bool` `true` Whether to include the default page title from `page.titleTemplate` ### `PlaceholderControlsSettings` Member Type Default Description showButton `bool` `true` Show the placeholder button. showOverlay `bool` `true` Show the overlay pattern. ### `Settings` Member Type Default Description alwaysHighlightPlaceholders `bool` `false` Whether placeholder elements should always be highlighted in the scene. basePath `string` `""` The root directory to be used when resolving relative paths or when accessing `bundle://` URIs on platforms that don’t offer bundles. blockAnimations `BlockAnimationSettings: bool enabled` `{}` Settings that configure the behavior of block animations. borderOutlineColor `Color` `createRGBColor(0., 0., 0., 1.0)` The border outline color, defaults to black. camera `CameraSettings: CameraClampingSettings clamping` `{}` Settings that configure the behavior of the camera. clearColor `Color` `createClear()` The color with which the render target is cleared before scenes get rendered. Only used while renderMode == Preview, else #00000000 (full transparency) is used. colorMaskingSettings `ColorMaskingSettings(Color maskColor, bool secondPass)` `{}` A collection of settings used to perform color masking. controlGizmo `ControlGizmoSettings(bool showCropHandles, bool showCropScaleHandles, bool showMoveHandles, bool showResizeHandles, bool showScaleHandles, bool showRotateHandles, float blockScaleDownLimit)` `{}` Settings that configure which touch/click targets for move/scale/rotate/etc. are enabled and displayed. cropOverlayColor `Color` `createRGBColor(0., 0., 0., 0.39)` Color of the dimming overlay that’s added in crop mode. debug `DebugFlags(bool useDebugMipmaps, bool showHandlesInteractionArea, bool enforceScopesInAPIs)` `{}` ? defaultEmojiFontFileUri `string` `EMOJI_FONT` URI of default font file for emojis. defaultFontFileUri `string` `DEFAULT_FONT` URI of default font file This font file is the default everywhere unless overriden in specific settings. doubleClickSelectionMode `DoubleClickSelectionMode` `Hierarchical` The current mode of selection on double-click. doubleClickToCropEnabled `bool` `true` Whether double clicking on an image element should switch into the crop editing mode. emscriptenCORSConfigurations `vector< CORSConfiguration >` `{}` CORS Configurations: `` pairs. See `FetchAsyncService-emscripten.cpp` for details. errorStateColor `Color` `createRGBColor(1., 1., 1., 0.7)` The error state color for design blocks. fallbackFontUri `string` `""` The URI of the fallback font to use for text that is missing certain characters. forceSystemEmojis `bool` `true` Whether the system emojis should be used for text. globalScopes `GlobalScopes(Text text, Fill fill, Stroke stroke, Shape shape, Layer layer, Appearance appearance, Lifecycle lifecycle, Editor editor)` `Allow)` Global scopes. handleFillColor `Color` `createWhite()` The fill color for handles. highlightColor `Color` `createRGBColor(0.2, 85. / 255., 1.)` Color of the selection, hover, and group frames and for the handle outlines for non-placeholder elements. license `string` `""` A valid license string in JWT format. maxImageSize `int` `4096` The maximum size at which images are loaded into the engine. Images that exceed this size are down-scaled prior to rendering. Reducing this size further reduces the memory footprint. Defaults to 4096x4096. mouse `MouseSettings(bool enableZoom, bool enableScroll)` `{}` Settings that configure the behavior of the mouse. page `PageSettings(PageTitleSettings title, Color marginFillColor, Color marginFrameColor, Color innerBorderColor, Color outerBorderColor, bool dimOutOfPageAreas, bool allowCropInteraction, bool allowResizeInteraction, bool restrictResizeInteractionToFixedAspectRatio, bool allowRotateInteraction, bool allowMoveInteraction, bool moveChildrenWhenCroppingFill)` `{}` Page related settings. placeholderControls `PlaceholderControlsSettings(bool showOverlay, bool showButton)` `{}` Supersedes how the blocks’ placeholder controls are applied. placeholderHighlightColor `Color` `createRGBColor(0.77, 0.06, 0.95)` Color of the selection, hover, and group frames and for the handle outlines for placeholder elements. positionSnappingThreshold `float` `4.` Position snapping threshold in screen space. progressColor `Color` `createRGBColor(1., 1., 1., 0.7)` The progress indicator color. renderTextCursorAndSelectionInEngine `bool` `true` Whether the engine should render the text cursor and selection highlights during text editing. This can be set to false, if the platform wants to perform this rendering itself. rotationSnappingGuideColor `Color` `createRGBColor(1., 0.004, 0.361)` Color of the rotation snapping guides. rotationSnappingThreshold `float` `0.15` Rotation snapping threshold in radians. ruleOfThirdsLineColor `Color` `createRGBColor(0.75, 0.75, 0.75, 0.75)` Color of the rule-of-thirds lines. showBuildVersion `bool` `false` Show the build version on the canvas. snappingGuideColor `Color` `createRGBColor(1., 0.004, 0.361)` Color of the position snapping guides. textVariableHighlightColor `Color` `createRGBColor(0.7, 0., 0.7)` Color of the text variable highlighting borders. touch `TouchSettings(bool dragStartCanSelect, bool singlePointPanning, PinchGestureAction pinchAction, RotateGestureAction rotateAction)` `{}` Settings that configure which touch gestures are enabled and which actions they trigger. useSystemFontFallback `bool` `false` Whether the IMG.LY hosted font fallback is used for fonts that are missing certain characters, covering most of the unicode range. ### `TouchSettings` Member Type Default Description dragStartCanSelect `bool` `true` Whether dragging an element requires selecting it first. When not set, elements can be directly dragged. pinchAction `PinchGestureAction` `Scale` The action to perform when a pinch gesture is performed. rotateAction `RotateGestureAction` `Rotate` Whether or not the two finger turn gesture can rotate selected elements. singlePointPanning `bool` `true` Whether or not dragging on the canvas should move the camera (scrolling). When not set, the scroll bars have to be used. This setting might get overwritten with the feature flag `preventScrolling`. ``` engine.editor.findAllSettings()engine.editor.getSettingType("doubleClickSelectionMode") engine.editor.onSettingsChanged() .onEach { println("Editor settings have changed") } .launchIn(CoroutineScope(Dispatchers.Main)) engine.editor.onRoleChanged() .onEach { role -> println("Role changed to $role") } .launchIn(CoroutineScope(Dispatchers.Main)) engine.editor.setSettingBoolean("doubleClickToCropEnabled", value = true)engine.editor.getSettingBoolean("doubleClickToCropEnabled")try engine.editor.setSettingInt("integerSetting", value: 0)try engine.editor.getSettingInt("integerSetting")engine.editor.setSettingFloat("positionSnappingThreshold", value = 2.0F)engine.editor.getSettingFloat("positionSnappingThreshold")engine.editor.setSettingString("license", value = "invalid")engine.editor.getSettingString("license")engine.editor.setSettingColor("highlightColor", Color.fromRGBA(r = 1F, g = 0F, b = 1F, a = 1F)) // Pinkengine.editor.getSettingColor("highlightColor")engine.editor.setSettingEnum("doubleClickSelectionMode", value = "Direct")engine.editor.getSettingEnum("doubleClickSelectionMode")engine.editor.getSettingEnumOptions("doubleClickSelectionMode")engine.editor.getRole()engine.editor.setRole("Adopter") ``` ## Change Settings In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to control with the `editor` API. A list of all available settings can be found above. ### Exploration ``` fun findAllSettings(): List ``` Returns a list of all the settings available. * Returns the list of all settings. ``` fun getSettingType(keypath: String): PropertyType ``` Get the type of a setting. * `keypath`: the settings enum keypath, e.g. `doubleClickSelectionMode`. * Returns the type of the setting. ### Functions ``` fun onSettingsChanged(): Flow ``` Subscribe to changes to the editor settings. * Returns flow of editor settings change events. ``` fun onRoleChanged(): Flow ``` Subscribe to changes to the editor role. * Returns flow of role change events. ``` fun setSettingBoolean( keypath: String, value: Boolean,) ``` Set a boolean setting. * `keypath`: the settings keypath, e.g. `doubleClickToCropEnabled`. * `value`: the value to set. ``` fun getSettingBoolean(keypath: String): Boolean ``` Get a boolean setting. * `keypath`: the settings keypath, e.g. `doubleClickToCropEnabled`. * Returns the current value. ``` fun setSettingInt( keypath: String, value: Int,) ``` Set an integer setting. * `keypath`: the settings keypath. * `value`: the value to set. ``` fun getSettingInt(keypath: String): Int ``` Get an integer setting. * `keypath`: the settings keypath. * Returns the current value. ``` fun setSettingFloat( keypath: String, value: Float,) ``` Set a float setting. * `keypath`: the settings keypath, e.g. `positionSnappingThreshold`. * `value`: the value to set. ``` fun getSettingFloat(keypath: String): Float ``` Get a float setting. * `keypath`: the settings keypath, e.g. `positionSnappingThreshold`. * Returns the current value. ``` fun setSettingString( keypath: String, value: String,) ``` Set a string setting. * `keypath`: the settings keypath, e.g. `license`. * `value`: the value to set. ``` fun getSettingString(keypath: String): String ``` Get a string setting. * `keypath`: the settings keypath, e.g. `license`. * Returns the current value. ``` fun setSettingColor( keypath: String, value: RGBAColor,) ``` Set a color setting. * `keypath`: the settings keypath, e.g. `highlightColor`. * `value`: the value to set. ``` fun getSettingColor(keypath: String): RGBAColor ``` Get a color setting. * `keypath`: the settings keypath, e.g. `highlightColor`. * Returns the current value. ``` fun setSettingEnum( keypath: String, value: String,) ``` Set an enum setting. * `keypath`: the settings keypath, e.g. `doubleClickSelectionMode`. * `value`: the value to set. ``` fun getSettingEnum(keypath: String): String ``` Get an enum setting. * `keypath`: the settings keypath, e.g. `doubleClickSelectionMode`. * Returns the current value. ``` fun getSettingEnumOptions(keypath: String): List ``` Get all the available options of an enum setting. * `keypath`: the settings enum keypath, e.g. `doubleClickSelectionMode`. * Returns the list of enum options. ``` fun getRole(): String ``` Get the current role of the user ``` fun setRole(role: String) ``` Set the role of the user and apply role-dependent defaults for scopes and settings ## Full Code Here’s the full code: ``` engine.editor.findAllSettings()engine.editor.getSettingType("doubleClickSelectionMode") engine.editor.onSettingsChanged() .onEach { println("Editor settings have changed") } .launchIn(CoroutineScope(Dispatchers.Main)) engine.editor.onRoleChanged() .onEach { role -> println("Role changed to $role") } .launchIn(CoroutineScope(Dispatchers.Main)) engine.editor.setSettingBoolean("doubleClickToCropEnabled", value = true)engine.editor.getSettingBoolean("doubleClickToCropEnabled")try engine.editor.setSettingInt("integerSetting", value: 0)try engine.editor.getSettingInt("integerSetting")engine.editor.setSettingFloat("positionSnappingThreshold", value = 2.0F)engine.editor.getSettingFloat("positionSnappingThreshold")engine.editor.setSettingString("license", value = "invalid")engine.editor.getSettingString("license")engine.editor.setSettingColor("highlightColor", Color.fromRGBA(r = 1F, g = 0F, b = 1F, a = 1F)) // Pinkengine.editor.getSettingColor("highlightColor")engine.editor.setSettingEnum("doubleClickSelectionMode", value = "Direct")engine.editor.getSettingEnum("doubleClickSelectionMode")engine.editor.getSettingEnumOptions("doubleClickSelectionMode")engine.editor.getRole()engine.editor.setRole("Adopter") ``` --- [Source](https:/img.ly/docs/cesdk/android/serve-assets-b0827c) # Serve Assets From Your Server In this example, we explain how to configure the Creative Engine to use assets hosted on your own servers. While we serve all assets from our own CDN by default, it is highly recommended to serve the assets from your own servers in a production environment. ## 1\. Register IMG.LY’s default assets If you want to use our default asset sources in your integration, call `fun Engine.addDefaultAssetSources(baseUri: Uri, exclude: Set)`. Right after initialization: ``` val engine = Engine("ly.img.engine.example")engine.start()engine.addDefaultAssetSources() ``` This call adds IMG.LY’s default asset sources for stickers, vectorpaths and filters to your engine instance. By default, these include the following sources represented by `DefaultAssetSource` enum\`: * `DefaultAssetSource.STICKER` - `ly.img.sticker` - Various stickers. * `DefaultAssetSource.VECTOR_PATH` - `ly.img.vectorpath` - Shapes and arrows. * `DefaultAssetSource.FILTER_LUT` - `ly.img.filter.lut` - LUT effects of various kinds. * `DefaultAssetSource.FILTER_DUO_TONE` - `ly.img.filter.duotone` - Color effects of various kinds. * `DefaultAssetSource.COLORS_DEFAULT_PALETTE` - `ly.img.colors.defaultPalette` - Default color palette. * `DefaultAssetSource.EFFECT` - `ly.img.effect` - Default effects. * `DefaultAssetSource.BLUR` - `ly.img.blur` - Default blurs. * `DefaultAssetSource.TYPEFACE` - `ly.img.typeface` - Default typefaces. If you don’t specify a `baseUri` option, the assets are parsed and served from the IMG.LY CDN. It is highly recommended to serve the assets from your own servers in a production environment, if you decide to use them. To do so, follow the steps below and pass a `baseUri` option to `addDefaultAssetSources`. If you only need a subset of the IDs above, use the `exclude` option to pass a list of ignored `DefaultAssetSource` objects\`. ## 2\. Copy Assets Download the IMG.LY default assets from [our CDN](https://cdn.img.ly/assets/v3/IMGLY-Assets.zip). Copy the extracted folders to your own CDN server or to the android assets folder if you want to use them offline. It can be on the root or any subfolder. ## 3\. Configure the IMGLYEngine to use your self-hosted assets Next, we need to configure the SDK to use the copied assets instead of the ones served via IMG.LY CDN. `Engine.addDefaultAssetSources` offers a `baseUri` option, that needs to be set to an absolute Uri, pointing to your newly added assets. In case you have copied to your own cdn path: ``` val baseUri = Uri.parse("https://cdn.your.custom.domain/assets") engine.addDefaultAssetSources(baseUri) ``` In case you have copied to android assets folder: ``` val baseUri = Uri.parse("file:///android_asset/assets") engine.addDefaultAssetSources(baseUri) ``` --- [Source](https:/img.ly/docs/cesdk/android/security-777bfd) # Security This document provides a comprehensive overview of CE.SDK’s security practices, focusing on data handling, privacy, and our commitment to maintaining the highest standards of security for our customers and their end users. ## Key Security Features * **Client-Side Processing**: All image and design processing occurs directly on the user’s device, not on our servers * **No Data Transmission**: Your content (e.g. images, designs, templates, videos, audio, etc.) is never uploaded to or processed on IMG.LY servers * **Minimal Data Collection**: We only collect device identifiers and count exports for licensing purposes * **GDPR Compliance**: Our data collection practices adhere to GDPR regulations * **Secure Licensing**: Enterprise licenses are secured with RSA SHA256 encryption ## Data Protection & Access Controls ### Data Collection CE.SDK requires minimal data to provide its services. The only potentially personally identifiable information (PII) collected includes device-specific identifiers such as `identifierForVendor` on iOS and `ANDROID_ID` on Android. These identifiers are: * Used solely for tracking monthly active users for our usage-based pricing models * Reset when the user reinstalls the app or resets their device * Collected under GDPR’s legitimate interest provision (no explicit consent required as they are necessary for our licensing system) Additionally, we track export operations for billing purposes in usage-based pricing models. For enterprise customers who prefer more accurate tracking, integrators can provide their own userID. This allows for more precise measurement of usage without requiring additional device identifiers. ### Data Storage & Encryption **We do not collect or store user data beyond the device identifiers and export counts mentioned above.** Since CE.SDK operates entirely client-side: * All content processing happens on the user’s device * No images, designs, or user content is transmitted to IMG.LY servers * No content data is stored on IMG.LY infrastructure We use standard HTTPS (SSL/TLS) encryption for all communications between CE.SDK instances and our licensing backend. ### Access Controls We are using established industry standard practices to handle sensitive customer data. Therefore access control concerns are minimized. The limited data we do handle is protected as follows: * Billing information is stored in Stripe and accessed only by members of our finance team and C-level executives * API keys and credentials are stored securely in 1Password or GitHub with granular access levels * All employees sign Confidentiality Agreements to protect customer information This refers to data of our direct customers, not their users or customers. ## Licensing System CE.SDK uses a licensing system that works as follows: 1. During instantiation, an API key is provided to the CE.SDK instance 2. This API key is held in memory (never stored permanently on the device) 3. The SDK validates the key with our licensing backend 4. Upon successful validation, the backend returns a temporary local license 5. This license is periodically refreshed to maintain valid usage For browser implementations, we protect licenses against misuse by pinning them to specific domains. For mobile applications, licenses are pinned to the application identifiers to prevent unauthorized use. For enterprise customers, we offer an alternative model: * A license file is passed directly to the instance * No communication with our licensing service is required * Licenses are secured using RSA SHA256 encryption ## Security Considerations for User Input As CE.SDK deals primarily with arbitrary user input, we’ve implemented specific security measures to handle data safely: * The CreativeEngine reads files from external resources to fetch images, fonts, structured data, and other sources for designs. These reads are safeguarded by platform-specific default measures. * The engine never loads executable code or attempts to execute any data acquired from dynamic content. It generally relies on provided mime types to decode image data or falls back to byte-level inspection to choose the appropriate decoder. * For data writing operations, we provide a callback that returns a pointer to the to-be-written data. The engine itself never unconditionally writes to an externally defined path. If it writes to files directly, these are part of internal directories and can’t be modified externally. * Generated PDFs may have original image files embedded if the image was not altered via effects or blurs and the [`exportPdfWithHighCompatibility` option](android/export-save-publish/export/overview-9ed3a8/) was **not** enabled. This means a malicious image file could theoretically be included in the exported PDF. * Inline text-editing allows arbitrary input of strings by users. The engine uses platform-specific default inputs and APIs and doesn’t apply additional sanitization. The acquired strings are stored and used exclusively for text rendering - they are neither executed nor used for file operations. ## Security Infrastructure ### Vulnerability Management We take a proactive approach to security vulnerability management: * We use GitHub to track dependency vulnerabilities * We regularly update affected dependencies * We don’t maintain a private network, eliminating network vulnerability concerns in that context * We don’t manually maintain servers or infrastructure, as we don’t have live systems beyond those required for licensing * For storage and licensing, we use virtual instances in Google Cloud which are managed by the cloud provider * All security-related fixes are published in our public changelog at [https://img.ly/docs/cesdk/changelog/](https://img.ly/docs/cesdk/changelog/) ### Security Development Practices Our development practices emphasize security: * We rely on established libraries with proven security track records * We don’t directly process sensitive user data in our code * Secrets (auth tokens, passwords, API credentials, certificates) are stored in GitHub or 1Password with granular access levels * We use RSA SHA256 encryption for our enterprise licenses * We rely on platform-standard SSL implementations for HTTPS communications ### API Key Management API keys for CE.SDK are handled securely: * Keys are passed during instantiation and held in memory only * Keys are never stored permanently on client devices * For web implementation, keys are pinned to specific domains to prevent unauthorized use * Enterprise licenses use a file-based approach that doesn’t require API key validation ## Compliance IMG.LY complies with the General Data Protection Regulation (GDPR) in all our operations, including CE.SDK. Our Privacy Policy is publicly available at [https://img.ly/privacy-policy](https://img.ly/privacy-policy). Our client-side approach to content processing significantly reduces privacy and compliance concerns, as user content never leaves their device environment for processing. ## FAQ ### Does CE.SDK upload my images or designs to IMG.LY servers? No. CE.SDK processes all content locally on the user’s device. Your images, designs, and other content are never transmitted to IMG.LY servers. ### What data does IMG.LY collect through CE.SDK? CE.SDK only collects device identifiers (such as identifierForVendor on iOS or ANDROID\_ID on Android) for licensing purposes and export counts. No user content or personal information is collected. ### How does IMG.LY protect API keys? API keys are never stored permanently; they are held in memory during SDK operation. For web implementations, keys are pinned to specific domains to prevent unauthorized use. ### Has IMG.LY experienced any security breaches? No, IMG.LY has not been involved in any cybersecurity breaches in the last 12 months. ### Does IMG.LY conduct security audits? As we don’t store customer data directly, but rely on third parties to do so, we focus our security efforts on dependency tracking and vulnerability management through GitHub’s security features. We don’t conduct security audits. ## Additional Information For more detailed information about our data collection practices, please refer to our Data Privacy and Retention information below. Should you have any additional questions regarding security practices or require more information, please contact our team at [support@img.ly](mailto:support@img.ly). ## Data Privacy and Retention At IMG.LY, we prioritize your data privacy and ensure that apart from a minimal contractually stipulated set of interactions with our servers all other operations take place on your local device. Below is an overview of our data privacy and retention policies: ### **Data Processing** All data processed by CE.SDK remains strictly on your device. We do not transfer your data to our servers for processing. This means that operations such as rendering, editing, and other in-app functionalities happen entirely locally, ensuring that sensitive project or personal data stays with you. ### **Data Retention** We do not store any project-related data on our servers. Since all data operations occur locally, no information about your edits, images, or video content is retained by CE.SDK. The only data that interacts with our servers is related to license validation and telemetry related to usage tied to your pricing plan. ### **License Validation** CE.SDK performs a license validation check with our servers once upon initialization to validate the software license being used. This interaction is minimal and does not involve the transfer of any personal, project, or media data. ### **Event Tracking** While CE.SDK does not track user actions other than the exceptions listed below through telemetry or analytics by default, there are specific events tracked to manage customer usage, particularly for API key usage tracking. We gather the following information during these events: * **When the engine loads:** App identifier, platform, engine version, user ID (provided by the client), device ID (mobile only), and session ID. * **When a photo or video is exported:** User ID, device ID, session ID, media type (photo/video), resolution (width and height), FPS (video only), and duration (video only). This tracking is solely for ensuring accurate usage calculation and managing monthly active user billing. Enterprise clients can opt out of this tracking under specific agreements. ### **Personal Identifiable Information (PII)** The only PII that is potentially collected includes device-specific identifiers such as `identifierForVendor` on iOS and `ANDROID_ID` on Android. These IDs are used for tracking purposes and are reset when the user reinstalls the app or resets the device. No consent is required for these identifiers because they are crucial for our usage-based pricing models. This is covered by the GDPR as legitimate interest. ### **User Consent** As mentioned above, user consent is not required when solely using the CE.SDK. However, this may change depending on the specific enterprise agreement or additional regulatory requirements. IMG.LY is committed to maintaining compliance with **GDPR** and other applicable data protection laws, ensuring your privacy is respected at all times. For details consult our [privacy policy](https://img.ly/privacy-policy). --- [Source](https:/img.ly/docs/cesdk/android/rules-1427c0) # Rules --- [Source](https:/img.ly/docs/cesdk/android/prebuilt-solutions-d0ed07) # Prebuilt Solutions --- [Source](https:/img.ly/docs/cesdk/android/overview-8cc730) # Overview --- [Source](https:/img.ly/docs/cesdk/android/overview-7d12d5) # Android Video Editor SDK Use CreativeEditor SDK (CE.SDK) to build robust video editing experiences directly in your app. CE.SDK supports both video and audio editing — including trimming, joining, adding text, annotating, and more — all performed client-side without requiring a server. Developers can integrate editing functionality using a built-in UI or programmatically via the SDK API. CE.SDK also supports voiceover, music, and sound effects alongside video editing. You can integrate custom or third-party AI models to streamline creative workflows, such as converting image to video or generating clips from text. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Core Capabilities CreativeEditor SDK includes a comprehensive set of video editing tools, accessible through both a UI and programmatic interface. Supported editing actions include: * **Trim, Split, Join, and Arrange**: Modify clips, reorder segments, and stitch together content. * **Transform**: Crop, rotate, resize, scale, and flip. * **Audio Editing**: Add, adjust, and synchronize audio including music, voiceovers, and effects. * **Programmatic Editing**: Control all editing features via API. CE.SDK is well-suited for scenarios like short-form content, reels, promotional videos, and other linear video workflows. ## Timeline Editor The built-in timeline editor provides a familiar video editing experience for users. It supports: * Layered tracks for video and audio * Drag-and-drop sequencing with snapping * Trim handles, in/out points, and time offsets * Real-time preview updates The timeline is the main control for video editing: ![The editor timeline control.](/docs/cesdk/_astro/video_mode_timeline.BkrXFlTn_2e2pv5.webp) ## AI-Powered Editing CE.SDK allows you to easily integrate AI tools directly into your video editing workflow. Users can generate images, videos, audio, and voiceovers from simple prompts — all from within the editor’s task bar, without switching tools or uploading external assets. [Launch AI Editor Demo](https://img.ly/showcases/cesdk/ai-editor/web) You can bring your own models or third-party APIs with minimal setup. AI tools can be added as standalone plugins, contextual buttons, or task bar actions. ## Supported Input Formats and Codecs CE.SDK supports a wide range of video input formats and encodings, including: Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. CE.SDK supports the most widely adopted video and audio codecs to ensure compatibility across platforms: ### **Video Codecs** * **H.264 / AVC** (in `.mp4`) * **H.265 / HEVC** (in `.mp4`, may require platform-specific support) ### **Audio Codecs** * **MP3** (in `.mp3` or within `.mp4`) * **AAC** (in `.m4a` or within `.mp4` or `.mov`) ## Output and Export Options You can export edited videos in several formats, with control over resolution, encoding, and file size: Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ## UI-Based vs. Programmatic Editing CE.SDK offers a fully interactive editor with intuitive UI tools for creators. At the same time, developers can build workflows entirely programmatically using the SDK API. * Use the UI to let users trim, arrange, and caption videos manually * Use the API to automate the assembly or editing of videos at scale ## Customization You can tailor the editor to match your product’s design and user needs: * Show or hide tools * Reorder UI elements and dock items * Apply custom themes, colors, or typography * Add additional plugin components ## Performance and File Size Considerations All editing operations are performed client-side. While this ensures user privacy and responsiveness, it introduces some limits: Constraint Recommendation / Limit **Resolution** Up to **4K UHD** is supported for **playback** and **export**, depending on the user’s hardware and available GPU resources. For **import**, CE.SDK does not impose artificial limits, but maximum video size is bounded by the **32-bit address space of WebAssembly (wasm32)** and the **browser tab’s memory cap (~2 GB)**. **Frame Rate** 30 FPS at 1080p is broadly supported; 60 FPS and high-res exports benefit from hardware acceleration **Duration** Stories and reels of up to **2 minutes** are fully supported. Longer videos are also supported, but we generally found a maximum duration of **10 minutes** to be a good balance for a smooth editing experience and a pleasant export duration of around one minute on modern hardware. Performance scales with client hardware. For best results with high-resolution or high-frame-rate video, modern CPUs/GPUs with hardware acceleration are recommended. --- [Source](https:/img.ly/docs/cesdk/android/overview-491658) # Overview In CE.SDK, _inserting media into a scene_ means placing visual or audio elements directly onto the canvas—images, videos, audio clips, shapes, or stickers—so they become part of the design. This differs from _importing assets_, which simply makes media available in the asset library. This guide helps you understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code. By the end, you’ll know how media are represented, modified, saved, and exported. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Inserting Media vs. Importing Assets Before you can insert media into a scene, it must first be available to CE.SDK—this is where _importing_ comes in. Imported assets are added to the **Asset Library** (from local uploads, remote sources, etc.), where they become available for use. _Inserting_ means placing those assets into the actual scene—either as the fill of a design block (like an image inside a rectangle), or as a standalone visual/audio layer. This process creates scene elements that users can see, move, style, and manipulate. ## How Media Is Handled in Scenes Internally, inserted media are structured as part of the scene graph. Most are represented as fills or as design blocks: * **Images and Videos** are typically inserted as _fills_ for graphic blocks or as independent blocks for visual layering. * **Audio** is inserted as a timeline-based media block, often invisible but timeline-active. * **Shapes and Stickers** are treated as standalone graphic blocks, with shape or vector fills. Each inserted item is assigned an ID and properties such as position, size, and rotation, and can be queried or modified programmatically. ## Inserting Media ### Insert via the UI You can insert media using the built-in CE.SDK interface. The most common methods are: * Drag-and-drop from the **Asset Library** into the canvas. * Clicking an asset in the panel to place it at the center of the scene. * Using context menus or toolbar buttons (e.g., “Add Image” or “Insert Audio”). You can also configure the UI to show or hide certain media categories, allowing for tailored user experiences. See the **Customize Asset Library** guide for more on controlling visible media types. ### Insert Programmatically Developers can insert media directly via the SDK. Whether you’re building a dynamic editor or triggering insertions via user input, CE.SDK exposes APIs for: * Creating a new design block and applying a media fill (e.g., image, video). * Controlling properties like position, size, rotation, opacity, and z-index. * Embedding logic to sync insertions with UI actions or backend data. ## Referencing Existing Assets Once an asset is imported, you can reference it multiple times without re-importing. Reuse is based on: * **URI** (useful for remote assets) When reusing an asset, you can apply different visual properties—each inserted instance can have its own size, position, rotation, or visual effects. ## Media Lifecycle Within a Scene Inserted media are part of the live scene graph and follow CE.SDK’s scene lifecycle: * **Saving a Scene**: Inserted media references (or embedded content) are included in the saved `.scene` or `.archive`. * **Reloading a Scene**: CE.SDK reconstructs the scene graph and fetches any required media URIs or binary data. * **Exporting**: Media may be embedded directly (e.g., for self-contained exports) or referenced externally (e.g., smaller file sizes, shared assets). Keep in mind that media integrity on reload/export depends on how the asset was inserted—linked URIs must remain available, whereas embedded assets are bundled. ## Embedding vs. Linking Media CE.SDK supports two strategies for handling inserted media: Mode Description Use Case **Referenced** (Scene Files) Scene references the media via URI. Smaller file sizes, shared asset use. **Embedded** (Archives) Media is stored directly in the saved archive. Offline editing, portable scenes. **Embedded** media increase file size but ensure portability. **Referenced** media reduce storage needs but require external hosting. You can control this behavior when saving or exporting a scene. --- [Source](https:/img.ly/docs/cesdk/android/outlines-b7820c) # Outlines --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor-23a1db) # Open the Editor --- [Source](https:/img.ly/docs/cesdk/android/llms-txt-eb9cc5) # LLMs.txt Our documentation is now available in LLMs.txt format, optimized for AI reasoning engines. To better support platform-specific development, we’ve created separate documentation files for each platform. For Android developers, this means you can now access documentation tailored to your specific platform, whether it’s iOS, Android, Web, or any other supported platform. This approach allows for a more focused and efficient use of AI tools in your development workflow. [ Download /android/llms-full.txt ](https://img.ly/docs/cesdk/android/llms-full.txt) These documentation files are substantial in size, with token counts exceeding the context windows of many AI models. This guide explains how to download and effectively use these platform-specific documentation files with AI tools to accelerate your development process. ## What are LLMs.txt files? LLMs.txt is an emerging standard for making documentation AI-friendly. Unlike traditional documentation formats, LLMs.txt: * Presents content in a clean, markdown-based format * Eliminates extraneous HTML, CSS, and JavaScript * Optimizes content for AI context windows * Provides a comprehensive view of documentation in a single file By using our platform-specific LLMs.txt files, you’ll ensure that AI tools have the most relevant and complete context for helping with your development tasks. ## Handling Large Documentation Files Due to the size of our documentation files (upward of 500 000 tokens) most AI tools will face context window limitations. Standard models typically have context windows ranging from 8,000 to 200,000 tokens, making it challenging to process our complete documentation in a single session. ### Recommended AI Model for Full Documentation For working with our complete documentation files, we recommend: * **Gemini 2.5 Flash**: Available via Google AI Studio with a context window of 1-2 million tokens, capable of handling even our largest documentation file --- [Source](https:/img.ly/docs/cesdk/android/licensing-8aa063) # Licensing Thanks for your interest in CreativeEditor SDK (CE.SDK). We offer flexible commercial licensing options to support teams and projects of all sizes. Whether you’re building a new product or scaling an existing one, our goal is to provide the best creative editing experience—backed by a licensing model that aligns with your needs. Get in touch with us through our [contact sales form](https://img.ly/forms/contact-sales). ## Commercial Licensing CE.SDK is offered through a subscription-based commercial model. This allows us to: * Deliver ongoing updates and performance improvements * Ensure compatibility with new browsers and devices * Provide dedicated technical support * Build long-term partnerships with our customers ## How Licensing Works CE.SDK licenses are tied to a single commercial product instance, verified by the hostname for web apps and bundle/app ID for mobile apps. Licensing typically uses remote validation and includes lightweight event tracking. It’s possible to disable tracking or use offline-compatible options. To explore these options, [contact our sales team](https://img.ly/forms/contact-sales). ## Trial License Key Trial licenses are available for evaluation and testing and are valid for **30 days**. They provide full access to CE.SDK’s features so you can explore its capabilities in your environment. If you need more time to evaluate, [contact our sales team](https://img.ly/forms/contact-sales). ## Testing and Production Paid license keys can be used across development, staging, and production environments. Multiple domains or app identifiers can be added to support this setup. --- [Source](https:/img.ly/docs/cesdk/android/key-capabilities-dbb5b1) # Key Capabilities This guide gives you a high-level look at what CreativeEditor SDK (CE.SDK) can do—and how deeply it can integrate into your workflows. Whether you’re building a design editor into your product, enabling automation, or scaling personalized content creation, CE.SDK provides a flexible and future-ready foundation. [Launch Web Demo](https://img.ly/showcases/cesdk) It’s designed for developers, product teams, and technical decision-makers evaluating how CE.SDK fits their use case. * 100% client-side processing * Built from the ground up (not based on open-source libraries) * Flexible enough for both low-code and fully custom implementations ## Design Creation and Editing Create, edit, compose, and customize designs—including images, video, and multi-page layouts—directly in the browser. Use the built-in editor interface or access the same functionality through code. You can: * Enable social media content creation or UGC flows * Power marketing tools for creative teams * Allow users to build branded assets, slides, or product visuals * Arrange scenes using composition tools like multi-page layouts, collages, and background blending Available features include filters, text editing, stickers, layers, layout tools, and more. ## Programmatic and UI-based You get full control over how your users interact with CE.SDK. * Use the built-in UI for manual workflows * Call the same editing and rendering functionality programmatically * Combine both for hybrid use cases (e.g., users edit manually, backend creates variations) Whether you’re serving designers or developers—or both—CE.SDK adapts to your product’s needs. ## Templates and Reusable Layouts Define reusable templates to simplify design creation. These templates support: * Role-based editing (lock/unlock elements based on user type) * Smart placeholders (predefined image/text drop zones) * Preset styles for consistent branding * Programmatic or user-driven updates Templates make it easy to scale consistent design output while keeping editing intuitive. ## Automation and Dynamic Content You can generate visuals automatically by combining templates with structured data. Common use cases include personalized ads, localizations, product catalogs, or A/B testing. The SDK works in headless mode and supports batch workflows, making it easy to automate at scale. ## Multi-modal CE.SDK supports a wide range of content types and formats: * Input types: images, video, audio, structured data, templates * Output formats: PNG, JPEG, WebP, MP4, PDF, raw data All operations—including export—run client-side, ensuring fast performance and data privacy. ## AI Integration Instantly bring best-in-class AI-powered image and video editing to your application with just a few lines of code. All tools run directly inside the existing editor interface—so users stay in the flow while benefiting from powerful automation. Examples of what you can enable: * **Text to Image** – Use prompts to generate original visual content directly in the editor * **Text to Graphics** – Create crisp, scalable illustrations from simple text input * **Style Transfer** – Change the mood or style of an image while preserving its structure * **Create Variants** – Generate endless visual variations of a single subject for campaigns or personalization * **Image to Video** – Transform static images into dynamic motion content with a click * **Text to Speech** – Turn written copy into natural-sounding voiceovers, with control over tone and speed * **Smart Text Editing** – Rewrite or refine text using intelligent editing suggestions * **Swap Background** – Remove or replace backgrounds in seconds * **Add / Remove Objects** – Modify images with generative precision—no external tools required These capabilities can be integrated connecting to any third-party AI model or API with minimal setup. ## Customizable UI The built-in interface is designed to be fully customizable: * Rearrange or rename tools * Apply theming and branding * Show/hide features based on user role * Add translations for international users It works across desktop and mobile, and can be extended or replaced entirely if needed. ## Extensibility Need to add a custom feature or integrate with your backend? CE.SDK supports extensibility at multiple levels: * Custom UI components (or build a completely custom UI) * Backend data integrations (e.g., asset management systems) * Custom logic or validation rules * Advanced export workflows The SDK’s plugin architecture ensures you can scale your functionality without rebuilding the core editor. ## Content Libraries CE.SDK ships with a robust system for managing reusable content: * Built-in libraries of stickers, icons, overlays, and fonts * Integration with third-party providers like Getty Images, Unsplash, or Airtable * Programmatic filtering and categorization * Access assets from both code and UI * Organize by brand, user, or use case This makes it easy to deliver a seamless editing experience—no matter how many assets you manage. --- [Source](https:/img.ly/docs/cesdk/android/insert-media-a217f5) # Insert Media Into Scenes --- [Source](https:/img.ly/docs/cesdk/android/import-media-4e3703) # Import Media --- [Source](https:/img.ly/docs/cesdk/android/guides-8d8b00) # Guides --- [Source](https:/img.ly/docs/cesdk/android/filters-and-effects-6f88ac) # Filters and Effects --- [Source](https:/img.ly/docs/cesdk/android/fills-402ddc) # Fills --- [Source](https:/img.ly/docs/cesdk/android/file-format-support-3c4b2a) # File Format Support CreativeEditor SDK (CE.SDK) supports a wide range of modern file types for importing assets and exporting final content. Whether you’re working with images, videos, audio, documents, or fonts, CE.SDK provides a client-side editing environment with excellent media compatibility and performance—optimized for modern client-side hardware. This guide outlines supported formats, codecs, and known limitations across media types. ## Importing Media Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. ## Exporting Media Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ## Importing Templates Format Description `.idml` InDesign `.psd` Photoshop `.scene` CE.SDK Native Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs to generate scenes programmatically. ## Font Formats Format Description `.ttf` TrueType Font `.otf` OpenType Font `.woff` Web Open Font Format `.woff2` Compressed Web Open Font Format 2 Fonts should be appropriately licensed before being embedded in your application. ## Video & Audio Codecs CE.SDK supports the most widely adopted video and audio codecs to ensure compatibility across platforms: ### **Video Codecs** * **H.264 / AVC** (in `.mp4`) * **H.265 / HEVC** (in `.mp4`, may require platform-specific support) ### **Audio Codecs** * **MP3** (in `.mp3` or within `.mp4`) * **AAC** (in `.m4a` or within `.mp4` or `.mov`) ## Size Limits ### Image Resolution Limits Constraint Recommendation / Limit **Input Resolution** Maximum input resolution is **4096×4096 pixels**. Images from external sources (e.g., Unsplash) are resized to this size before rendering on the canvas. You can modify this value using the `maxImageSize` setting. **Output Resolution** There is no enforced output resolution limit. Theoretically, the editor supports output sizes up to **16,384×16,384 pixels**. However, practical limits depend on the device’s GPU capabilities and available memory. All image processing in CE.SDK is handled client-side, so these values depend on the **maximum texture size** supported by the user’s hardware. The default limit of 4096×4096 is a safe baseline that works universally. Higher resolutions (e.g., 8192×8192) may work on certain devices but could fail on others during export if the GPU texture size is exceeded. To ensure consistent results across devices, it’s best to test higher output sizes on your target hardware and set conservative defaults in production. ### Video Resolution & Duration Limits Constraint Recommendation / Limit **Resolution** Up to **4K UHD** is supported for **playback** and **export**, depending on the user’s hardware and available GPU resources. For **import**, CE.SDK does not impose artificial limits, but maximum video size is bounded by the **32-bit address space of WebAssembly (wasm32)** and the **browser tab’s memory cap (~2 GB)**. **Frame Rate** 30 FPS at 1080p is broadly supported; 60 FPS and high-res exports benefit from hardware acceleration **Duration** Stories and reels of up to **2 minutes** are fully supported. Longer videos are also supported, but we generally found a maximum duration of **10 minutes** to be a good balance for a smooth editing experience and a pleasant export duration of around one minute on modern hardware. Performance scales with client hardware. For best results with high-resolution or high-frame-rate video, modern CPUs/GPUs with hardware acceleration are recommended. --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish-47e28a) # Export, Save, and Publish --- [Source](https:/img.ly/docs/cesdk/android/edit-video-19de01) # Edit Videos --- [Source](https:/img.ly/docs/cesdk/android/edit-image-c64912) # Edit Image --- [Source](https:/img.ly/docs/cesdk/android/create-video-c41a08) # Create Videos --- [Source](https:/img.ly/docs/cesdk/android/create-templates-3aef79) # Create Templates --- [Source](https:/img.ly/docs/cesdk/android/create-composition-db709c) # Create Compositions --- [Source](https:/img.ly/docs/cesdk/android/conversion-c3fbb3) # Conversion --- [Source](https:/img.ly/docs/cesdk/android/configuration-2c1c3d) # Configuration ``` import android.net.Uriimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EngineConfigurationimport ly.img.editor.core.engine.EngineRenderTargetimport ly.img.editor.rememberForDesign // Add this composable to your NavHost@Composablefun BasicEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", baseUri = Uri.parse("file:///android_asset/"), sceneUri = EngineConfiguration.defaultDesignSceneUri, renderTarget = EngineRenderTarget.SURFACE_VIEW, ) DesignEditor(engineConfiguration = engineConfiguration) { // You can set result here navController.popBackStack() }} ``` In this example, we will show you how to make basic configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). ## Configuration All the basic configuration settings are part of the `EngineConfiguration`. * `license` - the license to activate the [Engine](android/get-started/overview-e18f40/) with. ``` license = "", ``` * `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`. ``` userId = "", ``` * `baseUri` - is used to initialize the engine’s [`basePath` setting](android/settings-970c98/) before the editor’s [`onCreate` callback](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](https://cdn.img.ly/packages/imgly/cesdk-engine/1.51.0/assets) but it should be changed in production environments. ``` baseUri = Uri.parse("file:///android_asset/"), ``` * `sceneUri` - the [scene](android/open-the-editor/blank-canvas-18ff05/) URI to load content within the editor. Note that this configuration is only available in `EngineConfiguration.rememberFor{solution-name}` helper functions. This URI is used to load the scene in `EdiorConfiguration.onCreate`, therefore, you can configure the scene you load without helper functions too: simply invoke `EditorDefaults.onCreate(engine, sceneUri, eventHandler)` in `EdiorConfiguration.onCreate`. By default, helper functions load the scenes that are available at `EdiorConfiguration.default{solution-name}Scene`. Normally, you should not modify the `sceneUri`, however, you may want to save/restore the editing progress for your customers. If that is the case, you should [save the scene](android/export-save-publish/save-c8b124/) in one of the [callbacks](android/user-interface/events-514b70/), then provide the URI of the newly saved scene when your customer opens the editor next time. ``` sceneUri = EngineConfiguration.defaultDesignSceneUri, ``` * `renderTarget` - the target which should be used by the [Engine](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`. ``` renderTarget = EngineRenderTarget.SURFACE_VIEW, ``` ## Full Code Here’s the full code: ``` import android.net.Uriimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EngineConfigurationimport ly.img.editor.core.engine.EngineRenderTargetimport ly.img.editor.rememberForDesign // Add this composable to your NavHost@Composablefun BasicEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", baseUri = Uri.parse("file:///android_asset/"), sceneUri = EngineConfiguration.defaultDesignSceneUri, renderTarget = EngineRenderTarget.SURFACE_VIEW, ) DesignEditor(engineConfiguration = engineConfiguration) { // You can set result here navController.popBackStack() }} ``` --- [Source](https:/img.ly/docs/cesdk/android/concepts-b9153a) # Concepts --- [Source](https:/img.ly/docs/cesdk/android/colors-a9b79c) # Colors --- [Source](https:/img.ly/docs/cesdk/android/bundle-size-df9210) # Bundle Size ## 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. --- [Source](https:/img.ly/docs/cesdk/android/compatibility-139ef9) # System Compatibility CE.SDK runs entirely on clients and makes use of hardware acceleration provided within that environment. Therefore, the user’s hardware always acts as an upper bound of what’s achievable. The editor’s performance scales with scene complexity. We generally found scenes with up to 200 blocks well usable, but complex blocks like auto-sizing text or high-resolution image fills may affect performance negatively. This is always constrained by the processing power available on the user’s device, so for low-end devices, the experience may suffer earlier. Therefore, it’s generally desirable to keep scenes only as complex as needed. ## Hardware Limitations Each device has a limited amount of high performance hardware decoders and encoders. If the maximum number is reached it will fall back to (slow) software de- and encoding. Therefore users may encounter slow export performance when trying to export in parallel with other software that utilizes encoders and decoders, e.g. Zoom, Teams or video content in other tabs / apps. This, unfortunately, is a limitation of hardware, operating system and browser and cannot be solved. ## 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](android/export-save-publish/export/overview-9ed3a8/). --- [Source](https:/img.ly/docs/cesdk/android/automation-715209) # Automate Workflows --- [Source](https:/img.ly/docs/cesdk/android/animation-ce900c) # Animation --- [Source](https:/img.ly/docs/cesdk/android/user-interface/ui-extensions-d194d1) # UI Extensions --- [Source](https:/img.ly/docs/cesdk/android/user-interface/overview-41101a) # Overview The CreativeEditor SDK (CE.SDK) includes a powerful, fully-integrated user interface that enables your users to create, edit, and export stunning designs—without requiring you to build a UI from scratch. Whether you’re launching a full-featured editor or embedding design tools into a larger application, CE.SDK provides everything you need to get started quickly. Out of the box, the UI is professional, responsive, and production-ready. But it’s not a one-size-fits-all solution. You can fully **customize**, **extend**, or even **replace the UI entirely** with your own interface built on top of the CE.SDK engine. The SDK is designed to be as flexible as your product demands. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Architecture CE.SDK’s UI is modular, declaratively configured, and tightly integrated with the core engine. At a high level, it consists of: * **Core Engine APIs** — The underlying logic for manipulating scenes, blocks, assets, and rendering * **UI Components** — Panels, bars, buttons, and menus that interact with the engine through configuration and callbacks * **Event System** — A reactive layer that tracks user input, selections, and state transitions This separation of concerns allows you to extend, replace, or completely rebuild the UI without impacting the rendering and scene logic handled by the engine. ## Customizing the UI You can tailor the editor’s interface to match your brand and use case. CE.SDK provides flexible APIs and configuration options for customizing: ### Appearance * Change the UI theme or colors * Use custom fonts and icons * Localize labels and messages ### Layout * Show or hide components based on context * Reorder buttons or entire sections * Rearrange dock elements or panel positions ### Behavior * Enable or disable specific features * Apply feature-based logic (e.g., show certain tools only for certain block types) ## Extending the UI In addition to customizing what’s already there, you can **add entirely new functionality** to the UI: * **Quick Actions** — One-click tools that perform fast edits (e.g., remove background) * **Custom Buttons** — Add buttons to the dock, canvas menu, or canvas bar * **Custom Panels** — Create complex UIs to support advanced workflows like export wizards or AI tools * **Third-Party Integrations** — Connect with external APIs, such as QR generators or content management systems Use the **Plugin API** to encapsulate these enhancements into portable, declarative extensions. These plugins can be dynamically loaded, reused across projects, and even distributed. > Tip: You don’t need to use the Plugin API to modify the UI—but it’s the best approach when you want to encapsulate logic, reuse it, or offer it to others. ## Building Your Own UI While CE.SDK includes a fully-featured UI by default, you’re not locked into it. Many developers choose to **build a completely custom UI** on top of the CE.SDK engine. This approach gives you full control over layout, interaction patterns, and visual design. When building your own UI, you interact directly with: * **The CE.SDK Engine** — Use the core APIs to manage scenes, create or modify blocks, control playback, and export content * **Canvas Rendering** — Render and manipulate the canvas area within your application shell * **State and Events** — Observe selections, listen for changes, and update your UI reactively This approach is ideal when: * You need tight integration with a larger application or workflow * You want to match a highly specific design system * You’re building for a unique form factor or device * You need to simplify the UI dramatically for a focused use case ## Integrating with Custom Workflows The CE.SDK UI isn’t a closed system—it plays well with your broader application logic and workflows. You can: * **Sync programmatic state** — Reflect external data (e.g., product names or image URLs) directly in the editor * **Control headless rendering** — Run the engine without the UI for automation or server-side rendering * **Trigger external logic** — Connect UI actions (like export) to your own backend services The UI components can be programmatically configured, replaced, or completely bypassed depending on your needs. Whether you’re creating a collaborative editor, running batch jobs, or embedding CE.SDK in a no-code platform, you have full control over how the UI interacts with your app. --- [Source](https:/img.ly/docs/cesdk/android/user-interface/events-514b70) # UI Events ``` import android.net.Uriimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport kotlinx.coroutines.Jobimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport ly.img.editor.DesignEditorimport ly.img.editor.EditorDefaultsimport ly.img.editor.EngineConfigurationimport ly.img.editor.HideLoadingimport ly.img.editor.ShareFileEventimport ly.img.editor.ShowCloseConfirmationDialogEventimport ly.img.editor.ShowErrorDialogEventimport ly.img.editor.ShowLoadingimport ly.img.editor.core.event.EditorEventimport ly.img.editor.core.library.data.TextAssetSourceimport ly.img.editor.core.library.data.TypefaceProviderimport ly.img.engine.MimeTypeimport ly.img.engine.addDefaultAssetSourcesimport ly.img.engine.addDemoAssetSources // Add this composable to your NavHost@Composablefun CallbacksEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { // Note that lambda is copied from EditorDefaults.onCreate coroutineScope { // In case of process recovery, engine automatically recovers the scene that is why we need to check if (editorContext.engine.scene.get() == null) { editorContext.engine.scene.load(EngineConfiguration.defaultDesignSceneUri) } launch { val baseUri = Uri.parse("https://cdn.img.ly/assets/v3") editorContext.engine.addDefaultAssetSources(baseUri = baseUri) val defaultTypeface = TypefaceProvider().provideTypeface(editorContext.engine, "Roboto") requireNotNull(defaultTypeface) editorContext.engine.asset.addSource(TextAssetSource(editorContext.engine, defaultTypeface)) } launch { editorContext.engine.addDemoAssetSources( sceneMode = editorContext.engine.scene.getMode(), withUploadAssetSources = true, baseUri = Uri.parse("https://cdn.img.ly/assets/demo/v2"), ) } coroutineContext[Job]?.invokeOnCompletion { editorContext.eventHandler.send(HideLoading) } } }, onExport = { EditorDefaults.run { editorContext.eventHandler.send(ShowLoading) val blob = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) { scene.getPages().forEach { block.setScopeEnabled(it, key = "layer/visibility", enabled = true) block.setVisible(it, visible = true) } } val tempFile = writeToTempFile(blob) editorContext.eventHandler.send(HideLoading) editorContext.eventHandler.send(ShareFileEvent(tempFile, MimeType.PDF.key)) } }, onUpload = { assetDefinition, _ -> val meta = assetDefinition.meta ?: return@remember assetDefinition val sourceUri = Uri.parse(meta["uri"]) val uploadedUri = sourceUri // todo upload the asset here and return remote uri val newMeta = meta + listOf( "uri" to uploadedUri.toString(), "thumbUri" to uploadedUri.toString(), ) assetDefinition.copy(meta = newMeta) }, onClose = { hasUnsavedChanges -> if (hasUnsavedChanges) { editorContext.eventHandler.send(ShowCloseConfirmationDialogEvent) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } }, onError = { error -> editorContext.eventHandler.send(ShowErrorDialogEvent(error)) }, ) DesignEditor(engineConfiguration = engineConfiguration) { // You can set result here navController.popBackStack() }} ``` In this example, we will show you how to configure the callbacks of various editor events for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Note that the bodies of all callbacks except `onUpload` are copied from the `Design Editor` default implementations. ## Configuration All the callback configurations are part of the `EngineConfiguration` class. Note that all the callbacks receive `EditorEventHandler` parameter that can be used to send UI events. * `onCreate` - the callback that is invoked when the editor is created. This is the main initialization block of both the editor and engine. Normally, you should [load](android/open-the-editor/load-scene-478833/) or [create](android/open-the-editor/blank-canvas-18ff05/) a scene as well as prepare asset sources in this block. We recommend that you check the availability of the scene before creating/loading a new scene since a recreated scene may already exist if the callback is invoked after a process recreation. Callback does not have a default implementation, as default scenes are solution-specific, however, `EditorDefaults.onCreate` contains the default logic. By default, it loads a scene and adds all default and demo asset sources. Note that the “create” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. ``` onCreate = { // Note that lambda is copied from EditorDefaults.onCreate coroutineScope { // In case of process recovery, engine automatically recovers the scene that is why we need to check if (editorContext.engine.scene.get() == null) { editorContext.engine.scene.load(EngineConfiguration.defaultDesignSceneUri) } launch { val baseUri = Uri.parse("https://cdn.img.ly/assets/v3") editorContext.engine.addDefaultAssetSources(baseUri = baseUri) val defaultTypeface = TypefaceProvider().provideTypeface(editorContext.engine, "Roboto") requireNotNull(defaultTypeface) editorContext.engine.asset.addSource(TextAssetSource(editorContext.engine, defaultTypeface)) } launch { editorContext.engine.addDemoAssetSources( sceneMode = editorContext.engine.scene.getMode(), withUploadAssetSources = true, baseUri = Uri.parse("https://cdn.img.ly/assets/demo/v2"), ) } coroutineContext[Job]?.invokeOnCompletion { editorContext.eventHandler.send(HideLoading) } }}, ``` * `onExport` - the callback that is invoked when the export button is tapped. You may want to call one of the [export functions](android/export-save-publish/export/overview-9ed3a8/) in this callback. The default implementations call `BlockApi.export` or `BlockApi.exportVideo`, write the content into a temporary file and open a system dialog for sharing the exported file. Note that the “export” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background ``` onExport = { EditorDefaults.run { editorContext.eventHandler.send(ShowLoading) val blob = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) { scene.getPages().forEach { block.setScopeEnabled(it, key = "layer/visibility", enabled = true) block.setVisible(it, visible = true) } } val tempFile = writeToTempFile(blob) editorContext.eventHandler.send(HideLoading) editorContext.eventHandler.send(ShareFileEvent(tempFile, MimeType.PDF.key)) }}, ``` * `onUpload` - the callback that is invoked after an asset is added to `UploadAssetSourceType`. When selecting an asset to upload, a default `AssetDefinition` object is constructed based on the selected asset and the callback is invoked. By default, the callback leaves the asset definition unmodified and returns the same object. However, you may want to upload the selected asset to your server before adding it to the scene. This example demonstrates how you can access the URI of the new asset, use it to upload the file to your server, and then replace the URI with the URI of your server. Note that the “upload” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. ``` onUpload = { assetDefinition, _ -> val meta = assetDefinition.meta ?: return@remember assetDefinition val sourceUri = Uri.parse(meta["uri"]) val uploadedUri = sourceUri // todo upload the asset here and return remote uri val newMeta = meta + listOf( "uri" to uploadedUri.toString(), "thumbUri" to uploadedUri.toString(), ) assetDefinition.copy(meta = newMeta)}, ``` * `onClose` - the callback that is invoked after a tap on the navigation icon of the toolbar or on the system back button. The callback receives a boolean parameter that indicates whether editor has unsaved changes. Default implementation sends `ShowCloseConfirmationDialogEvent` event in case that parameter is `true` and closes the editor if it is `false`. Note that the “close” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. ``` onClose = { hasUnsavedChanges -> if (hasUnsavedChanges) { editorContext.eventHandler.send(ShowCloseConfirmationDialogEvent) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) }}, ``` * `onError` - the callback that is invoked after the editor captures an error. Default implementation sends `ShowErrorDialogEvent` event which displays a popup dialog with action button that closes the editor. Note that the “error” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. ``` onError = { error -> editorContext.eventHandler.send(ShowErrorDialogEvent(error))}, ``` ### Full Code Here’s the full code for configuring events: ``` import android.net.Uriimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport kotlinx.coroutines.Jobimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport ly.img.editor.DesignEditorimport ly.img.editor.EditorDefaultsimport ly.img.editor.EngineConfigurationimport ly.img.editor.HideLoadingimport ly.img.editor.ShareFileEventimport ly.img.editor.ShowCloseConfirmationDialogEventimport ly.img.editor.ShowErrorDialogEventimport ly.img.editor.ShowLoadingimport ly.img.editor.core.event.EditorEventimport ly.img.editor.core.library.data.TextAssetSourceimport ly.img.editor.core.library.data.TypefaceProviderimport ly.img.engine.MimeTypeimport ly.img.engine.addDefaultAssetSourcesimport ly.img.engine.addDemoAssetSources // Add this composable to your NavHost@Composablefun CallbacksEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { // Note that lambda is copied from EditorDefaults.onCreate coroutineScope { // In case of process recovery, engine automatically recovers the scene that is why we need to check if (editorContext.engine.scene.get() == null) { editorContext.engine.scene.load(EngineConfiguration.defaultDesignSceneUri) } launch { val baseUri = Uri.parse("https://cdn.img.ly/assets/v3") editorContext.engine.addDefaultAssetSources(baseUri = baseUri) val defaultTypeface = TypefaceProvider().provideTypeface(editorContext.engine, "Roboto") requireNotNull(defaultTypeface) editorContext.engine.asset.addSource(TextAssetSource(editorContext.engine, defaultTypeface)) } launch { editorContext.engine.addDemoAssetSources( sceneMode = editorContext.engine.scene.getMode(), withUploadAssetSources = true, baseUri = Uri.parse("https://cdn.img.ly/assets/demo/v2"), ) } coroutineContext[Job]?.invokeOnCompletion { editorContext.eventHandler.send(HideLoading) } } }, onExport = { EditorDefaults.run { editorContext.eventHandler.send(ShowLoading) val blob = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) { scene.getPages().forEach { block.setScopeEnabled(it, key = "layer/visibility", enabled = true) block.setVisible(it, visible = true) } } val tempFile = writeToTempFile(blob) editorContext.eventHandler.send(HideLoading) editorContext.eventHandler.send(ShareFileEvent(tempFile, MimeType.PDF.key)) } }, onUpload = { assetDefinition, _ -> val meta = assetDefinition.meta ?: return@remember assetDefinition val sourceUri = Uri.parse(meta["uri"]) val uploadedUri = sourceUri // todo upload the asset here and return remote uri val newMeta = meta + listOf( "uri" to uploadedUri.toString(), "thumbUri" to uploadedUri.toString(), ) assetDefinition.copy(meta = newMeta) }, onClose = { hasUnsavedChanges -> if (hasUnsavedChanges) { editorContext.eventHandler.send(ShowCloseConfirmationDialogEvent) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } }, onError = { error -> editorContext.eventHandler.send(ShowErrorDialogEvent(error)) }, ) DesignEditor(engineConfiguration = engineConfiguration) { // You can set result here navController.popBackStack() }} ``` ## Updating the UI When working with the callbacks, you may want to make UI updates before, during, or after the callback execution. This is when UI events come to help. All the callbacks receive an extra parameter EditorEventHandler, which can be used to send editor events. By default, there are existing ui events, which can be found in Events.kt file (i.e. ShowLoading, HideLoading etc). ``` onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) editorContext.eventHandler.send(OnCreateCustomEvent) }, ``` You may want to declare your own custom event. Simply inherit from class EditorEvent. ``` data object OnCreateCustomEvent : EditorEvent ``` After declaring the event, you can send the UI event using send function. Note that EditorEventHandler has another function called sendCloseEditorEvent, which can be used to forcefully close the mobile editor. ``` editorContext.eventHandler.send(OnCreateCustomEvent) ``` Once the event is sent, it can be captured in EditorConfiguration.onEvent. The lambda contains three parameters: activity, state and of course, the captured event. In this example, the editor state is of default type EditorUiState (initially provided in initialState), however, you can have your own state class that wraps the EditorUiState or does not contain EditorUiState at all. The only requirement is that it should be Parcelable. The lambda should return the updated state, which, if changed, will trigger overlay composable to be recomposed and the overlaying ui components will be updated. ``` onEvent = { state, event -> when (event) { OnCreateCustomEvent -> { Toast.makeText(editorContext.activity, "Editor is created!", Toast.LENGTH_SHORT).show() state } ShowLoading -> { state.copy(showLoading = true) } else -> { // handle other default events EditorDefaults.onEvent(editorContext.activity, state, event) } } }, ``` To handle your brand new custom event, simply check the type of the event and handle it per your needs. ``` OnCreateCustomEvent -> { Toast.makeText(editorContext.activity, "Editor is created!", Toast.LENGTH_SHORT).show() state } ``` Besides, you can override the behavior of existing events too. Simply extend your when block and override the behavior. ``` ShowLoading -> { state.copy(showLoading = true) } ``` If you want to leave the behavior of remaining default events unchanged, simply return the result of EditorDefaults.onEvent in the else block. ``` else -> { // handle other default events EditorDefaults.onEvent(editorContext.activity, state, event) } ``` ### Full Code Here’s the full code for making UI updates before, during, or after the callback execution ``` import android.widget.Toastimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EditorDefaultsimport ly.img.editor.EditorUiStateimport ly.img.editor.EngineConfigurationimport ly.img.editor.ShowLoadingimport ly.img.editor.core.event.EditorEvent data object OnCreateCustomEvent : EditorEvent // Add this composable to your NavHost@Composablefun UiEventsEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) editorContext.eventHandler.send(OnCreateCustomEvent) }, ) val editorConfiguration = EditorConfiguration.remember( initialState = EditorUiState(), onEvent = { state, event -> when (event) { OnCreateCustomEvent -> { Toast.makeText(editorContext.activity, "Editor is created!", Toast.LENGTH_SHORT).show() state } ShowLoading -> { state.copy(showLoading = true) } else -> { // handle other default events EditorDefaults.onEvent(editorContext.activity, state, event) } } }, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` --- [Source](https:/img.ly/docs/cesdk/android/user-interface/customization-72b2f8) # Customization --- [Source](https:/img.ly/docs/cesdk/android/user-interface/appearance-b155eb) # Appearance --- [Source](https:/img.ly/docs/cesdk/android/use-templates/overview-ae74e1) # Overview Templates in CreativeEditor SDK (CE.SDK) are pre-designed layouts that serve as starting points for generating static designs, videos, or print-ready outputs. Templates can be used to produce a wide range of media, including images, PDFs, and videos. Instead of creating a design from scratch, you can use a template to quickly produce content by adapting pre-defined elements like text, images, and layout structures. Using templates offers significant advantages: faster content creation, consistent visual style, and scalable design workflows across many outputs. CE.SDK supports two modes of using templates: * **Fully Programmatic**: Generate content variations automatically by merging external data into templates without user intervention. * **User-Assisted**: Let users load a template, customize editable elements, and export the result manually. Template-based generation can be performed entirely on the client, entirely on a server, or in a hybrid setup where users interact with templates client-side before triggering automated server-side generation. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Ways to Use Templates ### Programmatic Use You can automate the generation of final assets by merging data into templates entirely through code. This is ideal for workflows like mass personalization, product variations, or dynamic marketing content, where the design layout stays consistent, but the content (e.g., names, images, pricing) varies. ### User-Assisted Use Alternatively, templates can be used as editable starting points for users. CE.SDK allows you to define which parts of a template are editable and which are locked. This ensures that users can customize elements such as text or images while maintaining brand or design consistency where needed. This hybrid approach is useful for cases where users need some creative freedom but within structured design constraints. ## Working with Dynamic Content CE.SDK templates support dynamic placeholders for content such as text, images, and custom elements. You can replace dynamic content in several ways: * **Through the UI**: Users can manually edit placeholders directly on the canvas or through a form-based editing interface that lists all editable fields for faster and more structured updates. * **Programmatically**: External data (e.g., from a database, CSV file, or API) can be merged automatically into placeholders without requiring user interaction. This flexibility supports both one-off customizations and large-scale content generation scenarios. ## Exploring the Template Library Templates are stored, organized, and managed through the CE.SDK Template Library. This library acts as a centralized place where templates are categorized and retrieved either programmatically or through the UI. You can work with: * **Default Templates**: Templates that come bundled with the SDK to help you get started quickly. * **Custom Templates**: Templates you create based on your specific use case, offering full flexibility over design, dynamic fields, and editing constraints. * **Premium Templates**: Additional high-quality templates provided by IMG.LY, which can be integrated into your application if licensed. ## Selecting Templates Users can browse and select templates through the Asset Library, which you can customize to match your application’s design and user experience. You have full control over: * The appearance and layout of the template picker. * The filters and categories shown to users. * Whether to display only a subset of templates based on user roles or project types. ## Switching Between Templates Users can switch templates during the editing process. When a new template is applied: * Existing content may be preserved, repositioned, or reset depending on how the templates are configured. * The editor can guide users through the transition to avoid accidental loss of work. You can control how strict or flexible this behavior is, depending on your application’s needs. ## Applying Templates to Existing Scenes It’s possible to apply a template to an existing scene. In these cases: * The template structure can be merged with the current content. * Alternatively, the scene can be reset and rebuilt based on the new template, depending on the chosen integration approach. This enables workflows like refreshing an old design with a new branded layout without starting over. ## Output Formats When Using Templates When generating outputs from templates, CE.SDK supports: Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. Templates are format-aware, allowing you to design once and export to multiple formats seamlessly. For example, a single marketing template could be used to produce a social media graphic, a printable flyer, and a promotional video, all using the same underlying design. --- [Source](https:/img.ly/docs/cesdk/android/use-templates/apply-template-35c73e) # Apply a Template ``` val rawResId = R.raw.cesdk_postcard_1val rawResourcePath = context.resources.run { ContentResolver.SCHEME_ANDROID_RESOURCE + "://"+ getResourcePackageName(rawResId) + "/"" + getResourceTypeName(rawResId) + "/" + getResourceEntryName(rawResId)} engine.scene.applyTemplate(template = "UBQ1ewoiZm9ybWF0Ij...")engine.scene.applyTemplate(templateUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene"))engine.scene.applyTemplate(templateUri = Uri.parse("file:///android_asset/templates/cesdk_postcard_1.scene"))engine.scene.applyTemplate(templateUri = Uri.fromFile(File(filesDir, "templates/cesdk_postcard_1.scene")))engine.scene.applyTemplate(templateUri = Uri.parse(rawResourcePath))engine.scene.get() ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to apply the contents of a given template scene to the currently loaded scene through the `scene` API. ## Applying Template Scenes ``` suspend fun applyTemplate(template: String) ``` Applies the contents of the given template scene to the currently loaded scene. This loads the template scene while keeping the design unit and page dimensions of the current scene. The content of the pages is automatically adjusted to fit the new dimensions. * `template`: the template scene file contents, a base64 string. ``` suspend fun applyTemplate(templateUri: Uri) ``` Applies the contents of the given template scene to the currently loaded scene. This loads the template scene while keeping the design unit and page dimensions of the current scene. The content of the pages is automatically adjusted to fit the new dimensions. * `templateUri`: the resource of the template scene file. ``` fun get(): DesignBlock? ``` Return the currently active scene. * Returns the scene or null, if none was created yet. ## Full Code Here’s the full code: ``` val rawResId = R.raw.cesdk_postcard_1val rawResourcePath = context.resources.run { ContentResolver.SCHEME_ANDROID_RESOURCE + "://"+ getResourcePackageName(rawResId) + "/"" + getResourceTypeName(rawResId) + "/" + getResourceEntryName(rawResId)} engine.scene.applyTemplate(template = "UBQ1ewoiZm9ybWF0Ij...")engine.scene.applyTemplate(templateUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene"))engine.scene.applyTemplate(templateUri = Uri.parse("file:///android_asset/templates/cesdk_postcard_1.scene"))engine.scene.applyTemplate(templateUri = Uri.fromFile(File(filesDir, "templates/cesdk_postcard_1.scene")))engine.scene.applyTemplate(templateUri = Uri.parse(rawResourcePath))engine.scene.get() ``` --- [Source](https:/img.ly/docs/cesdk/android/text/styling-269c48) # Text Styling ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.AnimationTypeimport ly.img.engine.Colorimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.Fontimport ly.img.engine.FontStyleimport ly.img.engine.FontWeightimport ly.img.engine.SizeModeimport ly.img.engine.TextCaseimport ly.img.engine.Typeface fun textProperties( 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.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "Hello World") // Add a "!" at the end of the text engine.block.replaceText(text, text = "!", from = 11) // Replace "World" with "Alex" engine.block.replaceText(text, text = "Alex", from = 6, to = 11) engine.scene.zoomToBlock( block = text, paddingLeft = 100F, paddingTop = 100F, paddingRight = 100F, paddingBottom = 100F, ) // Remove the "Hello " engine.block.removeText(text, from = 0, to = 6) engine.block.setTextColor(text, color = Color.fromHex("#FFFF0000")) engine.block.setTextColor(text, color = Color.fromHex("#FFFF0000"), from = 1, to = 4) val allColors = engine.block.getTextColors(text) val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5) engine.block.setBoolean(text, property = "backgroundColor/enabled", value = true) val color = engine.block.getColor(text, property = "backgroundColor/color") engine.block.setColor(text, property = "backgroundColor/color", value = Color.fromRGBA(r = 0, g = 0, b = 1, a = 1)) engine.block.setFloat(text, property = "backgroundColor/paddingLeft", value = 1.0F) engine.block.setFloat(text, property = "backgroundColor/paddingTop", value = 2.0F) engine.block.setFloat(text, property = "backgroundColor/paddingRight", value = 3.0F) engine.block.setFloat(text, property = "backgroundColor/paddingBottom", value = 4.0F) engine.block.setFloat(text, property = "backgroundColor/cornerRadius", value = 4.0F) val animation = engine.block.createAnimation(AnimationType.Slide) engine.block.setEnum(animation, property = "textAnimationWritingStyle", value = "Block") engine.block.setInAnimation(text, animation = animation) engine.block.setOutAnimation(text, animation = animation) engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) val textCases = engine.block.getTextCases(text) val typeface = Typeface( name = "Roboto", fonts = listOf( Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily = "Italic", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ) engine.block.setFont(text, typeface.fonts[3].uri, typeface) engine.block.setTypeface(text, typeface, from = 1, to = 4) engine.block.setTypeface(text, typeface) val currentDefaultTypeface = engine.block.getTypeface(text) val currentTypefaces = engine.block.getTypefaces(text) val currentTypefacesOfRange = engine.block.getTypefaces(text, from = 1, to = 4) if (engine.block.canToggleBoldFont(text)) { engine.block.toggleBoldFont(text) } if (engine.block.canToggleBoldFont(text, from = 1, to = 4)) { engine.block.toggleBoldFont(text, from = 1, to = 4) } if (engine.block.canToggleItalicFont(text)) { engine.block.toggleItalicFont(text) } if (engine.block.canToggleItalicFont(text, from = 1, to = 4)) { engine.block.toggleItalicFont(text, from = 1, to = 4) } val fontWeights = engine.block.getTextFontWeights(text) engine.block.setTextFontStyle(text, FontStyle.NORMAL) val fontStyles = engine.block.getTextFontStyles(text) engine.stop()} ``` In this example, we want to show how to read and modify the text block’s contents via the API in the offscreen Engine. ## Editing the Text String You can edit the text string contents of a text block using the `fun replaceText(block: DesignBlock, text: String, from: Int = -1, to: Int = -1)` and `fun removeText(block: DesignBlock, from: Int = -1, to: Int = -1)` APIs. The range of text that should be edited is defined using the UTF-16 indices \[from, to). When omitting both the `from` and `to` arguments, the entire existing string is replaced. ``` engine.block.replaceText(text, text = "Hello World") ``` When only specifying the `from` index, the new text is inserted at this index. ``` // Add a "!" at the end of the textengine.block.replaceText(text, text = "!", from = 11) ``` When both `from` and `to` indices are specified, then that range of text is replaced with the new text. ``` // Replace "World" with "Alex"engine.block.replaceText(text, text = "Alex", from = 6, to = 11) ``` Similarly, the `removeText` API can be called to remove either a specific range or the entire text. ``` // Remove the "Hello "engine.block.removeText(text, from = 0, to = 6) ``` ## Text Colors Text blocks in the Engine allow different ranges to have multiple colors. Use the `fun setTextColor(block: DesignBlock, color: Color, from: Int = -1, to: Int = -1)` API to change either the color of the entire text ``` engine.block.setTextColor(text, color = Color.fromHex("#FFFF0000")) ``` or only that of a range. After these two calls, the text “Alex!” now starts with one yellow character, followed by three black characters and two more yellow ones. ``` engine.block.setTextColor(text, color = Color.fromHex("#FFFF0000"), from = 1, to = 4) ``` The `fun getTextColors(block: DesignBlock, from: Int = -1, to: Int = -1): List` API returns an ordered list of unique colors in the requested range. Here, `allColors` will be a list containing the colors yellow and black (in this order). ``` val allColors = engine.block.getTextColors(text) ``` When only the colors in the UTF-16 range from 2 to 5 are requested, the result will be an array containing black and then yellow, since black appears first in the requested range. ``` val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5) ``` ## Text Background You can create and edit the background of a text block by setting specific block properties. To add a colored background to a text block use the `fun setBoolean(block: DesignBlock, property: String, value: Boolean))` API and enable the `backgroundColor/enabled` property. ``` engine.block.setBoolean(text, property = "backgroundColor/enabled", value = true) ``` The color of the text background can be queried (by making use of the `fun setColor(block: DesignBlock, property: String, value: Color)` API ) and also changed (with the `fun setColor(block: DesignBlock, property: String, value: Color)` API). ``` val color = engine.block.getColor(text, property = "backgroundColor/color") ``` The padding of the rectangular background shape can be edited by using the `fun setFloat(block: DesignBlock, property: String, value: Float)` API and setting the target value for the desired padding property like: * `backgroundColor/paddingLeft`: * `backgroundColor/paddingRight`: * `backgroundColor/paddingTop`: * `backgroundColor/paddingBottom`: ``` engine.block.setFloat(text, property = "backgroundColor/paddingLeft", value = 1.0F) ``` Additionally, the rectangular shape of the background can be rounded by setting a corner radius with the `fun setFloat(block: DesignBlock, property: String, value: Float)` API to adjust the value of the `backgroundColor/cornerRadius` property. ``` engine.block.setFloat(text, property = "backgroundColor/cornerRadius", value = 4.0F) ``` Text backgrounds inherit the animations assigned to their respective text block when the animation text writing style is set to `Block`. ``` val animation = engine.block.createAnimation(AnimationType.Slide) ``` ## Text Case You can apply text case modifications to ranges of text in order to display them in upper case, lower case or title case. It is important to note that these modifiers do not change the `text` string value of the text block but are only applied when the block is rendered. By default, the text case of all text within a text block is set to `TextCase.NORMAL`, which does not modify the appearance of the text at all. The `fun setTextCase(block: DesignBlock, textCase: TextCase, from: Int = -1, to: Int = -1)` API sets the given text case for the selected range of text. Possible values for `TextCase` are: * `NORMAL`: The text string is rendered without modifications. * `UPPER_CASE`: All characters are rendered in upper case. * `LOWER_CASE`: All characters are rendered in lower case. * `TITLE_CASE`: The first character of each word is rendered in upper case. ``` engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) ``` The `fun getTextCases(block: DesignBlock, from: Int = -1, to: Int = -1): List` API returns the ordered list of text cases of the text in the selected range. ``` val textCases = engine.block.getTextCases(text) ``` ## Typefaces In order to change the font of a text block, you have to call the `fun setFont(block: DesignBlock, fontFileUri: Uri, typeface: Typeface)` API and provide it with both the uri of the font file to be actively used and the complete typeface definition of the corresponding typeface. Existing formatting of the block is reset. A typeface definition consists of the unique typeface name (as it is defined within the font files), and a list of all font definitions that belong to this typeface. Each font definition must provide a `Uri` which points to the font file and a `subFamily` string which is this font’s effective name within its typeface. The subfamily value is typically also defined within the font file. The `weight` and `style` properties default to `NORMAL`, but must be provided in other cases. For the sake of this example, we define a `Roboto` typeface with only four fonts: `Regular`, `Bold`, `Italic`, and `Bold Italic` and we change the font of the text block to the Roboto Regular font. ``` val typeface = Typeface( name = "Roboto", fonts = listOf( Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily = "Italic", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ),)engine.block.setFont(text, typeface.fonts[3].uri, typeface) ``` If the formatting, e.g., bold or italic, of the text should be kept, you have to call the `fun setTypeface(block: DesignBlock, fontFileUri: Uri, typeface: Typeface)` API and provide it with both the uri of the font file to be used and the complete typeface definition of the corresponding typeface. The font file should be a fallback font, e.g., `Regular`, from the same typeface. The actual font that matches the formatting is chosen automatically with the current formatting retained as much as possible. If the new typeface does not support the current formatting, the formatting changes to a reasonable close one, e.g. thin might change to light, bold to normal, and/or italic to non-italic. If no reasonable font can be found, the fallback font is used. ``` engine.block.setTypeface(text, typeface, from = 1, to = 4)engine.block.setTypeface(text, typeface) ``` You can query the currently used typeface definition of a text block by calling the `fun getTypeface(block: DesignBlock): Typeface` API. It is important to note that new text blocks don’t have any explicit typeface set until you call the `setFont` API. In this case, the `getTypeface` API will throw an error. ``` val currentDefaultTypeface = engine.block.getTypeface(text) ``` ## Font Weights and Styles Text blocks can also have multiple ranges with different weights and styles. In order to toggle the text of a text block between the normal and bold font weights, first call the `fun canToggleBoldFont(block: DesignBlock, from: Int = -1, to: Int = -1): Boolean` API to check whether such an edit is possible and if so, call the `fun toggleBoldFont(block: DesignBlock, from: Int = -1, to: Int = -1)` API to change the weight. ``` if (engine.block.canToggleBoldFont(text)) { engine.block.toggleBoldFont(text)}if (engine.block.canToggleBoldFont(text, from = 1, to = 4)) { engine.block.toggleBoldFont(text, from = 1, to = 4)} ``` In order to toggle the text of a text block between the normal and italic font styles, first call the `fun canToggleItalicFont(block: DesignBlock, from: Int = -1, to: Int = -1): Boolean` API to check whether such an edit is possible and if so, call the `fun toggleItalicFont(block: DesignBlock, from: Int = -1, to: Int = -1)` API to change the style. ``` if (engine.block.canToggleItalicFont(text)) { engine.block.toggleItalicFont(text)}if (engine.block.canToggleItalicFont(text, from = 1, to = 4)) { engine.block.toggleItalicFont(text, from = 1, to = 4)} ``` In order to change the font weight or style, the typeface definition of the text block must include a font definition that corresponds to the requested font weight and style combination. For example, if the text block currently uses a bold font and you want to toggle the font style to italic - such as in the example code - the typeface must contain a font that is both bold and italic. The `fun getTextFontWeights(block: DesignBlock, from: Int = -1, to: Int = -1): List` API returns an ordered list of unique font weights in the requested range, similar to the `getTextColors` API described above. For this example text, the result will be `listOf(FontWeight.BOLD)`. ``` val fontWeights = engine.block.getTextFontWeights(text) ``` The fun `setTextFontStyle(block: DesignBlock, fontStyle: FontStyle, from: Int = -1, to: Int = -1)` API sets a font style in the requested range. ``` engine.block.setTextFontStyle(text, FontStyle.NORMAL) ``` The `fun getTextFontStyles(block: DesignBlock, from: Int = -1, to: Int = -1): List` API returns an ordered list of unique font styles in the requested range, similar to the `getTextColors` API described above. For this example text, the result will be `listOf(FontWeight.ITALIC)`. ``` val fontStyles = engine.block.getTextFontStyles(text) ``` ## Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.*import ly.img.engine.* fun textProperties( license: String, userId: String,) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 100, height = 100) val scene = engine.scene.create() val text = engine.block.create(DesignBlockType.Text) engine.block.appendChild(parent = scene, child = text) engine.block.setWidthMode(text, mode = SizeMode.AUTO) engine.block.setHeightMode(text, mode = SizeMode.AUTO) engine.block.replaceText(text, text = "Hello World") // Add a "!" at the end of the text engine.block.replaceText(text, text = "!", from = 11) // Replace "World" with "Alex" engine.block.replaceText(text, text = "Alex", from = 6, to = 11) engine.scene.zoomToBlock( block = text, paddingLeft = 100F, paddingTop = 100F, paddingRight = 100F, paddingBottom = 100F, ) // Remove the "Hello " engine.block.removeText(text, from = 0, to = 6) engine.block.setTextColor(text, color = Color.fromHex("#FFFF0000")) engine.block.setTextColor(text, color = Color.fromHex("#FFFF0000"), from = 1, to = 4) val allColors = engine.block.getTextColors(text) val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5) engine.block.setBoolean(text, property = "backgroundColor/enabled", value = true) val color = engine.block.getColor(text, property = "backgroundColor/color") engine.block.setColor(text, property = "backgroundColor/color", value = Color.fromRGBA(r = 0, g = 0, b = 1, a = 1)) engine.block.setFloat(text, property = "backgroundColor/paddingLeft", value = 1.0F) engine.block.setFloat(text, property = "backgroundColor/paddingTop", value = 2.0F) engine.block.setFloat(text, property = "backgroundColor/paddingRight", value = 3.0F) engine.block.setFloat(text, property = "backgroundColor/paddingBottom", value = 4.0F) engine.block.setFloat(text, property = "backgroundColor/cornerRadius", value = 4.0F) val animation = engine.block.createAnimation(AnimationType.Slide) engine.block.setEnum(animation, property = "textAnimationWritingStyle", value = "Block") engine.block.setInAnimation(text, animation = animation) engine.block.setOutAnimation(text, animation = animation) engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE) val textCases = engine.block.getTextCases(text) val typeface = Typeface( name = "Roboto", fonts = listOf( Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily = "Bold", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily = "Bold Italic", weight = FontWeight.BOLD, style = FontStyle.ITALIC, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily = "Italic", weight = FontWeight.BOLD, style = FontStyle.NORMAL, ), Font( uri = Uri.parse("https://cdn.img.ly/assets/v2/ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily = "Regular", weight = FontWeight.NORMAL, style = FontStyle.NORMAL, ), ), ) engine.block.setFont(text, typeface.fonts[3].uri, typeface) engine.block.setTypeface(text, typeface, from = 1, to = 4) engine.block.setTypeface(text, typeface) val currentDefaultTypeface = engine.block.getTypeface(text) val currentTypefaces = engine.block.getTypefaces(text) val currentTypefacesOfRange = engine.block.getTypefaces(text, from = 1, to = 4) if (engine.block.canToggleBoldFont(text)) { engine.block.toggleBoldFont(text) } if (engine.block.canToggleBoldFont(text, from = 1, to = 4)) { engine.block.toggleBoldFont(text, from = 1, to = 4) } if (engine.block.canToggleItalicFont(text)) { engine.block.toggleItalicFont(text) } if (engine.block.canToggleItalicFont(text, from = 1, to = 4)) { engine.block.toggleItalicFont(text, from = 1, to = 4) } val fontWeights = engine.block.getTextFontWeights(text) engine.block.setTextFontStyle(text, FontStyle.NORMAL) val fontStyles = engine.block.getTextFontStyles(text) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/text/overview-0bd620) # Overview In CreativeEditor SDK (CE.SDK), a _text element_ is an editable, stylable block that you can add to your design. Whether you’re creating marketing graphics, videos, social media posts, or multilingual layouts, text plays a vital role in conveying information and enhancing your visuals. You can fully manipulate text elements using both the user interface and programmatic APIs, giving you maximum flexibility to control how text behaves and appears. Additionally, text can be animated to bring motion to your designs. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Text Capabilities in CE.SDK CE.SDK offers robust text editing capabilities designed for both designers and developers: * **Adding and editing text objects:** Easily insert text into your designs through the UI or dynamically create and update text blocks via the API. * **Resizing and positioning:** Move, align, and resize text objects freely on the canvas to achieve the perfect layout. * **Dynamic editing:** Programmatically update text content, style, or position based on data or user input, enabling powerful automation workflows. * **Inline Editing:** Directly see changes in the context of your design instead of a separate input. ## Text Styling, Formatting, Effects, and Spacing CE.SDK provides a comprehensive set of text styling and formatting options: * **Basic styling options:** * Choose from a range of fonts. * Adjust font size, color, and opacity. * Apply text formatting such as bold, italic, and underline. * **Advanced effects:** * Add shadows for depth. * Outline text for emphasis. * Apply glow effects to make text stand out. * **Spacing controls:** * Fine-tune letter spacing (tracking). * Adjust line height for readability. * Set paragraph spacing for structured layouts. You can apply all styling, formatting, effects, and spacing adjustments both manually through the UI and programmatically via the API, offering flexibility for user-driven design or fully automated generation. ## Text Containers and Auto-Sizing Text in CE.SDK can be configured in frames that adapt to different content scenarios: * **Fixed-size frame:** Keep text confined within a defined width and height. * **Growing frames:** Allow the text block to expand automatically as more text is added in specific directions. Auto-sizing behaviors can be customized to ensure your text either stays within design constraints or flows naturally as content changes. ## Emojis in Text CE.SDK fully supports inserting emojis alongside regular text: * **Emoji insertion:** Add emojis directly within text blocks, just like any other character. * **Styling:** Emojis inherit the styling of surrounding text, ensuring visual consistency. * **Format support:** Unicode-based emojis are supported with robust fallback behavior for consistent display across platforms. * **Customizable:** The font used for drawing Emojis can be configured. ## Fonts, Language, and Script Support Text handling in CE.SDK is built to support a wide range of languages, scripts, and font management needs: * **Custom fonts:** Upload and manage your own font collections to match brand guidelines. * **Font restrictions:** Control which fonts are available to end users for a curated design experience. * **Multi-script handling:** Seamlessly mix different scripts (e.g., Latin, Cyrillic, Kanji) within the same design. * **Fallbacks:** Handles an endless amount of characters by providing a robust fallback system that ensures your text is always readable. --- [Source](https:/img.ly/docs/cesdk/android/text/edit-c5106b) # Edit Text ``` val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, "Hello World")engine.block.removeText(text, from = 0, to = 6)engine.block.setTextColor(text, Color.fromHex("#FFFF0000"), from = 1, to = 4)val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5)engine.block.setTextFontWeight(text, fontWeight = FontWeight.BOLD, from = 0, to = 5)val fontWeights = engine.block.getTextFontWeights(text)val fontWeights = engine.block.setTextFontSize(text, fontSize: 14, from = 2, to = 5)val fontWeights = engine.block.getTextFontSizes(text)engine.block.setTextFontStyle(text, fontStyle = FontStyle.ITALIC, from = 0, to = 5)val fontStyles = engine.block.getTextFontStyles(text)engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE)val textCases = engine.block.getTextCases()val canToggleBoldFont = engine.block.canToggleBoldFont(text)val canToggleItalicFont = engine.block.canToggleItalicFont(text)engine.block.toggleBoldFont(text)engine.block.toggleItalicFont(text)val typefaceAssetResults = engine.asset.findAssets( sourceId = "ly.img.typeface", query = FindAssetsQuery( query = "Open Sans", page = 0, perPage = 100 ))val typeface = typefaceAssetResults.assets[0].payload.typefaceval font = typeface.fonts.first { font -> font.subFamily == "Bold" }engine.block.setFont(text, font.uri, typeface)engine.block.setTypeface(text, typeface, from = 0, to = 5)engine.block.setTypeface(text, typeface)val defaultTypeface = engine.block.getTypeface(text)val typefaces = engine.block.getTypefaces(text)val range = engine.block.getTextCursorRange()val lineCount = engine.block.getTextVisibleLineCount(text)val lineBoundingBox = engine.block.getTextLineBoundingBoxRect(text, 0) ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to edit ranges within text blocks. A list of all available settings can be found above. ``` fun replaceText( block: DesignBlock, text: String, from: Int = -1, to: Int = -1,) ``` Inserts the given text into the selected range of the text block. Required scope: “text/edit” * `block`: the text block into which to insert the given text. * `text`: the text which should replace the selected range in the block. * `from`: the start index of the range that should be replaced. If the value is negative, this will fall back to the start of the entire text range. * `to`: the index after the last character that should be replaced by the inserted text. If the value is negative, this will fall back to the end of the entire text range. ``` fun removeText( block: DesignBlock, from: Int = -1, to: Int = -1,) ``` Removes selected range of text of the given text block. Required scope: “text/edit” * `block`: the text block from which the selected text should be removed. * `from`: the start index of the range that should be removed. If the value is negative, this will fall back to the start of the entire text range. * `to`: the index after the last character that should be removed. If the value is negative, this will fall back to the end of the entire text range. ``` fun setTextColor( block: DesignBlock, color: Color, from: Int = -1, to: Int = -1,) ``` Changes the color of the text in the selected range to the given color. Required scope: “fill/change” * `block`: the text block whose color should be changed. * `color`: the new color of the selected text range. * `from`: the start index of the range whose color should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character whose color should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTextColors( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the ordered unique list of colors of the text in the selected range. * `block`: the text block whose colors should be returned. * `from`: the start index of the range whose colors should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose colors should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the ordered unique list of colors of the text in the selected range. ``` fun setTextFontWeight( block: DesignBlock, fontWeight: FontWeight, from: Int = -1, to: Int = -1,) ``` Changes the weight of the text in the selected range to the given weight. Required scope: “text/character” * `block`: the text block whose weight should be changed. * `fontWeight`: the new weight of the selected text range. * `from`: the start index of the UTF-16 range whose weight should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the UTF-16 index after the last grapheme whose weight should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTextFontWeights( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the ordered unique list of font weights of the text in the selected range. * `block`: the text block whose font weights should be returned. * `from`: the start index of the range whose font weights should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose font weights should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the ordered unique list of font weights of the text in the selected range. ``` fun setTextFontSize( block: DesignBlock, fontSize: Float, from: Int = -1, to: Int = -1,) ``` Changes the size of the text in the selected range to the given size. If the font size is applied to the entire text block, its font size property will be updated. Required scope: “text/character” * `block`: the text block whose size should be changed. * `fontStyle`: the new size of the selected text range. * `from`: the start index of the UTF-16 range whose size should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the UTF-16 index after the last grapheme whose size should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTextFontSizes( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the ordered unique list of font sizes of the text in the selected range. * `block`: the text block whose font sizes should be returned. * `from`: the start index of the range whose font sizes should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose font sizes should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the ordered unique list of font sizes of the text in the selected range. ``` fun setTextFontStyle( block: DesignBlock, fontStyle: FontStyle, from: Int = -1, to: Int = -1,) ``` Changes the style of the text in the selected range to the given style. Required scope: “text/character” * `block`: the text block whose style should be changed. * `fontStyle`: the new style of the selected text range. * `from`: the start index of the UTF-16 range whose style should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the UTF-16 index after the last grapheme whose style should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTextFontStyles( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the ordered unique list of font styles of the text in the selected range. * `block`: the text block whose font styles should be returned. * `from`: the start index of the range whose font styles should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose font styles should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the ordered unique list of font styles of the text in the selected range. ``` fun setTextFontStyle( block: DesignBlock, fontStyle: FontStyle, from: Int = -1, to: Int = -1,) ``` Changes the style of the text in the selected range to the given style. Required scope: “text/character” * `block`: the text block whose style should be changed. * `fontStyle`: the new style of the selected text range. * `from`: the start index of the UTF-16 range whose style should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the UTF-16 index after the last grapheme whose style should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected grapheme range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTextFontStyles( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the ordered unique list of font styles of the text in the selected range. * `block`: the text block whose font styles should be returned. * `from`: the start index of the range whose font styles should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose font styles should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the ordered unique list of font styles of the text in the selected range. ``` fun setTextCase( block: DesignBlock, textCase: TextCase, from: Int = -1, to: Int = -1,) ``` Sets the given text case for the selected range of text. Required scope: “text/character” * `block`: the text block whose text case should be changed. * `textCase`: the new text case value. * `from`: the start index of the range whose text case should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character whose text case should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTextCases( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the ordered list of text cases of the text in the selected range. * `block`: the text block whose text cases should be returned. * `from`: the start index of the range whose text cases should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose text cases should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the ordered list of text cases of the text in the selected range. ``` fun canToggleBoldFont( block: DesignBlock, from: Int = -1, to: Int = -1,): Boolean ``` Returns whether the font weight of the given block can be toggled between bold and normal. * `block`: the text block block whose font weight should be toggled. * `from`: the start index of the range whose whose font weight should be toggled. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character whose whose font weight should be toggled. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun canToggleItalicFont( block: DesignBlock, from: Int = -1, to: Int = -1,): Boolean ``` Returns whether the font style of the given block can be toggled between italic and normal. * `block`: the text block block whose font style should be toggled. * `from`: the start index of the range whose text case should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character whose text case should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun toggleBoldFont( block: DesignBlock, from: Int = -1, to: Int = -1,) ``` Toggles the font weight of the given block between bold and normal. Required scope: “text/character” * `block`: the text block whose font weight should be toggled. * `from`: the start index of the range whose font weight should be toggled. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character whose font weight should be toggled. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun toggleItalicFont( block: DesignBlock, from: Int = -1, to: Int = -1,) ``` Toggles the font style of the given block between italic and normal. Required scope: “text/character” * `block`: the text block whose font style should be toggled. * `from`: the start index of the range whose text case should be changed. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character whose text case should be changed. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun setFont( block: DesignBlock, fontFileUri: Uri, typeface: Typeface,) ``` Sets the given font and typeface for the text block. Existing formatting is reset. Required scope: “text/character” * `block`: the text block whose font should be changed. * `fontFileUri`: the Uri of the new font file. * `typeface`: the typeface of the new font. ``` fun setTypeface( block: DesignBlock, typeface: Typeface, from: Int = -1, to: Int = -1,) ``` Sets the given typeface for the text block. The current formatting, e.g., bold or italic, is retained as far as possible. Some formatting might change if the new typeface does not support it, e.g. thin might change to light, bold to normal, and/or italic to non-italic. If the typeface is applied to the entire text block, its typeface property will be updated. If a run does not support the new typeface, it will fall back to the default typeface from the typeface property. Required scope: “text/character” * `block`: the text block whose font should be changed. * `typeface`: the new typeface. * `from`: the start index of the range whose typeface should be set. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose typeface should be set. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. ``` fun getTypeface(block: DesignBlock): Typeface ``` Returns the typeface property of the text block. Does not return the typefaces of the text runs. * `block`: the text block whose typeface should be returned. * Returns the typeface of the text block or throws exception if the typeface is unknown. ``` fun getTypefaces( block: DesignBlock, from: Int = -1, to: Int = -1,): List ``` Returns the typefaces of the text block. * `block`: the text block whose typefaces should be returned. * `from`: the start index of the range whose typefaces should be returned. If the value is negative and the block is currently being edited, this will fall back to the start of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the start of the entire text range. * `to`: the index after the last character of the range whose typefaces should be returned. If the value is negative and the block is currently being edited, this will fall back to the end of the current cursor index / selected range. If the value is negative and the block is not being edited, this will fall back to the end of the entire text range. * Returns the typefaces of the text block or throws exception if the typeface is unknown. ``` fun getTextCursorRange(): IntRange? ``` Returns the indices of the selected range of the text block that is currently being edited. If both the start and end index of the returned range have the same value, then the text cursor is positioned at that index. * Returns the selected grapheme range or null if no text block is currently being edited. ``` fun getTextVisibleLineCount(block: DesignBlock): Int ``` Returns the number of visible lines in the given text block. * `block`: the text block whose line count should be returned. * Returns the number of lines of text in the block. ``` fun getTextLineBoundingBoxRect( block: DesignBlock, index: Int,): RectF ``` Returns the bounds of the visible area of the given line of the text block. The values are in the scene’s global coordinate space (which has its origin at the top left). * `block`: the text block whose line bounding box should be returned. * `index`: the index of the line whose bounding box should be returned. * Returns the bounding box of the line. ## Full Code Here’s the full code: ``` val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, "Hello World")engine.block.removeText(text, from = 0, to = 6)engine.block.setTextColor(text, Color.fromHex("#FFFF0000"), from = 1, to = 4)val colorsInRange = engine.block.getTextColors(text, from = 2, to = 5)engine.block.setTextFontWeight(text, fontWeight = FontWeight.BOLD, from = 0, to = 5)val fontWeights = engine.block.getTextFontWeights(text)val fontWeights = engine.block.setTextFontSize(text, fontSize: 14, from = 2, to = 5)val fontWeights = engine.block.getTextFontSizes(text)engine.block.setTextFontStyle(text, fontStyle = FontStyle.ITALIC, from = 0, to = 5)val fontStyles = engine.block.getTextFontStyles(text)engine.block.setTextCase(text, textCase = TextCase.TITLE_CASE)val textCases = engine.block.getTextCases()val canToggleBoldFont = engine.block.canToggleBoldFont(text)val canToggleItalicFont = engine.block.canToggleItalicFont(text)engine.block.toggleBoldFont(text)engine.block.toggleItalicFont(text)val typefaceAssetResults = engine.asset.findAssets( sourceId = "ly.img.typeface", query = FindAssetsQuery( query = "Open Sans", page = 0, perPage = 100 ))val typeface = typefaceAssetResults.assets[0].payload.typefaceval font = typeface.fonts.first { font -> font.subFamily == "Bold" }engine.block.setFont(text, font.uri, typeface)engine.block.setTypeface(text, typeface, from = 0, to = 5)engine.block.setTypeface(text, typeface)val defaultTypeface = engine.block.getTypeface(text)val typefaces = engine.block.getTypefaces(text)val range = engine.block.getTextCursorRange()val lineCount = engine.block.getTextVisibleLineCount(text)val lineBoundingBox = engine.block.getTextLineBoundingBoxRect(text, 0) ``` --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes/overview-ebd6eb) # Overview CreativeEditor SDK (CE.SDK) lets you enrich your designs with **stickers** and **shapes**—two flexible building blocks for visual creativity. * **Stickers** are multi-color, multi-path graphics typically used for icons, emojis, logos, and decorative elements. Stickers cannot be recolored once imported. * **Shapes** are customizable graphical elements—such as rectangles, circles, and polygons—that you can freely recolor and adjust. You can create, edit, and combine stickers and shapes either through the CE.SDK user interface or programmatically using the API. Both elements play essential roles in designing layouts, enhancing branding, and creating marketing graphics. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Stickers vs. Shapes: Key Differences Category Stickers Shapes **Color Editing** Not recolorable after import Fully recolorable **SVG Support** Multi-color, multi-path SVGs supported Single-color, single-path SVGs only **Canvas Behavior** Movable, resizable, rotatable Movable, resizable, rotatable, recolorable **Use Cases** Icons, brand logos, emoji, detailed graphics Graphical elements, backgrounds, visual framing When deciding between the two: * Use a **sticker** when you need detailed, multi-colored artwork that remains fixed in appearance. * Use a **shape** when you want design elements you can recolor, style, and adapt dynamically. ## Built-In Stickers and Shapes CE.SDK offers a library of default assets to help you get started quickly. * **Default Stickers**: Common categories such as emojis, badges, symbols, and decorative graphics. * **Default Shapes**: Predefined types including rectangles, circles, polygons, stars, and custom paths. These assets can be inserted directly through the UI or dynamically via the API. ## Custom Stickers and Custom Shapes You can also import your own vector sticker and shape assets into CE.SDK: * **Custom Stickers**: * Supported file type: SVG. * Stickers can contain multiple paths and colors. * Once imported, stickers behave like regular sticker assets but cannot be recolored. * **Custom Shapes**: * Supported file type: SVG. * Must consist of a **single path** and **single color**. * Shapes can be recolored, resized, and customized after import. Be mindful of these structural differences when preparing your SVG files. ## Editing Stickers and Shapes Once added to a scene, both stickers and shapes are fully interactive on the canvas. * **Editable properties for shapes**: * Fill color * Stroke color and width * Border radius (for certain shapes like rectangles) * Opacity * Rotation * Scaling * **Editable properties for stickers**: * Size * Rotation * Opacity * **Note:** Stickers cannot have their colors changed after import. Both types of elements can be manipulated manually via the UI or programmatically through the CE.SDK API. ## Combining Stickers and Shapes CE.SDK allows you to combine multiple stickers or shapes using boolean operations to create new, complex designs. * **Combine**: Merges all selected elements into a single new shape. * **Subtract**: Removes the top element’s area from the bottom element. * **Intersect**: Keeps only the overlapping area of all selected elements. * **Exclude**: Keeps only the non-overlapping parts, removing the intersections. These operations allow you to: * Create custom compound shapes. * Subtract logos or icons from backgrounds. * Design intricate, multi-part layouts from basic elements. After combining, the resulting object behaves like a standard shape or sticker and can be resized, rotated, or styled according to its type. ## Working with Cutouts **Cutouts**—also called **cutout lines**—define the outline path around an object, often used for printing applications like custom stickers or die-cut designs. In CE.SDK: * You can create cutouts around shapes or stickers. * Cutouts help in generating accurate printable paths or previewing how a sticker would be physically cut. * Cutouts are set in a specific spot color to control advanced print & cutting workflows. Cutouts are an essential tool when preparing designs for production or physical printing workflows. --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes/create-edit-7a2b5a) # Create and Edit --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes/create-cutout-384be3) # Create Cutout ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Colorimport ly.img.engine.CutoutOperationimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engine fun cutouts( 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.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 circle = engine.block.createCutoutFromPath( "M 0,25 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0 Z", ) engine.block.setFloat(circle, "cutout/offset", 3F) engine.block.setEnum(circle, "cutout/type", "Dashed") val square = engine.block.createCutoutFromPath("M 0,0 H 50 V 50 H 0 Z") engine.block.setFloat(square, "cutout/offset", 6F) val union = engine.block.createCutoutFromOperation( listOf(circle, square), op = CutoutOperation.UNION, ) engine.block.destroy(circle) engine.block.destroy(square) engine.editor.setSpotColor("CutContour", Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F)) engine.stop()} ``` Cutouts are a special feature one can use with cuttings printers. When printing a PDF file containing cutouts paths, a cutting printer will cut these paths with a cutter rather than print them with ink. Use cutouts to create stickers, iron on decals, etc. Cutouts can be created from an SVG string describing its underlying shape. Cutouts can also be created from combining multiple existing cutouts using the boolean operations `UNION`, `DIFFERENCE`, `INTERSECTION` and `XOR`. Cutouts have a type property which can take one of two values: `SOLID` and `DASHED`. Cutting printers recognize cutouts paths through their specially named spot colors. By default, `SOLID` cutouts have the spot color `"CutContour"` to produce a continuous cutting line and `DASHED` cutouts have the spot colors `"PerfCutContour"` to produce a perforated cutting line. You may need to adjust these spot color names for you printer. **Note** Note that the actual color approximation given to the spot color does not affect how the cutting printer interprets the cutout, only how it is rendered. The default color approximations are magenta for “CutContour” and green for “PerfCutContour”. Cutouts have an offset property that determines the distance at which the cutout path is rendered from the underlying path set when created. ## Setup the scene We first create a new scene with a new page. ``` 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) ``` ## Create cutouts Here we add two cutouts. First, a circle of type `DASHED` and with an offset of 3.0. Second, a square of default type `SOLID` and an offset of 6.0. ## Combining multiple cutouts into one Here we use the `UNION` operation to create a new cutout that consists of the combination of the earlier two cutouts we have created. Note that we destroy the previously created `circle` and `square` cutouts as we don’t need them anymore and we certainly don’t want to printer to cut through those paths as well. When combining multiple cutouts, the resulting cutout will be of the type of the first cutout given and an offset of 0. In this example, since the `circle` cutout is of type `DASHED`, the newly created cutout will also be of type `DASHED`. **Warning** When using the Difference operation, the first cutout is the cutout that is subtracted _from_. For other operations, the order of the cutouts don’t matter. ``` val union = engine.block.createCutoutFromOperation( listOf(circle, square), op = CutoutOperation.UNION,)engine.block.destroy(circle)engine.block.destroy(square) ``` ## Change the default color for Solid cutouts For some reason, we’d like the cutouts of type `SOLID` to not render as magenta but as blue. Knowing that `"CutContour"` is the spot color associated with `SOLID`, we change it RGB approximation to blue. Thought the cutout will render as blue, the printer will still interpret this path as a cutting because of its special spot color name. ``` engine.editor.setSpotColor("CutContour", Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F)) ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.*import ly.img.engine.* fun cutouts( 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) val circle = engine.block.createCutoutFromPath( "M 0,25 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0 Z", ) engine.block.setFloat(circle, "cutout/offset", 3F) engine.block.setEnum(circle, "cutout/type", CutoutType.DASHED.key) val square = engine.block.createCutoutFromPath("M 0,0 H 50 V 50 H 0 Z") engine.block.setFloat(square, "cutout/offset", 6F) val union = engine.block.createCutoutFromOperation( listOf(circle, square), op = CutoutOperation.UNION, ) engine.block.destroy(circle) engine.block.destroy(square) engine.editor.setSpotColor("CutContour", Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F)) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes/combine-2a9e26) # Combine ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.BooleanOperationimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun usingBoolOps( 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.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) val circle1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle1, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle1, fill = engine.block.createFill(FillType.Color)) engine.block.setPositionX(circle1, value = 30F) engine.block.setPositionY(circle1, value = 30F) engine.block.setWidth(circle1, value = 40F) engine.block.setHeight(circle1, value = 40F) engine.block.appendChild(parent = page, child = circle1) val circle2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle2, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle2, fill = engine.block.createFill(FillType.Color)) engine.block.setPositionX(circle2, value = 80F) engine.block.setPositionY(circle2, value = 30F) engine.block.setWidth(circle2, value = 40F) engine.block.setHeight(circle2, value = 40F) engine.block.appendChild(parent = page, child = circle2) val circle3 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle3, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle3, fill = engine.block.createFill(FillType.Color)) engine.block.setPositionX(circle3, value = 50F) engine.block.setPositionY(circle3, value = 50F) engine.block.setWidth(circle3, value = 50F) engine.block.setHeight(circle3, value = 50F) engine.block.appendChild(parent = page, child = circle3) engine.block.combine(listOf(circle1, circle2, circle3), op = BooleanOperation.UNION) val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, "Removed text") engine.block.setPositionX(text, value = 10F) engine.block.setPositionY(text, value = 40F) engine.block.setWidth(text, value = 80F) engine.block.setHeight(text, value = 10F) engine.block.appendChild(parent = page, child = text) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setFill(block = block, fill = imageFill) engine.block.setPositionX(block, value = 0F) engine.block.setPositionY(block, value = 0F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.appendChild(parent = page, child = block) engine.block.sendToBack(block) engine.block.forceLoadResources(listOf(block)) val difference = engine.block.combine(listOf(block, text), op = BooleanOperation.DIFFERENCE) engine.stop()} ``` You can use four different boolean operations on blocks to combine them into unique shapes. These operations are: * `'Union'`: adds all the blocks’ shapes into one * `'Difference'`: removes from the bottom-most block the shapes of the other blocks overlapping with it * `'Intersection'`: keeps only the overlapping parts of all the blocks’ shapes * `'XOR'`: removes the overlapping parts of all the block’s shapes Combining blocks allows you to create a new block with a customized shape. Combining blocks with the `union`, `intersection` or `XOR` operation will result in the new block whose fill is that of the top-most block and whose shape is the result of applying the operation pair-wise on blocks from the top-most block to the bottom-most block. Combining blocks with the `difference` operation will result in the new block whose fill is that of the bottom-most block and whose shape is the result of applying the operation pair-wise on blocks from the bottom-most block to the top-most block. The combined blocks will be destroyed. **Only the following blocks can be combined** * A graphics block * A text block ``` fun isCombinable(blocks: List): Boolean ``` Checks whether blocks could be combined. Only graphics blocks and text blocks can be combined. All blocks must have the “lifecycle/duplicate” scope enabled. * `blocks`: blocks for which the confirm combinability. * Returns whether the blocks can be combined or an error. ``` fun combine( blocks: List, op: BooleanOperation,): DesignBlock ``` Perform a boolean operation on the given blocks. All blocks must be combinable. See `isCombinable`. The parent, fill and sort order of the new block is that of the prioritized block. When performing a `Union`, `Intersection` or `XOR`, the operation is performed pair-wise starting with the element with the highest sort order. When performing a `Difference`, the operation is performed pair-wise starting with the element with the lowest sort order. Required scopes: “lifecycle/duplicate”, “lifecycle/destroy” * `blocks`: blocks to combine. They will be destroyed if “lifecycle/destroy” scope is enabled. * `op`: boolean operation to perform. * Returns the newly created block or an error. Here’s the full code: ``` // Create blocks and append to sceneval star = engine.block.create(DesignBlockType.STAR_SHAPE)val rect = engine.block.create(DesignBlockType.RECT_SHAPE)engine.block.appendChild(scene, child = star)engine.block.appendChild(scene, child = rect) // Check whether the blocks may be combinedif (engine.block.isCombinable(listOf(star, rect))) { val combined = engine.block.combine(listOf(star, rect), op = BooleanOperation.UNION)} ``` ## Combining three circles together We create three circles and arrange in a recognizable pattern. Combing them with `'Union'` result in a single block with a unique shape. The result will inherit the top-most block’s fill, in this case `circle3`’s fill. ``` val circle1 = engine.block.create(DesignBlockType.Graphic) ``` To create a special effect of text punched out from an image, we create an image and a text. We ensure that the image is at the bottom as that is the base block from which we want to remove shapes. The result will be a block with the size, shape and fill of the image but with a hole in it in the shape of the removed text. ``` val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, "Removed text") engine.block.setPositionX(text, value = 10F) engine.block.setPositionY(text, value = 40F) engine.block.setWidth(text, value = 80F) engine.block.setHeight(text, value = 10F) engine.block.appendChild(parent = page, child = text) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setFill(block = block, fill = imageFill) engine.block.setPositionX(block, value = 0F) engine.block.setPositionY(block, value = 0F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.appendChild(parent = page, child = block) engine.block.sendToBack(block) engine.block.forceLoadResources(listOf(block)) val difference = engine.block.combine(listOf(block, text), op = BooleanOperation.DIFFERENCE) ``` ### Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.BooleanOperationimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun usingBoolOps( 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) val circle1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle1, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle1, fill = engine.block.createFill(FillType.Color)) engine.block.setPositionX(circle1, value = 30F) engine.block.setPositionY(circle1, value = 30F) engine.block.setWidth(circle1, value = 40F) engine.block.setHeight(circle1, value = 40F) engine.block.appendChild(parent = page, child = circle1) val circle2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle2, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle2, fill = engine.block.createFill(FillType.Color)) engine.block.setPositionX(circle2, value = 80F) engine.block.setPositionY(circle2, value = 30F) engine.block.setWidth(circle2, value = 40F) engine.block.setHeight(circle2, value = 40F) engine.block.appendChild(parent = page, child = circle2) val circle3 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(circle3, shape = engine.block.createShape(ShapeType.Ellipse)) engine.block.setFill(circle3, fill = engine.block.createFill(FillType.Color)) engine.block.setPositionX(circle3, value = 50F) engine.block.setPositionY(circle3, value = 50F) engine.block.setWidth(circle3, value = 50F) engine.block.setHeight(circle3, value = 50F) engine.block.appendChild(parent = page, child = circle3) engine.block.combine(listOf(circle1, circle2, circle3), op = BooleanOperation.UNION) val text = engine.block.create(DesignBlockType.Text) engine.block.replaceText(text, "Removed text") engine.block.setPositionX(text, value = 10F) engine.block.setPositionY(text, value = 40F) engine.block.setWidth(text, value = 80F) engine.block.setHeight(text, value = 10F) engine.block.appendChild(parent = page, child = text) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setFill(block = block, fill = imageFill) engine.block.setPositionX(block, value = 0F) engine.block.setPositionY(block, value = 0F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg", ) engine.block.appendChild(parent = page, child = block) engine.block.sendToBack(block) engine.block.forceLoadResources(listOf(block)) val difference = engine.block.combine(listOf(block, text), op = BooleanOperation.DIFFERENCE) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/text/emojis-510651) # Emojis ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engine fun textWithEmojis( 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 uri = engine.editor.getSettingString(keypath = "ubq://defaultEmojiFontFileUri") // From a bundle engine.editor.setSettingString( keypath = "ubq://defaultEmojiFontFileUri", value = "file:///android_asset/ly.img.cesdk/fonts/NotoColorEmoji.ttf", ) // From a URL engine.editor.setSettingString( keypath = "ubq://defaultEmojiFontFileUri", value = "https://cdn.img.ly/assets/v3/emoji/NotoColorEmoji.ttf", ) 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 text = engine.block.create(DesignBlockType.Text) engine.block.setString(text, property = "text/text", value = "Text with an emoji 🧐") engine.block.setWidth(text, value = 50F) engine.block.setHeight(text, value = 10F) engine.block.appendChild(parent = page, child = text) engine.stop()} ``` Text blocks in CE.SDK support the use of emojis. A default emoji font is used to render these independently from the target platform. This guide shows how to change the default font and use emojis in text blocks. ``` 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, ) ``` ## Change the Default Emoji Font The default font URI can be changed when another emoji font should be used or when the font should be served from another website, a content delivery network (CDN), or a local file path. The preset is to use the [NotoColorEmoji](https://github.com/googlefonts/noto-emoji) font loaded from our [CDN](https://cdn.img.ly/assets/v3/emoji/NotoColorEmoji.ttf). This font file supports a wide variety of Emojis and is licensed under the [Open Font License](https://cdn.img.ly/assets/v3/emoji/LICENSE.txt). The file is relatively small with 9.9 MB but has the emojis stored as PNG images. As an alternative for higher quality emojis, e.g., this [NotoColorEmoji](https://fonts.google.com/noto/specimen/Noto+Color+Emoji) font can be used. This font file supports also a wide variety of Emojis and is licensed under the [SIL Open Font License, Version 1.1](https://fonts.google.com/noto/specimen/Noto+Color+Emoji/license). The file is significantly larger with 24.3 MB but has the emojis stored as vector graphics. In order to change the emoji font URI, call the `fun setSettingString(keypath: string, value: string)` [Editor API](android/settings-970c98/) with ‘defaultEmojiFontFileUri’ as keypath and the new URI as value. ``` val uri = engine.editor.getSettingString(keypath = "ubq://defaultEmojiFontFileUri")// From a bundleengine.editor.setSettingString( keypath = "ubq://defaultEmojiFontFileUri", value = "file:///android_asset/ly.img.cesdk/fonts/NotoColorEmoji.ttf",)// From a URLengine.editor.setSettingString( keypath = "ubq://defaultEmojiFontFileUri", value = "https://cdn.img.ly/assets/v3/emoji/NotoColorEmoji.ttf",) ``` ## Add a Text Block with an Emoji To add a text block with an emoji, add a text block and set the emoji as text content. ``` val text = engine.block.create(DesignBlockType.Text)engine.block.setString(text, property = "text/text", value = "Text with an emoji 🧐")engine.block.setWidth(text, value = 50F)engine.block.setHeight(text, value = 10F)engine.block.appendChild(parent = page, child = text) ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.*import ly.img.engine.* fun textWithEmojis( 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 uri = engine.editor.getSettingString(keypath = "ubq://defaultEmojiFontFileUri") // From a bundle engine.editor.setSettingString( keypath = "ubq://defaultEmojiFontFileUri", value = "file:///android_asset/ly.img.cesdk/fonts/NotoColorEmoji.ttf", ) // From a URL engine.editor.setSettingString( keypath = "ubq://defaultEmojiFontFileUri", value = "https://cdn.img.ly/assets/v3/emoji/NotoColorEmoji.ttf", ) 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 text = engine.block.create(DesignBlockType.Text) engine.block.setString(text, property = "text/text", value = "Text with an emoji 🧐") engine.block.setWidth(text, value = 50F) engine.block.setHeight(text, value = 10F) engine.block.appendChild(parent = page, child = text) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/rules/overview-e27832) # Overview In CreativeEditor SDK (CE.SDK), _rules_—referred to as **scopes** in the API and code—are automated validations that help enforce design and layout standards during editing. You can use scopes to maintain brand guidelines, ensure print readiness, moderate content, and enhance the overall editing experience. Scopes can be applied to both designs and videos, helping you deliver high-quality outputs while reducing the risk of common mistakes. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Understanding Rules in CE.SDK CE.SDK’s Rules system (scopes in code) can enable you to build validation logic to enforce a wide range of design and content requirements automatically. Here are some examples of what can be built on top of the SDK: * **Design constraints:** * Minimum image resolution * No elements placed outside visible canvas bounds * **Brand guidelines:** * Approved fonts, colors, and logo usage * **Print-readiness:** * Safe zones for trimming * Bleed margin requirements * High-resolution assets for print * **Content moderation:** * Flagging potentially inappropriate images or text * **UI-specific constraints:** * Safe zones to prevent critical content from being obscured by app UI elements * **Video editing rules:** * Enforcing required trimming * Protecting key content regions ## How Rules Work Validation with scopes happens continuously and automatically in the background as users edit their designs or videos. When a rule (scope) violation occurs, the SDK immediately notifies the user without requiring manual checks. If the user corrects an issue (for example, by replacing a low-resolution image), the related notification clears automatically without needing a manual refresh or revalidation step. This dynamic, always-on validation keeps the editing workflow smooth and responsive. ## Automatic Notifications When a scope is violated, CE.SDK surfaces a notification directly in the user interface. Notifications provide clear feedback and guide users to correct issues easily. * **Severity indicators:** * **Critical errors** are shown in red (e.g., content outside the printable area) * **Warnings** are shown in yellow (e.g., low-resolution image detected) * **Actionable feedback:** * Clicking a notification brings the user directly to the affected element. * Users can quickly resolve issues without needing technical expertise. This system simplifies workflows, especially for users who are not familiar with technical print requirements or detailed layout standards. ## Benefits of Using Rules Integrating scopes into your CE.SDK experience offers multiple advantages: * **Reduce errors:** Minimize risks of misprints, layout problems, and brand inconsistency. * **Automatic compliance:** Validate designs and videos in real time without disrupting the creative process. * **Professional results:** Help users — including those without technical or print knowledge — achieve polished, production-ready outputs. * **Streamlined workflows:** Speed up editing and reduce manual review by surfacing issues early and clearly. By leveraging scopes, you can ensure a higher standard of quality, consistency, and reliability across all designs created with CE.SDK. --- [Source](https:/img.ly/docs/cesdk/android/prebuilt-solutions/video-editor-9e533a) # Android Video Editor SDK The CreativeEditor SDK offers a comprehensive and versatile solution for video editing on Android devices. The CE.SDK video editor enables developers to integrate powerful video editing capabilities into their Android applications, providing users with an intuitive and fully customizable editing experience. Whether you’re building an app for social media, content creation, or any other platform that requires robust video editing tools, the CE.SDK Android Video Editor is designed to meet your needs. [Launch Web Demo](https://img.ly/showcases/cesdk/video-ui/android) [Get Started](android/get-started/overview-e18f40/) ## Key Capabilities of the Android Mobile Video Editor SDK ![Multi-Format Support](/docs/cesdk/_astro/Transform.By5kJRew_2acCrV.webp) ### Multi-Format Support Create videos in various formats, including story reels and Ultra HD, tailored for different channels like Instagram, TikTok, or custom formats. ![Templating](/docs/cesdk/_astro/Templating.eMNm9_jD_ycnVt.webp) ### Templating Jumpstart your users designs with easily adaptable templates including text variables and placeholders. ![Asset Management](/docs/cesdk/_astro/AssetLibraries.Ce9MfYvX_HmsaC.webp) ### Asset Management Record, upload, or select pre-existing videos, images, and other media from a custom library to enrich video content. ![Advanced Editing Tools](/docs/cesdk/_astro/Empty.BC0Yy-JZ_ZE3B5R.webp) ### Advanced Editing Tools Utilize features such as adjustments, filters, effects, and blur to fine-tune each element or the entire video, delivering a professional finish. ![Timeline Management](/docs/cesdk/_astro/Timeline.DdbXIpJt_ZhJ8vc.webp) ### Timeline Management Arrange multiple video clips, images, text, stickers, and shapes on a timeline for precise control over the final output. ![Audio Integration](/docs/cesdk/_astro/Audio.DQACp-LE_ZxUTVz.webp) ### Audio Integration Enhance videos with audio tracks, either imported or selected from a custom asset library, to add another layer of creativity. ![Customizable UI](/docs/cesdk/_astro/CustomizableUI.DtHv9rY-_2fNrB2.webp) ### Customizable UI Tailor the video editing interface to match your application’s branding and user experience needs, ensuring an intuitive and engaging experience. ## What is the Video Editor Solution? The Video Editor is a prebuilt solution powered by the CreativeEditor SDK (CE.SDK) that enables fast integration of high-performance video editing into web, mobile, and desktop applications. It’s designed to help your users create professional-grade videos—from short social clips to long-form stories—directly within your app. Skip building a video editor from scratch. This fully client-side solution provides a solid foundation with an extensible UI and a robust engine API to power video editing in any use case. ## Supported Platforms The Android Mobile Video Editor SDK is compatible with Android applications developed using Kotlin or Java, offering full support for both languages. ## Prerequisites Ensure that the **IMGLYUI Android Library** is added to your project. The SDK requires a valid license key, and optionally, unique user IDs for accurate user tracking. ## Supported Media Types [IMG.LY](http://img.ly/)’s Creative Editor SDK enables you to load, edit, and save **MP4 files** directly on the device without server dependencies. ### Importing Media Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. ### Exporting Media Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ### Importing Templates Format Description `.idml` InDesign `.psd` Photoshop `.scene` CE.SDK Native Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs to generate scenes programmatically. For detailed information, see the [full file format support list](android/file-format-support-3c4b2a/). ## Understanding CE.SDK Architecture & API The following sections provide an overview of the key components of the CE.SDK video editor UI and its API architecture. If you’re ready to start integrating CE.SDK into your Android application, check out our [Implementation Guide](android/prebuilt-solutions/video-editor-9e533a/). ### CreativeEditor SDK Mobile Video UI The CE.SDK video editor UI is a specific configuration of the CreativeEditor SDK, focusing on essential video editing features. It includes robust tools for video manipulation, customizable to suit different use cases. Key components include the **Canvas**, the main workspace where users interact with their video content. The **Timeline** provides control over the sequence and duration of video clips, images, and audio tracks. The **Tool Bar** provides essential editing options like adjustments, filters, effects, layer management or adding text or images in order of relevance. Lastly, the **Context Menu** presents relevant editing options for each selected element, simplifying the editing process for users. Learn more about interacting with and customizing the video editor UI in our design editor UI guide. ### CreativeEngine At the core of CE.SDK is the CreativeEngine, which handles all rendering and video manipulation tasks. It can be used in headless mode or alongside the CreativeEditor UI. Key features and APIs provided by CreativeEngine include functionalities for [**Scene Management:**](android/open-the-editor/overview-99444b/) to create, load, save, and manipulate video scenes programmatically. [**Block Management:**](android/concepts/blocks-90241e/) to manage video clips, images, text, and other elements within the timeline. [**Asset Management:**](android/import-media/concepts-5e6197/) to integrate and manage video, audio, and image assets from various sources. [**Variable Management:**](android/create-templates/add-dynamic-content/text-variables-7ecb50/) to define and manipulate variables for dynamic content within video scenes and [**Event Handling:**](android/concepts/events-353f97/) to subscribe to events like clip selection changes or timeline updates for dynamic interaction. ## Customizing the Android Video Editor CE.SDK provides extensive customization options, allowing you to tailor the UI and functionality to meet your specific needs. This can range from basic configuration settings to more advanced customizations involving callbacks and custom elements. ### Basic Customizations * [**Configuration Object:**](android/user-interface/customization-72b2f8/) Customize the editor’s appearance and functionality by passing a configuration object during initialization. * [**Custom Asset Sources:**](android/import-media/asset-panel/customize-c9a4de/) Serve custom video clips or audio tracks from a remote URL. See the [video editor getting started page](android/prebuilt-solutions/video-editor-9e533a/) for more configuration examples. ### UI Customization Options The editor’s appearance can be customized with options like choosing between ‘dark’ or ‘light’ themes. You can also configure the editor color palette to match a particular CI. [\- **Hook into UI Events:**](android/user-interface/events-514b70/) Register event listener for UI events, for example, this would allow you to display a loading spinner while the editor is being initialized. ## Framework Support CreativeEditor SDK’s video editor is compatible with Swift and Objective-C, making it easy to integrate into any Android application. ## Ready to get started? With a free trial and pricing that fits your needs, it's easy to find the best solution for your product. [Free Trial](https://img.ly/forms/free-trial) ### 500M+ video and photo creations are powered by IMG.LY every month ![HP logo](/docs/cesdk/_astro/HP.BZ1qDNii_ZpK5Lk.webp) ![Shopify logo](/docs/cesdk/_astro/Shopify.Dmyk4png_ZRKWXF.webp) ![Reuters logo](/docs/cesdk/_astro/Reuters.B8BV2Fek_ZLrHFJ.webp) ![Hootsuite logo](/docs/cesdk/_astro/Hootsuite.C94d5fhs_Zsc4gx.webp) ![Semrush logo](/docs/cesdk/_astro/Semrush.B2YsPaIn_23cDNx.webp) ![Shutterfly logo](/docs/cesdk/_astro/Shutterfly.Cc7Sw48y_Z3TjCs.webp) ![Sprout Social logo](/docs/cesdk/_astro/Sprout-Social.VxlY6_Tc_E0Dzh.webp) ![One.com logo](/docs/cesdk/_astro/Onecom.BQ_oPnlz_Z1btrtu.webp) ![Constant Contact logo](/docs/cesdk/_astro/Constant-Contact.1rh975Q__Z2ob7wU.webp) --- [Source](https:/img.ly/docs/cesdk/android/prebuilt-solutions/t-shirt-designer-02b48f) # T-Shirt Designer in Android Quickly add a professional-grade t-shirt design editor to your Android app with CE.SDK. [ Launch Web Demo ](https://img.ly/showcases/cesdk/apparel-editor-ui/)[ View on GitHub ](https://github.com/imgly/cesdk-android-examples/tree/main/editor-guides-solutions-apparel-editor) ## What is the T-Shirt Designer Solution? The T-Shirt Designer is a pre-configured instance of the CreativeEditor SDK (CE.SDK) tailored for apparel design workflows. It enables your users to create high-quality, print-ready t-shirt designs directly in your app—whether for a custom merchandise platform, print-on-demand storefront, or internal design tool. This solution comes with a realistic t-shirt mockup background, precise boundary enforcement, and a simplified UI. You can easily integrate it across web, mobile, or desktop platforms and customize it to match your brand or workflow. ## Key Features * **T-Shirt backdrop with placement guidance** A visually accurate t-shirt background helps users place artwork exactly where it will appear when printed. * **Strict print area enforcement** Elements that extend beyond the defined print region are clipped automatically, ensuring print precision. * **Placeholder-based template editing** Supports templates with editable placeholders, such as swappable images. Define which parts of a design are user-editable by toggling between Creator and Adopter modes. * **Print-ready PDF export** Outputs print-quality PDFs to seamlessly fit into your existing production workflows. * **Fully customizable UI** Adapt the interface and features to suit your brand and user needs using the CE.SDK configuration API. ## Why Use This Solution? * **Accelerated development** Save engineering time with a ready-made editor specifically configured for t-shirt design. * **Better user experience** The focused UI reduces complexity, guiding users through apparel creation with built-in visual feedback and safeguards. * **Seamless print integration** The export format and boundary enforcement make it ideal for print-on-demand systems with no additional post-processing required. * **Flexible and extensible** As with all CE.SDK solutions, the T-Shirt Designer is deeply customizable—extend features, change design constraints, or integrate external data and asset libraries. --- [Source](https:/img.ly/docs/cesdk/android/prebuilt-solutions/postcard-editor-61e1f6) # Android Postcard Editor SDK The Postcard Editor is a prebuilt CreativeEditor SDK (CE.SDK) solution designed for quickly creating and personalizing postcards and greeting cards. It provides an intuitive UI that guides users through selecting a design, editing its contents, and customizing messaging—all without needing design expertise. This ready-to-use editor can be easily added to your Android app and fully customized to match your brand, making it ideal for direct mail campaigns, seasonal greetings, or personalized customer engagement. [ Launch Web Demo ](https://img.ly/showcases/cesdk/post-greeting-cards/)[ View on GitHub ](https://github.com/imgly/cesdk-web-examples/tree/main/showcase-post-greeting-cards) ## What is the Postcard Editor Solution? The Postcard Editor is a prebuilt CreativeEditor SDK (CE.SDK) solution designed for quickly creating and personalizing postcards and greeting cards. It provides an intuitive UI that guides users through selecting a design, editing its contents, and customizing messaging—all without needing design expertise. With built-in support for style presets, design constraints, and variable-driven personalization, the Postcard Editor enables scalable creation of high-quality, print-ready content for direct mail, seasonal greetings, and personalized campaigns. ## Key Features * **Style presets** Jump-start the design process with a collection of professionally crafted postcard templates. * **Design mode** After selecting a style, users can personalize the design. Depending on the template configuration, they can: * Change accent and background colors * Replace photos from a library or upload their own * Edit headings and body text (fonts, colors, layout) * Add stickers, shapes, or other decorative elements * **Write mode** Users can add a personal message and address the card. Text styling options include font, size, and color customization. * **Dynamic variables** Enable scalable personalization using variables like `{{firstname}}` or `{{address}}`. Templates can be connected to external data sources for automated batch generation. * **Print-ready export** Designs are exported in high-resolution, print-friendly formats, suitable for direct mail or digital delivery. ## Why Use This Solution? * **Accelerate development** Save time with a pre-configured UI that’s production-ready and easily customizable via CE.SDK’s headless API. * **Empower non-designers** Make creative tools accessible to any user by enforcing design constraints and simplifying the editing experience. * **Scale personalization** Integrate with external data to programmatically generate personalized postcards for marketing, e-commerce, or events. * **Cross-platform ready** The Postcard Editor works across web, mobile, and desktop environments, offering a seamless user experience wherever it’s deployed. --- [Source](https:/img.ly/docs/cesdk/android/prebuilt-solutions/photo-editor-42ccb2) # Android Photo Editor SDK The CreativeEditor SDK provides a robust and user-friendly solution for photo editing on Android devices. The photo UI is a specific configuration of the CE.SDK UI which enables developers to seamlessly integrate essential photo editing features into their Android applications, offering users a powerful yet intuitive editing experience. Whether you are developing an app for social media, content creation, or any other platform requiring photo editing tools, the CE.SDK Android Photo Editor is designed to meet your needs. [Launch Web Demo](https://img.ly/showcases/cesdk/?tags=android) [Get Started](android/get-started/overview-e18f40/) ## Key Capabilities of the iOS Mobile Design Editor SDK ![Transforms](/docs/cesdk/_astro/Transform.By5kJRew_2acCrV.webp) ### Transforms Includes straightening, scaling, rotation, and flip functions. ![Advanced Adjustment Tools](/docs/cesdk/_astro/Templating.eMNm9_jD_ycnVt.webp) ### Advanced Adjustment Tools Includes brightness, saturation, contrast, gamma, clarity, exposure, shadows, highlights, and more. ![Filters](/docs/cesdk/_astro/Filters.D0Iue_r-_Z1VcFlR.webp) ### Filters Provide a wide range of built-in filters and effects or upload your own custom filters. ![Effects & Blur](/docs/cesdk/_astro/Effects.sSLfFAca_d6Uaj.webp) ### Effects & Blur Unique effects such as pixelize, glitch, or mirror. ![Text Editing](/docs/cesdk/_astro/TextEditing.B8Ra1KOE_2lGC8C.webp) ### Text Editing Add and style text blocks with various fonts, colors, and effects. ![Asset Libraries](/docs/cesdk/_astro/AssetLibraries.Ce9MfYvX_HmsaC.webp) ### Asset Libraries Add custom assets for stickers, filters, and shapes. ![Client-Side Processing](/docs/cesdk/_astro/ClientSide.CECpQO_1_c6mBh.webp) ### Client-Side Processing All design editing operations are executed directly on the device, with no need for server dependencies. ![Customizable UI](/docs/cesdk/_astro/CustomizableUI.DtHv9rY-_2fNrB2.webp) ### Customizable UI Tailor the photo editing interface to align with your application’s branding and UX requirements. ## What is the Photo Editor Solution? The Photo Editor is a fully customizable CE.SDK configuration focused on photo-centric use cases. It strips down the editor interface to include only the most relevant features for image adjustments — giving users a focused and responsive experience. Whether your users need to fine-tune selfies, prepare product photos, or create profile images, this solution makes it easy. Get a powerful photo editor into your app with minimal setup. The Photo Editor runs entirely client-side — which helps reduce cloud computing costs and improve privacy. ## Platform Compatibility The CE.SDK Photo Editor is optimized for use on Android devices, providing smooth and efficient performance across a variety of models. ## Prerequisites To get started with the CE.SDK Photo Editor on Android, ensure you have the latest version of Android Studio and Kotlin installed. ## Supported File Types The CE.SDK Photo Editor supports various image formats, enabling users to work with popular file types. ### Importing Media Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. ### Exporting Media Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ### Importing Templates Format Description `.idml` InDesign `.psd` Photoshop `.scene` CE.SDK Native Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs to generate scenes programmatically. For detailed information, see the [full file format support list](android/file-format-support-3c4b2a/). ## Understanding CE.SDK Architecture & API The following sections provide an overview of the key components of the CE.SDK photo editor UI and its API architecture. If you’re ready to start integrating CE.SDK into your Android application, check out our Implementation Guide. ### CreativeEditor SDK Mobile Photo UI The CE.SDK photo editor UI is a streamlined configuration of the CreativeEditor SDK, focusing on essential photo editing features. This configuration is fully customizable, allowing developers to adjust the UI and functionality to suit different use cases. Key components include: * **Canvas:** The primary workspace where users interact with their photo content. * **Inspector Bar:** Offers tools for adjusting properties like size, position, and effects for selected elements. * **Asset Library:** A collection of media resources available for use within the photo editor, including images and stickers. Learn more about interacting with and customizing the photo editor UI in our design editor UI guide. ### CreativeEngine At the heart of CE.SDK is the CreativeEngine, which powers all rendering and photo manipulation tasks. It can be used in headless mode or in combination with the CreativeEditor UI. Key features and APIs provided by CreativeEngine include: * [**Scene Management:**](android/open-the-editor/overview-99444b/) Create, load, save, and manipulate photo scenes programmatically. * [**Block Management:**](android/concepts/blocks-90241e/) Manage images, text, and other elements within the photo editor. * [**Asset Management:**](android/import-media/concepts-5e6197/) Integrate and manage photo and image assets from various sources. * [**Variable Management:**](android/create-templates/add-dynamic-content/text-variables-7ecb50/) Define and manipulate variables for dynamic content within photo scenes. * [**Event Handling:**](android/concepts/events-353f97/) Subscribe to events like image selection changes or editing actions for dynamic interaction. ## Customizing the Android Image Editor CE.SDK provides extensive [customization options](android/user-interface/ui-extensions-d194d1/), allowing you to tailor the UI and functionality to meet your specific needs. This can range from basic configuration settings to more advanced customizations involving custom asset sources and [hooking into UIEvents](android/user-interface/events-514b70/). ### Basic Customizations * **Configuration Object:** Customize the editor’s appearance and functionality by passing a configuration object during initialization. ``` val engineConfiguration = EngineConfiguration.rememberForPhoto( license = "", imageUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg"), imageSize = SizeF(1080F, 1920F), userId = "", ) ``` * **Custom Asset Sources:** Serve custom images or stickers from a remote URL. ### UI Customization Options * **Theme:** Choose between ‘dark’ or ‘light’ themes. ``` val editorConfiguration = EditorConfiguration.rememberForPhoto( uiMode = EditorUiMode.DARK,) ``` ## Ready to get started? With a free trial and pricing that fits your needs, it's easy to find the best solution for your product. [Free Trial](https://img.ly/forms/free-trial) ### 500M+ video and photo creations are powered by IMG.LY every month ![HP logo](/docs/cesdk/_astro/HP.BZ1qDNii_ZpK5Lk.webp) ![Shopify logo](/docs/cesdk/_astro/Shopify.Dmyk4png_ZRKWXF.webp) ![Reuters logo](/docs/cesdk/_astro/Reuters.B8BV2Fek_ZLrHFJ.webp) ![Hootsuite logo](/docs/cesdk/_astro/Hootsuite.C94d5fhs_Zsc4gx.webp) ![Semrush logo](/docs/cesdk/_astro/Semrush.B2YsPaIn_23cDNx.webp) ![Shutterfly logo](/docs/cesdk/_astro/Shutterfly.Cc7Sw48y_Z3TjCs.webp) ![Sprout Social logo](/docs/cesdk/_astro/Sprout-Social.VxlY6_Tc_E0Dzh.webp) ![One.com logo](/docs/cesdk/_astro/Onecom.BQ_oPnlz_Z1btrtu.webp) ![Constant Contact logo](/docs/cesdk/_astro/Constant-Contact.1rh975Q__Z2ob7wU.webp) --- [Source](https:/img.ly/docs/cesdk/android/prebuilt-solutions/design-editor-9bf041) # Android Design Tool & Design Editor Give your users a fast, intuitive way to personalize templates with layout, text, and image editing — no design experience needed. The Design Editor comes ready to use and can be easily added to your Android app with minimal setup. [ Launch Web Demo ](https://img.ly/showcases/cesdk/default-ui/)[ View on GitHub ](https://github.com/imgly/cesdk-android-examples/tree/main/editor-guides-solutions-design-editor) ## What is the Design Editor Solution? The Design Editor is a pre-built configuration of the CreativeEditor SDK (CE.SDK), tailored for non-professional users to easily adapt and personalize existing templates. It’s optimized for workflows that focus on editing layout elements like text, images, and shapes — all within clearly defined design constraints. Whether you’re building a product customization app, dynamic ad creator, or template-based marketing tool, the Design Editor brings a polished, user-friendly interface to your users — right out of the box. ## Key Features * **Template-based editing** Empower users to customize existing templates while preserving brand integrity and layout rules. * **Smart context menus** Clicking an element opens a simplified editing toolbar, showing only the most relevant actions — like replacing or cropping an image. * **Streamlined user interface** The interface is designed to surface essential tools first. A “More” button reveals the full set of features for deeper editing. * **Role-based permissions** Easily toggle between _Creator_ and _Adopter_ roles to define what elements users can modify, lock, or hide. * **Cross-platform support** Available for Web, iOS, Android, and Desktop — all powered by the same core SDK. ## Why Use This Solution? The Design Editor is the fastest way to offer layout editing with production-ready UX. It reduces the effort of building a complete UI from scratch, while giving you full control over customization and integration. Choose this solution if you want to: * Provide a ready-to-use template editor that feels intuitive to end users * Accelerate your time to market with a polished layout editing experience * Maintain creative control by restricting editable areas with template constraints * Avoid building custom design tooling for every use case --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/uri-resolver-36b624) # URI Resolver ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Engine fun uriResolver( 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) // This will return uri to "banana.jpg" asset file engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) // This will return uri to remote resource "https://example.com/orange.png" engine.editor.getAbsoluteUri(uri = Uri.parse("https://example.com/orange.png")) // Replace all .jpg files with the IMG.LY logo! engine.editor.setUriResolver { if (it.toString().endsWith(".jpg")) { Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg") } else { engine.editor.defaultUriResolver(it) } } // The custom resolver will return a path to the IMG.LY logo because the given path ends with ".jpg". // This applies regardless if the given path is relative or absolute. engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) // The custom resolver will not modify this path because it ends with ".png". engine.editor.getAbsoluteUri(uri = Uri.parse("https://example.com/orange.png")) // Because a custom resolver is set, relative paths that the resolver does not transform remain unmodified! engine.editor.getAbsoluteUri(uri = Uri.parse("/orange.png")) // Removes the previously set resolver. engine.editor.setUriResolver(null) // Since we've removed the custom resolver, this will return // Uri.Asset("banana.jpg") like before. engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) engine.editor.setUriResolver { uri -> val path = uri.path if (uri.host == "localhost" && path != null && path.startsWith("/scenes") && !path.endsWith(".scene")) { // Apply custom logic here, e.g. redirect to a different server } engine.editor.defaultUriResolver(uri) } engine.stop()} ``` CE.SDK gives you full control over how Uris should be resolved. To register a custom resolver, use `setUriResolver` and pass in a function implementing your resolution logic. If a custom resolver is set, any Uri (both relative and absolute) requested by the engine is passed through the resolver. The Uri your logic returns is then fetched by the engine. The resolved Uri is just used for the current request and not stored. If, and only if, no custom resolver is set, the engine performs the default behaviour: absolute uri objects are unchanged and relative objects are turned into android asset Uris. **Warning** Your custom Uri resolver must return an absolute Uri. We can preview the effects of setting a custom Uri resolver with the function `fun getAbsoluteUri(uri: Uri): Uri`. Before setting a custom Uri resolver, the default behavior is as before: absolute Uri is unchanged and relative is turned into android asset Uri. ``` // This will return uri to "banana.jpg" asset fileengine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg"))// This will return uri to remote resource "https://example.com/orange.png"engine.editor.getAbsoluteUri(uri = Uri.parse("https://example.com/orange.png")) ``` To show that the resolver can be fairly free-form, in this example we register a custom Uri resolver that replaces all `.jpg` images with our company logo. Note: you can still access the default Uri resolver by calling `fun defaultUriResolver(uri: Uri): Uri`. ``` // Replace all .jpg files with the IMG.LY logo!engine.editor.setUriResolver { if (it.toString().endsWith(".jpg")) { Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg") } else { engine.editor.defaultUriResolver(it) }} ``` Given the same uri as earlier, the custom resolver transforms it as specified. Note that after a custom resolver is set, uris that the resolver does not transform remain unmodified thanks to `defaultUriResolver`. ``` // The custom resolver will return a path to the IMG.LY logo because the given path ends with ".jpg". // This applies regardless if the given path is relative or absolute. engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) // The custom resolver will not modify this path because it ends with ".png". engine.editor.getAbsoluteUri(uri = Uri.parse("https://example.com/orange.png")) // Because a custom resolver is set, relative paths that the resolver does not transform remain unmodified! engine.editor.getAbsoluteUri(uri = Uri.parse("/orange.png")) ``` To remove a previously set custom resolver, call the function with a `null` value. The Uri resolution is now back to the default behavior. ``` // Removes the previously set resolver. engine.editor.setUriResolver(null) // Since we've removed the custom resolver, this will return // Uri.Asset("banana.jpg") like before. engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) ``` ## Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.*import ly.img.engine.* fun uriResolver( 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) // This will return uri to "banana.jpg" asset file engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) // This will return uri to remote resource "https://example.com/orange.png" engine.editor.getAbsoluteUri(uri = Uri.parse("https://example.com/orange.png")) // Replace all .jpg files with the IMG.LY logo! engine.editor.setUriResolver { if (it.toString().endsWith(".jpg")) { Uri.parse("https://img.ly/static/ubq_samples/imgly_logo.jpg") } else { engine.editor.defaultUriResolver(it) } } // The custom resolver will return a path to the IMG.LY logo because the given path ends with ".jpg". // This applies regardless if the given path is relative or absolute. engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) // The custom resolver will not modify this path because it ends with ".png". engine.editor.getAbsoluteUri(uri = Uri.parse("https://example.com/orange.png")) // Because a custom resolver is set, relative paths that the resolver does not transform remain unmodified! engine.editor.getAbsoluteUri(uri = Uri.parse("/orange.png")) // Removes the previously set resolver. engine.editor.setUriResolver(null) // Since we've removed the custom resolver, this will return // Uri.Asset("banana.jpg") like before. engine.editor.getAbsoluteUri(uri = Uri.parse("banana.jpg")) engine.editor.setUriResolver { uri -> val path = uri.path if (uri.host == "localhost" && path != null && path.startsWith("/scenes") && !path.endsWith(".scene")) { // Apply custom logic here, e.g. redirect to a different server } engine.editor.defaultUriResolver(uri) } engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/overview-99444b) # Overview CreativeEditor SDK (CE.SDK) offers multiple ways to open the editor. Whether you’re starting with a blank canvas or importing complex layered files, CE.SDK gives you the building blocks to launch an editing session tailored to your users’ needs. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Ways to Open the Editor You can initialize CE.SDK in several ways depending on your content pipeline: * **Start with a Blank Canvas** Useful for creating new content from scratch. Define canvas dimensions and scene mode manually or programmatically. * **Load a Scene** Load a saved scene from JSON, archive, or blob to restore a previous editing session or template. * **Create from Media** Initialize the editor with a preloaded image, video. * **Create from Template** Kick off the editor with a predefined template, including placeholders and editing constraints. * **Import a Design** Import external designs from InDesign or Photoshop by running them through an importer and edit the resulting scene or archive in the SDK. ## Using Low-Quality / High-Quality Assets To ensure responsive editing and high-quality exports, CE.SDK allows you to dynamically switch between asset resolutions: * **Edit with Low-Res Assets** Load smaller versions of images or videos during the editing process to reduce memory usage and improve performance. * **Export with High-Res Assets** Swap out low-res placeholders for full-quality assets just before exporting. This can be handled using the Scene or Block APIs by switching asset paths or making use of source sets for fills. This pattern is commonly used in design systems that require high-resolution print or web output while maintaining editing performance. ## Working with Watermarked or Placeholder Media CE.SDK supports licensing-based workflows where full-resolution assets are only available after purchase or user action: * **Use Watermarked or Preview Media on Load** Start with branded, obfuscated, or watermarked assets to limit unauthorized use. * **Swap with Purchased Assets Post-Checkout** Replace asset URIs within the same scene structure using a one-time update, ensuring consistency without disrupting layout or styling. ## Implementing a Custom URI Resolver CE.SDK provides a `setURIResolver()` method to intercept and customize asset loading: * **Why Use a URI Resolver?** Handle dynamic URL rewriting, token-based authentication, asset migration, CDN fallbacks, or redirect requests to internal APIs. * **How It Works** The engine routes every asset URI through your custom resolver function. This function returns the final, resolved URI used for the current fetch operation. * **Recommended Use Cases**: * Add auth headers or query params * Redirect public assets to internal mirrors * Handle signed URLs or token expiration --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/set-zoom-level-d31896) # Android Image Zoom Library ``` // Zoom to 100%engine.scene.setZoomLevel(level = 1F) // Zoom to 50%engine.scene.setZoomLevel(0.5F * engine.scene.getZoomLevel()) // Bring entire scene in view with padding of 20px in all directionsengine.scene.zoomToBlock( block = scene, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F)engine.scene.immediateZoomToBlock( block = scene, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F) // Follow page with padding of 20px in both directionsengine.scene.enableZoomAutoFit( block = page, axis = ZoomAutoFitAxis.BOTH, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F) // Stop following pageengine.scene.disableZoomAutoFit(page) // Query if zoom auto-fit is enabled for pageengine.scene.isZoomAutoFitEnabled(page) // Keep the scene with padding of 10px within the cameraengine.scene.enableCameraPositionClamping( blocks = listOf(scene), paddingLeft = 10F, paddingTop = 10F, paddingRight = 10F, paddingBottom = 10F, scaledPaddingLeft = 0F, scaledPaddingTop = 0F, scaledPaddingRight = 0F, scaledPaddingBottom = 0F) engine.scene.disableCameraPositionClamping() // Query if camera position clamping is enabled for the sceneengine.scene.isCameraPositionClampingEnabled(scene) // Allow zooming from 12.5% to 800% relative to the size of a pageengine.scene.enableCameraZoomClamping( listOf(page), minZoomLimit = 0.125F, maxZoomLimit = 8F, paddingLeft = 0F, paddingTop = 0F, paddingRight = 0F, paddingBottom = 0F,) engine.scene.disableCameraZoomClamping() // Query if camera zoom clamping is enabled for the sceneengine.scene.isCameraZoomClampingEnabled(scene) // Get notified when the zoom level changesengine.scene.onZoomLevelChanged() .onEach { val zoomLevel = engine.scene.getZoomLevel() println("Zoom level is now: $zoomLevel") } .launchIn(CoroutineScope(Dispatchers.Main)) ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to control and observe camera zoom via the `scene` API. ## Functions ``` fun getZoomLevel(): Float ``` Get the zoom level of the scene or for a camera in the scene. Returns the current zoom level of the scene in unit `dpx/dot`. A zoom level of 2F results in one dot in the design to be two pixels on the screen. * Returns the zoom level of the scene. ``` fun setZoomLevel(level: Float) ``` Set the zoom level of the scene, e.g., for headless versions. This only shows an effect if the zoom level is not handled/overwritten by the UI. Setting a zoom level of 2F results in one dot in the design to be two pixels on the screen. * `level`: the zoom level with unit `dpx/dot`. is shown on the screen. ``` suspend fun zoomToBlock( block: DesignBlock, paddingLeft: Float = 0F, paddingTop: Float = 0F, paddingRight: Float = 0F, paddingBottom: Float = 0F,) ``` Sets the zoom and focus to show a block. Without padding, this results in a tight view on the block. It is set asynchronous to ensure that the block dimensions are known. * `block`: the block that should be focused on. * `paddingLeft`: optional padding in screen pixels to the left of the block. * `paddingTop`: optional padding in screen pixels to the top of the block. * `paddingRight`: optional padding in screen pixels to the right of the block. * `paddingBottom`: optional padding in screen pixels to the bottom of the block. ``` fun immediateZoomToBlock( block: DesignBlock, paddingLeft: Float = 0F, paddingTop: Float = 0F, paddingRight: Float = 0F, paddingBottom: Float = 0F, forceUpdate: Boolean = false,) ``` Sets the zoom and focus to show a block. This only shows an effect if the zoom level is not handled/overwritten by the UI. Without padding, this results in a tight view on the block. It is set immediately and assumes that the block dimensions are known. The block should not be in pending state and it’s layout should be up to date. * `block`: the block that should be focused on. * `paddingLeft`: optional padding in screen pixels to the left of the block. * `paddingTop`: optional padding in screen pixels to the top of the block. * `paddingRight`: optional padding in screen pixels to the right of the block. * `paddingBottom`: optional padding in screen pixels to the bottom of the block. * `forceUpdate`: If true, the implicit update is called. ``` fun enableZoomAutoFit( block: DesignBlock, axis: ZoomAutoFitAxis, paddingLeft: Float = 0.0F, paddingTop: Float = 0.0F, paddingRight: Float = 0.0F, paddingBottom: Float = 0.0F,) ``` Continually adjusts the zoom level to fit the width or height of a block’s axis-aligned bounding box. This only shows an effect if the zoom level is not handled/overwritten by the UI. Without padding, this results in a tight view on the block. No more than one block per scene can have zoom auto-fit enabled. Calling `setZoomLevel` or `zoomToBlock` disables the continuous adjustment. * `block`: the block in the scene for which to enable a zoom auto-fit. * `axis`: the block axis (or axes) for which the zoom is adjusted. * `paddingLeft`: optional padding in screen pixels to the left of the block. * `paddingTop`: optional padding in screen pixels to the top of the block. * `paddingRight`: optional padding in screen pixels to the right of the block. * `paddingBottom`: optional padding in screen pixels to the bottom of the block. ``` fun disableZoomAutoFit(block: DesignBlock) ``` Disables any previously set zoom auto-fit. * `block`: the scene or a block in the scene for which to disable zoom auto-fit. ``` fun isZoomAutoFitEnabled(block: DesignBlock): Boolean ``` Queries whether zoom auto-fit is enabled for `block`. * `block`: the scene or a block in the scene for which to query if zoom auto-fit is set. * Returns true if the given block has auto-fit set or the scene contains a block for which auto-fit is set, false otherwise. ``` @UnstableEngineApifun enableCameraPositionClamping( blocks: List, paddingLeft: Float = 0.0F, paddingTop: Float = 0.0F, paddingRight: Float = 0.0F, paddingBottom: Float = 0.0F, scaledPaddingLeft: Float = 0.0F, scaledPaddingTop: Float = 0.0F, scaledPaddingRight: Float = 0.0F, scaledPaddingBottom: Float = 0.0F,) ``` Continually ensures the camera position to be within the width and height of the blocks axis-aligned bounding box. Without padding, this results in a tight clamp on the blocks. * `blocks`: the blocks for which the camera position is adjusted to, usually, the scene or a page. * `paddingLeft`: optional padding in screen pixels to the left of the block. * `paddingTop`: optional padding in screen pixels to the top of the block. * `paddingRight`: optional padding in screen pixels to the right of the block. * `paddingBottom`: optional padding in screen pixels to the bottom of the block. * `scaledPaddingLeft`: optional padding in screen pixels to the left of the block that scales with the zoom level until five times the initial value. * `scaledPaddingTop`: optional padding in screen pixels to the top of the block that scales with the zoom level until five times the initial value. * `scaledPaddingRight`: optional padding in screen pixels to the right of the block that scales with the zoom level until five times the initial value. * `scaledPaddingBottom`: optional padding in screen pixels to the bottom of the block that scales with the zoom level until five times the initial value. ``` @UnstableEngineApifun disableCameraPositionClamping() ``` Disables any previously set position clamping for the current scene. ``` @UnstableEngineApifun isCameraPositionClampingEnabled(blockOrScene: DesignBlock): Boolean ``` Queries whether position clamping is enabled for `blockOrScene`. * `blockOrScene`: the scene or a block in the scene for which to query the position clamping. * Returns true if the given block has position clamping set or the scene contains a block for which position clamping is set, false otherwise. ``` @UnstableEngineApifun enableCameraZoomClamping( blocks: List, minZoomLimit: Float = -1.0F, maxZoomLimit: Float = -1.0F, paddingLeft: Float = 0.0F, paddingTop: Float = 0.0F, paddingRight: Float = 0.0F, paddingBottom: Float = 0.0F,) ``` Continually ensures the zoom level of the camera in the active scene to be in the given range. * Note: A zoom level of 2.0 results in one pixel in the design to be two pixels on the screen. * `blocks`: the blocks for which the camera position is adjusted to, usually, the scene or a page. * `minZoomLimit`: the minimum zoom level limit when zooming out, unlimited when negative. * `maxZoomLimit`: the maximum zoom level limit when zooming in, unlimited when negative. * `paddingLeft`: optional padding in screen pixels to the left of the block. Only applied when the block is not a camera. * `paddingTop`: optional padding in screen pixels to the top of the block. Only applied when the block is not a camera. * `paddingRight`: optional padding in screen pixels to the right of the block. Only applied when the block is not a camera. * `paddingBottom`: optional padding in screen pixels to the bottom of the block. Only applied when the block is not a camera. ``` @UnstableEngineApifun disableCameraZoomClamping() ``` Disables previously set zoom clamping for the current scene. ``` @UnstableEngineApifun isCameraZoomClampingEnabled(blockOrScene: DesignBlock): Boolean ``` Queries whether zoom clamping is enabled. * `blockOrScene`: the scene or a block in the scene for which to query the zoom clamping. * Returns true if the given block has zoom clamping set or the scene contains a block for which zoom clamping is set, false otherwise. ``` fun onZoomLevelChanged(): Flow ``` Subscribe to changes to the zoom level. * Returns flow of zoom change events. ## Settings See clamp camera settings in the [editor settings](android/settings-970c98/). ## Full Code Here’s the full code: ``` // Zoom to 100%engine.scene.setZoomLevel(level = 1F) // Zoom to 50%engine.scene.setZoomLevel(0.5F * engine.scene.getZoomLevel()) // Bring entire scene in view with padding of 20px in all directionsengine.scene.zoomToBlock( block = scene, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F)engine.scene.immediateZoomToBlock( block = scene, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F) // Follow page with padding of 20px in both directionsengine.scene.enableZoomAutoFit( block = page, axis = ZoomAutoFitAxis.BOTH, paddingLeft = 20F, paddingTop = 20F, paddingRight = 20F, paddingBottom = 20F) // Stop following pageengine.scene.disableZoomAutoFit(page) // Query if zoom auto-fit is enabled for pageengine.scene.isZoomAutoFitEnabled(page) // Keep the scene with padding of 10px within the cameraengine.scene.enableCameraPositionClamping( blocks = listOf(scene), paddingLeft = 10F, paddingTop = 10F, paddingRight = 10F, paddingBottom = 10F, scaledPaddingLeft = 0F, scaledPaddingTop = 0F, scaledPaddingRight = 0F, scaledPaddingBottom = 0F) engine.scene.disableCameraPositionClamping() // Query if camera position clamping is enabled for the sceneengine.scene.isCameraPositionClampingEnabled(scene) // Allow zooming from 12.5% to 800% relative to the size of a pageengine.scene.enableCameraZoomClamping( listOf(page), minZoomLimit = 0.125F, maxZoomLimit = 8F, paddingLeft = 0F, paddingTop = 0F, paddingRight = 0F, paddingBottom = 0F,) engine.scene.disableCameraZoomClamping() // Query if camera zoom clamping is enabled for the sceneengine.scene.isCameraZoomClampingEnabled(scene) // Get notified when the zoom level changesengine.scene.onZoomLevelChanged() .onEach { val zoomLevel = engine.scene.getZoomLevel() println("Zoom level is now: $zoomLevel") } .launchIn(CoroutineScope(Dispatchers.Main)) ``` --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/load-scene-478833) # Load a Scene ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport java.io.ByteArrayOutputStreamimport java.net.URL fun loadSceneFromBlob( 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 sceneUrl = URL("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") val sceneBlob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() sceneUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } val blobString = String(sceneBlob, Charsets.UTF_8) val scene = engine.scene.load(scene = blobString) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(text, enabled = true) engine.stop()} ``` ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport java.io.ByteArrayOutputStreamimport java.net.URL fun loadSceneFromString( 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 sceneUrl = URL("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") val sceneBlob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() sceneUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } val blobString = String(sceneBlob, Charsets.UTF_8) val scene = engine.scene.load(scene = blobString) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(text, enabled = true) engine.stop()} ``` ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engine fun loadSceneFromRemote( 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 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 text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(text, enabled = true) engine.stop()} ``` Loading an existing scene allows resuming work on a previous session or adapting an existing template to your needs. **Warning** Saving a scene can be done as a either _scene file_ or as an _archive file_ (c.f. [Saving scenes](android/export-save-publish/save-c8b124/)). 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. ## Load Scenes from a Remote URL Determine a url that points to a scene binary string. Create an instance of `Uri` using the remote url. ``` val sceneUri = Uri.parse( "https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene",) ``` We can then pass the object `sceneUri` to the `suspend fun load(sceneUri: Uri): DesignBlock` function. The editor will reset and present the given scene to the user. The function is asynchronous and it does not throw if the scene load succeeded. ``` val scene = engine.scene.load(sceneUri = sceneUri) ``` From this point on we can continue to modify our scene. In this example, assuming the scene contains a text element, we add a drop shadow to it. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` val text = engine.block.findByType(DesignBlockType.Text).first()engine.block.setDropShadowEnabled(text, enabled = true) ``` Scene loads may be reverted using `engine.editor.undo()`. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ### Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.*import ly.img.engine.* fun loadSceneFromRemote( 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 text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(text, enabled = true) engine.stop()} ``` ## Load Scenes from a String In this example, we fetch a scene from a remote url and load it as a string. This string could also come from the result of `suspend fun saveToString(): String`. ``` val sceneUrl = URL("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")val sceneBlob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() sceneUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray()}val blobString = String(sceneBlob, Charsets.UTF_8) ``` We can then pass that string to the `suspend fun load(scene: String): DesignBlock` function. The editor will then reset and present the given scene to the user. The function is asynchronous and it does not throw if the scene load succeeded. ``` val scene = engine.scene.load(scene = blobString) ``` From this point on we can continue to modify our scene. In this example, assuming the scene contains a text element, we add a drop shadow to it. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` val text = engine.block.findByType(DesignBlockType.Text).first()engine.block.setDropShadowEnabled(text, enabled = true) ``` Scene loads may be reverted using `engine.editor.undo()`. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ### Full Code Here’s the full code: ``` import kotlinx.coroutines.*import ly.img.engine.*import java.io.ByteArrayOutputStreamimport java.net.URL fun loadSceneFromString( 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 sceneUrl = URL("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") val sceneBlob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() sceneUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } val blobString = String(sceneBlob, Charsets.UTF_8) val scene = engine.scene.load(scene = blobString) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(text, enabled = true) engine.stop()} ``` ## Load Scenes From a Blob In this example, we fetch a scene from a remote url and load it as `sceneBlob`. ``` val sceneUrl = URL("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")val sceneBlob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() sceneUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray()} ``` To acquire a scene string from `sceneBlob`, we need to read its contents into a string. ``` val blobString = String(sceneBlob, Charsets.UTF_8) ``` We can then pass that string to the `suspend fun load(scene: String): DesignBlock` function. The editor will reset and present the given scene to the user. The function is asynchronous and it does not throw if the scene load succeeded. ``` val scene = engine.scene.load(scene = blobString) ``` From this point on we can continue to modify our scene. In this example, assuming the scene contains a text element, we add a drop shadow to it. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` val text = engine.block.findByType(DesignBlockType.Text).first()engine.block.setDropShadowEnabled(text, enabled = true) ``` Scene loads may be reverted using `engine.editor.undo()`. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ### Full Code Here’s the full code: ``` import kotlinx.coroutines.*import ly.img.engine.*import java.io.ByteArrayOutputStreamimport java.net.URL fun loadSceneFromBlob( 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 sceneUrl = URL("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene") val sceneBlob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() sceneUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } val blobString = String(sceneBlob, Charsets.UTF_8) val scene = engine.scene.load(scene = blobString) val text = engine.block.findByType(DesignBlockType.Text).first() engine.block.setDropShadowEnabled(text, enabled = true) engine.stop()} ``` ``` val scene = requireNotNull(engine.scene.get()) // Forcing all resources of all the blocks in a scene or the resources of graphic block to loadengine.block.forceLoadResources(listOf(scene)) val graphics = engine.block.findByType(DesignBlockType)engine.block.forceLoadResources(graphics) ``` ## Loading Scene Archives Loading a scene archives requires unzipping the archives contents to a location, that’s accessible to the CreativeEngine. One could for example unzip the archive via `unzip archive.zip` and then serve its contents using `$ npx serve`. This spins up a local test server, that serves everything contained in the current directory at `http://localhost:3000` The archive can then be loaded by calling `engine.scene.load("http://localhost:3000/scene.scene")`. See [loading scenes](android/open-the-editor/load-scene-478833/) for more details. All asset paths in the archive are then resolved relative to the location of the `scene.scene` file. For an image, that would result in `'http://localhost:3000/images/1234.jpeg'`. After loading all URLs are fully resolved with the location of the `scene.scene` file and the scene behaves like any other scene. ### Resolving assets from a different source The engine will use its [URI resolver](android/open-the-editor/uri-resolver-36b624/) to resolve all asset paths it encounters. This allows you to redirect requests for the assets contained in archive to a different location. To do so, you can add a custom resolver, that redirects requests for assets to a different location. Assuming you store your archived scenes in a `scenes/` directory, this would be an example of how to do so: ``` engine.editor.setURIResolver { val uri = java.net.URI(it.path) if (uri.host == "localhost" && uri.path.startsWith("/scenes") && !uri.path.endsWith(".scene")) { // Apply custom logic here, e.g. redirect to a different server } engine.editor.defaultURIResolver(it) } ``` --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/from-image-ad9b5e) # Create From Image ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport java.io.ByteArrayOutputStreamimport java.io.Fileimport java.net.URLimport java.util.UUID fun createSceneFromImageBlob( 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 blobUrl = URL("https://img.ly/static/ubq_samples/sample_4.jpg") val blob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() blobUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } val blobFile = withContext(Dispatchers.IO) { File.createTempFile(UUID.randomUUID().toString(), ".tmp").apply { outputStream().use { it.write(blob) } } } val blobUri = Uri.fromFile(blobFile) val scene = engine.scene.createFromImage(blobUri) // Find the automatically added graphic block in the scene that contains the image fill. val block = engine.block.findByType(DesignBlockType.Graphic).first() // Change its opacity. engine.block.setOpacity(block, value = 0.5F) engine.stop()} ``` Starting from an existing image allows you to use the editor for customizing individual assets. This is done by using `suspend fun createFromImage(imageUri: URI, dpi: Float = 300F, pixelScaleFactor: Float = 1F): DesignBlock` and passing a URI as argument. The `dpi` argument sets the dots per inch of the scene. The `pixelScaleFactor` sets the display’s pixel scale factor. ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engine fun createSceneFromImageURL( 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 imageRemoteUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg") val scene = engine.scene.createFromImage(imageRemoteUri) // Find the automatically added graphic block in the scene that contains the image fill. val block = engine.block.findByType(DesignBlockType.Graphic).first() // Change its opacity. engine.block.setOpacity(block, value = 0.5F) engine.stop()} ``` ## Create a Scene From an Image URL In this example, we will show you how to initialize the [CreativeEditor SDK](https://img.ly/products/creative-sdk) with an initial image. Create an instance of `Uri` using the remote url. Use the object `imageRemoteUri` as a source for the initial image. ``` val imageRemoteUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg")val scene = engine.scene.createFromImage(imageRemoteUri) ``` We can retrieve the graphic block id of this initial image using `fun findByType(blockType: DesignBlockType): List`. Note that that function returns an array. Since there’s only a single graphic block in the scene, the block is at index `0`. ``` // Find the automatically added graphic block in the scene that contains the image fill.val block = engine.block.findByType(DesignBlockType.Graphic).first() ``` We can then manipulate and modify this block. Here we modify its opacity with `fun setOpacity(block: DesignBlock, @FloatRange(from = 0.0, to = 1.0) value: Float)`. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` // Change its opacity.engine.block.setOpacity(block, value = 0.5F) ``` When starting with an initial image, the scene’s page dimensions match the given resource and the scene is configured to be in pixel design units. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engine fun createSceneFromImageURL( 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 imageRemoteUri = Uri.parse("https://img.ly/static/ubq_samples/sample_4.jpg") val scene = engine.scene.createFromImage(imageRemoteUri) // Find the automatically added graphic block in the scene that contains the image fill. val block = engine.block.findByType(DesignBlockType.Graphic).first() // Change its opacity. engine.block.setOpacity(block, value = 0.5F) engine.stop()} ``` ## Create A Scene From an Image Blob In this example, we will show you how to initialize the [CreativeEditor SDK](https://img.ly/products/creative-sdk) with an initial image provided from a blob. First, get hold of a `blob` by fetching an image from the web. This is just for demonstration purposes and your `blob` object may come from a different source. ``` val blobUrl = URL("https://img.ly/static/ubq_samples/sample_4.jpg")val blob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() blobUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray()} ``` Afterward, create a temporary file and save the `Data`. Create an instance of `Uri` using the temporary file. ``` val blobFile = withContext(Dispatchers.IO) { File.createTempFile(UUID.randomUUID().toString(), ".tmp").apply { outputStream().use { it.write(blob) } }}val blobUri = Uri.fromFile(blobFile) ``` Use the object `blobUri` as a source for the initial image. ``` val scene = engine.scene.createFromImage(blobUri) ``` We can retrieve the graphic block id of this initial image using `fun findByType(blockType: DesignBlockType): List`. Note that that function returns a list. Since there’s only a single graphic block in the scene, the block is at index `0`. ``` // Find the automatically added graphic block in the scene that contains the image fill.val block = engine.block.findByType(DesignBlockType.Graphic).first() ``` We can then manipulate and modify this block. Here we modify its opacity with `fun setOpacity(block: DesignBlock, @FloatRange(from = 0.0, to = 1.0) value: Float)`. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` // Change its opacity.engine.block.setOpacity(block, value = 0.5F) ``` When starting with an initial image, the scenes page dimensions match the given image, and the scene is configured to be in pixel design units. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ### Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport java.io.ByteArrayOutputStreamimport java.io.Fileimport java.net.URLimport java.util.UUID fun createSceneFromImageBlob( 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 blobUrl = URL("https://img.ly/static/ubq_samples/sample_4.jpg") val blob = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() blobUrl.openStream().use { inputStream -> outputStream.use(inputStream::copyTo) } outputStream.toByteArray() } val blobFile = withContext(Dispatchers.IO) { File.createTempFile(UUID.randomUUID().toString(), ".tmp").apply { outputStream().use { it.write(blob) } } } val blobUri = Uri.fromFile(blobFile) val scene = engine.scene.createFromImage(blobUri) // Find the automatically added graphic block in the scene that contains the image fill. val block = engine.block.findByType(DesignBlockType.Graphic).first() // Change its opacity. engine.block.setOpacity(block, value = 0.5F) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/blank-canvas-18ff05) # Start With Blank Canvas ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun createSceneFromScratch( 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.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()} ``` In this example, we will show you how to initialize the [CreativeEditor SDK](https://img.ly/products/creative-sdk) from scratch and add a star shape. We create an empty scene via `engine.scene.create()` which sets up the default scene block with a camera attached. Afterwards, the scene can be populated by creating additional blocks and appending them to the scene. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` val scene = engine.scene.create() ``` We first add a page with `fun create(blockType: DesignBlockType): DesignBlock` specifying a `DesignBlockType.Page` and set a parent-child relationship between the scene and this page. ``` val page = engine.block.create(DesignBlockType.Page)engine.block.appendChild(parent = scene, child = page) ``` To this page, we add a graphic design block, again with `fun create(blockType: DesignBlockType): DesignBlock`. To make it more interesting, we set a star shape and a color fill to this block to give it a visual representation. Like for the page, we set the parent-child relationship between the page and the newly added block. From then on, modifications to this block are relative to the 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) ``` This example first appends a page child to the scene as would typically be done but it is not strictly necessary and any child block can be appended directly to a scene. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ### Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun createSceneFromScratch( 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.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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/outlines/strokes-c2e621) # Using Strokes In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify strokes through the `block` API. Strokes can be added to any shape or text and stroke styles are varying from plain solid lines to dashes and gaps of varying lengths and can have different end caps. ## Strokes ``` fun supportsStroke(block: DesignBlock): Boolean ``` Query if the given block has a stroke property. * `block`: the block to query. * Returns true if the block has a stroke property, false otherwise. ``` fun setStrokeEnabled( block: DesignBlock, enabled: Boolean,) ``` Enable or disable the stroke of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke should be enabled or disabled. * `enabled`: if true, the stroke will be enabled. ``` fun isStrokeEnabled(block: DesignBlock): Boolean ``` Query if the stroke of the given design block is enabled. * `block`: the block whose stroke state should be queried. * Returns true if the block’s stroke is enabled, false otherwise. ``` fun setStrokeColor( block: DesignBlock, color: Color,) ``` Set the stroke color of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke color should be set. * `color`: the color to set. ``` fun getStrokeColor(block: DesignBlock): Color ``` Get the stroke color of the given design block. * `block`: he block whose stroke color should be queried. * Returns the stroke color. ``` fun setStrokeWidth( block: DesignBlock, width: Float,) ``` Set the stroke width of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke width should be set. * `width`: the stroke width to be set. ``` fun getStrokeWidth(block: DesignBlock): Float ``` Get the stroke width of the given design block. * `block`: the block whose stroke width should be queried. * Returns the stroke’s width. ``` fun setStrokeStyle( block: DesignBlock, style: StrokeStyle,) ``` Set the stroke style of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke style should be set. * `style`: the stroke style to be set. ``` fun getStrokeStyle(block: DesignBlock): StrokeStyle ``` Get the stroke style of the given design block. * `block`: the block whose stroke style should be queried. * Returns the stroke’s style. ``` fun setStrokePosition( block: DesignBlock, position: StrokePosition,) ``` Set the stroke position of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke position should be set. * `position`: the stroke position to be set. ``` fun getStrokePosition(block: DesignBlock): StrokePosition ``` Get the stroke position of the given design block. * `block`: the block whose stroke position should be queried. * Returns the stroke position. ``` fun setStrokeCornerGeometry( block: DesignBlock, geometry: StrokeCornerGeometry,) ``` Set the stroke corner geometry of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke join geometry should be set. * `geometry`: the stroke join geometry to be set. ``` fun getStrokeCornerGeometry(block: DesignBlock): StrokeCornerGeometry ``` Get the stroke corner geometry of the given design block. * `block`: the block whose stroke join geometry should be queried. * Returns the stroke join geometry. ## Full Code Here’s the full code for using strokes: ``` // Check if block supports strokesif (engine.block.supportsStroke(block)) { // Enable the stroke engine.block.setStrokeEnabled(block, enabled = true) val strokeIsEnabled = engine.block.isStrokeEnabled(block) // Configure it engine.block.setStrokeColor(block, color = Color.fromRGBA(r = 1F, g = 0.75F, b = 0.8F, a = 1F)) val strokeColor = engine.block.getStrokeColor(block) engine.block.setStrokeWidth(block, width = 5F) val strokeWidth = engine.block.getStrokeWidth(block) engine.block.setStrokeStyle(block, style = StrokeStyle.DASHED) val strokeStyle = engine.block.getStrokeStyle(block) engine.block.setStrokePosition(block, position = StrokePosition.OUTER) val strokePosition = engine.block.getStrokePosition(block) engine.block.setStrokeCornerGeometry(block, geometry = StrokeCornerGeometry.ROUND) val strokeCornerGeometry = engine.block.getStrokeCornerGeometry(block)} ``` --- [Source](https:/img.ly/docs/cesdk/android/open-the-editor/from-video-86beb0) # Create From Video ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engine fun createSceneFromVideoURL( 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 videoRemoteUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4") val scene = engine.scene.createFromVideo(videoRemoteUri) // Find the automatically added graphic block in the scene that contains the video fill. val block = engine.block.findByType(DesignBlockType.Graphic).first() // Change its opacity. engine.block.setOpacity(block, value = 0.5F) engine.stop()} ``` In this example, we will show you how to initialize the [CreativeEditor SDK](https://img.ly/products/creative-sdk) with an initial video. Starting from an existing video allows you to use the editor for customizing individual assets. This is done by using `suspend fun createFromVideo(videoUri: URI): DesignBlock` and passing a URI as argument. Create an instance of `Uri` using the remote url. Use the object `videoRemoteUri` as a source for the initial video. ``` val videoRemoteUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4")val scene = engine.scene.createFromVideo(videoRemoteUri) ``` We can retrieve the graphic block id of this initial video using `fun findByType(blockType: DesignBlockType): List`. Note that that function returns an array. Since there’s only a single graphic block in the scene, the block is at index `0`. ``` // Find the automatically added graphic block in the scene that contains the video fill.val block = engine.block.findByType(DesignBlockType.Graphic).first() ``` We can then manipulate and modify this block. Here we modify its opacity with `fun setOpacity(block: DesignBlock, @FloatRange(from = 0.0, to = 1.0) value: Float)`. See [Modifying Scenes](android/concepts/blocks-90241e/) for more details. ``` // Change its opacity.engine.block.setOpacity(block, value = 0.5F) ``` When starting with an initial video, the scene’s page dimensions match the given resource and the scene is configured to be in pixel design units. To later save your scene, see [Saving Scenes](android/export-save-publish/save-c8b124/). ## Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.*import ly.img.engine.* fun createSceneFromVideoURL( 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 videoRemoteUri = Uri.parse("https://img.ly/static/ubq_video_samples/bbb.mp4") val scene = engine.scene.createFromVideo(videoRemoteUri) // Find the automatically added graphic block in the scene that contains the video fill. val block = engine.block.findByType(DesignBlockType.Graphic).first() // Change its opacity. engine.block.setOpacity(block, value = 0.5F) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/outlines/shadows-and-glows-6610fa) # Shadows and Glows ``` // Configure a basic colored drop shadow if the block supports themif (engine.block.supportsDropShadow(block)) { engine.block.setDropShadowEnabled(block, enabled = true) engine.block.setDropShadowColor(block, Color.fromRGBA(r = 1F, g = 0.75F, b = 0.8F, a = 1F)) val dropShadowColor = engine.block.getDropShadowColor(block) engine.block.setDropShadowOffsetX(block, offsetX = -10F) engine.block.setDropShadowOffsetY(block, offsetY = 5F) val dropShadowOffsetX = engine.block.getDropShadowOffsetX(block); val dropShadowOffsetY = engine.block.getDropShadowOffsetY(block); engine.block.setDropShadowBlurRadiusX(block, blurRadiusX = -10F) engine.block.setDropShadowBlurRadiusY(block, blurRadiusY = 5F) engine.block.setDropShadowClip(block, clip = false) val dropShadowClip = engine.block.getDropShadowClip(block) // Query a blocks drop shadow properties val dropShadowIsEnabled = engine.block.isDropShadowEnabled(block) val dropShadowBlurRadiusX = engine.block.getDropShadowBlurRadiusX(block) val dropShadowBlurRadiusY = engine.block.getDropShadowBlurRadiusY(block)} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify an block’s drop shadow through the `block` API. Drop shadows can be added to any shape, text or image. One can adjust its offset relative to its block on the X and Y axes, its blur factor on the X and Y axes and whether it is visible behind a transparent block. ## Functions ``` fun supportsDropShadow(block: DesignBlock): Boolean ``` Query if the given block has a drop shadow property. * `block`: the block to query. * Returns true if the block has a drop shadow property, false otherwise. ``` fun setDropShadowEnabled( block: DesignBlock, enabled: Boolean,) ``` Enable or disable the drop shadow of the given design block. Required scope: “appearance/shadow” * `block`: the block whose drop shadow should be enabled or disabled. * `enabled`: if true, the drop shadow will be enabled. ``` fun isDropShadowEnabled(block: DesignBlock): Boolean ``` Query if the drop shadow of the given design block is enabled. * `block`: the block whose drop shadow should be queried. * Returns true if the block’s drop shadow is enabled, false otherwise. ``` fun setDropShadowColor( block: DesignBlock, color: Color,) ``` Set the drop shadow color of the given design block. Required scope: “appearance/shadow” * `block`: the block whose drop shadow color should be set. * `color`: the color to set. ``` fun getDropShadowColor(block: DesignBlock): Color ``` Get the drop shadow color of the given design block. * `block`: the block whose drop shadow color should be queried. * Returns the drop shadow color. ``` fun setDropShadowOffsetX( block: DesignBlock, offsetX: Float,) ``` Set the drop shadow’s x offset of the given design block. Required scope: “appearance/shadow” * `block`: the block whose drop shadow’s x offset should be set. * `offsetX`: the x offset to be set. ``` fun setDropShadowOffsetY( block: DesignBlock, offsetY: Float,) ``` Set the drop shadow’s y offset of the given design block. Required scope: “appearance/shadow” * `block`: the block whose drop shadow’s y offset should be set. * `offsetY`: the y offset to be set. ``` fun getDropShadowOffsetX(block: DesignBlock): Float ``` Get the drop shadow’s x offset of the given design block. * `block`: the block whose drop shadow’s x offset should be queried. * Returns the offset. ``` fun getDropShadowOffsetY(block: DesignBlock): Float ``` Get the drop shadow’s y offset of the given design block. * `block`: the block whose drop shadow’s y offset should be queried. * Returns the offset. ``` fun setDropShadowBlurRadiusX( block: DesignBlock, blurRadiusX: Float,) ``` Set the drop shadow’s blur radius on the x axis of the given design block. Required scope: “appearance/shadow” * `block`: the block whose drop shadow’s blur radius on the x axis should be set. * `blurRadiusX`: the blur radius to be set. ``` fun setDropShadowBlurRadiusY( block: DesignBlock, blurRadiusY: Float,) ``` Set the drop shadow’s blur radius on the y axis of the given design block. Required scope: “appearance/shadow” * `block`: the block whose drop shadow’s blur radius on the y axis should be set. * `blurRadiusY`: the blur radius to be set. ``` fun setDropShadowClip( block: DesignBlock, clip: Boolean,) ``` Set the drop shadow’s clipping of the given design block. (Only applies to shapes.) Required scope: “appearance/shadow” * `block`: the block whose drop shadow’s clip should be set. * `clip`: the drop shadow’s clip to be set. ``` fun getDropShadowClip(block: DesignBlock): Boolean ``` Get the drop shadow’s clipping of the given design block. * `block`: the block whose drop shadow’s clipping should be queried. * Returns the drop shadow’s clipping. ``` fun getDropShadowBlurRadiusX(block: DesignBlock): Float ``` Get the drop shadow’s blur radius on the x axis of the given design block. * `block`: the block whose drop shadow’s blur radius on the x axis should be queried. * Returns the blur radius. ``` fun getDropShadowBlurRadiusY(block: DesignBlock): Float ``` Get the drop shadow’s blur radius on the y axis of the given design block. * `block`: the block whose drop shadow’s blur radius on the y axis should be queried. * Returns the blur radius. ## Full Code Here’s the full code: ``` // Configure a basic colored drop shadow if the block supports themif (engine.block.supportsDropShadow(block)) { engine.block.setDropShadowEnabled(block, enabled = true) engine.block.setDropShadowColor(block, Color.fromRGBA(r = 1F, g = 0.75F, b = 0.8F, a = 1F)) val dropShadowColor = engine.block.getDropShadowColor(block) engine.block.setDropShadowOffsetX(block, offsetX = -10F) engine.block.setDropShadowOffsetY(block, offsetY = 5F) val dropShadowOffsetX = engine.block.getDropShadowOffsetX(block); val dropShadowOffsetY = engine.block.getDropShadowOffsetY(block); engine.block.setDropShadowBlurRadiusX(block, blurRadiusX = -10F) engine.block.setDropShadowBlurRadiusY(block, blurRadiusY = 5F) engine.block.setDropShadowClip(block, clip = false) val dropShadowClip = engine.block.getDropShadowClip(block) // Query a blocks drop shadow properties val dropShadowIsEnabled = engine.block.isDropShadowEnabled(block) val dropShadowBlurRadiusX = engine.block.getDropShadowBlurRadiusX(block) val dropShadowBlurRadiusY = engine.block.getDropShadowBlurRadiusY(block)} ``` --- [Source](https:/img.ly/docs/cesdk/android/outlines/overview-dfeb12) # Overview In CreativeEditor SDK (CE.SDK), _outlines_ refer to visual enhancements added around design elements. They include strokes, shadows, and glows, each serving to emphasize, separate, or stylize content. Outlines help improve visibility, create visual contrast, and enhance the overall aesthetic of a design. You can add, edit, and remove outlines both through the CE.SDK user interface and programmatically via the API. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Understanding Outlines * **Stroke (Outline):** A solid line that directly traces the border of an element. Strokes can vary in thickness, color, and style (such as dashed or dotted lines). * **Shadow:** A duplicate of the element rendered with an offset and blur effect, creating the illusion of depth. * **Glow:** A soft, diffused light that radiates outward from the element, typically used to create a luminous or halo effect. We don’t support `glow` directly in our API, however, it can be achieved by using a brightly-colored shadow. Each type of outline offers different visual styles and purposes, allowing you to tailor your design’s look and feel. ## Supported Elements You can apply outlines to a wide range of elements in CE.SDK, including: * Text elements * Shapes * Images * Stickers Some asset types or highly customized components may have limitations on which outline effects they support. Always check element capabilities if outline options appear unavailable. ## UI Editing In the CE.SDK user interface, you can: * **Add outlines** by selecting an element and enabling stroke, shadow, or glow options in the properties panel. * **Edit outline properties** such as color, thickness, opacity, blur radius, and offset directly through the UI controls. * **Remove outlines** by disabling the stroke, shadow, or glow for a selected element. These tools allow designers to quickly apply and adjust outlines without needing to write code. ## Programmatic Editing Developers can also manage outlines programmatically using the CE.SDK API. This includes: * **Accessing and modifying properties** such as stroke color, stroke width, shadow blur, and shadow offset. * **Enabling or disabling outlines** for individual design blocks. * **Removing outlines** programmatically by disabling stroke or shadow effects on a block. Programmatic control enables dynamic styling and automation for design generation workflows. ## Customizing Outline Properties Outlines in CE.SDK offer a variety of customizable properties to fit different design needs: * **Color:** Define the stroke or glow color to match branding or design themes. * **Thickness (Stroke Width):** Adjust how bold or subtle the stroke appears around the element. * **Opacity:** Control the transparency of strokes, shadows, or glows for subtle or strong effects. * **Blur (for Shadows and Glows):** Soften the appearance of shadows or glows by adjusting their blur radius. * **Offset (for Shadows):** Set how far a shadow is displaced from the element to control the sense of depth. --- [Source](https:/img.ly/docs/cesdk/android/insert-media/position-and-align-cc6b6a) # Positioning and Alignment ``` val x = engine.block.getPositionX(block)val xMode = engine.block.getPositionXMode(block)val y = engine.block.getPositionY(block)val yMode = engine.block.getPositionYMode(block)engine.block.setPositionX(block, value = 0.25F)engine.block.setPositionXMode(block, mode = PositionMode.PERCENT)engine.block.setPositionY(block, value = 0.25)engine.block.setPositionYMode(block, mode = PositionMode.PERCENT) val rad = engine.block.getRotation(block)engine.block.setRotation(block, radians = PI.toFloat())val isFlipHorizontal = engine.block.isFlipHorizontal(block)val isFlipVertical = engine.block.isFlipVertical(block)engine.block.setFlipHorizontal(block, flip = true)engine.block.setFlipVertical(block, flip = false) val width = engine.block.getWidth(block)val widthMode = engine.block.getWidthMode(block)val height = engine.block.getHeight(block)val heightMode = engine.block.getHeightMode(block)engine.block.setWidth(block, value = 0.5F)engine.block.setWidth(block, value = 2.5F, maintainCrop = true)engine.block.setWidthMode(block, mode = SizeMode.PERCENT)engine.block.setHeight(block, value = 0.5F)engine.block.setHeight(block, value = 2.5F, maintainCrop = true)engine.block.setHeightMode(block, mode = SizeMode.PERCENT)val frameX = engine.block.getFrameX(block)val frameY = engine.block.getFrameY(block)val frameWidth = engine.block.getFrameWidth(block)val frameHeight = engine.block.getFrameHeight(block) engine.block.setAlwaysOnTop(block, false)val isAlwaysOnTop = engine.block.isAlwaysOnTop(block)engine.block.setAlwaysOnBottom(block, false)val isAlwaysOnBottom = engine.block.isAlwaysOnBottom(block)engine.block.bringToFront(block)engine.block.sendToBack(block)engine.block.bringForward(block)engine.block.sendBackward(block) val globalX = engine.block.getGlobalBoundingBoxX(block)val globalY = engine.block.getGlobalBoundingBoxY(block)val globalWidth = engine.block.getGlobalBoundingBoxWidth(block)val globalHeight = engine.block.getGlobalBoundingBoxHeight(block)val screenSpaceRect = engine.block.getScreenSpaceBoundingBoxRect(listOf(block)) engine.block.scale(block, scale = 2F, anchorX = 0.5F, anchorY = 0.5F) engine.block.fillParent(block) val pages = engine.scene.getPages()engine.block.resizeContentAware(pages, width = 100F, height = 100F) // Create blocks and append to sceneval member1 = engine.block.create(DesignBlockType.Graphic)val member2 = engine.block.create(DesignBlockType.Graphic)engine.block.appendChild(scene, child = member1)engine.block.appendChild(scene, child = member2)if (engine.block.isDistributable(listOf(member1, member2))) { engine.block.distributeHorizontally(listOf(member1, member2)) engine.block.distributeVertically(listOf(member1, member2))}if (engine.block.isAlignable(listOf(member1, member2))) { engine.block.alignHorizontally(listOf(member1, member2), alignment = HorizontalBlockAlignment.LEFT) engine.block.alignVertically(listOf(member1, member2), alignment = VerticalBlockAlignment.TOP)} val isTransformLocked = engine.block.isTransformLocked(block)if (!isTransformLocked) { engine.block.setTransformLocked(block, locked = true)} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify scenes layout through the `block` API. ## Layout of Blocks **Note on layout and frame size** The frame size is determined during the layout phase of the render process inside the engine. This means that calling `getFrameSize()` immediately after modifying the scene might return an inaccurate result. The CreativeEngine supports three different modes for positioning blocks. These can be set for each block and both coordinates independently: * `Absolute`: the position value is interpreted in the scene’s current design unit. * `Percent`: the position value is interpreted as percentage of the block’s parent’s size, where 1F means 100%. * `Auto` : the position is automatically determined. Likewise there are also three different modes for controlling a block’s size. Again both dimensions can be set independently: * `Absolute`: the size value is interpreted in the scene’s current design unit. * `Percent`: the size value is interpreted as percentage of the block’s parent’s size, where 1F means 100%. * `Auto` : the block’s size is automatically determined by the size of the block’s content. ### Positioning ``` fun getPositionX(block: DesignBlock): Float ``` Query a block’s x position. * `block`: the block to query. * Returns the value of the x position. ``` fun getPositionY(block: DesignBlock): Float ``` Query a block’s y position. * `block`: the block to query. * Returns the value of the y position. ``` fun getPositionXMode(block: DesignBlock): PositionMode ``` Query a block’s mode for its x position. * `block`: the block to query. * Returns the current mode for the x position. ``` fun getPositionYMode(block: DesignBlock): PositionMode ``` Query a block’s mode for its y position. * `block`: the block to query. * Returns the current mode for the y position. ``` fun setPositionX( block: DesignBlock, value: Float,) ``` Update a block’s x position. The position refers to the block’s local space, relative to its parent with the origin at the top left. Required scope: “layer/move” * `block`: the block to update. * `value`: the value of the x position. ``` fun setPositionY( block: DesignBlock, value: Float,) ``` Update a block’s y position. The position refers to the block’s local space, relative to its parent with the origin at the top left. Required scope: “layer/move” * `block`: the block to update. * `value`: the value of the y position. ``` fun setPositionXMode( block: DesignBlock, mode: PositionMode,) ``` Set a block’s mode for its x position. The position refers to the block’s local space, relative to its parent with the origin at the top left. Required scope: “layer/move” * `block`: the block to update. * `mode`: the x position mode. ``` fun setPositionYMode( block: DesignBlock, mode: PositionMode,) ``` Set a block’s mode for its y position. The position refers to the block’s local space, relative to its parent with the origin at the top left. Required scope: “layer/move” * `block`: the block to update. * `mode`: the y position mode. ### Size ``` fun getWidth(block: DesignBlock): Float ``` Query a block’s width. * `block`: the block to query. * Returns the value of the block’s width. ``` fun getWidthMode(block: DesignBlock): SizeMode ``` Query a block’s mode for its width. * `block`: the block to query. * Returns the current mode for the width. ``` fun getHeight(block: DesignBlock): Float ``` Query a block’s height. * `block`: the block to query. * Returns the value of the block’s height. ``` fun getHeightMode(block: DesignBlock): SizeMode ``` Query a block’s mode for its height. * `block`: the block to query. * Returns the current mode for the height. ``` fun setWidth( block: DesignBlock, value: Float, maintainCrop: Boolean = false,) ``` Update a block’s width and optionally maintain the crop. If the crop is maintained, the crop values will be automatically adjusted. The content fill mode `Cover` is only kept if the `features/transformEditsRetainCoverMode` setting is enabled, otherwise it will change to `Crop`. Required scope: “layer/resize” * `block`: the block to update. * `value`: the new width of the block. * `maintainCrop`: whether or not the crop values, if available, should be automatically adjusted. ``` fun setWidthMode( block: DesignBlock, mode: SizeMode,) ``` Set a block’s mode for its width. Required scope: “layer/resize” * `block`: the block to update. * `mode`: the width mode. ``` fun setHeight( block: DesignBlock, value: Float, maintainCrop: Boolean = false,) ``` Update a block’s height and optionally maintain the crop. If the crop is maintained, the crop values will be automatically adjusted. The content fill mode `Cover` is only kept if the `features/transformEditsRetainCoverMode` setting is enabled, otherwise it will change to `Crop`. Required scope: “layer/resize” * `block`: the block to update. * `value`: the new height of the block. * `maintainCrop`: whether or not the crop values, if available, should be automatically adjusted. ``` fun setHeightMode( block: DesignBlock, mode: SizeMode,) ``` Set a block’s mode for its height. Required scope: “layer/resize” * `block`: the block to update. * `mode`: the height mode. ### Rotation ``` fun getRotation(block: DesignBlock): Float ``` Query a block’s rotation in radians. * `block`: the block to query. * Returns the block’s rotation around its center in radians. ``` fun setRotation( block: DesignBlock, radians: Float,) ``` Update a block’s rotation. Required scope: “layer/rotate” * `block`: the block to update. * `radians`: the new rotation in radians. Rotation is applied around the block’s center. ### Flipping ``` fun setFlipHorizontal( block: DesignBlock, flip: Boolean,) ``` Update a block’s horizontal flip. Required scope: “layer/flip” * `block`: the block to update. * `flip`: if the flip should be enabled. ``` fun isFlipHorizontal(block: DesignBlock): Boolean ``` Query a block’s horizontal flip state. * `block`: the block to query. * Returns a boolean indicating for whether the block is flipped in the queried direction. ``` fun setFlipVertical( block: DesignBlock, flip: Boolean,) ``` Update a block’s vertical flip. Required scope: “layer/flip” * `block`: the block to update. * `flip`: if the flip should be enabled. ``` fun isFlipVertical(block: DesignBlock): Boolean ``` Query a block’s vertical flip state. * `block`: the block to query. * Returns a boolean indicating for whether the block is flipped in the queried direction. ### Scaling ``` fun scale( block: DesignBlock, scale: Float, @FloatRange(from = 0.0, to = 1.0) anchorX: Float, @FloatRange(from = 0.0, to = 1.0) anchorY: Float,) ``` Scales the block and all of its children proportionally around the specified relative anchor point. This updates the position, size and style properties (e.g. stroke width) of the block and its children. Required scope: “layer/resize” * `block`: the block that should be scaled. * `scale`: the scale factor to be applied to the current properties of the block. * `anchorX`: the relative position along the width of the block around which the scaling should occur. (0F = left edge, 0.5F = center, 1F = right edge) * `anchorY`: the relative position along the height of the block around which the scaling should occur. (0F = top edge, 0.5F = center, 1F = bottom edge) ### Fill a Block’s Parent ``` fun fillParent(block: DesignBlock) ``` Resize and position a block to entirely fill its parent block. The crop values of the block, except for the flip and crop rotation, are reset if it can be cropped. If the size of the block’s fill is unknown, the content fill mode is changed from `Crop` to `Cover` to prevent invalid crop values. Required scope: “layer/move” * “layer/resize” * `block`: The block that should fill its parent. ### Resize Blocks Content-aware ``` fun resizeContentAware( blocks: List, width: Float, height: Float,) ``` Resize all blocks to the given size. The content of the blocks is automatically adjusted to fit the new dimensions. Required scope: “layer/resize” * `blocks`: The blocks to resize. * `width`: The new width of the blocks. * `height`: The new height of the blocks. ### Even Distribution ``` fun isDistributable(blocks: List): Boolean ``` Confirms that a given set of blocks can be distributed. * `blocks`: a non-empty array of block ids. * Returns whether the blocks can be distributed. ``` fun distributeHorizontally(blocks: List) ``` Distribute multiple blocks vertically within their bounding box so that the space between them is even. Required scope: “layer/move” * `blocks`: a non-empty array of block ids that should be distributed. ``` fun distributeVertically(blocks: List) ``` Distribute multiple blocks vertically within their bounding box so that the space between them is even. Required scope: “layer/move” * `blocks`: a non-empty array of block ids that should be distributed. ### Alignment ``` fun isAlignable(blocks: List): Boolean ``` Confirms that a given set of blocks can be aligned. * `blocks`: a non-empty array of block ids. * Returns whether the blocks can be aligned. ``` fun alignHorizontally( blocks: List, alignment: HorizontalBlockAlignment,) ``` Align multiple blocks vertically within their bounding box or a single block to its parent. Required scope: “layer/move” * `blocks`: a non-empty array of block ids that should be aligned. * `alignment`: How they should be aligned: left, right, or center ``` fun alignVertically( blocks: List, alignment: VerticalBlockAlignment,) ``` Align multiple blocks vertically within their bounding box or a single block to its parent. Required scope: “layer/move” * `blocks`: a non-empty array of block ids that should be aligned. * `alignment`: How they should be aligned: top, bottom, or center ### Computed Dimensions ``` fun getFrameX(block: DesignBlock): Float ``` Get a block’s layout position on the x-axis. The layout position is only available after an internal update loop, which may not happen immediately. * `block`: the block to query. * Returns the layout position. ``` fun getFrameY(block: DesignBlock): Float ``` Get a block’s layout position on the y-axis. The layout position is only available after an internal update loop, which may not happen immediately. * `block`: the block to query. * Returns the layout position. ``` fun getFrameWidth(block: DesignBlock): Float ``` Get a block’s layout width. The layout width is only available after an internal update loop, which may not happen immediately. * `block`: the block to query. * Returns the layout width. ``` fun getFrameHeight(block: DesignBlock): Float ``` Get a block’s layout height. The layout height is only available after an internal update loop, which may not happen immediately. * `block`: the block to query. * Returns the layout height. ``` fun getGlobalBoundingBoxX(block: DesignBlock): Float ``` Get the x position of the block’s axis-aligned bounding box in the scene’s global coordinate space. The scene’s global coordinate space has its origin at the top left. * `block`: the block to query. * Returns the x coordinate of the position of the axis-aligned bounding box. ``` fun getGlobalBoundingBoxY(block: DesignBlock): Float ``` Get the y position of the block’s axis-aligned bounding box in the scene’s global coordinate space. The scene’s global coordinate space has its origin at the top left. * `block`: the block to query. * Returns the y coordinate of the position of the axis-aligned bounding box. ``` fun getGlobalBoundingBoxWidth(block: DesignBlock): Float ``` Get the width of the block’s axis-aligned bounding box in the scene’s global coordinate space. The scene’s global coordinate space has its origin at the top left. * `block`: the block to query. * Returns the width of the axis-aligned bounding box. ``` fun getGlobalBoundingBoxHeight(block: DesignBlock): Float ``` Get the height of the block’s axis-aligned bounding box in the scene’s global coordinate space. The scene’s global coordinate space has its origin at the top left. * `block`: the block to query. * Returns the height of the axis-aligned bounding box. ``` fun getScreenSpaceBoundingBoxRect(blocks: List): RectF ``` Get the position and size of the axis-aligned bounding box for the given blocks in screen space. * `blocks`: the blocks whose bounding box should be calculated. * Returns the position and size of the bounding box as RectF. ### Layers ``` fun setAlwaysOnTop( block: DesignBlock, enabled: Boolean,) ``` Update the block’s always-on-top property. If true, this blocks’s global sorting order is automatically adjusted to be higher than all other siblings without this property. If more than one block is set to be always-on-top, the child order decides which is on top. * `block`: the block to update. * `enabled`: whether the block shall be always-on-top. ``` fun isAlwaysOnTop(block: DesignBlock): Boolean ``` Query a block’s always-on-top property. * `block`: the block to query. * Returns true if the block is set to be always-on-top, false otherwise. ``` fun setAlwaysOnBottom( block: DesignBlock, enabled: Boolean,) ``` Update the block’s always-on-bottom property. If true, this blocks’s global sorting order is automatically adjusted to be lower than all other siblings without this property. If more than one block is set to be always-on-bottom, the child order decides which is on the bottom. * `block`: the block to update. * `enabled`: whether the block shall be always-on-bottom. ``` fun isAlwaysOnBottom(block: DesignBlock): Boolean ``` Query a block’s always-on-bottom property. * `block`: the block to query. * Returns true if the block is set to be always-on-bottom, false otherwise. ``` fun bringToFront(block: DesignBlock) ``` Updates the sorting order of this block and all of its manually created siblings so that the given block has the highest sorting order. If the block is parented to a track, it is first moved up in the hierarchy. * `block`: the block to be given the highest sorting order among its siblings. ``` fun sendToBack(block: DesignBlock) ``` Updates the sorting order of this block and all of its manually created siblings so that the given block has the lowest sorting order. If the block is parented to a track, it is first moved up in the hierarchy. * `block`: the block to be given the lowest sorting order among its siblings. ``` fun bringForward(block: DesignBlock) ``` Updates the sorting order of this block and all of its superjacent siblings so that the given block has a higher sorting order than the next superjacent sibling. If the block is parented to a track, it is first moved up in the hierarchy. Empty tracks and empty groups are passed by. * `block`: the block to be given a higher sorting than the next superjacent sibling. ``` fun sendBackward(block: DesignBlock) ``` Updates the sorting order of this block and all of its manually created and subjacent siblings so that the given block will have a lower sorting order than the next subjacent sibling. If the block is parented to a track, it is first moved up in the hierarchy. Empty tracks and empty groups are passed by. * `block`: the block to be given a lower sorting order than the next subjacent sibling. ### Transform Locking You can lock the transform of a block to prevent changes to any of its transformations. That is the block’s position, rotation, scale, and sizing. ``` fun isTransformLocked(block: DesignBlock): Boolean ``` Query a block’s transform locked state. If `true`, the block’s transform can’t be changed. * `block`: the block to query. * Returns true if block is locked, false otherwise. ``` fun setTransformLocked( block: DesignBlock, locked: Boolean,) ``` Update a block’s transform locked state. * `block`: the block to update. * `locked`: whether the block’s transform should be locked. ## Full Code Here’s the full code: ``` val x = engine.block.getPositionX(block)val xMode = engine.block.getPositionXMode(block)val y = engine.block.getPositionY(block)val yMode = engine.block.getPositionYMode(block)engine.block.setPositionX(block, value = 0.25F)engine.block.setPositionXMode(block, mode = PositionMode.PERCENT)engine.block.setPositionY(block, value = 0.25)engine.block.setPositionYMode(block, mode = PositionMode.PERCENT) val rad = engine.block.getRotation(block)engine.block.setRotation(block, radians = PI.toFloat())val isFlipHorizontal = engine.block.isFlipHorizontal(block)val isFlipVertical = engine.block.isFlipVertical(block)engine.block.setFlipHorizontal(block, flip = true)engine.block.setFlipVertical(block, flip = false) val width = engine.block.getWidth(block)val widthMode = engine.block.getWidthMode(block)val height = engine.block.getHeight(block)val heightMode = engine.block.getHeightMode(block)engine.block.setWidth(block, value = 0.5F)engine.block.setWidth(block, value = 2.5F, maintainCrop = true)engine.block.setWidthMode(block, mode = SizeMode.PERCENT)engine.block.setHeight(block, value = 0.5F)engine.block.setHeight(block, value = 2.5F, maintainCrop = true)engine.block.setHeightMode(block, mode = SizeMode.PERCENT)val frameX = engine.block.getFrameX(block)val frameY = engine.block.getFrameY(block)val frameWidth = engine.block.getFrameWidth(block)val frameHeight = engine.block.getFrameHeight(block) engine.block.setAlwaysOnTop(block, false)val isAlwaysOnTop = engine.block.isAlwaysOnTop(block)engine.block.setAlwaysOnBottom(block, false)val isAlwaysOnBottom = engine.block.isAlwaysOnBottom(block)engine.block.bringToFront(block)engine.block.sendToBack(block)engine.block.bringForward(block)engine.block.sendBackward(block) val globalX = engine.block.getGlobalBoundingBoxX(block)val globalY = engine.block.getGlobalBoundingBoxY(block)val globalWidth = engine.block.getGlobalBoundingBoxWidth(block)val globalHeight = engine.block.getGlobalBoundingBoxHeight(block)val screenSpaceRect = engine.block.getScreenSpaceBoundingBoxRect(listOf(block)) engine.block.scale(block, scale = 2F, anchorX = 0.5F, anchorY = 0.5F) engine.block.fillParent(block) val pages = engine.scene.getPages()engine.block.resizeContentAware(pages, width = 100F, height = 100F) // Create blocks and append to sceneval member1 = engine.block.create(DesignBlockType.Graphic)val member2 = engine.block.create(DesignBlockType.Graphic)engine.block.appendChild(scene, child = member1)engine.block.appendChild(scene, child = member2)if (engine.block.isDistributable(listOf(member1, member2))) { engine.block.distributeHorizontally(listOf(member1, member2)) engine.block.distributeVertically(listOf(member1, member2))}if (engine.block.isAlignable(listOf(member1, member2))) { engine.block.alignHorizontally(listOf(member1, member2), alignment = HorizontalBlockAlignment.LEFT) engine.block.alignVertically(listOf(member1, member2), alignment = VerticalBlockAlignment.TOP)} val isTransformLocked = engine.block.isTransformLocked(block)if (!isTransformLocked) { engine.block.setTransformLocked(block, locked = true)} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/source-sets-5679c8) # Source Sets ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Assetimport ly.img.engine.AssetContextimport ly.img.engine.AssetDefinitionimport ly.img.engine.AssetPayloadimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeTypeimport ly.img.engine.Source fun sourceSets( 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.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( block = page, paddingLeft = 50F, paddingTop = 50F, paddingRight = 50F, paddingBottom = 50F, ) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"), width = 1024, height = 683, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"), width = 2048, height = 1366, ), ), ) engine.block.setFill(block = block, fill = imageFill) engine.block.appendChild(parent = page, child = block) val assetWithSourceSet = AssetDefinition( id = "my-image", meta = mapOf( "kind" to "image", "fillType" to "//ly.img.ubq/fill/image", ), payload = AssetPayload( sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"), width = 1024, height = 683, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"), width = 2048, height = 1366, ), ), ), ) engine.asset.addLocalSource( sourceId = "my-dynamic-images", supportedMimeTypes = listOf("image/jpeg"), ) engine.asset.addAsset(sourceId = "my-dynamic-images", asset = assetWithSourceSet) // Could also acquire the asset using `findAssets` on the source val asset = Asset( id = assetWithSourceSet.id, meta = assetWithSourceSet.meta?.toMap(), context = AssetContext(sourceId = "my-dynamic-images"), ) val result = engine.asset.defaultApplyAsset(asset) val videoFill = engine.block.createFill(FillType.Video) engine.block.setSourceSet( block = videoFill, property = "fill/video/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/example-assets/sourceset/1x.mp4"), width = 720, height = 1280, ), ), ) engine.block.addVideoFileUriToSourceSet( block = videoFill, property = "fill/video/sourceSet", uri = "https://img.ly/static/example-assets/sourceset/2x.mp4", ) engine.stop()} ``` Source sets allow specifying an entire set of sources, each with a different size, that should be used for drawing a block. The appropriate source is then dynamically chosen based on the current drawing size. This allows using the same scene to render a preview on a mobile screen using a small image file and a high-resolution file for print in the backend. This guide will show you how to specify source sets both for existing blocks and when defining assets. ### Drawing When an image needs to be drawn, the current drawing size in screen pixels is calculated and the engine looks up the most appropriate source file to draw at that resolution. 1. If a source set is set, the source with the closest size exceeding the drawing size is used 2. If no source set is set, the full resolution image is downscaled to a maximum edge length of 4096 (configurable via `maxImageSize` setting) and drawn to the target area This also gives more control about up- and downsampling to you, as all intermediate resolutions can be generated using tooling of your choice. **Source sets are also used during export of your designs and will resolve to the best matching asset for the export resolution.** ## Setup the scene We first create a new scene with a new page. ``` 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( block = page, paddingLeft = 50F, paddingTop = 50F, paddingRight = 50F, paddingBottom = 50F, ) ``` ## Using a Source Set for an existing Block To make use of a source set for an existing image fill, we use the `setSourceSet` API. This defines a set of sources and specifies height and width for each of these sources. The engine then chooses the appropriate source during drawing. You may query an existing source set using `getSourceSet`. You can add sources to an existing source set with `addImageFileURIToSourceSet`. ``` val block = engine.block.create(DesignBlockType.Graphic)engine.block.setShape(block, engine.block.createShape(ShapeType.Rect))val imageFill = engine.block.createFill(FillType.Image)engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"), width = 1024, height = 683, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"), width = 2048, height = 1366, ), ),)engine.block.setFill(block = block, fill = imageFill)engine.block.appendChild(parent = page, child = block) ``` ## Using a Source Set in an Asset For assets, source sets can be defined in the `payload.sourceSet` field. This is directly translated to the `sourceSet` property when applying the asset. The resulting block is configured in the same way as the one described above. The code demonstrates how to add an asset that defines a source set to a local source and how `applyAsset` handles a populated `payload.sourceSet`. ``` val assetWithSourceSet = AssetDefinition( id = "my-image", meta = mapOf( "kind" to "image", "fillType" to "//ly.img.ubq/fill/image", ), payload = AssetPayload( sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"), width = 1024, height = 683, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"), width = 2048, height = 1366, ), ), ),) ``` ## Video Source Sets Source sets can also be used for video fills. This is done by setting the `sourceSet` property of the video fill. The engine will then use the source with the closest size exceeding the drawing size. Thumbnails will use the smallest source if `features/matchThumbnailSourceToFill` is disabled, which is the default. For low end devices or scenes with large videos, you can force the preview to always use the smallest source when editing by enabling `features/forceLowQualityVideoPreview`. On export, the highest quality source is used in any case. ``` val videoFill = engine.block.createFill(FillType.Video) engine.block.setSourceSet( block = videoFill, property = "fill/video/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/example-assets/sourceset/1x.mp4"), width = 720, height = 1280, ), ), ) engine.block.addVideoFileUriToSourceSet( block = videoFill, property = "fill/video/sourceSet", uri = "https://img.ly/static/example-assets/sourceset/2x.mp4", ) ``` ## Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Assetimport ly.img.engine.AssetContextimport ly.img.engine.AssetDefinitionimport ly.img.engine.AssetPayloadimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeTypeimport ly.img.engine.Source fun sourceSets( 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( block = page, paddingLeft = 50F, paddingTop = 50F, paddingRight = 50F, paddingBottom = 50F, ) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, engine.block.createShape(ShapeType.Rect)) val imageFill = engine.block.createFill(FillType.Image) engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"), width = 1024, height = 683, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"), width = 2048, height = 1366, ), ), ) engine.block.setFill(block = block, fill = imageFill) engine.block.appendChild(parent = page, child = block) val assetWithSourceSet = AssetDefinition( id = "my-image", meta = mapOf( "kind" to "image", "fillType" to "//ly.img.ubq/fill/image", ), payload = AssetPayload( sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_512x341.jpg"), width = 512, height = 341, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_1024x683.jpg"), width = 1024, height = 683, ), Source( uri = Uri.parse("https://img.ly/static/ubq_samples/sample_1_2048x1366.jpg"), width = 2048, height = 1366, ), ), ), ) engine.asset.addLocalSource( sourceId = "my-dynamic-images", supportedMimeTypes = listOf("image/jpeg"), ) engine.asset.addAsset(sourceId = "my-dynamic-images", asset = assetWithSourceSet) // Could also acquire the asset using `findAssets` on the source val asset = Asset( id = assetWithSourceSet.id, meta = assetWithSourceSet.meta?.toMap(), context = AssetContext(sourceId = "my-dynamic-images"), ) val result = engine.asset.defaultApplyAsset(asset) val videoFill = engine.block.createFill(FillType.Video) engine.block.setSourceSet( block = videoFill, property = "fill/video/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("https://img.ly/static/example-assets/sourceset/1x.mp4"), width = 720, height = 1280, ), ), ) engine.block.addVideoFileUriToSourceSet( block = videoFill, property = "fill/video/sourceSet", uri = "https://img.ly/static/example-assets/sourceset/2x.mp4", ) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/size-limits-c32275) # Size Limits CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio, but there are practical limits to consider based on the user’s device capabilities. ## Image Resolution Limits Constraint Recommendation / Limit **Input Resolution** Maximum input resolution is **4096×4096 pixels**. Images from external sources (e.g., Unsplash) are resized to this size before rendering on the canvas. You can modify this value using the `maxImageSize` setting. **Output Resolution** There is no enforced output resolution limit. Theoretically, the editor supports output sizes up to **16,384×16,384 pixels**. However, practical limits depend on the device’s GPU capabilities and available memory. All image processing in CE.SDK is handled client-side, so these values depend on the **maximum texture size** supported by the user’s hardware. The default limit of 4096×4096 is a safe baseline that works universally. Higher resolutions (e.g., 8192×8192) may work on certain devices but could fail on others during export if the GPU texture size is exceeded. To ensure consistent results across devices, it’s best to test higher output sizes on your target hardware and set conservative defaults in production. ## Video Resolution & Duration Limits Constraint Recommendation / Limit **Resolution** Up to **4K UHD** is supported for **playback** and **export**, depending on the user’s hardware and available GPU resources. For **import**, CE.SDK does not impose artificial limits, but maximum video size is bounded by the **32-bit address space of WebAssembly (wasm32)** and the **browser tab’s memory cap (~2 GB)**. **Frame Rate** 30 FPS at 1080p is broadly supported; 60 FPS and high-res exports benefit from hardware acceleration **Duration** Stories and reels of up to **2 minutes** are fully supported. Longer videos are also supported, but we generally found a maximum duration of **10 minutes** to be a good balance for a smooth editing experience and a pleasant export duration of around one minute on modern hardware. Performance scales with client hardware. For best results with high-resolution or high-frame-rate video, modern CPUs/GPUs with hardware acceleration are recommended. --- [Source](https:/img.ly/docs/cesdk/android/import-media/from-remote-source-b65faf) # Import From Remote Source --- [Source](https:/img.ly/docs/cesdk/android/import-media/overview-84bb23) # Overview In CE.SDK, assets are the building blocks of your creative workflow—whether they’re images, videos, audio, fonts, or templates. They power everything from basic image edits to dynamic, template-driven design generation. This guide gives you a high-level understanding of how to bring assets into CE.SDK, where they can come from, and how to decide on the right strategy for your application. Whether you’re working with local uploads, remote storage, or third-party sources, this guide will help you navigate your options and build an efficient import pipeline. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## What is an Asset? In CE.SDK, an _asset_ refers to any external media that can be inserted into a design scene. Assets are referenced via URIs, and are not stored or hosted by CE.SDK itself. Supported asset types include: * **Images** – JPEG, PNG, WebP, SVG, etc. * **Video** – MP4, WebM * **Audio / Music** – MP3, AAC * **Fonts** – Custom or system fonts * **Stickers** – SVG or PNG elements * **Templates** – Design presets or prebuilt scenes * **Custom Media** – Any content with a valid URI and supported format Assets are used throughout the editor to fill shapes, provide visual or audio decoration, or serve as core scene content. ## Asset Sources in CE.SDK CE.SDK supports a range of asset sources, allowing developers to import media from: * **Local Sources** Assets directly uploaded by the user (e.g., from their device). * **Remote Sources** Media served via external servers or your own backend (e.g., S3, CDN). * **Third-Party Sources** Create custom integrations with Unsplash, Getty, Airtable, etc. * **Dynamic Sources** Assets created or fetched on demand (e.g., generative AI images). **Note:** When a user opens the editor, CE.SDK downloads any referenced assets to the local environment for use within the editing session. ### When to Use Each Asset Source Use Case Recommended Source Type End-users upload personal media Local Source Use your company’s asset library Remote Source Enable searchable stock assets Third-Party Source Fetch real-time AI-generated content Dynamic Source With **Local Asset Sources**, CE.SDK powers search functionality directly in the browser. For **Remote Asset Sources**, searching and indexing are typically offloaded to the server to ensure scalability and performance. **Performance Tip:** If you’re managing **thousands of assets**, it’s best to offload search and filtering to a **remote backend** to avoid performance bottlenecks in the browser. ## Asset Storage and Management in CE.SDK ### No Built-In Storage Provided CE.SDK does **not** host or store assets. You must handle storage using your infrastructure: * Amazon S3 * Google Cloud Storage * Firebase * Your own custom backend ### How It Works * Assets are referenced via **URIs**—not embedded or stored internally. * You implement a **custom asset source**, which returns references to assets via CE.SDK’s API. * Assets are then made available in the **Asset Library** or loaded programmatically. ### Custom Asset Libraries CE.SDK is fully modular. You define how assets are fetched, filtered, categorized, and presented. This works for all asset types: images, videos, audio, and even uploads. ### Uploading Assets CE.SDK supports upload workflows using your own infrastructure: * Use `onUpload` callbacks to trigger uploads. * Refresh asset lists after uploads with `refetchAssetSources`. ### Best Practices Work with your backend team to: * Define where assets are stored and how they’re indexed. * Ensure secure, performant delivery of media files. * Implement custom asset source logic based on CE.SDK’s source API. ## Using and Customizing the Asset Library The built-in Asset Library gives users a way to browse and insert assets visually. You can: * Add or remove asset categories (images, stickers, audio, etc.). * Customize the layout, filters, and tab order. * Show assets from your own remote sources. * Dynamically refresh asset lists when new files are uploaded. ## Default Assets CE.SDK includes a set of default assets (images, templates, stickers, etc.) for testing or bootstrapping a demo. You can: * Use them in development for fast prototyping. * Customize or remove them entirely for production apps. Use `addDefaultAssetSources()` to populate the default library. By default, CE.SDK references assets from IMG.LY’s CDN (cdn.img.ly). This includes asset sources, but also default fonts and font fallbacks. It is highly recommended to [serve the assets](android/serve-assets-b0827c/) from your own servers in a production environment. This will ensure full availability and guarantee that your integration isn’t affected by changes to IMG.LY’s CDN. ## Third-Party Asset Integrations CE.SDK supports integrations with popular content platforms: * **Unsplash** – High-quality free stock images * **Pexels** – Free photos and videos * **Getty Images** – Licensed, premium content * **Airtable** – Structured media from database rows * **Soundstripe** – Music and audio tracks for video scenes You can also integrate any other source using a custom asset source and the standard asset source API. ## File Type Support CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio content. Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. ## Media Constraints ### Image Resolution Limits Constraint Recommendation / Limit **Input Resolution** Maximum input resolution is **4096×4096 pixels**. Images from external sources (e.g., Unsplash) are resized to this size before rendering on the canvas. You can modify this value using the `maxImageSize` setting. **Output Resolution** There is no enforced output resolution limit. Theoretically, the editor supports output sizes up to **16,384×16,384 pixels**. However, practical limits depend on the device’s GPU capabilities and available memory. All image processing in CE.SDK is handled client-side, so these values depend on the **maximum texture size** supported by the user’s hardware. The default limit of 4096×4096 is a safe baseline that works universally. Higher resolutions (e.g., 8192×8192) may work on certain devices but could fail on others during export if the GPU texture size is exceeded. To ensure consistent results across devices, it’s best to test higher output sizes on your target hardware and set conservative defaults in production. ### Video Resolution & Duration Limits Constraint Recommendation / Limit **Resolution** Up to **4K UHD** is supported for **playback** and **export**, depending on the user’s hardware and available GPU resources. For **import**, CE.SDK does not impose artificial limits, but maximum video size is bounded by the **32-bit address space of WebAssembly (wasm32)** and the **browser tab’s memory cap (~2 GB)**. **Frame Rate** 30 FPS at 1080p is broadly supported; 60 FPS and high-res exports benefit from hardware acceleration **Duration** Stories and reels of up to **2 minutes** are fully supported. Longer videos are also supported, but we generally found a maximum duration of **10 minutes** to be a good balance for a smooth editing experience and a pleasant export duration of around one minute on modern hardware. Performance scales with client hardware. For best results with high-resolution or high-frame-rate video, modern CPUs/GPUs with hardware acceleration are recommended. --- [Source](https:/img.ly/docs/cesdk/android/import-media/concepts-5e6197) # Concepts ``` val engine = Engine(id = "ly.img.engine.example")engine.start()engine.bindOffscreen(width = 1080, height = 1920)val coroutineScope = CoroutineScope(Dispatchers.Main) val scene = engine.scene.create()val page = engine.block.create(DesignBlockType.Page)val block = engine.block.create(DesignBlockType.Graphic)engine.block.appendChild(parent = scene, child = page)engine.block.appendChild(parent = page, child = block) val source = UnsplashAssetSource()engine.asset.addSource(source)val localSourceId = "LocalSourceId"engine.asset.addLocalSource(localSourceId, supportedMimeTypes = "image/jpeg")val assetSourceIds = engine.asset.findAllSources() // List [ "ly.img.asset.source.unsplash", "LocalSourceId", ... ]engine.asset.onAssetSourceAdded() .onEach { println("Asset source added: id=$it") } .launchIn(coroutineScope)engine.asset.onAssetSourceRemoved() .onEach { println("Asset source removed: id=$it") } .launchIn(coroutineScope)engine.asset.onAssetSourceUpdated() .onEach { println("Asset source updated: id=$it") } .launchIn(coroutineScope)val mimeTypes = engine.asset.getSourceSupportedMimeTypes(sourceId = "ly.img.asset.source.unsplash") val list = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10))val sortByNewest = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10, sortingOrder = SortingOrder.DESCENDING))val sortById = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10, sortingOrder = SortingOrder.ASCENDING, sortKey = "id"))val sortByMetaKeyValue = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10, sortingOrder = SortingOrder.ASCENDING, sortKey = "someMetaKey"))val search = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "banana", page = 1, perPage = 10)) val documentColors = engine.asset.findAssets( sourceId = "ly.img.scene.colors", query = FindAssetsQuery(query = "", page = 0, perPage = 99999))val colorAsset = documentColors.assets[0] engine.asset.applyAssetSourceAsset(sourceId = source.sourceId, asset = list.assets[0])engine.asset.applyAssetSourceAsset(sourceId = source.sourceId, asset = list.assets[0], block = block)engine.asset.defaultApplyAsset(asset = search.assets[0])engine.asset.defaultApplyAsset(asset = search.assets[0], block = block) val assetDefinition = AssetDefinition(id = "localAssetId",meta = mapOf( "uri" to "https://example.com/localAssetId.jpg", "thumbUri" to "https://example.com/thumbnails/localAssetId.jpg", "kind" to "image", "fillType" to FillType.Image, "width" to "1080", "height" to "1920"))engine.asset.addAsset(sourceId = localSourceId, asset = assetDefinition)engine.asset.removeAsset(sourceId = localSourceId, assetId = assetDefinition.id) engine.asset.assetSourceContentsChanged(sourceId = source.sourceId) engine.asset.removeSource(sourceId = source.sourceId)engine.asset.removeSource(sourceId = localSourceId) val credits = engine.asset.getCredits(sourceId = source.sourceId)val license = engine.asset.getLicense(sourceId = source.sourceId)val groups = engine.asset.getGroups(sourceId = source.sourceId) engine.stop() class UnsplashAssetSource : AssetSource(sourceId = "ly.img.asset.source.unsplash") { override suspend fun getGroups(): List? = null override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/") ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license") ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult { return if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() } private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page, "perPage" to perPage ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$BASE_URL/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = 0 ) } private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page, "perPage" to perPage ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$BASE_URL/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total ) } private suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } } private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { has("description") -> getString("description") has("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, thumbnailUri = getJSONObject("urls").getString("thumb").let { Uri.parse(it) }, width = getInt("width").toFloat(), height = getInt("height").toFloat(), meta = mapOf("uri" to getJSONObject("urls").getString("full")), context = AssetContext(sourceId = "unsplash", createdByRole = ""), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) } ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral") ) companion object { private const val BASE_URL = "" // INSERT YOUR UNSPLASH PROXY URL HERE }} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to manage assets through the `asset` API. To begin working with assets first you need at least one asset source. As the name might imply asset sources provide the engine with assets. These assets then show up in the editor’s asset library. But they can also be independently searched and used to create design blocks. Asset sources can be added dynamically using the `asset` API as we will show in this guide. ## Defining a Custom Asset Source Asset sources need at least an `id` and a `findAssets` function. You may notice `findAssets` method is `suspend`. This way you can use web requests or other long-running operations inside them and return results asynchronously. Let’s go over these member one by one: All functions of `AssetApi` refer to an asset source by its unique `id`. That’s why it has to be mandatory. Trying to add an asset source with an already registered `id` will fail. ``` class UnsplashAssetSource : AssetSource(sourceId = "ly.img.asset.source.unsplash") { ``` ``` abstract suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult ``` Searches assets based on the `query`. * `query`: the object that is used to filter results. * Returns asset search result. ``` abstract suspend fun getGroups(): List? ``` Specifies all the available groups in this asset source. * Returns list of available groups. ``` open val credits: AssetCredits? = null ``` Returns the credits info of this asset source. By default it is null. * Returns the credits info. ``` open val license: AssetLicense? = null ``` Returns the license info of this asset source. By default it is null. * Returns the license info. ## Registering a New Asset Source ``` fun addSource(source: AssetSource) ``` Adds a custom asset source. Its ID has to be unique. * `source`: the asset source. ``` fun addLocalSource( sourceId: String, supportedMimeTypes: List, applyAsset: (suspend (Asset) -> DesignBlock?)? = null, applyAssetToBlock: (suspend (Asset, DesignBlock) -> Unit)? = null,) ``` Adds a local asset source. Its ID has to be unique. * `sourceId`: the id of the new local asset source. * `supportedMimeTypes`: the mime types of assets that are allowed to be added to this local source. * `applyAsset`: an optional callback that can be used to override the default behavior of applying a given asset result to the active scene. * `applyAssetToBlock`: an optional callback that can be used to override the default behavior of applying an asset result to a given block. ``` fun findAllSources(): List ``` Finds all registered asset sources. * Returns a list with the IDs of all registered asset sources. ``` fun removeSource(sourceId: String) ``` Removes an asset source with the given ID. * `sourceId`: the ID to refer to the asset source. ``` fun onAssetSourceAdded(): Flow ``` Subscribe to asset source addition events. * Returns flow of asset source addition events. ``` fun onAssetSourceRemoved(): Flow ``` Subscribe to asset source removal events. * Returns flow of asset source removal events. ``` fun onAssetSourceUpdated(): Flow ``` Subscribe to asset source’s content update events. * Returns flow of asset source’s content update events. ## Finding and Applying Assets The `findAssets` function should return paginated asset results for the given `query`. The asset results have a set of mandatory and optional properties. For a listing with an explanation for each property please refer to the [Integrate a Custom Asset Source](android/import-media/from-remote-source/unsplash-8f31f0/) guide. The properties of the `query` and the pagination mechanism are also explained in this guide. ``` suspend fun findAssets( sourceId: String, query: FindAssetsQuery,): FindAssetsResult ``` Finds assets of a given type in a specific asset source. * `sourceId`: the ID of the asset source. * `query`: all the options to filter the search results by. * Returns the search results. The optional function ‘applyAssetSourceAsset’ is to define the behavior of what to do when an asset gets applied to the scene. You can use the engine’s APIs to do whatever you want with the given asset result. In this case, we always create an image block and add it to the first page we find. If you don’t provide this function the engine’s default behavior is to create a block based on the asset.meta\[“blockType”\] property, add the block to the active page, and sensibly position and size it. ``` suspend fun applyAssetSourceAsset( sourceId: String, asset: Asset,): DesignBlock? ``` Applies an asset to the active scene using custom `AssetSource.applyAsset` function. * `sourceId`: the sourceId of `AssetSource` which `AssetSource.applyAsset` function is going to be invoked. * `asset`: the asset to be applied to the active scene. Normally it’s a single result of a `findAssets` query. * Returns the newly created block or null if no new block is created. ``` suspend fun applyAssetSourceAsset( sourceId: String, asset: Asset, block: DesignBlock,) ``` Applies an asset to the `block` using custom `AssetSource.applyAsset` function. * `sourceId`: the sourceId of `AssetSource` which `AssetSource.applyAsset` function is going to be invoked. * `asset`: the asset that will be applied to the existing block. Normally it’s a single result of a `findAssets` query. * `block`: the block that will be modified by the asset. ``` suspend fun defaultApplyAsset(asset: Asset): DesignBlock? ``` This is the default implementation of applying asset to the active scene. This means a design block is instantiated and configured according to the `Asset.meta` properties. * `asset`: the asset to be applied to the active scene. Normally it’s a single result of a `findAssets` query. * Returns the newly created block or null if no new block is created. ``` suspend fun defaultApplyAsset( asset: Asset, block: DesignBlock,) ``` This is the default implementation of applying `asset` object to an existing `block`. This means it replaces the `block` content with `asset` content, if compatible. * `asset`: the asset that will be applied to the existing block. Normally it’s a single result of a `findAssets` query. * `block`: the block that will be modified by the asset. ``` fun getSourceSupportedMimeTypes(sourceId: String): List ``` Get the asset source’s list of supported mime types. * `sourceId`: the ID of the asset source. * Returns the list of supported mime types of this asset source. ## Document Asset Sources A document color asset source is automatically available that allows listing all colors in the document. This asset source is read-only and is updated when `findAssets` is called. ## Add an Asset ``` fun addAsset( sourceId: String, asset: AssetDefinition,) ``` Adds the given `asset` to a local asset source. * `sourceId`: The local asset source ID that the asset should be added to. * `asset`: the asset that should be added. ## Remove an Asset ``` fun removeAsset( sourceId: String, assetId: String,) ``` Removes the specified asset from its local asset source. * `sourceId`: the ID of the local asset source that currently contains the asset. * `assetId`: the asset id that should be removed. ## Asset Source Content Updates If the contents of your custom asset source change, you can call the `assetSourceContentsChanged` API to later notify all subscribers of the `onAssetSourceUpdated` API. ``` fun assetSourceContentsChanged(sourceId: String) ``` Notifies the engine that the contents of an asset source changed. * `sourceId`: the asset source whose contents changed. ## Groups in Assets ``` suspend fun getGroups(sourceId: String): List? ``` Queries the asset source’s groups for a certain asset type. * `sourceId`: the ID of the asset source. * Returns the asset groups. ## Credits and License ``` fun getCredits(sourceId: String): AssetCredits? ``` Queries the asset source’s credits info. * `sourceId`: the ID of the asset source. * Returns the asset source’s credits info consisting of a name and an optional URL. ``` fun getLicense(sourceId: String): AssetLicense? ``` Queries the asset source’s license info. * `sourceId`: the ID of the asset source. * Returns the asset source’s license info consisting of a name and an optional URL. ## Full Code Here’s the full code: ``` val engine = Engine(id = "ly.img.engine.example")engine.start()engine.bindOffscreen(width = 100, height = 100)val coroutineScope = CoroutineScope(Dispatchers.Main) val scene = engine.scene.create()val page = engine.block.create(DesignBlockType.Page)val block = engine.block.create(DesignBlockType.Graphic)engine.block.appendChild(parent = scene, child = page)engine.block.appendChild(parent = page, child = block) val source = UnsplashAssetSource()engine.asset.addSource(source)val localSourceId = "LocalSourceId"engine.asset.addLocalSource(localSourceId, supportedMimeTypes = "image/jpeg")val assetSourceIds = engine.asset.findAllSources() // List [ "ly.img.asset.source.unsplash", "LocalSourceId", ... ]engine.asset.onAssetSourceAdded() .onEach { println("Asset source added: id=$it") } .launchIn(coroutineScope)engine.asset.onAssetSourceRemoved() .onEach { println("Asset source removed: id=$it") } .launchIn(coroutineScope)engine.asset.onAssetSourceUpdated() .onEach { println("Asset source updated: id=$it") } .launchIn(coroutineScope)val mimeTypes = engine.asset.getSourceSupportedMimeTypes(sourceId = "ly.img.asset.source.unsplash") val list = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10))val sortByNewest = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10, sortingOrder = SortingOrder.DESCENDING))val sortById = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10, sortingOrder = SortingOrder.ASCENDING, sortKey = "id"))val sortByMetaKeyValue = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "", page = 1, perPage = 10, sortingOrder = SortingOrder.ASCENDING, sortKey = "someMetaKey"))val search = engine.asset.findAssets( sourceId = source.sourceId, query = FindAssetsQuery(query = "banana", page = 1, perPage = 10)) val documentColors = engine.asset.findAssets( sourceId = "ly.img.scene.colors", query = FindAssetsQuery(query = "", page = 0, perPage = 99999))val colorAsset = documentColors.assets[0] engine.asset.applyAssetSourceAsset(sourceId = source.sourceId, asset = list.assets[0])engine.asset.applyAssetSourceAsset(sourceId = source.sourceId, asset = list.assets[0], block = block)engine.asset.defaultApplyAsset(asset = search.assets[0])engine.asset.defaultApplyAsset(asset = search.assets[0], block = block) val assetDefinition = AssetDefinition(id = "localAssetId",meta = mapOf( "uri" to "https://example.com/localAssetId.jpg", "thumbUri" to "https://example.com/thumbnails/localAssetId.jpg", "kind" to "image", "fillType" to FillType.Image, "width" to "1080", "height" to "1920"))engine.asset.addAsset(sourceId = localSourceId, asset = assetDefinition)engine.asset.removeAsset(sourceId = localSourceId, assetId = assetDefinition.id) engine.asset.assetSourceContentsChanged(sourceId = source.sourceId) engine.asset.removeSource(sourceId = source.sourceId)engine.asset.removeSource(sourceId = localSourceId) val credits = engine.asset.getCredits(sourceId = source.sourceId)val license = engine.asset.getLicense(sourceId = source.sourceId)val groups = engine.asset.getGroups(sourceId = source.sourceId) engine.stop() class UnsplashAssetSource : AssetSource(sourceId = "ly.img.asset.source.unsplash") { override suspend fun getGroups(): List? = null override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/") ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license") ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult { return if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() } private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page, "perPage" to perPage ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$BASE_URL/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = 0 ) } private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page, "perPage" to perPage ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$BASE_URL/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total ) } private suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } } private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { has("description") -> getString("description") has("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, thumbnailUri = getJSONObject("urls").getString("thumb").let { Uri.parse(it) }, width = getInt("width").toFloat(), height = getInt("height").toFloat(), meta = mapOf("uri" to getJSONObject("urls").getString("full")), context = AssetContext(sourceId = "unsplash", createdByRole = ""), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) } ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral") ) companion object { private const val BASE_URL = "" // INSERT YOUR UNSPLASH PROXY URL HERE }} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/retrieve-mimetype-ed13bf) # Retrieve Mimetype When working with media assets in CE.SDK, it is often necessary to determine the mimetype of a resource before processing it. This guide explains how to use the `getMimeType(uri: Uri)` function to retrieve the mimetype of a given resource. Returns the mimetype of the resources at the given Uri. If the resource is not already downloaded, this function will download it. * `uri:` the Uri of the resource. * Returns the mimetype of the resource. ``` // Get the mimetype of a resourceval mimeType = engine.editor.getMimeType(Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.image/images/sample_1.jpg")) ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/capture-from-camera-92f388) # Capture From Camera --- [Source](https:/img.ly/docs/cesdk/android/import-media/asset-library-65d6c4) # Asset Library --- [Source](https:/img.ly/docs/cesdk/android/get-started/overview-e18f40) # Get Started Welcome to our documentation! This guide will help you get started with our SDK on your preferred platform. ## Choose Your Platform Jetpack Compose Activity-Based UI Fragment-Based UI [→ New Project](android/get-started/new-jetpack-compose-project-c6567i/)[→ Existing Project](android/get-started/existing-project-g0901m/)[→ Clone GitHub Project](android/get-started/clone-github-project-f9890l/) [→ New Project](android/get-started/new-activity-based-ui-project-d7678j/) [→ New Project](android/get-started/new-fragment-based-ui-project-e8789k/) --- [Source](https:/img.ly/docs/cesdk/android/get-started/new-jetpack-compose-project-c6567i) # New JetPack Compose Project ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.showcase" compileSdk 35 defaultConfig { applicationId "ly.img.editor.showcase" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' }} dependencies { implementation "ly.img:engine:1.51.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ## Introduction This guide will walk you through setting up CE.SDK editor in a new Jetpack Compose based project. By the end, you will have a powerful editor running in your new Android app, capable of editing various forms of media. ![CE.SDK editor running on an Android device](/docs/cesdk/_astro/FinalDesignEditorScreen.DjhE6ZAP_ZFTdN6.webp) ## Pre-requisites * Android Studio installed on your machine * CE.SDK license key. [Get one here](https://img.ly/pricing) ## Minimum Requirements * Android: `7.0` (Android SDK 24) * Kotlin `1.9.10` * Jetpack Compose BOM version `2023.05.01` (`1.4.3`) * Supported ABIs: `arm64-v8a`, `armeabi-v7a`, `x86_64` and `x86` ## Create the Project In Android Studio, select `File -> New -> New Project`. Under “Phone and Tablet” choose “Empty Activity”. ![Android Studio template chooser dialog](/docs/cesdk/_astro/ChooseTemplate.CLTOwLC__1uUY24.webp) Fill in desired app details, but ensure that the minimum SDK is set to at least API 24 (Android 7.0). That is the lowest supported version by the CE.SDK editor. ![Android Studio new project dialog](/docs/cesdk/_astro/CreateActivity.hfUpSahs_1wJ6OI.webp) ## Add Dependencies In your new project, let’s add the required dependencies for the CE.SDK editor. Add the IMG.LY repository to your `settings.gradle`: ``` dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name "IMG.LY Artifactory" url "https://artifactory.img.ly/artifactory/maven" mavenContent { includeGroup("ly.img") } } }} ``` Add the CE.SDK SDK dependency to your app’s `build.gradle`: ``` implementation "ly.img:engine:1.51.0" ``` ## Implementation With the dependencies successfully added to the project, you can now add the Editor to your app. Here, we will add the `DesignEditor`, but the process is similar for the other editors. Create a new file where you will add a new Composable called `EditorComposable`. ``` import androidx.compose.runtime.Composableimport ly.img.editor.DesignEditorimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign @Composablefun EditorComposable() { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", ) DesignEditor( engineConfiguration = engineConfiguration, onClose = { // Close the editor here // If using a navigation library, call pop() here } )} ``` Inside your new `EditorComposable`, you need to create `engineConfiguration`, that will then be passed into the `DesignEditor`. Within `onClose`, you can handle closing the editor appropriately. You can now call `EditorComposable` in `MainActivity.kt`. ``` import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdge class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { EditorComposable() } }} ``` ## Common Errors Here are some common errors you may encounter through this guide, and how to solve them. #### Dependency not found ``` > Could not resolve all files for configuration ':app:debugRuntimeClasspath'. > Could not find ly.img:editor:1.48.0. ``` **Solution** -> Make sure maven url is set up correctly in `settings.gradle` #### Invalid License ![Invalid license dialog](/docs/cesdk/_astro/InvalidLicense.nITHsx3P_1XdAj4.webp) ![Missing license dialog](/docs/cesdk/_astro/MissingLicense.BbjNy_Rw_Z2olSxD.webp) **Solution** -> Check whether you have supplied a valid license #### No Internet ![No internet dialog](/docs/cesdk/_astro/NoInternet.4U5h140Q_H2cG2.webp) **Solution** -> Check your internet connection --- [Source](https:/img.ly/docs/cesdk/android/get-started/new-fragment-based-ui-project-e8789k) # New Fragment-Based UI Project ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.showcase" compileSdk 35 defaultConfig { applicationId "ly.img.editor.showcase" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' }} dependencies { implementation "ly.img:engine:1.51.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ## Introduction This guide will walk you through setting up CE.SDK editor in a new Fragment based project. By the end, you will have a powerful editor running in your new Android app, capable of editing various forms of media. ![CE.SDK editor running on an Android device](/docs/cesdk/_astro/FinalDesignEditorScreen.DjhE6ZAP_ZFTdN6.webp) ## Pre-requisites * Android Studio installed on your machine * CE.SDK license key. [Get one here](https://img.ly/pricing) ## Minimum Requirements * Android: `7.0` (Android SDK 24) * Kotlin `1.9.10` * Jetpack Compose BOM version `2023.05.01` (`1.4.3`) * Supported ABIs: `arm64-v8a`, `armeabi-v7a`, `x86_64` and `x86` ## Create the Project In Android Studio, select `File -> New -> New Project`. Under “Phone and Tablet” choose “Empty Views Activity”. ![Android Studio template chooser dialog](/docs/cesdk/_astro/ChooseTemplate.4QvzO1Oy_251lOa.webp) Fill in desired app details, but ensure that the minimum SDK is set to at least API 24 (Android 7.0). That is the lowest supported version by the CE.SDK editor. ![Android Studio new project dialog](/docs/cesdk/_astro/CreateActivity.hfUpSahs_1wJ6OI.webp) ## Add Dependencies & Setup In your new project, let’s add the required dependencies for the CE.SDK editor. Add the IMG.LY repository to your `settings.gradle`: ``` dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name "IMG.LY Artifactory" url "https://artifactory.img.ly/artifactory/maven" mavenContent { includeGroup("ly.img") } } }} ``` Add the CE.SDK SDK dependency to your app’s `build.gradle`. ``` implementation "ly.img:engine:1.51.0" ``` Add Jetpack Compose libraries as well to your app’s `build.gradle`. ``` dependencies { implementation(platform('androidx.compose:compose-bom:2025.04.01')) implementation 'androidx.activity:activity-compose' } ``` Enable Compose features in your project in your app’s `build.gradle` file. If you are running kotlin version less than 2.0.0, also add the compose compiler version. ``` android { buildFeatures { compose true } // Only add this if you have Kotlin version lower than 2.0.0 composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } // ---------} ``` ## Implementation With the dependencies successfully added to the project and Jetpack Compose fully set up, you can now add the Editor to your app. Here, we will add the `DesignEditor`, but the process is similar for the other editors. Create a new file where you will create `EditorFragment` where you will implement the editor: ``` import android.os.Bundleimport androidx.fragment.app.Fragmentimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.compose.ui.platform.ComposeViewimport ly.img.editor.DesignEditorimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign class EditorFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return ComposeView(requireContext()).apply { setContent { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", ) DesignEditor(engineConfiguration = engineConfiguration) { // Close the editor here parentFragmentManager.popBackStack() } } } }} ``` Inside the `setContent` function, you need to create `engineConfiguration`, that will then be passed into the `DesignEditor`. Within `onClose`, you can handle closing the editor appropriately. You can now open the `EditorFragment` fragment to launch the editor. ## Common Errors Here are some common errors you may encounter through this guide, and how to solve them. #### Incompatible compose compiler ``` e: This version of the Compose Compiler requires Kotlin version x.x.xx but you appear to be using Kotlin version y.y.yy which is not known to be compatible. Please fix your configuration (or `suppressKotlinVersionCompatibilityCheck` but don't say I didn't warn you!). ``` **Solution** -> Ensure you have the proper compose compiler version for your project. Check official mappings [here](https://developer.android.com/jetpack/androidx/releases/compose-kotlin) #### Dependency not found ``` > Could not resolve all files for configuration ':app:debugRuntimeClasspath'. > Could not find ly.img:editor:1.48.0. ``` **Solution** -> Make sure maven url is set up correctly in `settings.gradle` #### Invalid License ![Invalid license dialog](/docs/cesdk/_astro/InvalidLicense.nITHsx3P_1XdAj4.webp) ![Missing license dialog](/docs/cesdk/_astro/MissingLicense.BbjNy_Rw_Z2olSxD.webp) **Solution** -> Check whether you have supplied a valid license #### No Internet ![No internet dialog](/docs/cesdk/_astro/NoInternet.4U5h140Q_H2cG2.webp) **Solution** -> Check your internet connection --- [Source](https:/img.ly/docs/cesdk/android/get-started/new-activity-based-ui-project-d7678j) # New Activity-Based UI Project ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.showcase" compileSdk 35 defaultConfig { applicationId "ly.img.editor.showcase" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' }} dependencies { implementation "ly.img:engine:1.51.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ## Introduction This guide will walk you through setting up CE.SDK editor in a new Activity based project. By the end, you will have a powerful editor running in your new Android app, capable of editing various forms of media. ![CE.SDK editor running on an Android device](/docs/cesdk/_astro/FinalDesignEditorScreen.DjhE6ZAP_ZFTdN6.webp) ## Pre-requisites * Android Studio installed on your machine * CE.SDK license key. [Get one here](https://img.ly/pricing) ## Minimum Requirements * Android: `7.0` (Android SDK 24) * Kotlin `1.9.10` * Jetpack Compose BOM version `2023.05.01` (`1.4.3`) * Supported ABIs: `arm64-v8a`, `armeabi-v7a`, `x86_64` and `x86` ## Create the Project In Android Studio, select `File -> New -> New Project`. Under “Phone and Tablet” choose “Empty Views Activity”. ![Android Studio template chooser dialog](/docs/cesdk/_astro/ChooseTemplate.4QvzO1Oy_251lOa.webp) Fill in desired app details, but ensure that the minimum SDK is set to at least API 24 (Android 7.0). That is the lowest supported version by the CE.SDK editor. ![Android Studio new project dialog](/docs/cesdk/_astro/CreateActivity.hfUpSahs_1wJ6OI.webp) ## Add Dependencies & Setup In your new project, let’s add the required dependencies for the CE.SDK editor. Add the IMG.LY repository to your `settings.gradle`: ``` dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name "IMG.LY Artifactory" url "https://artifactory.img.ly/artifactory/maven" mavenContent { includeGroup("ly.img") } } }} ``` Add the CE.SDK SDK dependency to your app’s `build.gradle`. ``` implementation "ly.img:engine:1.51.0" ``` Add Jetpack Compose libraries as well to your app’s `build.gradle`. ``` dependencies { implementation(platform('androidx.compose:compose-bom:2025.04.01')) implementation 'androidx.activity:activity-compose' } ``` Enable Compose features in your project in your app’s `build.gradle` file. If you are running kotlin version less than 2.0.0, also add the compose compiler version. ``` android { buildFeatures { compose true } // Only add this if you have Kotlin version lower than 2.0.0 composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } // ---------} ``` ## Implementation With the dependencies successfully added to the project and Jetpack Compose fully set up, you can now add the Editor to your app. Here, we will add the `DesignEditor`, but the process is similar for the other editors. Create a new file where you will create `EditorActivity` where you will implement the editor ``` import android.os.Bundleimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport ly.img.editor.DesignEditorimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign class EditorActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", ) DesignEditor( engineConfiguration = engineConfiguration, onClose = { // Close the editor here finish() } ) } }} ``` Inside the `setContent` function, you need to create `engineConfiguration`, that will then be passed into the `DesignEditor`. Within `onClose`, you can handle closing the editor appropriately. You can now open the `EditorActivity` activity to launch the editor. ## Common Errors Here are some common errors you may encounter through this guide, and how to solve them. #### Incompatible compose compiler ``` e: This version of the Compose Compiler requires Kotlin version x.x.xx but you appear to be using Kotlin version y.y.yy which is not known to be compatible. Please fix your configuration (or `suppressKotlinVersionCompatibilityCheck` but don't say I didn't warn you!). ``` **Solution** -> Ensure you have the proper compose compiler version for your project. Check official mappings [here](https://developer.android.com/jetpack/androidx/releases/compose-kotlin) #### Dependency not found ``` > Could not resolve all files for configuration ':app:debugRuntimeClasspath'. > Could not find ly.img:editor:1.48.0. ``` **Solution** -> Make sure maven url is set up correctly in `settings.gradle` #### Invalid License ![Invalid license dialog](/docs/cesdk/_astro/InvalidLicense.nITHsx3P_1XdAj4.webp) ![Missing license dialog](/docs/cesdk/_astro/MissingLicense.BbjNy_Rw_Z2olSxD.webp) **Solution** -> Check whether you have supplied a valid license #### No Internet ![No internet dialog](/docs/cesdk/_astro/NoInternet.4U5h140Q_H2cG2.webp) **Solution** -> Check your internet connection --- [Source](https:/img.ly/docs/cesdk/android/get-started/existing-project-g0901m) # Existing Project Setup ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.showcase" compileSdk 35 defaultConfig { applicationId "ly.img.editor.showcase" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' }} dependencies { implementation "ly.img:engine:1.51.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ## Introduction This guide will walk you through setting up CE.SDK editor in your existing Android app. By the end, you will have a powerful editor running in your app, capable of editing photos and videos. ![CE.SDK editor running on an Android device](/docs/cesdk/_astro/FinalDesignEditorScreen.DjhE6ZAP_ZFTdN6.webp) ## Pre-requisites * Existing Android project * Android Studio installed on your machine * CE.SDK license key. [Get one here](https://img.ly/pricing) ## Minimum Requirements * Android: `7.0` (Android SDK 24) * Kotlin `1.9.10` * Jetpack Compose BOM version `2023.05.01` (`1.4.3`) * Supported ABIs: `arm64-v8a`, `armeabi-v7a`, `x86_64` and `x86` ## Add Dependencies & Setup In your existing project, let’s add the required dependencies for the CE.SDK editor. Add the IMG.LY repository to your `settings.gradle`: ``` dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name "IMG.LY Artifactory" url "https://artifactory.img.ly/artifactory/maven" mavenContent { includeGroup("ly.img") } } }} ``` Add the CE.SDK dependency to your app’s `build.gradle`. ``` implementation "ly.img:engine:1.51.0" ``` If not yet present, add Jetpack Compose libraries as well to your app’s `build.gradle`. ``` dependencies { implementation(platform('androidx.compose:compose-bom:2025.04.01')) implementation 'androidx.activity:activity-compose' } ``` Also, make sure Compose features are enabled in your project in your app’s `build.gradle` file. If you are running kotlin version less than 2.0.0, also add the compose compiler version. ``` android { buildFeatures { compose true } // Only add this if you have Kotlin version lower than 2.0.0 composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } // ---------} ``` ## Implementation With the dependencies successfully added to the project and Jetpack Compose fully set up, you can now add the Editor to your app. Here, we will add the `DesignEditor`, but the process is similar for the other editors. Create a new file where you will add a new Composable called `EditorComposable`. ``` import androidx.compose.runtime.Composableimport ly.img.editor.DesignEditorimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign @Composablefun EditorComposable() { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", userId = "", ) DesignEditor( engineConfiguration = engineConfiguration, onClose = { // Close the editor here // If using a navigation library, call pop() here } )} ``` Inside your new `EditorComposable`, you need to create `engineConfiguration`, that will then be passed into the `DesignEditor`. Within `onClose`, you can handle closing the editor appropriately. You can now open the `EditorComposable` from any Compose function, or wrap inside `setContent { }` in any Activity or Fragment. ## Common Errors Here are some common errors you may encounter through this guide, and how to solve them. #### Incompatible compose compiler ``` e: This version of the Compose Compiler requires Kotlin version x.x.xx but you appear to be using Kotlin version y.y.yy which is not known to be compatible. Please fix your configuration (or `suppressKotlinVersionCompatibilityCheck` but don't say I didn't warn you!). ``` **Solution** -> Ensure you have the proper compose compiler version for your project. Check official mappings [here](https://developer.android.com/jetpack/androidx/releases/compose-kotlin) #### Dependency not found ``` > Could not resolve all files for configuration ':app:debugRuntimeClasspath'. > Could not find ly.img:editor:1.48.0. ``` **Solution** -> Make sure maven url is set up correctly in `settings.gradle` #### Invalid License ![Invalid license dialog](/docs/cesdk/_astro/InvalidLicense.nITHsx3P_1XdAj4.webp) ![Missing license dialog](/docs/cesdk/_astro/MissingLicense.BbjNy_Rw_Z2olSxD.webp) **Solution** -> Check whether you have supplied a valid license #### No Internet ![No internet dialog](/docs/cesdk/_astro/NoInternet.4U5h140Q_H2cG2.webp) **Solution** -> Check your internet connection --- [Source](https:/img.ly/docs/cesdk/android/get-started/clone-github-project-f9890l) # Clone GitHub Project 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 * CE.SDK license key. [Get one here](https://img.ly/pricing) ## 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](/docs/cesdk/_astro/CloneDialog.C5pseCTP_Zqehsk.webp) 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 ``` 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](/docs/cesdk/_astro/InvalidLicense.nITHsx3P_1XdAj4.webp) ![Missing license dialog](/docs/cesdk/_astro/MissingLicense.BbjNy_Rw_Z2olSxD.webp) **Solution** -> Check whether you have supplied a valid license #### No Internet ![No internet dialog](/docs/cesdk/_astro/NoInternet.4U5h140Q_H2cG2.webp) **Solution** -> Check your internet connection --- [Source](https:/img.ly/docs/cesdk/android/filters-and-effects/overview-299b15) # Android Filters & Effects SDK 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. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Understanding Filters vs. Effects * **Filters** are broad transformations that modify the entire appearance of an element. Common examples include: * Color adjustments (e.g., brightness, contrast) * Lookup Table (LUT) filters for advanced color grading * Duotone color schemes * **Effects** apply targeted visual modifications to an element. Typical examples include: * Blurring an area to soften details * Sharpening to enhance clarity * Distorting shapes for creative transformations **Key Differences:** Aspect Filters Effects Scope Alters the entire visual appearance Applies a specific modification Examples LUTs, duotone, adjustments Blur, sharpen, distortion Use Case Color grading, mood setting Stylizing or emphasizing parts Understanding the distinction helps you choose the right tool depending on whether you want broad tonal changes or focused visual alterations. ## Supported Filters and Effects CE.SDK offers a range of built-in filters and effects ready to use out of the box. **Available Filters:** * **Adjustment Filters:** Modify brightness, contrast, saturation, and more. * **LUT (Lookup Table) Filters:** Apply professional-grade color transformations. * **Duotone Filters:** Map the shadows and highlights of an image to two custom colors. **Available Effects:** * **Blur:** Soften an area for background separation or artistic focus. * **Sharpen:** Enhance fine details for a crisper appearance. * **Distortion:** Warp the geometry of an element for creative effects. * **Chroma Key (Green Screen):** Remove a background color, often used for compositing images. **Supported Element Types:** * Images * Videos * Shapes * Graphic blocks Before applying a filter or effect programmatically, you can verify support by checking the element type. ## Custom Filters and LUTs CE.SDK supports creating **custom filters** to meet specific visual requirements. One powerful method of creating a custom filter is by using a **LUT (Lookup Table)**: * A LUT is a preset map that transforms input colors to output colors, allowing for sophisticated color grading. * By importing a custom LUT, you can apply unique color styles across your designs consistently. * LUTs are especially useful for achieving cinematic tones, brand color consistency, or specific visual moods. You can create your own LUT files externally and integrate them into CE.SDK, giving you full control over the look and feel of your projects. --- [Source](https:/img.ly/docs/cesdk/android/filters-and-effects/support-a666dd) # Supported Filters and Effects The following tables document the currently available effects and their properties. ## Effects ### `effect/adjustments` Member Type Default Description blacks `float` `0.0` Adjustment of only the blacks. brightness `float` `0.0` Adjustment of the brightness. clarity `float` `0.0` Adjustment of the detail. contrast `float` `0.0` Adjustment of the contrast, the difference in color and light between parts. exposure `float` `0.0` Adjustment of the exposure. gamma `float` `0.0` Gamma correction, non-linear. highlights `float` `0.0` Adjustment of only the highlights. saturation `float` `0.0` Adjustment of the saturation. shadows `float` `0.0` Adjustment of only the shadows. sharpness `float` `0.0` Adjustment of the sharpness. temperature `float` `0.0` Adjustment of the color temperature. whites `float` `0.0` Adjustment of only the whites. ### `effect/black_and_white_color_mixer` Member Type Default Description perceivedBrightnessAqua `float` `0.` Increases or decreases the perceived brightness on the aqua colors. perceivedBrightnessBlue `float` `0.` Increases or decreases the perceived brightness on the blue colors. perceivedBrightnessGreen `float` `0.` Increases or decreases the perceived brightness on the green colors. perceivedBrightnessMagenta `float` `0.` Increases or decreases the perceived brightness on the magenta colors. perceivedBrightnessOrange `float` `0.` Increases or decreases the perceived brightness on the orange colors. perceivedBrightnessPurple `float` `0.` Increases or decreases the perceived brightness on the purple colors. perceivedBrightnessRed `float` `0.` Increases or decreases the perceived brightness on the red colors. perceivedBrightnessYellow `float` `0.` Increases or decreases the perceived brightness on the yellow colors. ### `effect/cross_cut` Distorts the image with horizontal slices. Member Type Default Description offset `float` `0.07` Horizontal offset per slice. slices `float` `5.0` Number of horizontal slices. speedV `float` `0.5` Vertical slice position. time `float` `1.0` Randomness input. ### `effect/dot_pattern` Displays image using a dot matrix. Member Type Default Description blur `float` `0.3` Global blur. dots `float` `30.` Number of dots. size `float` `0.5` Size of an individual dot. ### `effect/duotone_filter` Member Type Default Description darkColor `Color` `{}` The darker of the two colors. Negative filter intensities emphasize this color. intensity `float` `0.0` The mixing weight of the two colors in the range \[-1, 1\]. lightColor `Color` `{}` The brighter of the two colors. Positive filter intensities emphasize this color. ### `effect/extrude_blur` Applies radial blur. Member Type Default Description amount `float` `0.2` Blur intensity. time `float` `0.5` Continuous randomness input. ### `effect/glow` Applies artificial glow. Member Type Default Description amount `float` `0.5` Glow brightness. darkness `float` `0.3` Glow darkness. size `float` `4.0` Intensity. ### `effect/green_screen` Replaces a color with transparency. Member Type Default Description colorMatch `float` `0.4` Threshold between the source color and the from color. Below this threshold, the pixel is made transparent. \[0, 1\] fromColor `Color` `createRGBColor(0., 1., 0., 1.)` The color to be replaced. The alpha channel will be ignored. smoothness `float` `0.08` Controls the rate at which the color transition increases when similarity threshold is exceeded. \[0, 1\] spill `float` `0.0` Controls the desaturation of the source color. This turns the green spill on the subject to gray, making it less jarring. \[0, 1\] ### `effect/half_tone` Overlays halftone pattern. Member Type Default Description angle `float` `0.0` Angle of pattern. scale `float` `0.5` Scale of pattern. ### `effect/hsp_selective_adjustments` Member Type Default Description hueAqua `float` `0.0` Selective hue adjustment applied on the aqua colors. hueBlue `float` `0.0` Selective hue adjustment applied on the blue colors. hueGreen `float` `0.0` Selective hue adjustment applied on the green colors. hueMagenta `float` `0.0` Selective hue adjustment applied on the magenta colors. hueOrange `float` `0.0` Selective hue adjustment applied on the orange colors. huePurple `float` `0.0` Selective hue adjustment applied on the purple colors. hueRed `float` `0.0` Selective hue adjustment applied on the red colors. hueYellow `float` `0.0` Selective hue adjustment applied on the yellow colors. perceivedBrightnessAqua `float` `0.0` Selective perceived brightness adjustment applied on the aqua colors. perceivedBrightnessBlue `float` `0.0` Selective perceived brightness adjustment applied on the blue colors. perceivedBrightnessGreen `float` `0.0` Selective perceived brightness adjustment applied on the green colors. perceivedBrightnessMagenta `float` `0.0` Selective perceived brightness adjustment applied on the magenta colors. perceivedBrightnessOrange `float` `0.0` Selective perceived brightness adjustment applied on the yellow colors. perceivedBrightnessPurple `float` `0.0` Selective perceived brightness adjustment applied on the purple colors. perceivedBrightnessRed `float` `0.0` Selective perceived brightness adjustment applied on the red colors. perceivedBrightnessYellow `float` `0.0` Selective perceived brightness adjustment applied on the orange colors. saturationAqua `float` `0.0` Selective saturation adjustment applied on the aqua colors. saturationBlue `float` `0.0` Selective saturation adjustment applied on the blue colors. saturationGreen `float` `0.0` Selective saturation adjustment applied on the green colors. saturationMagenta `float` `0.0` Selective saturation adjustment applied on the magenta colors. saturationOrange `float` `0.0` Selective saturation adjustment applied on the orange colors. saturationPurple `float` `0.0` Selective saturation adjustment applied on the purple colors. saturationRed `float` `0.0` Selective saturation adjustment applied on the red colors. saturationYellow `float` `0.0` Selective saturation adjustment applied on the yellow colors. ### `effect/linocut` Overlays linocut pattern. Member Type Default Description scale `float` `0.5` Scale of pattern. ### `effect/liquid` Applies a liquefy effect. Member Type Default Description amount `float` `0.06` Severity of the applied effect. scale `float` `0.62` Global scale. speed `float` `0.08` Number of changes per time change. time `float` `0.5` Continuous randomness input. ### `effect/lut_filter` Member Type Default Description horizontalTileCount `int` `5` The horizontal number of tiles contained in the LUT image. intensity `float` `1.0` A value in the range of \[0, 1\]. Defaults to 1.0. lutFileURI `string` `""` The URI to a LUT PNG file. verticalTileCount `int` `5` The vertical number of tiles contained in the LUT image. ### `effect/mask_color` Member Type Default Description maskColor `Color` `createWhite()` All pixel that match this color will be treated as mask pixels. maskMultiplier `Color` `createWhite()` Color multiplier for pixels that match the maskColor. notMaskMultiplier `Color` `createWhite()` Color multiplier for pixels that don’t match the maskColor. replaceInsteadMultiply `vec2` `{}` Blend factors between `x: (maskColor * maskMultiplier) <-> (maskMultiplier)` for pixels matching maskColor `y: (pixelColor * notMaskMultiplier) <-> (notMaskMultiplier)` for pixels not matching maskColor ### `effect/mirror` Mirrors image along center. Member Type Default Description side `int` `1` Axis to mirror along. ### `effect/outliner` Highlights outlines. Member Type Default Description amount `float` `0.5` Intensity of edge highlighting. passthrough `float` `0.5` Visibility of input image in non-edge areas. ### `effect/pixelize` Reduces the image to individual pixels. Member Type Default Description horizontalPixelSize `int` `20` The number of pixels on the x-axis. verticalPixelSize `int` `20` The number of pixels on the y-axis. ### `effect/posterize` Displays image in reduced set of color levels. Member Type Default Description levels `float` `3.` Number of levels. ### `effect/radial_pixel` Reduces the image into radial pixel rows. Member Type Default Description radius `float` `0.1` Radius of an individual row of pixels, relative to the image. segments `float` `0.01` Proportional size of a pixel in each row. ### `effect/recolor` Replaces a color with another color. Member Type Default Description brightnessMatch `float` `1.0` Affects the weight of the brightness when calculating the similarity between source color and from color. This can be useful when recoloring to reach pixels of the same hue and saturation but that are much brighter or darker. \[0, 1\] colorMatch `float` `0.4` Threshold between the source color and the from color. Below this threshold, the pixel is recolored. \[0, 1\] fromColor `Color` `createRGBColor(1., 1., 1., 1.)` The color to be replaced. The alpha channel will be ignored. smoothness `float` `0.08` Controls the rate at which the color transition increases when similarity threshold is exceeded. \[0, 1\] toColor `Color` `createRGBColor(0., 0., 1., 1.)` The color to replace with. ### `effect/sharpie` Cartoon-like effect. No block-specific properties. ### `effect/shifter` Shifts individual color channels. Member Type Default Description amount `float` `0.05` Intensity. angle `float` `0.3` Shift direction. ### `effect/tilt_shift` Applies tilt shift effect. Member Type Default Description amount `float` `0.016` Blur intensity. position `float` `0.4` Horizontal position in image. ### `effect/tv_glitch` Mimics TV banding. Member Type Default Description distortion `float` `3.0` Rough horizontal distortion. distortion2 `float` `1.0` Fine horizontal distortion. rollSpeed `float` `1.0` Vertical offset. speed `float` `2.0` Number of changes per time change. time `float` `0.5` Continuous randomness input. ### `effect/vignette` Adds a vignette. Member Type Default Description darkness `float` `1.0` Brightness of vignette. offset `float` `1.` Radial offset. --- [Source](https:/img.ly/docs/cesdk/android/filters-and-effects/create-custom-lut-filter-6e3f49) # Create a Custom LUT Filter ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.EffectTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport 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 = 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.26.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](content-assets/6e3f49/identity.png) 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.](content-assets/6e3f49/imgly_lut_ad1920_5_5_128.png) ## 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. ``` 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. ``` 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. ``` 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. ``` 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.26.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. ``` engine.block.appendEffect(rect, effectBlock = lutFilter)engine.block.setFill(rect, fill = imageFill) ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.EffectTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport 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/1.51.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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/filters-and-effects/blur-71d642) # Blur ``` // Create and configure a radial blurval 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 blurif (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`. ``` 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` ``` val radialBlur = engine.block.createBlur(type = BlurType.Radial) ``` ## Configuring a Blur You can configure blurs just like you configure design blocks. See [Modify Properties](android/concepts/blocks-90241e/) for more detail. ``` engine.block.setFloat(radialBlur, property = "radial/radius", value = 100F) ``` ## Functions ``` 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. ``` 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. ``` 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. ``` 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. ``` 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: ``` // Create and configure a radial blurval 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 blurif (engine.block.supportsBlur(block)) { val existingBlur = engine.block.getBlur(block)} ``` --- [Source](https:/img.ly/docs/cesdk/android/filters-and-effects/add-c796ba) # Add a Filter or Effect ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.EffectTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport 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 = 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](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](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](android/concepts/blocks-90241e/). We create a scene containing a graphic block with an image fill and want to apply effects to this image. ``` 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. ``` engine.block.supportsEffects(scene) // Returns falseengine.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](android/concepts/blocks-90241e/) for a complete list of supported effect types. ``` 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. ``` 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. ``` // 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. ``` 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](android/concepts/blocks-90241e/) for a complete list of all available properties for each type of effect. ``` 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](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. ``` 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`. ``` engine.block.setEffectEnabled(pixelize, enabled = false)engine.block.setEffectEnabled(pixelize, !engine.block.isEffectEnabled(pixelize)) ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.EffectTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport 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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/fills/overview-3895ee) # Fills ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Colorimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType 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 = 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](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](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. ``` engine.block.supportsFill(scene) // Returns falseengine.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. ``` 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](android/concepts/blocks-90241e/) for a complete list of all available properties for each type of fill. ``` 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. ``` 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. ``` 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`. ``` 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. ``` 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. ``` 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. ``` 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: ``` 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()} ``` ``` // Check if block supports strokesif (engine.block.supportsStroke(block)) { // Enable the stroke engine.block.setStrokeEnabled(block, enabled = true) val strokeIsEnabled = engine.block.isStrokeEnabled(block) // Configure it engine.block.setStrokeColor(block, color = Color.fromRGBA(r = 1F, g = 0.75F, b = 0.8F, a = 1F)) val strokeColor = engine.block.getStrokeColor(block) engine.block.setStrokeWidth(block, width = 5F) val strokeWidth = engine.block.getStrokeWidth(block) engine.block.setStrokeStyle(block, style = StrokeStyle.DASHED) val strokeStyle = engine.block.getStrokeStyle(block) engine.block.setStrokePosition(block, position = StrokePosition.OUTER) val strokePosition = engine.block.getStrokePosition(block) engine.block.setStrokeCornerGeometry(block, geometry = StrokeCornerGeometry.ROUND) val strokeCornerGeometry = engine.block.getStrokeCornerGeometry(block)} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify strokes through the `block` API. Strokes can be added to any shape or text and stroke styles are varying from plain solid lines to dashes and gaps of varying lengths and can have different end caps. ## Strokes ``` fun supportsStroke(block: DesignBlock): Boolean ``` Query if the given block has a stroke property. * `block`: the block to query. * Returns true if the block has a stroke property, false otherwise. ``` fun setStrokeEnabled( block: DesignBlock, enabled: Boolean,) ``` Enable or disable the stroke of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke should be enabled or disabled. * `enabled`: if true, the stroke will be enabled. ``` fun isStrokeEnabled(block: DesignBlock): Boolean ``` Query if the stroke of the given design block is enabled. * `block`: the block whose stroke state should be queried. * Returns true if the block’s stroke is enabled, false otherwise. ``` fun setStrokeColor( block: DesignBlock, color: Color,) ``` Set the stroke color of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke color should be set. * `color`: the color to set. ``` fun getStrokeColor(block: DesignBlock): Color ``` Get the stroke color of the given design block. * `block`: he block whose stroke color should be queried. * Returns the stroke color. ``` fun setStrokeWidth( block: DesignBlock, width: Float,) ``` Set the stroke width of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke width should be set. * `width`: the stroke width to be set. ``` fun getStrokeWidth(block: DesignBlock): Float ``` Get the stroke width of the given design block. * `block`: the block whose stroke width should be queried. * Returns the stroke’s width. ``` fun setStrokeStyle( block: DesignBlock, style: StrokeStyle,) ``` Set the stroke style of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke style should be set. * `style`: the stroke style to be set. ``` fun getStrokeStyle(block: DesignBlock): StrokeStyle ``` Get the stroke style of the given design block. * `block`: the block whose stroke style should be queried. * Returns the stroke’s style. ``` fun setStrokePosition( block: DesignBlock, position: StrokePosition,) ``` Set the stroke position of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke position should be set. * `position`: the stroke position to be set. ``` fun getStrokePosition(block: DesignBlock): StrokePosition ``` Get the stroke position of the given design block. * `block`: the block whose stroke position should be queried. * Returns the stroke position. ``` fun setStrokeCornerGeometry( block: DesignBlock, geometry: StrokeCornerGeometry,) ``` Set the stroke corner geometry of the given design block. Required scope: “stroke/change” * `block`: the block whose stroke join geometry should be set. * `geometry`: the stroke join geometry to be set. ``` fun getStrokeCornerGeometry(block: DesignBlock): StrokeCornerGeometry ``` Get the stroke corner geometry of the given design block. * `block`: the block whose stroke join geometry should be queried. * Returns the stroke join geometry. ``` val solidColor = engine.block.createFill(type = FillType.Color)engine.block.setColor( solidColor, property = "fill/color/value", value = Color.fromRGBA(r = 0.44F, g = 0.76F, b = 0.76F, a = 1F))val previousFill = engine.block.getFill(block)engine.block.destroy(previousFill)engine.block.setFill(block, fill = solidColor)engine.block.supportsFill(block)engine.block.setFillEnabled(block, enabled = false)engine.block.isFillEnabled(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 fill through the `block` API. The fill defines the visual contents within a block’s shape. ## Creating a Fill To create a fill simply use `fun createFill(type: String): DesignBlock`. ``` val solidColor = engine.block.createFill(type = FillType.Color) ``` ``` fun createFill(fillType: FillType): DesignBlock ``` Create a new fill, fails if type is unknown. * `fillType`: the type of the fill object that shall be created. * Returns the created fill’s handle. We currently support the following fill types: * `FillType.Color` * `FillType.LinearGradient` * `FillType.RadialGradient` * `FillType.ConicalGradient` * `FillType.Image` * `FillType.Video` * `FillType.PixelStream` ``` val solidColor = engine.block.createFill(type = FillType.Color) ``` ## Functions You can configure fills just like you configure design blocks. See [Modify Properties](android/concepts/blocks-90241e/) for more detail. ``` engine.block.setColor( solidColor, property = "fill/color/value", value = Color.fromRGBA(r = 0.44F, g = 0.76F, b = 0.76F, a = 1F)) ``` ``` fun getFill(block: DesignBlock): DesignBlock ``` Returns the block containing the fill properties of the given block. * `block`: the block whose fill block should be returned. * Returns the block that currently defines the given block’s fill. Remember to first destroy the previous fill if you don’t need it any more. A single fill can also be connected to multiple design blocks. This way, modifying the properties of the fill will apply the changes to all connected design blocks at once. ``` engine.block.destroy(previousFill) ``` ``` fun setFill( block: DesignBlock, fill: DesignBlock,) ``` Sets the block containing the fill properties of the given block. Note: The previous fill block is not destroyed automatically. Required scopes: “fill/change”, “fill/changeType” * `block`: the block whose fill should be changed. * `fill`: the new fill. ``` fun supportsFill(block: DesignBlock): Boolean ``` Query if the given block has fill color properties. * `block`: the block to query. * Returns true if the block has fill color properties, false otherwise. ``` fun setFillEnabled( block: DesignBlock, enabled: Boolean,) ``` Enable or disable the fill of the given design block. Required scope: “fill/change” * `block`: the block whose fill should be enabled or disabled. * `enabled`: if true, the fill will be enabled. ``` fun isFillEnabled(block: DesignBlock): Boolean ``` Query if the fill of the given design block is enabled. * `block`: the block whose fill state should be queried. * Returns true if the fill is enabled, false otherwise. --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/store-custom-metadata-337248) # Store Custom Metadata ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport 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 = 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.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()} ``` 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. ``` 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`. ``` // 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. ``` // 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)`. ``` 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. ``` // 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: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport 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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/save-c8b124) # Save 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. **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. ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Engineimport java.net.HttpURLConnectionimport java.net.URLimport 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 = 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) 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()} ``` ## 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. ``` val savedSceneString = engine.scene.saveToString(scene = scene) ``` That `Blob` can then be treated as a form file parameter and sent to a remote location. ``` 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: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Engineimport java.net.HttpURLConnectionimport java.net.URLimport 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()} ``` ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Engineimport java.net.HttpURLConnectionimport 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 = 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. ``` 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. ``` val blob = savedSceneString.toByteArray(Charsets.UTF_8) ``` That object can then be treated as a form file parameter and sent to a remote location. ``` 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: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Engineimport java.net.HttpURLConnectionimport 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()} ``` ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport 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 = 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. ``` 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. ``` println(savedSceneString) ``` ## Full Code ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport 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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/overview-72284a) # Overview This guide helps you understand how to persist and share creative work using the CreativeEditor SDK (CE.SDK). Whether you’re building user-driven editors, automations, or backend publishing flows, CE.SDK gives you flexible tools to export, save, and publish projects. CE.SDK supports **multi-modal output**, allowing you to export creative as: * Static designs * Print-ready PDFs * Videos All exporting and saving operations are handled **entirely on the client**. You don’t need a server to export files or save scenes, although you can easily integrate uploads or publishing workflows if needed. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Export vs. Save: What’s the Difference? **Exporting** creates a media file meant for consumption — like downloading a finalized PNG, uploading a video for YouTube, or printing a poster. Once exported, the file cannot be edited unless you reimport it as a new asset. **Saving** preserves the entire editable scene, including layers, settings, and asset references. Saved scenes can be reloaded, edited, and exported again later. Action Purpose Example **Export** Create final output User downloads a social media asset as PNG **Save** Save editable project state User saves a draft design to continue editing later ## Ways to Export or Save ### Using the UI You can export and save directly through CE.SDK’s built-in UI, such as export buttons, menu items, or publish actions. This behavior is fully customizable — you can modify or extend the UI to fit your app’s specific needs. ### Programmatically You can trigger exports and saves silently through the API, ideal for background processes, automation workflows, or headless scenarios. ### Supported Export Formats Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ## Export Options and Configuration CE.SDK provides rich configuration options when exporting: * **Size and Resolution:** Customize dimensions or use presets for different platforms. * **Format Selection:** Choose from image, video, or document output. * **Compression:** Optimize file size without compromising quality. * **Masks:** Export with color masks if needed. * **Quality Settings:** Fine-tune output quality based on your requirements. You can also programmatically create multiple export outputs for different use cases (e.g., Facebook, Instagram, YouTube) from the same design. ## Partial Export and Selective Export CE.SDK supports exporting parts of a project instead of the full scene: * **Selected Layers/Blocks Export:** Export only selected layers or blocks. This flexibility is useful for scenarios like asset slicing, batch creation, or modular content publishing. ## Pre-Export Validation Before exporting, you can run validation checks to ensure quality and compliance: * **NSFW Detection:** Flag potentially inappropriate content. * **Resolution Checks:** Ensure minimum export dimensions are met. * **Content Bounds Check:** Detect elements that extend beyond the visible canvas (this is not available out-of-the-box, but can be achieved with our APIs). You can hook into the export flow to run your own validation logic before completing an export. ## Saving a Scene for Future Editing When saving an editable scene, your approach may vary depending on your use case — especially when working with templates. In most scenarios, the recommended format is to save the scene as an **Archive**. This bundles the entire scene along with all used assets into a self-contained file that can be loaded without requiring access to external asset URLs. This is ideal when portability or offline availability is important. Alternatively, you can save the scene as a **string**. This format references assets by URL and is only suitable if those asset URLs will be reachable wherever the scene is later used — such as when reloading in a consistent cloud environment. You have several options when storing saved scenes: * **Local Save:** Store in local browser storage or download as a file. * **Backend Save:** Upload the archive or string to your server. * **Cloud Storage Save:** Integrate with services like AWS S3, GCP Storage, etc. Saved scenes preserve: * Layer structure * Applied effects * Asset references * Variable data (e.g., dynamic fields for templates) **Pros and Cons:** Strategy Pros Cons Archive Fully self-contained and portable Larger file size String Smaller file size, easy to inspect Requires externally reachable asset URLs Local Save Fast, no backend needed Risk of loss on browser reset Backend Save Centralized, enables user sessions Requires server integration Cloud Storage Save Scalable, ideal for distributed environments Slightly more complex to set up --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/export-82f968) # Export --- [Source](https:/img.ly/docs/cesdk/android/edit-image/transform-9d189b) # Transform Image transformations in CreativeEditor SDK (CE.SDK) allow you to adjust the size, orientation, and framing of images to better fit your designs. Whether you need to crop out unnecessary areas, rotate an image for better composition, or resize assets for different outputs, transformations provide powerful tools for fine-tuning visuals. You can perform transformations both through the built-in user interface and programmatically using the SDK’s APIs, giving you flexibility depending on your workflow. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Available Transformations CE.SDK supports several types of image transformations: * **Crop**: Trim an image to focus on a specific area or fit a desired aspect ratio. * **Rotate**: Rotate the image by a custom degree to adjust orientation. * **Resize**: Change the width and height independently to fit layout requirements. * **Scale**: Uniformly enlarge or reduce the size of the image while maintaining its aspect ratio. * **Flip**: Mirror the image horizontally or vertically to create a reversed version. Each transformation can be used individually or combined for more complex edits. ## Applying Transformations ### UI-Based Transformation You can apply transformations directly in the CE.SDK user interface. The editor provides intuitive controls for cropping, rotating, resizing, scaling, and flipping images. This makes it easy for users to visually adjust images without writing any code. ### Programmatic Transformation Developers can also apply transformations programmatically by interacting with the SDK’s API. This allows for dynamic image adjustments based on application logic, user input, or automated processes. ## Combining Transformations CE.SDK lets you chain multiple transformations together in a single operation. For example, you might scale an image before cropping it to ensure the best possible resolution. When combining transformations, consider applying scaling first to avoid quality loss during cropping or rotation. Structuring transformations thoughtfully helps maintain visual clarity and optimize output quality. ## Guides --- [Source](https:/img.ly/docs/cesdk/android/edit-image/overview-5249ea) # Android Image Editor SDK The CreativeEditor SDK (CE.SDK) offers powerful image editing capabilities designed for seamless integration into your application. You can give your users full control through an intuitive user interface or implement fully automated workflows via the SDK’s programmatic API. Image editing with CE.SDK is fully client-side, ensuring fast performance, data privacy, and offline compatibility. Whether you’re building a photo editor, design tool, or automation workflow, CE.SDK provides everything you need—plus the flexibility to integrate AI tools for tasks like adding or removing objects, swapping backgrounds, or creating variants. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Core Capabilities CE.SDK includes a wide range of image editing features accessible both through the UI and programmatically. Key capabilities include: * **Transformations**: Crop, rotate, resize, scale, and flip images. * **Adjustments and effects**: Apply filters, control brightness and contrast, add vignettes, pixelization, and more. * **Background removal**: Automatically remove backgrounds from images using plugin integrations. * **Color tools**: Replace colors, apply gradients, adjust palettes, and convert to black and white. * **Vectorization**: Convert raster images into vector format (SVG). * **Programmatic editing**: Make all edits via API—ideal for automation and bulk processing. All operations are optimized for in-app performance and align with real-time editing needs. ## AI-powered Editing CE.SDK allows you to easily integrate AI tools directly into your editing workflow. Users can generate or edit images from simple prompts — all from within the editor’s task bar, without switching tools or uploading external assets. [Launch AI Editor Demo](https://img.ly/showcases/cesdk/ai-editor/web) Typical AI use cases include: * **Text-to-image**: Generate visuals from user prompts. * **Background removal**: Automatically extract subjects from photos. * **Style transfer**: Apply the look of one image to another. * **Variant generation**: Create multiple versions of a design or product image. * **Text-to-graphics**: Render typographic compositions from plain text. * **Object add/remove**: Modify compositions by adding or erasing visual elements. You can bring your own models or third-party APIs with minimal setup. AI tools can be added as standalone plugins, contextual buttons, or task bar actions. ## Supported Input Formats The SDK supports a broad range of image input types: Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. ## Output and export options Export edited images in the following formats: Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. You can define export resolution, compression level, and file metadata. CE.SDK also supports exporting with transparent backgrounds, underlayers, or color masks. ## UI-Based vs. Programmatic Editing CE.SDK provides two equally powerful ways to perform image edits: * **UI-based editing**: The built-in editor includes a customizable toolbar, side panels, and inspector views. End users can directly manipulate elements through the visual interface. * **Programmatic editing**: Every image transformation, effect, or layout operation can be executed via the SDK’s API. This is ideal for bulk operations, automated design workflows, or serverless rendering. You can freely combine both approaches in a single application. ## Customization The CE.SDK image editor is fully customizable: * **Tool configuration**: Enable, disable, or reorder individual editing tools. * **Panel visibility**: Show or hide interface elements like inspectors, docks, and canvas menus. * **Themes and styling**: Customize the UI appearance with brand colors, fonts, and icons. * **Localization**: Translate all interface text via the internationalization API. You can also add custom buttons, inject quick actions, or build your own interface on top of the engine using the headless mode. --- [Source](https:/img.ly/docs/cesdk/android/create-video/overview-b06512) # Create Videos Overview ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.MimeTypeimport ly.img.engine.ShapeType fun editVideo( 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) 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 video1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video1, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = "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(video1, fill = videoFill) val video2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video1, shape = engine.block.createShape(ShapeType.Rect)) val videoFill2 = engine.block.createFill(FillType.Video) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = "https://cdn.img.ly/assets/demo/v2/ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) engine.block.setFill(video2, fill = videoFill2) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.appendChild(parent = track, child = video1) engine.block.appendChild(parent = track, child = video2) engine.block.fillParent(track) engine.block.setDuration(video1, duration = 15.0) // Make sure that the video is loaded before calling the trim APIs. engine.block.forceLoadAVResource(videoFill) engine.block.setTrimOffset(videoFill, offset = 1.0) engine.block.setTrimLength(videoFill, length = 10.0) engine.block.setLooping(videoFill, looping = true) engine.block.setMuted(videoFill, muted = true) val audio = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = audio) engine.block.setString( block = audio, property = "audio/fileURI", value = "https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a", ) // Set the volume level to 70%. engine.block.setVolume(audio, volume = 0.7F) // Start the audio after two seconds of playback. engine.block.setTimeOffset(audio, offset = 2.0) // Give the Audio block a duration of 7 seconds. engine.block.setDuration(audio, duration = 7.0) // Export page as mp4 video. val blob = 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", ) }, ) engine.stop()} ``` In addition to static designs, CE.SDK also allows you to create and edit videos. Working with videos introduces the concept of time into the scene, which requires you to switch the scene into the `"Video"` mode. In this mode, each page in the scene has its own separate timeline within which its children can be placed. The `"playback/time"` property of each page controls the progress of time through the page. In order to add videos to your pages, you can add a block with a `FillType.video` fill. As the playback time of the page progresses, the corresponding point in time of the video fill is rendered by the block. You can also customize the video fill’s trim in order to control the portion of the video that should be looped while the block is visible. `DesignBlockType.Audio` blocks can be added to the scene in order to play an audio file during playback. The `playback/timeOffset` property controls after how many seconds the audio should begin to play, while the duration property defines how long the audio should play. The same APIs can be used for other design blocks as well, such as text or graphic blocks. Finally, the whole page can be exported as a video file using the `BlockApi.exportVideo` function. ## Creating a Video Scene First, we create a scene that is set up for video editing by calling the `scene.createForVideo()` API. Then we create a page, add it to the scene and define its dimensions. This page will hold our composition. ``` 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) ``` ## Setting Page Durations Next, we define the duration of the page using the `fun setDuration(block: DesignBlock, duration: Double)` API to be 20 seconds long. This will be the total duration of our exported video in the end. ``` engine.block.setDuration(page, duration = 20.0) ``` ## Adding Videos In this example, we want to show two videos, one after the other. For this, we first create two graphic blocks and assign two `'video'` fills to them. ``` val video1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video1, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = "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(video1, fill = videoFill) val video2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video1, shape = engine.block.createShape(ShapeType.Rect)) val videoFill2 = engine.block.createFill(FillType.Video) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = "https://cdn.img.ly/assets/demo/v2/ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) engine.block.setFill(video2, fill = videoFill2) ``` ## Creating a Track While we could add the two blocks directly to the page and manually set their sizes and time offsets, we can alternatively also use the `DesignBlockType.Track` block to simplify this work. A track automatically adjusts the time offsets of its children to make sure that they play one after another without any gaps, based on each child’s duration. Tracks themselves cannot be selected directly by clicking on the canvas, nor do they have any visual representation. We create a `DesignBlockType.Track` block, add it to the page and add both videos in the order in which they should play as the track’s children. Next, we use the `fillParent` API, which will resize all children of the track to the same dimensions as the page. The dimensions of a track are always derived from the dimensions of its children, so you should not call the `setWidth` or `setHeight` APIs on a track, but on its children instead if you can’t use the `fillParent` API. ``` val track = engine.block.create(DesignBlockType.Track)engine.block.appendChild(parent = page, child = track)engine.block.appendChild(parent = track, child = video1)engine.block.appendChild(parent = track, child = video2)engine.block.fillParent(track) ``` By default, each block has a duration of 5 seconds after it is created. If we want to show it on the page for a different amount of time, we can use the `setDuration` API. Note that we can just increase the duration of the first video block to 15 seconds without having to adjust anything about the second video. The `track` takes care of that for us automatically so that the second video starts playing after 15 seconds. ``` engine.block.setDuration(video1, duration = 15.0) ``` If the video is longer than the duration of the graphic block that it’s attached to, it will cut off once the duration of the graphic is reached. If it is too short, the video will automatically loop for as long as its graphic block is visible. We can also manually define the portion of our video that should loop within the page using the `fun setTrimOffset(block: DesignBlock, offset: Double)` and `fun setTrimLength(block: DesignBlock, length: Double)` APIs. We use the trim offset to cut away the first second of the video and the trim length to only play 10 seconds of the video. ``` // Make sure that the video is loaded before calling the trim APIs.engine.block.forceLoadAVResource(videoFill)engine.block.setTrimOffset(videoFill, offset = 1.0)engine.block.setTrimLength(videoFill, length = 10.0) ``` We can control if a video will loop back to its beginning by calling `fun setLooping(block: DesignBlock, looping: Boolean)`. Otherwise, the video will simply hold its last frame instead and audio will stop playing. Looping behavior is activated for all blocks by default. ``` engine.block.setLooping(videoFill, looping = true) ``` ## Audio If the video of a video fill contains an audio track, that audio will play automatically by default when the video is playing. We can mute it by calling `fun setMuted(block: DesignBlock, muted: Boolean)`. ``` engine.block.setMuted(videoFill, muted = true) ``` We can also add audio-only files to play together with the contents of the scene by adding an `audio` block to the scene and assigning it the uri of the audio file. ``` val audio = engine.block.create(DesignBlockType.Audio)engine.block.appendChild(parent = page, child = audio)engine.block.setString( block = audio, property = "audio/fileURI", value = "https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a",) ``` We can adjust the volume level of any audio block or video fill by calling `fun setVolume(block: DesignBlock, volume: Float)`. The volume is given as a fraction in the range of 0 to 1. ``` // Set the volume level to 70%.engine.block.setVolume(audio, volume = 0.7F) ``` By default, our audio block will start playing at the very beginning of the page. We can change this by specifying how many seconds into the page it should begin to play using the `fun setTimeOffset(block: DesignBlock, offset: Double)` API. ``` // Start the audio after two seconds of playback.engine.block.setTimeOffset(audio, offset = 2.0) ``` By default, our audio block will have a duration of 5 seconds. We can change this by specifying its duration in seconds by using the `fun setDuration(block: DesignBlock, duration: Double)` API. ``` // Give the Audio block a duration of 7 seconds.engine.block.setDuration(audio, duration = 7.0) ``` ## Exporting Video You can start exporting the entire page as a video file by calling `blockApi.exportVideo`. The encoding process will run in the background. You can get notified about the progress of the encoding process by using `progressCallback` parameter. Since the encoding process runs in the background, the engine will stay interactive. So, you can continue to use the engine to manipulate the scene. Please note that these changes won’t be visible in the exported video file because the scene’s state has been frozen at the start of the export. ``` // Export page as mp4 video.val blob = 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", ) },) ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.MimeTypeimport ly.img.engine.ShapeType fun editVideo( 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.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 video1 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video1, shape = engine.block.createShape(ShapeType.Rect)) val videoFill = engine.block.createFill(FillType.Video) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = "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(video1, fill = videoFill) val video2 = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(video1, shape = engine.block.createShape(ShapeType.Rect)) val videoFill2 = engine.block.createFill(FillType.Video) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = "https://cdn.img.ly/assets/demo/v2/ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) engine.block.setFill(video2, fill = videoFill2) val track = engine.block.create(DesignBlockType.Track) engine.block.appendChild(parent = page, child = track) engine.block.appendChild(parent = track, child = video1) engine.block.appendChild(parent = track, child = video2) engine.block.fillParent(track) engine.block.setDuration(video1, duration = 15.0) // Make sure that the video is loaded before calling the trim APIs. engine.block.forceLoadAVResource(videoFill) engine.block.setTrimOffset(videoFill, offset = 1.0) engine.block.setTrimLength(videoFill, length = 10.0) engine.block.setLooping(videoFill, looping = true) engine.block.setMuted(videoFill, muted = true) val audio = engine.block.create(DesignBlockType.Audio) engine.block.appendChild(parent = page, child = audio) engine.block.setString( block = audio, property = "audio/fileURI", value = "https://cdn.img.ly/assets/demo/v1/ly.img.audio/audios/far_from_home.m4a", ) // Set the volume level to 70%. engine.block.setVolume(audio, volume = 0.7F) // Start the audio after two seconds of playback. engine.block.setTimeOffset(audio, offset = 2.0) // Give the Audio block a duration of 7 seconds. engine.block.setDuration(audio, duration = 7.0) // Export page as mp4 video. val blob = 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", ) }, ) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/create-video/control-daba54) # Control Audio and Video In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to configure and control audio and video through the `block` API. ## Time Offset and Duration The time offset determines when a block becomes active during playback on the scene’s timeline, and the duration decides how long this block is active. Page blocks are a special case in that they have an implicitly calculated time offset that is determined by their order and the total duration of their preceding pages. As with any audio/video-related property, not every block supports these properties. Use `supportsTimeOffset` and `supportsDuration` to check. ``` fun supportsTimeOffset(block: DesignBlock): Boolean ``` Returns whether the block has a time offset property. * `block`: the block to query. * Returns true, if the block has a time offset property. ``` fun setTimeOffset( block: DesignBlock, offset: Double,) ``` Set the time offset of the given block relative to its parent. The time offset controls when the block is first active in the timeline. Note: The time offset is not supported by the page block. * `block`: the block whose time offset should be changed. * `offset`: the new time offset in seconds. ``` fun getTimeOffset(block: DesignBlock): Double ``` Get the time offset of the given block relative to its parent. * `block`: the block whose time offset should be queried. * Returns the time offset of the block. ``` fun supportsDuration(block: DesignBlock): Boolean ``` Returns whether the block has a duration property. * `block`: the block to query. * Returns true if the block has a duration property. ``` fun setDuration( block: DesignBlock, duration: Double,) ``` Set the playback duration of the given block in seconds. The duration defines for how long the block is active in the scene during playback. If a duration is set on the page block, it becomes the duration source block. Note: The duration is ignored when the scene is not in “Video” mode. * `block`: the block whose duration should be changed. * `duration`: the new duration in seconds. ``` fun getDuration(block: DesignBlock): Double ``` Get the playback duration of the given block in seconds. * `block`: the block whose duration should be returned. * Returns the block’s duration. ``` fun supportsPageDurationSource( page: DesignBlock, block: DesignBlock,): Boolean ``` Returns whether the block can be marked as the element that defines the duration of the given page. * `page`: the page block for which to query for. * `block`: the block to query. * Returns true, if the block can be marked as the element that defines the duration of the given page. ``` fun setPageDurationSource( page: DesignBlock, block: DesignBlock,) ``` Set an block as duration source so that the overall page duration is automatically determined by this. If no defining block is set, the page duration is calculated over all children. Only one block per page can be marked as duration source. Will automatically unmark the previously marked. Note: This is only supported for blocks that have a duration. * `page`: the page block for which it should be enabled. * `block`: the block which should be marked as duration source. ``` fun isPageDurationSource(block: DesignBlock): Boolean ``` Returns whether the block is a duration source block. * `block`: the block whose duration source property should be queried. * Returns if the block is a duration source for a page. ``` fun removePageDurationSource(block: DesignBlock) ``` Remove the block as duration source block for the page. If a scene or page given set as block, it is deactivated for all blocks in the scene or page. * `block`: the block whose duration source property should be removed. ## Trim You can select a specific range of footage from your audio/video resource by providing a trim offset and a trim length. The footage will loop if the trim’s length is shorter than the block’s duration. This behavior can also be disabled using the `setLooping` function. ``` fun supportsTrim(block: DesignBlock): Boolean ``` Returns whether the block has trim properties. * `block`: the block to query. * Returns true, if the block has trim properties. ``` fun setTrimOffset( block: DesignBlock, offset: Double,) ``` Set the trim offset of the given block or fill. Sets the time in seconds within the fill at which playback of the audio or video clip should begin. Note: This requires the video or audio clip to be loaded. * `block`: the block whose trim should be updated. * `offset`: the new trim offset in seconds. ``` fun getTrimOffset(block: DesignBlock): Double ``` Get the trim offset of this block. Note: This requires the video or audio clip to be loaded. * `block`: the block whose trim offset should be queried. * Returns the trim offset in seconds. ``` fun setTrimLength( block: DesignBlock, length: Double,) ``` Set the trim length of the given block or fill. The trim length is the duration of the audio or video clip that should be used for playback. Note: After reaching this value during playback, the trim region will loop. Note: This requires the video or audio clip to be loaded. * `block`: the object whose trim length should be updated. * `block`: the new trim length in seconds. ``` fun getTrimLength(block: DesignBlock): Double ``` Get the trim length of the given block or fill. * `block`: the object whose trim length should be queried. * Returns the trim length of the object. ## Playback Control You can start and pause playback and seek to a certain point on the scene’s timeline. There’s also a solo playback mode to preview audio and video blocks individually while the rest of the scene stays frozen. Finally, you can enable or disable the looping behavior of blocks and control their audio volume. ``` fun setPlaying( block: DesignBlock, enabled: Boolean,) ``` Set whether the block should be during active playback. * `block`: the block that should be updated. * `enabled`: whether the block should be playing its contents. ``` fun isPlaying(block: DesignBlock): Boolean ``` Returns whether the block is currently during active playback. * `block`: the block to query. * Returns whether the block is during playback. ``` fun setSoloPlaybackEnabled( block: DesignBlock, enabled: Boolean,) ``` Set whether the given block or fill should play its contents while the rest of the scene remains paused. Note: Setting this to true for one block will automatically set it to false on all other blocks. * `block`: the block or fill to update. * `enabled`: whether the block’s playback should progress as time moves on. ``` fun isSoloPlaybackEnabled(block: DesignBlock): Boolean ``` Return whether the given block or fill is currently set to play its contents while the rest of the scene remains paused. * `block`: the block or fill to query. * Returns whether solo playback is enabled for this block. ``` fun supportsPlaybackTime(block: DesignBlock): Boolean ``` Returns whether the block has a playback time property. * `block`: the block to query. * Returns whether the block has a playback time property. ``` fun setPlaybackTime( block: DesignBlock, time: Double,) ``` Set the playback time of the given block. * `block`: the block whose playback time should be updated. * `time`: the new playback time of the block in seconds. ``` fun getPlaybackTime(block: DesignBlock): Double ``` Get the playback time of the given block. * `block`: the block to query. * Returns the playback time of the block in seconds. ``` fun isVisibleAtCurrentPlaybackTime(block: DesignBlock): Boolean ``` Returns whether the block should be visible on the canvas at the current playback time. * `block`: the block to query. * Returns the visibility state if the query succeeded. ``` fun supportsPlaybackControl(block: DesignBlock): Boolean ``` Returns whether the block supports a playback control. * `block`: the block to query. * Returns whether the block has playback control. ``` fun setLooping( block: DesignBlock, looping: Boolean,) ``` Set whether the block should start from the beginning again or stop. * `block`: the block or video fill to update. * Returns whether the block should loop to the beginning or stop. ``` fun isLooping(block: DesignBlock): Boolean ``` Query whether the block is looping. * `block`: the block to query. * Returns whether the block is looping. ``` fun setMuted( block: DesignBlock, muted: Boolean,) ``` Set whether the audio of the block is muted. * `block`: the block or video fill to update. * `muted`: whether the audio should be muted. ``` fun isMuted(block: DesignBlock): Boolean ``` Query whether the block is muted. * `block`: the block to query. * Returns whether the block is muted. ``` fun setVolume( block: DesignBlock, @FloatRange(from = 0.0, to = 1.0) volume: Float,) ``` Set the audio volume of the given block. * `block`: the block or video fill to update. * `volume`: the desired volume with a range of `0, 1`. ``` @FloatRange(from = 0.0, to = 1.0)fun getVolume(block: DesignBlock): Float ``` Get the audio volume of the given block. * `block`: the block to query. * Returns volume with a range of `0, 1`. ## Resource Control Until an audio/video resource referenced by a block is loaded, properties like the duration of the resource aren’t available, and accessing those will lead to an error. You can avoid this by forcing the resource you want to access to load using `forceLoadAVResource`. ``` suspend fun forceLoadAVResource(block: DesignBlock) ``` Begins loading the required audio and video resource for the given video fill or audio block. If the resource had been loaded earlier and resulted in an error, it will be reloaded. * `block`: the video fill or audio block whose resource should be loaded. ``` @UnstableEngineApifun isAVResourceLoaded(block: DesignBlock): Boolean ``` Returns whether the audio and video resource for the given video fill or audio block is loaded. Note that the function is unstable and mared with `UnstableEngineApi`. * `block`: the video fill or audio block. * Returns whether the resource is loaded. ``` fun getAVResourceTotalDuration(block: DesignBlock): Double ``` Get the duration in seconds of the video or audio resource that is attached to the given block. * `block`: the video fill or audio block. * Returns the video or audio file duration. ``` fun getVideoWidth(videoFill: DesignBlock): Int ``` Get the video width in pixels of the video resource that is attached to the given block. * `videoFill`: the video fill. * Returns the video width in pixels. ``` fun getVideoHeight(videoFill: DesignBlock): Int ``` Get the video height in pixels of the video resource that is attached to the given block. * `videoFill`: the video fill. * Returns the video height in pixels. ## Thumbnail Previews For a user interface, it can be helpful to have image previews in the form of thumbnails for any given video resource. For videos, the engine can provide one or more frames using `generateVideoThumbnailSequence`. Pass the video fill that references the video resource. In addition to video thumbnails, the engine can also render compositions of design blocks over time. To do this pass in the respective design block. The video editor uses these to visually represent blocks in the timeline. In order to visualize audio signals `generateAudioThumbnailSequence` can be used. This generates a sequence of values in the range of 0 to 1 that represent the loudness of the signal. These values can be used to render a waveform pattern in any custom style. Note: there can be at most one thumbnail generation request per block at any given time. If you don’t want to wait for the request to finish before issuing a new request, you can cancel it by calling `cancel()` on the `Job` object returned on launching the flow. ``` fun generateVideoThumbnailSequence( block: DesignBlock, thumbnailHeight: Int, timeBegin: Double, timeEnd: Double, numberOfFrames: Int,): Flow ``` Generate a thumbnail sequence for the given video fill or design block. * `block`: the video fill or a design block. * `thumbnailHeight`: the height of a thumbnail. The width will be calculated from the video aspect ratio. * `timeBegin`: the time in seconds relative to the time offset of the design block at which the thumbnail sequence should start. * `timeEnd`: the time in seconds relative to the time offset of the design block at which the thumbnail sequence should end. * `numberOfFrames`: the number of frames to generate. * Returns a flow of `VideoThumbnailResult` object which emits for every generated frame thumbnail. It emits exactly `numberOfFrames` times. ``` fun generateAudioThumbnailSequence( block: DesignBlock, samplesPerChunk: Int, timeBegin: Double, timeEnd: Double, numberOfSamples: Int, numberOfChannels: Int,): Flow ``` Generate a thumbnail sequence for the given audio block or video fill. A thumbnail in this case is a chunk of samples in the range of 0 to 1. In case stereo data is requested, the samples are interleaved, starting with the left channel. * `block`: the audio block or video fill. * `samplesPerChunk`: the number of samples per chunk. * `timeBegin`: the time in seconds at which the thumbnail sequence should start. * `timeEnd`: the time in seconds at which the thumbnail sequence should end. * `numberOfSamples`: the total number of samples to generate. * `numberOfChannels`: the number of channels in the output. 1 for mono, 2 for stereo. * Returns a flow of `AudioThumbnailResult` object which emits numberOfSamples / samplesPerChunk times. ## Full Code Here’s the full code: ``` // Time Offset and Durationengine.block.supportsTimeOffset(audio)engine.block.setTimeOffset(audio, offset = 2.0)engine.block.getTimeOffset(audio) /* Returns 2 */ engine.block.supportsDuration(page)engine.block.setDuration(page, duration = 10.0)engine.block.getDuration(page) /* Returns 10 */ // Duration of the page can be that of a blockengine.block.supportsPageDurationSource(page, block)engine.block.setPageDurationSource(page, block)engine.block.isPageDurationSource(block)engine.block.getDuration(page) /* Returns duration plus offset of the block */ // Duration of the page can be the maximum end time of all page child blocksengine.block.removePageDurationSource(page)engine.block.getDuration(page) /* Returns the maximum end time of all page child blocks */ // Trimengine.block.supportsTrim(videoFill)engine.block.setTrimOffset(videoFill, offset = 1.0)engine.block.getTrimOffset(videoFill) /* Returns 1 */engine.block.setTrimLength(videoFill, length = 5.0)engine.block.getTrimLength(videoFill) /* Returns 5 */ // Playback Controlengine.block.setPlaying(page, enabled = true)engine.block.isPlaying(page) engine.block.setSoloPlaybackEnabled(videoFill, enabled = true)engine.block.isSoloPlaybackEnabled(videoFill) engine.block.supportsPlaybackTime(page)engine.block.setPlaybackTime(page, time = 1.0)engine.block.getPlaybackTime(page)engine.block.isVisibleAtCurrentPlaybackTime(block) engine.block.supportsPlaybackControl(videoFill)engine.block.setLooping(videoFill, looping = true)engine.block.isLooping(videoFill)engine.block.setMuted(videoFill, muted = true)engine.block.isMuted(videoFill)engine.block.setVolume(videoFill, volume = 0.5F) /* 50% volume */engine.block.getVolume(videoFill) // Resource Controlengine.block.forceLoadAVResource(videoFill)/* Unstable engine api */engine.block.isAVResourceLoaded(videoFill)engine.block.getAVResourceTotalDuration(videoFill)val videoWidth = engine.block.getVideoWidth(videoFill)val videoHeight = engine.block.getVideoHeight(videoFill) // Thumbnail Previewsengine.block.generateVideoThumbnailSequence( block = videoFill, thumbnailHeight = 128, timeBegin = 0.5, timeEnd = 9.5, numberOfFrames = 10).onEach { println("frameIndex = ${it.frameIndex}, width = ${it.width}, height = ${it.height}, imageData:size = ${it.imageData.size}")}.launchIn(viewLifecycleScope)engine.block.generateAudioThumbnailSequence( block = audio, samplesPerChunk = 20, timeBegin = 0.5, timeEnd = 9.5, numberOfSamples = 10 * 20, numberOfChannels = 2,).onEach { println("chunkIndex = ${it.chunkIndex}, samples:size = ${it.samples.size}") drawWavePattern(it.samples)}.launchIn(viewLifecycleScope) ``` --- [Source](https:/img.ly/docs/cesdk/android/create-templates/overview-4ebe30) # Overview 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. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Why Use Templates? Templates are a powerful tool for: * Maintaining **brand consistency** across all user-generated designs. * **Scaling** asset creation for campaigns, catalogs, or print products. * Providing a **guided experience** where users adapt content without starting from scratch. They are ideal for use cases like: * Personalized marketing campaigns * Dynamic social media ads * Product catalogs and e-commerce visuals * Custom print materials and photo books ## Ways to Create Templates You can create templates from scratch or by importing an existing template. **From Scratch:** Start a new project and design a scene with the intent of turning it into a template. You can define variables, placeholders, and constraints directly in the editor. **Import Existing Designs:** If you already have assets created in other tools, you can import them as templates. Format Description `.idml` InDesign `.psd` Photoshop `.scene` CE.SDK Native Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs to generate scenes programmatically. These imported designs can then be adapted into editable, structured templates inside CE.SDK. ## Dynamic Content in Templates Templates support dynamic content to enable data-driven generation of assets. CE.SDK provides several mechanisms: * **Text Variables**: Bind text elements to dynamic values (e.g., user names, product SKUs). * **Image Placeholders**: Reserve space for images to be inserted later. * **Video Placeholders**: Reserve space for videos, enabling dynamic insertion of video clips in a templated layout. This makes it easy to generate hundreds or thousands of personalized variations from a single design. ## Controlling Template Editing Templates in CE.SDK offer powerful tools for controlling the editing experience: * **Editing Constraints**: Lock specific properties such as position, style, or content of elements. * **Locked Templates**: Prevent any edits outside allowed fields to protect design integrity. * **Fully Editable Templates**: Allow unrestricted editing for power users or advanced workflows. * **Form-Based Editing**: Build a custom editing interface for users to fill in variables and placeholders via input forms (a ready-made UI is not currently provided, but can be built using our APIs). These options let you strike a balance between creative freedom and design control. ## Working with Templates Programmatically and Through the UI You can manage templates using both the UI and API: * **UI Integration**: Users can select, apply, and edit templates directly inside the CE.SDK interface. * **Programmatic Access**: Use the SDK’s APIs to create, apply, or modify templates as part of an automated workflow. * **Asset Library Integration**: Templates can appear in the asset library, allowing users to browse and pick templates visually. * The Asset Library’s appearance and behavior can be fully customized to fit your app’s needs. ## Managing Templates Templates are saved and reused just like any other CE.SDK asset: * **Save Templates** to a _Template Library_. * **Edit or Remove** existing templates from your asset library. * Templates are saved as Scene (`.scene`) or Archive (`.zip`) files and can be loaded across all platforms supported by CE.SDK (Web, Mobile, Server, Desktop) ## Default and Premium Templates * **Default Templates**: CE.SDK may include a small number of starter templates depending on your configuration. * **Premium Templates**: IMG.LY offers a growing collection of professionally designed templates available for licensing. * Templates can be imported, customized, and used directly within your app. Check your license or speak with our team for details on accessing premium templates. ## Templates as Assets Templates are treated as **assets** in CE.SDK. That means: * They can be included in local or remote asset libraries. * They can be shared, versioned, and indexed using the same systems as images or videos. * You can apply your own metadata, tags, and search capabilities to them. --- [Source](https:/img.ly/docs/cesdk/android/create-templates/lock-131489) # Lock the Template ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport 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 = 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()`. ``` 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 ``` // 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")`. ``` // 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)`. ``` // 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`. ``` // 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. ``` // 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: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport 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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/create-templates/add-dynamic-content-53fad7) # Dynamic Content --- [Source](https:/img.ly/docs/cesdk/android/create-composition/layer-management-18f07a) # Layer Management ``` engine.block.insertChild(parent = page, child = block, index = 0)val parent = engine.block.getParent(block)val childIds = engine.block.getChildren(block)engine.block.appendChild(parent = parent, child = block) ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify the hierarchy of blocks through the `block` API. ## Manipulate the hierarchy of blocks Only blocks that are direct or indirect children of a `page` block are rendered. Scenes without any `page` child may not be properly displayed by the CE.SDK editor. ``` fun getParent(block: DesignBlock): DesignBlock? ``` Query a block’s parent. * `block`: the block to query. * Returns the parent’s handle or null if the block has no parent. ``` fun getChildren(block: DesignBlock): List ``` Get all children of the given block. Children are sorted in their rendering order: Last child is rendered in front of other children. * `block`: the block to query. * Returns a list of block ids. ``` fun insertChild( parent: DesignBlock, child: DesignBlock, index: Int,) ``` Insert a new or existing child at a certain position in the parent’s children. Required scope: “editor/add” * `parent`: the block to update. * `child`: the child to insert. Can be an existing child of `parent`. * `index`: the index to insert or move to. ``` fun appendChild( parent: DesignBlock, child: DesignBlock,) ``` Appends a new or existing child to a block’s children. Required scope: “editor/add” * `parent`: the block to update. * `child`: the child to insert. Can be an existing child of `parent`. When adding a block to a new parent, it is automatically removed from its previous parent. ## Full Code Here’s the full code: ``` engine.block.insertChild(parent = page, child = block, index = 0)val parent = engine.block.getParent(block)val childIds = engine.block.getChildren(block)engine.block.appendChild(parent = parent, child = block) ``` --- [Source](https:/img.ly/docs/cesdk/android/create-composition/group-and-ungroup-62565a) # Group and Ungroup Objects ``` // Create blocks and append to sceneval member1 = engine.block.create(DesignBlockType.Graphic)val member2 = engine.block.create(DesignBlockType.Graphic)engine.block.appendChild(scene, child = member1)engine.block.appendChild(scene, child = member2) // Check whether the blocks may be groupedif (engine.block.isGroupable(listOf(member1, member2))) { val group = engine.block.group(listOf(member1, member2)) engine.block.setSelected(group, selected = true) engine.block.enterGroup(group) engine.block.setSelected(member1, selected = true) engine.block.exitGroup(member1) engine.block.ungroup(group)} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to group blocks through the `block` API. Groups form a cohesive unit. ## Grouping Multiple blocks can be grouped together to form a cohesive unit. A group being a block, it can itself be part of a group. **What cannot be grouped** * A scene * A block that already is part of a group ``` fun isGroupable(blocks: List): Boolean ``` Confirms that a given set of blocks can be grouped together. * `blocks`: a non-empty array of block ids. * Returns whether the blocks can be grouped together. ``` fun group(blocks: List): DesignBlock ``` Group blocks together. * `blocks`: a non-empty array of block ids. * Returns the block id of the created group. ``` fun ungroup(block: DesignBlock) ``` Ungroups a group. * `block`: the group id from a previous call to `group`. ``` fun enterGroup(block: DesignBlock) ``` Changes selection from selected group to a block within that group. Nothing happens if `block` is not a group. Required scope: “editor/select” * `block`: the group id from a previous call to `group`. ``` fun exitGroup(block: DesignBlock) ``` Changes selection from a group’s selected block to that group. Nothing happens if `block` is not a group. Required scope: “editor/select” * `block`: a block id. ## Full Code Here’s the full code: ``` // Create blocks and append to sceneval member1 = engine.block.create(DesignBlockType.Graphic)val member2 = engine.block.create(DesignBlockType.Graphic)engine.block.appendChild(scene, child = member1)engine.block.appendChild(scene, child = member2) // Check whether the blocks may be groupedif (engine.block.isGroupable(listOf(member1, member2))) { val group = engine.block.group(listOf(member1, member2)) engine.block.setSelected(group, selected = true) engine.block.enterGroup(group) engine.block.setSelected(member1, selected = true) engine.block.exitGroup(member1) engine.block.ungroup(group)} ``` --- [Source](https:/img.ly/docs/cesdk/android/create-composition/blend-modes-ad3519) # Blend Modes ``` engine.block.supportsOpacity(image)engine.block.setOpacity(image, value = 0.5F)engine.block.getOpacity(image) engine.block.supportsBlendMode(image)engine.block.setBlendMode(image, blendMode = BlendMode.MULTIPLY)engine.block.getBlendMode(image) if (engine.block.supportsBackgroundColor(image)) { engine.block.setBackgroundColor(page, Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F) // Red engine.block.getBackgroundColor(page) engine.block.setBackgroundColorEnabled(page, enabled = true) engine.block.isBackgroundColorEnabled(page)} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify a blocks appearance through the `block` API. ## Common Properties Common properties are properties that occur on multiple block types. For instance, fill color properties are available for all the shape blocks and the text block. That’s why we built convenient setter and getter functions for these properties. So you don’t have to use the generic setters and getters and don’t have to provide a specific property path. There are also `has*` functions to query if a block supports a set of common properties. ### Opacity Set the translucency of the entire block. ``` fun supportsOpacity(block: DesignBlock): Boolean ``` Query if the given block has an opacity. * `block`: the block to query. * Returns true if the block has an opacity, false otherwise. ``` fun setOpacity( block: DesignBlock, @FloatRange(from = 0.0, to = 1.0) value: Float,) ``` Set the opacity of the given design block. Required scope: “layer/opacity” * `block`: the block whose opacity should be set. * `value`: the opacity to be set. The valid range is 0 to 1. ``` @FloatRange(from = 0.0, to = 1.0)fun getOpacity(block: DesignBlock): Float ``` Get the opacity of the given design block. * `block`: the block whose opacity should be queried. * Returns the opacity. ### Blend Mode Define the blending behaviour of a block. ``` fun supportsBlendMode(block: DesignBlock): Boolean ``` Query if the given block has a blend mode. * `block`: the block to query. * Returns true if the block has a blend mode, false otherwise. ``` fun setBlendMode( block: DesignBlock, blendMode: BlendMode,) ``` Set the blend mode of the given design block. Required scope: “layer/blendMode” * `block`: the block whose blend mode should be set. * `blendMode`: the blend mode to be set. ``` fun getBlendMode(block: DesignBlock): BlendMode ``` Get the blend mode of the given design block. * `block`: the block whose blend mode should be queried. * Returns the blend mode. ### Background Color Manipulate the background of a block. To understand the difference between fill and background color take the text block. The glyphs of the text itself are colored by the fill color. The rectangular background given by the bounds of the block on which the text is drawn is colored by the background color. ``` fun supportsBackgroundColor(block: DesignBlock): Boolean ``` Query if the given block has background color properties. * `block`: the block to query. * Returns true if the block has background color properties, false otherwise. ``` fun setBackgroundColor( block: DesignBlock, color: RGBAColor,) ``` Set the background color of the given design block. Required scope: “fill/change” * `block`: the block whose background color should be set. * `color`: the color to set. ``` fun getBackgroundColor(block: DesignBlock): RGBAColor ``` Get the background color of the given design block. * `block`: the block whose background color should be queried. * Returns the background color. ``` fun setBackgroundColorEnabled( block: DesignBlock, enabled: Boolean,) ``` Enable or disable the background of the given design block. Required scope: “fill/change” * `block`: the block whose background should be enabled or disabled. * `enabled`: if true, the background will be enabled. ``` fun isBackgroundColorEnabled(block: DesignBlock): Boolean ``` Query if the background of the given design block is enabled. * `block`: the block whose background state should be queried. * Returns true if background is enabled, false otherwise. ## Full Code Here’s the full code: ``` engine.block.supportsOpacity(image)engine.block.setOpacity(image, value = 0.5F)engine.block.getOpacity(image) engine.block.supportsBlendMode(image)engine.block.setBlendMode(image, blendMode = BlendMode.MULTIPLY)engine.block.getBlendMode(image) if (engine.block.supportsBackgroundColor(image)) { engine.block.setBackgroundColor(page, Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F) // Red engine.block.getBackgroundColor(page) engine.block.setBackgroundColorEnabled(page, enabled = true) engine.block.isBackgroundColorEnabled(page)} ``` --- [Source](https:/img.ly/docs/cesdk/android/conversion/overview-44dc58) # Overview CreativeEditor SDK (CE.SDK) allows you to export designs into a variety of formats, making it easy to prepare assets for web publishing, printing, storage, and other workflows. You can trigger conversions either programmatically through the SDK’s API or manually using the built-in export options available in the UI. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Supported Input and Output Formats CE.SDK accepts a range of input formats when working with designs, including: Category Supported Formats **Images** `.png`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` **Video** `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC) **Audio** `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3) Need to import a format not listed here? CE.SDK allows you to create custom importers for any file type by using our Scene and Block APIs programmatically. When it comes to exporting or converting designs, the SDK supports the following output formats: Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. Each format serves different use cases, giving you the flexibility to adapt designs for your application’s needs. ## Conversion Methods There are two main ways to trigger a conversion: * **Programmatically:** Use CE.SDK’s API methods to perform conversions directly from your code. This gives you full control over the export process, allowing you to customize settings, automate workflows, and integrate with other systems. * **Through the UI:** End users can trigger exports manually through CE.SDK’s built-in export options. The UI provides an intuitive way to export designs without writing code, ideal for non-technical users. Both methods provide access to core conversion features, ensuring you can choose the workflow that fits your project. ## Customization Options When exporting designs, CE.SDK offers several customization options to meet specific output requirements: * **Resolution and DPI Settings:** Adjust the resolution for raster exports like PNG to optimize for screen or print. * **Output Dimensions:** Define custom width and height settings for the exported file, independent of the original design size. * **File Quality:** For formats that support compression (such as PNG or PDF), you can control the quality level to balance file size and visual fidelity. * **Background Transparency:** Choose whether to preserve transparent backgrounds or export with a solid background color. * **Page Selection:** When exporting multi-page documents (e.g., PDFs), you can select specific pages or export all pages at once. * **Video Frame Selection:** When exporting from a video, you can select a specific frame to export as an image, allowing for thumbnail generation or frame captures. These options help ensure that your exported content is optimized for its intended platform, whether it’s a website, a mobile app, or a print-ready document. --- [Source](https:/img.ly/docs/cesdk/android/concepts/undo-and-history-99479d) # Undo and History ``` // Manage history stacksval newHistory = engine.editor.createHistory()val oldHistory = engine.editor.getActiveHistory()engine.editor.setActiveHistory(newHistory)engine.editor.destroyHistory(oldHistory) engine.editor.onHistoryUpdated() .onEach { println("Editor history updated") } .launchIn(CoroutineScope(Dispatchers.Main)) // Push a new state to the undo stackengine.editor.addUndoStep() // Perform an undo, if possible.if (engine.editor.canUndo()) { engine.editor.undo()} // Perform a redo, if possible.if (engine.editor.canRedo()) { engine.editor.redo()} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to undo and redo steps in the `editor` API. ## Functions ``` fun createHistory(): History ``` * Brief: Create a history which consists of an undo/redo stack for editing operations. There can be multiple. But only one can be active at a time. * Returns the handle to the created history. ``` fun destroyHistory(history: History) ``` Destroy the given history, returns an error if the handle doesn’t refer to a history. * `history`: the history to be destroyed. ``` fun setActiveHistory(history: History) ``` Mark the given history as active, returns an error if the handle doesn’t refer to a history. All other histories get cleared from the active state. Undo/redo operations only apply to the active history. * `history`: the history to be marked as active. ``` fun getActiveHistory(): History ``` Get the handle to the currently active history. If there’s none it will be created. * Returns the handle to the active history. ``` fun addUndoStep() ``` Adds a new history state to the stack, if undoable changes were made. ``` fun undo() ``` Undo one step in the history if an undo step is available. ``` fun canUndo(): Boolean ``` If an undo step is available. * Returns true if an undo step is available. ``` fun redo() ``` Redo one step in the history if a redo step is available. ``` fun canRedo(): Boolean ``` If a redo step is available. * Returns true if a redo step is available. ``` fun onHistoryUpdated(): Flow ``` Subscribe to changes to the undo/redo history. * Returns flow of history updates. ## Full Code Here’s the full code: ``` // Manage history stacksval newHistory = engine.editor.createHistory()val oldHistory = engine.editor.getActiveHistory()engine.editor.setActiveHistory(newHistory)engine.editor.destroyHistory(oldHistory) engine.editor.onHistoryUpdated() .onEach { println("Editor history updated") } .launchIn(CoroutineScope(Dispatchers.Main)) // Push a new state to the undo stackengine.editor.addUndoStep() // Perform an undo, if possible.if (engine.editor.canUndo()) { engine.editor.undo()} // Perform a redo, if possible.if (engine.editor.canRedo()) { engine.editor.redo()} ``` --- [Source](https:/img.ly/docs/cesdk/android/concepts/scenes-e8596d) # Scenes ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun modifyingScenes( 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) // In engine only mode we have to create our own scene and page. if (engine.scene.get() == null) { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) } // Find all pages in our scene. val pages = engine.block.findByType(DesignBlockType.Page) // Use the first page we found. val page = pages.first() // Create a graphic block and add it to the scene's page. val block = engine.block.create(DesignBlockType.Graphic) val fill = engine.block.createFill(FillType.Image) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block = block, fill = fill) engine.block.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/imgly_logo.jpg", ) // The content fill mode 'Contain' ensures the entire image is visible. engine.block.setEnum( block = block, property = "contentFill/mode", value = "Contain", ) engine.block.appendChild(parent = page, child = block) // Zoom the scene's camera on our page. engine.scene.zoomToBlock(page) engine.stop()} ``` Commonly, a scene contains several pages which in turn contain any other blocks such as images and texts. A block (or design block) is the main building unit in CE.SDK. Blocks are organized in a hierarchy through parent-child relationships. A scene is a specialized block that acts as the root of this hierarchy. At any time, the engine holds only a single scene. Loading or creating a scene will replace the current one. ## Interacting With The Scene ### Creating or Using an Existing Scene When using the Engine’s API in the context of the CE.SDK editor, there’s already an existing scene. You can obtain a handle to this scene by calling the [SceneAPI](android/concepts/scenes-e8596d/)’s `fun get(): DesignBlock?` method. However, when using the Engine on its own you first have to create a scene, e.g. using `fun create(): DesignBlock`. See the [Creating Scenes](android/open-the-editor-23a1db/) guide for more details and options. ``` // In engine only mode we have to create our own scene and page. if (engine.scene.get() == null) { val scene = engine.scene.create() ``` Next, we need a page to place our blocks on. The scene automatically arranges its pages either in a vertical (the default) or horizontal layout. Again in the context of the editor, there’s already an existing page. To fetch that page call the [BlockAPI](android/concepts/blocks-90241e/)’s `fun findByType(blockType: DesignBlockType): List` method and use the first element of the returned list. When only using the engine, you have to create a page yourself and append it to the scene. To do that create the page using `fun fun create(): DesignBlock` and append it to the scene with `fun appendChild(parent: DesignBlock, child: DesignBlock)`. ``` val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) } // Find all pages in our scene. val pages = engine.block.findByType(DesignBlockType.Page) // Use the first page we found. val page = pages.first() ``` At this point, you should have a handle to an existing scene as well as a handle to its page. Now it gets interesting when we start to add different types of blocks to the scene’s page. ### Modifying the Scene As an example, we create a graphic block using the [BlockAPI](android/concepts/blocks-90241e/)’s `create()` method which we already used for creating our page. Then we set a rect shape and an image fill to this newly created block to give it a visual representation. To see what other kinds of blocks are available see the [Block Types](android/concepts/blocks-90241e/) in the API Reference. ``` // Create a graphic block and add it to the scene's page. val block = engine.block.create(DesignBlockType.Graphic) val fill = engine.block.createFill(FillType.Image) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block = block, fill = fill) ``` We set a property of our newly created image fill by giving it a URL to reference an image file from. We also make sure the entire image stays visible by setting the block’s content fill mode to `'Contain'`. To learn more about block properties check out the [Block Properties](android/concepts/blocks-90241e/) API Reference. ``` engine.block.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/imgly_logo.jpg", ) // The content fill mode 'Contain' ensures the entire image is visible. engine.block.setEnum( block = block, property = "contentFill/mode", value = "Contain", ) ``` And finally, for our image to be visible we have to add it to our page using `appendChild`. ``` engine.block.appendChild(parent = page, child = block) ``` To frame everything nicely and put it into view we direct the scene’s camera to zoom on our page. ``` // Zoom the scene's camera on our page. engine.scene.zoomToBlock(page) ``` ### Full Code Here’s the full code snippet for interacting with the scene: ``` import kotlinx.coroutines.*import ly.img.engine.* fun modifyingScenes( 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) // In engine only mode we have to create our own scene and page. if (engine.scene.get() == null) { val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) } // Find all pages in our scene. val pages = engine.block.findByType(DesignBlockType.Page) // Use the first page we found. val page = pages.first() // Create a graphic block and add it to the scene's page. val block = engine.block.create(DesignBlockType.Graphic) val fill = engine.block.createFill(FillType.Image) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Rect)) engine.block.setFill(block = block, fill = fill) engine.block.setString( block = fill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/imgly_logo.jpg", ) // The content fill mode 'Contain' ensures the entire image is visible. engine.block.setEnum( block = block, property = "contentFill/mode", value = "Contain", ) engine.block.appendChild(parent = page, child = block) // Zoom the scene's camera on our page. engine.scene.zoomToBlock(page) engine.stop()} ``` ## Exploring Scene Contents Using The Scene API Learn how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to explore scene contents through the `scene` API. ``` fun getPages(): List ``` Get the sorted list of pages in the scene. * Returns the sorted list of pages in the scene. ``` fun getCurrentPage(): DesignBlock? ``` Get the current page, i.e., the page of the first selected element if this page is at least 25% visible or, otherwise, the page nearest to the viewport center. * Returns the current page in the scene or null. ``` fun findNearestToViewPortCenterByType(blockType: DesignBlockType): List ``` Finds all blocks with the given type sorted by distance to viewport center. * `blockType`: the type to search for. * Returns a list of block ids sorted by distance to viewport center. ``` fun findNearestToViewPortCenterByKind(blockKind: String): List ``` Finds all blocks with the given kind sorted by distance to viewport center. * `blockKind`: the kind to search for. * Returns a list of block ids sorted by distance to viewport center. ``` fun setDesignUnit(designUnit: DesignUnit) ``` Converts all values of the current scene into the given design unit. * `designUnit`: the new design unit of the scene. ``` fun getDesignUnit(): DesignUnit ``` Returns the design unit of the current scene. * Returns The current design unit. ### Full Code Here’s the full code snippet for exploring a scene’s contents using the `scene` API: ``` val pages = engine.scene.getPages()val currentPage = engine.scene.getCurrentPage();val nearestPageByType = engine.scene.findNearestToViewPortCenterByType(DesignBlockType.Page).first();val nearestImageByKind = engine.sce.findNearestToViewPortCenterByKind("image").first(); engine.scene.setDesignUnit(DesignUnit.PIXEL) /* Now returns DesignUnit.PIXEL */engine.scene.getDesignUnit() ``` ## Exploring Scene Contents Using The Block API Learn how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to explore scenes through the `block` API. ### Functions ``` fun findAll(): List ``` Return all blocks currently known to the engine. * Returns a list of block ids. ``` fun findAllPlaceholders(): List ``` Return all placeholder blocks in the current scene. * Returns a list of block ids. ``` fun findByType(type: DesignBlockType): List ``` Finds all design blocks with the given type. * `type`: the type to search for. * Returns a list of block ids. ``` fun findByType(type: ShapeType): List ``` Finds all shape blocks with the given type. * `type`: the type to search for. * Returns a list of block ids. ``` fun findByType(type: EffectType): List ``` Finds all effect blocks with the given type. * `type`: the type to search for. * Returns a list of block ids. ``` fun findByType(type: BlurType): List ``` Finds all blur blocks with the given type. * `type`: the type to search for. * Returns a list of block ids. ``` fun findByKind(blockKind: String): List ``` Finds all blocks with the given kind. * `blockKind`: the kind to search for. * Returns a list of block ids. ``` fun findByName(name: String): List ``` Finds all blocks with the given name. * `name`: the name to search for. * Returns a list of block ids. ### Full Code Here’s the full code snippet for exploring a scene’s contents using the `block` API: ``` val allIds = engine.block.findAll()val allPlaceholderIds = engine.block.findAllPlaceholders()val allPages = engine.block.findByType(DesignBlockType.Page)val allImageFills = engine.block.findByType(FillType.Image)val allStarShapes = engine.block.findByType(ShapeType.Star)val allHalfToneEffects = engine.block.findByType(EffectType.HalfTone)val allUniformBlurs = engine.block.findByType(BlurType.Uniform)val allStickers = engine.block.findByKind("sticker")val ids = engine.block.findByName("someName") ``` --- [Source](https:/img.ly/docs/cesdk/android/concepts/resources-a58d71) # Working With Resources By default, a scene’s resources are loaded on-demand. You can manually trigger the loading of all resources in a scene of for specific blocks by calling `forceLoadResources`. Any set of blocks can be passed as argument and whatever resources these blocks require will be loaded. In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to forcibly pre-load all resources contained in a scene. ``` suspend fun forceLoadResources(blocks: List) ``` Begins loading the resources of the given blocks and their children. If the resource had been loaded earlier and resulted in an error, it will be reloaded. Note: This function is useful for preloading resources before they are needed. Warning: For elements with a source set, all elements in the source set will be loaded. * `blocks`: the blocks whose resources should be loaded. The given blocks don’t require to have resources and can have children blocks (e.g. a scene block or a page block). ### Full Code Here’s the full code: ``` val scene = requireNotNull(engine.scene.get()) // Forcing all resources of all the blocks in a scene or the resources of graphic block to loadengine.block.forceLoadResources(listOf(scene)) val graphics = engine.block.findByType(DesignBlockType)engine.block.forceLoadResources(graphics) ``` --- [Source](https:/img.ly/docs/cesdk/android/create-composition/overview-5b19c5) # Overview 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. All composition processing is handled entirely on the client side, ensuring fast, secure, and efficient editing 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 are building layouts manually through the UI or generating them dynamically with code, compositions give you the flexibility and control to design at scale. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Working with Multiple Pages and Artboards CE.SDK supports working with multiple artboards or canvases within a single document, enabling you to design multi-page layouts or create several design variations within the same project. Typical multi-page use cases include: * Designing multi-page marketing brochures. * Exporting designs as multi-page PDFs. * Building multiple versions of a design for different audiences or platforms. ## Working with Elements You can easily arrange and manage elements within a composition: * **Positioning and Aligning:** Move elements precisely and align them to each other or to the canvas. * **Guides and Snapping:** Use visual guides and automatic snapping to align objects accurately. * **Grouping:** Group elements for easier collective movement and editing. * **Layer Management:** Control the stacking order and organize elements in layers. * **Locking:** Lock elements to prevent accidental changes during editing. ## UI vs. Programmatic Creation ### Using the UI The CE.SDK UI provides drag-and-drop editing, alignment tools, a layer panel, and snapping guides, making it easy to visually build complex compositions. You can group, align, lock, and arrange elements directly through intuitive controls. ### Programmatic Creation You can also build compositions programmatically by using CE.SDK’s APIs. This is especially useful for: * Automatically generating large volumes of designs. * Creating data-driven layouts. * Integrating CE.SDK into a larger automated workflow. ## Exporting Compositions CE.SDK compositions can be exported in several formats: Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. --- [Source](https:/img.ly/docs/cesdk/android/concepts/pages-7b6bae) # Pages Although the editor can manage a collection of pages with varying dimensions, our interfaces are presently designed to maintain a consistent dimension across all pages. Consequently, loading scenes with pages of different dimensions may lead to unexpected behavior, and the editor might adjust your scene accordingly. --- [Source](https:/img.ly/docs/cesdk/android/concepts/events-353f97) # Events ``` val coroutineScope = CoroutineScope(Dispatchers.Main)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.Star))engine.block.setFill(block, fill = engine.block.createFill(FillType.Color))engine.block.appendChild(parent = page, child = block) engine.event.subscribe(listOf(block)) .onEach { events -> events.forEach { event -> println("Event: ${event.type} ${event.block}") if (engine.block.isValid(event.block)) { val type = engine.block.getType(event.block) println("Block type: $type") } } } .launchIn(coroutineScope) coroutineScope.launch { delay(1000) engine.block.setRotation(block, radians = 0.5F * PI.toFloat()) delay(1000) engine.block.destroy(block) delay(1000)} ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to subscribe to creation, update, and destruction events of design blocks. ## Subscribing to Events The event API provides a single function to subscribe to design block events. The types of events are: * `Created`: The design block was just created. * `Updated`: A property of the design block was updated. * `Destroyed`: The design block was destroyed. Note that a destroyed block will have become invalid and trying to use Block API functions on it will result in an exception. You can always use the Block API’s `isValid` function to verify whether a block is valid before use. All events that occur during an engine update are batched, deduplicated, and always delivered at the very end of the engine update. Deduplication means you will receive at most one `Updated` event per block per subscription, even though there could potentially be multiple updates to a block during the engine update. To be clear, this also means the order of the event list provided to your event callback won’t reflect the actual order of events within an engine update. ``` 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. ## Full Code Here’s the full code: ``` val coroutineScope = CoroutineScope(Dispatchers.Main)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.Star))engine.block.setFill(block, fill = engine.block.createFill(FillType.Color))engine.block.appendChild(parent = page, child = block) engine.event.subscribe(listOf(block)) .onEach { events -> events.forEach { event -> println("Event: ${event.type} ${event.block}") if (engine.block.isValid(event.block)) { val type = engine.block.getType(event.block) println("Block type: $type") } } } .launchIn(coroutineScope) coroutineScope.launch { delay(1000) engine.block.setRotation(block, radians = 0.5F * PI.toFloat()) delay(1000) engine.block.destroy(block) delay(1000)} ``` --- [Source](https:/img.ly/docs/cesdk/android/concepts/editing-workflow-032d27) # Editing Workflow ## Roles User roles allow the CE.SDK to change and adapt its UI layout and functionality to provide the optimal editing experience for a specific purpose. ### Creator The `Creator` role is the most powerful and least restrictive role that is offered by the CE.SDK. Running the editor with this role means that there are no limits to what the user can do with the loaded scene. Elements can be added, moved, deleted, and modified. All types of controls for modifying the selected elements are shown inside of the inspector. ### Adopter The `Adopter` role allows new elements to be added and modified. Existing elements of a scene are only modifiable based on the set of constraints that the `Creator` has manually enabled. This provides the `Adopter` with a simpler interface that is reduced to only the properties that they should be able to change and prevents them from accidentally changing or deleting parts of a design that should not be modified. An example use case for how such a distinction between `Creator` and `Adopter` roles can provide a lot of value is the process of designing business cards. A professional designer (using the `Creator` role) can create a template design of the business card with the company name, logo, colors, etc. They can then use the constraints to make only the name text editable for non-creators. Non-designers (either the employees themselves or the HR department) can then easily open the design in a CE.SDK instance with the `Adopter` role and are able to quickly change the name on the business card and export it for printing, without a designer having to get involved. ### Role customization Roles in the CE.SDK are sets of global scopes and settings. When changing the role via the `setRole` command in the EditorAPI, the internal defaults for that role are applied as described in the previous sections. The CE.SDK and Engine provide a `onRoleChanged` callback subscription on the EditorAPI. Callbacks registered here are invoked whenever the role changes and can be used to configure additional settings or adjust the default scopes and settings. --- [Source](https:/img.ly/docs/cesdk/android/concepts/edit-modes-1f5b6c) # Editor State ``` engine.editor.onStateChanged() .onEach { println("Editor history has changed") } .launchIn(CoroutineScope(Dispatchers.Main)) // Native modes: "Transform", "Crop", "Text"engine.editor.setEditMode("Crop")engine.editor.getEditMode() // "Crop" engine.editor.isInteractionHappening() // Query information about the text cursor positionengine.editor.getTextCursorPositionInScreenSpaceX()engine.editor.getTextCursorPositionInScreenSpaceY() ``` The CreativeEditor SDK operates in different states called **Edit Modes**, each designed for a specific type of interaction on the canvas: * `Transform`: this is the default mode which allow to move, resize and manipulate things on the canvas * `Text`: Allows to edit the text elements on the canvas * `Crop`: Allow to Crop media blocks (images, videos, etc…) * `Trim`: Trim the clips in video mode * `Playback`: Play the media (mostly video) in video mode While users typically interact with these modes through the UI (e.g., showing or hiding specific controls based on the active mode), it’s also possible to manage them programmatically via the engine’s API, though this isn’t always required. In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to set and query the editor state in the `editor` API, i.e., what type of content the user is currently able to edit. ## State The editor state consists of the current edit mode, which informs what type of content the user is currently able to edit. The edit mode can be set to either `Transform`, `Crop`, `Text`, or a custom user-defined one. You can also query the intended mouse cursor and the location of the text cursor while editing text. Instead of having to constantly query the state in a loop, you can also be notified when the state has changed to then act on these changes in a callback. ``` fun onStateChanged(): Flow ``` Subscribe to changes to the editor state. * Returns flow of editor state change events. ``` fun setEditMode(editMode: String) ``` Set the edit mode of the editor. An edit mode defines what type of content can currently be edited by the user. Note: The initial edit mode is “Transform”. * `editMode`: “Transform”, “Crop”, “Text”, “Playback”, “Trim” or a custom value. ``` fun getEditMode(): String ``` Get the current edit mode of the editor. An edit mode defines what type of content can currently be edited by the user. * Returns “Transform”, “Crop”, “Text”, “Playback”, “Trim” or a custom value. ``` @UnstableEngineApifun isInteractionHappening(): Boolean ``` If an user interaction is happening, e.g., a resize edit with a drag handle or a touch gesture. * Returns true if an interaction is happening. ## Cursor ``` fun getTextCursorPositionInScreenSpaceX(): Float ``` Get the current text cursor’s x position in screen space. * Returns the text cursor’s x position in screen space. ``` fun getTextCursorPositionInScreenSpaceY(): Float ``` Get the current text cursor’s y position in screen space. * Returns the text cursor’s y position in screen space. ## Full Code Here’s the full code: ``` engine.editor.onStateChanged() .onEach { println("Editor history has changed") } .launchIn(CoroutineScope(Dispatchers.Main)) // Native modes: "Transform", "Crop", "Text"engine.editor.setEditMode("Crop")engine.editor.getEditMode() // "Crop" engine.editor.isInteractionHappening() // Query information about the text cursor positionengine.editor.getTextCursorPositionInScreenSpaceX()engine.editor.getTextCursorPositionInScreenSpaceY() ``` --- [Source](https:/img.ly/docs/cesdk/android/concepts/blocks-90241e) # Blocks Blocks are the base building blocks for scenes and represent elements on the canvas. You’re able to create complex hierarchies of blocks by appending them as children to other blocks either directly or through groups. By default, the following blocks are available: * Page: `//ly.img.ubq/page` or `page` * Graphic: `//ly.img.ubq/graphic` or `graphic` * Text: `//ly.img.ubq/text` or `text` * Audio: `//ly.img.ubq/audio` or `audio` * Cutout: `//ly.img.ubq/cutout` or `cutout` ## Lifecycle Only blocks that are direct or indirect children of a `page` block are rendered. Scenes without any `page` child may not be properly displayed by the CE.SDK editor. ### Functions ``` fun create(blockType: DesignBlockType): DesignBlock ``` Create a new block. * `blockType`: the type of the block that shall be created. * Returns the created blocks handle. To create a scene, use [`scene.create`](android/open-the-editor-23a1db/) instead. ``` suspend fun saveToString( blocks: List, allowedResourceSchemes: List = listOf("bundle", "file", "http", "https"),): String ``` Saves the given blocks to a proprietary string. If a resource uri has a scheme that is not in `allowedResourceSchemes`, an exception will be thrown. Note: All given block handles must be valid, otherwise an exception will be thrown. * `blocks`: the blocks to save. * `allowedResourceSchemes`: the list of allowed resource schemes in the scene. * Returns a string representation of the blocks. ``` suspend fun saveToArchive(blocks: List): ByteBuffer ``` Saves the given blocks to an archive. Note: All given block handles must be valid, otherwise this call returns an error. * `blocks`: the blocks to save. * Returns a string representation of the blocks. ``` suspend fun loadFromString(block: String): List ``` Loads existing blocks from the given string. The blocks are not attached by default and won’t be visible until attached to a page or the scene. The UUID of the loaded blocks is replaced with a new one. * `block`: a string representing the given blocks. * Returns a list of loaded blocks. ``` suspend fun loadFromArchive(archiveUri: Uri): List ``` Loads existing blocks from an archive. The blocks are not attached by default and won’t be visible until attached to a page or the scene. The UUID of the loaded blocks is replaced with a new one. * `archiveUri`: the uri of the blocks archive file. * Returns a list of loaded blocks. ``` fun getType(block: DesignBlock): String ``` Get the type of the given block, fails if the block is invalid. * `block`: the block to query. * Returns the block type. ``` fun setName( block: DesignBlock, name: String,) ``` Update a block’s name. * `block`: the block to update. * `name`: the name to set. ``` fun getName(block: DesignBlock): String ``` Get a block’s name. * `block:`: The block to query. * Returns The block’s name. ``` fun duplicate(block: DesignBlock): DesignBlock ``` Duplicates a block including its children. Required scope: “lifecycle/duplicate” If the block is parented to a track that is set always-on-bottom, the duplicate is inserted in the same track immediately after the block. Otherwise, the duplicate is moved up in the hierarchy. * `block`: the block to duplicate. * Returns the handle of the duplicate. ``` fun destroy(block: DesignBlock) ``` Destroys a block. Required scope: “lifecycle/destroy” * `block`: the block to destroy. ``` fun isValid(block: DesignBlock): Boolean ``` Check if a block is valid. A block becomes invalid once it has been destroyed. * `block`: the block to query. * Returns true if the block is valid, false otherwise. ### Full Code In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify scenes through the `block` Api. ``` // Create, save and load blocksval block = engine.block.create(DesignBlockType.Graphic)val savedBlocksString = engine.block.saveToString(blocks = listOf(block))val loadedBlocksString = engine.block.loadFromString(savedBlocks)val savedBlocksArchive = engine.block.saveToArchive(blocks = listOf(block))val loadedBlocksArchive = engine.block.loadFromArchive(blocksUri = Uri.parse("https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1_blocks.zip")) // Check a blocks typeval blockType = engine.block.getType(block) // Alter a blocks nameengine.block.setName(block, name = "someName")val name = engine.block.getName(block) // You may duplicate or destroy blocksval duplicate = engine.block.duplicate(block)engine.block.destroy(duplicate)engine.block.isValid(duplicate) // false ``` ## Properties ### UUID A universally unique identifier (UUID) is assigned to each block upon creation and can be queried. This is stable across save & load and may be used to reference blocks. ``` fun getUUID(block: DesignBlock): String ``` Get a block’s unique identifier. * `block:`: The block to query. * Returns The block’s UUID. ### Reflection For every block, you can get a list of all its properties by calling `findAllProperties`. Properties specific to a block are prefixed with the block’s type followed by a forward slash. There are also common properties shared between blocks which are prefixed by their respective type. A list of all properties can be found in the [Blocks Overview](android/concepts/blocks-90241e/). ``` fun findAllProperties(block: DesignBlock): List ``` Get all available properties of a block. * `block`: the block whose properties should be queried. * Returns a list of the property names. Given a property you can query its type using `getPropertyType`. ``` fun getPropertyType(property: String): PropertyType ``` Get the type of a property given its name. * `property`: the name of the property whose type should be queried. * Returns the property type. To get a list of possible values for an enum property call `getEnumValues(enumProperty: string): string[]`. ``` fun getEnumValues(enumProperty: String): List ``` Get all the possible values of an enum given an enum property. * `enumProperty`: the name of the property whose enum values should be queried. * Returns a list of the enum value names as string. Some properties can only be written to or only be read. To find out what is possible with a property, you can use the `isPropertyReadable` and `isPropertyWritable` methods. ``` fun isPropertyReadable(property: String): Boolean ``` Check if a property with a given name is readable. * `property`: the name of the property whose type should be queried. * Returns whether the property is readable or not. Will return false for unknown properties. ``` fun isPropertyWritable(property: String): Boolean ``` Check if a property with a given name is writeable. * `property`: the name of the property whose type should be queried. * Returns whether the property is writeable or not. Will return false for unknown properties. ### Generic Properties There are dedicated setter and getter functions for each property type. You have to provide a block and the property path. Use `findAllProperties` to get a list of all the available properties a block has. Please make sure you call the setter and getter function matching the type of the property you want to set or query or else you will get an error. Use `getType` to figure out the pair of functions you need to use. ``` fun findAllProperties(block: DesignBlock): List ``` Get all available properties of a block. * `block`: the block whose properties should be queried. * Returns a list of the property names. ``` fun setBoolean( block: DesignBlock, property: String, value: Boolean,) ``` Set a boolean property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getBoolean( block: DesignBlock, property: String,): Boolean ``` Get the value of a boolean property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to set. * Returns the value of the property. ``` fun setInt( block: DesignBlock, property: String, value: Int,) ``` Set an int property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getInt( block: DesignBlock, property: String,): Int ``` Get the value of an int property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to set. * Returns the value of the property. ``` fun setFloat( block: DesignBlock, property: String, value: Float,) ``` Set a float property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getFloat( block: DesignBlock, property: String,): Float ``` Get the value of a float property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to set. * Returns the value of the property. ``` fun setDouble( block: DesignBlock, property: String, value: Double,) ``` Set a double property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getDouble( block: DesignBlock, property: String,): Double ``` Get the value of a double property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to set. * Returns the value of the property. ``` fun setString( block: DesignBlock, property: String, value: String,) ``` Set a string property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getString( block: DesignBlock, property: String,): String ``` Get the value of a string property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to set. * Returns the value of the property. ``` fun setColor( block: DesignBlock, property: String, value: Color,) ``` Set a color property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getColor( block: DesignBlock, property: String,): Color ``` Get the value of a color property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to set. * Returns the value of the property. ``` fun setEnum( block: DesignBlock, property: String, value: String,) ``` Set an enum property of the given design block to the given value. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `value`: the value to set. ``` fun getEnum( block: DesignBlock, property: String,): String ``` Get the value of an enum property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to get. * Returns the value of the property. ``` fun setGradientColorStops( block: DesignBlock, property: String, colorStops: List,) ``` Set a gradient color stops property of the given design block. * `block`: the block whose property should be set. * `property`: the name of the property to set. * `colorStops`: the list of color stops. ``` fun getGradientColorStops( block: DesignBlock, property: String,): List ``` Get the gradient color stops property of the given design block. * `block`: the block whose property should be queried. * `property`: the name of the property to query. * Returns the list of gradient color stops. ``` fun setSourceSet( block: DesignBlock, property: String, sourceSet: List,) ``` Set the source set of a source set property of the given block. The crop and content fill mode of the associated block will be set to the default values. * `block`: the block whose source set should be set. * `property`: the name of the property to set. * `sourceSet`: the new source set. ``` fun getSourceSet( block: DesignBlock, property: String,): List ``` Returns the source set of a source set property of the given block. * `block`: the block whose property should be queried. * `property`: the name of the property to get. * Returns the source set of the given block. ``` suspend fun addImageFileUriToSourceSet( block: DesignBlock, property: String, uri: String,) ``` Add a source to the `sourceSet` property of the given block. If there already exists in source set an image with the same width, that existing image will be replaced. If the source set is or gets empty, the crop and content fill mode of the associated block will be set to the default values. Note: This fetches the resource from the given URI to obtain the image dimensions. It is recommended to use setSourceSet if the dimension is known. * `block`: the block to update. * `property`: the name of the property to modify. * `uri`: the source to add to the source set. ``` suspend fun addVideoFileUriToSourceSet( block: DesignBlock, property: String, uri: String,) ``` Add a source to the `sourceSet` property of the given block. If there already exists in source set a video with the same width, that existing video will be replaced. If the source set is or gets empty, the crop and content fill mode of the associated block will be set to the default values. Note: This fetches the resource from the given URI to obtain the video dimensions. It is recommended to use setSourceSet if the dimension is known. * `block`: the block to update. * `property`: the name of the property to modify. * `uri`: the source to add to the source set. ### Modifying Properties Here’s the full code snippet for modifying a block’s properties: ``` val uuid = engine.block.getUUID(block)val propertyNamesStar = engine.block .findAllProperties(starShape) // List [ "shape/star/innerDiameter", "shape/star/points", "opacity/value", ... ]val propertyNamesImage = engine.block .findAllProperties(imageFill) // List [ "fill/image/imageFileURI", "fill/image/previewFileURI", "fill/image/externalReference", ... ]val propertyNamesText = engine.block .findAllProperties(text) // List [ "text/text", "text/fontFileUri", "text/externalReference", "text/fontSize", "text/horizontalAlignment", ... ] val pointsType = engine.block.getPropertyType(property = "shape/star/points") // "Int"val alignmentType = engine.block.getPropertyType(property = "text/horizontalAlignment") // "Enum"engine.block.getEnumValues(enumProperty = "text/horizontalAlignment")val readable = engine.block.isPropertyReadable("shape/star/points")val writable = engine.block.isPropertyWritable("shape/star/points") // Generic Propertiesengine.block.setBoolean(scene, property = "scene/aspectRatioLock", value = false)engine.block.getBoolean(scene, property = "scene/aspectRatioLock")engine.block.setInt(star, property = "shape/star/points", value = points + 2)val points = engine.block.getInt(star, property = "shape/star/points")engine.block.setFloat(star, property = "shape/star/innerDiameter", value = 0.75F)engine.block.getFloat(star, property = "shape/star/innerDiameter")val audio = engine.block.create(DesignBlockType.Audio)engine.block.appendChild(scene, audio)engine.block.setDouble(audio, property = "playback/duration", value = 1.0)engine.block.getDouble(audio, property = "playback/duration")engine.block.setString(text, property = "text/text", value = "*o*")engine.block.setString( imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_4.jpg")engine.block.getString(text, property = "text/text")engine.block.getString(imageFill, property = "fill/image/imageFileURI")engine.block.setColor( colorFill, property = "fill/color/value", value = Color.fromString("#FFFFFFFF")) // Whiteengine.block.getColor(colorFill, property = "fill/color/value")engine.block.setEnum(text, property = "text/horizontalAlignment", value = "Center")engine.block.setEnum(text, property = "text/verticalAlignment", value = "Center")engine.block.getEnum(text, property = "text/horizontalAlignment")engine.block.getEnum(text, property = "text/verticalAlignment")engine.block.setGradientColorStops( block = gradientFill, property = "fill/gradient/colors", colorStops = listOf( GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 0.1F, g = 0.2F, b = 0.3F, a = 0.4F)), GradientColorStop(stop = 1F, color = Color.fromSpotColor("test")) ))engine.block.getGradientColorStops(block = gradientFill, property = "fill/gradient/colors") val imageFill = engine.block.createFill(FillType.Image)engine.block.setSourceSet( block = imageFill, property = "fill/image/sourceSet", sourceSet = listOf( Source( uri = Uri.parse("http://img.ly/my-image.png"), width = 800, height = 600 ) ) )engine.block.getSourceSet(block = imageFill, property = "fill/image/sourceSet")engine.block.addImageFileUriToSourceSet(block = imageFill, property = "fill/image/sourceSet", uri = "https://img.ly/static/ubq_samples/sample_1.jpg");val videoFill = engine.block.createFill(FillType.Video)engine.block.addVideoFileUriToSourceSet(block = videoFill, property = "fill/video/sourceSet", uri = "https://img.ly/static/example-assets/sourceset/1x.mp4"); ``` ### Kind Property The `kind` of a design block is a custom string that can be assigned to a block in order to categorize it and distinguish it from other blocks that have the same type. The user interface can then customize its appearance based on the kind of the selected blocks. It can also be used for automation use cases in order to process blocks in a different way based on their kind. ``` fun setKind( block: DesignBlock, kind: String,) ``` Set the kind of the given block, fails if the block is invalid. * `block`: the block to modify. * `kind`: the block kind. ``` fun getKind(block: DesignBlock): String ``` Get the kind of the given block, fails if the block is invalid. * `block`: the block to query. * Returns the block kind. ``` fun findByKind(blockKind: String): List ``` Finds all blocks with the given kind. * `blockKind`: the kind to search for. * Returns a list of block ids. #### Full Code In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify a and query the kind property of design blocks through the `block` API. ``` engine.block.setKind(text, kind = "title")val kind = engine.block.getKind(text)val allTitles = engine.block.findByKind(blockKind = "title") ``` ## Selection & Visibility A block can potentially be _invisible_ (in the sense that you can’t see it), even though `isVisible()` returns true. This could be the case when a block has not been added to a parent, the parent itself is not visible, or the block is obscured by another block on top of it. ### Select blocks and change their visibility ``` fun setSelected( block: DesignBlock, selected: Boolean,) ``` Update the selection state of a block. Fails for invalid blocks. Note: Previously selected blocks remain selected. Required scope: “editor/select” * `block`: the block to query. * `selected`: whether or not the block should be selected. ``` fun isSelected(block: DesignBlock): Boolean ``` Get the selected state of a block. * `block`: the block to query. * Returns true if the block is selected, false otherwise. ``` fun select(block: DesignBlock) ``` Selects the given block and deselects all other blocks. * `block`: the block to be selected. ``` fun findAllSelected(): List ``` Get all currently selected blocks. * Returns An array of block ids. ``` fun setVisible( block: DesignBlock, visible: Boolean,) ``` Update a block’s visibility. Required scope: “layer/visibility” * `block`: the block to update. * `visible`: whether the block shall be visible. ``` fun isVisible(block: DesignBlock): Boolean ``` Query a block’s visibility. * `block`: the block to query. * Returns true if visible, false otherwise. ``` fun setClipped( block: DesignBlock, clipped: Boolean,) ``` Update a block’s clipped state. Required scope: “layer/clipping” * `block`: the block to update. * `clipped`: whether the block should clips its contents to its frame. ``` fun isClipped(block: DesignBlock): Boolean ``` Query a block’s clipped state. If `true`, the block should clip * `block`: the block to query. * Returns true if clipped, false otherwise. ``` fun onSelectionChanged(): Flow ``` Subscribe to changes in the current set of selected blocks. * Returns flow of selected block change events. ``` fun onClicked(): Flow ``` Subscribe to block click events. Note: `DesignBlock` is emitted at the end of the engine update if it has been clicked. * Returns flow of block click events. ``` fun isIncludedInExport(block: DesignBlock): Boolean ``` Query if the given block is included on the exported result. * `block`: the block to query if it’s included on the exported result. * Returns true, if the block is included on the exported result, false otherwise. ``` fun setIncludedInExport( block: DesignBlock, enabled: Boolean,) ``` Set if you want the given design block to be included in exported result. * `block`: the block whose exportable state should be set. * `enabled`: if true, the block will be included on the exported result. ### Full Code In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify scenes through the `block` API. ``` engine.block.setSelected(block, selected = true)val isSelected = engine.block.isSelected(block)engine.block.select(block)val selectedIds = engine.block.findAllSelected() val isVisible = engine.block.isVisible(block)engine.block.setVisible(block, visible = true) val isClipped = engine.block.isClipped(page)engine.block.setClipped(page, clipped = true) val coroutineScope = CoroutineScope(Dispatchers.Main)engine.block.onSelectionChanged() .onEach { println("Change in the set of selected blocks") } .launchIn(coroutineScope)engine.block.onClicked() .onEach { println("Block $it was clicked") } .launchIn(coroutineScope) val isIncludedInExport = engine.block.isIncludedInExport(block)engine.block.setIncludedInExport(block, enabled = true) ``` ## State Blocks can perform operations that take some time or that can end in bad results. When that happens, blocks put themselves in a pending state or an error state and visual feedback is shown pertaining to the state. When an external operation is done to blocks, for example with a plugin, you may want to manually set the block’s state to pending (if that external operation takes time) or to error (if that operation resulted in an error). The possible states of a block are: ``` data object Ready : BlockState data class Pending( @FloatRange(from = 0.0, to = 1.0) val progress: Float) : BlockState data class Error(val type: Type) : BlockState { enum class Type { AUDIO_DECODING, // Failed to decode the block's audio stream. IMAGE_DECODING, // Failed to decode the block's image stream. FILE_FETCH, // Failed to retrieve the block's remote content. UNKNOWN, // An unknown error occurred. VIDEO_DECODING // Failed to decode the block's video stream. }} ``` When calling `getState`, the returned state reflects the combined state of a block, the block’s fill, the block’s shape and the block’s effects. If any of these blocks is in an `Error` state, the returned state will reflect that error. If none of these blocks is in error state but any is in `Pending` state, the returned state will reflect the aggregate progress of the block’s progress. If none of the blocks are in error state or pending state, the returned state is `Ready`. ``` fun getState(block: DesignBlock): BlockState ``` Get the current state of a block. Note If this block is in error state or this block has a `Shape` block, `Fill` block or `Effect` block(s), that is in error state, the returned state will be `BlockState.Error`. Else, if this block is in pending state or this block has a `Shape` block, `Fill` block or `Effect` block(s), that is in pending state, the returned state will be `BlockState.Pending`. Else, the returned state will be `BlockState.Ready`. * `block`: the block whose state should be queried. * Returns the state of the block. ``` fun setState( block: DesignBlock, state: BlockState,) ``` Set the state of a block. * `block`: the block whose state should be set. * `state`: the new state to set. ``` fun onStateChanged(blocks: List): Flow> ``` Subscribe to changes to the state of a block. Like `getState`, the state of a block is determined by the state of itself and its `Shape`, `Fill` and `Effect` block(s). * `blocks`: a list of blocks to filter events by. If the list is empty, events for every block are sent. * Returns flow of block state change events. ### Full Code In this example, we will show you how to use [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to retrieve’s a block’s state and to manually set a block in a pending state, an error state or back to a ready state. ``` engine.block.onStateChanged(listOf(block)) .onEach { println("State of blocks $it is updated.") } .launchIn(CoroutineScope(Dispatchers.Main))val state = engine.block.getState(block)engine.block.setState(block, state = BlockState.Pending(progress = 0.5F))engine.block.setState(block, state = BlockState.Ready)engine.block.setState(block, state = BlockState.Error(BlockState.Error.Type.IMAGE_DECODING)) ``` --- [Source](https:/img.ly/docs/cesdk/android/concepts/design-units-cc6597) # Design Units The CreativeEditor SDK (CE.SDK) Engine supports three different design units for all layouting values. This includes element positions, sizes and bleed margins. Supported design units are: * Pixels `px` * Millimeters `mm` * Inches `in` ## Font sizes Unlike all other values, font sizes are always defined in points `pt`. ## DPI If a scene uses a unit other than pixel `px`, the `dpi` property of that scene determines all conversions between the unit and pixels whenever necessary. Therefore, the `dpi` controls the export resolution of such a scenes blocks. This `dpi` value is also used in order to convert all font sizes into the design unit of the current scene. If the `dpi` property is changed in a scene with pixel unit, all text block’s font sizes are automatically adjusted so that the text stays visually the same size. --- [Source](https:/img.ly/docs/cesdk/android/colors/for-screen-1911f8) # For Screen --- [Source](https:/img.ly/docs/cesdk/android/colors/create-color-palette-7012e0) # Create a Color Palette ``` import androidx.compose.runtime.Composableimport androidx.compose.runtime.rememberimport androidx.compose.ui.graphics.Colorimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign // Add this composable to your NavHost@Composablefun ColorPaletteEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", ) val editorConfiguration = EditorConfiguration.rememberForDesign( colorPalette = remember { listOf( Color(0xFF4A67FF), Color(0xFFFFD333), Color(0xFFC41230), Color(0xFF000000), Color(0xFFFFFFFF), ) }, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` In this example, we will show you how to make color palette configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). ## Configuration Color palette configuration is part of the `EditorConfiguration` class. Use the `EditorConfiguration.getDefault` helper function to make color palette configurations. * `colorPalette` - the color palette used for UI elements that contain predefined color options, e.g., for “Fill Color” or “Stroke Color”. ``` colorPalette = remember { listOf( Color(0xFF4A67FF), Color(0xFFFFD333), Color(0xFFC41230), Color(0xFF000000), Color(0xFFFFFFFF), )}, ``` ## Full Code Here’s the full code: ``` import androidx.compose.runtime.Composableimport androidx.compose.runtime.rememberimport androidx.compose.ui.graphics.Colorimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign // Add this composable to your NavHost@Composablefun ColorPaletteEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", ) val editorConfiguration = EditorConfiguration.rememberForDesign( colorPalette = remember { listOf( Color(0xFF4A67FF), Color(0xFFFFD333), Color(0xFFC41230), Color(0xFF000000), Color(0xFFFFFFFF), ) }, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` --- [Source](https:/img.ly/docs/cesdk/android/colors/conversion-bcd82b) # Color Conversion To ease implementing advanced color interfaces, you may rely on the engine to perform color conversions. Converts a color to the given color space. * `color`: The color to convert. * `colorSpace`: The color space to convert to. * Returns The converted color. ``` // Convert a colorval rgbaGreen = Color.fromRGBA(r = 0F, g = 1F, b = 0F, a = 0F)val cmykGreen = engine.editor.convertColorToColorSpace(color = rgbaGreen, colorSpace = ColorSpace.CMYK) ``` --- [Source](https:/img.ly/docs/cesdk/android/colors/basics-307115) # Basics When specifying a color property, you can use one of three color spaces: [RGB](https://en.wikipedia.org/wiki/RGB_color_model), [CMYK](https://en.wikipedia.org/wiki/CMYK_color_model) and [spot color](https://en.wikipedia.org/wiki/Spot_color). The following properties can be set with the function `setColor` and support all three color spaces: * `'backgroundColor/color'` * `'camera/clearColor'` * `'dropShadow/color'` * `'fill/color/value'` * `'stroke/color'` ## RGB RGB is the color space used when rendering a color to a screen. All values of `R`, `G`, `B`must be between `0.0` and `1.0` When using RGB, you typically also specify opacity or alpha as a value between `0.0` and `1.0` and is then referred to as `RGBA`. When a RGB color has an alpha value that is not `1.0`, it will be rendered to screen with corresponding transparency. ## CMYK CMYK is the color space used when color printing. All values of `C`, `M`, `Y`, and `K` must be between `0.0` and `1.0` When using CMYK, you can also specify a tint value between `0.0` and `1.0`. When a CMYK color has a tint value that is not `1.0`, it will be rendered to screen as if transparent over a white background. When rendering to screen, CMYK colors are first converted to RGB using a simple mathematical conversion. Currently, the same conversion happens when exporting a scene to a PDF file. ## Spot Color Spot colors are typically used for special printers or other devices that understand how to use them. Spot colors are defined primarily by their name as that is the information that the device will use to render. For the purpose of rendering a spot color to screen, it must be given either an RGB or CMYK color approximation or both. These approximations adhere to the same restrictions respectively described above. You can specify a tint as a value between `0.0` and `1.0` which will be interpreted as opacity when rendering to screen. It is up to the special printer or device how to interpret the tint value. You can also specify an external reference, a string describing the origin of this spot color. When rendering to screen, the spot color’s RGB or CMYK approximation will be used, in that order of preference. When exporting a scene to a PDF file, spot colors will be saved as a [Separation Color Space](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.6.pdf#G9.1850648). Using a spot color is a two step process: 1. you define a spot color with its name and color approximation(s) in the spot color registry. 2. you instantiate a spot color with its name, a tint and an external reference. The spot color registry allows you to: * list the defined spot colors * define a new a spot color with a name and its RGB or CMYK approximation * re-define an existing spot color’s RGB or CMYK approximation * retrieve the RGB or CMYK approximation of an already defined spot color * remove a spot color from the list of defined spot colors Multiple blocks and their properties can refer to the same spot color and each can have a different tint and external reference. **Warning** If a block’s color property refers to an undefined spot color, the default color magenta with an RGB approximation of (1, 0, 1) will be used. ## Converting between colors A utility function `convertColorToColorSpace` is provided to create a new color from an existing color and a new color space. RGB and CMYK colors can be converted between each other and is done so using a simple mathematical conversion. Spot colors can be converted to RGB and CMYK simply by using the corresponding approximation. RGB and CMYK colors cannot be converted to spot colors. ## Custom Color Libraries You can configure CE.SDK with custom color libraries. More information is found [here](android/colors/create-color-palette-7012e0/). --- [Source](https:/img.ly/docs/cesdk/android/colors/overview-16a177) # Overview 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. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## For Print and Screen Designing for screen and designing for print involve different color requirements, and CreativeEditor SDK (CE.SDK) is built to support both. * **Screen workflows** use RGB-based color spaces like sRGB and Display P3. These spaces are optimized for digital displays, ensuring vibrant, consistent colors across different devices. * **Print workflows** typically rely on CMYK and Spot Colors. These spaces reflect how physical inks combine on paper and require more precise color control to match print output. CE.SDK makes it easy to design for both by: * Supporting inputs in multiple color spaces, including sRGB, CMYK, and Spot Colors. * Automatically managing color conversions between spaces. * Providing tooling to ensure that exported files are optimized for either screen (e.g., PNG, WebP) or print (e.g., PDF with Spot Color support). **Note:** CE.SDK allows you to specify colors using CMYK values, but these are always converted to RGB internally. You cannot produce a true CMYK PDF with CE.SDK, and we do not support print-specific PDF variants like PDF/X. However, Spot Colors are fully supported and properly embedded in exported PDFs. ## Color Management Color management is the process of controlling how colors are represented across different devices and outputs—such as screens, printers, and files—to ensure that the colors you see during design match the final result. In CreativeEditor SDK (CE.SDK), color management includes: * Supporting multiple input color spaces (e.g., sRGB, Display P3 for screens, CMYK, Spot Colors for print). * Handling accurate conversions between color spaces (e.g., RGB ↔ CMYK, approximating Spot Colors). * Allowing custom color palettes and color libraries to maintain consistent color choices across a project or brand. * Providing tools to adjust color properties like brightness, contrast, and temperature. * Ensuring consistent color appearance across different export formats and output mediums. Color management ensures that your designs maintain color accuracy and visual integrity whether they are displayed on a screen or printed professionally. ## Color Libraries and Custom Palettes Color libraries in CE.SDK are collections of predefined colors that appear in the color picker, helping users maintain brand or project consistency. Libraries are implemented as asset sources, where each color is represented as an individual asset. Each color library is identified by a unique source ID, allowing you to create, configure, and manage custom palettes tailored to your design needs. This setup makes it easy to offer curated color options to end users, improving both usability and design consistency. ## Supported Color Spaces CreativeEditor SDK supports a wide range of color spaces to meet different design requirements: * **sRGB:** The standard color space for most digital screens. * **Display P3:** A wider gamut color space often used on high-end displays. * **CMYK:** A subtractive color model used primarily for print workflows. * **Spot Colors:** Special premixed inks for print-specific designs requiring precise color matching. Each color space serves different use cases, ensuring that your designs remain accurate whether viewed on a screen or produced in print. ## Applying and Modifying Colors CE.SDK allows you to apply and modify colors both through the UI and programmatically via the API. You can set colors for fills, strokes, outlines, and other properties dynamically, enabling a wide range of creative control. Whether you are adjusting a single design element or programmatically updating entire templates, the SDK provides the flexibility you need. ## Color Adjustments and Properties CreativeEditor SDK provides a range of color adjustments to fine-tune your designs. Commonly used adjustments include: * **Brightness:** Adjust the overall lightness or darkness. * **Contrast:** Adjust the difference between light and dark areas. * **Saturation:** Increase or decrease the intensity of colors. * **Exposure:** Modify the overall exposure level. * **Temperature:** Adjust the color balance between warm and cool tones. * **Sharpness:** Enhance the crispness and clarity of edges. In addition to these, CE.SDK supports many more detailed adjustments. You can also modify specific color-related properties like fill color, stroke color, and opacity. All adjustments and property changes can be applied both through the UI and programmatically. ## Color Conversion CreativeEditor SDK automatically manages color conversions to help ensure consistent visual output across different mediums. The following conversion behaviors are supported: * **RGB and CMYK colors** can be converted between each other using a mathematical approximation. * **Spot colors** can be represented in RGB and CMYK by using a corresponding color approximation. * **RGB and CMYK colors cannot be converted to spot colors.** These automatic conversions help ensure that designs maintain visual consistency whether they are viewed on a digital screen or prepared for professional printing. --- [Source](https:/img.ly/docs/cesdk/android/colors/for-print-59bc05) # For Print --- [Source](https:/img.ly/docs/cesdk/android/colors/apply-2211e3) # Apply Colors ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Colorimport ly.img.engine.ColorSpaceimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun colors( 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.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) 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) 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 spotPinkFlamingo = Color.fromSpotColor( name = "Pink-Flamingo", tint = 1F, externalReference = "Crayola", ) val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F) engine.block.setColor(fill, property = "fill/color/value", value = rgbaBlue) engine.block.setColor(fill, property = "fill/color/value", value = cmykRed) engine.block.setColor(block, property = "stroke/color", value = cmykPartialRed) engine.block.setColor(fill, property = "fill/color/value", value = spotPinkFlamingo) engine.block.setColor(block, property = "dropShadow/color", value = spotPartialYellow) val cmykBlueConverted = engine.editor.convertColorToColorSpace( rgbaBlue, colorSpace = ColorSpace.CMYK, ) val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace( spotPinkFlamingo, colorSpace = ColorSpace.SRGB, ) engine.editor.findAllSpotColors() // ["Crayola-Pink-Flamingo", "Yellow"] engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F)) engine.editor.removeSpotColor("Yellow") engine.stop()} ``` ## Setup the scene We first create a new scene with a graphic block that has color fill. ``` 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) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block, fill = fill) ``` ## Create colors Here we instantiate a few colors with RGB and CMYK color spaces. We also define two spot colors, one with an RGB approximation and another with a CMYK approximation. Note that a spot colors can have both color space approximations. ``` 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) 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 spotPinkFlamingo = Color.fromSpotColor( name = "Pink-Flamingo", tint = 1F, externalReference = "Crayola", ) val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F) ``` ## Applying colors to a block We can use the defined colors to modify certain properties of a fill or properties of a shape. Here we apply it to `'fill/color/value'`, `'stroke/color'` and `'dropShadow/color'`. ``` engine.block.setColor(fill, property = "fill/color/value", value = rgbaBlue)engine.block.setColor(fill, property = "fill/color/value", value = cmykRed)engine.block.setColor(block, property = "stroke/color", value = cmykPartialRed)engine.block.setColor(fill, property = "fill/color/value", value = spotPinkFlamingo)engine.block.setColor(block, property = "dropShadow/color", value = spotPartialYellow) ``` ## Converting colors Using the utility function `convertColorToColorSpace` we create a new color in the CMYK color space by converting the `rgbaBlue` color to the CMYK color space. We also create a new color in the RGB color space by converting the `spotPinkFlamingo` color to the RGB color space. ``` val cmykBlueConverted = engine.editor.convertColorToColorSpace( rgbaBlue, colorSpace = ColorSpace.CMYK,)val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace( spotPinkFlamingo, colorSpace = ColorSpace.SRGB,) ``` ## Listing spot colors This function returns the list of currently defined spot colors. ``` engine.editor.findAllSpotColors() // ["Crayola-Pink-Flamingo", "Yellow"] ``` ## Redefine a spot color We can re-define the RGB and CMYK approximations of an already defined spot color. Doing so will change the rendered color of the blocks. We change it for the CMYK approximation of `'Yellow'` and make it a bit greenish. The properties that have `'Yellow'` as their spot color will change when re-rendered. ``` engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F)) ``` ## Removing the definition of a spot color We can undefine a spot color. Doing so will make all the properties still referring to that spot color (`'Yellow'` in this case) use the default magenta RGB approximation. ``` engine.editor.removeSpotColor("Yellow") ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.Colorimport ly.img.engine.ColorSpaceimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun colors( 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) 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) 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) 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 spotPinkFlamingo = Color.fromSpotColor( name = "Pink-Flamingo", tint = 1F, externalReference = "Crayola", ) val spotPartialYellow = Color.fromSpotColor(name = "Yellow", tint = 0.3F) engine.block.setColor(fill, property = "fill/color/value", value = rgbaBlue) engine.block.setColor(fill, property = "fill/color/value", value = cmykRed) engine.block.setColor(block, property = "stroke/color", value = cmykPartialRed) engine.block.setColor(fill, property = "fill/color/value", value = spotPinkFlamingo) engine.block.setColor(block, property = "dropShadow/color", value = spotPartialYellow) val cmykBlueConverted = engine.editor.convertColorToColorSpace( rgbaBlue, colorSpace = ColorSpace.CMYK, ) val rgbaPinkFlamingoConverted = engine.editor.convertColorToColorSpace( spotPinkFlamingo, colorSpace = ColorSpace.SRGB, ) engine.editor.findAllSpotColors() // ["Crayola-Pink-Flamingo", "Yellow"] engine.editor.setSpotColor("Yellow", Color.fromCMYK(c = 0.2F, m = 0F, y = 1F, k = 0F)) engine.editor.removeSpotColor("Yellow") engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/automation/overview-34d971) # Overview 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. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## What Can Be Automated with CE.SDK CE.SDK supports a wide variety of automation use cases, including: * **Design generation at scale**: Create thousands of variants from a single template, such as product cards or regionalized campaigns. * **Data-driven customization**: Merge external data (e.g., CSV, JSON, APIs) into templates to personalize text, images, or layout. * **Responsive output creation**: Automatically resize designs and export assets in different aspect ratios or dimensions for various platforms. * **Pre-export validation**: Detect issues like empty placeholders or low-resolution images before generating final output. * **Multimodal exporting**: Automate delivery to multiple formats including JPG, PNG, PDF, and MP4. ## Automation Contexts ### Headless / Server-Side Automation Server-side automation provides complete control over content generation without rendering a UI. This is ideal for background processing, such as creating assets in response to API requests or batch-generating print files for a mail campaign. ### UI-Integrated Automation (Human-in-the-Loop) For workflows that require user input or final approval, you can embed automation into the CE.SDK UI. Users can review, customize, or finalize designs that were pre-filled with dynamic data—ideal for marketing teams, e-commerce admins, or print professionals. ### Client-Side vs. Backend-Supported Workflows Many automation workflows can run fully in the browser thanks to CE.SDK’s client-side architecture. However, a backend may be required for use cases involving: * Secure access to private assets * Large dataset lookups * Server-side template rendering * Scheduled or event-based triggers ## Customization Capabilities CE.SDK gives you deep control over how your automation pipeline behaves: ### Data Sources Connect to a variety of inputs: * Local or remote JSON * CSV files * REST APIs * CMS or PIM systems ### Template Customization * Define dynamic variables and conditional placeholders * Use reusable templates or generate them on-the-fly * Lock or constrain specific fields to preserve brand integrity ### Design Rules Enforce visual and content constraints: * Brand-compliant colors and fonts * Overflow handling and text auto-resizing * Show/hide conditions and fallback logic ### Output Formats Category Supported Formats **Images** `.png` (with transparency), `.jpeg`, `.webp`, `.tga` **Video** `.mp4` (H.264 or H.265 on supported platforms with limited transparency support) **Print** `.pdf` (supports underlayer printing and spot colors) **Scene** `.scene` (description of the scene without any assets) **Archive** `.zip` (fully self-contained archive that bundles the `.scene` file with all assets) Our custom cross-platform C++ based rendering and layout engine ensures consistent output quality across devices. ## UI Customization for Automation You can extend the CE.SDK UI to trigger and manage automation tasks directly in the interface: * Add buttons or panels to trigger workflows * Dynamically update the scene based on user input or external data * Customize visibility of UI components depending on the stage (e.g., pre-fill vs. review) This makes it easy to integrate human-in-the-loop flows while preserving a tailored editing experience. ## Mapping Your Use Case Use the table below to quickly find the automation guide that matches your workflow: Use Case Relevant Guide Create 500 product images \[Batch Processing\] Merge CSV with template \[Data Merge\] Generate platform-specific variations \[Product Variations\] Use UI to customize before export \[Actions\] If you’re not sure which model fits your workflow, start with a hybrid approach using both automation and manual review. You can always migrate more logic to the backend as you scale. --- [Source](https:/img.ly/docs/cesdk/android/animation/create-15cf50) # Create Animations --- [Source](https:/img.ly/docs/cesdk/android/animation/overview-6a2ef2) # Overview Animations in CreativeEditor SDK (CE.SDK) bring your designs to life by adding motion to images, text, and design elements. 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. With CE.SDK, you can create and edit animations either through the built-in UI timeline or programmatically using the CreativeEngine API. Animated designs can be exported as MP4 videos, allowing you to deliver polished, motion-rich content entirely client-side. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](android/get-started/overview-e18f40/) ## Animation Capabilities in CE.SDK CE.SDK enables animation across a variety of design elements, giving you the flexibility to animate: * **Images:** Animate image blocks with movements like fades, zooms, or rotations. * **Text:** Animate text layers to create effects such as typewriter reveals or slide-ins. * **Shapes and Graphics:** Add motion to vector shapes, icons, and graphic blocks to create visually rich layouts. You can animate key properties of these elements, including: * **Position:** Move elements across the canvas. * **Scale:** Zoom in or out dynamically. * **Rotation:** Spin or pivot elements over time. * **Opacity:** Fade elements in and out. ## Supported Animation Types CE.SDK provides a range of animation types designed for common motion effects. Core categories include: * **Fade:** Smooth transitions in or out using opacity changes. * **Slide:** Move elements into or out of the frame from any direction. * **Zoom:** Scale elements up or down to create dynamic emphasis. * **Rotate:** Apply rotational motion for spins or turns. These animations can be used as in, out or loop animations to create complex sequences. ## Timeline and Keyframes Animations in CE.SDK are structured around a **timeline-based editing system**. Each scene has a timeline where elements are placed and animated relative to playback time. Animations can be created entirely through the UI’s timeline editor or managed programmatically by interacting with the CreativeEngine’s animation APIs, offering flexibility for different workflows. --- [Source](https:/img.ly/docs/cesdk/android/user-interface/appearance/theming-4b0938) # Theming In this example, we will show you how to make theming configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). ## Configuration Theming configuration is part of the `EditorConfiguration` class. Use the `EditorConfiguration.getDefault` helper function to make theming configurations. * `uiMode` - the UI mode of the editor. The default value is `EditorUiMode.SYSTEM`. These are the available values: * `EditorUiMode.SYSTEM` - editor will use the light color scheme if the system is in light mode and will use the dark color scheme if the system is in dark mode. * `EditorUiMode.LIGHT` - editor will always use the light color scheme. * `EditorUiMode.DARK` - editor will always use the dark color scheme. ``` import androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EditorUiModeimport ly.img.editor.EngineConfigurationimport ly.img.editor.rememberForDesign // Add this composable to your NavHost@Composablefun ThemingEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", ) val editorConfiguration = EditorConfiguration.rememberForDesign( uiMode = EditorUiMode.DARK, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` --- [Source](https:/img.ly/docs/cesdk/android/user-interface/appearance/overlay-b7e891) # Overlay In this example, we will show you how to make overlay configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-android-examples/tree/v1.51.0/editor-guides-configuration-overlay). ## Configuration When working with [UI events](android/user-interface/events-514b70/), you may want to update the UI that is drawn over the editor (a.k.a `Overlay`) upon receiving events. A great example would be showing a loading indicator when the editor is being loaded or showing a custom dialog asking for export configurations when your customer taps on the `Export` button. This is when overlay configuration comes to help. In this example, we are going to demonstrate how upon capturing show/hide loading events you can render your own loading dialog. The default `EditorUiState` already contains `showLoading` property that is responsible for drawing an overlaying loading. Although we can use the same property, let’s create our own state class in order to demonstrate how the default state can be wrapped and extended. The only requirement of the state class is that it has to be `Parcelable`. By default, both `onCreate` and `onExport` [callbacks](android/user-interface/events-514b70/) send `ShowLoading` and `HideLoading` UI events. All we have to do is capture these events and override the default behavior. Similar to the [UI events](android/user-interface/events-514b70/) guide, the events are captured and an updated state is returned. Note that instead of updating the `EditorUiState.showLoading` property we update the property of the brand new state: `OverlayCustomState.showCustomLoading`. As the state is updated, the updated state is received in the `overlay` composable callback to be rendered. Finally, we can render our custom loading dialog and render remaining default overlay components via `EditorDefaults.Overlay` composable function. Note that the callback receives `EditorEventHandler` object too in case your composable component requires UI interaction. In this example, tapping on the confirm button of the loading dialog closes the dialog and the editor. Note that the overlay is edge-to-edge, therefore it is your responsibility to draw over system bars too. --- [Source](https:/img.ly/docs/cesdk/android/animation/types-4e5f41) # Supported Animation Types ## 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'` --- [Source](https:/img.ly/docs/cesdk/android/user-interface/customization/navigation-bar-4e5d39) # Navigation Bar In this example, we will show you how to make navigation bar configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-android-examples/tree/v1.51.0/editor-guides-configuration-navigation-bar). ## Navigation Bar Architecture ![Navigation Bar on Android](/docs/cesdk/_astro/navigation-bar-android.CIRnSRr4_1qVw8e.webp) `NavigationBar` is a list of items placed horizontally at the top of the editor. It has type `NavigationBar : EditorComponent` and every item in the navigation bar has type `NavigationBar.Item : EditorComponent`. `NavigationBar.Item` is an abstract class that currently has two implementations: `NavigationBar.Button` and `NavigationBar.Custom`. `NavigationBar.Button` is an editor component that has an icon and a text positioned in a column, while `NavigationBar.Custom` is a fully custom editor component that allows drawing arbitrary content. Prefer using `NavigationBar.Custom` for rendering custom content in the navigation bar over inheriting from `NavigationBar.Item`. ## Navigation Bar Configuration `NavigationBar` is part of the `EditorConfiguration`, therefore, in order to configure the navigation bar we need to configure the `EditorConfiguration`. Below you can find the list of available configurations of the navigation bar. To demonstrate the default values, all parameters are assigned to their default values. Consider using `NavigationBar.rememberFor{solution_name}` helper functions when providing a navigation bar for a specific [solution](android/prebuilt-solutions-d0ed07/). * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the navigation bar. Prefer updating individual `NavigationBar.Item`s over updating the whole navigation bar. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the `Engine`. By default the scope is updated only when the parent scope (accessed via `LocalEditorScope`) is updated. * `visible` - whether the navigation bar should be visible. Default value is always true. * `enterTransition` - transition of the navigation bar when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the navigation bar when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the navigation bar. Useful when you want to add custom background, foreground, shadow, paddings etc. By default decoration adds background color and applies paddings to the navigation bar. * `listBuilder` - a builder that registers the list of `NavigationBar.Item`s that should be part of the navigation bar. Note that registering does not mean displaying. The items will be displayed if `NavigationBar.Item.visible` is true for them. By default listBuilder does not add anything to the navigation bar. For configuration details, see the ListBuilder Configuration section. * `horizontalArrangement` - the horizontal arrangement that should be used to render the items in the navigation bar horizontally. Default value is `Arrangement.SpaceEvenly`. * `itemDecoration` - decoration of the items in the navigation bar. Useful when you want to add custom background, foreground, shadow, paddings etc to the items. Prefer using this decoration when you want to apply the same decoration to all the items, otherwise set decoration to individual items. Default value is always no decoration. ## NavigationBar.ListBuilder Configuration There are two main ways to create an instance of `NavigationBar.ListBuilder`. First way is to call `modify` on an existing builder, and the second way is to create the builder from scratch. Currently, there are three available builders: * `NavigationBar.ListBuilder.rememberForDesign()` that is recommended to be used in [Design Editor](android/prebuilt-solutions/design-editor-9bf041/). * `NavigationBar.ListBuilder.rememberForPhoto()` that is recommended to be used in [Photo Editor](android/prebuilt-solutions/photo-editor-42ccb2/). * `NavigationBar.ListBuilder.rememberForVideo()` that is recommended to be used in [Video Editor](android/prebuilt-solutions/video-editor-9e533a/). * `NavigationBar.ListBuilder.rememberForApparel()` that is recommended to be used in [Apparel Editor](android/prebuilt-solutions/t-shirt-designer-02b48f/). * `NavigationBar.ListBuilder.rememberForPostcard()` that is recommended to be used in [Postcard Editor](android/prebuilt-solutions/postcard-editor-61e1f6/). ### Modifying an Existing Builder In this example, we will modify `NavigationBar.ListBuilder.rememberForDesign`. Modifying builders can be used, when you do not want to touch the default general order of the items in the builder, but rather add additional items and replace/hide some of the default items. To achieve that, there are multiple available functions in the scope of `modify` lambda: * `addFirst` - prepends a new `NavigationBar.Item` in the list. Note that the parameter `alignment` is mandatory in case the original `ListBuilder` contains aligned items in order to indicate which aligned group the new item belongs to. In this example, the orginal `ListBuilder` contains aligned items. More about aligned items can be found in the next section. * `addLast` - appends a new `NavigationBar.Item` in the list. Note that the parameter `alignment` is mandatory in case the original `ListBuilder` contains aligned items in order to indicate which aligned group the new item belongs to. In this example, the orginal `ListBuilder` contains aligned items. More about aligned items can be found in the next section. * `addAfter` - adds a new `NavigationBar.Item` right after the item with provided id. An exception will be thrown if no item exists with provided id in the default builder. * `addBefore` - adds a new `NavigationBar.Item` right before the item with provided id. An exception will be thrown if no item exists with provided id in the default builder. * `replace` - replaces the `NavigationBar.Item` with provided id. An exception will be thrown if no item exists with provided id in the default builder. Also note that the new item does not need to have the same id. * `remove` - removes the `NavigationBar.Item` with provided id. An exception will be thrown if no item exists with provided id in the default builder. **Warning** Note that the order of items may change between editor versions, therefore ListBuilder.modify must be used with care. Consider creating a new builder if you want to have strict ordering between different editor versions. ### Creating a New Builder In this example, we will create a builder from scratch that will be used in the `NavigationBar` of `DesignEditor` solution. Creating a new builder is recommended, when you want to touch the default order of available builders, as well as when available builders do not contain the items that you want. This example mimics reordering the default order of items in `NavigationBar.ListBuilder.rememberForDesign` builder. In addition, some items are removed and a new custom item is added in a new aligned group. As you can see, all the items are part of aligned groups positioned in different horizontal locations, however it is possible to add all the items outside `aligned` functions and control their horziontal arrangement via `horizontalArrangement` (see the NavigationBar Configuration section). See analogous documentation of [Dock](android/user-interface/customization/dock-cb916c/) and [InspectorBar](android/user-interface/customization/inspector-bar-8ca1cd/) for reference of non-aligned items. **Warning** It is not allowed to add items both inside and outside align blocks at the same time: either all items should be in aligned groups, or no items at all. ## NavigationBar.Item Configuration As mentioned in the Navigation Bar Architecture section, `NavigationBar.Item` is an `EditorComponent` and it has two subtypes: `NavigationBar.Button` and `NavigationBar.Custom`. ### NavigationBar.Button Configuration In order to create a navigation bar button, use `NavigationBar.Button.remember` composable function. Below you can find the list of available configurations when creating a `NavigationBar.Button`. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `id` - the id of the button. Note that it is highly recommended that every unique `EditorComponent` has a unique id. Parameter does not have a default value. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the button. Ideally, scope should be updated when the parent scope (scope of the parent component `NavigationBar` - `NavigationBar.Scope`) is updated and when you want to observe changes from the `Engine`. By default the scope is updated only when the parent component scope (`NavigationBar.scope`, accessed via `LocalEditorScope`) is updated. * `visible` - whether the button should be visible. Default value is always true. * `enterTransition` - transition of the button when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the button when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the button. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration. * `onClick` - the callback that is invoked when the button is clicked. Parameter does not have a default value. * `icon` - the icon content of the button. If null, it will not be rendered. Default value is null. * `text` - the text content of the button. If null, it will not be rendered. Default value is null. * `enabled` - whether the button is enabled. Default value is always true. Other than the main `NavigationBar.Button.remember` function, there is one more convenience overload that has three differences: 1. `icon` is replaced with `vectorIcon` lambda, that returns `VectorIcon` instead of drawing the icon content. 2. `text` is replaced with `text` lambda, that returns `String` instead of drawing the text content. 3. An extra parameter `contentDescription` is added that is used by accessibility services to describe what the button does. Note that it is not required to provide value when `text` is not null, since its value will be used by accessibility services, however having both `text` and `contentDescription` as null will cause a crash. ### NavigationBar.Custom Configuration In order to create a custom navigation bar item, use `NavigationBar.Custom.remember` composable function. Below you can find the list of available configurations when creating a `NavigationBar.Custom` item. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `id` - the id of the custom item. Note that it is highly recommended that every unique `EditorComponent` has a unique id. Parameter does not have a default value. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the custom item. Ideally, scope should be updated when the parent scope (scope of the parent component `NavigationBar` - `NavigationBar.Scope`) is updated and when you want to observe changes from the `Engine`. Parameter does not have a default value. * `visible` - whether the custom item should be visible. Default value is always true. * `enterTransition` - transition of the custom item when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the custom item when it exits the parent composable. Default value is always no exit transition. * `content` - the content of the custom item. You are responsible for drawing it, handling clicks etc. Parameter does not have a default value. ### List of Available NavigationBar.Buttons As you often saw in the previous sections, there are composable functions that look like this: `NavigationBar.Button.remember{name}`. All these functions return a `NavigationBar.Button`, they are public and can be used in your application. Note that all the parameters of these functions have default values, therefore, you do not need to provide any values, however, if you want to modify any of the parameters, it is exactly the same as described in NavigationBar.Button Configuration section. Button Id Description `NavigationBar.Button.rememberCloseEditor` `NavigationBar.Button.Id.closeEditor` Triggers [EngineConfiguration.onClose](android/user-interface/events-514b70/) callback via `EditorEvent.OnClose`. `NavigationBar.Button.rememberUndo` `NavigationBar.Button.Id.undo` Does undo operation in the editor via [EditorApi.undo](android/concepts/undo-and-history-99479d/) engine API. `NavigationBar.Button.rememberRedo` `NavigationBar.Button.Id.redo` Does redo operation in the editor via [EditorApi.redo](android/concepts/undo-and-history-99479d/) engine API. `NavigationBar.Button.rememberExport` `NavigationBar.Button.Id.export` Triggers [EngineConfiguration.onExport](android/user-interface/events-514b70/) callback via `EditorEvent.Export`. `NavigationBar.Button.rememberTogglePreviewMode` `NavigationBar.Button.Id.togglePreviewMode` Updates editor view mode via `EditorEvent.SetViewMode`: when current view mode is `EditorViewMode.Edit`, then `EditorViewMode.Preview` is set and vice versa. Note that this button is intended to be used in [Photo Editor](android/prebuilt-solutions/photo-editor-42ccb2/), [Apparel Editor](android/prebuilt-solutions/t-shirt-designer-02b48f/) and [Postcard Editor](android/prebuilt-solutions/postcard-editor-61e1f6/) and may cause unexpected behaviors when used in other solutions. `NavigationBar.Button.rememberTogglePagesMode` `NavigationBar.Button.Id.togglePagesMode` Updates editor view mode via `EditorEvent.SetViewMode`: when current view mode is `EditorViewMode.Edit`, then `EditorViewMode.Pages` is set and vice versa. Note that this button is intended to be used in [Design Editor](android/prebuilt-solutions/design-editor-9bf041/) and may cause unexpected behaviors when used in other solutions. `NavigationBar.Button.rememberPreviousPage` `NavigationBar.Button.Id.previousPage` Navigates to the previous page via `EditorEvent.Navigation.ToPreviousPage`. `NavigationBar.Button.rememberNextPage` `NavigationBar.Button.Id.nextPage` Navigates to the next page via `EditorEvent.Navigation.ToNextPage`. --- [Source](https:/img.ly/docs/cesdk/android/user-interface/customization/inspector-bar-8ca1cd) # Inspector Bar In this example, we will show you how to make inspector bar configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-android-examples/tree/v1.51.0/editor-guides-configuration-inspector-bar). ## Inspector Bar Architecture ![Inspector Bar](/docs/cesdk/_astro/inspector-bar-android.BqMKr0yV_Z21T0yw.webp) The inspector bar is a list of items placed horizontally at the bottom of the editor. It is visible when a design block is selected and its items provide different editing capabilities to the selected design block. Every item in the inspector bar conforms to `InspectorBar.Item: EditorComponent` where its `Context` is constraint to be of type `InspectorBar.Context`. `InspectorBar.Button` is a provided implementation of the `InspectorBar.Item` protocol that has an icon and a title. You can also create your own fully custom editor component that allows drawing arbitrary content by conforming your type to `InspectorBar.Item`. ## Modifiers After initializing an editor SwiftUI view you can apply any SwiftUI _modifier_ to customize it like for any other SwiftUI view. All public Swift `extension`s of existing types provided by IMG.LY, e.g., for the SwiftUI `View` protocol, are exposed in a separate `.imgly` property namespace. The inspector bar configuration to customize the editor is no exception to this rule and is implemented as SwiftUI _modifiers_. All inspector bar _modifiers_ and `InspectorBar.Item`s provide the `InspectorBar.Context` to access the engine, the asset library configuration, the `EditorEventHandler`, and the current selected design block. Prefer using this provided selection for any logic instead of accessing the same values from the engine because the engine values will update immediately on changes whereas the provided selection value is cached for the presentation time of the navigation bar including its appear and disappear animations. * `inspectorBarItems` - the `@InspectorBar.Builder` that registers the `InspectorBar.Item`s and defines their order. Note that registering does not mean displaying. The items will be displayed if `InspectorBar.Item.isVisible(_:)` returns true for them. By default, the items shown in this example are defined for all [editor solutions](android/prebuilt-solutions-d0ed07/). * `modifyInspectorBarItems` - the modifications that should be applied to the order of items defined by the `.imgly.inspectorBarItems` builder above. This _modifier_ can be used, when you do not want to touch the default general order of the items, but rather add additional items and replace/hide some of the default items. To achieve that, use the `InspectorBar.Modifier` provided as the second closure argument, named `items` in this example, to add, replace, and remove items as highlighted in [modify inspector bar items](android/user-interface/customization/inspector-bar-8ca1cd/). By default, no modifications are applied. ### Modify Inspector Bar Items In this example, we will modify the default inspector bar `items` with the `.imgly.modifyInspectorBarItems` _modifier_. Each modification is only applied to the items defined by `.imgly.inspectorBarItems`. The operations also accept a `@InspectorBar.Builder` so that you can define multiple items in the closures. * `addFirst` - prepends new `InspectorBar.Item`s. * `addLast` - appends new `InspectorBar.Item`s. * `addAfter` - adds new `InspectorBar.Item`s right after the item with the provided id. An error will be thrown if no item exists with the provided id. * `addBefore` - adds new `InspectorBar.Item`s right before the item with the provided id. An error will be thrown if no item exists with the provided id. * `replace` - replaces the `InspectorBar.Item` with the provided id with new `InspectorBar.Item`s. An error will be thrown if no item exists with the provided id. The new items don’t need to have the same id as the replaced item. * `remove` - removes the `InspectorBar.Item` with the provided id. An error will be thrown if no item exists with the provided id. **Warning** Note that the order of items may change between editor versions, therefore `.imgly.modifyInspectorBarItems` must be used with care. Consider overwriting the default items instead with `.imgly.inspectorBarItems` if you want to have strict ordering between different editor versions. ## InspectorBar.Item Configuration As mentioned in the [Inspector Bar Architecture](android/user-interface/customization/inspector-bar-8ca1cd/) section, `InspectorBar.Item` conforms to `EditorComponent`. Its `id` must be unique which is a requirement of the underlying SwiftUI [`ForEach`](https://developer.apple.com/documentation/swiftui/foreach) type. Depending on your needs there are multiple ways to define an item. In this example, we demonstrate your options with increasing complexity. ### Use Predefined Buttons The most basic option is to use our predefined buttons which are provided in the nested `InspectorBar.Buttons.` namespace. All [available predefined buttons are listed below](android/user-interface/customization/inspector-bar-8ca1cd/). ### Customize Predefined Buttons All parameters of our predefined buttons are initialized with default values which allows you to change any of them if needed to finetune the button’s behavior and style. This example uses the default values of this particular predefined button. * `action` - the action to perform when the user triggers the button. In this example, the event handler is used to open a sheet to format text. * `title` - the title `View` that should be used to label the button. Don’t encode the visibility in this view. Use `isVisible` instead. In this example, a `Text` view is used. * `icon` - the icon `View` that should be used to label the button. Don’t encode the visibility in this view. Use `isVisible` instead. In this example, an `Image` view is used. We provide all our used icon images in the nested `Image.imgly.` namespace. * `isEnabled` - whether the button is enabled. In this example, true is always used. * `isVisible` - whether the button should be visible. Prefer using this parameter to toggle the visibility instead of encoding it in the `title` and `icon` views. In this example, true is only used when the selected design block is of type text and its scope allows for text editing. ### Create New Buttons If our predefined buttons don’t fit your needs you can create your own. * `id` - the unique id of the button. This parameter is required. * `action` - the action to perform when the user triggers the button. This parameter is required. * `label` - a `View` that describes the purpose of the button’s `action`. Don’t encode the visibility in this view. Use `isVisible` instead. This parameter is required. * `isEnabled` - whether the button is enabled. By default, true is always used. * `isVisible` - whether the button should be visible. Prefer using this parameter to toggle the visibility instead of encoding it in the `label` view. By default, true is always used. ### Create New Custom Items If you need something completely custom you can use arbitrary views as items. Therefore, you need to conform your type to the `InspectorBar.Item` protocol. * `var id: EditorComponentID { get }` - the unique id of the item. This property is required. * `func body(_: InspectorBar.Context) throws -> some View` - the body of your view. Don’t encode the visibility in this view. Use `isVisible` instead. This property is required. * `func isVisible(_: InspectorBar.Context) throws -> Bool` - whether the item should be visible. Prefer using this parameter to toggle the visibility instead of encoding it in the `body` view. By default, true is always used. ### List of Available InspectorBar.Buttons This is a comprehensive list of all predefined buttons. Some of them were already used in the previous sections. They are static functions that look like this: `InspectorBar.Buttons.{name}`. All these functions return a `InspectorBar.Button`. They are public and can be used in your application. Note that all the parameters of these functions have default values, therefore, you do not need to provide any values, however, if you want to modify any of the parameters, it is exactly the same as described in the [Customize Predefined Buttons](android/user-interface/customization/inspector-bar-8ca1cd/) section. Button ID Description Renders For `InspectorBar.Buttons.replace` `InspectorBar.Buttons.ID.replace` Opens a library sheet via editor event `.openSheet`. By default `DesignBlockType`, `FillType` and kind of the selected design block are used to find the library in the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). Selected asset will replace the content of the currently selected design block. Video, Image, Sticker, Audio `InspectorBar.Buttons.editText` `InspectorBar.Buttons.ID.editText` Enters text editing mode for the selected design block. Text `InspectorBar.Buttons.formatText` `InspectorBar.Buttons.ID.formatText` Opens format text sheet via editor event `.openSheet`. Text `InspectorBar.Buttons.fillStroke` `InspectorBar.Buttons.ID.fillStroke` Opens fill & stroke sheet via editor event `.openSheet`. Page, Video, Image, Shape, Text `InspectorBar.Buttons.editVoiceover` `InspectorBar.Buttons.ID.editVoiceover` Opens voiceover sheet via editor event `.openSheet`. Video, Audio, Voiceover `InspectorBar.Buttons.volume` `InspectorBar.Buttons.ID.volume` Opens volume sheet via editor event `.openSheet`. Video, Audio, Voiceover `InspectorBar.Buttons.crop` `InspectorBar.Buttons.ID.crop` Opens crop sheet via editor event `.openSheet`. Video, Image `InspectorBar.Buttons.adjustments` `InspectorBar.Buttons.ID.adjustments` Opens adjustments sheet via editor event `.openSheet`. Video, Image `InspectorBar.Buttons.filter` `InspectorBar.Buttons.ID.filter` Opens filter sheet via editor event `.openSheet`. Video, Image `InspectorBar.Buttons.effect` `InspectorBar.Buttons.ID.effect` Opens effect sheet via editor event `.openSheet`. Video, Image `InspectorBar.Buttons.blur` `InspectorBar.Buttons.ID.blur` Opens blur sheet via editor event `.openSheet`. Video, Image `InspectorBar.Buttons.shape` `InspectorBar.Buttons.ID.shape` Opens shape sheet via editor event `.openSheet`. Video, Image, Shape `InspectorBar.Buttons.selectGroup` `InspectorBar.Buttons.ID.selectGroup` Selects the group design block that contains the currently selected design block via editor event `.selectGroupForSelection`. Video, Image, Sticker, Shape, Text `InspectorBar.Buttons.enterGroup` `InspectorBar.Buttons.ID.enterGroup` Changes selection from the selected group design block to a design block within that group via editor event `.enterGroupForSelection`. Group `InspectorBar.Buttons.layer` `InspectorBar.Buttons.ID.layer` Opens layer sheet via editor event `.openSheet`. Video, Image, Sticker, Shape, Text `InspectorBar.Buttons.split` `InspectorBar.Buttons.ID.split` Splits currently selected design block via editor event `.splitSelection` in a video scene. Video, Image, Sticker, Shape, Text, Audio `InspectorBar.Buttons.moveAsClip` `InspectorBar.Buttons.ID.moveAsClip` Moves currently selected design block into the background track as clip via editor event `.moveSelectionAsClip` Video, Image, Sticker, Shape, Text `InspectorBar.Buttons.moveAsOverlay` `InspectorBar.Buttons.ID.moveAsOverlay` Moves currently selected design block from the background track to an overlay via editor event `.moveSelectionAsOverlay` Video, Image, Sticker, Shape, Text `InspectorBar.Buttons.reorder` `InspectorBar.Buttons.ID.reorder` Opens reorder sheet via editor event `.openSheet`. Video, Image, Sticker, Shape, Text `InspectorBar.Buttons.duplicate` `InspectorBar.Buttons.ID.duplicate` Duplicates currently selected design block via editor event `.duplicateSelection`. Video, Image, Sticker, Shape, Text, Audio `InspectorBar.Buttons.delete` `InspectorBar.Buttons.ID.delete` Deletes currently selected design block via editor event `.deleteSelection`. Video, Image, Sticker, Shape, Text, Audio, Voiceover --- [Source](https:/img.ly/docs/cesdk/android/user-interface/customization/dock-cb916c) # Dock In this example, we will show you how to make dock configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-android-examples/tree/v1.51.0/editor-guides-configuration-dock). ## Dock Architecture ![Inspector Bar](/docs/cesdk/_astro/dock-android.CRuwSjHP_WNR2w.webp) `Dock` is a list of items placed horizontally at the bottom of the editor. It has type `Dock : EditorComponent` and every item in the dock has type `Dock.Item : EditorComponent`. `Dock.Item` is an abstract class that currently has two implementations: `Dock.Button` and `Dock.Custom`. `Dock.Button` is an editor component that has an icon and a text positioned in a column, while `Dock.Custom` is a fully custom editor component that allows drawing arbitrary content. Prefer using `Dock.Custom` for rendering custom content in the dock over inheriting from `Dock.Item`. ## Dock Configuration `Dock` is part of the `EditorConfiguration`, therefore, in order to configure the dock we need to configure the `EditorConfiguration`. Below you can find the list of available configurations of the dock. To demonstrate the default values, all parameters are assigned to their default values. Consider using `Dock.rememberFor{solution_name}` helper functions when providing a dock for a specific [solutions](android/prebuilt-solutions-d0ed07/). * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the dock. Prefer updating individual `Dock.Item`s over updating the whole dock. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the `Engine`. By default the scope is updated only when the parent scope (accessed via `LocalEditorScope`) is updated. * `visible` - whether the dock should be visible. Default value is always true. * `enterTransition` - transition of the dock when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the dock when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the dock. Useful when you want to add custom background, foreground, shadow, paddings etc. By default decoration adds background color and applies paddings to the dock. * `listBuilder` - a builder that registers the list of `Dock.Item`s that should be part of the dock. Note that registering does not mean displaying. The items will be displayed if `Dock.Item.visible` is true for them. By default listBuilder does not add anything to the dock. For configuration details, see [ListBuilder Configuration](android/user-interface/customization/dock-cb916c/) section. * `horizontalArrangement` - the horizontal arrangement that should be used to render the items in the dock horizontally. Default value is `Arrangement.SpaceEvenly`. * `itemDecoration` - decoration of the items in the dock. Useful when you want to add custom background, foreground, shadow, paddings etc to the items. Prefer using this decoration when you want to apply the same decoration to all the items, otherwise set decoration to individual items. Default value is always no decoration. ## Dock.ListBuilder Configuration There are two main ways to create an instance of `Dock.ListBuilder`. First way is to call `modify` on an existing builder, and the second way is to create the builder from scratch. Currently, there are three available builders: * `Dock.ListBuilder.rememberForDesign()` that is recommended to be used in [Design Editor](android/prebuilt-solutions/design-editor-9bf041/). * `Dock.ListBuilder.rememberForPhoto()` that is recommended to be used in [Photo Editor](android/prebuilt-solutions/photo-editor-42ccb2/). * `Dock.ListBuilder.rememberForVideo()` that is recommended to be used in [Video Editor](android/prebuilt-solutions/video-editor-9e533a/). Note that `Dock.Button.rememberImglyCamera()` can be used only if `ly.img:camera:` dependency is added in your`build.gradle` file. For more information, check the camera [documentation](android/get-started/overview-e18f40/). ### Modifying an Existing Builder In this example, we will modify `Dock.ListBuilder.rememberForDesign`. Modifying builders can be used, when you do not want to touch the default general order of the items in the builder, but rather add additional items and replace/hide some of the default items. To achieve that, there are multiple available functions in the scope of `modify` lambda: * `addFirst` - prepends a new `Dock.Item` in the list. * `addLast` - appends a new `Dock.Item` in the list. * `addAfter` - adds a new `Dock.Item` right after the item with provided id. An exception will be thrown if no item exists with provided id in the default builder. * `addBefore` - adds a new `Dock.Item` right before the item with provided id. An exception will be thrown if no item exists with provided id in the default builder. * `replace` - replaces the `Dock.Item` with provided id. An exception will be thrown if no item exists with provided id in the default builder. Also note that the new item does not need to have the same id. * `remove` - removes the `Dock.Item` with provided id. An exception will be thrown if no item exists with provided id in the default builder. **Warning** Note that the order of items may change between editor versions, therefore ListBuilder.modify must be used with care. Consider creating a new builder if you want to have strict ordering between different editor versions. ### Creating a New Builder In this example, we will create a builder from scratch that will be used in the `Dock` of `DesignEditor` solution. Creating a new builder is recommended, when you want to touch the default order of available builders, as well as when available builders do not contain the items that you want. This example mimics reordering the default order of items in `Dock.ListBuilder.rememberForDesign` builder. In addition, some items are removed and a new custom item is prepended. ## Dock.Item Configuration As mentioned in the [Dock Architecture](android/user-interface/customization/dock-cb916c/) section, `Dock.Item` is an `EditorComponent` and it has two subtypes: `Dock.Button` and `Dock.Custom`. ### Dock.Button Configuration In order to create a dock button, use `Dock.Button.remember` composable function. Below you can find the list of available configurations when creating a `Dock.Button`. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `id` - the id of the button. Note that it is highly recommended that every unique `EditorComponent` has a unique id. Parameter does not have a default value. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the button. Ideally, scope should be updated when the parent scope (scope of the parent component `Dock` - `Dock.Scope`) is updated and when you want to observe changes from the `Engine`. By default the scope is updated only when the parent component scope (`Dock.scope`, accessed via `LocalEditorScope`) is updated. * `visible` - whether the button should be visible. Default value is always true. * `enterTransition` - transition of the button when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the button when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the button. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration. * `onClick` - the callback that is invoked when the button is clicked. Parameter does not have a default value. * `icon` - the icon content of the button. If null, it will not be rendered. Default value is null. * `text` - the text content of the button. If null, it will not be rendered. Default value is null. * `enabled` - whether the button is enabled. Default value is always true. Other than the main `Dock.Button.remember` function, there is one more convenience overload that has three differences: 1. `icon` is replaced with `vectorIcon` lambda, that returns `VectorIcon` instead of drawing the icon content. 2. `text` is replaced with `text` lambda, that returns `String` instead of drawing the text content. 3. An extra parameter `contentDescription` is added that is used by accessibility services to describe what the button does. Note that it is not required to provide value when `text` is not null, since its value will be used by accessibility services, however having both `text` and `contentDescription` as null will cause a crash. ### Dock.Custom Configuration In order to create a custom dock item, use `Dock.Custom.remember` composable function. Below you can find the list of available configurations when creating a `Dock.Custom` item. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `id` - the id of the custom item. Note that it is highly recommended that every unique `EditorComponent` has a unique id. Parameter does not have a default value. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the custom item. Ideally, scope should be updated when the parent scope (scope of the parent component `Dock` - `Dock.Scope`) is updated and when you want to observe changes from the `Engine`. Parameter does not have a default value. * `visible` - whether the custom item should be visible. Default value is always true. * `enterTransition` - transition of the custom item when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the custom item when it exits the parent composable. Default value is always no exit transition. * `content` - the content of the custom item. You are responsible for drawing it, handling clicks etc. Parameter does not have a default value. ### List of Available Dock.Buttons As you often saw in the previous sections, there are composable functions that look like this: `Dock.Button.remember{name}`. All these functions return a `Dock.Button`, they are public and can be used in your application. Note that all the parameters of these functions have default values, therefore, you do not need to provide any values, however, if you want to modify any of the parameters, it is exactly the same as described in [Dock.Button Configuration](android/user-interface/customization/dock-cb916c/) section. Button Id Description `Dock.Button.rememberElementsLibrary` `Dock.Button.Id.elementsLibrary` Opens library sheet with elements via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberOverlaysLibrary` `Dock.Button.Id.overlaysLibrary` Opens library sheet with overlays via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberImagesLibrary` `Dock.Button.Id.imagesLibrary` Opens library sheet with images via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberTextLibrary` `Dock.Button.Id.textLibrary` Opens library sheet with text via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberShapesLibrary` `Dock.Button.Id.shapesLibrary` Opens library sheet with shapes via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberStickersLibrary` `Dock.Button.Id.stickersLibrary` Opens library sheet with stickers via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberAudiosLibrary` `Dock.Button.Id.audiosLibrary` Opens library sheet with audios via `EditorEvent.Sheet.Open`. By default, the `LibraryCategory` is picked from the [Asset Library](android/import-media/asset-panel/customize-c9a4de/). `Dock.Button.rememberSystemGallery` `Dock.Button.Id.systemGallery` Opens the system gallery via `EditorEvent.LaunchContract`. `Dock.Button.rememberSystemCamera` `Dock.Button.Id.systemCamera` Opens the system camera via `EditorEvent.LaunchContract`. `Dock.Button.rememberImglyCamera` `Dock.Button.Id.imglyCamera` Opens the IMG.LY camera via `EditorEvent.LaunchContract`. Note that the button can be used only if `ly.img:camera:` dependency is added in your `build.gradle` file. For more information, check the camera [documentation](android/get-started/overview-e18f40/). `Dock.Button.rememberReorder` `Dock.Button.Id.reorder` Opens reorder sheet via `EditorEvent.Sheet.Open`. `Dock.Button.rememberAdjustments` `Dock.Button.Id.adjustments` Opens adjustment sheet via `EditorEvent.Sheet.Open`. `Dock.Button.rememberFilter` `Dock.Button.Id.filter` Opens filter sheet via `EditorEvent.Sheet.Open`. `Dock.Button.rememberEffect` `Dock.Button.Id.effect` Opens effect sheet via `EditorEvent.Sheet.Open`. `Dock.Button.rememberBlur` `Dock.Button.Id.blur` Opens blur sheet via `EditorEvent.Sheet.Open`. `Dock.Button.rememberCrop` `Dock.Button.Id.crop` Opens crop sheet via `EditorEvent.Sheet.Open`. --- [Source](https:/img.ly/docs/cesdk/android/user-interface/customization/canvas-menu-0d2b5b) # Canvas Menu In this example, we will show you how to make canvas menu configurations for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-android-examples/tree/v1.51.0/editor-guides-configuration-canvas-menu). ## Canvas Menu Architecture ![Canvas Menu](/docs/cesdk/_astro/canvas-menu-android.CYJ-j41I_ZgT6RW.webp) `CanvasMenu` is a list of items placed horizontally that is displayed when a design block is selected. Items in this list provide different editing capabilities to the selected design block. It has type `CanvasMenu : EditorComponent` and every item in the canvas menu has type `CanvasMenu.Item : EditorComponent`. `CanvasMenu.Item` is an abstract class that currently has three implementations: `CanvasMenu.Button`, `CanvasMenu.Divider` and `CanvasMenu.Custom`. `CanvasMenu.Button` is an editor component that has an icon and a text positioned in a column, `CanvasMenu.Divider` is a component that renders a divider between items, while `CanvasMenu.Custom` is a fully custom editor component that allows drawing arbitrary content. Prefer using `CanvasMenu.Custom` for rendering custom content in the canvas menu over inheriting from `CanvasMenu.Item`. ## CanvasMenu Configuration `CanvasMenu` is part of the `EditorConfiguration`, therefore, in order to configure the canvas menu we need to configure the `EditorConfiguration`. Below you can find the list of available configurations of the canvas menu. To demonstrate the default values, all parameters are assigned to their default values. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the canvas menu. Prefer updating individual `CanvasMenu.Item`s over updating the whole canvas menu. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the `Engine`. By default the scope is updated when the parent scope (accessed via `LocalEditorScope`) is updated, when selection is changed to a different design block (or unselected) and when the parent of the currently selected design block is changed to a different design block. * `visible` - whether the canvas menu should be visible. By default the value is true when a design block is selected, it is not part of a group, canvas is not moving (finger is not on canvas), selected design block does not have a type `DesignBlockType.Audio` or `DesignBlockType.Page`, no sheet is displayed currently and the keyboard is not visible. In addition, selected design block should be visible at current playback time and containing scene should be on pause if design block is selected in a video scene. * `enterTransition` - transition of the canvas menu when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the canvas menu when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the canvas menu. Useful when you want to add custom background, foreground, shadow, paddings etc. By default decoration adds background color, applies rounded corners and positions the list of items next to the selected design block. * `listBuilder` - a builder that registers the list of `CanvasMenu.Item`s that should be part of the canvas menu. Note that registering does not mean displaying. The items will be displayed if `CanvasMenu.Item.visible` is true for them. By default `CanvasMenu.ListBuilder.remember` is used. For more details, see [ListBuilder Configuration](android/user-interface/customization/canvas-menu-0d2b5b/) section. * `itemDecoration` - decoration of the items in the canvas menu. Useful when you want to add custom background, foreground, shadow, paddings etc to the items. Prefer using this decoration when you want to apply the same decoration to all the items, otherwise set decoration to individual items. Default value is always no decoration. ## CanvasMenu.ListBuilder Configuration There are two main ways to create an instance of `CanvasMenu.ListBuilder`. First way is to call `modify` on an existing builder, and the second way is to create the builder from scratch. Currently, there is a single available builder, accessible via `CanvasMenu.ListBuilder.remember`. It provides a single general order of items in the canvas menu. ### Modifying an Existing Builder In this example, we will modify the only available builder: `CanvasMenu.ListBuilder.remember`. Modifying builders can be used, when you do not want to touch the default general order of the items in the builder, but rather add additional items and replace/hide some of the default items. To achieve that, there are multiple available functions in the scope of `modify` lambda: * `addFirst` - prepends a new `CanvasMenu.Item` in the list. * `addLast` - appends a new `CanvasMenu.Item` in the list. * `addAfter` - adds a new `CanvasMenu.Item` right after the item with provided id. An exception will be thrown if no item exists with provided id in the default builder. * `addBefore` - adds a new `CanvasMenu.Item` right before the item with provided id. An exception will be thrown if no item exists with provided id in the default builder. * `replace` - replaces the `CanvasMenu.Item` with provided id. An exception will be thrown if no item exists with provided id in the default builder. Also note that the new item does not need to have the same id. * `remove` - removes the `CanvasMenu.Item` with provided id. An exception will be thrown if no item exists with provided id in the default builder. **Warning** Note that the order of items may change between editor versions, therefore ListBuilder.modify must be used with care. Consider creating a new builder if you want to have strict ordering between different editor versions. ### Creating a New Builder In this example, we will create a builder from scratch that will be used in the `CanvasMenu` of `DesignEditor` solution. Creating a new builder is recommended, when you do not want to use the default order of items provided by IMG.LY. This example mimics reordering the default order of items in `CanvasMenu.ListBuilder.remember` builder. In addition, some items are removed and a new custom item is prepended. ## CanvasMenu.Item Configuration As mentioned in the [CanvasMenu Architecture](android/user-interface/customization/canvas-menu-0d2b5b/) section, `CanvasMenu.Item` is an `EditorComponent` and it has two subtypes: `CanvasMenu.Button` and `CanvasMenu.Custom`. ### CanvasMenu.Button Configuration In order to create a canvas menu button, use `CanvasMenu.Button.remember` composable function. Below you can find the list of available configurations when creating an `CanvasMenu.Button`. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `id` - the id of the button. Note that it is highly recommended that every unique `EditorComponent` has a unique id. Parameter does not have a default value. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the button. Ideally, scope should be updated when the parent scope (scope of the parent component `CanvasMenu` - `CanvasMenu.Scope`) is updated and when you want to observe changes from the `Engine`. By default the scope is updated only when the parent component scope (`CanvasMenu.scope`, accessed via `LocalEditorScope`) is updated. * `visible` - whether the button should be visible. Default value is always true. * `enterTransition` - transition of the button when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the button when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the button. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration. * `onClick` - the callback that is invoked when the button is clicked. Parameter does not have a default value. * `icon` - the icon content of the button. If null, it will not be rendered. Default value is null. * `text` - the text content of the button. If null, it will not be rendered. Default value is null. * `enabled` - whether the button is enabled. Default value is always true. Other than the main `CanvasMenu.Button.remember` function, there is one more convenience overload that has three differences: 1. `icon` is replaced with `vectorIcon` lambda, that returns `VectorIcon` instead of drawing the icon content. 2. `text` is replaced with `text` lambda, that returns `String` instead of drawing the text content. 3. An extra parameter `contentDescription` is added that is used by accessibility services to describe what the button does. Note that it is not required to provide value when `text` is not null, since its value will be used by accessibility services, however having both `text` and `contentDescription` as null will cause a crash. ### CanvasMenu.Divider Configuration In order to create a canvas menu divider, use `CanvasMenu.Divider.remember` composable function. Below you can find the list of available configurations when creating a `CanvasMenu.Divider` item. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the custom item. Ideally, scope should be updated when the parent scope (scope of the parent component `CanvasMenu` - `CanvasMenu.Scope`) is updated and when you want to observe changes from the `Engine`. Parameter does not have a default value. * `visible` - whether the divider should be visible. Default value is always true. * `enterTransition` - transition of the divider when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the divider when it exits the parent composable. Default value is always no exit transition. * `decoration` - decoration of the divider. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration. * `modifier` - the modifier of the divider. Default value is always a `Modifier` that sets size and paddings to the divider. ### CanvasMenu.Custom Configuration In order to create a custom canvas menu item, use `CanvasMenu.Custom.remember` composable function. Below you can find the list of available configurations when creating a `CanvasMenu.Custom` item. To demonstrate the default values, all parameters are assigned to their default values whenever possible. * `id` - the id of the custom item. Note that it is highly recommended that every unique `EditorComponent` has a unique id. Parameter does not have a default value. * `scope` - the scope of this component. Every new value will trigger recomposition of all the lambda parameters below. If you need to access `EditorScope` to construct the scope, use `LocalEditorScope`. Consider using Compose `State` objects in the lambda parameters below for granular recompositions over updating the scope, since scope change triggers full recomposition of the custom item. Ideally, scope should be updated when the parent scope (scope of the parent component `CanvasMenu` - `CanvasMenu.Scope`) is updated and when you want to observe changes from the `Engine`. Parameter does not have a default value. * `visible` - whether the custom item should be visible. Default value is always true. * `enterTransition` - transition of the custom item when it enters the parent composable. Default value is always no enter transition. * `exitTransition` - transition of the custom item when it exits the parent composable. Default value is always no exit transition. * `content` - the content of the custom item. You are responsible for drawing it, handling clicks etc. Parameter does not have a default value. ### List of Available CanvasMenu.Buttons As you often saw in the previous sections, there are composable functions that look like this: `CanvasMenu.Button.remember{name}`. All these functions return an `CanvasMenu.Button`, they are public and can be used in your application. Note that all the parameters of these functions have default values, therefore, you do not need to provide any values, however, if you want to modify any of the parameters, it is exactly the same as described in [CanvasMenu.Button Configuration](android/user-interface/customization/canvas-menu-0d2b5b/) section. Button Id Description `CanvasMenu.Button.rememberBringForward` `CanvasMenu.Button.Id.bringForward` Brings forward currently selected design block via `EditorEvent.Selection.BringForward`. `CanvasMenu.Button.rememberSendBackward` `CanvasMenu.Button.Id.sendBackward` Sends backward currently selected design block via `EditorEvent.Selection.SendBackward`. `CanvasMenu.Button.rememberDuplicate` `CanvasMenu.Button.Id.duplicate` Duplicates currently selected design block via `EditorEvent.Selection.Duplicate`. `CanvasMenu.Button.rememberDelete` `CanvasMenu.Button.Id.delete` Deletes currently selected design block via `EditorEvent.Selection.Delete`. --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/edit-shapes-d67cfb) # Edit Shapes ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun usingShapes( 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.create() val graphic = engine.block.create(DesignBlockType.Graphic) 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.setWidth(graphic, value = 100F) engine.block.setHeight(graphic, value = 100F) engine.block.appendChild(parent = scene, child = graphic) engine.scene.zoomToBlock( graphic, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) engine.block.supportsShape(graphic) // Returns true val text = engine.block.create(DesignBlockType.Text) engine.block.supportsShape(text) // Returns false val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(graphic, shape = rectShape) val shape = engine.block.getShape(graphic) val shapeType = engine.block.getType(shape) val starShape = engine.block.createShape(ShapeType.Star) engine.block.destroy(engine.block.getShape(graphic)) engine.block.setShape(graphic, shape = starShape) // The following line would also destroy the currently attached starShape // engine.block.destroy(graphic) val allShapeProperties = engine.block.findAllProperties(starShape) engine.block.setInt(starShape, property = "shape/star/points", value = 6) engine.stop()} ``` The `graphic` [design block](android/concepts/blocks-90241e/) in CE.SDK allows you to modify and replace its shape. CreativeEditor SDK supports many different types of shapes, such as rectangles, lines, ellipses, polygons and custom vector paths. Similarly to blocks, each shape object has a numeric id which can be used to query and [modify its properties](android/concepts/blocks-90241e/). ## Accessing Shapes In order to query whether a block supports shapes, you should call the `fun supportsShape(block: DesignBlock): Boolean` API. Currently, only the `graphic` design block supports shape objects. ``` engine.block.supportsShape(graphic) // Returns trueval text = engine.block.create(DesignBlockType.Text)engine.block.supportsShape(text) // Returns false ``` To query the shape of a design block, call the `fun getShape(block: DesignBlock): DesignBlock` API. You can now pass the returned result into other APIs in order to query more information about the shape, e.g. its type via the `fun getType(block: DesignBlock): String` API. ``` val shape = engine.block.getShape(graphic)val shapeType = engine.block.getType(shape) ``` When replacing the shape of a design block, remember to destroy the previous shape object if you don’t intend to use it any further. Shape objects that are not attached to a design block will never be automatically destroyed. Destroying a design block will also destroy its attached shape block. ``` val starShape = engine.block.createShape(ShapeType.Star)engine.block.destroy(engine.block.getShape(graphic))engine.block.setShape(graphic, shape = starShape)// The following line would also destroy the currently attached starShape// engine.block.destroy(graphic) ``` ## Shape Properties Just like design blocks, shapes 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 shape. For the star shape in this example, the call would return `["name", "shape/star/innerDiameter", "shape/star/points", "type", "uuid"]`. Please refer to the [API docs](android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) for a complete list of all available properties for each type of shape. ``` val allShapeProperties = engine.block.findAllProperties(starShape) ``` Once we know the property keys of a shape, we can use the same APIs as for design blocks in order to modify those properties. For example, we can use `fun setInt(block: DesignBlock, property: String, value: Int)` in order to change the number of points of the star to six. ``` engine.block.setInt(starShape, property = "shape/star/points", value = 6) ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun usingShapes( 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 graphic = engine.block.create(DesignBlockType.Graphic) 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.setWidth(graphic, value = 100F) engine.block.setHeight(graphic, value = 100F) engine.block.appendChild(parent = scene, child = graphic) engine.scene.zoomToBlock( graphic, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) engine.block.supportsShape(graphic) // Returns true val text = engine.block.create(DesignBlockType.Text) engine.block.supportsShape(text) // Returns false val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(graphic, shape = rectShape) val shape = engine.block.getShape(graphic) val shapeType = engine.block.getType(shape) val starShape = engine.block.createShape(ShapeType.Star) engine.block.destroy(engine.block.getShape(graphic)) engine.block.setShape(graphic, shape = starShape) // The following line would also destroy the currently attached starShape // engine.block.destroy(graphic) val allShapeProperties = engine.block.findAllProperties(starShape) engine.block.setInt(starShape, property = "shape/star/points", value = 6) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/stickers-and-shapes/create-edit/create-shapes-64acc0) # Create Shapes ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun usingShapes( 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.create() val graphic = engine.block.create(DesignBlockType.Graphic) 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.setWidth(graphic, value = 100F) engine.block.setHeight(graphic, value = 100F) engine.block.appendChild(parent = scene, child = graphic) engine.scene.zoomToBlock( graphic, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) engine.block.supportsShape(graphic) // Returns true val text = engine.block.create(DesignBlockType.Text) engine.block.supportsShape(text) // Returns false val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(graphic, shape = rectShape) val shape = engine.block.getShape(graphic) val shapeType = engine.block.getType(shape) val starShape = engine.block.createShape(ShapeType.Star) engine.block.destroy(engine.block.getShape(graphic)) engine.block.setShape(graphic, shape = starShape) // The following line would also destroy the currently attached starShape // engine.block.destroy(graphic) val allShapeProperties = engine.block.findAllProperties(starShape) engine.block.setInt(starShape, property = "shape/star/points", value = 6) engine.stop()} ``` The CE.SDK provides a flexible way to create and customize shapes, including rectangles, circles, lines, and polygons. ## Supported Shapes The following shapes are supported in CE.SDK: * `ShapeType.Rect` * `ShapeType.Line` * `ShapeType.Ellipse` * `ShapeType.Polygon` * `ShapeType.Star` * `ShapeType.VectorPath` ## Creating Shapes `graphic` blocks don’t have any shape after you create them, which leaves them invisible by default. In order to make them visible, we need to assign both a shape and a fill to the `graphic` block. You can find more information on fills [here](android/fills-402ddc/). In this example we have created and attached an image fill. In order to create a new shape, we must call the `fun createShape(type: ShapeType): DesignBlock` API. ``` val rectShape = engine.block.createShape(ShapeType.Rect) ``` In order to assign this shape to the `graphic` block, call the `fun setShape(block: DesignBlock, shape: DesignBlock)` API. ``` engine.block.setShape(graphic, shape = rectShape) ``` Just like design blocks, shapes with different types have different properties that you can set via the API. Please refer to the [API docs](android/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) for a complete list of all available properties for each type of shape. ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeType fun usingShapes( 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 graphic = engine.block.create(DesignBlockType.Graphic) 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.setWidth(graphic, value = 100F) engine.block.setHeight(graphic, value = 100F) engine.block.appendChild(parent = scene, child = graphic) engine.scene.zoomToBlock( graphic, paddingLeft = 40F, paddingTop = 40F, paddingRight = 40F, paddingBottom = 40F, ) engine.block.supportsShape(graphic) // Returns true val text = engine.block.create(DesignBlockType.Text) engine.block.supportsShape(text) // Returns false val rectShape = engine.block.createShape(ShapeType.Rect) engine.block.setShape(graphic, shape = rectShape) engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/from-remote-source/unsplash-8f31f0) # From A Custom Source ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Assetimport ly.img.engine.AssetColorimport ly.img.engine.AssetContextimport ly.img.engine.AssetCreditsimport ly.img.engine.AssetDefinitionimport ly.img.engine.AssetLicenseimport ly.img.engine.AssetPayloadimport ly.img.engine.AssetSourceimport ly.img.engine.AssetUTMimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.FindAssetsQueryimport ly.img.engine.FindAssetsResultimport ly.img.engine.ShapeTypeimport org.json.JSONArrayimport org.json.JSONObjectimport java.net.HttpURLConnectionimport java.net.URL fun customAssetSource( license: String, userId: String, unsplashBaseUrl: 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 source = UnsplashAssetSource(unsplashBaseUrl) // INSERT YOUR UNSPLASH PROXY URL HERE engine.asset.addSource(source) val list = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "", page = 1, perPage = 10), ) }.getOrElse { it.printStackTrace() } val search = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "banana", page = 1, perPage = 10), ) }.getOrElse { it.printStackTrace() } engine.asset.addLocalSource( sourceId = "background-videos", supportedMimeTypes = listOf("video/mp4"), ) val asset = AssetDefinition( id = "ocean-waves-1", label = mapOf( "en" to "relaxing ocean waves", "es" to "olas del mar relajantes", ), tags = mapOf( "en" to listOf("ocean", "waves", "soothing", "slow"), "es" to listOf("mar", "olas", "calmante", "lento"), ), meta = mapOf( "uri" to "https://example.com/ocean-waves-1.mp4", "thumbUri" to "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType" to "video/mp4", "width" to "1920", "height" to "1080", ), payload = AssetPayload(color = AssetColor.RGB(r = 0F, g = 0F, b = 1F)), ) engine.asset.addAsset(sourceId = "background-videos", asset = asset) engine.stop()} class UnsplashAssetSource( private val baseUrl: String,) : AssetSource(sourceId = "ly.img.asset.source.unsplash") { override suspend fun getGroups(): List? = null override val supportedMimeTypes = listOf("image/jpeg") override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() } private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$baseUrl/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = Int.MAX_VALUE, ) } private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total, ) } private suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } } private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = "unsplash"), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) }, ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"), )} ``` In this example, we will show you how to integrate your custom asset sources into [CE.SDK](https://img.ly/products/creative-sdk). With CE.SDK you can directly add external image providers like Unsplash or your own backend. A third option we will explore in this guide is using the engine’s Asset API directly. Follow along with this example while we are going to add the Unsplash library. Adding an asset source is done creating an asset source definition and adding it using `fun addSource(source: AssetSource)`. The asset source needs a unique identifier as part of an object implementing the interface of the source. All Asset API methods require the asset source’s unique identifier. ``` val source = UnsplashAssetSource(unsplashBaseUrl) // INSERT YOUR UNSPLASH PROXY URL HEREengine.asset.addSource(source) ``` The most important function to implement is `suspend fun findAssets(sourceId: String, query: FindAssetsQuery): FindAssetsResult`. With this function alone you can define the complete asset source. It receives the asset query as an argument and returns results asynchronously. * The argument is the `query` and describes the slice of data the engine wants to use. This includes a query string and pagination information. * The result of this query, besides the actual asset data, returns information like the current page, the next page and the total number of assets available for this specific query. Providing a `suspend` function gives us great flexibility since we are completely agnostic of how we want to get the assets. We can use `HttpURLConnection`, local storage, cache or import a 3rd party library to return the result. ``` val list = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "", page = 1, perPage = 10), )}.getOrElse { it.printStackTrace() } ``` Let us implement an Unsplash asset source. Please note that this is just for demonstration purposes only and may not be ideal if you want to integrate Unsplash in your production environment. We will create a class implementing abstract class `AssetSource`. A unique `sourceId = "ly.img.asset.source.unsplash"` is passed via the constructor. There are multiple abstract methods that we need to implement, however, `findAssets` is the most important one. ``` class UnsplashAssetSource( private val baseUrl: String,) : AssetSource(sourceId = "ly.img.asset.source.unsplash") { ``` `findAssets` is the function that receives `query` object from the engine and is supposed to return the corresponding results. Unsplash has different API endpoints for different use cases. If we want to search we need to call a different endpoint as if we just want to display images without any search term. Therefore we need to check if the query data contains a `query` string. If `findAssets` was called with a non-empty `query` we call the `/search` endpoint via `getSearchList` function, otherwise we call `getPopularList`. As we can see in the example, we are passing the `query` object to `findAssets` method, containing the following fields: * `query.query`: The current search string from the search bar in the asset library. * `query.page`: For Unsplash specifically the requested page number starts with 1. We do not query all assets at once but by pages. As the user scrolls down more pages will be requested by calls to the `findAssets` method. * `query.perPage`: Determines how many assets we want to have included per page. This might change between calls. For instance, `perPage` can be called with a small number to display a small preview, but with a higher number e.g. if we want to show more assets in a grid view. ``` override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() } private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$baseUrl/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = Int.MAX_VALUE, ) } private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total, ) } ``` Once we receive the response and check for success we need to map Unsplash’s result to what the asset source API needs as a result. The CE.SDK expects an object with the following properties: * `assets`: An array of assets for the current query. We will take a look at what these have to look like in the next paragraph. * `currentPage`: Return the current page that was requested. * `nextPage`: This is the next page that can be requested after the current one. Should be `-1` if there is no other page (no more assets). In this case we stop querying for more even if the user has scrolled to the bottom. * `total`: The total number of assets available for the current query. If we search for “Cat” with `perPage` set to 30, we will get 30 assets, but `total` likely will be a much higher number. ``` val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject)val assetsArray = response.getJSONArray("results")val total = response.getInt("total")val totalPages = response.getInt("total_pages")return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total,) ``` Every image we get as a result of Unsplash needs to be translated into an object that is expected by the asset source API. We will describe every mandatory and optional property in the following paragraphs. ``` private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = "unsplash"), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) }, ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"),) ``` `id`: The id of the asset (mandatory). This has to be unique for this source configuration. ``` id = getString("id"), ``` `locale` (optional): The language locale for this asset is used in `label` and `tags`. ``` locale = "en", ``` `label` (optional): The label of this asset. It could be displayed in the tooltip as well as in the credits of the asset. ``` label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null}, ``` `tags` (optional): The tags of this asset. It could be displayed in the credits of the asset. ``` tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") }}?.takeIf { it.isNotEmpty() }, ``` `meta`: The meta object stores asset properties that depend on the specific asset type. ``` meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(),), ``` `uri`: For an image asset this is the URL to the image file that will be used to add the image to the scene. Note that we have to use the Unsplash API to obtain a usable URL at first. ``` "uri" to getJSONObject("urls").getString("full"), ``` `thumbUri`: The URI of the asset’s thumbnail. It could be used in an asset library. ``` "thumbUri" to getJSONObject("urls").getString("thumb"), ``` `blockType`: The type id of the design block that should be created when this asset is applied to the scene. If omitted, CE.SDK will try to infer the block type from an optionally provided `mimeType` property (e.g. `image/jpeg`) or by loading the asset data behind `uri` and parsing the mime type from that. However, this will cause a delay before the asset can be added to the scene, which is why it is always recommended to specify the `blockType` upfront. ``` "blockType" to DesignBlockType.Graphic.key, ``` `fillType`: The type id of the fill that should be attached to the block when this asset is applied to the scene. If omitted, CE.SDK will default to a solid color fill `//ly.img.ubq/fill/color`. ``` "fillType" to FillType.Image.key, ``` `shapeType`: The type id of the shape that should be attached to the block when this asset is applied to the scene. If omitted, CE.SDK will default to a rect shape `//ly.img.ubq/shape/rect`. ``` "shapeType" to ShapeType.Rect.key, ``` `kind`: The kind that should be set to the block when this asset is applied to the scene. If omitted, CE.SDK will default to an empty string. ``` "kind" to "image", ``` `width`: The original width of the image. `height`: The original height of the image. ``` "width" to getInt("width").toString(),"height" to getInt("height").toString(), ``` `looping`: Determines whether the asset allows looping (applicable only to Video and GIF). When set to `true`, the asset can extend beyond its original length by looping for the specified duration. `context`: Adds contextual information to the asset. Right now, this only includes the source id of the source configuration. ``` context = AssetContext(sourceId = "unsplash"), ``` `credits` (optional): Some image providers require to display credits to the asset’s artist. If set, it has to be an object with the artist’s `name` and a `url` to the artist page. ``` credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) },), ``` `utm` (optional): Some image providers require to add UTM parameters to all links to the source or the artist. If set, it contains a string to the `source` (added as `utm_source`) and the `medium` (added as `utm_medium`) ``` utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"), ``` After translating the asset to match the interface from the asset source API, the array of assets for the current page can be returned. Going further with our Unsplash integration we need to handle the case when no query was provided. Unsplash requires us to call a different API endpoint (`/photos`) with slightly different parameters but the basics are the same. We need to check for success, calculate `total` and `nextPage` and translate the assets. ``` val search = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "banana", page = 1, perPage = 10), )}.getOrElse { it.printStackTrace() } ``` In addition to `findAssets`, there are couple more methods that need to be implemented. We have already seen that an asset can define credits for the artist. Depending on the image provider you might need to add credits and the license for the source. In case of Unsplash, this includes a link as well as the license of all assets from this source. ``` override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), ) ``` ## Local Asset Sources In many cases, you already have various finite sets of assets that you want to make available via asset sources. In order to save you the effort of having to implement custom asset query callbacks for each of these asset sources, CE.SDK also allows you to create “local” asset sources, which are managed by the engine and provide search and pagination functionalities. In order to add such a local asset source, simply call the `addLocalSource` API and choose a unique id with which you can later access the asset source. ``` engine.asset.addLocalSource( sourceId = "background-videos", supportedMimeTypes = listOf("video/mp4"),) ``` The `fun addAsset(sourceId: String, asset: AssetDefinition)` API allows you to add new asset instances to your local asset source. The local asset source then keeps track of these assets and returns matching items as the result of asset queries. Asset queries return the assets in the same order in which they were inserted into the local asset source. Note that the `AssetDefinition` type that we pass to the `addAsset` API is slightly different than the `Asset` type which is returned by asset queries. The `AssetDefinition` for example contains all localizations of the labels and tags of the same asset whereas the `Asset` is specific to the locale property of the query. ``` val asset = AssetDefinition( id = "ocean-waves-1", label = mapOf( "en" to "relaxing ocean waves", "es" to "olas del mar relajantes", ), tags = mapOf( "en" to listOf("ocean", "waves", "soothing", "slow"), "es" to listOf("mar", "olas", "calmante", "lento"), ), meta = mapOf( "uri" to "https://example.com/ocean-waves-1.mp4", "thumbUri" to "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType" to "video/mp4", "width" to "1920", "height" to "1080", ), payload = AssetPayload(color = AssetColor.RGB(r = 0F, g = 0F, b = 1F)),)engine.asset.addAsset(sourceId = "background-videos", asset = asset) ``` ## Full Code Here’s the full code: ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Assetimport ly.img.engine.AssetColorimport ly.img.engine.AssetContextimport ly.img.engine.AssetCreditsimport ly.img.engine.AssetDefinitionimport ly.img.engine.AssetLicenseimport ly.img.engine.AssetPayloadimport ly.img.engine.AssetSourceimport ly.img.engine.AssetUTMimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.FindAssetsQueryimport ly.img.engine.FindAssetsResultimport ly.img.engine.ShapeTypeimport org.json.JSONArrayimport org.json.JSONObjectimport java.net.HttpURLConnectionimport java.net.URL fun customAssetSource( license: String, userId: String, unsplashBaseUrl: 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 source = UnsplashAssetSource(unsplashBaseUrl) // INSERT YOUR UNSPLASH PROXY URL HERE engine.asset.addSource(source) val list = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "", page = 1, perPage = 10), ) }.getOrElse { it.printStackTrace() } val search = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "banana", page = 1, perPage = 10), ) }.getOrElse { it.printStackTrace() } engine.asset.addLocalSource( sourceId = "background-videos", supportedMimeTypes = listOf("video/mp4"), ) val asset = AssetDefinition( id = "ocean-waves-1", label = mapOf( "en" to "relaxing ocean waves", "es" to "olas del mar relajantes", ), tags = mapOf( "en" to listOf("ocean", "waves", "soothing", "slow"), "es" to listOf("mar", "olas", "calmante", "lento"), ), meta = mapOf( "uri" to "https://example.com/ocean-waves-1.mp4", "thumbUri" to "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType" to "video/mp4", "width" to "1920", "height" to "1080", ), payload = AssetPayload(color = AssetColor.RGB(r = 0F, g = 0F, b = 1F)), ) engine.asset.addAsset(sourceId = "background-videos", asset = asset) engine.stop()} class UnsplashAssetSource( private val baseUrl: String,) : AssetSource(sourceId = "ly.img.asset.source.unsplash") { override suspend fun getGroups(): List? = null override val supportedMimeTypes = listOf("image/jpeg") override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), ) override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), ) override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() } private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$baseUrl/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = Int.MAX_VALUE, ) } private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total, ) } private suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } } private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = "unsplash"), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) }, ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"), )} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/capture-from-camera/recordings-c2ca1e) # Access Recordings ``` import android.os.Bundleimport android.util.Logimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport ly.img.camera.core.CameraResultimport ly.img.camera.core.CaptureVideoimport ly.img.camera.core.EngineConfiguration private const val TAG = "RecordingsCameraActivity" class RecordingsCameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureVideo.Input( engineConfiguration = EngineConfiguration( license = "", userId = "", ), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult } when (result) { is CameraResult.Record -> { for (recording in result.recordings) { Log.d(TAG, "Duration: ${recording.duration}") for (video in recording.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } } } is CameraResult.Reaction -> { Log.d(TAG, "Video uri: ${result.video.uri}") for (reaction in result.reaction) { Log.d(TAG, "Duration: ${reaction.duration}") for (video in reaction.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } } } else -> { Log.d(TAG, "Unhandled result") } } } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } }} ``` Learn how to get the recorded videos from the `CameraResult` type using the Activity Result APIs. ## Success A `Recording` has a `duration` and contains a list of `Video`s. The list contains either one `Video` (for single camera recording) or two `Video`s (for dual camera recordings). Dual camera is currently only available for iOS. Each `Video` has: * A `uri` to the video file that is stored in `Context::getFilesDir()`. Make sure to copy the file to a permanent location if you want to access it later. * A `rect` that contains the position of the video as it was shown in the camera preview. ``` when (result) { is CameraResult.Record -> { for (recording in result.recordings) { Log.d(TAG, "Duration: ${recording.duration}") for (video in recording.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } } } is CameraResult.Reaction -> { Log.d(TAG, "Video uri: ${result.video.uri}") for (reaction in result.reaction) { Log.d(TAG, "Duration: ${reaction.duration}") for (video in reaction.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } } } else -> { Log.d(TAG, "Unhandled result") } } ``` ### Standard Camera If the user has recorded videos, the `CameraResult.Record` case will contain a list of `Recording`s, each representing a segment of the recorded video. ``` is CameraResult.Record -> { for (recording in result.recordings) { Log.d(TAG, "Duration: ${recording.duration}") for (video in recording.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } }} ``` ### Video Reaction If the user has recorded a reaction, the `CameraResult.Reaction` case will contain the video that was reacted to and a list of `Recording`s, each representing a segment of the recorded video. ``` is CameraResult.Reaction -> { Log.d(TAG, "Video uri: ${result.video.uri}") for (reaction in result.reaction) { Log.d(TAG, "Duration: ${reaction.duration}") for (video in reaction.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } }} ``` ## Failure The result is `null` in case the user dismissed the camera at any point. ``` result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult} ``` ## Full Code Here’s the full code: ``` import android.os.Bundleimport android.util.Logimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport ly.img.camera.core.CameraResultimport ly.img.camera.core.CaptureVideoimport ly.img.camera.core.EngineConfiguration private const val TAG = "RecordingsCameraActivity" class RecordingsCameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureVideo.Input( engineConfiguration = EngineConfiguration( license = "", userId = "", ), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult } when (result) { is CameraResult.Record -> { for (recording in result.recordings) { Log.d(TAG, "Duration: ${recording.duration}") for (video in recording.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } } } is CameraResult.Reaction -> { Log.d(TAG, "Video uri: ${result.video.uri}") for (reaction in result.reaction) { Log.d(TAG, "Duration: ${reaction.duration}") for (video in reaction.videos) { Log.d(TAG, "Video Uri: ${video.uri} Video Rect: ${video.rect}") } } } else -> { Log.d(TAG, "Unhandled result") } } } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } }} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/capture-from-camera/record-video-47819b) # Record Video ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.camera" compileSdk 35 defaultConfig { applicationId "ly.img.editor.camera" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' }} dependencies { implementation "ly.img:engine-camera:1.51.0" implementation "androidx.camera:camera-core:1.2.3" implementation "androidx.camera:camera-camera2:1.2.3" implementation "androidx.camera:camera-view:1.2.3" implementation "androidx.camera:camera-lifecycle:1.2.3" implementation "androidx.camera:camera-video:1.2.3" implementation "ly.img:engine:1.51.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ``` import android.view.SurfaceViewimport androidx.appcompat.app.AppCompatActivityimport androidx.camera.core.CameraSelectorimport androidx.camera.core.MirrorModeimport androidx.camera.core.Previewimport androidx.camera.lifecycle.ProcessCameraProviderimport androidx.camera.video.FileOutputOptionsimport androidx.camera.video.Qualityimport androidx.camera.video.QualitySelectorimport androidx.camera.video.Recorderimport androidx.camera.video.VideoCaptureimport androidx.camera.video.VideoRecordEventimport androidx.core.content.ContextCompatimport androidx.core.net.toUriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.delayimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.EffectTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.camera.setCameraPreviewimport java.io.File fun usingCamera( activity: AppCompatActivity, surfaceView: SurfaceView, cameraProvider: ProcessCameraProvider, license: String, userId: String,) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindSurfaceView(surfaceView) val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_FRONT) .build() val preview = Preview.Builder().build() val qualitySelector = QualitySelector.from(Quality.FHD) val recorder = Recorder.Builder() .setQualitySelector(qualitySelector) .build() val videoCapture = VideoCapture.Builder(recorder) .setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY) .build() cameraProvider.bindToLifecycle(activity, cameraSelector, preview, videoCapture) val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) val pixelStreamFill = engine.block.createFill(FillType.PixelStream) engine.block.setFill(block = page, fill = pixelStreamFill) engine.setCameraPreview(pixelStreamFill, preview, mirrored = false) engine.block.appendEffect( block = page, effectBlock = engine.block.createEffect(EffectType.HalfTone), ) // If camerax preview transformation info rotation is 90, this will return Left. If we passed mirrored = true, this would be LeftMirrored. val orientation = engine.block.getEnum( block = pixelStreamFill, property = "fill/pixelStream/orientation", ) val recordingFile = File(surfaceView.context.filesDir, "temp.mp4") val fileOutputOptions = FileOutputOptions.Builder(recordingFile).build() val recording = videoCapture.output .prepareRecording(activity, fileOutputOptions) .start(ContextCompat.getMainExecutor(surfaceView.context)) { if (it !is VideoRecordEvent.Finalize) return@start val videoFill = engine.block.createFill(FillType.Video) engine.block.setFill(block = page, fill = videoFill) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = recordingFile.toUri().toString(), ) } delay(5000L) recording.stop()} ``` Other than having pre-recorded [video](android/create-video-c41a08/) in your scene you can also have a live preview from a camera in the engine. This allows you to make full use of the engine’s capabilities such as [effects](android/filters-and-effects-6f88ac/), [strokes](android/outlines/strokes-c2e621/) and [drop shadows](android/outlines/shadows-and-glows-6610fa/), while the preview integrates with the composition of your scene. Simply swap out the `VideoFill` of a block with a `PixelStreamFill`. This guide shows you how the `PixelStreamFill` can be used in combination with a camera. Before starting the implementation, we are going to need couple more dependencies other than the Engine itself. Camera preview implementation is based on the android library [camerax](https://developer.android.com/training/camerax), therefore, we should include all required camerax dependencies to our project. You can check all available dependencies [here](https://developer.android.com/training/camerax/architecture). Other than camerax, we also need to include the Engine camera extension dependency. This dependency provides API for a single line bridging between camerax and the Engine. It is important that we use camerax version >= 1.1.0 in order to avoid unexpected crashes due to API signature changes. Also, it is highly recommended to always use the exact same version for both `engine` and `engine-camera` dependencies. ``` implementation "ly.img:engine-camera:1.51.0"implementation "androidx.camera:camera-core:1.2.3"implementation "androidx.camera:camera-camera2:1.2.3"implementation "androidx.camera:camera-view:1.2.3"implementation "androidx.camera:camera-lifecycle:1.2.3"implementation "androidx.camera:camera-video:1.2.3" ``` Now we have all the required dependencies to work with the camera. We instantiate all required camerax objects in order to start previewing. You can check all available camerax preview configurations [here](https://developer.android.com/training/camerax/architecture). ``` val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_FRONT) .build() val preview = Preview.Builder().build() val qualitySelector = QualitySelector.from(Quality.FHD) val recorder = Recorder.Builder() .setQualitySelector(qualitySelector) .build() val videoCapture = VideoCapture.Builder(recorder) .setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY) .build() cameraProvider.bindToLifecycle(activity, cameraSelector, preview, videoCapture) ``` We create a video scene with a single page. Then we create a `PixelStreamFill` and assign it to the page. Then we connect camerax `Preview` and `PixelStreamFill` objects via `setCameraPreview` extension function that is provided by `engine-camera` dependency. To demonstrate the live preview capabilities of the engine we also apply an effect to the page. ``` val scene = engine.scene.createForVideo()val page = engine.block.create(DesignBlockType.Page)engine.block.appendChild(parent = scene, child = page)val pixelStreamFill = engine.block.createFill(FillType.PixelStream)engine.block.setFill(block = page, fill = pixelStreamFill)engine.setCameraPreview(pixelStreamFill, preview, mirrored = false)engine.block.appendEffect( block = page, effectBlock = engine.block.createEffect(EffectType.HalfTone),) ``` ## Orientation To not waste expensive compute time by transforming the pixel data of the buffer itself, it’s often beneficial to apply a transformation during rendering and let the GPU handle this work much more efficiently. For this purpose the `PixelStreamFill` has an `orientation` property. You can use it to mirror the image or rotate it in 90° steps. This property lets you easily mirror an image from a front facing camera or rotate the image by 90° when the user holds a device sideways. Note that its initial value is set in `setCameraPreview` based on camerax preview transformation info listener and `mirrored` flag. Available values are `Left`, `LeftMirrored`, `Down`, `DownMirrored`, `Right`, `RightMirrored`, `Up`, `UpMirrored`. ``` // If camerax preview transformation info rotation is 90, this will return Left. If we passed mirrored = true, this would be LeftMirrored.val orientation = engine.block.getEnum( block = pixelStreamFill, property = "fill/pixelStream/orientation",) ``` ## Camera Camerax is a very powerful library and it allows video capturing, image capturing and other use cases. Note that the Engine does not limit usage of any of the camerax use cases: it only provides a mechanism to render camera preview into the Engine canvas. For demonstration purposes, we will proceed with video capture. We create a video capture session and start recording the frames into a temporary file in `filesDir`. Once the recording is finished we swap the `PixelStreamFill` with a `VideoFill` to play back the recorded video file. ``` val recordingFile = File(surfaceView.context.filesDir, "temp.mp4")val fileOutputOptions = FileOutputOptions.Builder(recordingFile).build()val recording = videoCapture.output .prepareRecording(activity, fileOutputOptions) .start(ContextCompat.getMainExecutor(surfaceView.context)) { if (it !is VideoRecordEvent.Finalize) return@start val videoFill = engine.block.createFill(FillType.Video) engine.block.setFill(block = page, fill = videoFill) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = recordingFile.toUri().toString(), ) }delay(5000L)recording.stop() ``` ## Full Code Here’s the full code for both files: ### build.gradle ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.camera" compileSdk 35 defaultConfig { applicationId "ly.img.editor.camera" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' }} dependencies { implementation "ly.img:engine-camera:1.47.0" implementation "androidx.camera:camera-core:1.2.3" implementation "androidx.camera:camera-camera2:1.2.3" implementation "androidx.camera:camera-view:1.2.3" implementation "androidx.camera:camera-lifecycle:1.2.3" implementation "androidx.camera:camera-video:1.2.3" implementation "ly.img:engine:1.47.0" implementation "androidx.appcompat:appcompat:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ### UsingCamera.kt ``` import android.view.SurfaceViewimport androidx.appcompat.app.AppCompatActivityimport androidx.camera.core.CameraSelectorimport androidx.camera.core.MirrorModeimport androidx.camera.core.Previewimport androidx.camera.lifecycle.ProcessCameraProviderimport androidx.camera.video.FileOutputOptionsimport androidx.camera.video.Qualityimport androidx.camera.video.QualitySelectorimport androidx.camera.video.Recorderimport androidx.camera.video.VideoCaptureimport androidx.camera.video.VideoRecordEventimport androidx.core.content.ContextCompatimport androidx.core.net.toUriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.delayimport kotlinx.coroutines.launchimport ly.img.engine.DesignBlockTypeimport ly.img.engine.EffectTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.camera.setCameraPreviewimport java.io.File fun usingCamera( activity: AppCompatActivity, surfaceView: SurfaceView, cameraProvider: ProcessCameraProvider, license: String, userId: String,) = CoroutineScope(Dispatchers.Main).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindSurfaceView(surfaceView) val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_FRONT) .build() val preview = Preview.Builder().build() val qualitySelector = QualitySelector.from(Quality.FHD) val recorder = Recorder.Builder() .setQualitySelector(qualitySelector) .build() val videoCapture = VideoCapture.Builder(recorder) .setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY) .build() cameraProvider.bindToLifecycle(activity, cameraSelector, preview, videoCapture) val scene = engine.scene.createForVideo() val page = engine.block.create(DesignBlockType.Page) engine.block.appendChild(parent = scene, child = page) val pixelStreamFill = engine.block.createFill(FillType.PixelStream) engine.block.setFill(block = page, fill = pixelStreamFill) engine.setCameraPreview(pixelStreamFill, preview, mirrored = false) engine.block.appendEffect( block = page, effectBlock = engine.block.createEffect(EffectType.HalfTone), ) // If camerax preview transformation info rotation is 90, this will return Left. If we passed mirrored = true, this would be LeftMirrored. val orientation = engine.block.getEnum( block = pixelStreamFill, property = "fill/pixelStream/orientation", ) val recordingFile = File(surfaceView.context.filesDir, "temp.mp4") val fileOutputOptions = FileOutputOptions.Builder(recordingFile).build() val recording = videoCapture.output .prepareRecording(activity, fileOutputOptions) .start(ContextCompat.getMainExecutor(surfaceView.context)) { if (it !is VideoRecordEvent.Finalize) return@start val videoFill = engine.block.createFill(FillType.Video) engine.block.setFill(block = page, fill = videoFill) engine.block.setString( block = videoFill, property = "fill/video/fileURI", value = recordingFile.toUri().toString(), ) } delay(5000L) recording.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/capture-from-camera/integrate-33d863) # Integrate Mobile Camera In this example, we will show you how to initialize the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s mobile camera in your Android app. Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-android-examples/tree/v1.51.0/camera-guides-quickstart/). ## Adding dependency Add IMG.LY maven repository to the list of maven urls in the `settings.gradle` file. ``` maven { name "IMG.LY Artifactory" url "https://artifactory.img.ly/artifactory/maven" mavenContent { includeGroup("ly.img") } } ``` Add camera dependency in the `build.gradle` file of your application module. ``` implementation "ly.img:camera:1.47.0" ``` ## Requirements In order to use the mobile camera, your application should meet the following requirements: * `buildFeatures.compose` should be `true`, as the camera is written in Jetpack Compose. ``` buildFeatures { compose true } ``` * `composeOptions.kotlinCompilerExtensionVersion` should match the kotlin version. Use the official compatibility map in [here](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). ``` composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } ``` * `compose-bom` version is `2023.05.01` or higher if your project uses Jetpack Compose dependencies. Note that using lower versions may cause crashes and issues in your own compose code, as our version will override yours. In case you are not using BOM, you can find the BOM to compose library version mapping in [here](https://developer.android.com/jetpack/compose/bom/bom-mapping). ``` implementation(platform("androidx.compose:compose-bom:2023.05.01")) ``` * Kotlin version is 1.9.10 or higher. * `minSdk` is 24 (Android 7) or higher. ``` minSdk 24 ``` By default, the mobile camera supports following ABIs: `arm64-v8a`, `armeabi-v7a`, `x86_64` and `x86`. If you want to filter out some of the ABIs, use `abiFilters`. ``` ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } ``` ## Usage This example shows the basic usage of the camera using the [Activity Result APIs](https://developer.android.com/training/basics/intents/result). In this integration example, on tapping the button, the `ActivityResultLauncher` is launched, presenting the camera `Activity`. ``` cameraLauncher.launch(cameraInput) ``` ### Initialization The camera input is initialized with `EngineConfiguration`. You need to provide the license key that you received from IMG.LY. Optionally, you can provide a unique ID tied to your application’s user. This helps us accurately calculate monthly active users (MAU) and it is especially useful when one person uses the app on multiple devices with a sign-in feature, ensuring they’re counted once. ``` val cameraInput = CaptureVideo.Input( engineConfiguration = EngineConfiguration( license = "", userId = "", ), ) ``` ### CaptureVideo ActivityResultContract Here, we register a request to start the Camera designated by the `CaptureVideo` contract. ``` val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> ``` ### Result The `CaptureVideo` contract’s output is a `CameraResult?`. It is `null` when the camera is dismissed by the user and non-null when the user has recorded videos. `CameraResult` is a sealed interface and the result can be obtained by casting it appropriately. ``` result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult } when (result) { is CameraResult.Record -> { val recordedVideoUris = result.recordings.flatMap { it.videos.map { it.uri } } // Do something with the recorded videos Log.d(TAG, "Recorded videos: $recordedVideoUris") } else -> { Log.d(TAG, "Unhandled result") } } ``` That is all. For more than basic configuration, check out all the available [configurations](android/user-interface/customization-72b2f8/). ## Full Code Here’s the full code for all 3 files. ### settings.gradle ``` pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() }} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { name "IMG.LY Artifactory" url "https://artifactory.img.ly/artifactory/maven" mavenContent { includeGroup("ly.img") } } }} rootProject.name = "My App"include ':app' ``` ### build.gradle ``` plugins { id 'com.android.application' id 'kotlin-android'} android { namespace "ly.img.editor.showcase" compileSdk 35 defaultConfig { applicationId "ly.img.editor.showcase" minSdk 24 targetSdk 35 versionCode 1 versionName "1.0" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64", "x86" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" }} dependencies { implementation "ly.img:camera:1.47.0" implementation(platform("androidx.compose:compose-bom:2023.05.01")) implementation "androidx.activity:activity-compose:1.8.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"} ``` ### CameraActivity.kt ``` import android.os.Bundleimport android.util.Logimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport ly.img.camera.core.CameraResultimport ly.img.camera.core.CaptureVideoimport ly.img.camera.core.EngineConfiguration private const val TAG = "CameraActivity" class CameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureVideo.Input( engineConfiguration = EngineConfiguration( license = "", userId = "", ), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult } when (result) { is CameraResult.Record -> { val recordedVideoUris = result.recordings.flatMap { it.videos.map { it.uri } } // Do something with the recorded videos Log.d(TAG, "Recorded videos: $recordedVideoUris") } else -> { Log.d(TAG, "Unhandled result") } } } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } }} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/capture-from-camera/camera-configuration-46afd0) # Mobile Camera Configuration ``` import android.os.Bundleimport android.util.Logimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport androidx.compose.ui.graphics.Colorimport ly.img.camera.core.CameraConfigurationimport ly.img.camera.core.CameraModeimport ly.img.camera.core.CaptureVideoimport ly.img.camera.core.EngineConfigurationimport kotlin.time.Duration.Companion.seconds private const val TAG = "ConfiguredCameraActivity" class ConfiguredCameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureVideo.Input( engineConfiguration = EngineConfiguration( license = "", userId = "", ), cameraConfiguration = CameraConfiguration( recordingColor = Color.Blue, maxTotalDuration = 30.seconds, allowExceedingMaxDuration = false, ), cameraMode = CameraMode.Standard(), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult } Log.d(TAG, "Result: $result") } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } }} ``` In this example, we will show you how to configure the camera. ## EngineConfiguration All the basic engine configuration settings are part of the `EngineConfiguration` which are required to initialize the camera. ``` engineConfiguration = EngineConfiguration( license = "", userId = "",), ``` * `license` – the license to activate the [Engine](android/get-started/overview-e18f40/) with. ``` license = "", ``` * `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`. ``` userId = "", ``` ## CameraConfiguration You can optionally pass a `CameraConfiguration` object to the `CaptureVideo.Input` constructor to customise the camera experience and behaviour. ``` cameraConfiguration = CameraConfiguration( recordingColor = Color.Blue, maxTotalDuration = 30.seconds, allowExceedingMaxDuration = false,), ``` * `recordingColor` – the color of the record button. ``` recordingColor = Color.Blue, ``` * `maxTotalDuration` – the total duration that the camera is allowed to record. ``` maxTotalDuration = 30.seconds, ``` * `allowExceedingMaxDuration` – Set to `true` to allow exceeding the `maxTotalDuration`. ``` allowExceedingMaxDuration = false, ``` ## Mode You can optionally configure the initial mode of the camera. ### Available Modes * `Standard`: the regular camera. This is the default. * `Reaction(video, cameraLayoutMode, positionsSwapped)`: records with the camera while playing back a video. * `video` Uri of the video to react to. * `cameraLayoutMode` The layout mode. Available options are `Horizontal` and `Vertical`. * `positionsSwapped` A boolean indicating if the video and camera feed should swap positions. By default, it is false and the video being reacted to is on the top/left (depending on `cameraLayoutMode`) ``` cameraMode = CameraMode.Standard(), ``` ## Full Code Here’s the full code: ``` import android.os.Bundleimport android.util.Logimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport androidx.compose.ui.graphics.Colorimport ly.img.camera.core.CameraConfigurationimport ly.img.camera.core.CameraModeimport ly.img.camera.core.CaptureVideoimport ly.img.camera.core.EngineConfigurationimport kotlin.time.Duration.Companion.seconds private const val TAG = "ConfiguredCameraActivity" class ConfiguredCameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraInput = CaptureVideo.Input( engineConfiguration = EngineConfiguration( license = "", userId = "", ), cameraConfiguration = CameraConfiguration( recordingColor = Color.Blue, maxTotalDuration = 30.seconds, allowExceedingMaxDuration = false, ), cameraMode = CameraMode.Standard(), ) setContent { val cameraLauncher = rememberLauncherForActivityResult(contract = CaptureVideo()) { result -> result ?: run { Log.d(TAG, "Camera dismissed") return@rememberLauncherForActivityResult } Log.d(TAG, "Result: $result") } Button( onClick = { cameraLauncher.launch(cameraInput) }, ) { Text(text = "Open Camera") } } }} ``` --- [Source](https:/img.ly/docs/cesdk/android/import-media/asset-panel/customize-c9a4de) # Customize ``` import androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EngineConfigurationimport ly.img.editor.core.library.AssetLibraryimport ly.img.editor.rememberForDesign // Add this composable to your NavHost@Composablefun AssetLibraryEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.rememberForDesign( license = "", ) val editorConfiguration = EditorConfiguration.rememberForDesign( assetLibrary = AssetLibrary.getDefault(), ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` ``` import androidx.compose.runtime.Composableimport androidx.compose.runtime.rememberimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EditorDefaultsimport ly.img.editor.EngineConfigurationimport ly.img.editor.core.library.AssetLibraryimport ly.img.editor.core.library.AssetTypeimport ly.img.editor.core.library.LibraryCategoryimport ly.img.editor.core.library.LibraryContentimport ly.img.editor.core.library.addSectionimport ly.img.editor.core.library.data.AssetSourceTypeimport ly.img.editor.core.library.dropSectionimport ly.img.editor.core.library.replaceSectionimport ly.img.editor.rememberForDesignimport ly.img.editor.smoketests.R // Add this composable to your NavHost@Composablefun DefaultAssetLibraryEditorSolution(navController: NavHostController) { val unsplashAssetSource = remember { UnsplashAssetSource("") } val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) { _, _ -> editorContext.engine.asset.addSource(unsplashAssetSource) } }, ) val assetLibrary = remember { val unsplashSection = LibraryContent.Section( titleRes = R.string.unsplash, sourceTypes = listOf(AssetSourceType(sourceId = unsplashAssetSource.sourceId)), assetType = AssetType.Image, ) AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, AssetLibrary.Tab.SHAPES, AssetLibrary.Tab.STICKERS, AssetLibrary.Tab.TEXT, ), images = LibraryCategory.Images .replaceSection(index = 0) { // We replace the title: "Image Uploads" -> "Uploads" copy(titleRes = R.string.uploads) } .dropSection(index = 1) .addSection(unsplashSection), ) } val editorConfiguration = EditorConfiguration.rememberForDesign( assetLibrary = assetLibrary, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` ``` import androidx.compose.runtime.Composableimport androidx.compose.runtime.rememberimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EditorDefaultsimport ly.img.editor.EngineConfigurationimport ly.img.editor.core.Rimport ly.img.editor.core.iconpack.IconPackimport ly.img.editor.core.iconpack.LibraryElementsimport ly.img.editor.core.library.AssetLibraryimport ly.img.editor.core.library.AssetTypeimport ly.img.editor.core.library.LibraryCategoryimport ly.img.editor.core.library.LibraryCategory.Companion.sourceTypesimport ly.img.editor.core.library.LibraryContentimport ly.img.editor.core.library.addSectionimport ly.img.editor.core.library.data.AssetSourceTypeimport ly.img.editor.rememberForDesignimport ly.img.editor.smoketests.R as SmokeTestR // Add this composable to your NavHost@Composablefun CustomAssetLibraryEditorSolution(navController: NavHostController) { val unsplashAssetSource = remember { UnsplashAssetSource("") } val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) { _, _ -> editorContext.engine.asset.addSource(unsplashAssetSource) } }, ) val assetLibrary = remember { // We create a custom tab with title "My Assets" that contains 2 sections: // 1. Stickers - expanding it opens the default stickers content // 2. Text - expanding it opens the default text content. Note that the title is skipped. val myAssetsCategory = LibraryCategory( tabTitleRes = SmokeTestR.string.my_assets, tabSelectedIcon = IconPack.LibraryElements, tabUnselectedIcon = IconPack.LibraryElements, content = LibraryContent.Sections( titleRes = SmokeTestR.string.my_assets, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_stickers, sourceTypes = LibraryContent.Stickers.sourceTypes, assetType = AssetType.Sticker, expandContent = LibraryContent.Stickers, ), LibraryContent.Section( sourceTypes = LibraryContent.Text.sourceTypes, assetType = AssetType.Text, expandContent = LibraryContent.Text, ), ), ), ) AssetLibrary( tabs = { listOf( myAssetsCategory, LibraryCategory.Images, ) }, images = { val unsplashSection = LibraryContent.Section( titleRes = SmokeTestR.string.unsplash, sourceTypes = listOf(AssetSourceType(sourceId = unsplashAssetSource.sourceId)), assetType = AssetType.Image, ) // See how the images is different in tabs and here LibraryCategory.Images.addSection(unsplashSection) }, ) } val editorConfiguration = EditorConfiguration.rememberForDesign( assetLibrary = assetLibrary, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }} ``` In this example, we will show you how to customize the asset library for the mobile editor. The example is based on the `Design Editor`, however, it is exactly the same for all the other [solutions](android/prebuilt-solutions-d0ed07/). Explore a full code sample on [GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.51.0/editor-guides-configuration-asset-library/). ## Configuration Asset library configuration is part of the `EditorConfiguration` class. Use the `EditorConfiguration.getDefault` helper function to make asset library configurations. * `assetLibrary` - the asset library UI definition used by the editor. It defines the content of the tabs when floating action button is clicked as well as the content of sheets when images and stickers are replaced. By default, `AssetLibrary.getDefault()` is used. ``` val editorConfiguration = EditorConfiguration.rememberForDesign( assetLibrary = AssetLibrary.getDefault(),) ``` ### Custom Asset Source To use custom asset sources in the asset library UI, the custom asset source must be first added to the engine. In addition to creating or loading a scene, registering the asset sources should be done in the [`onCreate` callback](android/user-interface/events-514b70/). In this example, the `EditorDefaults.onCreate` default implementation is used and afterwards, the [custom `UnsplashAssetSource`](android/import-media/from-remote-source/unsplash-8f31f0/) is added. ``` val unsplashAssetSource = remember { UnsplashAssetSource("")}val engineConfiguration = EngineConfiguration.remember( license = "", onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) { _, _ -> editorContext.engine.asset.addSource(unsplashAssetSource) } },) ``` ### Default Asset Library `AssetLibrary.getDefault()` provides a simple API in case you want to reorder/drop predefined tabs, as well as adjust the content of the tabs. In this example, firstly, we drop the `Elements` tab and reorder the remaining ones and secondly, we manipulate the sections of the `images` tab by dropping, swapping and appending sections. Note that we append the section based on the `UnsplashAssetSource` shown above. ``` val assetLibrary = remember { val unsplashSection = LibraryContent.Section( titleRes = R.string.unsplash, sourceTypes = listOf(AssetSourceType(sourceId = unsplashAssetSource.sourceId)), assetType = AssetType.Image, ) AssetLibrary.getDefault( tabs = listOf( AssetLibrary.Tab.IMAGES, AssetLibrary.Tab.SHAPES, AssetLibrary.Tab.STICKERS, AssetLibrary.Tab.TEXT, ), images = LibraryCategory.Images .replaceSection(index = 0) { // We replace the title: "Image Uploads" -> "Uploads" copy(titleRes = R.string.uploads) } .dropSection(index = 1) .addSection(unsplashSection), )}val editorConfiguration = EditorConfiguration.rememberForDesign( assetLibrary = assetLibrary,) ``` ### Custom Asset Library In case you want to adjust the asset library even further (i.e. create your own completely custom tabs, use different `images` category in FAB and replace sheets etc), you should use the constructor of the `AssetLibrary` instead of the helper `AssetLibrary.getDefault()` function. In this example, firstly, we fill the tabs with a custom `My Assets` tab as well as with the default `images` tab and secondly, we use a different `images` category for the replace sheet, which contains the default `images` + `UnsplashAssetSource` section. ``` val assetLibrary = remember { // We create a custom tab with title "My Assets" that contains 2 sections: // 1. Stickers - expanding it opens the default stickers content // 2. Text - expanding it opens the default text content. Note that the title is skipped. val myAssetsCategory = LibraryCategory( tabTitleRes = SmokeTestR.string.my_assets, tabSelectedIcon = IconPack.LibraryElements, tabUnselectedIcon = IconPack.LibraryElements, content = LibraryContent.Sections( titleRes = SmokeTestR.string.my_assets, sections = listOf( LibraryContent.Section( titleRes = R.string.ly_img_editor_stickers, sourceTypes = LibraryContent.Stickers.sourceTypes, assetType = AssetType.Sticker, expandContent = LibraryContent.Stickers, ), LibraryContent.Section( sourceTypes = LibraryContent.Text.sourceTypes, assetType = AssetType.Text, expandContent = LibraryContent.Text, ), ), ), ) AssetLibrary( tabs = { listOf( myAssetsCategory, LibraryCategory.Images, ) }, images = { val unsplashSection = LibraryContent.Section( titleRes = SmokeTestR.string.unsplash, sourceTypes = listOf(AssetSourceType(sourceId = unsplashAssetSource.sourceId)), assetType = AssetType.Image, ) // See how the images is different in tabs and here LibraryCategory.Images.addSection(unsplashSection) }, )}val editorConfiguration = EditorConfiguration.rememberForDesign( assetLibrary = assetLibrary,) ``` --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/export/overview-9ed3a8) # Options ``` import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Engineimport ly.img.engine.ExportOptionsimport ly.img.engine.MimeTypeimport ly.img.engine.addDefaultAssetSourcesimport java.io.Fileimport java.util.UUID fun exportingBlocks( 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) engine.scene.create() engine.editor.setSettingString( "basePath", value = "https://cdn.img.ly/packages/imgly/cesdk-engine/1.23.0/assets", ) engine.addDefaultAssetSources() 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) // 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, 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. 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. 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. ``` 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 ``` 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 ``` 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. ``` 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. ``` 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. ``` fun getAvailableMemory(): Long ``` Get the currently available memory in bytes. * Returns the currently available memory in bytes. --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/export/size-limits-6f0695) # Size Limits CreativeEditor SDK (CE.SDK) supports exporting high-resolution image, video, and audio content, but there are practical limits to consider based on the user’s device capabilities. ## Image Resolution Limits Constraint Recommendation / Limit **Input Resolution** Maximum input resolution is **4096×4096 pixels**. Images from external sources (e.g., Unsplash) are resized to this size before rendering on the canvas. You can modify this value using the `maxImageSize` setting. **Output Resolution** There is no enforced output resolution limit. Theoretically, the editor supports output sizes up to **16,384×16,384 pixels**. However, practical limits depend on the device’s GPU capabilities and available memory. All image processing in CE.SDK is handled client-side, so these values depend on the **maximum texture size** supported by the user’s hardware. The default limit of 4096×4096 is a safe baseline that works universally. Higher resolutions (e.g., 8192×8192) may work on certain devices but could fail on others during export if the GPU texture size is exceeded. To ensure consistent results across devices, it’s best to test higher output sizes on your target hardware and set conservative defaults in production. ## Video Resolution & Duration Limits Constraint Recommendation / Limit **Resolution** Up to **4K UHD** is supported for **playback** and **export**, depending on the user’s hardware and available GPU resources. For **import**, CE.SDK does not impose artificial limits, but maximum video size is bounded by the **32-bit address space of WebAssembly (wasm32)** and the **browser tab’s memory cap (~2 GB)**. **Frame Rate** 30 FPS at 1080p is broadly supported; 60 FPS and high-res exports benefit from hardware acceleration **Duration** Stories and reels of up to **2 minutes** are fully supported. Longer videos are also supported, but we generally found a maximum duration of **10 minutes** to be a good balance for a smooth editing experience and a pleasant export duration of around one minute on modern hardware. Performance scales with client hardware. For best results with high-resolution or high-frame-rate video, modern CPUs/GPUs with hardware acceleration are recommended. --- [Source](https:/img.ly/docs/cesdk/android/edit-image/transform/crop-f67a47) # Crop Images in Android ``` engine.block.supportsCrop(image)engine.block.setCropScaleX(image, scaleX = 2F)engine.block.setCropScaleY(image, scaleY = 1.5F)engine.block.setCropScaleRatio(image, scaleRatio = 3F)engine.block.setCropRotation(image, rotation = PI.toFloat())engine.block.setCropTranslationX(image, translationX = -1F)engine.block.setCropTranslationY(image, translationY = 1F)engine.block.adjustCropToFillFrame(image, minScaleRatio = 1F)engine.block.setContentFillMode(image, mode = ContentFillMode.CONTAIN)engine.block.resetCrop(image)engine.block.getCropScaleX(image)engine.block.getCropScaleY(image)engine.block.flipCropHorizontal(image)engine.block.flipCropVertical(image)engine.block.getCropScaleRatio(image)engine.block.getCropRotation(image)engine.block.getCropTranslationX(image)engine.block.getCropTranslationY(image)engine.block.supportsContentFillMode(image)engine.block.getContentFillMode(image) ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to modify a blocks crop through the `block` Api. ## Common Properties Common properties are properties that occur on multiple block types. For instance, fill color properties are available for all the shape blocks and the text block. That’s why we built convenient setter and getter functions for these properties. So you don’t have to use the generic setters and getters and don’t have to provide a specific property path. There are also `has*` functions to query if a block supports a set of common properties. ### Crop Manipulate the cropping region of a block by setting its scale, rotation, and translation. ``` fun supportsCrop(block: DesignBlock): Boolean ``` Query if the given block has crop properties. * `block`: the block to query. * Returns true if the block has crop properties, false otherwise. ``` fun setCropScaleX( block: DesignBlock, scaleX: Float,) ``` Set the crop scale in x direction of the given design block. Required scope: “layer/crop” * `block`: the block whose crop scale in x direction should be set. * `scaleX`: the crop scale in x direction. ``` fun setCropScaleY( block: DesignBlock, scaleY: Float,) ``` Set the crop scale in y direction of the given design block. Required scope: “layer/crop” * `block`: the block whose crop scale in y direction should be set. * `scaleY`: the crop scale in y direction. ``` fun setCropScaleRatio( block: DesignBlock, scaleRatio: Float,) ``` Set the crop scale ratio of the given design block. This will uniformly scale the content up or down. The center of the scale operation is the center of the crop frame. Required scope: “layer/crop” * `block`: the block whose crop scale ratio should be set. * `scaleRatio`: the rotation in radians. ``` fun setCropRotation( block: DesignBlock, rotation: Float,) ``` Set the crop rotation of the given design block. Required scope: “layer/crop” * `block`: the block whose crop rotation should be set. * `rotation`: the rotation in radians. ``` fun setCropTranslationX( block: DesignBlock, translationX: Float,) ``` Set the crop translation in x direction of the given design block. Required scope: “layer/crop” * `block`: the block whose crop translation in x direction should be set. * `translationX`: the translation in x direction. ``` fun setCropTranslationY( block: DesignBlock, translationY: Float,) ``` Set the crop translation in y direction of the given design block. Required scope: “layer/crop” * `block`: the block whose crop translation in y direction should be set. * `translationY`: the translation in y direction. ``` fun adjustCropToFillFrame( block: DesignBlock, minScaleRatio: Float,) ``` Adjust the crop position/scale to at least fill the crop frame. Required scope: “layer/crop” * `block`: the block to query. * `minScaleRatio`: the minimal crop scale ratio to go down to. ``` fun setContentFillMode( block: DesignBlock, mode: ContentFillMode,) ``` Set a block’s content fill mode. Required scope: “layer/crop” * `block`: the block to update. * `mode`: the content fill mode. ``` fun resetCrop(block: DesignBlock) ``` Resets the manually set crop of the given design block. The block’s content fill mode is set to `ContentFillMode.COVER`. If the block has a fill, the crop values are updated so that it covers the block. Required scope: “layer/crop” * `block`: the block whose crop should be reset. ``` fun getCropScaleX(block: DesignBlock): Float ``` Get the crop scale in x direction of the given design block. * `block`: the block whose crop scale in x direction should be queried. * Returns the crop scale in x direction. ``` fun getCropScaleY(block: DesignBlock): Float ``` Get the crop scale in y direction of the given design block. * `block`: the block whose crop scale in y direction should be queried. * Returns the crop scale in y direction. ``` fun flipCropHorizontal(block: DesignBlock) ``` Adjusts the crop in order to flip the content along its own horizontal axis. * `block`: the block whose crop should be updated. ``` fun flipCropVertical(block: DesignBlock) ``` Adjusts the crop in order to flip the content along its own vertical axis. * `block`: the block whose crop should be updated. ``` fun getCropScaleRatio(block: DesignBlock): Float ``` Get the crop scale ratio of the given design block. * `block`: the block whose crop scale ratio should be queried. * Returns the crop scale ratio. ``` fun getCropRotation(block: DesignBlock): Float ``` Get the crop rotation of the given design block. * `block`: the block whose crop scale rotation should be queried. * Returns the crop rotation. ``` fun getCropTranslationX(block: DesignBlock): Float ``` Get the crop translation in x direction of the given design block. * `block`: the block whose crop translation in x direction should be queried. * Returns the crop translation in x direction. ``` fun getCropTranslationY(block: DesignBlock): Float ``` Get the crop translation in y direction of the given design block. * `block`: the block whose crop translation in y direction should be queried. * Returns the crop translation in y direction. ``` fun supportsContentFillMode(block: DesignBlock): Boolean ``` Query if the given block has a content fill mode. * `block`: the block to query. * Returns true if the block has a content fill mode, false otherwise. ``` fun getContentFillMode(block: DesignBlock): ContentFillMode ``` Query a block’s content fill mode. * `block`: the block to query. * Returns the current mode. ## Full Code Here’s the full code: ``` engine.block.supportsCrop(image)engine.block.setCropScaleX(image, scaleX = 2F)engine.block.setCropScaleY(image, scaleY = 1.5F)engine.block.setCropScaleRatio(image, scaleRatio = 3F)engine.block.setCropRotation(image, rotation = PI.toFloat())engine.block.setCropTranslationX(image, translationX = -1F)engine.block.setCropTranslationY(image, translationY = 1F)engine.block.adjustCropToFillFrame(image, minScaleRatio = 1F)engine.block.setContentFillMode(image, mode = ContentFillMode.CONTAIN)engine.block.resetCrop(image)engine.block.getCropScaleX(image)engine.block.getCropScaleY(image)engine.block.flipCropHorizontal(image)engine.block.flipCropVertical(image)engine.block.getCropScaleRatio(image)engine.block.getCropRotation(image)engine.block.getCropTranslationX(image)engine.block.getCropTranslationY(image)engine.block.supportsContentFillMode(image)engine.block.getContentFillMode(image) ``` --- [Source](https:/img.ly/docs/cesdk/android/create-video/audio/buffers-9c565b) # Buffers ``` // Create an audio block and append it to the pageval audioBlock = engine.block.create(DesignBlockType.Audio)engine.block.appendChild(parent = page, child = audioBlock) // Create a bufferval audioBuffer = engine.editor.createBuffer() // Reference the audio buffer resource from the audio blockengine.block.setUri( block = audioBlock, property = "audio/fileURI", value = audioBuffer) // Generate 10 seconds of stereo 48 kHz audio dataval sampleCount = 10 * 48000val byteBuffer = ByteBuffer.allocate(2 * 4 * sampleCount) //2 channels, each 4 bytesrepeat(sampleCount) { val sample = sin((440 * it * 2 * PI) / 48000).toFloat() byteBuffer.putFloat(sample) byteBuffer.putFloat(sample)}// Assign the audio data to the bufferval data = ByteArray(byteBuffer.capacity())byteBuffer.position(0)byteBuffer.get(data)engine.editor.setBufferData(uri = audioBuffer, offset = 0, data = data) // We can get subranges of the buffer dataval chunk = engine.editor.getBufferData(uri = audioBuffer, offset = 0, length = 4096) // Get current length of the buffer in bytesval length = engine.editor.getBufferLength(uri = audioBuffer) // Reduce the buffer to half its length, leading to 5 seconds worth of audioengine.editor.setBufferLength(uri = audioBuffer, length = data.size / 2) // Free dataengine.editor.destroyBuffer(uri = audioBuffer) ``` In this example, we will show you how to use the [CreativeEditor SDK](https://img.ly/products/creative-sdk)’s CreativeEngine to create buffers through the `editor` API. Buffers can hold arbitrary data. **Limitations** Buffers are intended for temporary data only. * Buffer data is not part of the [scene serialization](android/concepts/scenes-e8596d/) * Changes to buffers can’t be undone using the [history system](android/concepts/undo-and-history-99479d/) ``` fun createBuffer(): Uri ``` Create a resizable buffer that can hold arbitrary data. * Returns a uri to identify the buffer. ``` fun destroyBuffer(uri: Uri) ``` Destroy a buffer and free its resources. * `uri`: the uri of the buffer to destroy. ``` fun setBufferData( uri: Uri, offset: Int, data: ByteBuffer,) ``` Set the data of a buffer. * `uri`: the uri of the buffer. * `offset`: the offset in bytes at which to start writing. * `data`: the data to write. Note that it has to be a direct `ByteBuffer`, created either via `ByteBuffer.allocateDirect` or via JNI NewDirectByteBuffer API. ``` fun getBufferData( uri: Uri, offset: Int, length: Int,): ByteBuffer ``` Get the data of a buffer. * `uri`: the uri of the buffer. * `offset`: the offset in bytes at which to start reading. * `length`: the number of bytes to read. * Returns the data read from the buffer or an error. ``` fun setBufferLength( uri: Uri, length: Int,) ``` Set the length of a buffer. * `uri`: the uri of the buffer. * `length`: the new length of the buffer in bytes. ``` fun getBufferLength(uri: Uri): Int ``` Get the length of a buffer. * `uri`: the uri of the buffer. * Returns the length of the buffer in bytes. ## Full Code Here’s the full code: ``` // Create an audio block and append it to the pageval audioBlock = engine.block.create(DesignBlockType.Audio)engine.block.appendChild(parent = page, child = audioBlock) // Create a bufferval audioBuffer = engine.editor.createBuffer() // Reference the audio buffer resource from the audio blockengine.block.setUri( block = audioBlock, property = "audio/fileURI", value = audioBuffer) // Generate 10 seconds of stereo 48 kHz audio dataval sampleCount = 10 * 48000val byteBuffer = ByteBuffer.allocate(2 * 4 * sampleCount) //2 channels, each 4 bytesrepeat(sampleCount) { val sample = sin((440 * it * 2 * PI) / 48000).toFloat() byteBuffer.putFloat(sample) byteBuffer.putFloat(sample)}// Assign the audio data to the bufferval data = ByteArray(byteBuffer.capacity())byteBuffer.position(0)byteBuffer.get(data)engine.editor.setBufferData(uri = audioBuffer, offset = 0, data = data) // We can get subranges of the buffer dataval chunk = engine.editor.getBufferData(uri = audioBuffer, offset = 0, length = 4096) // Get current length of the buffer in bytesval length = engine.editor.getBufferLength(uri = audioBuffer) // Reduce the buffer to half its length, leading to 5 seconds worth of audioengine.editor.setBufferLength(uri = audioBuffer, length = data.size / 2) // Free dataengine.editor.destroyBuffer(uri = audioBuffer) ``` --- [Source](https:/img.ly/docs/cesdk/android/export-save-publish/export/to-pdf-95e04b) # To PDF ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Colorimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.ExportOptionsimport ly.img.engine.FillTypeimport ly.img.engine.MimeTypeimport ly.img.engine.ShapeTypeimport java.io.File fun underlayer( 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.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Star)) engine.block.setPositionX(block, value = 350F) engine.block.setPositionY(block, value = 400F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block, fill = fill) val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F) engine.block.setColor(fill, property = "fill/color/value", value = rgbaBlue) engine.editor.setSpotColor(name = "RDG_WHITE", Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F)) val mimeType = MimeType.PDF val options = ExportOptions( exportPdfWithUnderlayer = true, underlayerSpotColorName = "RDG_WHITE", underlayerOffset = -2.0F, ) val blob = engine.block.export(scene, mimeType = mimeType, options = options) withContext(Dispatchers.IO) { File.createTempFile("underlayer_example", ".pdf").apply { outputStream().channel.write(blob) } } engine.stop()} ``` When printing on a non-white medium or on a special medium like fabric or glass, printing your design over an underlayer helps achieve the desired result. An underlayer will typically be printed using a special ink and be of the exact shape of your design. When exporting to PDF, you can specify that an underlayer be automatically generated in the `ExportOptions`. An underlayer will be generated by detecting the contour of all elements on a page and inserting a new block with the shape of the detected contour. This new block will be positioned behind all existing block. After exporting, the new block will be removed. The result will be a PDF file containing an additional shape of the same shape as your design and sitting behind it. The ink to be used by the printer is specified in the `ExportOptions` with a [spot color](android/colors-a9b79c/). You can also adjust the scale of the underlayer shape with a negative or positive offset, in design units. **Warning** Do not flatten the resulting PDF file or you will lose the underlayer shape which sits behind your design. ## Setup the scene We first create a new scene with a graphic block that has a color fill. ``` val scene = engine.scene.create() val page = engine.block.create(DesignBlockType.Page) engine.block.setWidth(page, value = 800F) engine.block.setHeight(page, value = 600F) engine.block.appendChild(parent = scene, child = page) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Star)) engine.block.setPositionX(block, value = 350F) engine.block.setPositionY(block, value = 400F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block, fill = fill) val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F) engine.block.setColor(fill, property = "fill/color/value", value = rgbaBlue) ``` ## Add the underlayer’s spot color Here we instantiate a spot color with the known name of the ink the printer should use for the underlayer. The visual color approximation is not so important, so long as the name matches what the printer expects. ``` engine.editor.setSpotColor(name = "RDG_WHITE", Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F)) ``` ## Exporting with an underlayer We enable the automatic generation of an underlayer on export with the option `exportPdfWithUnderlayer = true`. We specify the ink to use with `underlayerSpotColorName = 'RDG_WHITE'`. In this instance, we make the underlayer a bit smaller than our design so we specify an offset of 2 design units (e.g. millimeters) with `underlayerOffset = -2.0`. ``` val mimeType = MimeType.PDFval options = ExportOptions( exportPdfWithUnderlayer = true, underlayerSpotColorName = "RDG_WHITE", underlayerOffset = -2.0F,)val blob = engine.block.export(scene, mimeType = mimeType, options = options)withContext(Dispatchers.IO) { File.createTempFile("underlayer_example", ".pdf").apply { outputStream().channel.write(blob) }} ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Colorimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.ExportOptionsimport ly.img.engine.FillTypeimport ly.img.engine.MimeTypeimport ly.img.engine.ShapeTypeimport java.io.File fun underlayer( 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) val block = engine.block.create(DesignBlockType.Graphic) engine.block.setShape(block, shape = engine.block.createShape(ShapeType.Star)) engine.block.setPositionX(block, value = 350F) engine.block.setPositionY(block, value = 400F) engine.block.setWidth(block, value = 100F) engine.block.setHeight(block, value = 100F) val fill = engine.block.createFill(FillType.Color) engine.block.setFill(block, fill = fill) val rgbaBlue = Color.fromRGBA(r = 0F, g = 0F, b = 1F, a = 1F) engine.block.setColor(fill, property = "fill/color/value", value = rgbaBlue) engine.editor.setSpotColor(name = "RDG_WHITE", Color.fromRGBA(r = 0.8F, g = 0.8F, b = 0.8F)) val mimeType = MimeType.PDF val options = ExportOptions( exportPdfWithUnderlayer = true, underlayerSpotColorName = "RDG_WHITE", underlayerOffset = -2.0F, ) val blob = engine.block.export(scene, mimeType = mimeType, options = options) withContext(Dispatchers.IO) { File.createTempFile("underlayer_example", ".pdf").apply { outputStream().channel.write(blob) } } engine.stop()} ``` --- [Source](https:/img.ly/docs/cesdk/android/create-templates/add-dynamic-content/placeholders-d9ba8a) # Placeholders ``` // Check if block supports placeholder behaviorif (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 ``` fun supportsPlaceholderBehavior(block: DesignBlock): Boolean ``` Query whether the block supports placeholder behavior. * `block`: the block to query. * Returns whether the block supports placeholder behavior. ``` 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. ``` 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. ``` 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. ``` 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. ``` fun supportsPlaceholderControls(block: DesignBlock): Boolean ``` Checks whether the block supports placeholder controls. * `block`: The block to query. * Returns whether the block supports placeholder controls. ``` 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. ``` 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. ``` 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. ``` 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: ``` // Check if block supports placeholder behaviorif (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) }} ``` --- [Source](https:/img.ly/docs/cesdk/android/create-templates/add-dynamic-content/text-variables-7ecb50) # Text Variables ``` // Query all variablesval variableNames = engine.variable.findAll() // Set, get and remove a variableengine.variable.set(key = "name", value = "Chris")val name = engine.variable.get(key = "name") // Chrisengine.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 ``` fun findAll(): List ``` Get all text variables currently stored in the engine. * Returns a list of variable names. ``` fun set( key: String, value: String,) ``` Set a text variable. * `key`: the variable’s key. * `value`: the text to replace the variable with. ``` fun get(key: String): String ``` Get a text variable. * `key`: the variable’s key. * Returns the text value of the variable. ``` fun remove(key: String) ``` Destroy a text variable. * `key`: the variable’s key. ``` 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. ![](/docs/cesdk/_astro/variables-dark.BuQESLUM_1Bmrn3.webp) ## Full Code Here’s the full code: ``` // Query all variablesval variableNames = engine.variable.findAll() // Set, get and remove a variableengine.variable.set(key = "name", value = "Chris")val name = engine.variable.get(key = "name") // Chrisengine.variable.remove(key = "name") val block = engine.block.create(DesignBlockType.Graphic)engine.block.referencesAnyVariables(block) ``` --- [Source](https:/img.ly/docs/cesdk/android/colors/for-print/spot-c3a150) # Spot Colors ``` // 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 ``` fun findAllSpotColors(): List ``` Queries the names of currently set spot colors previously set with \`setSpotColor“. * Returns the names of set spot colors. ``` 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. ``` 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. ``` 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. ``` 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. ``` 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: ``` // 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") ``` --- [Source](https:/img.ly/docs/cesdk/android/colors/for-screen/p3-706127) # P3 Colors 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. ``` // Check whether the current device supports working in the P3 color spaceval p3IsSupported = engine.editor.supportsP3()try { engine.editor.checkP3Support()} catch (ex: Exception) { // P3 is not supported on the current device} ``` --- [Source](https:/img.ly/docs/cesdk/android/animation/create/text-d6f4aa) # Text Animations ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.AnimationTypeimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeTypeimport 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")}") 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. ``` 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. ``` 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: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.AnimationTypeimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeTypeimport 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()} ``` --- [Source](https:/img.ly/docs/cesdk/android/animation/create/base-0fc5c4) # Base Animations ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.AnimationTypeimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeTypeimport 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")}") 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](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. ``` 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"` ``` 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. ``` engine.block.setInAnimation(block, slideInAnimation) ``` In order to assign a _Loop_ animation to the block, call the `fun setLoopAnimation(block: DesignBlock, animation: DesignBlock)` API. ``` engine.block.setLoopAnimation(block, breathingLoopAnimation) ``` In order to assign an _Out_ animation to the block, call the `fun setOutAnimation(block: DesignBlock, animation: DesignBlock)` API. ``` 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. ``` 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. ``` 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](android/animation/types-4e5f41/) for a complete list of all available properties for each type of animation. ``` 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. ``` 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. ``` 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. ``` engine.block.setEnum(slideInAnimation, "animationEasing", "EaseOut")println("Available easing options: ${engine.block.getEnumValues("animationEasing")}") ``` ## Full Code Here’s the full code: ``` import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport ly.img.engine.AnimationTypeimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.ShapeTypeimport 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()} ```