summaryrefslogtreecommitdiff
path: root/lib/Graphics/Fountainhead/Metrics.hs
blob: e9b3c39d7b54b2fdb30ac43a03b52d48797a3683 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
{- This Source Code Form is subject to the terms of the Mozilla Public License,
   v. 2.0. If a copy of the MPL was not distributed with this file, You can
   obtain one at https://mozilla.org/MPL/2.0/. -}

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DuplicateRecordFields #-}

module Graphics.Fountainhead.Metrics
    ( FontBBox(..)
    , FontDescriptor(..)
    , MetricsError(..)
    , Number
    , FontDescriptorFlag(..)
    , collectMetrics
    ) where

import Data.ByteString (ByteString)
import Data.List (findIndex)
import Data.Text (Text)
import qualified Data.Text.Encoding as Text
import Graphics.Fountainhead.TrueType
    ( HeadTable(..)
    , HheaTable(..)
    , NameRecord(..)
    , NameTable(..)
    , PostHeader(..)
    , PostTable(..)
    , findTableByTag
    )
import Graphics.Fountainhead.Parser
    ( ParseErrorBundle
    , nameTableP
    , parseFontDirectory
    , parseTable
    , headTableP
    , hheaTableP
    , postTableP
    )
import qualified Text.Megaparsec as Megaparsec
import Data.Bifunctor (Bifunctor(..))
import Data.Int (Int16, Int32)
import Data.Word (Word16)
import GHC.Records (HasField(..))
import Graphics.Fountainhead.Type (Fixed32(..))

type Number = Float

data FontDescriptorFlag
    = FixedPitch
    | Serif
    | Symbolic
    | Script
    | Nonsymbolic
    | Italic
    | AllCap
    | SmallCap
    | ForceBold
    deriving (Eq, Show)

instance Enum FontDescriptorFlag
  where
    toEnum 1 = FixedPitch
    toEnum 2 = Serif
    toEnum 3 = Symbolic
    toEnum 4 = Script
    toEnum 6 = Nonsymbolic
    toEnum 7 = Italic
    toEnum 17 = AllCap
    toEnum 18 = SmallCap
    toEnum 19 = ForceBold
    toEnum _ = error "Font description flag is not supported."
    fromEnum FixedPitch = 1
    fromEnum Serif = 2
    fromEnum Symbolic = 3
    fromEnum Script = 4
    fromEnum Nonsymbolic = 6
    fromEnum Italic = 7
    fromEnum AllCap = 17
    fromEnum SmallCap = 18
    fromEnum ForceBold = 19

data FontBBox = FontBBox Number Number Number Number
    deriving (Eq, Show)

data FontDescriptor = FontDescriptor
    { fontName :: Text
    , flags :: [FontDescriptorFlag]
    , stemV :: Number
    , missingWidth :: Number
    , fontBBox :: FontBBox
    , italicAngle :: Number
    , capHeight :: Number
    , ascender :: Number
    , descender :: Number
    } deriving (Eq, Show)

data MetricsError
    = MetricsParseError ParseErrorBundle
    | MetricsRequiredTableMissingError String
    | MetricsNameRecordNotFound Word16
    deriving Eq

instance Show MetricsError
  where
    show (MetricsParseError errorBundle) = Megaparsec.errorBundlePretty errorBundle
    show (MetricsRequiredTableMissingError tableName) =
        "Required table " <> tableName <> " is missing."
    show (MetricsNameRecordNotFound nameId) =
        "Name record with ID " <> show nameId <> " was not found."

collectMetrics :: FilePath -> ByteString -> Either MetricsError FontDescriptor
collectMetrics fontFile ttfContents =
    case parseFontDirectory fontFile ttfContents of
        (_processedState, Left initialResult) -> Left
            $ MetricsParseError initialResult
        (processedState, Right initialResult) -> do
            nameEntry <- maybeMetricsError (MetricsRequiredTableMissingError "name")
                $ findTableByTag "name" initialResult
            NameTable{ nameRecord, variable } <- first MetricsParseError
                $ parseTable nameEntry nameTableP processedState
            psNameIndex <- maybeMetricsError (MetricsNameRecordNotFound 6)
                $ findIndex ((6 ==) . getField @"nameID") nameRecord

            headEntry <- maybeMetricsError (MetricsRequiredTableMissingError "head")
                $ findTableByTag "head" initialResult
            headTable@HeadTable{ unitsPerEm } <- first MetricsParseError
                $ parseTable headEntry headTableP processedState
            let scale = (1000.0 :: Float) / fromIntegral unitsPerEm

            hheaEntry <- maybeMetricsError (MetricsRequiredTableMissingError "hhea")
                $ findTableByTag "hhea" initialResult
            HheaTable{ ascent, descent } <- first MetricsParseError
                $ parseTable hheaEntry hheaTableP processedState

            postEntry <- maybeMetricsError (MetricsRequiredTableMissingError "post")
                $ findTableByTag "post" initialResult
            PostTable{ postHeader } <- first MetricsParseError
                $ parseTable postEntry postTableP processedState

            pure $ FontDescriptor
                { fontName = variableText nameRecord variable psNameIndex
                , flags = []
                , stemV = 1
                , missingWidth = 0
                , fontBBox =  calculateBoundingBox scale headTable
                , italicAngle = realToFrac $ getField @"italicAngle" postHeader
                , capHeight = 0
                , ascender = fromIntegral $ scalePs scale ascent
                , descender = fromIntegral $ scalePs scale descent
                }
  where
    calculateBoundingBox scale HeadTable{ xMin, xMax, yMin, yMax } =
        let xMin' = fromIntegral $ scalePs scale xMin
            yMin' = fromIntegral $ scalePs scale yMin
            xMax' = fromIntegral $ scalePs scale xMax
            yMax' = fromIntegral $ scalePs scale yMax
         in FontBBox xMin' yMin' xMax' yMax'
    scalePs :: Float -> Int16 -> Int16
    scalePs scale value = truncate $ fromIntegral value * scale
    variableText records variables recordIndex =
        let NameRecord{ platformID } = records !! recordIndex
            variable = variables !! recordIndex
         in if platformID == 1
            then Text.decodeUtf8 variable
            else Text.decodeUtf16BE variable
    maybeMetricsError metricsError Nothing = Left metricsError
    maybeMetricsError _ (Just result) = Right result