Skip to content

Commit

Permalink
Add stroke properties to generated Path (#14)
Browse files Browse the repository at this point in the history
Also, refactor ImageVectorGenerator code and add default values for Vector and VectorNode.Path classes.
  • Loading branch information
serbelga authored Feb 4, 2024
1 parent 3338ea4 commit 82de316
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,21 @@ package dev.sergiobelda.compose.vectorize.generator

import dev.sergiobelda.compose.vectorize.generator.vector.FillType
import dev.sergiobelda.compose.vectorize.generator.vector.PathParser
import dev.sergiobelda.compose.vectorize.generator.vector.StrokeCap
import dev.sergiobelda.compose.vectorize.generator.vector.StrokeJoin
import dev.sergiobelda.compose.vectorize.generator.vector.Vector
import dev.sergiobelda.compose.vectorize.generator.vector.Vector.Companion.DefaultHeight
import dev.sergiobelda.compose.vectorize.generator.vector.Vector.Companion.DefaultViewportHeight
import dev.sergiobelda.compose.vectorize.generator.vector.Vector.Companion.DefaultViewportWidth
import dev.sergiobelda.compose.vectorize.generator.vector.Vector.Companion.DefaultWidth
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultFillAlpha
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultFillType
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeAlpha
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeCap
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeLineJoin
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeLineMiter
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeWidth
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParser.END_DOCUMENT
import org.xmlpull.v1.XmlPullParser.END_TAG
Expand All @@ -43,10 +56,10 @@ class ImageParser(private val image: Image) {

check(parser.name == VECTOR) { "The start tag must be <vector>!" }

var width = ""
var height = ""
var viewportWidth = 0f
var viewportHeight = 0f
var width = DefaultWidth
var height = DefaultHeight
var viewportWidth = DefaultViewportWidth
var viewportHeight = DefaultViewportHeight
val nodes = mutableListOf<VectorNode>()

var currentGroup: VectorNode.Group? = null
Expand All @@ -56,10 +69,10 @@ class ImageParser(private val image: Image) {
START_TAG -> {
when (parser.name) {
VECTOR -> {
width = parser.getValueAsString(WIDTH).processDpDimension()
height = parser.getValueAsString(HEIGHT).processDpDimension()
viewportWidth = parser.getValueAsFloat(VIEWPORT_WIDTH) ?: 0f
viewportHeight = parser.getValueAsFloat(VIEWPORT_HEIGHT) ?: 0f
width = parser.getValueAsString(WIDTH)?.processDpDimension() ?: DefaultWidth
height = parser.getValueAsString(HEIGHT)?.processDpDimension() ?: DefaultHeight
viewportWidth = parser.getValueAsFloat(VIEWPORT_WIDTH) ?: DefaultViewportWidth
viewportHeight = parser.getValueAsFloat(VIEWPORT_HEIGHT) ?: DefaultViewportHeight
}

PATH -> {
Expand All @@ -68,21 +81,40 @@ class ImageParser(private val image: Image) {
PATH_DATA,
)
val fillAlpha = parser.getValueAsFloat(FILL_ALPHA)
val strokeAlpha = parser.getValueAsFloat(STROKE_ALPHA)
val fillColor = parser.getValueAsString(FILL_COLOR).processFillColor()

val fillColor = parser.getValueAsString(FILL_COLOR)?.processColor()
val fillType = when (parser.getAttributeValue(null, FILL_TYPE)) {
// evenOdd and nonZero are the only supported values here, where
// nonZero is the default if no values are defined.
EVEN_ODD -> FillType.EvenOdd
else -> FillType.NonZero
else -> DefaultFillType
}
val strokeAlpha = parser.getValueAsFloat(STROKE_ALPHA)
val strokeCap = when (parser.getAttributeValue(null, STROKE_LINE_CAP)) {
ROUND -> StrokeCap.Round
SQUARE -> StrokeCap.Square
else -> DefaultStrokeCap
}
val strokeColor = parser.getValueAsString(STROKE_COLOR)?.processColor()
val strokeLineJoin =
when (parser.getAttributeValue(null, STROKE_LINE_JOIN)) {
BEVEL -> StrokeJoin.Bevel
ROUND -> StrokeJoin.Round
else -> DefaultStrokeLineJoin
}
val strokeMiterLimit = parser.getValueAsFloat(STROKE_MITER_LIMIT)
val strokeWidth = parser.getValueAsFloat(STROKE_WIDTH)

val path = VectorNode.Path(
strokeAlpha = strokeAlpha ?: 1f,
fillAlpha = fillAlpha ?: 1f,
fillColor = fillColor.uppercase(),
fillAlpha = fillAlpha ?: DefaultFillAlpha,
fillColor = fillColor?.uppercase(),
fillType = fillType,
nodes = PathParser.parsePathString(pathData),
strokeAlpha = strokeAlpha ?: DefaultStrokeAlpha,
strokeCap = strokeCap,
strokeColor = strokeColor,
strokeLineMiter = strokeMiterLimit ?: DefaultStrokeLineMiter,
strokeLineJoin = strokeLineJoin,
strokeWidth = strokeWidth ?: DefaultStrokeWidth,
)
if (currentGroup != null) {
currentGroup.paths.add(path)
Expand Down Expand Up @@ -124,8 +156,8 @@ private fun XmlPullParser.getValueAsFloat(name: String): Float? =
/**
* @return the string value for the attribute [name], or null if it couldn't be found
*/
private fun XmlPullParser.getValueAsString(name: String): String =
getAttributeValue(null, name)
private fun XmlPullParser.getValueAsString(name: String): String? =
getAttributeValue(null, name)?.toString()

private fun XmlPullParser.seekToStartTag(): XmlPullParser {
var type = next()
Expand All @@ -145,7 +177,7 @@ private fun XmlPullParser.isAtEnd() =
private fun String.processDpDimension(): String =
this.replace("dp", "")

private fun String.processFillColor(): String {
private fun String.processColor(): String {
val diff = ARGB_HEXADECIMAL_COLOR_LENGTH - this.length
return if (diff > 0) {
this.replace("#", "#${"F".repeat(diff)}")
Expand All @@ -169,9 +201,17 @@ private const val FILL_TYPE = "android:fillType"
private const val HEIGHT = "android:height"
private const val PATH_DATA = "android:pathData"
private const val STROKE_ALPHA = "android:strokeAlpha"
private const val STROKE_COLOR = "android:strokeColor"
private const val STROKE_LINE_CAP = "android:strokeLineCap"
private const val STROKE_LINE_JOIN = "android:strokeLineJoin"
private const val STROKE_MITER_LIMIT = "android:strokeMiterLimit"
private const val STROKE_WIDTH = "android:strokeWidth"
private const val VIEWPORT_HEIGHT = "android:viewportHeight"
private const val VIEWPORT_WIDTH = "android:viewportWidth"
private const val WIDTH = "android:width"

// XML attribute values
private const val BEVEL = "bevel"
private const val EVEN_ODD = "evenOdd"
private const val ROUND = "round"
private const val SQUARE = "square"
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.buildCodeBlock
import dev.sergiobelda.compose.vectorize.generator.utils.setIndent
import dev.sergiobelda.compose.vectorize.generator.vector.FillType
import dev.sergiobelda.compose.vectorize.generator.vector.StrokeCap
import dev.sergiobelda.compose.vectorize.generator.vector.StrokeJoin
import dev.sergiobelda.compose.vectorize.generator.vector.Vector
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultFillAlpha
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultFillType
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeAlpha
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeCap
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeLineJoin
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeLineMiter
import dev.sergiobelda.compose.vectorize.generator.vector.VectorNode.Path.Companion.DefaultStrokeWidth
import java.util.Locale

/**
Expand Down Expand Up @@ -103,17 +112,24 @@ class ImageVectorGenerator(
)
.addCode(
buildCodeBlock {
val controlFlow = buildString {
append("%N = %M(\n")
append(" name = \"%N\",\n")
append(" width = ${vector.width}f,\n")
append(" height = ${vector.height}f,\n")
append(" viewportWidth = ${vector.viewportWidth}f,\n")
append(" viewportHeight = ${vector.viewportHeight}f\n")
append(")")
val parameterList = listOfNotNull(
"name = \"%N\"",
"width = ${vector.width}f",
"height = ${vector.height}f",
"viewportWidth = ${vector.viewportWidth}f",
"viewportHeight = ${vector.viewportHeight}f",
)
val parameters = if (parameterList.isNotEmpty()) {
parameterList.joinToString(
prefix = "%N = %M(\n\t",
postfix = "\n)",
separator = ",\n\t",
)
} else {
""
}
beginControlFlow(
controlFlow,
parameters,
backingProperty,
MemberNames.ImageVector,
imageName,
Expand Down Expand Up @@ -167,40 +183,73 @@ private fun CodeBlock.Builder.addPath(
path: VectorNode.Path,
pathBody: CodeBlock.Builder.() -> Unit,
) {
// Only set the fill type if it is EvenOdd - otherwise it will just be the default.
val setFillType = path.fillType == FillType.EvenOdd
val parameterList = mutableListOf<String>()
val memberList = mutableListOf<MemberName>()

val parameterList = with(path) {
listOfNotNull(
"fill = %M(%M(${fillColor.replace("#", "0x")}))",
"fillAlpha = ${fillAlpha}f".takeIf { fillAlpha != 1f },
"strokeAlpha = ${strokeAlpha}f".takeIf { strokeAlpha != 1f },
"pathFillType = %M".takeIf { setFillType },
)
with(path) {
memberList.add(MemberNames.Path)

fillAlpha.takeIf { it != DefaultFillAlpha }?.let {
parameterList.add("fillAlpha = ${it}f")
}
fillColor?.let {
parameterList.add("fill = %M(%M(${it.replace("#", "0x")}))")
memberList.add(MemberNames.SolidColor)
memberList.add(MemberNames.Color)
}
fillType.takeIf { it != DefaultFillType }?.let {
parameterList.add("pathFillType = %M")
memberList.add(MemberNames.PathFillType.EvenOdd)
}
strokeAlpha.takeIf { it != DefaultStrokeAlpha }?.let {
parameterList.add("strokeAlpha = ${it}f")
}
strokeColor?.let {
parameterList.add("stroke = %M(%M(${it.replace("#", "0x")}))")
memberList.add(MemberNames.SolidColor)
memberList.add(MemberNames.Color)
}
strokeCap.takeIf { it != DefaultStrokeCap }?.let {
parameterList.add("strokeLineCap = %M")
when (strokeCap) {
StrokeCap.Round -> memberList.add(MemberNames.StrokeCapType.Round)
StrokeCap.Square -> memberList.add(MemberNames.StrokeCapType.Square)
else -> memberList.add(MemberNames.StrokeCapType.Butt)
}
}
strokeLineMiter.takeIf { it != DefaultStrokeLineMiter }?.let {
parameterList.add("strokeLineMiter = ${strokeLineMiter}f")
}
strokeLineJoin.takeIf { it != DefaultStrokeLineJoin }?.let {
parameterList.add("strokeLineJoin = %M")
when (strokeLineJoin) {
StrokeJoin.Bevel -> memberList.add(MemberNames.StrokeJoinType.Bevel)
StrokeJoin.Round -> memberList.add(MemberNames.StrokeJoinType.Round)
else -> memberList.add(MemberNames.StrokeJoinType.Miter)
}
}
strokeWidth.takeIf { it != DefaultStrokeWidth }?.let {
parameterList.add("strokeLineWidth = ${strokeWidth}f")
}
}
addPathParameters(parameterList, memberList)
pathBody()
endControlFlow()
}

private fun CodeBlock.Builder.addPathParameters(
parameterList: List<String>,
memberList: List<MemberName>,
) {
val parameters = if (parameterList.isNotEmpty()) {
parameterList.joinToString(prefix = "(", postfix = ")")
parameterList.joinToString(
prefix = "(\n\t",
postfix = "\n)",
separator = ",\n\t",
)
} else {
""
}

if (setFillType) {
beginControlFlow(
"%M$parameters",
MemberNames.Path,
MemberNames.SolidColor,
MemberNames.Color,
MemberNames.EvenOdd,
)
} else {
beginControlFlow(
"%M$parameters",
MemberNames.Path,
MemberNames.SolidColor,
MemberNames.Color,
)
}
pathBody()
endControlFlow()
beginControlFlow("%M$parameters", *memberList.toTypedArray())
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,40 @@ enum class PackageNames(val packageName: String) {
object ClassNames {
val ImageVector = PackageNames.VectorPackage.className("ImageVector")
val PathFillType = PackageNames.GraphicsPackage.className("PathFillType", "Companion")
val StrokeCapType = PackageNames.GraphicsPackage.className("StrokeCap", "Companion")
val StrokeJoinType = PackageNames.GraphicsPackage.className("StrokeJoin", "Companion")
}

/**
* [MemberName]s used for image generation.
*/
object MemberNames {
val Color = MemberName(PackageNames.GraphicsPackage.packageName, "Color")
val EvenOdd = MemberName(ClassNames.PathFillType, "EvenOdd")
val Group = MemberName(PackageNames.VectorPackage.packageName, "group")
val ImageVector = MemberName(
PackageNames.ComposeVectorizeCore.packageName,
"imageVector",
)
val Path = MemberName(PackageNames.VectorPackage.packageName, "path")

val Color = MemberName(PackageNames.GraphicsPackage.packageName, "Color")
val SolidColor = MemberName(PackageNames.GraphicsPackage.packageName, "SolidColor")

val Group = MemberName(PackageNames.VectorPackage.packageName, "group")
val Path = MemberName(PackageNames.VectorPackage.packageName, "path")

object PathFillType {
val EvenOdd = MemberName(ClassNames.PathFillType, "EvenOdd")
}

object StrokeCapType {
val Butt = MemberName(ClassNames.StrokeCapType, "Butt")
val Round = MemberName(ClassNames.StrokeCapType, "Round")
val Square = MemberName(ClassNames.StrokeCapType, "Square")
}

object StrokeJoinType {
val Bevel = MemberName(ClassNames.StrokeJoinType, "Bevel")
val Miter = MemberName(ClassNames.StrokeJoinType, "Miter")
val Round = MemberName(ClassNames.StrokeJoinType, "Round")
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2024 Sergio Belda
*
* 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 dev.sergiobelda.compose.vectorize.generator.vector

/**
* Styles to use for line endings.
*
* This maps to [androidx.compose.ui.graphics.StrokeCap] used in the framework, and can be defined in XML
* via `android:strokeLineCap`.
*/
enum class StrokeCap {
Butt,
Round,
Square,
}
Loading

0 comments on commit 82de316

Please sign in to comment.