Skip to content

Commit a589e31

Browse files
committed
Support inherited SVG paint attributes
1 parent 57d139d commit a589e31

6 files changed

Lines changed: 151 additions & 16 deletions

File tree

components/parser/kmp/svg/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ plugins {
55
}
66

77
kotlin {
8+
compilerOptions {
9+
freeCompilerArgs.add("-Xcontext-parameters")
10+
}
11+
812
sourceSets {
913
commonMain.dependencies {
1014
implementation(projects.sdk.ir.core)

components/parser/kmp/svg/src/commonMain/kotlin/io/github/composegears/valkyrie/parser/kmp/svg/SVG.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ internal data class SVG(
1717
@SerialName("height") val height: String? = null,
1818
@SerialName("viewBox") val viewBox: String? = null,
1919
@SerialName("fill") val fill: String? = null,
20+
@SerialName("stroke") val strokeColor: String? = null,
21+
@SerialName("stroke-width") val strokeWidth: String? = null,
22+
@SerialName("stroke-linecap") val strokeLineCap: String? = null,
23+
@SerialName("stroke-linejoin") val strokeLineJoin: String? = null,
24+
@SerialName("stroke-opacity") val strokeAlpha: String? = null,
25+
@SerialName("stroke-miterlimit") val strokeMiter: String? = null,
2026
@XmlPolyChildren([GROUP, PATH, CIRCLE, RECTANGLE, ELLIPSE, POLYGON])
2127
val children: List<@Polymorphic Child> = emptyList(),
2228
) {
@@ -48,6 +54,7 @@ internal data class SVG(
4854
data class Group(
4955
@SerialName("id") override val id: String? = null,
5056
@SerialName("transform") val transform: String? = null,
57+
@SerialName("fill") val fill: String? = null,
5158
@XmlPolyChildren([GROUP, PATH, CIRCLE, RECTANGLE, ELLIPSE, POLYGON])
5259
val children: List<@Polymorphic Child> = emptyList(),
5360

components/parser/kmp/svg/src/commonMain/kotlin/io/github/composegears/valkyrie/parser/kmp/svg/SVGParser.kt

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,23 @@ object SVGParser {
3939
defaultHeight = height ?: DEFAULT_HEIGHT,
4040
viewportWidth = rect?.width ?: width ?: DEFAULT_WIDTH,
4141
viewportHeight = rect?.height ?: height ?: DEFAULT_HEIGHT,
42-
nodes = children.mapNotNull { it.toIrVectorNode() },
42+
nodes = with(toPaintContext()) {
43+
children.mapNotNull { it.toIrVectorNode() }
44+
},
4345
)
4446
}
4547

48+
private fun SVG.toPaintContext(): PaintContext = PaintContext(
49+
fill = fill,
50+
strokeColor = strokeColor,
51+
strokeWidth = strokeWidth,
52+
strokeLineCap = strokeLineCap,
53+
strokeLineJoin = strokeLineJoin,
54+
strokeAlpha = strokeAlpha,
55+
strokeMiter = strokeMiter,
56+
)
57+
58+
context(paintContext: PaintContext)
4659
private fun SVG.Child.toIrVectorNode(): IrVectorNode? = when (this) {
4760
is SVG.Path -> toVectorPath()
4861
is SVG.Circle -> toVectorPath()
@@ -52,11 +65,14 @@ object SVGParser {
5265
is SVG.Ellipse -> toVectorPath()
5366
}
5467

68+
context(paintContext: PaintContext)
5569
private fun SVG.Path.toVectorPath(): IrVectorNode.IrPath {
56-
var fillColor: IrColor? = fill?.let(SvgColorParser::parse)
70+
val resolvedFill = fill ?: paintContext.fill
71+
val resolvedStrokeColor = strokeColor ?: paintContext.strokeColor
72+
var fillColor: IrColor? = resolvedFill?.let(SvgColorParser::parse)
5773
// NOTE: Only when fill and strokeColor is null use black FillColor as default color as
5874
// fill can be none resulting to null.
59-
fillColor = if (fill == null && strokeColor == null) Black else fillColor
75+
fillColor = if (resolvedFill == null && resolvedStrokeColor == null) Black else fillColor
6076
val stroke = getSVGStrokeWithDefaults()
6177
return IrVectorNode.IrPath(
6278
name = id.orEmpty(),
@@ -73,15 +89,16 @@ object SVGParser {
7389
)
7490
}
7591

92+
context(paintContext: PaintContext)
7693
private fun SVG.Circle.toVectorPath(): IrVectorNode.IrPath {
7794
val cx = centerX.toFloat()
7895
val cy = centerY.toFloat()
7996
val r = radius.toFloat()
80-
val color = fill?.let(SvgColorParser::parse) ?: Black
97+
val fill = resolveFill(fill)
8198
val stroke = getSVGStrokeWithDefaults()
8299
return IrVectorNode.IrPath(
83100
name = id.orEmpty(),
84-
fill = IrFill.Color(color),
101+
fill = fill,
85102
fillAlpha = fillAlpha?.toFloat() ?: 1f,
86103
stroke = stroke.color?.let { IrStroke.Color(it) },
87104
strokeAlpha = stroke.alpha,
@@ -115,11 +132,12 @@ object SVGParser {
115132
)
116133
}
117134

135+
context(paintContext: PaintContext)
118136
private fun SVG.Polygon.toVectorPath(): IrVectorNode.IrPath {
119137
val stroke = getSVGStrokeWithDefaults()
120138
return IrVectorNode.IrPath(
121139
name = id.orEmpty(),
122-
fill = if (fill != null) SvgColorParser.parse(fill)?.let { IrFill.Color(it) } else IrFill.Color(Black),
140+
fill = resolveFill(fill),
123141
fillAlpha = fillAlpha?.toFloat() ?: 1f,
124142
stroke = stroke.color?.let { IrStroke.Color(it) },
125143
strokeAlpha = stroke.alpha,
@@ -144,13 +162,25 @@ object SVGParser {
144162
)
145163
}
146164

165+
context(paintContext: PaintContext)
147166
private fun SVG.Group.toVectorGroup(): IrVectorNode.IrGroup {
167+
val groupContext = PaintContext(
168+
fill = fill ?: paintContext.fill,
169+
strokeColor = strokeColor ?: paintContext.strokeColor,
170+
strokeWidth = strokeWidth ?: paintContext.strokeWidth,
171+
strokeLineCap = strokeLineCap ?: paintContext.strokeLineCap,
172+
strokeLineJoin = strokeLineJoin ?: paintContext.strokeLineJoin,
173+
strokeAlpha = strokeAlpha ?: paintContext.strokeAlpha,
174+
strokeMiter = strokeMiter ?: paintContext.strokeMiter,
175+
)
148176
val pivot = transform?.getPivot() ?: Translation.Default
149177
val translation = transform?.getTranslation() ?: Translation.Default
150178
val scale = transform?.getScale() ?: Scale.Default
151179
return IrVectorNode.IrGroup(
152180
name = id.orEmpty(),
153-
nodes = children.mapNotNull { it.toIrVectorNode() }.toMutableList(),
181+
nodes = with(groupContext) {
182+
children.mapNotNull { it.toIrVectorNode() }.toMutableList()
183+
},
154184
rotate = transform?.getRotation() ?: 0f,
155185
pivotX = pivot.x,
156186
pivotY = pivot.y,
@@ -163,6 +193,7 @@ object SVGParser {
163193
)
164194
}
165195

196+
context(paintContext: PaintContext)
166197
private fun SVG.Rectangle.toVectorPath(): IrVectorNode.IrPath? {
167198
val x = x.toFloat()
168199
val y = y.toFloat()
@@ -174,7 +205,7 @@ object SVGParser {
174205
return IrVectorNode.IrPath(
175206
name = id.orEmpty(),
176207
pathFillType = IrPathFillType.NonZero,
177-
fill = if (fill != null) SvgColorParser.parse(fill)?.let { IrFill.Color(it) } else IrFill.Color(Black),
208+
fill = resolveFill(fill),
178209
fillAlpha = 1f,
179210
stroke = stroke.color?.let { IrStroke.Color(it) },
180211
strokeAlpha = stroke.alpha,
@@ -192,6 +223,7 @@ object SVGParser {
192223
)
193224
}
194225

226+
context(paintContext: PaintContext)
195227
private fun SVG.Ellipse.toVectorPath(): IrVectorNode.IrPath {
196228
val cx = centerX.toFloat()
197229
val cy = centerY.toFloat()
@@ -203,7 +235,7 @@ object SVGParser {
203235
return IrVectorNode.IrPath(
204236
name = id.orEmpty(),
205237
pathFillType = IrPathFillType.NonZero,
206-
fill = if (fill != null) SvgColorParser.parse(fill)?.let { IrFill.Color(it) } else IrFill.Color(Black),
238+
fill = resolveFill(fill),
207239
fillAlpha = 1f,
208240
stroke = stroke.color?.let { IrStroke.Color(it) },
209241
strokeAlpha = stroke.alpha,
@@ -261,6 +293,26 @@ object SVGParser {
261293
return functionStart.substring(1 until endIndex).split(" ", ",").map { it.toFloat() }
262294
}
263295

296+
context(paintContext: PaintContext)
297+
private fun resolveFill(fill: String?): IrFill.Color? {
298+
val resolvedFill = fill ?: paintContext.fill
299+
val color = when (resolvedFill) {
300+
null -> Black
301+
else -> SvgColorParser.parse(resolvedFill)
302+
}
303+
return color?.let { IrFill.Color(it) }
304+
}
305+
264306
@Suppress("PrivatePropertyName")
265307
private val Black: IrColor = IrColor(0xff000000)
266308
}
309+
310+
internal data class PaintContext(
311+
val fill: String?,
312+
val strokeColor: String?,
313+
val strokeWidth: String?,
314+
val strokeLineCap: String?,
315+
val strokeLineJoin: String?,
316+
val strokeAlpha: String?,
317+
val strokeMiter: String?,
318+
)

components/parser/kmp/svg/src/commonMain/kotlin/io/github/composegears/valkyrie/parser/kmp/svg/SvgExtensions.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import io.github.composegears.valkyrie.sdk.ir.core.IrPathFillType
44
import io.github.composegears.valkyrie.sdk.ir.core.IrStrokeLineCap
55
import io.github.composegears.valkyrie.sdk.ir.core.IrStrokeLineJoin
66

7+
context(paintContext: PaintContext)
78
internal fun SVG.Child.getSVGStrokeWithDefaults(): SVGStroke = SVGStroke(
8-
color = strokeColor?.let { SvgColorParser.parse(it) },
9-
alpha = strokeAlpha?.toFloat() ?: 1f,
10-
width = strokeWidth?.toFloat() ?: 0f,
11-
cap = strokeLineCap?.let { SVGStroke.Cap(it) } ?: SVGStroke.Cap.Butt,
12-
join = strokeLineJoin?.let { SVGStroke.Join(it) } ?: SVGStroke.Join.Miter,
13-
miter = strokeMiter?.toFloat() ?: 4f,
9+
color = (strokeColor ?: paintContext.strokeColor)?.let { SvgColorParser.parse(it) },
10+
alpha = (strokeAlpha ?: paintContext.strokeAlpha)?.toFloat() ?: 1f,
11+
width = (strokeWidth ?: paintContext.strokeWidth)?.toFloat() ?: 0f,
12+
cap = (strokeLineCap ?: paintContext.strokeLineCap)?.let { SVGStroke.Cap(it) } ?: SVGStroke.Cap.Butt,
13+
join = (strokeLineJoin ?: paintContext.strokeLineJoin)?.let { SVGStroke.Join(it) } ?: SVGStroke.Join.Miter,
14+
miter = (strokeMiter ?: paintContext.strokeMiter)?.toFloat() ?: 4f,
1415
)
1516

1617
internal fun SVGStroke.Cap.toIrStrokeLineCap(): IrStrokeLineCap = when (this) {

components/parser/kmp/svg/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/kmp/svg/SVGParserTest.kt

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ internal class SVGParserTest {
4343

4444
@Test
4545
fun parse_path_with_stroke_from_SVG() {
46-
val svg = svg {
46+
val svg = svg(fill = "none") {
4747
"""<path d="" stroke="black" stroke-width="2" stroke-linecap="square" stroke-linejoin="round" stroke-miterlimit="0.25" stroke-opacity="0.5"/>"""
4848
}
4949
assertEquals(
@@ -65,6 +65,69 @@ internal class SVGParserTest {
6565
)
6666
}
6767

68+
@Test
69+
fun parse_path_with_inherited_root_stroke() {
70+
val svg = svg(
71+
fill = "none",
72+
stroke = "#ff0000",
73+
strokeWidth = "1.5",
74+
strokeLineCap = "round",
75+
strokeLineJoin = "round",
76+
) {
77+
"""<path d="M4 4h16"/>"""
78+
}
79+
80+
assertEquals(
81+
actual = SVGParser.parse(svg).nodes,
82+
expected = listOf(
83+
IrVectorNode.IrPath(
84+
pathFillType = IrPathFillType.NonZero,
85+
fill = null,
86+
paths = listOf(
87+
IrPathNode.MoveTo(4f, 4f),
88+
IrPathNode.RelativeHorizontalTo(16f),
89+
),
90+
fillAlpha = 1f,
91+
stroke = IrStroke.Color(IrColor(0xFFFF0000)),
92+
strokeAlpha = 1f,
93+
strokeLineWidth = 1.5f,
94+
strokeLineCap = IrStrokeLineCap.Round,
95+
strokeLineJoin = IrStrokeLineJoin.Round,
96+
strokeLineMiter = 4f,
97+
),
98+
),
99+
)
100+
}
101+
102+
@Test
103+
fun parse_path_with_inherited_group_fill() {
104+
val svg = svg(fill = "none") {
105+
"""
106+
<g fill="#ff0000">
107+
<path d="M4 4h16"/>
108+
</g>
109+
"""
110+
}
111+
112+
val actual: List<IrVectorNode> = SVGParser.parse(svg).groups.single().nodes
113+
114+
assertEquals(
115+
expected = listOf<IrVectorNode>(
116+
IrVectorNode.IrPath(
117+
pathFillType = IrPathFillType.NonZero,
118+
fill = IrFill.Color(IrColor(0xFFFF0000)),
119+
paths = listOf(
120+
IrPathNode.MoveTo(4f, 4f),
121+
IrPathNode.RelativeHorizontalTo(16f),
122+
),
123+
fillAlpha = 1f,
124+
stroke = null,
125+
),
126+
),
127+
actual = actual,
128+
)
129+
}
130+
68131
@Test
69132
fun parse_SVG_file_with_correct_fill_color() {
70133
val svg = svg {

components/parser/kmp/svg/src/commonTest/kotlin/io/github/composegears/valkyrie/parser/kmp/svg/Utils.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ internal inline fun svg(
1919
height: String = "24px",
2020
viewBox: String? = "0 0 24 24",
2121
fill: String = "#000000",
22+
stroke: String? = null,
23+
strokeWidth: String? = null,
24+
strokeLineCap: String? = null,
25+
strokeLineJoin: String? = null,
2226
block: () -> String,
2327
): String {
2428
return buildString {
@@ -27,6 +31,10 @@ internal inline fun svg(
2731
appendLine("""height="$height"""")
2832
appendLine("""width="$width"""")
2933
if (viewBox != null) appendLine("""viewBox="$viewBox"""")
34+
if (stroke != null) appendLine("""stroke="$stroke"""")
35+
if (strokeWidth != null) appendLine("""stroke-width="$strokeWidth"""")
36+
if (strokeLineCap != null) appendLine("""stroke-linecap="$strokeLineCap"""")
37+
if (strokeLineJoin != null) appendLine("""stroke-linejoin="$strokeLineJoin"""")
3038
appendLine("""fill="$fill">""")
3139
appendLine(block())
3240
appendLine("</svg>")

0 commit comments

Comments
 (0)