diff --git a/changelog.md b/changelog.md index 6582beb9..66b9462b 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/graphic/command/CommandString.kt b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/graphic/command/CommandString.kt index 9bec7fd6..f89c4f7d 100644 --- a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/graphic/command/CommandString.kt +++ b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/graphic/command/CommandString.kt @@ -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) } @@ -145,26 +181,19 @@ value class CommandString( return SmoothCubicBezierCurve.Parameter(endControl, end) } - private fun mapEllipticalArcCurveParameter(components: List): 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*") diff --git a/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/graphic/command/ParserTests.kt b/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/graphic/command/ParserTests.kt index 7b664920..25c364b3 100644 --- a/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/graphic/command/ParserTests.kt +++ b/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/graphic/command/ParserTests.kt @@ -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 @@ -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" @@ -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 @@ -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