Browse Source

[resources] Add functions to retrieve bytes from drawable or font resources. (#4651)

Implemented two new experimental functions:
```kotlin
/**
 * Retrieves the byte array of the drawable resource.
 *
 * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
 * @param resource The drawable resource.
 * @return The byte array representing the drawable resource.
 */
@ExperimentalResourceApi
suspend fun getDrawableResourceBytes(
    environment: ResourceEnvironment,
    resource: DrawableResource
): ByteArray {...}

/**
 * Retrieves the byte array of the font resource.
 *
 * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
 * @param resource The font resource.
 * @return The byte array representing the font resource.
 */
@ExperimentalResourceApi
suspend fun getFontResourceBytes(
    environment: ResourceEnvironment,
    resource: FontResource
): ByteArray {...}
```

fixes https://github.com/JetBrains/compose-multiplatform/issues/4360
pull/4695/head v1.6.10-beta02
Konstantin 7 months ago committed by GitHub
parent
commit
afe548b6d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      components/README.md
  2. 2
      components/gradle.properties
  3. 16
      components/resources/demo/shared/build.gradle.kts
  4. 53
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt
  5. 1
      components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt
  6. 17
      components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt
  7. 1
      components/resources/library/build.gradle.kts
  8. 17
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt
  9. 17
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt
  10. 45
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt
  11. 57
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt
  12. 17
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt
  13. 39
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  14. 24
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt
  15. 5
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt

4
components/README.md

@ -13,10 +13,6 @@ in Android Studio or in AppCode with [installed CocoaPods](https://kotlinlang.or
### Run JS in browser with WebAssembly Skia via Gradle: ### Run JS in browser with WebAssembly Skia via Gradle:
`./gradlew :resources:demo:shared:jsBrowserDevelopmentRun` `./gradlew :resources:demo:shared:jsBrowserDevelopmentRun`
### Run MacOS via Gradle:
- on Intel CPU: `./gradlew :resources:demo:shared:runDebugExecutableMacosX64`
- on Apple Silicon: `./gradlew :resources:demo:shared:runDebugExecutableMacosArm64`
# Tests # Tests
Run script: Run script:
```bash ```bash

2
components/gradle.properties

@ -8,7 +8,7 @@ android.useAndroidX=true
#Versions #Versions
kotlin.version=1.9.23 kotlin.version=1.9.23
compose.version=1.6.10-beta01 compose.version=1.6.10-dev1596
agp.version=8.2.2 agp.version=8.2.2
#Compose #Compose

16
components/resources/demo/shared/build.gradle.kts

@ -39,24 +39,8 @@ kotlin {
binaries.executable() binaries.executable()
} }
listOf(
macosX64(),
macosArm64()
).forEach { macosTarget ->
macosTarget.binaries {
executable {
entryPoint = "main"
}
}
}
applyDefaultHierarchyTemplate() applyDefaultHierarchyTemplate()
sourceSets { sourceSets {
all {
languageSettings {
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}
val desktopMain by getting val desktopMain by getting
val wasmJsMain by getting val wasmJsMain by getting

53
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt

@ -9,11 +9,16 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.droid_icon
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getDrawableResourceBytes
import org.jetbrains.compose.resources.rememberResourceEnvironment
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
fun FileRes(paddingValues: PaddingValues) { fun FileRes(paddingValues: PaddingValues) {
Column( Column(
modifier = Modifier.padding(paddingValues) modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState())
) { ) {
Text( Text(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
@ -48,6 +53,34 @@ fun FileRes(paddingValues: PaddingValues) {
Text(bytes.decodeToString()) Text(bytes.decodeToString())
""".trimIndent() """.trimIndent()
) )
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp))
OutlinedCard(
modifier = Modifier.padding(horizontal = 16.dp),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
val composeEnv = rememberResourceEnvironment()
var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) {
bytes = getDrawableResourceBytes(composeEnv, Res.drawable.droid_icon)
}
Text(
modifier = Modifier.padding(8.dp),
text = "droid_icon byte size = " + bytes.size,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text(
modifier = Modifier.padding(16.dp),
text = """
val composeEnv = rememberResourceEnvironment()
var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) {
bytes = getDrawableResourceBytes(composeEnv, Res.drawable.droid_icon)
}
Text("droid_icon byte size = " + bytes.size)
""".trimIndent()
)
Text( Text(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
text = "File: 'files/platform-text.txt'", text = "File: 'files/platform-text.txt'",
@ -80,5 +113,23 @@ fun FileRes(paddingValues: PaddingValues) {
Text(bytes.decodeToString()) Text(bytes.decodeToString())
""".trimIndent() """.trimIndent()
) )
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp))
OutlinedCard(
modifier = Modifier.padding(horizontal = 16.dp),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Text(
modifier = Modifier.padding(8.dp),
text = "File URI: " + Res.getUri("files/platform-text.txt"),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text(
modifier = Modifier.padding(16.dp),
text = """
Text("File URI: " + Res.getUri("files/platform-text.txt"))
""".trimIndent()
)
} }
} }

1
components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt

@ -1 +0,0 @@
macOS platform

17
components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt

@ -1,17 +0,0 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import androidx.compose.ui.window.Window
import org.jetbrains.compose.resources.demo.shared.UseResources
import platform.AppKit.NSApp
import platform.AppKit.NSApplication
fun main() {
NSApplication.sharedApplication()
Window("Resources demo") {
UseResources()
}
NSApp?.run()
}

1
components/resources/library/build.gradle.kts

@ -53,6 +53,7 @@ kotlin {
optIn("kotlinx.cinterop.ExperimentalForeignApi") optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("kotlin.experimental.ExperimentalNativeApi") optIn("kotlin.experimental.ExperimentalNativeApi")
optIn("org.jetbrains.compose.resources.InternalResourceApi") optIn("org.jetbrains.compose.resources.InternalResourceApi")
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
} }
} }

17
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt

@ -2,6 +2,7 @@ package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
/** /**
@ -33,3 +34,19 @@ expect fun Font(
weight: FontWeight = FontWeight.Normal, weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal style: FontStyle = FontStyle.Normal
): Font ): Font
/**
* Retrieves the byte array of the font resource.
*
* @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
* @param resource The font resource.
* @return The byte array representing the font resource.
*/
@ExperimentalResourceApi
suspend fun getFontResourceBytes(
environment: ResourceEnvironment,
resource: FontResource
): ByteArray {
val resourceItem = resource.getResourceItemByEnvironment(environment)
return DefaultResourceReader.read(resourceItem.path)
}

17
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt

@ -95,7 +95,6 @@ internal expect fun SvgElement.toSvgPainter(density: Density): Painter
private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) } private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) }
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
private fun svgPainter(resource: DrawableResource): Painter { private fun svgPainter(resource: DrawableResource): Painter {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
@ -110,6 +109,22 @@ private fun svgPainter(resource: DrawableResource): Painter {
return svgPainter return svgPainter
} }
/**
* Retrieves the byte array of the drawable resource.
*
* @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
* @param resource The drawable resource.
* @return The byte array representing the drawable resource.
*/
@ExperimentalResourceApi
suspend fun getDrawableResourceBytes(
environment: ResourceEnvironment,
resource: DrawableResource
): ByteArray {
val resourceItem = resource.getResourceItemByEnvironment(environment)
return DefaultResourceReader.read(resourceItem.path)
}
internal expect fun ByteArray.toImageBitmap(): ImageBitmap internal expect fun ByteArray.toImageBitmap(): ImageBitmap
internal expect fun ByteArray.toXmlElement(): Element internal expect fun ByteArray.toXmlElement(): Element
internal expect fun ByteArray.toSvgElement(): SvgElement internal expect fun ByteArray.toSvgElement(): SvgElement

45
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt

@ -43,7 +43,24 @@ fun pluralStringResource(resource: PluralStringResource, quantity: Int): String
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/ */
suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String = suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String =
loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment()) loadPluralString(resource, quantity, DefaultResourceReader, getSystemResourceEnvironment())
/**
* Loads a string using the specified string resource.
*
* @param environment The resource environment.
* @param resource The string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getPluralString(
environment: ResourceEnvironment,
resource: PluralStringResource,
quantity: Int
): String = loadPluralString(resource, quantity, DefaultResourceReader, environment)
private suspend fun loadPluralString( private suspend fun loadPluralString(
resource: PluralStringResource, resource: PluralStringResource,
@ -99,7 +116,31 @@ suspend fun getPluralString(resource: PluralStringResource, quantity: Int, varar
resource, quantity, resource, quantity,
formatArgs.map { it.toString() }, formatArgs.map { it.toString() },
DefaultResourceReader, DefaultResourceReader,
getResourceEnvironment(), getSystemResourceEnvironment(),
)
/**
* Loads a string using the specified string resource.
*
* @param environment The resource environment.
* @param resource The string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @param formatArgs The arguments to be inserted into the formatted string.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getPluralString(
environment: ResourceEnvironment,
resource: PluralStringResource,
quantity: Int,
vararg formatArgs: Any
): String = loadPluralString(
resource, quantity,
formatArgs.map { it.toString() },
DefaultResourceReader,
environment
) )
private suspend fun loadPluralString( private suspend fun loadPluralString(

57
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt

@ -5,12 +5,35 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
internal data class ResourceEnvironment( @ExperimentalResourceApi
val language: LanguageQualifier, class ResourceEnvironment internal constructor(
val region: RegionQualifier, internal val language: LanguageQualifier,
val theme: ThemeQualifier, internal val region: RegionQualifier,
val density: DensityQualifier internal val theme: ThemeQualifier,
) internal val density: DensityQualifier
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ResourceEnvironment
if (language != other.language) return false
if (region != other.region) return false
if (theme != other.theme) return false
if (density != other.density) return false
return true
}
override fun hashCode(): Int {
var result = language.hashCode()
result = 31 * result + region.hashCode()
result = 31 * result + theme.hashCode()
result = 31 * result + density.hashCode()
return result
}
}
internal interface ComposeEnvironment { internal interface ComposeEnvironment {
@Composable @Composable
@ -39,14 +62,32 @@ internal val DefaultComposeEnvironment = object : ComposeEnvironment {
//ComposeEnvironment provider will be overridden for tests //ComposeEnvironment provider will be overridden for tests
internal val LocalComposeEnvironment = staticCompositionLocalOf { DefaultComposeEnvironment } internal val LocalComposeEnvironment = staticCompositionLocalOf { DefaultComposeEnvironment }
/**
* Returns an instance of [ResourceEnvironment].
*
* The [ResourceEnvironment] class represents the environment for resources.
*
* @return An instance of [ResourceEnvironment] representing the current environment.
*/
@ExperimentalResourceApi
@Composable
fun rememberResourceEnvironment(): ResourceEnvironment {
val composeEnvironment = LocalComposeEnvironment.current
return composeEnvironment.rememberEnvironment()
}
internal expect fun getSystemEnvironment(): ResourceEnvironment internal expect fun getSystemEnvironment(): ResourceEnvironment
//the function reference will be overridden for tests //the function reference will be overridden for tests
//@TestOnly
internal var getResourceEnvironment = ::getSystemEnvironment
/** /**
* Provides the resource environment for non-composable access to string resources. * Provides the resource environment for non-composable access to resources.
* It is an expensive operation! Don't use it in composable functions with no cache! * It is an expensive operation! Don't use it in composable functions with no cache!
*/ */
internal var getResourceEnvironment = ::getSystemEnvironment @ExperimentalResourceApi
fun getSystemResourceEnvironment(): ResourceEnvironment = getResourceEnvironment()
@OptIn(InternalResourceApi::class) @OptIn(InternalResourceApi::class)
internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironment): ResourceItem { internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironment): ResourceItem {

17
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt

@ -46,7 +46,22 @@ fun stringArrayResource(resource: StringArrayResource): List<String> {
* @throws IllegalStateException if the string array with the given ID is not found. * @throws IllegalStateException if the string array with the given ID is not found.
*/ */
suspend fun getStringArray(resource: StringArrayResource): List<String> = suspend fun getStringArray(resource: StringArrayResource): List<String> =
loadStringArray(resource, DefaultResourceReader, getResourceEnvironment()) loadStringArray(resource, DefaultResourceReader, getSystemResourceEnvironment())
/**
* Loads a list of strings using the specified string array resource.
*
* @param environment The resource environment.
* @param resource The string array resource to be used.
* @return A list of strings representing the items in the string array.
*
* @throws IllegalStateException if the string array with the given ID is not found.
*/
@ExperimentalResourceApi
suspend fun getStringArray(
environment: ResourceEnvironment,
resource: StringArrayResource
): List<String> = loadStringArray(resource, DefaultResourceReader, environment)
private suspend fun loadStringArray( private suspend fun loadStringArray(
resource: StringArrayResource, resource: StringArrayResource,

39
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt

@ -39,7 +39,20 @@ fun stringResource(resource: StringResource): String {
* @throws IllegalArgumentException If the provided ID is not found in the resource file. * @throws IllegalArgumentException If the provided ID is not found in the resource file.
*/ */
suspend fun getString(resource: StringResource): String = suspend fun getString(resource: StringResource): String =
loadString(resource, DefaultResourceReader, getResourceEnvironment()) loadString(resource, DefaultResourceReader, getSystemResourceEnvironment())
/**
* Loads a string using the specified string resource.
*
* @param environment The resource environment.
* @param resource The string resource to be used.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getString(environment: ResourceEnvironment, resource: StringResource): String =
loadString(resource, DefaultResourceReader, environment)
private suspend fun loadString( private suspend fun loadString(
resource: StringResource, resource: StringResource,
@ -83,7 +96,29 @@ suspend fun getString(resource: StringResource, vararg formatArgs: Any): String
resource, resource,
formatArgs.map { it.toString() }, formatArgs.map { it.toString() },
DefaultResourceReader, DefaultResourceReader,
getResourceEnvironment() getSystemResourceEnvironment()
)
/**
* Loads a formatted string using the specified string resource and arguments.
*
* @param environment The resource environment.
* @param resource The string resource to be used.
* @param formatArgs The arguments to be inserted into the formatted string.
* @return The formatted string resource.
*
* @throws IllegalArgumentException If the provided ID is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getString(
environment: ResourceEnvironment,
resource: StringResource,
vararg formatArgs: Any
): String = loadString(
resource,
formatArgs.map { it.toString() },
DefaultResourceReader,
environment
) )
private suspend fun loadString( private suspend fun loadString(

24
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt

@ -6,7 +6,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.jetbrains.skiko.URIManager
import kotlin.test.* import kotlin.test.*
@OptIn(ExperimentalTestApi::class, InternalResourceApi::class) @OptIn(ExperimentalTestApi::class, InternalResourceApi::class)
@ -304,4 +303,27 @@ class ComposeResourceTest {
assertTrue(uri1.endsWith("/1.png")) assertTrue(uri1.endsWith("/1.png"))
assertTrue(uri2.endsWith("/2.png")) assertTrue(uri2.endsWith("/2.png"))
} }
@OptIn(ExperimentalResourceApi::class)
@Test
fun testGetResourceBytes() = runTest {
val env = getSystemEnvironment()
val imageBytes = getDrawableResourceBytes(env, TestDrawableResource("1.png"))
assertEquals(946, imageBytes.size)
val fontBytes = getFontResourceBytes(env, TestFontResource("font_awesome.otf"))
assertEquals(134808, fontBytes.size)
}
@OptIn(ExperimentalResourceApi::class)
@Test
fun testGetResourceEnvironment() = runComposeUiTest {
var environment: ResourceEnvironment? = null
setContent {
environment = rememberResourceEnvironment()
}
waitForIdle()
val systemEnvironment = getSystemEnvironment()
assertEquals(systemEnvironment, environment)
}
} }

5
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt

@ -40,6 +40,11 @@ internal fun TestDrawableResource(path: String) = DrawableResource(
setOf(ResourceItem(emptySet(), path, -1, -1)) setOf(ResourceItem(emptySet(), path, -1, -1))
) )
internal fun TestFontResource(path: String) = FontResource(
path,
setOf(ResourceItem(emptySet(), path, -1, -1))
)
internal fun parsePluralSamples(samples: String): List<Int> { internal fun parsePluralSamples(samples: String): List<Int> {
return samples.split(',').flatMap { return samples.split(',').flatMap {
val range = it.trim() val range = it.trim()

Loading…
Cancel
Save