Extract some convenience parsing functions

This commit is contained in:
Eugen Wissner 2024-02-07 10:40:00 +01:00
parent 23271d6f6c
commit c5f715ac7c
Signed by: belka
GPG Key ID: A27FDC1E8EE902C0
9 changed files with 139 additions and 64 deletions

View File

@ -65,3 +65,15 @@ executable fountainhead
time time
hs-source-dirs: src hs-source-dirs: src
ghc-options: -Wall ghc-options: -Wall
test-suite fountainhead-test
import: dependencies
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
other-modules:
Graphics.Fountainhead.MetricsSpec
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends:
fountainhead,
hspec >= 2.9 && < 3

View File

@ -11,37 +11,24 @@ module Graphics.Fountainhead
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Void (Void) import Data.Void (Void)
import Graphics.Fountainhead.Dumper (dumpTable, dumpTables, DumpError(..)) import Graphics.Fountainhead.Dumper (dumpTable, dumpTables, DumpError(..))
import Graphics.Fountainhead.Parser (ParseErrorBundle, fontDirectoryP) import Graphics.Fountainhead.Parser (ParseErrorBundle, parseFontDirectory)
import Graphics.Fountainhead.TrueType (FontDirectory(..)) import Graphics.Fountainhead.TrueType (FontDirectory(..))
import qualified Text.Megaparsec as Megaparsec import Text.Megaparsec (State(..))
import Text.Megaparsec (PosState(..), State(..))
import System.IO (IOMode(..), withBinaryFile) import System.IO (IOMode(..), withBinaryFile)
import Data.Bifunctor (Bifunctor(..)) import Data.Bifunctor (Bifunctor(..))
import qualified Data.Text.Lazy.Builder as Text.Builder import qualified Data.Text.Lazy.Builder as Text.Builder
import Graphics.Fountainhead.Compression (hDecompress) import Graphics.Fountainhead.Compression (hDecompress)
-- | Does initial parsing of the font at the given path and returns the font
-- directory and parsing state that can be used to parse other tables in the
-- font.
parseFontDirectoryFromFile :: FilePath parseFontDirectoryFromFile :: FilePath
-> IO (State ByteString Void, Either ParseErrorBundle FontDirectory) -> IO (State ByteString Void, Either ParseErrorBundle FontDirectory)
parseFontDirectoryFromFile fontFile = parseFontDirectoryFromFile fontFile = withBinaryFile fontFile ReadMode
withBinaryFile fontFile ReadMode withFontHandle $ fmap (parseFontDirectory fontFile) . hDecompress
where
withFontHandle fontHandle = doParsing
<$> hDecompress fontHandle
doParsing ttfContents =
let initialState = Megaparsec.State
{ stateInput = ttfContents
, stateOffset = 0
, statePosState = Megaparsec.PosState
{ pstateInput = ttfContents
, pstateOffset = 0
, pstateSourcePos = Megaparsec.initialPos fontFile
, pstateTabWidth = Megaparsec.defaultTabWidth
, pstateLinePrefix = ""
}
, stateParseErrors = []
}
in Megaparsec.runParser' fontDirectoryP initialState
-- | Dumps the contents of the font in the file. If the table name is given,
-- dumps only this one table.
dumpFontFile :: FilePath -> Maybe String -> IO (Either DumpError Text.Builder.Builder) dumpFontFile :: FilePath -> Maybe String -> IO (Either DumpError Text.Builder.Builder)
dumpFontFile fontFile tableName = do dumpFontFile fontFile tableName = do
let dumpRequest = maybe dumpTables dumpTable tableName let dumpRequest = maybe dumpTables dumpTable tableName

View File

@ -26,13 +26,10 @@ module Graphics.Fountainhead.Dumper
, dumpPost , dumpPost
, dumpTable , dumpTable
, dumpTables , dumpTables
, dumpTrueType
, dumpOffsetTable , dumpOffsetTable
) where ) where
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as Char8
import Data.Int (Int64, Int16) import Data.Int (Int64, Int16)
import Data.Word (Word8, Word16, Word32) import Data.Word (Word8, Word16, Word32)
import qualified Data.IntMap as IntMap import qualified Data.IntMap as IntMap
@ -44,7 +41,6 @@ import qualified Data.Text.Lazy.Builder.Int as Text.Builder
import qualified Data.Text.Lazy.Builder.RealFloat as Text.Builder import qualified Data.Text.Lazy.Builder.RealFloat as Text.Builder
import Data.Vector (Vector) import Data.Vector (Vector)
import qualified Data.Vector as Vector import qualified Data.Vector as Vector
import Data.Void
import GHC.Records (HasField(..)) import GHC.Records (HasField(..))
import Graphics.Fountainhead.TrueType import Graphics.Fountainhead.TrueType
( CmapTable(..) ( CmapTable(..)
@ -94,12 +90,12 @@ import Graphics.Fountainhead.TrueType
, OutlineFlag(..) , OutlineFlag(..)
, ComponentGlyphFlags(..) , ComponentGlyphFlags(..)
, GlyphTransformationOption(..) , GlyphTransformationOption(..)
, findTableByTag
) )
import qualified Text.Megaparsec as Megaparsec import qualified Text.Megaparsec as Megaparsec
import Graphics.Fountainhead.Parser import Graphics.Fountainhead.Parser
( ParseErrorBundle ( ParseErrorBundle
, ParseState , ParseState
, fontDirectoryP
, parseTable , parseTable
, cmapTableP , cmapTableP
, headTableP , headTableP
@ -118,10 +114,9 @@ import Graphics.Fountainhead.Type
( Fixed32(..) ( Fixed32(..)
, succIntegral , succIntegral
, ttfEpoch , ttfEpoch
, newlineBuilder
, fixed2Double , fixed2Double
) )
import Data.Foldable (Foldable(..), find) import Data.Foldable (Foldable(..))
import Data.Maybe (fromMaybe, catMaybes) import Data.Maybe (fromMaybe, catMaybes)
import Data.Time (LocalTime(..), NominalDiffTime, diffLocalTime, midnight) import Data.Time (LocalTime(..), NominalDiffTime, diffLocalTime, midnight)
import Data.Bits (Bits(..), (.>>.)) import Data.Bits (Bits(..), (.>>.))
@ -130,7 +125,7 @@ import Data.List (intersperse)
import Prelude hiding (repeat) import Prelude hiding (repeat)
data DumpError data DumpError
= DumpParseError (Megaparsec.ParseErrorBundle ByteString Void) = DumpParseError ParseErrorBundle
| DumpRequiredTableMissingError String | DumpRequiredTableMissingError String
| DumpRequestedTableMissingError String | DumpRequestedTableMissingError String
deriving Eq deriving Eq
@ -149,6 +144,9 @@ data RequiredTables = RequiredTables
, locaTable :: LocaTable , locaTable :: LocaTable
} deriving (Eq, Show) } deriving (Eq, Show)
newlineBuilder :: Text.Builder.Builder
newlineBuilder = Text.Builder.singleton '\n'
paddedHexadecimal :: Integral a => a -> Text.Builder.Builder paddedHexadecimal :: Integral a => a -> Text.Builder.Builder
paddedHexadecimal = ("0x" <>) paddedHexadecimal = ("0x" <>)
. Text.Builder.fromLazyText . Text.Builder.fromLazyText
@ -797,9 +795,9 @@ dumpTable
-> ParseState -> ParseState
-> FontDirectory -> FontDirectory
-> Either DumpError Text.Builder.Builder -> Either DumpError Text.Builder.Builder
dumpTable needle processedState FontDirectory{..} dumpTable needle processedState fontDirectory
| Just neededTable <- find ((needle ==) . Char8.unpack . getField @"tag") tableDirectory | Just neededTable <- findTableByTag needle fontDirectory
= parseRequired processedState tableDirectory = parseRequired processedState fontDirectory
>>= maybe (pure mempty) (first DumpParseError) >>= maybe (pure mempty) (first DumpParseError)
. dumpSubTable processedState neededTable . dumpSubTable processedState neededTable
| otherwise = Left $ DumpRequestedTableMissingError needle | otherwise = Left $ DumpRequestedTableMissingError needle
@ -809,7 +807,7 @@ dumpTables
-> FontDirectory -> FontDirectory
-> Either DumpError Text.Builder.Builder -> Either DumpError Text.Builder.Builder
dumpTables processedState directory@FontDirectory{..} dumpTables processedState directory@FontDirectory{..}
= parseRequired processedState tableDirectory >>= traverseDirectory = parseRequired processedState directory >>= traverseDirectory
where where
traverseDirectory parsedRequired = traverseDirectory parsedRequired =
let initial = Right $ dumpOffsetTable directory let initial = Right $ dumpOffsetTable directory
@ -821,12 +819,8 @@ dumpTables processedState directory@FontDirectory{..}
concatDump accumulator builderDump = ((accumulator <> newlineBuilder) <>) concatDump accumulator builderDump = ((accumulator <> newlineBuilder) <>)
<$> builderDump <$> builderDump
parseRequired parseRequired :: ParseState -> FontDirectory -> Either DumpError RequiredTables
:: (Foldable t) parseRequired processedState fontDirectory = do
=> ParseState
-> t TableDirectory
-> Either DumpError RequiredTables
parseRequired processedState tableDirectory = do
requiredHhea <- findRequired "hhea" hheaTableP requiredHhea <- findRequired "hhea" hheaTableP
requiredHead@HeadTable{ indexToLocFormat } <- requiredHead@HeadTable{ indexToLocFormat } <-
findRequired "head" headTableP findRequired "head" headTableP
@ -841,7 +835,7 @@ parseRequired processedState tableDirectory = do
let missingError = Left $ DumpRequiredTableMissingError tableName let missingError = Left $ DumpRequiredTableMissingError tableName
parseFound tableEntry = parseTable tableEntry parser processedState parseFound tableEntry = parseTable tableEntry parser processedState
in maybe missingError (first DumpParseError . parseFound) in maybe missingError (first DumpParseError . parseFound)
$ find ((== Char8.pack tableName) . getField @"tag") tableDirectory $ findTableByTag tableName fontDirectory
dumpSubTable dumpSubTable
:: ParseState :: ParseState
@ -864,21 +858,3 @@ dumpSubTable processedState tableEntry RequiredTables{..} =
"gasp" -> Just $ dumpGASP <$> parseTable tableEntry gaspTableP processedState "gasp" -> Just $ dumpGASP <$> parseTable tableEntry gaspTableP processedState
"glyf" -> Just $ dumpGlyf <$> parseTable tableEntry (glyfTableP locaTable) processedState "glyf" -> Just $ dumpGlyf <$> parseTable tableEntry (glyfTableP locaTable) processedState
_ -> Nothing _ -> Nothing
dumpTrueType :: ByteString -> FilePath -> Either DumpError Text.Builder.Builder
dumpTrueType ttfContents fontFile =
let initialState = Megaparsec.State
{ stateInput = ttfContents
, stateOffset = 0
, statePosState = Megaparsec.PosState
{ pstateInput = ttfContents
, pstateOffset = 0
, pstateSourcePos = Megaparsec.initialPos fontFile
, pstateTabWidth = Megaparsec.defaultTabWidth
, pstateLinePrefix = ""
}
, stateParseErrors = []
}
(processedState, initialResult) = Megaparsec.runParser' fontDirectoryP initialState
in first DumpParseError initialResult >>= dumpTables processedState

View File

@ -7,11 +7,22 @@
module Graphics.Fountainhead.Metrics module Graphics.Fountainhead.Metrics
( FontBBox(..) ( FontBBox(..)
, FontDescriptor(..) , FontDescriptor(..)
, MetricsError(..)
, Number , Number
, FontDescriptorFlag(..) , FontDescriptorFlag(..)
, collectMetrics
) where ) where
import Data.ByteString (ByteString)
import Data.Text (Text) import Data.Text (Text)
import Graphics.Fountainhead.TrueType (findTableByTag)
import Graphics.Fountainhead.Parser
( ParseErrorBundle
, nameTableP
, parseFontDirectory
, parseTable
)
import qualified Text.Megaparsec as Megaparsec
type Number = Double type Number = Double
@ -66,3 +77,27 @@ data FontDescriptor = FontDescriptor
, ascender :: Number , ascender :: Number
, descender :: Number , descender :: Number
} deriving (Eq, Show) } deriving (Eq, Show)
data MetricsError
= MetricsParseError ParseErrorBundle
| MetricsRequiredTableMissingError String
deriving Eq
instance Show MetricsError
where
show (MetricsParseError errorBundle) = Megaparsec.errorBundlePretty errorBundle
show (MetricsRequiredTableMissingError tableName) =
"Required table " <> tableName <> " is missing."
collectMetrics :: FilePath -> ByteString -> Either MetricsError FontDescriptor
collectMetrics fontFile ttfContents =
case parseFontDirectory fontFile ttfContents of
(_processedState, Left initialResult) -> Left
$ MetricsParseError initialResult
(processedState, Right initialResult)
| Just tableEntry <- findTableByTag "name" initialResult
, Right parsedNameTable <- parseTable tableEntry nameTableP processedState ->
pure $ FontDescriptor
{
}
| otherwise -> Left $ MetricsRequiredTableMissingError "name"

View File

@ -33,6 +33,7 @@ module Graphics.Fountainhead.Parser
, nameTableP , nameTableP
, os2TableP , os2TableP
, panoseP , panoseP
, parseFontDirectory
, parseTable , parseTable
, pascalStringP , pascalStringP
, postTableP , postTableP
@ -157,6 +158,29 @@ type Parser = Megaparsec.Parsec Void ByteString
type ParseErrorBundle = Megaparsec.ParseErrorBundle ByteString Void type ParseErrorBundle = Megaparsec.ParseErrorBundle ByteString Void
type ParseState = Megaparsec.State ByteString Void type ParseState = Megaparsec.State ByteString Void
-- | Does initial parsing and returns the font directory and parsing state
-- that can be used to parse other tables in the font.
--
-- Font file name can be empty.
parseFontDirectory
:: FilePath
-> ByteString
-> (ParseState, Either ParseErrorBundle FontDirectory)
parseFontDirectory fontFile ttfContents =
let initialState = Megaparsec.State
{ stateInput = ttfContents
, stateOffset = 0
, statePosState = Megaparsec.PosState
{ pstateInput = ttfContents
, pstateOffset = 0
, pstateSourcePos = Megaparsec.initialPos fontFile
, pstateTabWidth = Megaparsec.defaultTabWidth
, pstateLinePrefix = ""
}
, stateParseErrors = []
}
in Megaparsec.runParser' fontDirectoryP initialState
-- * Font directory -- * Font directory
offsetSubtableP :: Parser OffsetSubtable offsetSubtableP :: Parser OffsetSubtable

View File

@ -2,6 +2,7 @@
v. 2.0. If a copy of the MPL was not distributed with this file, You can 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/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
@ -81,11 +82,13 @@ module Graphics.Fountainhead.TrueType
, UVSMapping(..) , UVSMapping(..)
, UnicodeValueRange(..) , UnicodeValueRange(..)
, VariationSelectorMap , VariationSelectorMap
, findTableByTag
, unLocaTable , unLocaTable
, nameStringOffset , nameStringOffset
) where ) where
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as Char8
import Data.Int (Int8, Int16) import Data.Int (Int8, Int16)
import Data.IntMap (IntMap) import Data.IntMap (IntMap)
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
@ -93,6 +96,8 @@ import Data.Time (LocalTime(..))
import Data.Vector (Vector) import Data.Vector (Vector)
import Data.Word (Word8, Word16, Word32) import Data.Word (Word8, Word16, Word32)
import Graphics.Fountainhead.Type (F2Dot14(..), Fixed32(..), FWord, UFWord) import Graphics.Fountainhead.Type (F2Dot14(..), Fixed32(..), FWord, UFWord)
import GHC.Records (HasField(..))
import Data.Foldable (find)
-- * Font directory -- * Font directory
@ -101,6 +106,10 @@ data FontDirectory = FontDirectory
, tableDirectory :: [TableDirectory] , tableDirectory :: [TableDirectory]
} deriving (Eq, Show) } deriving (Eq, Show)
findTableByTag :: String -> FontDirectory -> Maybe TableDirectory
findTableByTag needle = find ((needle ==) . Char8.unpack . getField @"tag")
. getField @"tableDirectory"
data OffsetSubtable = OffsetSubtable data OffsetSubtable = OffsetSubtable
{ scalerType :: Word32 { scalerType :: Word32
, numTables :: Int , numTables :: Int

View File

@ -9,12 +9,10 @@ module Graphics.Fountainhead.Type
, FWord , FWord
, UFWord , UFWord
, fixed2Double , fixed2Double
, newlineBuilder
, succIntegral , succIntegral
, ttfEpoch , ttfEpoch
) where ) where
import qualified Data.Text.Lazy.Builder as Text.Builder
import Data.Bits ((.>>.), (.&.)) import Data.Bits ((.>>.), (.&.))
import Data.Int (Int16) import Data.Int (Int16)
import Data.Word (Word16, Word32) import Data.Word (Word16, Word32)
@ -41,6 +39,3 @@ fixed2Double (F2Dot14 fixed) =
let mantissa = realToFrac (fixed .>>. 14) let mantissa = realToFrac (fixed .>>. 14)
fraction = realToFrac (fixed .&. 0x3fff) / 16384.0 fraction = realToFrac (fixed .&. 0x3fff) / 16384.0
in mantissa + fraction in mantissa + fraction
newlineBuilder :: Text.Builder.Builder
newlineBuilder = Text.Builder.singleton '\n'

View File

@ -0,0 +1,32 @@
{- 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 OverloadedStrings #-}
module Graphics.Fountainhead.MetricsSpec
( spec
) where
import Graphics.Fountainhead.Metrics
import Test.Hspec (Spec, describe, it, shouldBe)
spec :: Spec
spec =
describe "collectMetrics" $
it "collects information from the name table" $ do
let expected = FontDescriptor
{ fontName = "fontName"
, flags = []
, fullName = "fullName"
, familyName = "familyName"
, weight = "weight"
, fontBBox = FontBBox 0 0 0 0
, version = "1.0.0"
, notice = "Notice"
, encodingScheme = "encodingScheme"
, isFixedPitch = False
, ascender = 0
, descender = 0
}
in collectMetrics `shouldBe` expected

5
test/Spec.hs Normal file
View File

@ -0,0 +1,5 @@
{- 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/. -}
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}