fountainhead/lib/Graphics/Fountainhead/Metrics.hs

230 lines
7.8 KiB
Haskell
Raw Normal View History

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
( FontBBox(..)
, FontDescriptor(..)
, MetricsError(..)
, Number
, FontDescriptorFlag(..)
, collectMetrics
2024-02-04 11:07:15 +01:00
) where
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
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
)
import Graphics.Fountainhead.Parser
2024-02-13 09:16:14 +01:00
( Parser
, ParseErrorBundle
, ParseState
, 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
)
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
data FontDescriptorFlag
= FixedPitch
| Serif
| Symbolic
| Script
| Nonsymbolic
| Italic
| AllCap
| SmallCap
| ForceBold
deriving (Eq, Show)
2024-02-04 11:07:15 +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
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
, fontBBox :: FontBBox
2024-02-11 18:50:25 +01:00
, italicAngle :: Number
, capHeight :: Number
, ascender :: Number
, descender :: Number
} deriving (Eq, Show)
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
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."
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