~alcinnz/fontconfig-pure

4da6f787545bfef4460ec18e4093233d0015df48 — Adrian Cochrane 10 months ago 66dce74
Datamodel FontConfig in Haskell.
M app/Main.hs => app/Main.hs +0 -2
@@ 1,8 1,6 @@
module Main where

import qualified MyLib (someFunc)

main :: IO ()
main = do
  putStrLn "Hello, Haskell!"
  MyLib.someFunc

M fontconfig-pure.cabal => fontconfig-pure.cabal +8 -2
@@ 63,7 63,10 @@ library
    import:           warnings

    -- Modules exported by the library.
    exposed-modules:  MyLib
    exposed-modules:  Graphics.Text.Font.Choose.CharSet, Graphics.Text.Font.Choose.LangSet,
            Graphics.Text.Font.Choose.ObjectSet, Graphics.Text.Font.Choose.Range,
            Graphics.Text.Font.Choose.StrSet, Graphics.Text.Font.Choose.Value,
            Graphics.Text.Font.Choose.Pattern, Graphics.Text.Font.Choose.FontSet

    -- Modules included in this library but not exported.
    -- other-modules:


@@ 72,7 75,10 @@ library
    -- other-extensions:

    -- Other library packages from which modules are imported.
    build-depends:    base >=4.12 && <5, containers >=0.1 && <1, css-syntax, freetype2 >=0.2 && <0.3, hashable >=1.3 && <2, linear >=1.0.1 && <2, scientific, stylist-traits >=0.1.1 && <1, text, inline-c
    build-depends:    base >=4.12 && <5, containers >=0.1 && <1, css-syntax,
            freetype2 >=0.2 && <0.3, hashable >=1.3 && <2, linear >=1.0.1 && <2,
            scientific, stylist-traits >=0.1.1 && <1, text, msgpack >= 1.0 && <2,
            vector >= 0.13 && <1

    -- Directories containing source files.
    hs-source-dirs:   lib

A lib/Graphics/Text/Font/Choose/CharSet.hs => lib/Graphics/Text/Font/Choose/CharSet.hs +57 -0
@@ 0,0 1,57 @@
module Graphics.Text.Font.Choose.CharSet where

import Data.IntSet (IntSet, union)
import qualified Data.IntSet as IntSet

import Data.Char (isHexDigit)
import Numeric (readHex)

import Data.MessagePack (MessagePack(..))

-- | An FcCharSet is a set of Unicode characters.
type CharSet = IntSet

parseChar :: String -> Int
parseChar str | ((x, _):_) <- readHex str = toEnum x
    | otherwise = 0
replaceWild :: Char -> String -> String
replaceWild ch ('?':rest) = ch:replaceWild ch rest
replaceWild ch (c:cs) = c:replaceWild ch cs
replaceWild _ "" = ""
parseWild :: Char -> String -> Int
parseWild ch str = parseChar $ replaceWild ch str
-- | Utility for parsing "unicode-range" @font-face property.
parseCharSet :: String -> Maybe CharSet
parseCharSet ('U':rest) = parseCharSet ('u':rest) -- lowercase initial "u"
parseCharSet ('u':'+':cs)
    | (start@(_:_), '-':ends) <- span isHexDigit cs,
        (end@(_:_), rest) <- span isHexDigit ends, Just set <- parseCharSet' rest =
            Just $ union set $ IntSet.fromList [parseChar start..parseChar end]
    | (codepoint@(_:_), rest) <- span isHexDigit cs, Just set <- parseCharSet' rest =
        Just $ flip IntSet.insert set $ parseChar codepoint
    | (codepoint@(_:_), rest) <- span (\c -> isHexDigit c || c == '?') cs,
        Just set <- parseCharSet' rest =
            Just $ IntSet.union set $ IntSet.fromList [
                parseWild '0' codepoint..parseWild 'f' codepoint]
parseCharSet _ = Nothing
parseCharSet' :: String -> Maybe CharSet
parseCharSet' (',':rest) = parseCharSet rest
parseCharSet' "" = Just IntSet.empty
parseCharSet' _ = Nothing

-- NOTE: Serial already provides IntSet a CBOR codec, but its quite naive.
-- I suspect that CharSets are typically quite dense,
-- So a diff-compression pass should play well with 

diffCompress :: Int -> [Int] -> [Int]
diffCompress prev (x:xs) = x - prev:diffCompress x xs
diffCompress _ [] = []
diffDecompress :: Int -> [Int] -> [Int]
diffDecompress prev (x:xs) = prev + x:diffDecompress x xs
diffDecompress _ [] = []

newtype CharSet' = CharSet' { unCharSet :: CharSet }
instance MessagePack CharSet' where
    toObject = toObject . diffCompress 0 . IntSet.toAscList . unCharSet
    fromObject msg =
        CharSet' <$> IntSet.fromAscList <$> diffDecompress 0 <$> fromObject msg

A lib/Graphics/Text/Font/Choose/FontSet.hs => lib/Graphics/Text/Font/Choose/FontSet.hs +81 -0
@@ 0,0 1,81 @@
module Graphics.Text.Font.Choose.FontSet where

import Graphics.Text.Font.Choose.Pattern

type FontSet = [Pattern']

------
--- CSS Bindings
------

-- | `StyleSheet` wrapper to parse @font-face rules.
data FontFaceParser a = FontFaceParser { cssFonts :: FontSet, cssInner :: a}

{- parseFontFaceSrc (Function "local":Ident name:RightParen:Comma:rest) =
    ("local:" ++ unpack name):parseFontFaceSrc rest
parseFontFaceSrc (Function "local":String name:RightParen:Comma:rest) =
    ("local:" ++ unpack name):parseFontFaceSrc rest
parseFontFaceSrc (Function "local":Ident name:RightParen:[]) = ["local:" ++ unpack name]
parseFontFaceSrc (Function "local":String name:RightParen:[]) = ["local:" ++ unpack name]

parseFontFaceSrc (Url link:toks)
    | Comma:rest <- skipMeta toks = unpack link:parseFontFaceSrc rest
    | [] <- skipMeta toks = [unpack link]
    | otherwise = [""] -- Error indicator!
  where
    skipMeta (Function "format":Ident _:RightParen:rest) = skipMeta rest
    skipMeta (Function "format":String _:RightParen:rest) = skipMeta rest
    skipMeta (Function "tech":Ident _:RightParen:rest) = skipMeta rest
    skipMeta (Function "tech":String _:RightParen:rest) = skipMeta rest
    skipMeta toks = toks

parseFontFaceSrc _ = [""]

properties2font :: [(Text, [Token])] -> Pattern
properties2font (("font-family", [String font]):props) =
    setValue "family" Strong (unpack font) $ properties2font props
properties2font (("font-family", [Ident font]):props) =
    setValue "family" Strong (unpack font) $ properties2font props

properties2font (("font-stretch", [tok]):props) | Just x <- parseFontStretch tok =
    setValue "width" Strong x $ properties2font props
properties2font (("font-stretch", [start, end]):props)
    | Just x <- parseFontStretch start, Just y <- parseFontStretch end =
        setValue "width" Strong (x `iRange` y) $ properties2font props

properties2font (("font-weight", [tok]):props) | Just x <- parseFontWeight tok =
    setValue "width" Strong x $ properties2font props
properties2font (("font-weight", [start, end]):props)
    | Just x <- parseFontStretch start, Just y <- parseFontStretch end =
        setValue "weight" Strong (x `iRange` y) $ properties2font props

properties2font (("font-feature-settings", toks):props)
    | (features, True, []) <- parseFontFeatures toks =
        setValue "fontfeatures" Strong (intercalate "," $ map fst features) $
            properties2font props

properties2font (("font-variation-settings", toks):props)
    | (_, True, []) <- parseFontVars toks =
        setValue "variable" Strong True $ properties2font props

properties2font (("unicode-range", toks):props)
    | Just chars <- parseCharSet $ unpack $ serialize toks =
        setValue "charset" Strong chars $ properties2font props

-- Ignoring metadata & trusting in FreeType's broad support for fonts.
properties2font (("src", toks):props)
    | fonts@(_:_) <- parseFontFaceSrc toks, "" `notElem` fonts =
        setValue "web-src" Strong (intercalate "\t" fonts) $ properties2font props

properties2font (_:props) = properties2font props
properties2font [] = []

instance StyleSheet a => StyleSheet (FontFaceParser a) where
    setPriorities v (FontFaceParser x self) = FontFaceParser x $ setPriorities v self
    addRule (FontFaceParser x self) rule = FontFaceParser x $ addRule self rule

    addAtRule (FontFaceParser fonts self) "font-face" toks =
        let ((props, _), toks') = parseProperties toks
        in (FontFaceParser (properties2font props:fonts) self, toks')
    addAtRule (FontFaceParser x self) key toks =
        let (a, b) = addAtRule self key toks in (FontFaceParser x a, b) -}

A lib/Graphics/Text/Font/Choose/LangSet.hs => lib/Graphics/Text/Font/Choose/LangSet.hs +15 -0
@@ 0,0 1,15 @@
module Graphics.Text.Font.Choose.LangSet where

import Data.Set (Set)
import qualified Data.Set as S

import Data.MessagePack (MessagePack(..))

type LangSet = Set String
newtype LangSet' = LangSet' { unLangSet :: LangSet }

instance MessagePack LangSet' where
    toObject = toObject . S.toList . unLangSet
    fromObject msg = LangSet' <$> S.fromList <$> fromObject msg

-- TODO: Implement language bindings! But first: Datamodel everything!

A lib/Graphics/Text/Font/Choose/ObjectSet.hs => lib/Graphics/Text/Font/Choose/ObjectSet.hs +5 -0
@@ 0,0 1,5 @@
module Graphics.Text.Font.Choose.ObjectSet where

type ObjectSet = [String]

-- NOTE: Already has all the typeclass instances I want, including MessagePack!

A lib/Graphics/Text/Font/Choose/Pattern.hs => lib/Graphics/Text/Font/Choose/Pattern.hs +155 -0
@@ 0,0 1,155 @@
{-# LANGUAGE DeriveGeneric #-}
module Graphics.Text.Font.Choose.Pattern where

import Data.Map as M
import Data.MessagePack (MessagePack(..), Object(..))
import Data.Hashable (Hashable(..))
import GHC.Generics (Generic)

import Graphics.Text.Font.Choose.Value

type Pattern = Map String [(Binding, Value)]
data Pattern' = Pattern' { unPattern :: Pattern }
data Binding = Strong | Weak | Same deriving (Eq, Ord, Enum, Read, Show, Generic)

instance Hashable Binding where
    hash = fromEnum
instance MessagePack Binding where
    fromObject (ObjectBool True) = Just Strong
    fromObject (ObjectBool False) = Just Weak
    fromObject ObjectNil = Just Same
    fromObject _ = Nothing
    toObject Strong = ObjectBool True
    toObject Weak = ObjectBool False
    toObject Same = ObjectNil

------
--- CSS
------

{-parseFontFamily :: [Token] -> ([String], Bool, [Token])
parseFontFamily (String font:Comma:tail) = let (fonts, b, tail') = parseFontFamily tail
    in (unpack font:fonts, b, tail')
parseFontFamily (Ident font:Comma:tail) = let (fonts, b, tail') = parseFontFamily tail
    in (unpack font:fonts, b, tail')
parseFontFamily (String font:tail) = ([unpack font], True, tail)
parseFontFamily (Ident font:tail) = ([unpack font], True, tail)
parseFontFamily toks = ([], False, toks) -- Invalid syntax!

parseFontFeatures :: [Token] -> ([(String, Int)], Bool, [Token])
parseFontFeatures (String feat:toks) | feature@(_:_:_:_:[]) <- unpack feat = case toks of
    Comma:tail -> let (feats, b, tail') = parseFontFeatures tail in ((feature, 1):feats, b, tail')
    Ident "on":Comma:tail -> let (f, b, t) = parseFontFeatures tail in ((feature, 1):f, b, t)
    Ident "on":tail -> ([(feature, 1)], True, tail)
    Ident "off":Comma:tail -> let (f, b, t) = parseFontFeatures tail in ((feature, 1):f, b, t)
    Ident "off":tail -> ([(feature, 1)], True, tail)
    Number _ (NVInteger x):Comma:tail ->
        let (feats, b, tail') = parseFontFeatures tail in ((feature, fromEnum x):feats, b, tail')
    Number _ (NVInteger x):tail -> ([(feature, fromEnum x)], True, tail)
parseFontFeatures toks = ([], False, toks)

parseFontVars :: [Token] -> ([(String, Double)], Bool, [Token])
parseFontVars (String var':Number _ x:Comma:tail) | var@(_:_:_:_:[]) <- unpack var' =
    let (vars, b, tail') = parseFontVars tail in ((var, nv2double x):vars, b, tail')
parseFontVars (String var':Number _ x:tail) | var@(_:_:_:_:[]) <- unpack var' =
    ([(var, nv2double x)], True, tail)
parseFontVars toks = ([], False, toks)

parseLength :: Double -> NumericValue -> Text -> Double
parseLength super length unit = convert (nv2double length) unit
  where
    convert = c
    c x "pt" = x -- Unit FontConfig expects!
    c x "pc" = x/6 `c` "in"
    c x "in" = x/72 `c` "pt"
    c x "Q" = x/40 `c` "cm"
    c x "mm" = x/10 `c` "cm"
    c x "cm" = x/2.54 `c` "in"
    c x "px" = x/96 `c` "in" -- Conversion factor during early days of CSS, got entrenched.
    c x "em" = x * super
    c x "%" = x/100 `c` "em"
    c _ _ = 0/0 -- NaN

parseFontStretch :: Token -> Maybe Int -- Result in percentages
parseFontStretch (Percentage _ x) = Just $ fromEnum $ nv2double x
parseFontStretch (Ident "ultra-condensed") = Just 50
parseFontStretch (Ident "extra-condensed") = Just 63 -- 62.5%, but round towards 100%
parseFontStretch (Ident "condensed") = Just 75
parseFontStretch (Ident "semi-condensed") = Just 88 -- 87.5% actually...
parseFontStretch (Ident "normal") = Just 100
parseFontStretch (Ident "initial") = Just 100
parseFontStretch (Ident "semi-expanded") = Just 112 -- 112.5% actually...
parseFontStretch (Ident "expanded") = Just 125
parseFontStretch (Ident "extra-expanded") = Just 150
parseFontStretch (Ident "ultra-expanded") = Just 200
parseFontStretch _ = Nothing

-- Conversion between CSS scale & FontConfig scale is non-trivial, use lookuptable.
parseFontWeight :: Token -> Maybe Int
parseFontWeight (Ident k) | k `elem` ["initial", "normal"] = Just 80
parseFontWeight (Ident "bold") = Just 200
parseFontWeight (Number _ (NVInteger x)) = Just $ weightFromOpenType $ fromEnum x
parseFontWeight _ = Nothing

nv2double (NVInteger x) = fromInteger x
nv2double (NVNumber x) = toRealFloat x

sets a b c d = Just $ setValues a b c d
set a b c d = Just $ setValue a b c d
seti a b c d = Just $ setValue a b (c :: Int) d
unset' a b = Just $ unset a b

getSize pat | ValueDouble x <- getValue "size" pat = x
    | otherwise = 10

instance PropertyParser Pattern where
    temp = []

    longhand _ self "font-family" toks
        | (fonts, True, []) <- parseFontFamily toks = sets "family" Strong fonts self

    -- font-size: initial should be configurable!
    longhand super self "font-size" [Dimension _ x unit]
        | let y = parseLength (getSize super) x unit, not $ isNaN y =
            set "size" Strong y self
    longhand super self "font-size" [Percentage x y] =
        longhand super self "font-size" [Dimension x y "%"]

    longhand _ self "font-style" [Ident "initial"] = seti "slant" Strong 0 self
    longhand _ self "font-style" [Ident "normal"] = seti "slant" Strong 0 self
    longhand _ self "font-style" [Ident "italic"] = seti "slant" Strong 100 self
    longhand _ self "font-style" [Ident "oblique"] = seti "slant" Strong 110 self

    -- Conversion between CSS scale & FontConfig scale is non-trivial, use lookuptable.
    longhand _ self "font-weight" [tok]
        | Just x <- parseFontWeight tok = seti "weight" Strong x self
    longhand super self "font-weight" [Number _ (NVInteger x)]
        | x > 920 = longhand super self "font-weight" [Number "" $ NVInteger 950]
        | otherwise = longhand super self "font-weight" [Number "" $ NVInteger $ (x `div` 100) * 100]
    longhand _ self "font-weight" [Ident "lighter"]
        | ValueInt x <- getValue "weight" self, x > 200 = seti "weight" Strong 200 self
        -- minus 100 adhears to the CSS standard awefully well in this new scale.
        | ValueInt x <- getValue "weight" self = seti "weight" Strong (max (x - 100) 0) self
        | otherwise = seti "weight" Strong 0 self
    longhand _ self "font-weight" [Ident "bolder"]
        | ValueInt x <- getValue "weight" self, x <= 65 = seti "weight" Strong 80 self
        | ValueInt x <- getValue "weight" self, x <= 150 = seti "weight" Strong 200 self
        | ValueInt x <- getValue "weight" self, x < 210 = seti "weight" Strong 210 self
        | ValueInt _ <- getValue "weight" self = Just self -- As bold as it goes...
        | otherwise = seti "weight" Strong 200 self

    longhand _ self "font-feature-settings" [Ident k]
        | k `elem` ["initial", "normal"] = unset' "fontfeatures" self
    longhand _ self "font-feature-settings" toks
        | (features, True, []) <- parseFontFeatures toks =
            set "fontfeatures" Strong (intercalate "," $ map fst features) self

    longhand _ self "font-variation-settings" [Ident k]
        | k `elem` ["initial", "normal"] = unset' "variable" self
    longhand _ self "font-variation-settings" toks
        | (_, True, []) <- parseFontVars toks = set "variable" Strong True self

    longhand _ s "font-stretch" [tok]
        | Just x <- parseFontStretch tok = seti "width" Strong x s

    longhand _ _ _ _ = Nothing -}

A lib/Graphics/Text/Font/Choose/Range.hs => lib/Graphics/Text/Font/Choose/Range.hs +21 -0
@@ 0,0 1,21 @@
module Graphics.Text.Font.Choose.Range where

import Data.MessagePack (MessagePack(..), Object(..))
import qualified Data.Vector as V
import qualified Data.IntMap as IM

-- | Matches a numeric range.
data Range = Range Double Double deriving (Eq, Show, Ord)
-- | Matches an integral range.
iRange :: Int -> Int -> Range
iRange i j = toEnum i `Range` toEnum j

instance MessagePack Range where
    toObject (Range start end) = ObjectMap $ V.fromList [
        (ObjectInt 0, ObjectDouble start),
        (ObjectInt 1, ObjectDouble end)
      ]
    fromObject msg
        | Just msg' <- fromObject msg =
            Just (IM.findWithDefault 0 0 msg' `Range` IM.findWithDefault 0 1 msg')
        | otherwise = Nothing

A lib/Graphics/Text/Font/Choose/StrSet.hs => lib/Graphics/Text/Font/Choose/StrSet.hs +12 -0
@@ 0,0 1,12 @@
module Graphics.Text.Font.Choose.StrSet where

import Data.Set (Set)
import qualified Data.Set as S

import Data.MessagePack (MessagePack(..))

newtype StrSet = StrSet { unStrSet :: Set String }

instance MessagePack StrSet where
    toObject = toObject . S.toList . unStrSet
    fromObject msg = StrSet <$> S.fromList <$> fromObject msg

A lib/Graphics/Text/Font/Choose/Value.hs => lib/Graphics/Text/Font/Choose/Value.hs +97 -0
@@ 0,0 1,97 @@
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
module Graphics.Text.Font.Choose.Value where

import Linear.Matrix (M22)
import Linear.V2 (V2(..))
import Graphics.Text.Font.Choose.CharSet (CharSet, CharSet'(..))
--import FreeType.Core.Base (FT_Face(..))
import Graphics.Text.Font.Choose.LangSet (LangSet, LangSet'(..))
import Graphics.Text.Font.Choose.Range (Range)

import Data.MessagePack (MessagePack(..), Object(..))
import qualified Data.Text as Txt

-- | A dynamic type system for `Pattern`s.
data Value = ValueVoid
    | ValueInt Int
    | ValueDouble Double
    | ValueString String
    | ValueBool Bool
    | ValueMatrix (M22 Double)
    | ValueCharSet CharSet
--    | ValueFTFace FT_Face -- FIXME: Is it worth going through the trouble to bridge this?
    | ValueLangSet LangSet
    | ValueRange Range deriving (Eq, Show, Ord)

instance MessagePack Value where
    toObject ValueVoid = ObjectNil
    toObject (ValueInt x) = ObjectInt x
    toObject (ValueDouble x) = ObjectDouble x
    toObject (ValueString x) = ObjectStr $ Txt.pack x
    toObject (ValueBool x) = ObjectBool x
    toObject (ValueMatrix (V2 (V2 xx yx) (V2 xy yy))) = toObject [xx, xy, yx, yy]
    toObject (ValueCharSet x) = toObject $ CharSet' x
    toObject (ValueLangSet x) = toObject $ LangSet' x
    toObject (ValueRange x) = toObject x

    fromObject ObjectNil = Just ValueVoid
    fromObject (ObjectBool x) = Just $ ValueBool x
    fromObject (ObjectInt x) = Just $ ValueInt x
    fromObject (ObjectFloat x) = Just $ ValueDouble $ realToFrac x
    fromObject (ObjectDouble x) = Just $ ValueDouble x
    fromObject (ObjectStr x) = Just $ ValueString $ Txt.unpack x
    fromObject (ObjectBin _) = Nothing -- Would use for to transfer font faces via underlying bytes.
    fromObject msg
        | Just charset <- fromObject msg = Just $ ValueCharSet $ unCharSet charset
        | Just langset <- fromObject msg = Just $ ValueLangSet $ unLangSet langset
        | Just range <- fromObject msg = Just $ ValueRange range
        | Just [xx, xy, yx, yy] <- fromObject msg :: Maybe [Double] =
            -- [Double] decoding is overly generous, potentially conflicts with above.
            Just $ ValueMatrix $ V2 (V2 xx yx) (V2 xy yy)
        | otherwise = Nothing

-- | Coerces compiletime types to runtime types.
class ToValue x where
    toValue :: x -> Value
    fromValue :: Value -> Maybe x
    fromValue' :: Value -> x -- throws Result.Error
    fromValue' self | Just ret <- fromValue self = ret
    fromValue' _ = error "Type mismatch!" -- TODO: Throw something nicer!

instance ToValue () where
    toValue () = ValueVoid
    fromValue ValueVoid = Just ()
    fromValue _ = Nothing
instance ToValue Int where
    toValue = ValueInt
    fromValue (ValueInt x) = Just x
    fromValue _ = Nothing
instance ToValue Double where
    toValue = ValueDouble
    fromValue (ValueDouble x) = Just x
    fromValue _ = Nothing
instance ToValue String where
    toValue = ValueString
    fromValue (ValueString x) = Just x
    fromValue _ = Nothing
instance ToValue Bool where
    toValue = ValueBool
    fromValue (ValueBool x) = Just x
    fromValue _ = Nothing
instance ToValue (M22 Double) where
    toValue = ValueMatrix
    fromValue (ValueMatrix x) = Just x
    fromValue _ = Nothing
instance ToValue CharSet' where
    toValue = ValueCharSet . unCharSet
    fromValue (ValueCharSet x) = Just $ CharSet' x
    fromValue _ = Nothing
--instance ToValue FT_Face where
--    toValue = ValueFTFace
--    fromValue (ValueFTFace x) = Just x
--    fromValue _ = Nothing
instance ToValue LangSet' where
    toValue = ValueLangSet . unLangSet
    fromValue (ValueLangSet x) = Just $ LangSet' x
    fromValue _ = Nothing