packages feed

waterfall-cad-svg-0.6.3.0: src/Waterfall/SVG/FromSVG.hs

{-# LANGUAGE TypeApplications #-}
{-|
Load [SVG Data](https://developer.mozilla.org/en-US/docs/Web/SVG) into `Waterfall.Path2D`
-}
module Waterfall.SVG.FromSVG
( SVGErrorKind (..)
, SVGError (..)
, convertPathCommands
, parsePath
, convertTransform
, convertTree
, convertDocument
, readSVG
) where

import qualified Waterfall
import qualified Data.Attoparsec.Text as Atto
import Graphics.Svg.PathParser (pathParser)
import qualified Graphics.Svg as Svg
import qualified Data.Text as T
import Linear (V3 (..), V2 (..), zero, Metric (norm), normalize, (^*), (*^), _x, _y, unit)
import Control.Lens ((^.), ala, each)
import Data.Monoid (Endo (..))
import Control.Arrow (second)
import Prelude hiding (Foldable(..))
import Data.Foldable (Foldable(..))
import Control.Exception (IOException, try)
import Control.Monad (join, (<=<))
import Data.Bifunctor (first)
import Data.Maybe (catMaybes)
import Data.Function ((&))

-- | Categories of error that may occur when processing an SVG
data SVGErrorKind
    = SVGIOError
    | SVGTreeError
    | SVGParseError
    | SVGPathError
    | SVGTransformError
    | SVGNumberError
    deriving (Eq, Ord, Show)

-- | Type representing an error that occured when processing an SVG
data SVGError = SVGError SVGErrorKind String
        deriving (Eq, Ord, Show)

uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
uncurry6 fn (a, b, c, d, e, f) = fn a b c d e f

pathFromToWithControlPoint :: [Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))] -> V2 Double -> Either SVGError (V2 Double, Waterfall.Path2D)
pathFromToWithControlPoint commands start = 
    let go (cp, (pos, paths)) cmd = second (second (:paths)) <$> cmd cp pos
        go' b a = join (go <$> b <*> pure a)
    in case foldl' go' (Right (Nothing, (start, []))) commands of
        Right (_cp, (end, allPaths)) -> Right (end, mconcat . reverse $ allPaths)
        Left err -> Left err

ellipseToRelative :: Double -> Double -> Double -> Bool -> Bool -> V2 Double -> V2 Double -> (V2 Double, Waterfall.Path2D)
ellipseToRelative rx ry angleDeg largeArcFlag sweepFlag relativeEnd =
    let angleRads = angleDeg * pi / 180
        scaleFac = ry / rx
        transformForward :: Waterfall.Transformable2D a => a -> a
        transformForward = Waterfall.rotate2D (angleRads) . Waterfall.scale2D (V2 (1/scaleFac) 1)
        transformBack :: Waterfall.Transformable2D a => a -> a
        transformBack = Waterfall.scale2D (V2 (scaleFac) 1) . Waterfall.rotate2D (-angleRads)
        relativeEndTransformed@(V2 retX retY) = transformBack relativeEnd
        transformedDistance = norm relativeEndTransformed
        halfTD = transformedDistance * 0.5
        perp = normalize (V2 (-retY) retX)
        p1 = if sweepFlag == largeArcFlag then negate perp else perp
        p2 = if largeArcFlag then p1 else negate p1
        radius = max ry halfTD 
        centerPerpDistance = sqrt (radius * radius - halfTD * halfTD)
        center = (relativeEndTransformed ^* 0.5) + (p1 ^* centerPerpDistance)
        midPoint = center + (p2 ^* radius)
        
        in Waterfall.splice . transformForward $ Waterfall.arcVia zero midPoint relativeEndTransformed 

quadraticBezierAbsolute :: V2 Double -> V2 Double -> V2 Double -> (V2 Double, Waterfall.Path2D)
quadraticBezierAbsolute p0 p1 p2 = (p2, Waterfall.bezier2D p0 (p0 + ((p1 - p0) ^* (2/3))) (p2 + ((p1 - p2) ^* (2/3))) p2)

curveToAbsolute :: (V2 Double, V2 Double, V2 Double) -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
curveToAbsolute (cp1, cp2, cp3) _ cp0 = Right (Just (cp3 + cp3 - cp2), Waterfall.bezierTo2D cp1 cp2 cp3 cp0)

curveToRelative :: (V2 Double, V2 Double, V2 Double) -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
curveToRelative (cp1, cp2, cp3) _ cp0 = curveToAbsolute (cp0 + cp1, cp0 + cp2, cp0 + cp3) Nothing cp0

quadraticBezierAbsolute' :: (V2 Double, V2 Double) -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
quadraticBezierAbsolute' (cp1, cp2) _ cp0 = Right (Just (cp2 + cp2 - cp1), quadraticBezierAbsolute cp0 cp1 cp2)

quadraticBezierRelative' :: (V2 Double, V2 Double) -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
quadraticBezierRelative' (cp1, cp2) _ cp0 = quadraticBezierAbsolute' (cp0 + cp1, cp0 + cp2) Nothing cp0

ellipseToAbsolute :: Double -> Double -> Double -> Bool -> Bool -> V2 Double -> V2 Double -> (V2 Double, Waterfall.Path2D)
ellipseToAbsolute rx ry angleDeg largeArcFlag sweepFlag absoluteEnd start =
    ellipseToRelative rx ry angleDeg largeArcFlag sweepFlag (absoluteEnd - start) start

smoothCurveToAbsolute :: (V2 Double, V2 Double) -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
smoothCurveToAbsolute _ Nothing _ = Left (SVGError SVGPathError "S command must follow either an S, s, C or c command")
smoothCurveToAbsolute (cp2, cp3) (Just cp1) cp0 = Right (Just (cp3 + cp3 - cp2), Waterfall.bezierTo2D cp1 cp2 cp3 cp0) 

smoothCurveToRelative ::  (V2 Double, V2 Double) -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
smoothCurveToRelative _ Nothing _ = Left (SVGError SVGPathError "s command must follow either an S, s, C or c command")
smoothCurveToRelative (cp2, cp3) cp1 cp0 = smoothCurveToAbsolute (cp0 + cp2, cp0 + cp3) cp1 cp0

smoothQuadraticBezierCurveToAbsolute :: V2 Double -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
smoothQuadraticBezierCurveToAbsolute _ Nothing _ = Left (SVGError SVGPathError "T command must follow either an T, t, Q or q command")
smoothQuadraticBezierCurveToAbsolute cp2 (Just cp1) cp0 = Right (Just (cp2 + cp2 - cp1), quadraticBezierAbsolute cp0 cp1 cp2)

smoothQuadraticBezierCurveToRelative :: V2 Double -> Maybe (V2 Double) -> V2 Double -> Either SVGError (Maybe (V2 Double), (V2 Double, Waterfall.Path2D))
smoothQuadraticBezierCurveToRelative _ Nothing _ = Left (SVGError SVGPathError "t command must follow either an T, t, Q or q command")
smoothQuadraticBezierCurveToRelative cp2 cp1 cp0 = smoothQuadraticBezierCurveToRelative (cp0 + cp2) cp1 cp0

-- | Generate `Waterfall.Path2D`s from a parsed list of `Svg.PathCommand`s.
-- 
-- Consective `Svg.PathCommands` will be merged into the same `Waterfall.Path2D` 
-- unless either a move command ('m', 'M') or a close path command ('z', 'Z') is encountered.
convertPathCommands :: [Svg.PathCommand] -> Either SVGError [Waterfall.Path2D]
convertPathCommands cs =
    let
        relativeLocation _ Svg.OriginAbsolute v = v
        relativeLocation curPos Svg.OriginRelative v = curPos + v
        buildPathInProgress (origin, segments) = 
            pathFromToWithControlPoint segments origin
        withoutControlPoint f _cp o = Right (Nothing, f o)
        go (cmd:rest) pathInProgress@(o, segments) paths = 
            let goSegment ss = go rest (o, segments <> ss) paths  
            in case cmd of
                (Svg.MoveTo origin (v:vs)) ->
                    let restPlusImplicitLineTo =
                            case vs of
                                [] -> rest
                                implicitLineTos -> Svg.LineTo origin implicitLineTos : rest
                    in if null segments
                        then go restPlusImplicitLineTo (relativeLocation o origin v, []) paths
                        else case buildPathInProgress pathInProgress of
                            Right (currentPosition, newPath) ->  go restPlusImplicitLineTo (relativeLocation currentPosition origin v, []) (newPath : paths)
                            Left err -> Left err
                (Svg.MoveTo _ []) -> Left (SVGError SVGPathError "Empty MoveTo command")
                (Svg.LineTo Svg.OriginAbsolute vs) -> goSegment (withoutControlPoint . Waterfall.lineTo2D <$> vs )
                (Svg.LineTo Svg.OriginRelative vs) -> goSegment (withoutControlPoint . Waterfall.lineRelative2D <$> vs)
                (Svg.HorizontalTo Svg.OriginAbsolute ds) -> 
                    let f d v@(V2 _x y) = let v' = V2 d y in (v', Waterfall.line2D v v')
                        in goSegment (withoutControlPoint . f <$> ds) 
                (Svg.HorizontalTo Svg.OriginRelative ds) -> 
                    let f d v = let v' = v + V2 d 0 in (v', Waterfall.line2D v v')
                    in goSegment (withoutControlPoint . f <$> ds)
                (Svg.VerticalTo Svg.OriginAbsolute ds) -> 
                    let f d v@(V2 x _y) = let v' = V2 x d in (v', Waterfall.line2D v v')
                     in goSegment (withoutControlPoint . f <$> ds) 
                (Svg.VerticalTo Svg.OriginRelative ds) -> 
                    let f d v = let v' = v + V2 0 d in (v', Waterfall.line2D v v')
                     in goSegment (withoutControlPoint . f <$> ds) 
                (Svg.CurveTo Svg.OriginAbsolute points) -> goSegment (curveToAbsolute <$> points)
                (Svg.CurveTo Svg.OriginRelative points) -> goSegment (curveToRelative <$> points)
                (Svg.EllipticalArc Svg.OriginAbsolute points) -> goSegment (withoutControlPoint . uncurry6 ellipseToAbsolute <$> points)
                (Svg.EllipticalArc Svg.OriginRelative points) -> goSegment (withoutControlPoint . uncurry6 ellipseToRelative <$> points)
                Svg.QuadraticBezier Svg.OriginAbsolute points -> goSegment (quadraticBezierAbsolute' <$> points)
                Svg.QuadraticBezier Svg.OriginRelative points -> goSegment (quadraticBezierRelative' <$> points)
                Svg.SmoothCurveTo Svg.OriginAbsolute points -> goSegment (smoothCurveToAbsolute <$> points)
                Svg.SmoothCurveTo Svg.OriginRelative points -> goSegment (smoothCurveToRelative <$> points)
                Svg.SmoothQuadraticBezierCurveTo Svg.OriginAbsolute points -> goSegment (smoothQuadraticBezierCurveToAbsolute <$> points)
                Svg.SmoothQuadraticBezierCurveTo Svg.OriginRelative points -> goSegment (smoothQuadraticBezierCurveToRelative <$> points)
                Svg.EndPath -> 
                    if null segments 
                        then go rest (o, []) paths
                        else case buildPathInProgress pathInProgress of
                                 Right (_, newPath) -> go rest (o, []) (Waterfall.closeLoop newPath : paths)
                                 Left err -> Left err
        go [] pathInProgress@(_o, segments) paths = 
            if null segments 
                then Right paths
                else (:paths) . snd <$> buildPathInProgress pathInProgress
    in reverse <$> go cs (zero, []) []

-- | Parse [SVG Path data](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) 
-- and convert it into a `Path2D`
parsePath :: String -> Either SVGError [Waterfall.Path2D]
parsePath s =
    case Atto.parseOnly (pathParser <* Atto.endOfInput) (T.pack s) of 
        Right r -> convertPathCommands r
        Left msg -> Left (SVGError SVGParseError msg)

-- | Parse a `Svg.Transformation` into a function that can be applied to 
-- any Waterfall type with a `Waterfall.Transformable2D` instance
-- 
-- This should handle every case except for `TransformUnknown`
convertTransform :: Waterfall.Transformable2D a => Svg.Transformation -> Either SVGError (a -> a)
convertTransform (Svg.TransformMatrix a b c d e f) = Right $ Waterfall.matTransform2D (V2 (V3 a c e) (V3 b d f))
convertTransform (Svg.Translate x y) = Right $ Waterfall.translate2D (V2 x y)
convertTransform (Svg.Scale v Nothing) = Right $ Waterfall.uScale2D v
convertTransform (Svg.Scale x (Just y)) = Right $ Waterfall.scale2D (V2 x y)
convertTransform (Svg.Rotate angleDeg center) = 
    let center' = maybe zero (uncurry V2) center 
        fwd = Waterfall.translate2D (negate center')
        angleRad = angleDeg * pi / 180
        back = Waterfall.translate2D center'
     in Right (back . Waterfall.rotate2D angleRad . fwd)
convertTransform (Svg.SkewX x) = Right $ Waterfall.matTransform2D (V2 (V3 x 0 0) (V3 0 1 0))
convertTransform (Svg.SkewY y) = Right $ Waterfall.matTransform2D (V2 (V3 1 0 0) (V3 0 y 0))
convertTransform Svg.TransformUnknown = Left . (SVGError SVGTransformError) $ "Unknown Transform"

chain :: [a -> a] -> a -> a
chain = ala Endo foldMap

svgDPI :: Svg.Dpi
svgDPI = 300

convertNumber :: Svg.Number -> Either SVGError Double
convertNumber n = 
    -- toUserUnit should guarantee we either get a Num, Em, or Percent value here
    -- of which only Num is supported
    case Svg.toUserUnit svgDPI n of 
        Svg.Num v -> Right v
        Svg.Px _ -> Left (SVGError SVGNumberError "Unexpected Px value")
        Svg.Em _ -> Left (SVGError SVGNumberError "Unsupported Em value")
        Svg.Percent _ -> Left (SVGError SVGNumberError "Unsupported Percent value")
        Svg.Pc _ ->  Left (SVGError SVGNumberError "Unexpected Pc value")
        Svg.Inches _ -> Left (SVGError SVGNumberError "Unexpected Inches value")
        Svg.Mm _ -> Left (SVGError SVGNumberError "Unexpected Mm value")
        Svg.Cm _ -> Left (SVGError SVGNumberError "Unexpected Cm value")
        Svg.Point _ -> Left (SVGError SVGNumberError "Unexpected Point value")


convertPoint :: Svg.Point -> Either SVGError (V2 Double)
convertPoint = fmap (uncurry V2) . each convertNumber 

convertCircle :: Svg.Circle -> Either SVGError [Waterfall.Path2D]
convertCircle circle = do 
    center <- convertPoint (circle ^. Svg.circleCenter)
    radius <- circle ^. Svg.circleRadius & convertNumber
    return
        . fmap (Waterfall.translate2D center . Waterfall.uScale2D radius)
        . Waterfall.shapePaths 
        $ Waterfall.unitCircle

convertPoints :: [Svg.RPoint] -> [Waterfall.Path2D]
convertPoints (h:t) = pure $ Waterfall.pathFrom h (Waterfall.lineTo <$> t)
convertPoints [] = []

convertPolyLine  :: Svg.PolyLine -> [Waterfall.Path2D]
convertPolyLine polyLine = convertPoints (polyLine ^. Svg.polyLinePoints)

wrap :: [a] -> [a]
wrap (h:t) = h:t <> [h] 
wrap [] = []

convertPolygon  :: Svg.Polygon -> [Waterfall.Path2D]
convertPolygon polygon = convertPoints (polygon ^. Svg.polygonPoints & wrap)

convertLine :: Svg.Line -> Either SVGError Waterfall.Path2D
convertLine line = 
    Waterfall.line 
        <$> convertPoint (line ^. Svg.linePoint1)
        <*> convertPoint (line ^. Svg.linePoint2)
        
convertEllipse :: Svg.Ellipse -> Either SVGError [Waterfall.Path2D]
convertEllipse ellipse = do 
    center <- convertPoint (ellipse ^. Svg.ellipseCenter)
    rX <- ellipse ^. Svg.ellipseXRadius & convertNumber
    rY <- ellipse ^. Svg.ellipseYRadius & convertNumber
    return 
        . fmap (Waterfall.translate2D center . Waterfall.scale2D (V2 rX rY))
        . Waterfall.shapePaths
        $ Waterfall.unitCircle

convertRectangle :: Svg.Rectangle -> Either SVGError [Waterfall.Path2D]
convertRectangle rect = do
    upperLeft <- convertPoint (rect ^. Svg.rectUpperLeftCorner)    
    (rX', rY') <- each convertNumber (rect ^. Svg.rectCornerRadius)
    w <- convertNumber (rect ^. Svg.rectWidth)
    h <- convertNumber (rect ^. Svg.rectHeight)
    let rX = min rX' (w/2)
    let rY = min rY' (h/2)
    let w' = w - 2 * rX
    let h' = h - 2 * rY
    let quarterCircle = Waterfall.arcVia (negate $ unit _y) (normalize (V2 1 (-1))) (unit _x)
    let scaleBevel = Waterfall.scale2D (V2 rX rY)
    if rX == 0 || rY == 0 
        then Waterfall.unitSquare &
                Waterfall.scale2D (V2 w h) &
                Waterfall.translate2D upperLeft &
                Waterfall.shapePaths & 
                return
        else return . pure . Waterfall.pathFrom (V2 rX 0) . catMaybes $
                [ if w' > 0 then Just (Waterfall.lineRelative (w' *^ unit _x)) else Nothing
                , quarterCircle 
                    & scaleBevel 
                    & Waterfall.splice
                    & pure
                , if h' > 0 then Just (Waterfall.lineRelative (h' *^ unit _y)) else Nothing
                , quarterCircle 
                    & Waterfall.rotate2D (pi/2)
                    & scaleBevel 
                    & Waterfall.splice
                    & pure
                , if w' > 0 then Just (Waterfall.lineRelative (negate (w' *^ unit _x))) else Nothing
                , quarterCircle 
                    & Waterfall.rotate2D pi
                    & scaleBevel 
                    & Waterfall.splice
                    & pure
                , if h' > 0 then Just (Waterfall.lineRelative (negate (h' *^ unit _y))) else Nothing
                , quarterCircle 
                    & Waterfall.rotate2D (-pi/2)
                    & scaleBevel 
                    & Waterfall.splice
                    & pure
                ]

-- | Recursively convert an `Svg.Tree` into a list of `Waterfall.Path2D`s
--
-- Text elements are not supported
convertTree :: Svg.Tree -> Either SVGError [Waterfall.Path2D]
convertTree tree = do
    transform <- maybe (pure id) (fmap chain . traverse convertTransform) (tree ^. Svg.drawAttr . Svg.drawAttributes . Svg.transform)
    fmap transform <$> case tree of
        Svg.PathTree path -> convertPathCommands (path ^. Svg.pathDefinition)
        Svg.GroupTree group ->  mconcat <$> traverse convertTree (group ^. Svg.groupChildren)
        Svg.SymbolTree sym ->  mconcat <$> traverse convertTree (sym ^. Svg.groupOfSymbol . Svg.groupChildren)
        Svg.CircleTree circle -> convertCircle circle
        Svg.PolyLineTree polyLine -> pure $ convertPolyLine polyLine
        Svg.PolygonTree polygon -> pure $ convertPolygon polygon
        Svg.LineTree line -> pure <$> convertLine line
        Svg.EllipseTree ellipse -> convertEllipse ellipse
        Svg.RectangleTree rectangle -> convertRectangle rectangle
        _ -> Right []

-- | Convert an `Svg.Document` into a list of `Path2Ds`
convertDocument :: Svg.Document -> Either SVGError [Waterfall.Path2D]
convertDocument doc = fmap mconcat . traverse convertTree $ (doc ^. Svg.elements) 

-- | Load an SVG file into a `Waterfall.Path2D`
readSVG :: FilePath -> IO (Either SVGError [Waterfall.Path2D])
readSVG path =
    let fileReadErr = Left . SVGError SVGTreeError $ "Failed to parse svg tree in file: " <> path
        fileOpenErr = SVGError SVGIOError . show @IOException
    in (convertDocument <=< maybe fileReadErr Right <=< first fileOpenErr)
        <$> try (Svg.loadSvgFile path)