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-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
|
|
|
|
( HeadTable(..)
|
|
|
|
, HheaTable(..)
|
|
|
|
, NameRecord(..)
|
|
|
|
, NameTable(..)
|
2024-02-12 14:05:39 +01:00
|
|
|
, Os2BaseFields(..)
|
|
|
|
, Os2Version4Fields(..)
|
|
|
|
, Os2Version5Fields(..)
|
|
|
|
, Os2Table(..)
|
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
|
|
|
|
( ParseErrorBundle
|
|
|
|
, nameTableP
|
|
|
|
, parseFontDirectory
|
|
|
|
, parseTable
|
2024-02-11 18:50:25 +01:00
|
|
|
, headTableP
|
|
|
|
, hheaTableP
|
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
|
|
|
|
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
|
|
|
|
|
2024-02-12 14:05:39 +01:00
|
|
|
(capHeight, weightClass) <- getCapHeight processedState initialResult
|
|
|
|
|
2024-02-11 18:50:25 +01:00
|
|
|
pure $ FontDescriptor
|
|
|
|
{ fontName = variableText nameRecord variable psNameIndex
|
|
|
|
, flags = []
|
2024-02-12 14:05:39 +01:00
|
|
|
, stemV = calculateStemV $ fromIntegral weightClass
|
2024-02-11 18:50:25 +01:00
|
|
|
, missingWidth = 0
|
|
|
|
, 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
|
|
|
|
os2Entry <- maybeMetricsError (MetricsRequiredTableMissingError "OS/2")
|
|
|
|
$ findTableByTag "OS/2" initialResult
|
|
|
|
os2Table <- first MetricsParseError
|
|
|
|
$ parseTable os2Entry os2TableP processedState
|
|
|
|
case os2Table of
|
|
|
|
Os2Version4CommonFields Os2BaseFields{ usWeightClass } Os2Version4Fields{ sCapHeight } ->
|
|
|
|
Right (sCapHeight, usWeightClass)
|
|
|
|
Os2Version5 Os2BaseFields{ usWeightClass } _ Os2Version5Fields{ sCapHeight } ->
|
|
|
|
Right (sCapHeight, usWeightClass)
|
|
|
|
_ -> 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'
|
|
|
|
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
|