package org.jetbrains.compose.resources import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.jetbrains.compose.resources.vector.toImageVector import org.jetbrains.compose.resources.vector.xmldom.Element /** * Represents an image resource. * * @param id The unique identifier of the image resource. * @param items The set of resource items associated with the image resource. */ @Immutable class ImageResource(id: String, items: Set) : Resource(id, items) /** * Creates an [ImageResource] object with the specified path. * * @param path The path of the image resource. * @return An [ImageResource] object. */ fun ImageResource(path: String): ImageResource = ImageResource( id = "ImageResource:$path", items = setOf(ResourceItem(emptySet(), path)) ) /** * Retrieves a [Painter] using the specified image resource. * Automatically select a type of the Painter depending on the file extension. * * @param resource The image resource to be used. * @return The [Painter] loaded from the resource. */ @ExperimentalResourceApi @Composable fun painterResource(resource: ImageResource): Painter { val filePath = remember(resource) { resource.getPathByEnvironment() } val isXml = filePath.endsWith(".xml", true) if (isXml) { return rememberVectorPainter(vectorResource(resource)) } else { return BitmapPainter(imageResource(resource)) } } private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } /** * Retrieves an ImageBitmap using the specified image resource. * * @param resource The image resource to be used. * @return The ImageBitmap loaded from the resource. */ @ExperimentalResourceApi @Composable fun imageResource(resource: ImageResource): ImageBitmap { val resourceReader = LocalResourceReader.current val imageBitmap by rememberState(resource, { emptyImageBitmap }) { val path = resource.getPathByEnvironment() val cached = loadImage(path, resourceReader) { ImageCache.Bitmap(it.toImageBitmap()) } as ImageCache.Bitmap cached.bitmap } return imageBitmap } private val emptyImageVector: ImageVector by lazy { ImageVector.Builder("emptyImageVector", 1.dp, 1.dp, 1f, 1f).build() } /** * Retrieves an ImageVector using the specified image resource. * * @param resource The image resource to be used. * @return The ImageVector loaded from the resource. */ @ExperimentalResourceApi @Composable fun vectorResource(resource: ImageResource): ImageVector { val resourceReader = LocalResourceReader.current val density = LocalDensity.current val imageVector by rememberState(resource, { emptyImageVector }) { val path = resource.getPathByEnvironment() val cached = loadImage(path, resourceReader) { ImageCache.Vector(it.toXmlElement().toImageVector(density)) } as ImageCache.Vector cached.vector } return imageVector } internal expect fun ByteArray.toImageBitmap(): ImageBitmap internal expect fun ByteArray.toXmlElement(): Element private sealed interface ImageCache { class Bitmap(val bitmap: ImageBitmap) : ImageCache class Vector(val vector: ImageVector) : ImageCache } private val imageCacheMutex = Mutex() private val imageCache = mutableMapOf>() //@TestOnly internal fun dropImageCache() { imageCache.clear() } private suspend fun loadImage( path: String, resourceReader: ResourceReader, decode: (ByteArray) -> ImageCache ): ImageCache = coroutineScope { val deferred = imageCacheMutex.withLock { imageCache.getOrPut(path) { //LAZY - to free the mutex lock as fast as possible async(start = CoroutineStart.LAZY) { decode(resourceReader.read(path)) } } } deferred.await() }