module Graphics.Rendering.PGF
( renderWith
, RenderM
, Render
, initialState
, scope
, epsilon
, style
, bp
, pt
, mm
, px
, ln
, raw
, rawString
, pgf
, bracers
, brackets
, path
, trail
, segment
, usePath
, lineTo
, curveTo
, moveTo
, closePath
, clip
, stroke
, fill
, asBoundingBox
, setDash
, setLineWidth
, setLineCap
, setLineJoin
, setMiterLimit
, setLineColor
, setLineOpacity
, setFillColor
, setFillRule
, setFillOpacity
, setTransform
, applyTransform
, baseTransform
, applyScale
, resetNonTranslations
, linearGradient
, radialGradient
, colorSpec
, shadePath
, opacityGroup
, image
, embeddedImage
, embeddedImage'
, renderText
, setTextAlign
, setTextRotation
, setFontWeight
, setFontSlant
) where
import Codec.Compression.Zlib
import Codec.Picture
import Control.Monad.RWS
import Data.ByteString.Builder
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B (replicate)
import Data.ByteString.Internal (fromForeignPtr)
import qualified Data.ByteString.Lazy as LB
import qualified Data.Foldable as F (foldMap)
import Data.List (intersperse)
import Data.Maybe (catMaybes)
import Data.Typeable
import qualified Data.Vector.Storable as S
import Numeric
import Diagrams.Core.Transform
import Diagrams.Prelude hiding (Render, image, moveTo,
opacity, opacityGroup, stroke,
(<>))
import Diagrams.TwoD.Text (FontSlant (..), FontWeight (..),
TextAlignment (..))
import Diagrams.Backend.PGF.Surface
data RenderState n = RenderState
{ _pos :: P2 n
, _indent :: Int
, _style :: Style V2 n
}
makeLenses ''RenderState
data RenderInfo = RenderInfo
{ _format :: TexFormat
, _pprint :: Bool
}
makeLenses ''RenderInfo
type RenderM n m = RWS RenderInfo Builder (RenderState n) m
type Render n = RenderM n ()
initialState :: (Typeable n, Floating n) => RenderState n
initialState = RenderState
{ _pos = origin
, _indent = 0
, _style = lc black mempty
}
renderWith :: (RealFloat n, Typeable n)
=> Surface -> Bool -> Bool -> V2 n -> Render n -> Builder
renderWith s readable standalone bounds r = builder
where
bounds' = fmap (fromInteger . floor) bounds
(_,builder) = evalRWS r'
(RenderInfo (s^.texFormat) readable)
initialState
r' = do
when standalone $ do
ln . rawString $ s^.preamble
maybe (return ())
(ln . rawString . ($ fmap ceiling bounds'))
(s^.pageSize)
ln . rawString $ s^.beginDoc
picture $ rectangleBoundingBox bounds' >> r
when standalone $ rawString $ s^.endDoc
raw :: Builder -> Render n
raw = tell
rawByteString :: ByteString -> Render n
rawByteString = tell . byteString
rawString :: String -> Render n
rawString = tell . stringUtf8
pgf :: Builder -> Render n
pgf c = raw $ "\\pgf" <> c
rawChar :: Char -> Render n
rawChar = tell . char8
emit :: Render n
emit = do
pp <- view pprint
when pp $ do
tab <- use indent
rawByteString $ B.replicate tab ' '
ln :: Render n -> Render n
ln r = do
emit
r
rawChar '\n'
bracers :: Render n -> Render n
bracers r = do
rawChar '{'
r
rawChar '}'
bracersBlock :: Render n -> Render n
bracersBlock rs = do
raw "{\n"
inBlock rs
emit
rawChar '}'
brackets :: Render n -> Render n
brackets r = do
rawChar '['
r
rawChar ']'
parens :: Render n -> Render n
parens r = do
rawChar '('
r
rawChar ')'
commaIntersperce :: [Render n] -> Render n
commaIntersperce = sequence_ . intersperse (rawChar ',')
inBlock :: Render n -> Render n
inBlock r = do
indent += 2
r
indent -= 2
point :: RealFloat n => P2 n -> Render a
point = tuplePoint . unp2
bracerPoint :: RealFloat n => P2 n -> Render a
bracerPoint (P (V2 x y)) = do
bracers (bp x)
bracers (bp y)
tuplePoint :: RealFloat n => (n,n) -> Render a
tuplePoint (x,y) = do
pgf "qpoint"
bracers (bp x)
bracers (bp y)
n :: RealFloat a => a -> Render n
n x = rawString $ showFFloat (Just 4) x ""
bp :: RealFloat a => a -> Render n
bp = (>> raw "bp") . n
px :: RealFloat a => a -> Render n
px = (>> raw "px") . n
mm :: RealFloat a => a -> Render n
mm = (>> raw "mm") . n
pt :: RealFloat a => a -> Render n
pt = (>> raw "pt") . n
epsilon :: Fractional n => n
epsilon = 0.0001
picture :: Render n -> Render n
picture r = do
f <- view format
ln . raw $ case f of
LaTeX -> "\\begin{pgfpicture}"
ConTeXt -> "\\startpgfpicture"
PlainTeX -> "\\pgfpicture"
inBlock r
ln . raw $ case f of
LaTeX -> "\\end{pgfpicture}"
ConTeXt -> "\\stoppgfpicture"
PlainTeX -> "\\endpgfpicture"
rectangleBoundingBox :: RealFloat n => V2 n -> Render n
rectangleBoundingBox bounds = do
ln $ do
pgf "pathrectangle"
bracers $ pgf "pointorigin"
bracers $ tuplePoint (unr2 bounds)
ln $ do
pgf "usepath"
bracers $ raw "use as bounding box"
scope :: Render n -> Render n
scope r = do
f <- view format
ln . raw $ case f of
LaTeX -> "\\begin{pgfscope}"
ConTeXt -> "\\startpgfscope"
PlainTeX -> "\\pgfscope"
inBlock r
ln . raw $ case f of
LaTeX -> "\\end{pgfscope}"
ConTeXt -> "\\stoppgfscope"
PlainTeX -> "\\endpgfscope"
transparencyGroup :: Render n -> Render n
transparencyGroup r = do
f <- view format
ln . raw $ case f of
LaTeX -> "\\begin{pgftransparencygroup}"
ConTeXt -> "\\startpgftransparencygroup"
PlainTeX -> "\\pgftransparencygroup"
inBlock r
ln . raw $ case f of
LaTeX -> "\\end{pgftransparencygroup}"
ConTeXt -> "\\stoppgftransparencygroup"
PlainTeX -> "\\endpgftransparencygroup"
opacityGroup :: RealFloat a => a -> Render n -> Render n
opacityGroup x r = scope $ do
setFillOpacity x
transparencyGroup r
texColor :: RealFloat a => a -> a -> a -> Render n
texColor r g b = do
n r
rawChar ','
n g
rawChar ','
n b
contextColor :: RealFloat a => a -> a -> a -> Render n
contextColor r g b = do
raw "r=" >> n r
rawChar ','
raw "g=" >> n g
rawChar ','
raw "b=" >> n b
defineColour :: RealFloat a => ByteString -> a -> a -> a -> Render n
defineColour name r g b = do
f <- view format
ln $ case f of
ConTeXt -> do
raw "\\definecolor"
brackets $ rawByteString name
brackets $ contextColor r g b
_ -> do
raw "\\definecolor"
bracers $ rawByteString name
bracers $ raw "rgb"
bracers $ texColor r g b
parensColor :: Color c => c -> Render n
parensColor c = parens $ texColor r g b
where (r,g,b,_) = colorToSRGBA c
closePath :: Render n
closePath = ln $ pgf "pathclose"
moveTo :: RealFloat n => P2 n -> Render n
moveTo v = ln $ do
pos .= v
pgf "pathqmoveto"
bracerPoint v
lineTo :: RealFloat n => V2 n -> Render n
lineTo v = ln $ do
p <- use pos
let v' = p .+^ v
pos .= v'
pgf "pathqlineto"
bracerPoint v'
curveTo :: RealFloat n => V2 n -> V2 n -> V2 n -> Render n
curveTo v2 v3 v4 = ln $ do
p <- use pos
let [v2',v3',v4'] = map (p .+^) [v2,v3,v4]
pos .= v4'
pgf "pathqcurveto"
mapM_ bracerPoint [v2', v3', v4']
stroke :: Render n
stroke = ln $ pgf "usepathqstroke"
fill :: Render n
fill = ln $ pgf "usepathqfill"
clip :: Render n
clip = ln $ pgf "usepathqclip"
path :: RealFloat n => Path V2 n -> Render n
path (Path trs) = do
mapM_ renderTrail trs
where
renderTrail (viewLoc -> (p, tr)) = do
moveTo p
trail tr
trail :: RealFloat n => Trail V2 n -> Render n
trail t = withLine (render' . lineSegments) t
where
render' segs = do
mapM_ segment segs
when (isLoop t) closePath
segment :: RealFloat n => Segment Closed V2 n -> Render n
segment (Linear (OffsetClosed v)) = lineTo v
segment (Cubic v1 v2 (OffsetClosed v3)) = curveTo v1 v2 v3
usePath :: Bool -> Bool -> Render n
usePath False False = return ()
usePath doFill doStroke = ln $ do
pgf "usepathq"
when doFill $ raw "fill"
when doStroke $ raw "stroke"
asBoundingBox :: Render n
asBoundingBox = ln $ do
pgf "usepath"
bracers $ raw "use as bounding box"
setLineWidth :: RealFloat n => n -> Render n
setLineWidth w = ln $ do
pgf "setlinewidth"
bracers $ bp w
setLineCap :: LineCap -> Render n
setLineCap cap = ln . pgf $ case cap of
LineCapButt -> "setbuttcap"
LineCapRound -> "setroundcap"
LineCapSquare -> "setrectcap"
setLineJoin :: LineJoin -> Render n
setLineJoin lJoin = ln . pgf $ case lJoin of
LineJoinBevel -> "setbeveljoin"
LineJoinRound -> "setroundjoin"
LineJoinMiter -> "setmiterjoin"
setMiterLimit :: RealFloat n => n -> Render n
setMiterLimit l = do
pgf "setmiterlimit"
bracers $ bp l
setDash :: RealFloat n => Dashing n -> Render n
setDash (Dashing ds offs) = setDash' ds offs
setDash' :: RealFloat n => [n] -> n -> Render n
setDash' ds off = ln $ do
pgf "setdash"
bracers $ mapM_ (bracers . bp) ds
bracers $ bp off
setLineColor :: (RealFloat a, Color c) => c -> Render a
setLineColor c = do
defineColour "sc" r g b
ln $ pgf "setstrokecolor{sc}"
when (a /= 1) $ setLineOpacity (realToFrac a)
where
(r,g,b,a) = colorToSRGBA c
setLineOpacity :: RealFloat n => n -> Render n
setLineOpacity a = ln $ do
pgf "setstrokeopacity"
bracers $ n a
setFillRule :: FillRule -> Render n
setFillRule rule = ln $ case rule of
Winding -> pgf "setnonzerorule"
EvenOdd -> pgf "seteorule"
setFillColor :: Color c => c -> Render n
setFillColor (colorToSRGBA -> (r,g,b,a)) = do
defineColour "fc" r g b
ln $ pgf "setfillcolor{fc}"
when (a /= 1) $ setFillOpacity (realToFrac a :: Double)
setFillOpacity :: RealFloat a => a -> Render n
setFillOpacity a = ln $ do
pgf "setfillopacity"
bracers $ n a
getMatrix :: Num n => Transformation V2 n -> (n, n, n, n, n, n)
getMatrix t = (a1,a2,b1,b2,c1,c2)
where
[a1, a2, b1, b2, c1, c2] = concat $ matrixHomRep t
applyTransform :: RealFloat n => Transformation V2 n -> Render n
applyTransform t
| isID = return ()
| shiftOnly = ln $ do
pgf "transformshift"
bracers p
| otherwise = ln $ do
pgf "transformcm"
mapM_ (bracers . n) [a, b, c, d] >> bracers p
where
(a,b,c,d,e,f) = getMatrix t
p = tuplePoint (e,f)
shiftOnly = (a,b,c,d) == (1,0,0,1)
isID = shiftOnly && (e,f) == (0,0)
setTransform :: RealFloat n => Transformation V2 n -> Render n
setTransform t = do
pgf "settransformentries"
mapM_ (bracers . n) [a, b, c, d] >> mapM_ (bracers . bp) [e, f]
where
(a,b,c,d,e,f) = getMatrix t
applyScale :: RealFloat n => n -> Render n
applyScale s = ln $ do
pgf "transformscale"
bracers $ n s
resetNonTranslations :: Render n
resetNonTranslations = ln $ pgf "transformresetnontranslations"
baseTransform :: RealFloat n => Transformation V2 n -> Render n
baseTransform t = ln $ do
pgf "lowlevel"
bracers $ setTransform t
linearGradient :: RealFloat n => Path V2 n -> LGradient n -> Render n
linearGradient p lg = scope $ do
path p
let (stops', t) = calcLinearStops p lg
ln $ do
pgf "declarehorizontalshading"
bracers $ raw "ft"
bracers $ raw "100bp"
bracersBlock $ colorSpec 1 stops'
clip
baseTransform t
useShading $ raw "ft"
calcLinearStops :: RealFloat n
=> Path V2 n -> LGradient n -> ([GradientStop n], T2 n)
calcLinearStops (Path []) _ = ([], mempty)
calcLinearStops pth (LGradient stops p0 p1 gt sm)
= (linearStops' x0 x1 stops sm, t <> ft)
where
t = gt
<> translation (p0 ^. _Point)
<> scaling (norm (p1 .-. p0))
<> rotationTo (dirBetween p1 p0)
p' = transform (inv t) pth
Just (x0,x1) = extentX p'
Just (y0,y1) = extentY p'
ft = translation (V2 x0 y0) <> scalingV ((*0.01) . abs <$> V2 (x0 x1) (y0 y1)) <> translation 50
scalingV :: (Additive v, Fractional n) => v n -> Transformation v n
scalingV v = fromSymmetric $ liftU2 (*) v <-> liftU2 (flip (/)) v
useShading :: Render n -> Render n
useShading nm = ln $ do
pgf "useshading"
bracers nm
_translation :: Lens' (Transformation v n) (v n)
_translation f (Transformation a b v) = f v <&> \v' -> Transformation a b v'
linearStops' :: RealFloat n
=> n -> n -> [GradientStop n] -> SpreadMethod -> [GradientStop n]
linearStops' x0 x1 stops sm =
GradientStop c1' 0 : filter (inRange . view stopFraction) stops' ++ [GradientStop c2' 100]
where
stops' = case sm of
GradPad -> over (each . stopFraction) normalise stops
GradRepeat -> flip F.foldMap [i0 .. i1] $ \i ->
increaseFirst $
over (each . stopFraction)
(normalise . (+ fromIntegral i))
stops
GradReflect -> flip F.foldMap [i0 .. i1] $ \i ->
over (each . stopFraction)
(normalise . (+ fromIntegral i))
(reverseOdd i stops)
increaseFirst = over (_head . stopFraction) (+0.001)
reverseOdd i
| odd i = reverse . over (each . stopFraction) (1 )
| otherwise = id
i0 = floor x0 :: Int
i1 = ceiling x1
c1' = SomeColor $ colourInterp stops' 0
c2' = SomeColor $ colourInterp stops' 100
inRange x = x > 0 && x < 100
normalise x = 100 * (x x0) / (x1 x0)
colourInterp :: RealFloat n => [GradientStop n] -> n -> AlphaColour Double
colourInterp cs0 x = go cs0
where
go (GradientStop c1 a : c@(GradientStop c2 b) : cs)
| x <= a = toAlphaColour c1
| x > a && x < b = blend y (toAlphaColour c2) (toAlphaColour c1)
| otherwise = go (c : cs)
where
y = realToFrac $ (x a) / (b a)
go [GradientStop c2 _] = toAlphaColour c2
go _ = transparent
radialGradient :: RealFloat n => Path V2 n -> RGradient n -> Render n
radialGradient p rg = scope $ do
path p
let (stops', t, p0) = calcRadialStops p rg
ln $ do
pgf "declareradialshading"
bracers $ raw "ft"
bracers $ point p0
bracersBlock $ colorSpec 1 stops'
clip
baseTransform t
useShading $ raw "ft"
calcRadialStops :: RealFloat n
=> Path V2 n -> RGradient n -> ([GradientStop n], T2 n, P2 n)
calcRadialStops (Path []) _ = ([], mempty, origin)
calcRadialStops pth (RGradient stops p0 r0 p1 r1 gt _sm)
= (stops', t <> ft, P cv)
where
cv = tp0 .-. tp1
tp0 = papply gt p0
tp1 = papply gt p1
t = gt
<> translation (p1 ^. _Point)
<> scaling r1
p' = transform (inv t) pth
Just (x0,x1) = extentX p'
Just (y0,y1) = extentY p'
d = 2 * max (max (abs $ x0 x1) (abs $ y0 y1)) (lstop ^. stopFraction)
ft = scaling 0.01
stops' = head stops : over (each . stopFraction) refrac stops ++ [lstop & stopFraction .~ 100*d]
refrac x = 100 * ((r0 + x * (r1 r0)) / r1)
lstop = last stops
colorSpec :: RealFloat n => n -> [GradientStop n] -> Render n
colorSpec d = mapM_ ln
. combinePairs
. intersperse (rawChar ';')
. map mkColor
where
mkColor (GradientStop c sf) = do
raw "rgb"
parens $ bp (d*sf)
raw "="
parensColor c
combinePairs :: Monad m => [m a] -> [m a]
combinePairs (x1:x2:xs) = (x1 >> x2) : combinePairs xs
combinePairs xs = xs
shadePath :: RealFloat n => Angle n -> Render n -> Render n
shadePath (view deg -> θ) name = ln $ do
pgf "shadepath"
bracers name
bracers $ n θ
image :: RealFloat n => DImage n External -> Render n
image (DImage (ImageRef ref) w h t2) = scope $ do
applyTransform t2
ln $ do
pgf "text"
bracers $ do
pgf "image"
brackets $ do
raw "width=" >> bp (fromIntegral w :: Double)
rawChar ','
raw "height=" >> bp (fromIntegral h :: Double)
bracers $ rawString ref
embeddedImage :: RealFloat n => DImage n Embedded -> Render n
embeddedImage (DImage (ImageRaster (ImageRGB8 img)) w h t) =
embeddedImage' (hexImage img) w h t
embeddedImage _ = error "Unsupported embedded image. Only ImageRGB8 is currently supported."
hexImage :: Image PixelRGB8 -> LB.ByteString
hexImage (imageData -> v) = compress $ LB.fromStrict bs
where
bs = fromForeignPtr p i nn
(p, i, nn) = S.unsafeToForeignPtr v
embeddedImage' :: RealFloat n => LB.ByteString -> Int -> Int -> T2 n -> Render n
embeddedImage' img w h t = scope $ do
baseTransform t
ln $ raw "\\immediate\\pdfliteral{"
rawLn "q"
rawLn $ s w <> " 0 0 " <> s h <> " -" <> half w <> " -" <> half h <> " cm"
rawLn "BI"
rawLn $ "/W " <> s w
rawLn $ "/H " <> s h
rawLn "/CS /RGB"
rawLn "/BPC 8"
rawLn "/F [/AHx /Fl]"
rawLn "ID"
rawLn $ hexChunk img <> char8 '>'
rawLn "EI"
rawLn "Q"
rawLn "}"
where
rawLn r = raw r >> rawChar '\n'
s = intDec
half x = s (x `div` 2) <> if odd x then ".5" else mempty
hexChunk :: LB.ByteString -> Builder
hexChunk (LB.splitAt 40 -> (a,b))
| LB.null b = lazyByteStringHex a
| otherwise = lazyByteStringHex a <> char8 '\n' <> hexChunk b
renderText :: [Render n] -> Render n -> Render n
renderText ops txt = ln $ do
pgf "text"
brackets . commaIntersperce $ ops
bracers txt
setTextAlign :: RealFloat n => TextAlignment n -> [Render n]
setTextAlign a = case a of
BaselineText -> [raw "base", raw "left"]
BoxAlignedText xt yt -> catMaybes [xt', yt']
where
xt' | xt > 0.75 = Just $ raw "right"
| xt < 0.25 = Just $ raw "left"
| otherwise = Nothing
yt' | yt > 0.75 = Just $ raw "top"
| yt < 0.25 = Just $ raw "bottom"
| otherwise = Nothing
setTextRotation :: RealFloat n => Angle n -> [Render n]
setTextRotation a = case a^.deg of
0 -> []
θ -> [raw "rotate=" >> n θ]
setFontWeight :: FontWeight -> Render n
setFontWeight FontWeightBold = raw "\\bf "
setFontWeight _ = return ()
setFontSlant :: FontSlant -> Render n
setFontSlant FontSlantNormal = return ()
setFontSlant FontSlantItalic = raw "\\it "
setFontSlant FontSlantOblique = raw "\\sl "