{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Geometry.Ipe.Reader(
readRawIpeFile
, readIpeFile
, readSinglePageFile
, ConversionError
, fromIpeXML
, readXML
, IpeReadText(..)
, IpeRead(..)
, IpeReadAttr(..)
, ipeReadTextWith
, ipeReadObject
, ipeReadAttrs
, ipeReadRec
, Coordinate(..)
) where
import Control.Applicative((<|>))
import Control.Lens hiding (Const, rmap)
import qualified Data.ByteString as B
import Data.Colour.SRGB (RGB(..))
import Data.Either (rights)
import Data.Ext
import Data.Geometry.Box
import Data.Geometry.Ipe.Attributes
import Data.Geometry.Ipe.ParserPrimitives (pInteger, pWhiteSpace)
import Data.Geometry.Ipe.PathParser
import Data.Geometry.Ipe.Types
import Data.Geometry.Ipe.Value
import Data.Geometry.Ipe.Color(IpeColor(..))
import Data.Geometry.Point
import Data.Geometry.PolyLine
import qualified Data.Geometry.Polygon as Polygon
import qualified Data.Geometry.Transformation as Trans
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Proxy
import qualified Data.LSeq as LSeq
import Data.Singletons
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Traversable as Tr
import Data.Vinyl hiding (Label)
import Data.Vinyl.Functor
import Data.Vinyl.TypeLevel
import Text.XML.Expat.Tree
type ConversionError = Text
readRawIpeFile :: (Coordinate r, Eq r)
=> FilePath -> IO (Either ConversionError (IpeFile r))
readRawIpeFile = fmap fromIpeXML . B.readFile
readIpeFile :: (Coordinate r, Eq r)
=> FilePath -> IO (Either ConversionError (IpeFile r))
readIpeFile = fmap (bimap id applyMatrices) . readRawIpeFile
readSinglePageFile :: (Coordinate r, Eq r)
=> FilePath -> IO (Either ConversionError (IpePage r))
readSinglePageFile = fmap f . readIpeFile
where
f (Left e) = Left e
f (Right i) = maybe (Left "No Ipe pages found") Right . firstOf (pages.traverse) $ i
fromIpeXML :: IpeRead (t r) => B.ByteString -> Either ConversionError (t r)
fromIpeXML b = readXML b >>= ipeRead
readXML :: B.ByteString -> Either ConversionError (Node Text Text)
readXML = bimap (T.pack . show) id . parse' defaultParseOptions
class IpeReadText t where
ipeReadText :: Text -> Either ConversionError t
class IpeRead t where
ipeRead :: Node Text Text -> Either ConversionError t
instance IpeReadText Text where
ipeReadText = Right
instance IpeReadText Int where
ipeReadText = fmap fromInteger . runParser pInteger
instance Coordinate r => IpeReadText (Point 2 r) where
ipeReadText = readPoint
instance Coordinate r => IpeReadText (Trans.Matrix 3 3 r) where
ipeReadText = readMatrix
instance IpeReadText LayerName where
ipeReadText = Right . LayerName
instance IpeReadText PinType where
ipeReadText "yes" = Right Yes
ipeReadText "h" = Right Horizontal
ipeReadText "v" = Right Vertical
ipeReadText "" = Right No
ipeReadText _ = Left "invalid PinType"
instance IpeReadText TransformationTypes where
ipeReadText "affine" = Right Affine
ipeReadText "rigid" = Right Rigid
ipeReadText "translations" = Right Translations
ipeReadText _ = Left "invalid TransformationType"
instance IpeReadText FillType where
ipeReadText "wind" = Right Wind
ipeReadText "eofill" = Right EOFill
ipeReadText _ = Left "invalid FillType"
instance Coordinate r => IpeReadText (IpeArrow r) where
ipeReadText t = case T.split (== '/') t of
[n,s] -> IpeArrow <$> pure n <*> ipeReadText s
_ -> Left "ipeArrow: name contains not exactly 1 / "
instance Coordinate r => IpeReadText (IpeDash r) where
ipeReadText t = Right . DashNamed $ t
ipeReadTextWith :: (Text -> Either t v) -> Text -> Either ConversionError (IpeValue v)
ipeReadTextWith f t = case f t of
Right v -> Right (Valued v)
Left _ -> Right (Named t)
instance Coordinate r => IpeReadText (Rectangle () r) where
ipeReadText = readRectangle
instance Coordinate r => IpeReadText (RGB r) where
ipeReadText = runParser (pRGB <|> pGrey)
where
pGrey = (\c -> RGB c c c) <$> pCoordinate
pRGB = RGB <$> pCoordinate <* pWhiteSpace
<*> pCoordinate <* pWhiteSpace
<*> pCoordinate
instance Coordinate r => IpeReadText (IpeColor r) where
ipeReadText = fmap IpeColor . ipeReadTextWith ipeReadText
instance Coordinate r => IpeReadText (IpePen r) where
ipeReadText = fmap IpePen . ipeReadTextWith readCoordinate
instance Coordinate r => IpeReadText (IpeSize r) where
ipeReadText = fmap IpeSize . ipeReadTextWith readCoordinate
instance Coordinate r => IpeReadText [Operation r] where
ipeReadText = readPathOperations
instance (Coordinate r, Eq r) => IpeReadText (NE.NonEmpty (PathSegment r)) where
ipeReadText t = ipeReadText t >>= fromOpsN
where
fromOpsN xs = case fromOps xs of
Left l -> Left l
Right [] -> Left "No path segments produced"
Right (p:ps) -> Right $ p NE.:| ps
fromOps [] = Right []
fromOps (MoveTo p:xs) = fromOps' p xs
fromOps _ = Left "Path should start with a move to"
fromOps' _ [] = Left "Found only a MoveTo operation"
fromOps' s (LineTo q:ops) = let (ls,xs) = span' _LineTo ops
pts = map ext $ s:q:mapMaybe (^?_LineTo) ls
poly = Polygon.fromPoints . dropRepeats $ pts
pl = fromPoints pts
in case xs of
(ClosePath : xs') -> PolygonPath poly <<| xs'
_ -> PolyLineSegment pl <<| xs
fromOps' _ _ = Left "fromOpts': rest not implemented yet."
span' pr = L.span (not . isn't pr)
x <<| xs = (x:) <$> fromOps xs
dropRepeats :: Eq a => [a] -> [a]
dropRepeats = map head . L.group
instance (Coordinate r, Eq r) => IpeReadText (Path r) where
ipeReadText = fmap (Path . LSeq.fromNonEmpty) . ipeReadText
class IpeReadAttr t where
ipeReadAttr :: Text -> Node Text Text -> Either ConversionError t
instance IpeReadText (Apply f at) => IpeReadAttr (Attr f at) where
ipeReadAttr n (Element _ ats _) = GAttr <$> Tr.mapM ipeReadText (lookup n ats)
ipeReadAttr _ _ = Left "IpeReadAttr: Element expected, Text found"
zipTraverseWith :: forall f g h i (rs :: [u]). Applicative h
=> (forall (x :: u). f x -> g x -> h (i x))
-> Rec f rs -> Rec g rs -> h (Rec i rs)
zipTraverseWith _ RNil RNil = pure RNil
zipTraverseWith f (x :& xs) (y :& ys) = (:&) <$> f x y <*> zipTraverseWith f xs ys
ipeReadRec :: forall f ats.
( RecApplicative ats
, ReifyConstraint IpeReadAttr (Attr f) ats
, RecAll (Attr f) ats IpeReadAttr
, AllSatisfy IpeAttrName ats
)
=> Proxy f -> Proxy ats
-> Node Text Text
-> Either ConversionError (Rec (Attr f) ats)
ipeReadRec _ _ x = zipTraverseWith f (writeAttrNames r) r'
where
r = rpure (GAttr Nothing)
r' = reifyConstraint @IpeReadAttr r
f :: forall at.
Const Text at
-> (Dict IpeReadAttr :. Attr f) at
-> Either ConversionError (Attr f at)
f (Const n) (Compose (Dict _)) = ipeReadAttr n x
ipeReadAttrs :: forall proxy proxy' i r f ats.
( f ~ AttrMapSym1 r, ats ~ AttributesOf i
, ReifyConstraint IpeReadAttr (Attr f) ats
, RecApplicative ats
, RecAll (Attr f) ats IpeReadAttr
, AllSatisfy IpeAttrName ats
)
=> proxy i -> proxy' r
-> Node Text Text
-> Either ConversionError (IpeAttributes i r)
ipeReadAttrs _ _ = fmap Attrs . ipeReadRec (Proxy :: Proxy f) (Proxy :: Proxy ats)
ipeReadObject :: ( IpeRead (i r)
, f ~ AttrMapSym1 r, ats ~ AttributesOf i
, RecApplicative ats
, ReifyConstraint IpeReadAttr (Attr f) ats
, RecAll (Attr f) ats IpeReadAttr
, AllSatisfy IpeAttrName ats
)
=> Proxy i -> proxy r -> Node Text Text
-> Either ConversionError (i r :+ IpeAttributes i r)
ipeReadObject prI prR xml = (:+) <$> ipeRead xml <*> ipeReadAttrs prI prR xml
instance Coordinate r => IpeRead (IpeSymbol r) where
ipeRead (Element "use" ats _) = case lookup "pos" ats of
Nothing -> Left "symbol without position"
Just ps -> flip Symbol name <$> ipeReadText ps
where
name = fromMaybe "mark/disk(sx)" $ lookup "name" ats
ipeRead _ = Left "symbol element expected, text found"
allText :: [Node Text Text] -> Either ConversionError Text
allText = fmap T.unlines . mapM unT
where
unT (Text t) = Right t
unT _ = Left "allText: Expected Text, found an Element"
instance (Coordinate r, Eq r) => IpeRead (Path r) where
ipeRead (Element "path" _ chs) = allText chs >>= ipeReadText
ipeRead _ = Left "path: expected element, found text"
lookup' :: Text -> [(Text,a)] -> Either ConversionError a
lookup' k = maybe (Left $ "lookup' " <> k <> " not found") Right . lookup k
instance Coordinate r => IpeRead (TextLabel r) where
ipeRead (Element "text" ats chs)
| lookup "type" ats == Just "label" = Label
<$> allText chs
<*> (lookup' "pos" ats >>= ipeReadText)
| otherwise = Left "Not a Text label"
ipeRead _ = Left "textlabel: Expected element, found text"
instance Coordinate r => IpeRead (MiniPage r) where
ipeRead (Element "text" ats chs)
| lookup "type" ats == Just "minipage" = MiniPage
<$> allText chs
<*> (lookup' "pos" ats >>= ipeReadText)
<*> (lookup' "width" ats >>= readCoordinate)
| otherwise = Left "Not a MiniPage"
ipeRead _ = Left "MiniPage: Expected element, found text"
instance Coordinate r => IpeRead (Image r) where
ipeRead (Element "image" ats _) = Image () <$> (lookup' "rect" ats >>= ipeReadText)
ipeRead _ = Left "Image: Element expected, text found"
instance (Coordinate r, Eq r) => IpeRead (IpeObject r) where
ipeRead x = firstRight [ IpeUse <$> ipeReadObject (Proxy :: Proxy IpeSymbol) r x
, IpePath <$> ipeReadObject (Proxy :: Proxy Path) r x
, IpeGroup <$> ipeReadObject (Proxy :: Proxy Group) r x
, IpeTextLabel <$> ipeReadObject (Proxy :: Proxy TextLabel) r x
, IpeMiniPage <$> ipeReadObject (Proxy :: Proxy MiniPage) r x
, IpeImage <$> ipeReadObject (Proxy :: Proxy Image) r x
]
where
r = Proxy :: Proxy r
firstRight :: [Either ConversionError a] -> Either ConversionError a
firstRight = maybe (Left "No matching object") Right . firstOf (traverse._Right)
instance (Coordinate r, Eq r) => IpeRead (Group r) where
ipeRead (Element "group" _ chs) = Right . Group . rights . map ipeRead $ chs
ipeRead _ = Left "ipeRead Group: expected Element, found Text"
instance IpeRead LayerName where
ipeRead (Element "layer" ats _) = LayerName <$> lookup' "name" ats
ipeRead _ = Left "layer: Expected element, found text"
instance IpeRead View where
ipeRead (Element "view" ats _) = (\lrs a -> View (map LayerName $ T.words lrs) a)
<$> lookup' "layers" ats
<*> (lookup' "active" ats >>= ipeReadText)
ipeRead _ = Left "View Expected element, found text"
instance (Coordinate r, Eq r) => IpeRead (IpePage r) where
ipeRead (Element "page" _ chs) = Right $ IpePage (readAll chs) (readAll chs) (readAll chs)
ipeRead _ = Left "page: Element expected, text found"
readAll :: IpeRead a => [Node Text Text] -> [a]
readAll = rights . map ipeRead
instance (Coordinate r, Eq r) => IpeRead (IpeFile r) where
ipeRead (Element "ipe" _ chs) = case readAll chs of
[] -> Left "Ipe: no pages found"
pgs -> Right $ IpeFile Nothing [] (NE.fromList pgs)
ipeRead _ = Left "Ipe: Element expected, text found"