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"))
}
@eirirlar
Copy link

eirirlar commented Jul 1, 2021

Which library is Mat33 and Vec2 from?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment