chart-svg-0.3.0: src/Data/Path/Parser.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RebindableSyntax #-}
{-# OPTIONS_GHC -Wall #-}
-- | SVG path manipulation
module Data.Path.Parser
( -- * Parsing
-- $parsing
parsePath,
svgToPathData,
pathDataToSvg,
PathCommand (..),
Origin (..),
)
where
import Chart.Data
import Control.Applicative
import Control.Monad.State.Lazy
import qualified Data.Attoparsec.Text as A
import Data.Either
import Data.FormatN
import Data.Functor
import Data.Path
import Data.Scientific (toRealFloat)
import Data.Text (Text, pack)
import qualified Data.Text as Text
import GHC.Generics
import GHC.OverloadedLabels
import NumHask.Prelude
import Optics.Core hiding ((<|))
-- import qualified Data.List as List
-- $parsing
-- Every element of an svg path can be thought of as exactly two points in space, with instructions of how to draw a curve between them. From this point of view, one which this library adopts, a path chart is thus very similar to a line chart. There's just a lot more information about the style of this line to deal with.
--
-- References:
--
-- [SVG d](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d)
--
-- [SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths)
-- | Parse a raw path string.
--
-- >>> let outerseg1 = "M-1.0,0.5 A0.5 0.5 0.0 1 1 0.0,-1.2320508075688774 1.0 1.0 0.0 0 0 -0.5,-0.3660254037844387 1.0 1.0 0.0 0 0 -1.0,0.5 Z"
-- >>> parsePath outerseg1
-- Right [MoveTo OriginAbsolute [Point -1.0 0.5],EllipticalArc OriginAbsolute [(0.5,0.5,0.0,True,True,Point 0.0 -1.2320508075688774),(1.0,1.0,0.0,False,False,Point -0.5 -0.3660254037844387),(1.0,1.0,0.0,False,False,Point -1.0 0.5)],EndPath]
--
-- https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
parsePath :: Text -> Either String [PathCommand]
parsePath = A.parseOnly pathParser
commaWsp :: A.Parser ()
commaWsp = A.skipSpace *> A.option () (A.string "," $> ()) <* A.skipSpace
point :: A.Parser (Point Double)
point = Point <$> num <* commaWsp <*> num
points :: A.Parser [Point Double]
points = fromList <$> point `A.sepBy1` commaWsp
pointPair :: A.Parser (Point Double, Point Double)
pointPair = (,) <$> point <* commaWsp <*> point
pointPairs :: A.Parser [(Point Double, Point Double)]
pointPairs = fromList <$> pointPair `A.sepBy1` commaWsp
pathParser :: A.Parser [PathCommand]
pathParser = fromList <$> (A.skipSpace *> A.many1 command)
num :: A.Parser Double
num = realToFrac <$> (A.skipSpace *> plusMinus <* A.skipSpace)
where
doubleNumber :: A.Parser Double
doubleNumber = toRealFloat <$> A.scientific <|> shorthand
plusMinus =
negate <$ A.string "-" <*> doubleNumber
<|> A.string "+" *> doubleNumber
<|> doubleNumber
shorthand = process' <$> (A.string "." *> A.many1 A.digit)
process' = fromRight 0 . A.parseOnly doubleNumber . pack . (++) "0."
nums :: A.Parser [Double]
nums = num `A.sepBy1` commaWsp
flag :: A.Parser Bool
flag = fmap (/= '0') A.digit
command :: A.Parser PathCommand
command =
MoveTo OriginAbsolute <$ A.string "M" <*> points
<|> MoveTo OriginRelative <$ A.string "m" <*> points
<|> LineTo OriginAbsolute <$ A.string "L" <*> points
<|> LineTo OriginRelative <$ A.string "l" <*> points
<|> HorizontalTo OriginAbsolute <$ A.string "H" <*> nums
<|> HorizontalTo OriginRelative <$ A.string "h" <*> nums
<|> VerticalTo OriginAbsolute <$ A.string "V" <*> nums
<|> VerticalTo OriginRelative <$ A.string "v" <*> nums
<|> CurveTo OriginAbsolute <$ A.string "C" <*> fmap fromList (manyComma curveToArgs)
<|> CurveTo OriginRelative <$ A.string "c" <*> fmap fromList (manyComma curveToArgs)
<|> SmoothCurveTo OriginAbsolute <$ A.string "S" <*> pointPairs
<|> SmoothCurveTo OriginRelative <$ A.string "s" <*> pointPairs
<|> QuadraticBezier OriginAbsolute <$ A.string "Q" <*> pointPairs
<|> QuadraticBezier OriginRelative <$ A.string "q" <*> pointPairs
<|> SmoothQuadraticBezierCurveTo OriginAbsolute <$ A.string "T" <*> points
<|> SmoothQuadraticBezierCurveTo OriginRelative <$ A.string "t" <*> points
<|> EllipticalArc OriginAbsolute <$ A.string "A" <*> manyComma ellipticalArgs
<|> EllipticalArc OriginRelative <$ A.string "a" <*> manyComma ellipticalArgs
<|> EndPath <$ A.string "Z" <* commaWsp
<|> EndPath <$ A.string "z" <* commaWsp
where
curveToArgs =
(,,) <$> (point <* commaWsp)
<*> (point <* commaWsp)
<*> point
manyComma a = fromList <$> a `A.sepBy1` commaWsp
numComma = num <* commaWsp
flagComma = flag <* commaWsp
ellipticalArgs =
(,,,,,) <$> numComma
<*> numComma
<*> numComma
<*> flagComma
<*> flagComma
<*> point
-- | Path command definition (ripped from reanimate-svg).
data PathCommand
= -- | M or m command
MoveTo !Origin ![Point Double]
| -- | Line to, L or l Svg path command.
LineTo !Origin ![Point Double]
| -- | Equivalent to the H or h svg path command.
HorizontalTo !Origin ![Double]
| -- | Equivalent to the V or v svg path command.
VerticalTo !Origin ![Double]
| -- | Cubic bezier, C or c command
CurveTo !Origin ![(Point Double, Point Double, Point Double)]
| -- | Smooth cubic bezier, equivalent to S or s command
SmoothCurveTo !Origin ![(Point Double, Point Double)]
| -- | Quadratic bezier, Q or q command
QuadraticBezier !Origin ![(Point Double, Point Double)]
| -- | Quadratic bezier, T or t command
SmoothQuadraticBezierCurveTo !Origin ![Point Double]
| -- | Elliptical arc, A or a command.
EllipticalArc !Origin ![(Double, Double, Double, Bool, Bool, Point Double)]
| -- | Close the path, Z or z svg path command.
EndPath
deriving (Eq, Show, Generic)
-- | Tell if a path command is absolute (in the current
-- user coordiante) or relative to the previous point.
data Origin
= -- | Next point in absolute coordinate
OriginAbsolute
| -- | Next point relative to the previous
OriginRelative
deriving (Eq, Show, Generic)
-- | To fit in with the requirements of the library design, specifically the separation of what a chart is into XY data Points from representation of these points, path instructions need to be decontructed into:
--
-- - define a single chart element as a line.
--
-- - split a single path element into the start and end points of the line, which become the 'Chart.Types.xys' of a 'Chart.Types.Chart', and the rest of the information, which is called 'PathInfo' and incorporated into the 'Chart.Types.Chart' 'Chart.Types.annotation'.
--
-- An arc path is variant to affine transformations of the 'Chart.Types.xys' points: angles are not presevred in the new reference frame.
data PathInfo a
= StartI
| LineI
| CubicI (Point a) (Point a)
| QuadI (Point a)
| ArcI (ArcInfo a)
deriving (Show, Eq, Generic)
pointToSvgCoords :: Point Double -> Point Double
pointToSvgCoords (Point x y) = Point x (-y)
svgCoords :: PathData Double -> PathData Double
svgCoords (CubicP a b p) = CubicP (pointToSvgCoords a) (pointToSvgCoords b) (pointToSvgCoords p)
svgCoords (QuadP a p) = QuadP (pointToSvgCoords a) (pointToSvgCoords p)
svgCoords (StartP p) = StartP (pointToSvgCoords p)
svgCoords (LineP p) = LineP (pointToSvgCoords p)
svgCoords (ArcP i p) = ArcP i (pointToSvgCoords p)
-- | Convert from a path info, start point, end point triple to a path text clause.
--
-- Note that morally,
--
-- > toPathsAbsolute . toInfos . parsePath == id
--
-- but the round trip destroys much information, including:
--
-- - path text spacing
--
-- - "Z", which is replaced by a LineI instruction from the end point back to the original start of the path.
--
-- - Sequences of the same instruction type are uncompressed
--
-- - As the name suggests, relative paths are translated to absolute ones.
--
-- - implicit L's in multiple M instructions are separated.
--
-- In converting between chart-svg and SVG there are two changes in reference:
--
-- - arc rotation is expressed as positive degrees for a clockwise rotation in SVG, and counter-clockwise in radians for chart-svg
--
-- - A positive y-direction is down for SVG and up for chart-svg
toPathAbsolute ::
PathData Double ->
-- | path text
Text
toPathAbsolute (StartP p) = "M " <> pp p
toPathAbsolute (LineP p) = "L " <> pp p
toPathAbsolute (CubicP c1 c2 p) =
"C "
<> pp c1
<> " "
<> pp c2
<> " "
<> pp p
toPathAbsolute (QuadP control p) =
"Q "
<> pp control
<> " "
<> pp p
toPathAbsolute (ArcP (ArcInfo (Point x y) phi' l sw) x2) =
"A "
<> (pack . show) x
<> " "
<> (pack . show) y
<> " "
<> (pack . show) (-phi' * 180 / pi)
<> " "
<> bool "0" "1" l
<> " "
<> bool "0" "1" sw
<> " "
<> pp x2
-- | Render a point (including conversion to SVG Coordinates).
pp :: Point Double -> Text
pp (Point x y) =
formatOrShow (FixedStyle 4) Nothing x <> ","
<> formatOrShow (FixedStyle 4) Nothing (bool (-y) y (y == zero))
data PathCursor = PathCursor
{ -- | previous position
curPrevious :: Point Double,
-- | start point (to close out the path)
curStart :: Point Double,
-- | last control point
curControl :: Maybe (Point Double)
}
deriving (Eq, Show, Generic)
stateCur0 :: PathCursor
stateCur0 = PathCursor zero zero Nothing
-- | Convert an SVG d path text snippet to a [PathData Double]
svgToPathData :: Text -> [PathData Double]
svgToPathData = toPathDatas . either error id . parsePath
-- | Convert [PathData] to an SVG d path text.
pathDataToSvg :: [PathData Double] -> Text
pathDataToSvg xs = Text.intercalate " " $ fmap toPathAbsolute xs
-- | Convert from a path command list to a PathA specification
toPathDatas :: [PathCommand] -> [PathData Double]
toPathDatas xs = fmap svgCoords $ mconcat $ flip evalState stateCur0 $ sequence $ toInfo <$> xs
-- | Convert relative points to absolute points
relToAbs :: (Additive a) => a -> [a] -> [a]
relToAbs p xs = accsum (p : xs)
moveTo :: [Point Double] -> State PathCursor [PathData Double]
moveTo xs = do
put (PathCursor (last xs) (head xs) Nothing)
pure (StartP (head xs) : (LineP <$> tail xs))
lineTo :: [Point Double] -> State PathCursor [PathData Double]
lineTo xs = do
modify ((#curPrevious .~ last xs) . (#curControl .~ Nothing))
pure $ LineP <$> xs
horTo :: [Double] -> State PathCursor [PathData Double]
horTo xs = do
(PathCursor (Point _ y) _ _) <- get
lineTo (fmap (`Point` y) xs)
verTo :: [Double] -> State PathCursor [PathData Double]
verTo ys = do
(PathCursor (Point x _) _ _) <- get
lineTo (fmap (Point x) ys)
curveTo :: [(Point Double, Point Double, Point Double)] -> State PathCursor [PathData Double]
curveTo xs = do
modify
( (#curPrevious .~ (\(_, _, p) -> p) (last xs))
. (#curControl ?~ (\(_, c2, _) -> c2) (last xs))
)
pure $ (\(c1, c2, x2) -> CubicP c1 c2 x2) <$> xs
-- | Convert relative points to absolute points
relToAbs3 :: Additive a => a -> [(a, a, a)] -> [(a, a, a)]
relToAbs3 p xs = xs'
where
x1 = (\(x, _, _) -> x) <$> xs
x2 = (\(_, x, _) -> x) <$> xs
x3 = (\(_, _, x) -> x) <$> xs
x1' = fmap (p +) (accsum x1)
x2' = fmap (p +) (accsum x2)
x3' = fmap (p +) (accsum x3)
xs' = zip3 x1' x2' x3'
reflControlPoint :: State PathCursor (Point Double)
reflControlPoint = do
(PathCursor p _ c) <- get
case c of
Nothing -> pure p
Just c' -> pure (p - (c' - p))
smoothCurveToStep :: (Point Double, Point Double) -> State PathCursor (PathData Double)
smoothCurveToStep (c2, x2) = do
c1 <- reflControlPoint
modify ((#curControl ?~ c2) . (#curPrevious .~ x2))
pure (CubicP c1 c2 x2)
smoothCurveTo :: [(Point Double, Point Double)] -> State PathCursor [PathData Double]
smoothCurveTo xs =
sequence (smoothCurveToStep <$> xs)
-- | Convert relative points to absolute points
relToAbs2 :: Additive a => a -> [(a, a)] -> [(a, a)]
relToAbs2 p xs = xs'
where
x1 = fst <$> xs
x2 = snd <$> xs
x1' = fmap (p +) (accsum x1)
x2' = fmap (p +) (accsum x2)
xs' = zip x1' x2'
quad :: [(Point Double, Point Double)] -> State PathCursor [PathData Double]
quad xs = do
modify
( (#curPrevious .~ snd (last xs))
. (#curControl ?~ fst (last xs))
)
pure $ uncurry QuadP <$> xs
smoothQuadStep :: Point Double -> State PathCursor (PathData Double)
smoothQuadStep x2 = do
c1 <- reflControlPoint
modify ((#curControl ?~ c1) . (#curPrevious .~ x2))
pure (QuadP c1 x2)
smoothQuad :: [Point Double] -> State PathCursor [PathData Double]
smoothQuad xs =
sequence (smoothQuadStep <$> xs)
arcTo :: [(Double, Double, Double, Bool, Bool, Point Double)] -> State PathCursor [PathData Double]
arcTo xs = do
modify ((#curPrevious .~ (\(_, _, _, _, _, p) -> p) (last xs)) . (#curControl .~ Nothing))
pure $ fromPathEllipticalArc <$> xs
fromPathEllipticalArc :: (a, a, a, Bool, Bool, Point a) -> PathData a
fromPathEllipticalArc (x, y, r, l, s, p) = ArcP (ArcInfo (Point x y) r l s) p
-- | Convert relative points to absolute points
relToAbsArc :: Additive a => Point a -> [(a, a, a, Bool, Bool, Point a)] -> [(a, a, a, Bool, Bool, Point a)]
relToAbsArc p xs = xs'
where
ps = (\(_, _, _, _, _, pt) -> pt) <$> xs
ps' = fmap (p +) (accsum ps)
xs' = zipWith (\(x0, x1, x2, x3, x4, _) pt -> (x0, x1, x2, x3, x4, pt)) xs ps'
-- | Convert a path command fragment to PathData
--
-- flips the y-dimension of points.
toInfo :: PathCommand -> State PathCursor [PathData Double]
toInfo (MoveTo OriginAbsolute xs) = moveTo xs
toInfo (MoveTo OriginRelative xs) = do
(PathCursor p _ _) <- get
moveTo (relToAbs p xs)
toInfo EndPath = do
(PathCursor _ s _) <- get
pure [LineP s]
toInfo (LineTo OriginAbsolute xs) = lineTo xs
toInfo (LineTo OriginRelative xs) = do
(PathCursor p _ _) <- get
lineTo (relToAbs p xs)
toInfo (HorizontalTo OriginAbsolute xs) = horTo xs
toInfo (HorizontalTo OriginRelative xs) = do
(PathCursor (Point x _) _ _) <- get
horTo (relToAbs x xs)
toInfo (VerticalTo OriginAbsolute xs) = verTo xs
toInfo (VerticalTo OriginRelative ys) = do
(PathCursor (Point _ y) _ _) <- get
verTo (relToAbs y ys)
toInfo (CurveTo OriginAbsolute xs) = curveTo xs
toInfo (CurveTo OriginRelative xs) = do
(PathCursor p _ _) <- get
curveTo (relToAbs3 p xs)
toInfo (SmoothCurveTo OriginAbsolute xs) = smoothCurveTo xs
toInfo (SmoothCurveTo OriginRelative xs) = do
(PathCursor p _ _) <- get
smoothCurveTo (relToAbs2 p xs)
toInfo (QuadraticBezier OriginAbsolute xs) = quad xs
toInfo (QuadraticBezier OriginRelative xs) = do
(PathCursor p _ _) <- get
quad (relToAbs2 p xs)
toInfo (SmoothQuadraticBezierCurveTo OriginAbsolute xs) = smoothQuad xs
toInfo (SmoothQuadraticBezierCurveTo OriginRelative xs) = do
(PathCursor p _ _) <- get
smoothQuad (relToAbs p xs)
toInfo (EllipticalArc OriginAbsolute xs) = arcTo xs
toInfo (EllipticalArc OriginRelative xs) = do
(PathCursor p _ _) <- get
arcTo (relToAbsArc p xs)