Skip to content

Commit

Permalink
Parse arc flag parameter shorthand (#125)
Browse files Browse the repository at this point in the history
* Parse arc flags without digit separator

* Add another failing case

* Handle multiple arc parameters using flag shorthand

* Add a changelog entry
  • Loading branch information
jzbrooks authored Jan 18, 2025
1 parent c98a9af commit f955bd4
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 39 deletions.
11 changes: 1 addition & 10 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,8 @@

## Unreleased

### Added

### Changed

### Deprecated

### Removed

### Fixed

### Security
- Incorrect parsing of elliptical arc parameters when no separator was present between flag parameters

## 3.0.0 - 2024-11-30

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,49 @@ value class CommandString(
}
command.startsWith('A', true) -> {
val parameters =
number
.findAll(command)
.map(MatchResult::value)
.map(String::toFloat)
.chunked(7)
.map(::mapEllipticalArcCurveParameter)
.toList()
buildList {
val parameterValues = number.findAll(command).map(MatchResult::value).toMutableList()
while (parameterValues.size > 5) {
if (parameterValues[3].length > 1) {
val flagShorthand = parameterValues[3]

add(
EllipticalArcCurve.Parameter(
parameterValues.removeFirst().toFloat(),
parameterValues.removeFirst().toFloat(),
parameterValues.removeFirst().toFloat(),
mapArcFlag(flagShorthand[0].toString().toFloat()).also {
parameterValues.removeFirst()
},
mapSweepFlag(flagShorthand[1].toString().toFloat()),
Point(
parameterValues.removeFirst().toFloat(),
parameterValues.removeFirst().toFloat(),
),
),
)
} else {
check(parameterValues.size > 6) {
"$parameterValues must have 7 distinct numerical arguments if the" +
" flags are not combined without an argument separator (space or comma)"
}

add(
EllipticalArcCurve.Parameter(
parameterValues.removeFirst().toFloat(),
parameterValues.removeFirst().toFloat(),
parameterValues.removeFirst().toFloat(),
mapArcFlag(parameterValues.removeFirst().toFloat()),
mapSweepFlag(parameterValues.removeFirst().toFloat()),
Point(
parameterValues.removeFirst().toFloat(),
parameterValues.removeFirst().toFloat(),
),
),
)
}
}
}

EllipticalArcCurve(variant, parameters)
}
Expand Down Expand Up @@ -145,26 +181,19 @@ value class CommandString(
return SmoothCubicBezierCurve.Parameter(endControl, end)
}

private fun mapEllipticalArcCurveParameter(components: List<Float>): EllipticalArcCurve.Parameter {
val radiusX = components[0]
val radiusY = components[1]
val angle = components[2]
val arcFlag =
when (components[3]) {
1f -> EllipticalArcCurve.ArcFlag.LARGE
0f -> EllipticalArcCurve.ArcFlag.SMALL
else -> throw IllegalArgumentException("Unexpected elliptical curve arc flag value: ${components[4]}\nExpected 0 or 1.")
}
val sweepFlag =
when (components[4]) {
1f -> EllipticalArcCurve.SweepFlag.CLOCKWISE
0f -> EllipticalArcCurve.SweepFlag.ANTICLOCKWISE
else -> throw IllegalArgumentException("Unexpected elliptical curve sweep flag value: ${components[4]}\nExpected 0 or 1.")
}
val end = Point(components[5], components[6])

return EllipticalArcCurve.Parameter(radiusX, radiusY, angle, arcFlag, sweepFlag, end)
}
private fun mapArcFlag(value: Float): EllipticalArcCurve.ArcFlag =
when (value) {
1f -> EllipticalArcCurve.ArcFlag.LARGE
0f -> EllipticalArcCurve.ArcFlag.SMALL
else -> throw IllegalArgumentException("Unexpected elliptical curve arc flag value: $value\nExpected 0 or 1.")
}

private fun mapSweepFlag(value: Float): EllipticalArcCurve.SweepFlag =
when (value) {
1f -> EllipticalArcCurve.SweepFlag.CLOCKWISE
0f -> EllipticalArcCurve.SweepFlag.ANTICLOCKWISE
else -> throw IllegalArgumentException("Unexpected elliptical curve sweep flag value: $value\nExpected 0 or 1.")
}

companion object {
private val commandRegex = Regex("(?=[MmLlHhVvCcSsQqTtAaZz])\\s*")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.containsOnly
import assertk.assertions.first
import assertk.assertions.hasClass
import assertk.assertions.index
import assertk.assertions.isEqualTo
Expand Down Expand Up @@ -200,6 +201,62 @@ class ParserTests {
)
}

@Test
fun testArcParameterWithoutCommaBetweenFlags() {
val pathCommandString = "M1,1 A 1,1,0,01,3,3"

val commands = CommandString(pathCommandString).toCommandList()

assertThat(commands).containsExactly(
moveToSingle,
EllipticalArcCurve(
CommandVariant.ABSOLUTE,
listOf(
EllipticalArcCurve.Parameter(
1f,
1f,
0f,
EllipticalArcCurve.ArcFlag.SMALL,
EllipticalArcCurve.SweepFlag.CLOCKWISE,
Point(3f, 3f),
),
),
),
)
}

@Test
fun testMultipleArcParametersWithoutCommaBetweenFlags() {
val pathCommandString = "M1,1 A 1,1,0,01,3,3 2,2,0,10,4,4"

val commands = CommandString(pathCommandString).toCommandList()

assertThat(commands).containsExactly(
moveToSingle,
EllipticalArcCurve(
CommandVariant.ABSOLUTE,
listOf(
EllipticalArcCurve.Parameter(
1f,
1f,
0f,
EllipticalArcCurve.ArcFlag.SMALL,
EllipticalArcCurve.SweepFlag.CLOCKWISE,
Point(3f, 3f),
),
EllipticalArcCurve.Parameter(
2f,
2f,
0f,
EllipticalArcCurve.ArcFlag.LARGE,
EllipticalArcCurve.SweepFlag.ANTICLOCKWISE,
Point(4f, 4f),
),
),
),
)
}

@Test
fun testParseRelativeCommandString() {
val pathCommandString = "l2 5"
Expand All @@ -220,7 +277,7 @@ class ParserTests {

val lineCommand = commands[0] as LineTo

assertThat(lineCommand.parameters[0]).isEqualTo(Point(2.1f, 5f))
assertThat(lineCommand::parameters).first().isEqualTo(Point(2.1f, 5f))
}

@Test
Expand All @@ -231,7 +288,7 @@ class ParserTests {

val lineCommand = commands[0] as LineTo

assertThat(lineCommand.parameters[0]).isEqualTo(Point(200f, 5f))
assertThat(lineCommand::parameters).first().isEqualTo(Point(200f, 5f))
}

@Test
Expand Down

0 comments on commit f955bd4

Please sign in to comment.