Skip to content

Instantly share code, notes, and snippets.

@nornagon
Last active July 1, 2021 20:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nornagon/96c9df8aee663d1f8985e481e1a1c567 to your computer and use it in GitHub Desktop.
Save nornagon/96c9df8aee663d1f8985e481e1a1c567 to your computer and use it in GitHub Desktop.
SVG path and transform parsers in Scala, using fastparse
// License: GPLv3
// Email me and I'll grant you a free eternal license to use it however you want! nornagon@nornagon.net
object SVGMicroSyntax {
// https://github.com/lihaoyi/fastparse
import fastparse.all._
object PathData {
sealed trait PathCommand
sealed trait DrawToCommand extends PathCommand
case class MoveTo(coords: Seq[Vec2], relative: Boolean) extends PathCommand
case class ClosePath() extends DrawToCommand
case class LineTo(coords: Seq[Vec2], relative: Boolean) extends DrawToCommand
case class HorizontalLineTo(xs: Seq[Double], relative: Boolean) extends DrawToCommand
case class VerticalLineTo(ys: Seq[Double], relative: Boolean) extends DrawToCommand
case class CurveTo(coords: Seq[(Vec2, Vec2, Vec2)], relative: Boolean) extends DrawToCommand
case class SmoothCurveTo(coords: Seq[(Vec2, Vec2)], relative: Boolean) extends DrawToCommand
case class QuadraticBezierCurveTo(coords: Seq[(Vec2, Vec2)], relative: Boolean) extends DrawToCommand
case class SmoothQuadraticBezierCurveTo(coords: Seq[Vec2], relative: Boolean) extends DrawToCommand
case class EllipticalArc(coords: Seq[(Vec2, Double, Boolean, Boolean, Vec2)], relative: Boolean) extends DrawToCommand
// https://www.w3.org/TR/SVG/paths.html#PathData
lazy val `svg-path`: Parser[Seq[(MoveTo, Seq[DrawToCommand])]] = P(wsp.rep ~ `moveto-drawto-command-groups`.?.map(_.getOrElse(Seq.empty)) ~ wsp.rep)
lazy val `moveto-drawto-command-groups` = P(`moveto-drawto-command-group`.rep(min = 1, sep = wsp.rep))
lazy val `moveto-drawto-command-group` = P(moveto ~ wsp.rep ~ `drawto-commands`.?.map(_.getOrElse(Seq.empty)))
lazy val `drawto-commands` = P(`drawto-command`.rep(min = 1, sep = wsp.rep))
lazy val `drawto-command`: Parser[DrawToCommand] = P(
closepath
| lineto
| `horizontal-lineto`
| `vertical-lineto`
| curveto
| `smooth-curveto`
| `quadratic-bezier-curveto`
| `smooth-quadratic-bezier-curveto`
| `elliptical-arc`
)
lazy val `moveto` = P(CharIn("Mm").! ~/ wsp.rep ~ `moveto-argument-sequence`).map {
case (c, coords) => MoveTo(coords, c.head.isLower) }
lazy val `moveto-argument-sequence` = P(`coordinate-pair`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `closepath` = P(CharIn("Zz").!).map(_ => ClosePath())
lazy val `lineto` = P(CharIn("Ll").! ~/ wsp.rep ~ `lineto-argument-sequence`).map {
case (c, coords) => LineTo(coords, c.head.isLower) }
lazy val `lineto-argument-sequence` = P(`coordinate-pair`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `horizontal-lineto` = P(CharIn("Hh").! ~/ wsp.rep ~ `horizontal-lineto-argument-sequence`).map {
case (c, coords) => HorizontalLineTo(coords, c.head.isLower)
}
lazy val `horizontal-lineto-argument-sequence` = P(coordinate.rep(min = 1, sep = `comma-wsp`.?))
lazy val `vertical-lineto` = P(CharIn("Vv").! ~/ wsp.rep ~ `vertical-lineto-argument-sequence`).map {
case (c, coords) => VerticalLineTo(coords, c.head.isLower)
}
lazy val `vertical-lineto-argument-sequence` = P(coordinate.rep(min = 1, sep = `comma-wsp`.?))
lazy val `curveto` = P(CharIn("Cc").! ~/ wsp.rep ~ `curveto-argument-sequence`).map {
case (c, coords) => CurveTo(coords, c.head.isLower)
}
lazy val `curveto-argument-sequence` = P(`curveto-argument`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `curveto-argument` = P(`coordinate-pair` ~ `comma-wsp`.? ~ `coordinate-pair` ~ `comma-wsp`.? ~ `coordinate-pair`)
lazy val `smooth-curveto` = P(CharIn("Ss").! ~/ wsp.rep ~ `smooth-curveto-argument-sequence`).map {
case (c, coords) => SmoothCurveTo(coords, c.head.isLower)
}
lazy val `smooth-curveto-argument-sequence` = P(`smooth-curveto-argument`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `smooth-curveto-argument` = P(`coordinate-pair` ~ `comma-wsp`.? ~ `coordinate-pair`)
lazy val `quadratic-bezier-curveto` = P(CharIn("Qq").! ~/ wsp.rep ~ `quadratic-bezier-curveto-argument-sequence`).map {
case (c, coords) => QuadraticBezierCurveTo(coords, c.head.isLower)
}
lazy val `quadratic-bezier-curveto-argument-sequence` = P(`quadratic-bezier-curveto-argument`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `quadratic-bezier-curveto-argument` = P(`coordinate-pair` ~ `comma-wsp`.? ~ `coordinate-pair`)
lazy val `smooth-quadratic-bezier-curveto` = P(CharIn("Tt").! ~/ wsp.rep ~ `smooth-quadratic-bezier-curveto-argument-sequence`).map {
case (c, coords) => SmoothQuadraticBezierCurveTo(coords, c.head.isLower)
}
lazy val `smooth-quadratic-bezier-curveto-argument-sequence` = P(`coordinate-pair`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `elliptical-arc` = P(CharIn("Aa").! ~/ wsp.rep ~ `elliptical-arc-argument-sequence`).map {
case (c, coords) => EllipticalArc(coords, c.head.isLower)
}
lazy val `elliptical-arc-argument-sequence` = P(`elliptical-arc-argument`.rep(min = 1, sep = `comma-wsp`.?))
lazy val `elliptical-arc-argument` = P(
(`nonnegative-number` ~ `comma-wsp`.? ~ `nonnegative-number`).map { case (x, y) => Vec2(x, y) } ~ `comma-wsp`.?
~ number ~ `comma-wsp` ~ flag ~ `comma-wsp`.? ~ flag ~ `comma-wsp`.? ~ `coordinate-pair`
)
lazy val `coordinate-pair` = P(coordinate ~ `comma-wsp`.? ~ coordinate).map { case (x, y) => Vec2(x, y) }
lazy val coordinate = number
lazy val `nonnegative-number` = P(`integer-constant` | `floating-point-constant`).!.map(_.toDouble)
}
object Transform {
// https://www.w3.org/TR/SVG/coords.html#TransformAttribute
lazy val `transform-list` = P(wsp.rep ~ transforms.?.map(_.getOrElse(Mat33.identity)) ~ wsp.rep)
lazy val transforms = P(transform.rep(min = 1, sep = `comma-wsp`.rep(min = 1))).map { ms =>
ms.foldLeft(Mat33.identity) { (m, o) => m * o }
}
lazy val transform = P(matrix | translate | scale | rotate | skewX | skewY)
lazy val matrix = P("matrix" ~/ wsp.rep ~ "(" ~/ wsp.rep
~ number ~ `comma-wsp`
~ number ~ `comma-wsp`
~ number ~ `comma-wsp`
~ number ~ `comma-wsp`
~ number ~ `comma-wsp`
~ number ~ wsp.rep ~ ")").map {
case (a, b, c, d, e, f) => Mat33(a, b, c, d, e, f, 0, 0, 1)
}
lazy val translate = P("translate" ~/ wsp.rep ~ "(" ~/ wsp.rep ~ number ~ (`comma-wsp` ~ number).? ~ wsp.rep ~ ")").map {
case (tx, ty) => Mat33.translate(tx, ty.getOrElse(0))
}
lazy val scale = P("scale" ~/ wsp.rep ~ "(" ~/ wsp.rep ~ number ~ (`comma-wsp` ~ number).? ~ wsp.rep ~ ")").map {
case (sx, sy) => Mat33.scale(sx, sy.getOrElse(sx))
}
lazy val rotate = P("rotate" ~/ wsp.rep ~ "(" ~/ wsp.rep ~ number ~ (`comma-wsp` ~ number ~ `comma-wsp` ~ number).? ~ wsp.rep ~ ")").map {
case (angle, Some((cx, cy))) => Mat33.translate(-cx, -cy) * Mat33.rotate(angle) * Mat33.translate(cx, cy) // TODO: not sure if this should be flipped?
case (angle, None) => Mat33.rotate(angle)
}
lazy val skewX = P("skewX" ~/ wsp.rep ~ "(" ~/ wsp.rep ~ number ~ wsp.rep ~ ")").map { angle => Mat33.skewX(angle) }
lazy val skewY = P("skewY" ~/ wsp.rep ~ "(" ~/ wsp.rep ~ number ~ wsp.rep ~ ")").map { angle => Mat33.skewX(angle) }
}
lazy val number = P((sign.? ~ `integer-constant`) | (sign.? ~ `floating-point-constant`)).!.map(_.toDouble)
lazy val flag = P(CharIn("01")).!.map(_ == "1")
lazy val `comma-wsp` = P((wsp.rep(1) ~ comma.? ~ wsp.rep) | (comma ~ wsp.rep))
lazy val comma = P(",")
lazy val `integer-constant` = P(`digit-sequence`)
lazy val `floating-point-constant` = P((`fractional-constant` ~ exponent.?) | (`digit-sequence` ~ exponent))
lazy val `fractional-constant` = P((`digit-sequence`.? ~ "." ~ `digit-sequence`) | (`digit-sequence` ~ "."))
lazy val exponent = P(CharIn("eE") ~ sign.? ~ `digit-sequence`)
lazy val sign = P(CharIn("+-"))
lazy val `digit-sequence` = P(digit.rep(1))
lazy val digit = P(CharIn("0123456789"))
lazy val wsp = P(CharIn("\u0020\u0009\u000D\u000A"))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment