In this example, we will show you how to use the CreativeEditor 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<String>?
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<String>, 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<String>
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<String>
Subscribe to asset source addition events.
- Returns flow of asset source addition events.
fun onAssetSourceRemoved(): Flow<String>
Subscribe to asset source removal events.
- Returns flow of asset source removal events.
fun onAssetSourceUpdated(): Flow<String>
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 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 ofAssetSource
whichAssetSource.applyAsset
function is going to be invoked. -
asset
: the asset to be applied to the active scene. Normally it’s a single result of afindAssets
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 ofAssetSource
whichAssetSource.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 afindAssets
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 afindAssets
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 afindAssets
query. -
block
: the block that will be modified by the asset.
fun getSourceSupportedMimeTypes(sourceId: String): List<String>
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<String>?
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<String>? = 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 }}