lp-diagrams-svg-1.1: Graphics/Diagrams/Backend/SVGTree.hs
{-# LANGUAGE TypeSynonymInstances, FlexibleContexts, FlexibleInstances, GeneralizedNewtypeDeriving, MultiParamTypeClasses, RecursiveDo, TypeFamilies, OverloadedStrings, RecordWildCards,UndecidableInstances, PackageImports, TemplateHaskell, RankNTypes #-}
module Graphics.Diagrams.Backend.SVGTree (renderDiagram, saveDiagram, SvgDiagram) where
import Graphics.Diagrams.Core as D
import Prelude hiding (sum,mapM_,mapM,concatMap,Num(..),(/))
import qualified Prelude
import Graphics.Svg as S hiding (Group)
import Control.Monad.RWS
import Linear.V2 (V2(..))
import Codec.Picture.Types (PixelRGBA8(..))
import Graphics.Text.TrueType
import qualified Data.Vector.Unboxed as V
import qualified Data.Map as M
import Algebra.Classes
instance Additive a => Additive (V2 a) where
a + b = (+) <$> a <*> b
zero = pure zero
instance Group a => Group (V2 a) where
a - b = (-) <$> a <*> b
negate = fmap negate
-- TODO -- add a newtype so this does not leak to the user code.
type SvgM = RWS Font [Path] (V2 Double,V2 Double)
type SvgDiagram = Diagram String SvgM
arrowHead :: Marker
arrowHead = Marker {
_markerDrawAttributes = mempty,
_markerRefPoint = (Px 0,Px 0),
_markerWidth = Nothing,
_markerHeight = Nothing,
_markerOrient = Just OrientationAuto,
_markerUnits = Nothing,
_markerViewBox = Nothing,
_markerOverflow = Just OverflowVisible,
_markerElements =
[PathTree $ S.Path
mempty {_transform = Just [Scale 0.8 Nothing, Rotate 180 Nothing, Translate 12.5 0]
,_fillRule = Last $ Just $ FillEvenOdd
,_strokeColor = Last $ Just $ ColorRef $ PixelRGBA8 0 0 0 0
,_strokeWidth = Last $ Just $ (S.Point 1)
,_strokeOpacity = Just 1
,S._fillColor = Last $ Just $ ColorRef $ PixelRGBA8 0 0 0 0
,_fillOpacity = Just 1
}
[ MoveTo OriginAbsolute [V2 0 0], lA 5.0 (-5.0), lA (-12.5) 0.0, lA 5.0 5.0, lA 0.0 0.0, EndPath]]}
where lA x y = LineTo OriginAbsolute [V2 x y]
saveDiagram :: FilePath -> String -> SvgDiagram () -> IO ()
saveDiagram fn fontFam d = do
putStrLn $ "Finding font: " ++ fontFam
Just fontFn <- findFontOfFamily fontFam (FontStyle False False)
putStrLn $ "Loading font: " ++ fontFn
mfont <- loadFontFile fontFn
case mfont of
Right font -> saveXmlFile fn $ renderDiagram font d
Left err -> error err
renderDiagram :: Font -> SvgDiagram a -> Document
renderDiagram font d = Document
{_viewBox = Just (lo'x,lo'y,hi'x-lo'x,hi'y-lo'y)
,_width = Just $ Px $ hi'x-lo'x
,_height = Just $ Px $ hi'y-lo'y
,_definitions = M.fromList [("stealth",ElementMarker arrowHead)]
,_description = "lp-diagrams-svg generated document"
,_documentLocation = "no location"
,_styleRules = []
,_elements = map PathTree paths}
where (_,(lo,hi),paths) = runRWS (runDiagram svgBackend d) font infimum
V2 lo'x lo'y = lo - border
V2 hi'x hi'y = hi + border
border = V2 5 5
ptToPx :: forall a. Field a => a -> a
ptToPx z = fromInteger 4*z / fromInteger 3
renderPoint :: FrozenPoint -> V2 Double
renderPoint (D.Point x y) = V2 (ptToPx x) (negate $ ptToPx y)
renderSegment :: Segment Coord -> PathCommand
renderSegment (StraightTo p) = LineTo OriginAbsolute [renderPoint p]
renderSegment (D.CurveTo c d p) = S.CurveTo OriginAbsolute [(renderPoint c,renderPoint d,renderPoint p)]
renderSegment Cycle = EndPath
renderTip :: LineTip -> Maybe ElementRef
renderTip t = case t of
NoTip -> Nothing
_ -> Just $ Ref "stealth"
renderDashPattern :: [(Double, Double)] -> Maybe [Number]
renderDashPattern [] = Nothing
renderDashPattern xs = Just $ concat [[S.Point on, S.Point off] | (on,off) <- xs]
renderPathOptions :: PathOptions -> DrawAttributes
renderPathOptions PathOptions{..} = mempty
{_strokeWidth = Last $ Just $ S.Point $ _lineWidth
,_strokeColor = Last $ col _drawColor
,S._fillColor = Last $ col _fillColor
,_strokeLineCap = Last $ Just $ case _lineCap of
RoundCap -> CapRound
RectCap -> CapSquare
ButtCap -> CapButt
-- _strokeOpacity,
,_strokeLineJoin = case _lineJoin of
RoundJoin -> Last $ Just $ JoinRound
BevelJoin -> Last $ Just $ JoinBevel
MiterJoin -> Last $ Just $ JoinMiter
-- _strokeMiterLimit,
-- _fillOpacity,
-- _groupOpacity,
-- _transform, _fillRule,
-- _maskRef, _clipPathRef,
-- _clipRule, _attrClass,
-- _attrId, _strokeOffset,
,_strokeDashArray = Last $ renderDashPattern _dashPattern
-- _fontSize, _fontFamily,
-- _fontStyle,
-- _textAnchor,
,_markerStart = Last $ renderTip _startTip
-- _markerMid,
,_markerEnd = Last $ renderTip _endTip
}
-- <> toSvg _startTip <> "-" <> toSvg _endTip <> ","
col :: Maybe String -> Maybe Texture
col c = case c of
Nothing -> Just $ FillNone
Just "black" -> Just $ ColorRef $ PixelRGBA8 0 0 0 0
Just "red" -> Just $ ColorRef $ PixelRGBA8 255 0 0 100
Just "blue" -> Just $ ColorRef $ PixelRGBA8 0 0 255 100
Just c' -> Just $ TextureRef c'
lbound :: V2 Double -> V2 Double -> V2 Double
lbound (V2 x1 y1) (V2 x2 y2) = V2 (min x1 x2) (min y1 y2)
maxPt :: V2 Double
maxPt = V2 1000.0 1000.0
infimum :: (V2 Double, V2 Double)
infimum = (maxPt, negate maxPt)
union :: (V2 Double, V2 Double) -> (V2 Double, V2 Double) -> (V2 Double, V2 Double)
union (l1,h1) (l2,h2) = (lbound l1 l2, negate (lbound (negate h1) (negate h2)))
dup :: t -> (t, t)
dup x = (x,x)
pathPoints :: Path' t -> [Point' t]
pathPoints EmptyPath = []
pathPoints (D.Path start segs) = start:concat (map segPoints segs)
segPoints :: Segment t -> [Point' t]
segPoints (D.StraightTo x) = [x]
segPoints (D.CurveTo c d p) = [c,d,p]
segPoints Cycle = []
mkPairs :: forall t. [t] -> [(t, t)]
mkPairs (x:y:xs) = (x,y):mkPairs xs
mkPairs _ = []
svgBackend :: Backend String SvgM
svgBackend = Backend {..} where
_tracePath _ EmptyPath = return ()
_tracePath options pth@(D.Path start segs) = do
tell [S.Path (renderPathOptions options) (MoveTo OriginAbsolute [renderPoint start]:map renderSegment segs)]
let bx = foldr union infimum $ map dup $ map renderPoint $ pathPoints pth
modify (union bx)
_traceLabel :: Monad x =>
(location -> (FrozenPoint -> SvgM ()) -> x ()) -> -- freezer
(forall a. SvgM a -> x a) -> -- embedder
location ->
String -> -- label specification
x BoxSpec
_traceLabel freezer embedder point lab = do
font <- embedder ask
let dpi = 72
-- 144 -- 72 * 4/3
pointSize = PointSize 10
bbox = stringBoundingBox font dpi pointSize lab
boxWidth = realToFrac (_xMax bbox - _xMin bbox)
boxHeight = realToFrac (_yMax bbox - _baselineHeight bbox)
boxDepth = realToFrac (_baselineHeight bbox - _yMin bbox)
freezer point $ \p -> do
let contours = getStringCurveAtPoint dpi (0,0) [(font,pointSize,lab)]
renderPair :: (Float,Float) -> V2 Double
renderPair (x,y) = renderPoint $ (D.Point (realToFrac x) (negate (realToFrac y) - boxHeight)) + p
tell [S.Path textAttrs $
concat [[MoveTo OriginAbsolute [renderPair (V.head c)]
,QuadraticBezier OriginAbsolute (mkPairs $ fmap renderPair (V.toList (V.tail c)))]
| c <- contour, not (V.null c)]
| contour <- contours]
return (BoxSpec {..})
-- DrawAttributes for text rendering
textAttrs :: DrawAttributes
textAttrs = mempty {S._fillColor = Last $ col $ Just "black"}