From 7e1323e394e4ade33537ddf2c545528af1dbfaae Mon Sep 17 00:00:00 2001 From: Mario Ferreira Vilanova <65548670+MarioFerreiraStorytel@users.noreply.github.com> Date: Fri, 23 Jul 2021 14:55:33 +0200 Subject: [PATCH] Added support for gradients (#6) * Added different types of fill * Added new testing script * The parser detects now gradient tags * The parser reads now Linear and Radial gradient tags * The parser reads now gradient color stops * The parser correctly parses now linear gradients * Parser now parses radial gradients * Fixed issues with the parsing arguments * The parser now supports linear and radial gradients * Adjusted the gradient imports --- .../material/icons/generator/IconParser.kt | 78 ++++++++++++++++- .../compose/material/icons/generator/Names.kt | 7 ++ .../icons/generator/VectorAssetGenerator.kt | 83 +++++++++++++++++-- .../material/icons/generator/vector/Vector.kt | 22 ++++- .../com/devsrsouza/svg2compose/Svg2Compose.kt | 8 +- src/test/kotlin/EmojiTest.kt | 21 +++++ 6 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 src/test/kotlin/EmojiTest.kt diff --git a/src/main/kotlin/androidx/compose/material/icons/generator/IconParser.kt b/src/main/kotlin/androidx/compose/material/icons/generator/IconParser.kt index e544bf24..3b10bdc7 100644 --- a/src/main/kotlin/androidx/compose/material/icons/generator/IconParser.kt +++ b/src/main/kotlin/androidx/compose/material/icons/generator/IconParser.kt @@ -83,9 +83,13 @@ class IconParser(private val icon: Icon) { val fillColor = parser.getAttributeValue(null, FILL_COLOR) ?.toHexColor() + val fill = when { + fillColor != null -> Fill.Color(fillColor) + else -> null + } val path = VectorNode.Path( - fillColorHex = fillColor, + fill = fill, strokeColorHex = strokeColor, strokeAlpha = strokeAlpha ?: 1f, fillAlpha = fillAlpha ?: 1f, @@ -110,6 +114,55 @@ class IconParser(private val icon: Icon) { } CLIP_PATH -> { /* TODO: b/147418351 - parse clipping paths */ } + GRADIENT -> { + val gradient = when (parser.getAttributeValue(null, TYPE)){ + LINEAR -> { + val startX = parser.getValueAsFloat(START_X) ?: 0f + val startY = parser.getValueAsFloat(START_Y) ?: 0f + val endX = parser.getValueAsFloat(END_X) ?: 0f + val endY = parser.getValueAsFloat(END_Y) ?: 0f + Fill.LinearGradient( + startY = startY, + startX = startX, + endX = endX, + endY = endY + ) + } + RADIAL -> { + val gradientRadius = parser.getValueAsFloat(GRADIENT_RADIUS) ?: 0f + val centerX = parser.getValueAsFloat(CENTER_X) ?: 0f + val centerY = parser.getValueAsFloat(CENTER_Y) ?: 0f + Fill.RadialGradient( + gradientRadius = gradientRadius, + centerX = centerX, + centerY = centerY + ) + } + else -> null + } + + val lastPath = currentGroup?.paths?.removeLast() ?: nodes.removeLast() + if (lastPath as? VectorNode.Path != null && lastPath.fill == null){ + val gradientPath = lastPath.copy (fill = gradient) + if (currentGroup != null) { + currentGroup.paths.add(gradientPath) + } else { + nodes.add(gradientPath) + } + } + } + ITEM -> { + val offset = parser.getValueAsFloat(OFFSET) ?: 0f + val colorHex = parser.getAttributeValue(null, COLOR).toHexColor() + + val colorStop = Pair(offset,colorHex) + val lastPath = (currentGroup?.paths?.last() ?: nodes.last()) as? VectorNode.Path + when (lastPath?.fill){ + is Fill.LinearGradient -> lastPath.fill.colorStops.add(colorStop) + is Fill.RadialGradient -> lastPath.fill.colorStops.add(colorStop) + else -> {} + } + } } } } @@ -149,7 +202,7 @@ private fun XmlPullParser.isAtEnd() = private val hexRegex = "^[0-9a-fA-F]{6,8}".toRegex() -private fun String.toHexColor(): String? { +private fun String.toHexColor(): String { return removePrefix("#") .let { if(hexRegex.matches(it)) { @@ -165,6 +218,12 @@ private fun String.toHexColor(): String? { private const val CLIP_PATH = "clip-path" private const val GROUP = "group" private const val PATH = "path" +private const val GRADIENT = "gradient" +private const val ITEM = "item" + +// XML names +private const val LINEAR = "linear" +private const val RADIAL = "radial" // Path XML attribute names private const val PATH_DATA = "android:pathData" @@ -178,12 +237,25 @@ private const val STROKE_MITER_LIMIT = "android:strokeMiterLimit" private const val STROKE_COLOR = "android:strokeColor" private const val FILL_COLOR = "android:fillColor" +// Gradient XML attribute names +private const val TYPE = "android:type" +private const val START_Y = "android:startY" +private const val START_X = "android:startX" +private const val END_Y = "android:endY" +private const val END_X = "android:endX" +private const val GRADIENT_RADIUS = "android:gradientRadius" +private const val CENTER_X = "android:centerX" +private const val CENTER_Y = "android:centerY" + +// Item XML attribute names +private const val OFFSET = "android:offset" +private const val COLOR = "android:color" + // Vector XML attribute names private const val WIDTH = "android:width" private const val HEIGHT = "android:height" private const val VIEWPORT_WIDTH = "android:viewportWidth" private const val VIEWPORT_HEIGHT = "android:viewportHeight" - // XML attribute values private const val EVEN_ODD = "evenOdd" diff --git a/src/main/kotlin/androidx/compose/material/icons/generator/Names.kt b/src/main/kotlin/androidx/compose/material/icons/generator/Names.kt index 45568af8..fa19dd90 100644 --- a/src/main/kotlin/androidx/compose/material/icons/generator/Names.kt +++ b/src/main/kotlin/androidx/compose/material/icons/generator/Names.kt @@ -26,6 +26,7 @@ enum class PackageNames(val packageName: String) { MaterialIconsPackage("androidx.compose.material.icons"), GraphicsPackage("androidx.compose.ui.graphics"), VectorPackage(GraphicsPackage.packageName + ".vector"), + GeometryPackage("androidx.compose.ui.geometry"), Unit("androidx.compose.ui.unit"), } @@ -38,6 +39,7 @@ object ClassNames { val PathFillType = PackageNames.GraphicsPackage.className("PathFillType", CompanionImportName) val StrokeCap = PackageNames.GraphicsPackage.className("StrokeCap", CompanionImportName) val StrokeJoin = PackageNames.GraphicsPackage.className("StrokeJoin", CompanionImportName) + val Brush = PackageNames.GraphicsPackage.className("Brush", CompanionImportName) } /** @@ -65,6 +67,11 @@ object MemberNames { val Color = MemberName(PackageNames.GraphicsPackage.packageName, "Color") val SolidColor = MemberName(PackageNames.GraphicsPackage.packageName, "SolidColor") + + val LinearGradient = MemberName(ClassNames.Brush, "linearGradient") + val RadialGradient = MemberName(ClassNames.Brush, "radialGradient") + + val Offset = MemberName(PackageNames.GeometryPackage.packageName, "Offset") } /** diff --git a/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt b/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt index 576d19cf..98918681 100644 --- a/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt +++ b/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt @@ -18,6 +18,7 @@ package androidx.compose.material.icons.generator import androidx.compose.material.icons.generator.util.backingPropertySpec import androidx.compose.material.icons.generator.util.withBackingProperty +import androidx.compose.material.icons.generator.vector.Fill import androidx.compose.material.icons.generator.vector.Vector import androidx.compose.material.icons.generator.vector.VectorNode import com.squareup.kotlinpoet.* @@ -149,12 +150,11 @@ private fun CodeBlock.Builder.addPath( path: VectorNode.Path, pathBody: CodeBlock.Builder.() -> Unit ) { - val hasFillColor = path.fillColorHex != null val hasStrokeColor = path.strokeColorHex != null val parameterList = with(path) { listOfNotNull( - "fill = ${if(hasFillColor) "%M(%M(0x$fillColorHex))" else "null"}", + "fill = ${getPathFill(path)}", "stroke = ${if(hasStrokeColor) "%M(%M(0x$strokeColorHex))" else "null"}", "fillAlpha = ${fillAlpha}f".takeIf { fillAlpha != 1f }, "strokeAlpha = ${strokeAlpha}f".takeIf { strokeAlpha != 1f }, @@ -170,15 +170,37 @@ private fun CodeBlock.Builder.addPath( val members: Array = listOfNotNull( MemberNames.Path, - MemberNames.SolidColor.takeIf { hasFillColor }, - MemberNames.Color.takeIf { hasFillColor }, MemberNames.SolidColor.takeIf { hasStrokeColor }, MemberNames.Color.takeIf { hasStrokeColor }, path.strokeLineWidth.memberName, path.strokeLineCap.memberName, path.strokeLineJoin.memberName, path.fillType.memberName - ).toTypedArray() + ).toMutableList().apply { + var fillIndex = 1 + when (path.fill){ + is Fill.Color -> { + add(fillIndex, MemberNames.SolidColor) + add(++fillIndex, MemberNames.Color) + } + is Fill.LinearGradient -> { + add(fillIndex, MemberNames.LinearGradient) + path.fill.colorStops.forEach { _ -> + add(++fillIndex, MemberNames.Color) + } + add(++fillIndex, MemberNames.Offset) + add(++fillIndex, MemberNames.Offset) + } + is Fill.RadialGradient -> { + add(fillIndex, MemberNames.RadialGradient) + path.fill.colorStops.forEach { _ -> + add(++fillIndex, MemberNames.Color) + } + add(++fillIndex, MemberNames.Offset) + } + null -> {} + } + }.toTypedArray() beginControlFlow( "%M$parameters", @@ -189,4 +211,55 @@ private fun CodeBlock.Builder.addPath( endControlFlow() } +private fun getPathFill ( + path: VectorNode.Path +) = when (path.fill){ + is Fill.Color -> "%M(%M(0x${path.fill.colorHex}))" + is Fill.LinearGradient -> { + with (path.fill){ + "%M(" + + "${getGradientStops(path.fill.colorStops).toString().removeSurrounding("[","]")}, " + + "start = %M(${startX}f,${startY}f), " + + "end = %M(${endX}f,${endY}f))" + } + } + is Fill.RadialGradient -> { + with (path.fill){ + "%M(${getGradientStops(path.fill.colorStops).toString().removeSurrounding("[","]")}, " + + "center = %M(${centerX}f,${centerY}f), " + + "radius = ${gradientRadius}f)" + } + } + else -> "null" +} + +private fun getGradientStops( + stops: List> +) = stops.map { stop -> + "${stop.first}f to %M(0x${stop.second})" +} + +private fun CodeBlock.Builder.addLinearGradient( + gradient: Fill.LinearGradient, + pathBody: CodeBlock.Builder.() -> Unit +){ + //"0.0f to Color.Red" + val parameterList = with(gradient) { + listOfNotNull( + "start = %M(${gradient.startX},${gradient.startY})", + "end = %M(${gradient.endX},${gradient.endY})" + ) + } + + val parameters = parameterList.joinToString(prefix = "(", postfix = ")") + + val members: Array = listOfNotNull( + MemberNames.LinearGradient, + MemberNames.Offset, + MemberNames.Offset + ).toTypedArray() + + +} + private val GraphicUnit.withMemberIfNotNull: String get() = "${value}${if (memberName != null) ".%M" else "f"}" \ No newline at end of file diff --git a/src/main/kotlin/androidx/compose/material/icons/generator/vector/Vector.kt b/src/main/kotlin/androidx/compose/material/icons/generator/vector/Vector.kt index 052b100f..4c57ffdd 100644 --- a/src/main/kotlin/androidx/compose/material/icons/generator/vector/Vector.kt +++ b/src/main/kotlin/androidx/compose/material/icons/generator/vector/Vector.kt @@ -17,6 +17,7 @@ package androidx.compose.material.icons.generator.vector import androidx.compose.material.icons.generator.GraphicUnit +import java.awt.LinearGradientPaint /** * Simplified representation of a vector, with root [nodes]. @@ -38,8 +39,8 @@ class Vector( */ sealed class VectorNode { class Group(val paths: MutableList = mutableListOf()) : VectorNode() - class Path( - val fillColorHex: String?, + data class Path( + val fill: Fill?, val strokeColorHex: String?, val strokeAlpha: Float, val fillAlpha: Float, @@ -50,4 +51,21 @@ sealed class VectorNode { val fillType: FillType, val nodes: List ) : VectorNode() +} + +sealed class Fill { + data class Color(val colorHex: String) : Fill() + data class LinearGradient( + val startY: Float, + val startX: Float, + val endY: Float, + val endX: Float, + val colorStops: MutableList> = mutableListOf() + ) : Fill() + data class RadialGradient( + val gradientRadius: Float, + val centerX: Float, + val centerY: Float, + val colorStops: MutableList> = mutableListOf() + ): Fill() } \ No newline at end of file diff --git a/src/main/kotlin/br/com/devsrsouza/svg2compose/Svg2Compose.kt b/src/main/kotlin/br/com/devsrsouza/svg2compose/Svg2Compose.kt index 7c9772de..f012a7b4 100644 --- a/src/main/kotlin/br/com/devsrsouza/svg2compose/Svg2Compose.kt +++ b/src/main/kotlin/br/com/devsrsouza/svg2compose/Svg2Compose.kt @@ -37,15 +37,15 @@ object Svg2Compose { vectorsDirectory.walkTopDown() .maxDepth(10) - .onEnter { - val dirIcons = it.listFiles() + .onEnter { file -> + val dirIcons = file.listFiles() .filter { it.isDirectory.not() } .filter { it.extension.equals(type.extension, ignoreCase = true) } val previousGroup = groupStack.peekOrNull() // if there is no previous group, this is the root dir, and the group name should be the accessorName - val groupName = if(previousGroup == null) accessorName else it.name.toKotlinPropertyName() + val groupName = if(previousGroup == null) accessorName else file.name.toKotlinPropertyName() val groupPackage = previousGroup?.let { group -> "${group.groupPackage}.${group.groupName.second.toLowerCase()}" } ?: "$applicationIconPackage" val iconsPackage = "$groupPackage.${groupName.toLowerCase()}" @@ -100,7 +100,7 @@ object Svg2Compose { val result = GeneratedGroup( groupPackage, - it to groupName, + file to groupName, generatedIconsMemberNames, groupClassName, groupFileSpec, diff --git a/src/test/kotlin/EmojiTest.kt b/src/test/kotlin/EmojiTest.kt new file mode 100644 index 00000000..df958e87 --- /dev/null +++ b/src/test/kotlin/EmojiTest.kt @@ -0,0 +1,21 @@ +package br.com.devsrsouza.svg2compose + +import java.io.File + + +fun main(){ + val iconTest = File("/Users/marioferreiravilanova/Documents/Workspace/kotlin/svg-to-compose/src/test/icons") + val src = File("/Users/marioferreiravilanova/Documents/Workspace/kotlin/svg-to-compose/src/test/results").apply { mkdirs() } + + Svg2Compose.parse( + applicationIconPackage = "com.test", + accessorName = "Icons", + outputSourceDirectory = src, + vectorsDirectory = iconTest, + type = VectorType.SVG, + iconNameTransformer = { name, group -> + name.split("-").joinToString(separator = "").removePrefix(group) + }, + allAssetsPropertyName = "AllIcons" + ) +}