Browse Source

Support for XML Vector Drawables.

pull/2705/merge
Nikita Lipsky 2 years ago
parent
commit
6060e86cbd
  1. 7
      components/resources/demo/androidApp/build.gradle.kts
  2. 6
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt
  3. 34
      components/resources/demo/shared/src/commonMain/resources/vector.xml
  4. 8
      components/resources/library/build.gradle.kts
  5. 56
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt
  6. 104
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/ValueParsers.kt
  7. 276
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt
  8. 11
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Element.kt
  9. 14
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt
  10. 10
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeList.kt
  11. 5
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt
  12. 114
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/xmldom/DomXmlParser.kt
  13. 5
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt
  14. 22
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt
  15. 10
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt
  16. 28
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt
  17. 5
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt

7
components/resources/demo/androidApp/build.gradle.kts

@ -1,6 +1,7 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
id("org.jetbrains.compose")
} }
dependencies { dependencies {
@ -12,12 +13,6 @@ dependencies {
} }
android { android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
compileSdk = 33 compileSdk = 33
defaultConfig { defaultConfig {
applicationId = "me.user.androidApp" applicationId = "me.user.androidApp"

6
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt

@ -2,8 +2,10 @@ package org.jetbrains.compose.resources.demo.shared
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import org.jetbrains.compose.resources.* import org.jetbrains.compose.resources.*
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
@ -19,5 +21,9 @@ internal fun UseResources() {
bitmap = resource("img.webp").rememberImageBitmap().orEmpty(), bitmap = resource("img.webp").rememberImageBitmap().orEmpty(),
contentDescription = null, contentDescription = null,
) )
Icon(
imageVector = resource("vector.xml").rememberImageVector(LocalDensity.current).orEmpty(),
contentDescription = null
)
} }
} }

34
components/resources/demo/shared/src/commonMain/resources/vector.xml

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

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

@ -38,8 +38,12 @@ kotlin {
val skikoMain by creating { val skikoMain by creating {
dependsOn(commonMain) dependsOn(commonMain)
} }
val jvmAndAndroidMain by creating {
dependsOn(commonMain)
}
val desktopMain by getting { val desktopMain by getting {
dependsOn(skikoMain) dependsOn(skikoMain)
dependsOn(jvmAndAndroidMain)
} }
val desktopTest by getting { val desktopTest by getting {
dependencies { dependencies {
@ -48,7 +52,9 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
} }
} }
val androidMain by getting {} val androidMain by getting {
dependsOn(jvmAndAndroidMain)
}
val androidTest by getting { val androidTest by getting {
dependencies { dependencies {

56
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt

@ -7,19 +7,25 @@ package org.jetbrains.compose.resources
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Density
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.parseVectorRoot
import androidx.compose.ui.unit.dp
private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) }
/** private val emptyImageVector: ImageVector by lazy {
* Get and remember resource. While loading and if resource not exists result will be null. ImageVector.Builder(defaultWidth = 1.dp, defaultHeight = 1.dp, viewportWidth = 1f, viewportHeight = 1f).build()
*/ }
@ExperimentalResourceApi
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
fun Resource.rememberImageBitmap(): LoadState<ImageBitmap> { private fun <T> Resource.rememberLoadingResource(fromByteArrayConverter: ByteArray.()->T): LoadState<T> {
val state: MutableState<LoadState<ImageBitmap>> = remember(this) { mutableStateOf(LoadState.Loading()) } val state: MutableState<LoadState<T>> = remember(this) { mutableStateOf(LoadState.Loading()) }
LaunchedEffect(this) { LaunchedEffect(this) {
state.value = try { state.value = try {
LoadState.Success(readBytes().toImageBitmap()) LoadState.Success(readBytes().fromByteArrayConverter())
} catch (e: Exception) { } catch (e: Exception) {
LoadState.Error(e) LoadState.Error(e)
} }
@ -28,13 +34,41 @@ fun Resource.rememberImageBitmap(): LoadState<ImageBitmap> {
} }
/** /**
* return current ImageBitmap or return empty while loading * Get and remember resource. While loading and if resource not exists result will be null.
*/
@ExperimentalResourceApi
@Composable
fun Resource.rememberImageBitmap(): LoadState<ImageBitmap> =
rememberLoadingResource { toImageBitmap() }
/**
* Get and remember resource. While loading and if resource not exists result will be null.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
fun LoadState<ImageBitmap>.orEmpty(): ImageBitmap = when (this) { @Composable
is LoadState.Loading -> emptyImageBitmap fun Resource.rememberImageVector(density: Density): LoadState<ImageVector> =
rememberLoadingResource { toImageVector(density) }
private fun <T> LoadState<T>.orEmpty(emptyValue: T): T = when (this) {
is LoadState.Loading -> emptyValue
is LoadState.Success -> this.value is LoadState.Success -> this.value
is LoadState.Error -> emptyImageBitmap is LoadState.Error -> emptyValue
} }
/**
* return current ImageBitmap or return empty while loading
*/
@ExperimentalResourceApi
fun LoadState<ImageBitmap>.orEmpty(): ImageBitmap = orEmpty(emptyImageBitmap)
/**
* return current ImageVector or return empty while loading
*/
@ExperimentalResourceApi
fun LoadState<ImageVector>.orEmpty(): ImageVector = orEmpty(emptyImageVector)
internal expect fun ByteArray.toImageBitmap(): ImageBitmap internal expect fun ByteArray.toImageBitmap(): ImageBitmap
internal expect fun parseXML(byteArray: ByteArray): Element
internal fun ByteArray.toImageVector(density: Density): ImageVector = parseXML(this).parseVectorRoot(density)

104
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/ValueParsers.kt

@ -0,0 +1,104 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.compose.resources.vector
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
private const val ALPHA_MASK = 0xFF000000.toInt()
// parseColorValue is copied from Android:
// https://cs.android.com/android-studio/platform/tools/base/+/05fadd8cb2aaafb77da02048c7a240b2147ff293:sdk-common/src/main/java/com/android/ide/common/vectordrawable/VdUtil.kt;l=58
/**
* Parses a color value in #AARRGGBB format.
*
* @param color the color value string
* @return the integer color value
*/
internal fun parseColorValue(color: String): Int {
require(color.startsWith("#")) { "Invalid color value $color" }
return when (color.length) {
7 -> {
// #RRGGBB
color.substring(1).toUInt(16).toInt() or ALPHA_MASK
}
9 -> {
// #AARRGGBB
color.substring(1).toUInt(16).toInt()
}
4 -> {
// #RGB
val v = color.substring(1).toUInt(16).toInt()
var k = (v shr 8 and 0xF) * 0x110000
k = k or (v shr 4 and 0xF) * 0x1100
k = k or (v and 0xF) * 0x11
k or ALPHA_MASK
}
5 -> {
// #ARGB
val v = color.substring(1).toUInt(16).toInt()
var k = (v shr 12 and 0xF) * 0x11000000
k = k or (v shr 8 and 0xF) * 0x110000
k = k or (v shr 4 and 0xF) * 0x1100
k = k or (v and 0xF) * 0x11
k or ALPHA_MASK
}
else -> ALPHA_MASK
}
}
internal fun parseFillType(fillType: String): PathFillType = when (fillType) {
"nonZero" -> PathFillType.NonZero
"evenOdd" -> PathFillType.EvenOdd
else -> throw UnsupportedOperationException("unknown fillType: $fillType")
}
internal fun parseStrokeCap(strokeCap: String): StrokeCap = when (strokeCap) {
"butt" -> StrokeCap.Butt
"round" -> StrokeCap.Round
"square" -> StrokeCap.Square
else -> throw UnsupportedOperationException("unknown strokeCap: $strokeCap")
}
internal fun parseStrokeJoin(strokeJoin: String): StrokeJoin = when (strokeJoin) {
"miter" -> StrokeJoin.Miter
"round" -> StrokeJoin.Round
"bevel" -> StrokeJoin.Bevel
else -> throw UnsupportedOperationException("unknown strokeJoin: $strokeJoin")
}
internal fun parseTileMode(tileMode: String): TileMode = when (tileMode) {
"clamp" -> TileMode.Clamp
"repeated" -> TileMode.Repeated
"mirror" -> TileMode.Mirror
else -> throw throw UnsupportedOperationException("unknown tileMode: $tileMode")
}
internal fun String?.parseDp(density: Density): Dp = with(density) {
return when {
this@parseDp == null -> 0f.dp
endsWith("dp") -> removeSuffix("dp").toFloat().dp
endsWith("px") -> removeSuffix("px").toFloat().toDp()
else -> throw UnsupportedOperationException("value should ends with dp or px")
}
}

276
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt

@ -0,0 +1,276 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.compose.resources.vector
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.vector.DefaultPivotX
import androidx.compose.ui.graphics.vector.DefaultPivotY
import androidx.compose.ui.graphics.vector.DefaultRotation
import androidx.compose.ui.graphics.vector.DefaultScaleX
import androidx.compose.ui.graphics.vector.DefaultScaleY
import androidx.compose.ui.graphics.vector.DefaultTranslationX
import androidx.compose.ui.graphics.vector.DefaultTranslationY
import androidx.compose.ui.graphics.vector.EmptyPath
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.addPathNodes
import org.jetbrains.compose.resources.vector.BuildContext.Group
import androidx.compose.ui.unit.Density
import org.jetbrains.compose.resources.vector.xmldom.*
// Parsing logic is the same as in Android implementation
// (compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.kt)
//
// Except there is no support for linking with external resources
// (for example, we can't reference to color defined in another file)
//
// Specification:
// https://developer.android.com/reference/android/graphics/drawable/VectorDrawable
private const val ANDROID_NS = "http://schemas.android.com/apk/res/android"
private const val AAPT_NS = "http://schemas.android.com/aapt"
private class BuildContext {
val currentGroups = mutableListOf<Group>()
enum class Group {
/**
* Group that exists in xml file
*/
Real,
/**
* Group that doesn't exist in xml file. We add it manually when we see <clip-path> node.
* It will be automatically popped when the real group will be popped.
*/
Virtual
}
}
fun Element.parseVectorRoot(density: Density): ImageVector {
val context = BuildContext()
val builder = ImageVector.Builder(
defaultWidth = attributeOrNull(ANDROID_NS, "width").parseDp(density),
defaultHeight = attributeOrNull(ANDROID_NS, "height").parseDp(density),
viewportWidth = attributeOrNull(ANDROID_NS, "viewportWidth")?.toFloat() ?: 0f,
viewportHeight = attributeOrNull(ANDROID_NS, "viewportHeight")?.toFloat() ?: 0f
)
parseVectorNodes(builder, context)
return builder.build()
}
private fun Element.parseVectorNodes(builder: ImageVector.Builder, context: BuildContext) {
childrenSequence
.filterIsInstance<Element>()
.forEach {
it.parseVectorNode(builder, context)
}
}
private fun Element.parseVectorNode(builder: ImageVector.Builder, context: BuildContext) {
when (nodeName) {
"path" -> parsePath(builder)
"clip-path" -> parseClipPath(builder, context)
"group" -> parseGroup(builder, context)
}
}
private fun Element.parsePath(builder: ImageVector.Builder) {
builder.addPath(
pathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData")),
pathFillType = attributeOrNull(ANDROID_NS, "fillType")
?.let(::parseFillType) ?: PathFillType.NonZero,
name = attributeOrNull(ANDROID_NS, "name") ?: "",
fill = attributeOrNull(ANDROID_NS, "fillColor")?.let(::parseStringBrush)
?: apptAttr(ANDROID_NS, "fillColor")?.let(Element::parseElementBrush),
fillAlpha = attributeOrNull(ANDROID_NS, "fillAlpha")?.toFloat() ?: 1.0f,
stroke = attributeOrNull(ANDROID_NS, "strokeColor")?.let(::parseStringBrush)
?: apptAttr(ANDROID_NS, "strokeColor")?.let(Element::parseElementBrush),
strokeAlpha = attributeOrNull(ANDROID_NS, "strokeAlpha")?.toFloat() ?: 1.0f,
strokeLineWidth = attributeOrNull(ANDROID_NS, "strokeWidth")?.toFloat() ?: 1.0f,
strokeLineCap = attributeOrNull(ANDROID_NS, "strokeLineCap")
?.let(::parseStrokeCap) ?: StrokeCap.Butt,
strokeLineJoin = attributeOrNull(ANDROID_NS, "strokeLineJoin")
?.let(::parseStrokeJoin) ?: StrokeJoin.Miter,
strokeLineMiter = attributeOrNull(ANDROID_NS, "strokeMiterLimit")?.toFloat() ?: 1.0f,
trimPathStart = attributeOrNull(ANDROID_NS, "trimPathStart")?.toFloat() ?: 0.0f,
trimPathEnd = attributeOrNull(ANDROID_NS, "trimPathEnd")?.toFloat() ?: 1.0f,
trimPathOffset = attributeOrNull(ANDROID_NS, "trimPathOffset")?.toFloat() ?: 0.0f
)
}
private fun Element.parseClipPath(builder: ImageVector.Builder, context: BuildContext) {
builder.addGroup(
name = attributeOrNull(ANDROID_NS, "name") ?: "",
clipPathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData"))
)
context.currentGroups.add(Group.Virtual)
}
private fun Element.parseGroup(builder: ImageVector.Builder, context: BuildContext) {
builder.addGroup(
attributeOrNull(ANDROID_NS, "name") ?: "",
attributeOrNull(ANDROID_NS, "rotation")?.toFloat() ?: DefaultRotation,
attributeOrNull(ANDROID_NS, "pivotX")?.toFloat() ?: DefaultPivotX,
attributeOrNull(ANDROID_NS, "pivotY")?.toFloat() ?: DefaultPivotY,
attributeOrNull(ANDROID_NS, "scaleX")?.toFloat() ?: DefaultScaleX,
attributeOrNull(ANDROID_NS, "scaleY")?.toFloat() ?: DefaultScaleY,
attributeOrNull(ANDROID_NS, "translateX")?.toFloat() ?: DefaultTranslationX,
attributeOrNull(ANDROID_NS, "translateY")?.toFloat() ?: DefaultTranslationY,
EmptyPath
)
context.currentGroups.add(Group.Real)
parseVectorNodes(builder, context)
do {
val removedGroup = context.currentGroups.removeLastOrNull()
builder.clearGroup()
} while (removedGroup == Group.Virtual)
}
private fun parseStringBrush(str: String) = SolidColor(Color(parseColorValue(str)))
private fun Element.parseElementBrush(): Brush? =
childrenSequence
.filterIsInstance<Element>()
.find { it.nodeName == "gradient" }
?.parseGradient()
private fun Element.parseGradient(): Brush? {
return when (attributeOrNull(ANDROID_NS, "type")) {
"linear" -> parseLinearGradient()
"radial" -> parseRadialGradient()
"sweep" -> parseSweepGradient()
else -> null
}
}
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
private fun Element.parseLinearGradient() = Brush.linearGradient(
colorStops = parseColorStops(),
start = Offset(
attributeOrNull(ANDROID_NS, "startX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "startY")?.toFloat() ?: 0f
),
end = Offset(
attributeOrNull(ANDROID_NS, "endX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "endY")?.toFloat() ?: 0f
),
tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp
)
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
private fun Element.parseRadialGradient() = Brush.radialGradient(
colorStops = parseColorStops(),
center = Offset(
attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f
),
radius = attributeOrNull(ANDROID_NS, "gradientRadius")?.toFloat() ?: 0f,
tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp
)
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
private fun Element.parseSweepGradient() = Brush.sweepGradient(
colorStops = parseColorStops(),
center = Offset(
attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f,
)
)
private fun Element.parseColorStops(): Array<Pair<Float, Color>> {
val items = childrenSequence
.filterIsInstance<Element>()
.filter { it.nodeName == "item" }
.toList()
val colorStops = items.mapIndexedNotNullTo(mutableListOf()) { index, item ->
item.parseColorStop(defaultOffset = index.toFloat() / items.lastIndex.coerceAtLeast(1))
}
if (colorStops.isEmpty()) {
val startColor = attributeOrNull(ANDROID_NS, "startColor")?.let(::parseColorValue)
val centerColor = attributeOrNull(ANDROID_NS, "centerColor")?.let(::parseColorValue)
val endColor = attributeOrNull(ANDROID_NS, "endColor")?.let(::parseColorValue)
if (startColor != null) {
colorStops.add(0f to Color(startColor))
}
if (centerColor != null) {
colorStops.add(0.5f to Color(centerColor))
}
if (endColor != null) {
colorStops.add(1f to Color(endColor))
}
}
return colorStops.toTypedArray()
}
private fun Element.parseColorStop(defaultOffset: Float): Pair<Float, Color>? {
val offset = attributeOrNull(ANDROID_NS, "offset")?.toFloat() ?: defaultOffset
val color = attributeOrNull(ANDROID_NS, "color")?.let(::parseColorValue) ?: return null
return offset to Color(color)
}
private fun Element.attributeOrNull(namespace: String, name: String): String? {
val value = getAttributeNS(namespace, name)
return if (value.isNotBlank()) value else null
}
/**
* Attribute of an element can be represented as a separate child:
*
* <path ...>
* <aapt:attr name="android:fillColor">
* <gradient ...
* ...
* </gradient>
* </aapt:attr>
* </path>
*
* instead of:
*
* <path android:fillColor="red" ... />
*/
private fun Element.apptAttr(
namespace: String,
name: String
): Element? {
val prefix = lookupPrefix(namespace)
return childrenSequence
.filterIsInstance<Element>()
.find {
it.namespaceURI == AAPT_NS && it.localName == "attr" &&
it.getAttribute("name") == "$prefix:$name"
}
}
private val Element.childrenSequence get() = sequence<Node> {
for (i in 0 until childNodes.length) {
yield(childNodes.item(i))
}
}

11
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Element.kt

@ -0,0 +1,11 @@
package org.jetbrains.compose.resources.vector.xmldom
/**
* XML DOM Element.
*/
interface Element: Node {
fun getAttributeNS(nameSpaceURI: String, localName: String): String
fun getAttribute(name: String): String
}

14
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt

@ -0,0 +1,14 @@
package org.jetbrains.compose.resources.vector.xmldom
/**
* XML DOM Node.
*/
interface Node {
val nodeName: String
val localName: String
val childNodes: NodeList
val namespaceURI: String
fun lookupPrefix(namespaceURI: String): String
}

10
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeList.kt

@ -0,0 +1,10 @@
package org.jetbrains.compose.resources.vector.xmldom
/**
* XML DOM Node list.
*/
interface NodeList {
fun item(i: Int): Node
val length: Int
}

5
components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt

@ -5,9 +5,10 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.vector.xmldom.Element
import kotlinx.cinterop.addressOf import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned import kotlinx.cinterop.usePinned
import org.jetbrains.compose.resources.vector.xmldom.parse
import platform.Foundation.NSBundle import platform.Foundation.NSBundle
import platform.Foundation.NSData import platform.Foundation.NSData
import platform.Foundation.NSFileManager import platform.Foundation.NSFileManager
@ -35,3 +36,5 @@ private class UIKitResourceImpl(path: String) : AbstractResourceImpl(path) {
internal actual class MissingResourceException actual constructor(path: String) : internal actual class MissingResourceException actual constructor(path: String) :
Exception("Missing resource with path: $path") Exception("Missing resource with path: $path")
internal actual fun parseXML(byteArray: ByteArray): Element = parse(byteArray.decodeToString())

114
components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/xmldom/DomXmlParser.kt

@ -0,0 +1,114 @@
/*
* Copyright 2020-2021 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.
*/
package org.jetbrains.compose.resources.vector.xmldom
import platform.Foundation.*
import platform.darwin.NSObject
fun parse(xml: String): Element {
val parser = DomXmlParser()
NSXMLParser((xml as NSString).dataUsingEncoding(NSUTF8StringEncoding)!!).apply {
shouldReportNamespacePrefixes = true
shouldProcessNamespaces = true
delegate = parser
}.parse()
return parser.root!!
}
class MalformedXMLException(message: String?) : Exception(message)
private class ElementImpl(override val localName: String,
override val nodeName: String,
override val namespaceURI: String,
val prefixMap: Map<String, String>,
val attributes: Map<Any?, *>): Element {
override val childNodes: NodeList
get() = object : NodeList {
override fun item(i: Int): Node {
return childs[i]
}
override val length: Int
get() = childs.size
}
var childs = mutableListOf<Node>()
override fun getAttributeNS(nameSpaceURI: String, localName: String): String {
val prefix = prefixMap[nameSpaceURI]
val attrKey = if (prefix == null) localName else "$prefix:$localName"
return getAttribute(attrKey)
}
override fun getAttribute(name: String): String = attributes[name] as String? ?:""
override fun lookupPrefix(uri: String): String = prefixMap[uri]?:""
}
@Suppress("CONFLICTING_OVERLOADS")
private class DomXmlParser(
) : NSObject(), NSXMLParserDelegateProtocol {
val curPrefixMapInverted = mutableMapOf<String, String>()
var curPrefixMap: Map<String, String> = emptyMap()
val nodeStack = mutableListOf<ElementImpl>()
var root: Element? = null
override fun parser(
parser: NSXMLParser,
didStartElement: String,
namespaceURI: String?,
qualifiedName: String?,
attributes: Map<Any?, *>
) {
val node = ElementImpl(didStartElement, qualifiedName!!, namespaceURI?:"",
curPrefixMap, attributes)
if (root == null) root = node
if (!nodeStack.isEmpty())
nodeStack.last().childs.add(node)
nodeStack.add(node)
}
override fun parser(
parser: NSXMLParser,
didEndElement: String,
namespaceURI: String?,
qualifiedName: String?
) {
val node = nodeStack.removeLast()
assert(node.localName.equals(didEndElement))
}
override fun parser(parser: NSXMLParser,
didStartMappingPrefix: String,
toURI: String
) {
curPrefixMapInverted.put(didStartMappingPrefix, toURI)
curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key })
}
override fun parser(parser: NSXMLParser, didEndMappingPrefix: String) {
curPrefixMapInverted.remove(didEndMappingPrefix)
curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key })
}
override fun parser(parser: NSXMLParser, validationErrorOccurred: NSError) {
throw MalformedXMLException("validation error occurred")
}
override fun parser(parser: NSXMLParser, parseErrorOccurred: NSError) {
throw MalformedXMLException("parse error occurred")
}
}

5
components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt

@ -5,6 +5,7 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Int8Array import org.khronos.webgl.Int8Array
import org.w3c.xhr.ARRAYBUFFER import org.w3c.xhr.ARRAYBUFFER
@ -42,3 +43,7 @@ private fun ArrayBuffer.toByteArray() = Int8Array(this, 0, byteLength).unsafeCas
internal actual class MissingResourceException actual constructor(path: String) : internal actual class MissingResourceException actual constructor(path: String) :
Exception("Missing resource with path: $path") Exception("Missing resource with path: $path")
internal actual fun parseXML(byteArray: ByteArray): Element {
throw UnsupportedOperationException("XML Vector Drawables are not supported for Web target")
}

22
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt

@ -0,0 +1,22 @@
/*
* 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.
*/
package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.ElementImpl
import org.xml.sax.InputSource
import java.io.ByteArrayInputStream
import javax.xml.parsers.DocumentBuilderFactory
internal actual fun parseXML(byteArray: ByteArray): Element =
ElementImpl(
DocumentBuilderFactory
.newInstance().apply {
isNamespaceAware = true
}
.newDocumentBuilder()
.parse(InputSource(ByteArrayInputStream(byteArray)))
.documentElement)

10
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt

@ -0,0 +1,10 @@
package org.jetbrains.compose.resources.vector.xmldom
import org.w3c.dom.Element as DomElement
class ElementImpl(val element: DomElement): NodeImpl(element), Element {
override fun getAttributeNS(nameSpaceURI: String, localName: String): String =
element.getAttributeNS(nameSpaceURI, localName)
override fun getAttribute(name: String): String = element.getAttribute(name)
}

28
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt

@ -0,0 +1,28 @@
package org.jetbrains.compose.resources.vector.xmldom
import org.w3c.dom.Node as DomNode
import org.w3c.dom.Element as DomElement
open class NodeImpl(val n: DomNode): Node {
override val nodeName: String
get() = n.nodeName
override val localName: String
get() = n.localName
override val childNodes: NodeList
get() =
object: NodeList {
override fun item(i: Int): Node {
val child = n.childNodes.item(i)
return if (child is DomElement) ElementImpl(child) else NodeImpl(child)
}
override val length: Int = n.childNodes.length
}
override val namespaceURI: String
get() = n.namespaceURI
override fun lookupPrefix(namespaceURI: String): String = n.lookupPrefix(namespaceURI)
}

5
components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt

@ -5,6 +5,7 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.vector.xmldom.Element
import kotlinx.cinterop.addressOf import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned import kotlinx.cinterop.usePinned
import platform.Foundation.NSData import platform.Foundation.NSData
@ -37,3 +38,7 @@ private class MacOSResourceImpl(path: String) : AbstractResourceImpl(path) {
internal actual class MissingResourceException actual constructor(path: String) : internal actual class MissingResourceException actual constructor(path: String) :
Exception("Missing resource with path: $path") Exception("Missing resource with path: $path")
internal actual fun parseXML(byteArray: ByteArray): Element {
throw UnsupportedOperationException("XML Vector Drawables are not supported for MacOS target")
}

Loading…
Cancel
Save