2024-02-04 11:07:15 +01:00
|
|
|
{- 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/. -}
|
|
|
|
|
2024-02-11 18:50:25 +01:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
2024-02-04 11:07:15 +01:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2024-02-11 18:50:25 +01:00
|
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
2024-02-12 14:05:39 +01:00
|
|
|
{-# LANGUAGE PatternSynonyms #-}
|
2024-02-04 11:07:15 +01:00
|
|
|
|
|
|
|
module Graphics.Fountainhead.Metrics
|
2024-02-06 12:14:07 +01:00
|
|
|
( FontBBox(..)
|
|
|
|
, FontDescriptor(..)
|
2024-02-07 10:40:00 +01:00
|
|
|
, MetricsError(..)
|
2024-02-06 12:14:07 +01:00
|
|
|
, Number
|
|
|
|
, FontDescriptorFlag(..)
|
2024-02-07 10:40:00 +01:00
|
|
|
, collectMetrics
|
2024-02-04 11:07:15 +01:00
|
|
|
) where
|
|
|
|
|
2024-02-07 10:40:00 +01:00
|
|
|
import Data.ByteString (ByteString)
|
2024-02-11 18:50:25 +01:00
|
|
|
import Data.List (findIndex)
|
2024-02-13 09:16:14 +01:00
|
|
|
import qualified Data.List.NonEmpty as NonEmpty
|
2024-02-06 12:14:07 +01:00
|
|
|
import Data.Text (Text)
|
2024-02-11 18:50:25 +01:00
|
|
|
import qualified Data.Text.Encoding as Text
|
|
|
|
import Graphics.Fountainhead.TrueType
|
2024-02-13 09:16:14 +01:00
|
|
|
( BSerifStyle(..)
|
|
|
|
, FontDirectory(..)
|
|
|
|
, HeadTable(..)
|
2024-02-11 18:50:25 +01:00
|
|
|
, HheaTable(..)
|
2024-02-13 09:16:14 +01:00
|
|
|
, HmtxTable(..)
|
|
|
|
, LongHorMetric(..)
|
2024-02-11 18:50:25 +01:00
|
|
|
, NameRecord(..)
|
|
|
|
, NameTable(..)
|
2024-02-12 14:05:39 +01:00
|
|
|
, Os2BaseFields(..)
|
|
|
|
, Os2Version4Fields(..)
|
|
|
|
, Os2Version5Fields(..)
|
|
|
|
, Os2Table(..)
|
2024-02-13 09:16:14 +01:00
|
|
|
, Panose(..)
|
2024-02-11 18:50:25 +01:00
|
|
|
, PostHeader(..)
|
|
|
|
, PostTable(..)
|
|
|
|
, findTableByTag
|
2024-02-12 14:05:39 +01:00
|
|
|
, pattern Os2Version4CommonFields
|
2024-02-11 18:50:25 +01:00
|
|
|
)
|
2024-02-07 10:40:00 +01:00
|
|
|
import Graphics.Fountainhead.Parser
|
2024-02-13 09:16:14 +01:00
|
|
|
( Parser
|
|
|
|
, ParseErrorBundle
|
|
|
|
, ParseState
|
2024-02-07 10:40:00 +01:00
|
|
|
, nameTableP
|
|
|
|
, parseFontDirectory
|
|
|
|
, parseTable
|
2024-02-11 18:50:25 +01:00
|
|
|
, headTableP
|
|
|
|
, hheaTableP
|
2024-02-13 09:16:14 +01:00
|
|
|
, hmtxTableP
|
2024-02-12 14:05:39 +01:00
|
|
|
, os2TableP
|
2024-02-11 18:50:25 +01:00
|
|
|
, postTableP
|
2024-02-07 10:40:00 +01:00
|
|
|
)
|
|
|
|
import qualified Text.Megaparsec as Megaparsec
|
2024-02-11 18:50:25 +01:00
|
|
|
import Data.Bifunctor (Bifunctor(..))
|
2024-02-12 14:05:39 +01:00
|
|
|
import Data.Int (Int16)
|
2024-02-11 18:50:25 +01:00
|
|
|
import Data.Word (Word16)
|
|
|
|
import GHC.Records (HasField(..))
|
2024-02-04 11:07:15 +01:00
|
|
|
|
2024-02-11 18:50:25 +01:00
|
|
|
type Number = Float
|
2024-02-06 12:14:07 +01:00
|
|
|
|
|
|
|
data FontDescriptorFlag
|
|
|
|
= FixedPitch
|
|
|
|
| Serif
|
|
|
|
| Symbolic
|
|
|
|
| Script
|
|
|
|
| Nonsymbolic
|
|
|
|
| Italic
|
|
|
|
| AllCap
|
|
|
|
| SmallCap
|
|
|
|
| ForceBold
|
|
|
|
deriving (Eq, Show)
|
2024-02-04 11:07:15 +01:00
|
|
|
|
2024-02-06 12:14:07 +01:00
|
|
|
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
|
2024-02-04 11:07:15 +01:00
|
|
|
|
2024-02-06 12:14:07 +01:00
|
|
|
data FontBBox = FontBBox Number Number Number Number
|
|
|
|
deriving (Eq, Show)
|
|
|
|
|
|
|
|
data FontDescriptor = FontDescriptor
|
|
|
|
{ fontName :: Text
|
|
|
|
, flags :: [FontDescriptorFlag]
|
2024-02-11 18:50:25 +01:00
|
|
|
, stemV :: Number
|
|
|
|
, missingWidth :: Number
|
2024-02-06 12:14:07 +01:00
|
|
|
, fontBBox :: FontBBox
|
2024-02-11 18:50:25 +01:00
|
|
|
, italicAngle :: Number
|
|
|
|
, capHeight :: Number
|
2024-02-06 12:14:07 +01:00
|
|
|
, ascender :: Number
|
|
|
|
, descender :: Number
|
|
|
|
} deriving (Eq, Show)
|
2024-02-07 10:40:00 +01:00
|
|
|
|
|
|
|
data MetricsError
|
|
|
|
= MetricsParseError ParseErrorBundle
|
|
|
|
| MetricsRequiredTableMissingError String
|
2024-02-11 18:50:25 +01:00
|
|
|
| MetricsNameRecordNotFound Word16
|
2024-02-12 14:05:39 +01:00
|
|
|
| UnsupportedOs2VersionError
|
2024-02-07 10:40:00 +01:00
|
|
|
deriving Eq
|
|
|
|
|
|
|
|
instance Show MetricsError
|
|
|
|
where
|
|
|
|
show (MetricsParseError errorBundle) = Megaparsec.errorBundlePretty errorBundle
|
|
|
|
show (MetricsRequiredTableMissingError tableName) =
|
|
|
|
"Required table " <> tableName <> " is missing."
|
2024-02-11 18:50:25 +01:00
|
|
|
show (MetricsNameRecordNotFound nameId) =
|
|
|
|
"Name record with ID " <> show nameId <> " was not found."
|
2024-02-12 14:05:39 +01:00
|
|
|
show UnsupportedOs2VersionError =
|
|
|
|
"OS/2 version 1 does not contain cap height."
|
2024-02-07 10:40:00 +01:00
|
|
|
|
|
|
|
collectMetrics :: FilePath -> ByteString -> Either MetricsError FontDescriptor
|
|
|
|
collectMetrics fontFile ttfContents =
|
|
|
|
case parseFontDirectory fontFile ttfContents of
|
|
|
|
(_processedState, Left initialResult) -> Left
|
|
|
|
$ MetricsParseError initialResult
|
2024-02-11 18:50:25 +01:00
|
|
|
(processedState, Right initialResult) -> do
|
2024-02-13 09:16:14 +01:00
|
|
|
let parseForMetrics' = parseForMetrics processedState initialResult
|
|
|
|
|
|
|
|
NameTable{ nameRecord, variable } <- parseForMetrics' "name" nameTableP
|
2024-02-11 18:50:25 +01:00
|
|
|
psNameIndex <- maybeMetricsError (MetricsNameRecordNotFound 6)
|
|
|
|
$ findIndex ((6 ==) . getField @"nameID") nameRecord
|
|
|
|
|
2024-02-13 09:16:14 +01:00
|
|
|
headTable@HeadTable{ unitsPerEm } <- parseForMetrics' "head" headTableP
|
2024-02-11 18:50:25 +01:00
|
|
|
let scale = (1000.0 :: Float) / fromIntegral unitsPerEm
|
|
|
|
|
2024-02-13 09:16:14 +01:00
|
|
|
HheaTable{ ascent, descent, numOfLongHorMetrics } <-
|
|
|
|
parseForMetrics' "hhea" hheaTableP
|
|
|
|
PostTable{ postHeader } <- parseForMetrics' "post" postTableP
|
|
|
|
|
|
|
|
(capHeight, os2BaseFields) <- getCapHeight processedState initialResult
|
|
|
|
let Os2BaseFields{ usWeightClass, panose } = os2BaseFields
|
2024-02-11 18:50:25 +01:00
|
|
|
|
2024-02-13 09:16:14 +01:00
|
|
|
HmtxTable{ hMetrics } <- parseForMetrics' "hmtx"
|
|
|
|
$ hmtxTableP numOfLongHorMetrics
|
2024-02-11 18:50:25 +01:00
|
|
|
|
2024-02-13 09:16:14 +01:00
|
|
|
let fixedPitchFlag = if getField @"isFixedPitch" postHeader > 0 then Just FixedPitch else Nothing
|
|
|
|
isSerifFlag = if isSerif $ getField @"bSerifStyle" panose then Just Serif else Nothing
|
2024-02-12 14:05:39 +01:00
|
|
|
|
2024-02-11 18:50:25 +01:00
|
|
|
pure $ FontDescriptor
|
|
|
|
{ fontName = variableText nameRecord variable psNameIndex
|
|
|
|
, flags = []
|
2024-02-13 09:16:14 +01:00
|
|
|
, stemV = calculateStemV $ fromIntegral usWeightClass
|
|
|
|
, missingWidth = fromIntegral $ scalePs scale
|
|
|
|
$ getField @"advanceWidth" $ NonEmpty.head hMetrics
|
2024-02-11 18:50:25 +01:00
|
|
|
, fontBBox = calculateBoundingBox scale headTable
|
|
|
|
, italicAngle = realToFrac $ getField @"italicAngle" postHeader
|
2024-02-12 14:05:39 +01:00
|
|
|
, capHeight = fromIntegral $ scalePs scale capHeight
|
2024-02-11 18:50:25 +01:00
|
|
|
, ascender = fromIntegral $ scalePs scale ascent
|
|
|
|
, descender = fromIntegral $ scalePs scale descent
|
|
|
|
}
|
|
|
|
where
|
2024-02-12 14:05:39 +01:00
|
|
|
calculateStemV weightClass = 10 + 220 * (weightClass - 50) / 900
|
|
|
|
getCapHeight processedState initialResult = do
|
2024-02-13 09:16:14 +01:00
|
|
|
os2Table <- parseForMetrics processedState initialResult "OS/2" os2TableP
|
2024-02-12 14:05:39 +01:00
|
|
|
case os2Table of
|
2024-02-13 09:16:14 +01:00
|
|
|
Os2Version4CommonFields os2BaseFields Os2Version4Fields{ sCapHeight } ->
|
|
|
|
Right (sCapHeight, os2BaseFields)
|
|
|
|
Os2Version5 os2BaseFields _ Os2Version5Fields{ sCapHeight } ->
|
|
|
|
Right (sCapHeight, os2BaseFields)
|
2024-02-12 14:05:39 +01:00
|
|
|
_ -> Left UnsupportedOs2VersionError
|
2024-02-11 18:50:25 +01:00
|
|
|
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'
|
2024-02-13 09:16:14 +01:00
|
|
|
scalePs :: Integral a => Float -> a -> Int16
|
2024-02-11 18:50:25 +01:00
|
|
|
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
|
2024-02-13 09:16:14 +01:00
|
|
|
|
|
|
|
parseForMetrics
|
|
|
|
:: forall a
|
|
|
|
. ParseState
|
|
|
|
-> FontDirectory
|
|
|
|
-> String
|
|
|
|
-> Parser a
|
|
|
|
-> Either MetricsError a
|
|
|
|
parseForMetrics processedState fontDirectory tableName tableParser =
|
|
|
|
let foundTable = findTableByTag tableName fontDirectory
|
|
|
|
missingError = MetricsRequiredTableMissingError tableName
|
|
|
|
parseTable' rawTable = parseTable rawTable tableParser processedState
|
|
|
|
in maybeMetricsError missingError foundTable
|
|
|
|
>>= first MetricsParseError . parseTable'
|
|
|
|
|
|
|
|
maybeMetricsError :: forall a. MetricsError -> Maybe a -> Either MetricsError a
|
|
|
|
maybeMetricsError metricsError Nothing = Left metricsError
|
|
|
|
maybeMetricsError _ (Just result) = Right result
|
|
|
|
|
|
|
|
isSerif :: BSerifStyle -> Bool
|
|
|
|
isSerif AnySerifStyle = False
|
|
|
|
isSerif NoFitSerifStyle = False
|
|
|
|
isSerif CoveSerifStyle = True
|
|
|
|
isSerif ObtuseCoveSerifStyle = True
|
|
|
|
isSerif SquareCoveSerifStyle = True
|
|
|
|
isSerif ObtuseSquareCoveSerifStyle = True
|
|
|
|
isSerif SquareSerifStyle = True
|
|
|
|
isSerif ThinSerifStyle = True
|
|
|
|
isSerif BoneSerifStyle = True
|
|
|
|
isSerif ExaggeratedSerifStyle =True
|
|
|
|
isSerif TriangleSerifStyle = True
|
|
|
|
isSerif NormalSansSerifStyle = False
|
|
|
|
isSerif ObtuseSansSerifStyle = False
|
|
|
|
isSerif PerpSansSerifStyle = False
|
|
|
|
isSerif FlaredSerifStyle = True
|
|
|
|
isSerif RoundedSerifStyle = True
|