diff options
Diffstat (limited to 'lib/Graphics/Fountainhead/Dumper.hs')
| -rw-r--r-- | lib/Graphics/Fountainhead/Dumper.hs | 854 |
1 files changed, 854 insertions, 0 deletions
diff --git a/lib/Graphics/Fountainhead/Dumper.hs b/lib/Graphics/Fountainhead/Dumper.hs new file mode 100644 index 0000000..bbb17c2 --- /dev/null +++ b/lib/Graphics/Fountainhead/Dumper.hs @@ -0,0 +1,854 @@ +{- 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 PatternGuards #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TypeApplications #-} + +-- | Outputs information about a font as text. +module Graphics.Fountainhead.Dumper + ( DumpError(..) + , dumpCmap + , dumpGlyf + , dumpHead + , dumpHmtx + , dumpHhea + , dumpLoca + , dumpName + , dumpMaxp + , dumpOs2 + , dumpPost + , dumpTables + , dumpTrueType + , dumpOffsetTable + ) where + +import Data.ByteString (ByteString) +import qualified Data.ByteString as ByteString +import qualified Data.ByteString.Char8 as Char8 +import Data.Int (Int64, Int16) +import Data.Word (Word8, Word16, Word32) +import qualified Data.IntMap as IntMap +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import qualified Data.Text.Lazy as Text.Lazy +import qualified Data.Text.Lazy.Builder as Text.Builder +import qualified Data.Text.Lazy.Builder.Int as Text.Builder +import qualified Data.Text.Lazy.Builder.RealFloat as Text.Builder +import Data.Vector (Vector) +import qualified Data.Vector as Vector +import Data.Void +import GHC.Records (HasField(..)) +import Graphics.Fountainhead.TrueType + ( CmapTable(..) + , CompoundGlyphDefinition(..) + , ComponentGlyphPartDescription(..) + , FontDirectory(..) + , FontDirectionHint(..) + , GASPRange(..) + , GASPTable(..) + , GlyphArgument(..) + , HeadTable(..) + , HheaTable(..) + , HmtxTable(..) + , OffsetSubtable(..) + , PostHeader(..) + , PostSubtable(..) + , PostFormat2Table(..) + , PostTable(..) + , TableDirectory(..) + , CmapEncoding(..) + , CmapSubtable(..) + , CmapFormat4Table(..) + , FontStyle(..) + , GlyphArgument(..) + , GlyphCoordinate(..) + , GlyphDefinition(..) + , GlyphDescription(..) + , GlyfTable(..) + , LongHorMetric(..) + , LocaTable(..) + , NameRecord (..) + , NameTable(..) + , IndexToLocFormat(..) + , OpenMaxpTable(..) + , MaxpTable(..) + , TrueMaxpTable(..) + , nameStringOffset + , Os2BaseFields(..) + , Os2MicrosoftFields(..) + , Os2Version1Fields(..) + , Os2Version4Fields(..) + , Os2Version5Fields(..) + , Os2Table(..) + , Panose(..) + , SimpleGlyphDefinition(..) + , CVTable(..) + , OutlineFlag(..) + , ComponentGlyphFlags(..) + , GlyphTransformationOption(..) + ) +import qualified Text.Megaparsec as Megaparsec +import Graphics.Fountainhead.Parser + ( fontDirectoryP + , parseTable + , cmapTableP + , headTableP + , hheaTableP + , hmtxTableP + , gaspTableP + , locaTableP + , maxpTableP + , nameTableP + , os2TableP + , postTableP + , cvTableP + , glyfTableP + ) +import Graphics.Fountainhead.Type + ( Fixed32(..) + , succIntegral + , ttfEpoch + , fixed2Double + ) +import Data.Foldable (Foldable(..), find) +import Data.Maybe (fromMaybe, catMaybes) +import Data.Time (LocalTime(..), NominalDiffTime, diffLocalTime, midnight) +import Data.Bits (Bits(..), (.>>.)) +import Data.Bifunctor (Bifunctor(first)) +import Data.List (intersperse) +import Prelude hiding (repeat) + +data DumpError + = DumpParseError (Megaparsec.ParseErrorBundle ByteString Void) + | DumpRequiredTableMissingError String + deriving Eq + +instance Show DumpError + where + show (DumpParseError errorBundle) = Megaparsec.errorBundlePretty errorBundle + show (DumpRequiredTableMissingError tableName) = + "Required table " <> tableName <> " is missing." + +data RequiredTables = RequiredTables + { hheaTable :: HheaTable + , headTable :: HeadTable + , locaTable :: LocaTable + } deriving (Eq, Show) + +paddedHexadecimal :: Integral a => a -> Text.Builder.Builder +paddedHexadecimal = ("0x" <>) + . Text.Builder.fromLazyText + . Text.Lazy.justifyRight 8 '0' + . Text.Builder.toLazyText + . Text.Builder.hexadecimal + +halfPaddedHexadecimal :: Integral a => a -> Text.Builder.Builder +halfPaddedHexadecimal = Text.Builder.fromLazyText + . Text.Lazy.justifyRight 4 '0' + . Text.Builder.toLazyText + . Text.Builder.hexadecimal + +justifyNumber :: Integral a => Int64 -> a -> Text.Builder.Builder +justifyNumber count = Text.Builder.fromLazyText + . Text.Lazy.justifyRight count ' ' + . Text.Builder.toLazyText + . Text.Builder.decimal + +newlineBuilder :: Text.Builder.Builder +newlineBuilder = Text.Builder.singleton '\n' + +dumpCaption :: String -> Text.Builder.Builder +dumpCaption headline = Text.Builder.fromString headline + <> newlineBuilder + <> Text.Builder.fromLazyText (Text.Lazy.replicate headlineLength "-") + <> newlineBuilder + where + headlineLength = fromIntegral $ Prelude.length headline + +dumpOffsetTable :: FontDirectory -> Text.Builder.Builder +dumpOffsetTable directory + = dumpCaption "Offset Table" + <> " sfnt version: 1.0\n number of tables: " + <> Text.Builder.decimal (numTables $ offsetSubtable directory) + <> newlineBuilder + <> dumpOffsetSummary (tableDirectory directory) + where + dumpOffsetSummary = mconcat . fmap dumpOffsetRow . zip [0 :: Int ..] + dumpOffsetRow (index, table) = justifyNumber 4 index + <> ". '" + <> Text.Builder.fromText (Text.decodeASCII $ tag table) + <> "' - checksum = " + <> paddedHexadecimal (getField @"checkSum" table) + <> ", offset = " + <> paddedHexadecimal (getField @"offset" table) + <> ", len = " + <> justifyNumber 9 (getField @"length" table) + <> newlineBuilder + +dumpFixed32 :: Fixed32 -> Text.Builder.Builder +dumpFixed32 (Fixed32 word) + = Text.Builder.decimal (shiftR word 16) + <> Text.Builder.singleton '.' + <> Text.Builder.decimal (word .&. 0xff00) + +dumpHmtx :: HmtxTable -> Text.Builder.Builder +dumpHmtx HmtxTable{..} = + let caption = dumpCaption "'hmtx' Table - Horizontal Metrics" + lastAccumulator = foldl' dumpHMetric (0 :: Int, caption) hMetrics + in snd $ foldl' dumpLeftSideBear lastAccumulator leftSideBearing + where + dumpLeftSideBear (index, accumulator) leftSideBearing' = + let withNewLine = dumpIndex index <> ". LSbear: " + <> justifyNumber 4 leftSideBearing' <> newlineBuilder + in (succ index, accumulator <> withNewLine) + dumpHMetric (index, accumulator) metric = + let LongHorMetric{ leftSideBearing = leftSideBearing', ..} = metric + withNewLine = dumpIndex index <> ". advWid: " + <> justifyNumber 4 advanceWidth <> ", LSBear: " + <> justifyNumber 4 leftSideBearing' <> newlineBuilder + in (succ index, accumulator <> withNewLine) + dumpIndex = justifyNumber 12 + +dumpHhea :: HheaTable -> Text.Builder.Builder +dumpHhea HheaTable{..} + = dumpCaption "'hhea' Table - Horizontal Header" + <> " 'hhea' version: " <> dumpFixed32 version <> newlineBuilder + <> " yAscender: " <> Text.Builder.decimal ascent <> newlineBuilder + <> " yDescender: " <> Text.Builder.decimal descent <> newlineBuilder + <> " yLineGap: " <> Text.Builder.decimal lineGap <> newlineBuilder + <> " advanceWidthMax: " <> Text.Builder.decimal advanceWidthMax <> newlineBuilder + <> " minLeftSideBearing: " <> Text.Builder.decimal minLeftSideBearing <> newlineBuilder + <> " minRightSideBearing: " <> Text.Builder.decimal minRightSideBearing <> newlineBuilder + <> " xMaxExtent: " <> Text.Builder.decimal xMaxExtent <> newlineBuilder + <> " caretSlopeRise: " <> Text.Builder.decimal caretSlopeRise <> newlineBuilder + <> " caretSlopeRun: " <> Text.Builder.decimal caretSlopeRun <> newlineBuilder + <> " reserved0: 0" <> newlineBuilder + <> " reserved1: 0" <> newlineBuilder + <> " reserved2: 0" <> newlineBuilder + <> " reserved3: 0" <> newlineBuilder + <> " reserved4: 0" <> newlineBuilder + <> " metricDataFormat: " <> Text.Builder.decimal metricDataFormat <> newlineBuilder + <> " numberOfHMetrics: " <> Text.Builder.decimal numOfLongHorMetrics <> newlineBuilder + +dumpHead :: HeadTable -> Text.Builder.Builder +dumpHead HeadTable{..} + = dumpCaption "'head' Table - Font Header" + <> " head version: " <> dumpFixed32 version <> newlineBuilder + <> " fontRevision: " <> dumpFixed32 fontRevision <> newlineBuilder + <> " checkSumAdjustment: " <> paddedHexadecimal checkSumAdjustment <> newlineBuilder + <> " magicNumber: " <> paddedHexadecimal magicNumber <> newlineBuilder + <> " flags: 0x" <> halfPaddedHexadecimal flags <> newlineBuilder + <> " unitsPerEm: " <> Text.Builder.decimal unitsPerEm <> newlineBuilder + <> " created: " <> "0x" <> longDateTime created <> newlineBuilder + <> " modified: " <> "0x" <> longDateTime modified <> newlineBuilder + <> " xMin: " <> Text.Builder.decimal xMin <> newlineBuilder + <> " yMin: " <> Text.Builder.decimal yMin <> newlineBuilder + <> " xMax: " <> Text.Builder.decimal xMax <> newlineBuilder + <> " yMax: " <> Text.Builder.decimal yMax <> newlineBuilder + <> " macStyle bits: " <> "0x" <> dumpFontStyle macStyle <> newlineBuilder + <> " lowestRecPPEM " <> Text.Builder.decimal lowestRecPPEM <> newlineBuilder + <> " fontDirectionHint " <> dumpFontDirectionHint fontDirectionHint <> newlineBuilder + <> " indexToLocFormat " <> dumpIndexToLocFormat indexToLocFormat <> newlineBuilder + <> " glyphDataFormat " <> Text.Builder.decimal glyphDataFormat <> newlineBuilder + +dumpIndexToLocFormat :: IndexToLocFormat -> Text.Builder.Builder +dumpIndexToLocFormat ShortOffsetIndexToLocFormat = "0" +dumpIndexToLocFormat LongOffsetIndexToLocFormat = "1" + +dumpFontDirectionHint :: FontDirectionHint -> Text.Builder.Builder +dumpFontDirectionHint = \case + MixedDirectionalGlyphs -> "0" + StronglyLeftToRightGlyphs -> "1" + LeftToRightGlyphsWithNeutrals -> "2" + StronglyRightToLeftGlyphs -> "-1" + RightToLeftGlyphsWithNeutrals -> "-2" + +dumpFontStyle :: FontStyle -> Text.Builder.Builder +dumpFontStyle FontStyle{..} = halfPaddedHexadecimal + $ foldr (go . fst) (zeroBits :: Int) + $ filter snd + $ zip [0..] [bold, italic, underline, outline, shadow, condensed, extended] + where + go bitNumber accumulator = setBit accumulator bitNumber + +longDateTime :: LocalTime -> Text.Builder.Builder +longDateTime localTime = Text.Builder.fromLazyText + $ Text.Lazy.justifyRight 16 '0' + $ Text.Builder.toLazyText + $ Text.Builder.hexadecimal + $ (truncate :: NominalDiffTime -> Int) + $ diffLocalTime localTime (LocalTime ttfEpoch midnight) + +dumpCVTable :: CVTable -> Text.Builder.Builder +dumpCVTable (CVTable cvTable) = dumpCaption "'cvt ' Table - Control Value Table" + <> "Size = " <> Text.Builder.decimal (tableSize * 2) + <> " bytes, " <> Text.Builder.decimal tableSize <> " entries\n" + <> foldMap (uncurry go) (zip [0..] cvTable) + where + tableSize = Prelude.length cvTable + go :: Int -> Int16 -> Text.Builder.Builder + go index' entry = justifyNumber 13 index' <> ". " + <> Text.Builder.decimal entry <> newlineBuilder + +dumpOs2 :: Os2Table -> Text.Builder.Builder +dumpOs2 = (dumpCaption "'OS/2' Table - OS/2 and Windows Metrics" <>) . go + where + go = \case + Os2Version0 baseFields msFields -> dumpBaseFields baseFields + <> maybe "" dumpMicrosoftFields msFields + Os2Version1 baseFields msFields extraFields -> dumpBaseFields baseFields + <> dumpMicrosoftFields msFields <> dumpVersion1Fields extraFields + Os2Version2 baseFields msFields extraFields -> dumpBaseFields baseFields + <> dumpMicrosoftFields msFields <> dumpVersion4Fields extraFields + Os2Version3 baseFields msFields extraFields -> dumpBaseFields baseFields + <> dumpMicrosoftFields msFields <> dumpVersion4Fields extraFields + Os2Version4 baseFields msFields extraFields -> dumpBaseFields baseFields + <> dumpMicrosoftFields msFields <> dumpVersion4Fields extraFields + Os2Version5 baseFields msFields extraFields -> dumpBaseFields baseFields + <> dumpMicrosoftFields msFields <> dumpVersion5Fields extraFields + dumpVersion1Fields Os2Version1Fields{..} + = " CodePage Range 1( Bits 0 - 31 ): " <> paddedHexadecimal ulCodePageRange1 <> newlineBuilder + <> " CodePage Range 2( Bits 32- 63 ): " <> paddedHexadecimal ulCodePageRange2 <> newlineBuilder + dumpVersion4Fields Os2Version4Fields{..} + = dumpVersion1Fields (Os2Version1Fields ulCodePageRange1 ulCodePageRange2) + <> " sxHeight: " <> Text.Builder.decimal sxHeight <> newlineBuilder + <> " sCapHeight: " <> Text.Builder.decimal sCapHeight <> newlineBuilder + <> " usDefaultChar: 0x" <> halfPaddedHexadecimal usDefaultChar <> newlineBuilder + <> " usBreakChar: 0x" <> halfPaddedHexadecimal usBreakChar <> newlineBuilder + <> " usMaxContext: " <> Text.Builder.decimal usMaxContext <> newlineBuilder + dumpVersion5Fields Os2Version5Fields{..} + = dumpVersion1Fields (Os2Version1Fields ulCodePageRange1 ulCodePageRange2) + <> " sxHeight: " <> Text.Builder.decimal sxHeight <> newlineBuilder + <> " sCapHeight: " <> Text.Builder.decimal sCapHeight <> newlineBuilder + <> " usDefaultChar: 0x" <> halfPaddedHexadecimal usDefaultChar <> newlineBuilder + <> " usBreakChar: 0x" <> halfPaddedHexadecimal usBreakChar <> newlineBuilder + <> " usMaxContext: " <> Text.Builder.decimal usMaxContext <> newlineBuilder + <> " usLowerOpticalPointSize: " + <> Text.Builder.decimal usLowerOpticalPointSize <> newlineBuilder + <> " usUpperOpticalPointSize: " + <> Text.Builder.decimal usUpperOpticalPointSize <> newlineBuilder + dumpMicrosoftFields Os2MicrosoftFields{..} + = " sTypoAscender: " <> Text.Builder.decimal sTypoAscender <> newlineBuilder + <> " sTypoDescender: " <> Text.Builder.decimal sTypoDescender <> newlineBuilder + <> " sTypoLineGap: " <> Text.Builder.decimal sTypoLineGap <> newlineBuilder + <> " usWinAscent: " <> Text.Builder.decimal usWinAscent <> newlineBuilder + <> " usWinDescent: " <> Text.Builder.decimal usWinDescent <> newlineBuilder + dumpBaseFields Os2BaseFields{..} + = " 'OS/2' version: " <> Text.Builder.decimal version <> newlineBuilder + <> " xAvgCharWidth: " <> Text.Builder.decimal xAvgCharWidth <> newlineBuilder + <> " usWeightClass: " <> weightClass usWeightClass <> newlineBuilder + <> " usWidthClass: " <> widthClass usWidthClass <> newlineBuilder + <> " fsType: " <> Text.Builder.decimal fsType <> newlineBuilder + <> " ySubscriptXSize: " <> Text.Builder.decimal ySubscriptXSize <> newlineBuilder + <> " ySubscriptYSize: " <> Text.Builder.decimal ySubscriptYSize <> newlineBuilder + <> " ySubscriptXOffset: " <> Text.Builder.decimal ySubscriptXOffset <> newlineBuilder + <> " ySubscriptYOffset: " <> Text.Builder.decimal ySubscriptYOffset <> newlineBuilder + <> " ySuperscriptXSize: " <> Text.Builder.decimal ySuperscriptXSize <> newlineBuilder + <> " ySuperscriptYSize: " <> Text.Builder.decimal ySuperscriptYSize <> newlineBuilder + <> " ySuperscriptXOffset: " <> Text.Builder.decimal ySuperscriptXOffset <> newlineBuilder + <> " ySuperscriptYOffset: " <> Text.Builder.decimal ySuperscriptYOffset <> newlineBuilder + <> " yStrikeoutSize: " <> Text.Builder.decimal yStrikeoutSize <> newlineBuilder + <> " yStrikeoutPosition: " <> Text.Builder.decimal yStrikeoutPosition <> newlineBuilder + <> " sFamilyClass:" <> familyClass sFamilyClass <> newlineBuilder + <> " PANOSE:" <> newlineBuilder <> dumpPanose panose + <> fold (Vector.imap dumpUnicodeRange ulUnicodeRange) + <> " achVendID: '" <> achVendID' achVendID <> "'\n" + <> " fsSelection: 0x" <> fsSelection' fsSelection <> newlineBuilder + <> " usFirstCharIndex: 0x" <> halfPaddedHexadecimal fsFirstCharIndex <> newlineBuilder + <> " usLastCharIndex: 0x" <> halfPaddedHexadecimal fsLastCharIndex <> newlineBuilder + fsSelection' value = + let description = fold + [ if testBit value 0 then "Italic " else "" + , if testBit value 5 then "Bold " else "" + ] + in halfPaddedHexadecimal value <> " '" <> description <> "'" + achVendID' = Text.Builder.fromText . Text.decodeLatin1 . ByteString.pack . fmap fromIntegral . toList + dumpUnicodeRange index value = + let bits = index * 32 + parens = "( Bits " <> Text.Builder.decimal bits <> " - " + <> Text.Builder.decimal (bits + 31) <> " ):" + in " Unicode Range: " <> Text.Builder.decimal (index + 1) + <> Text.Builder.fromLazyText (Text.Lazy.justifyLeft 25 ' ' (Text.Builder.toLazyText parens)) + <> paddedHexadecimal value + <> newlineBuilder + dumpPanose Panose{..} + = " Family Kind: " <> dumpPanoseField bFamilyType + <> " Serif Style: " <> dumpPanoseField bSerifStyle + <> " Weight: " <> dumpPanoseField bWeight + <> " Proportion: " <> dumpPanoseField bProportion + <> " Contrast: " <> dumpPanoseField bContrast + <> " Stroke: " <> dumpPanoseField bStrokeVariation + <> " Arm Style: " <> dumpPanoseField bArmStyle + <> " Lettreform: " <> dumpPanoseField bLetterform + <> " Midline: " <> dumpPanoseField bMidline + <> " X-height: " <> dumpPanoseField bXHeight + dumpPanoseField field' = + let numericField = Text.Builder.fromLazyText + $ Text.Lazy.justifyLeft 8 ' ' + $ Text.Builder.toLazyText + $ Text.Builder.decimal + $ fromEnum field' + in numericField + <> Text.Builder.singleton '\'' + <> Text.Builder.fromString (show field') + <> Text.Builder.singleton '\'' + <> newlineBuilder + familyClass value = + " " <> Text.Builder.decimal (value .>>. 8) <> " subclass = " <> Text.Builder.decimal (value .&. 0x00ff) + weightClass classValue + | Just wordValue <- fWeight classValue = Text.Builder.decimal classValue <> " '" <> wordValue <> "'" + | otherwise = Text.Builder.decimal classValue + widthClass classValue + | Just wordValue <- fWidth classValue = Text.Builder.decimal classValue <> " '" <> wordValue <> "'" + | otherwise = Text.Builder.decimal classValue + fWeight 100 = Just "Thin" + fWeight 200 = Just "Extra-light" + fWeight 300 = Just "Light" + fWeight 400 = Just "Normal" + fWeight 500 = Just "Medium" + fWeight 600 = Just "Semi-bold" + fWeight 700 = Just "Bold" + fWeight 800 = Just "Extra-bold" + fWeight 900 = Just "Black" + fWeight _ = Nothing + fWidth 1 = Just "Ultra-condensed" + fWidth 2 = Just "Extra-condensed" + fWidth 3 = Just "Condensed" + fWidth 4 = Just "Semi-condensed" + fWidth 5 = Just "Medium" + fWidth 6 = Just "Semi-expanded" + fWidth 7 = Just "Expanded" + fWidth 8 = Just "Extra-expanded" + fWidth 9 = Just "Ultra-expanded" + fWidth _ = Nothing + +dumpPost :: PostTable -> Text.Builder.Builder +dumpPost PostTable{ postHeader = PostHeader{..}, postSubtable } + = dumpCaption "'post' Table - PostScript" <> newlineBuilder + <> " 'post' format: " <> dumpFixed32 format <> newlineBuilder + <> " italicAngle: " <> dumpFixed32 format <> newlineBuilder + <> " underlinePosition: " <> Text.Builder.decimal underlinePosition <> newlineBuilder + <> " underlineThichness: " <> Text.Builder.decimal underlineThickness <> newlineBuilder + <> " isFixedPitch: " <> dNumber isFixedPitch <> newlineBuilder + <> " minMemType42: " <> dNumber minMemType42 <> newlineBuilder + <> " maxMemType42: " <> dNumber maxMemType42 <> newlineBuilder + <> " minMemType1: " <> dNumber minMemType1 <> newlineBuilder + <> " maxMemType1: " <> dNumber maxMemType1 <> newlineBuilder + <> dumpPostSubtable + where + dNumber = (<> Text.Builder.singleton 'd') . Text.Builder.decimal + dumpPostSubtable = case postSubtable of + None -> "" -- Format 1 and 3 do not require a subtable. + PostFormat2 PostFormat2Table{..} + -> " Format 2.0: Non-Standard (for PostScript) TrueType Glyph Set." <> newlineBuilder + <> " numGlyphs: " <> Text.Builder.decimal (Prelude.length glyphNameIndex) + <> newlineBuilder <> fold (Vector.imap (dumpFormat2Pair names) glyphNameIndex) + PostFormat25 _ -> "Format 2.5" + PostFormat4 _ -> "Format 4.0" + dumpFormat2Pair names index glyphNameIndex' + = " Glyf " <> justifyNumber 3 index + <> " -> " <> glyphType names glyphNameIndex' + <> newlineBuilder + glyphType names glyphNameIndex' + | glyphNameIndex' >= 0 + , glyphNameIndex' <= 257 = "Mac Glyph # " <> justifyNumber 3 glyphNameIndex' + | glyphNameIndex' >= 258 + , glyphNameIndex' <= 32767 = + let nameIndex = fromIntegral $ glyphNameIndex' - 258 + in "PSGlyph Name # " <> justifyNumber 3 (succ nameIndex) <> ", '" + <> Text.Builder.fromText (Text.decodeASCII (names Vector.! nameIndex)) + <> Text.Builder.singleton '\'' + | otherwise = "Reserved" + +dumpCmap :: CmapTable -> Text.Builder.Builder +dumpCmap CmapTable{..} + = dumpCaption "'cmap' Table - Character to Glyph Index Mapping Table" + <> " 'cmap' version: " <> Text.Builder.decimal version <> newlineBuilder + <> " number of encodings: " <> Text.Builder.decimal encodingsLength <> newlineBuilder + <> " number of subtables: " <> Text.Builder.decimal (Prelude.length subtables) <> newlineBuilder + <> newlineBuilder + <> snd (foldr dumpCmapEncoding (pred encodingsLength, "") encodings) <> newlineBuilder + <> snd (foldr dumpCmapSubTable (pred subTablesLength, "") subtables) <> newlineBuilder + where + encodingsLength = Prelude.length encodings + subTablesLength = IntMap.size subtables + dumpCmapEncoding CmapEncoding{..} (index, accumulator) = + let findSubTableIndex = Text.Builder.decimal + . Prelude.length + . filter ((< offset) . fromIntegral) + . IntMap.keys + summary = "Encoding " <> Text.Builder.decimal index + <> ". PlatformID: " <> Text.Builder.decimal platformID <> newlineBuilder + <> " EcodingID: " <> Text.Builder.decimal platformSpecificID <> newlineBuilder + <> " SubTable: " <> findSubTableIndex subtables + <> ", Offset: " <> paddedHexadecimal offset <> newlineBuilder + in (pred index, summary <> newlineBuilder <> accumulator) + dumpCmapSubTable currentSubTable (index, accumulator) = + let contents = "SubTable " <> Text.Builder.decimal index + <> ". " <> dumpCmapSubTableFormat currentSubTable + in (pred index, contents <> accumulator) + dumpCmapSubTableFormat = \case + (CmapFormat0 _) -> "Format 0" + (CmapFormat2 _) -> "Format 2" + (CmapFormat4 CmapFormat4Table{..}) -> + let segCount = Vector.length startCode + dumpSegment' = dumpSegment segCount glyphIndexArray + in "Format 4 - Segment mapping to delta values\n\ + \ Length: 994\n\ + \ Version: 0\n\ + \ segCount: " + <> Text.Builder.decimal segCount + <> newlineBuilder <> " searchRange: " + <> Text.Builder.decimal searchRange + <> newlineBuilder <> " entrySelector: " + <> Text.Builder.decimal entrySelector + <> newlineBuilder <> " rangeShift: " + <> Text.Builder.decimal (segCount * 2 - fromIntegral searchRange) + <> newlineBuilder + <> fold (Vector.izipWith4 (dumpSegmentSummary segCount) startCode endCode idDelta idRangeOffset) + <> " Number of glyphIndex " + <> Text.Builder.decimal (Vector.length glyphIndexArray) <> newlineBuilder + <> fold (Vector.imap dumpGlyphAtIndex glyphIndexArray) + <> fold (Vector.izipWith4 dumpSegment' startCode endCode idDelta idRangeOffset) + (CmapFormat6 _) -> "Format 6" + (CmapFormat8 _) -> "Format 8" + (CmapFormat10 _) -> "Format 10" + (CmapFormat12 _) -> "Format 12" + (CmapFormat13 _) -> "Format 13" + (CmapFormat14 _) -> "Format 14" + dumpSegment :: Int -> Vector Word16 -> Int -> Word16 -> Word16 -> Word16 -> Word16 -> Text.Builder.Builder + dumpSegment segCount glyphIndexArray' segmentIndex startCode' endCode' idDelta' idRangeOffset' = + let charRange = [startCode'..endCode'] + dumpSegmentCharIndex' = + dumpSegmentCharIndex segCount glyphIndexArray' segmentIndex idDelta' idRangeOffset' startCode' + in "Segment " <> Text.Builder.decimal segmentIndex <> ":\n" + <> foldMap dumpSegmentCharIndex' charRange + dumpSegmentCharIndex segCount glyphIndexArray' segmentIndex idDelta' idRangeOffset' startCode' charCode = + let calculateGlyphIndex' = + calculateGlyphIndex charCode segmentIndex segCount glyphIndexArray' idRangeOffset' idDelta' startCode' + in " Char 0x" + <> halfPaddedHexadecimal charCode <> " -> Index " + <> Text.Builder.decimal calculateGlyphIndex' + <> newlineBuilder + dumpSegmentSummary segCount index startCode' endCode' idDelta' idRangeOffset' + = " Seg " <> justifyNumber 5 index + <> " : St = " <> halfPaddedHexadecimal startCode' + <> ", En = " <> halfPaddedHexadecimal endCode' + <> ", D = " <> justifyNumber 6 idDelta' + <> ", RO = " <> justifyNumber 6 idRangeOffset' + <> ", gId# = " <> dumpGlyphId index segCount idRangeOffset' + <> newlineBuilder + dumpGlyphId segmentIndex segCount idRangeOffset' + = maybe "N/A" Text.Builder.decimal + $ calculateGlyphId segmentIndex segCount idRangeOffset' + calculateGlyphIndex :: Word16 -> Int -> Int -> Vector Word16 -> Word16 -> Word16 -> Word16 -> Int + calculateGlyphIndex c segmentIndex segCount glyphIndexArray' idRangeOffset' idDelta' startCode' = + let defaultIndex = fromIntegral $ c + idDelta' + addOffset = fromIntegral + . fromMaybe 0 + . (glyphIndexArray' Vector.!?) + . (+ fromIntegral (c - startCode')) + in maybe defaultIndex addOffset + $ calculateGlyphId segmentIndex segCount idRangeOffset' + calculateGlyphId segmentIndex segCount idRangeOffset' + | idRangeOffset' == 0 = Nothing + | otherwise = Just $ segmentIndex - segCount + (fromIntegral idRangeOffset' `div` 2) + dumpGlyphAtIndex index element = " glyphIdArray[" <> Text.Builder.decimal index <> "] = " + <> Text.Builder.decimal element <> newlineBuilder + +dumpLoca :: LocaTable -> Text.Builder.Builder +dumpLoca table = + dumpCaption "'loca' Table - Index to Location" + <> go table + where + go (LongLocaTable elements) = dumpElements elements + go (ShortLocaTable elements) = dumpElements + $ (* 2) + . (fromIntegral :: Word16 -> Word32) + <$> elements + dumpElements elements = + case Vector.unsnoc elements of + Just (init', last') + -> foldMap dumpLocaLine (Vector.indexed init') + <> " Ended at " <> paddedHexadecimal last' <> newlineBuilder + Nothing -> mempty + dumpLocaLine :: Integral a => (Int, a) -> Text.Builder.Builder + dumpLocaLine (index, element) + = " Idx " <> justifyNumber 6 index + <> " -> GlyphOffset " <> paddedHexadecimal element <> newlineBuilder + +dumpName :: NameTable -> Text.Builder.Builder +dumpName table'@NameTable{..} = dumpCaption "'name' Table - Naming Table" + <> " Format: " <> Text.Builder.decimal format <> newlineBuilder + <> " Number of Record: " <> Text.Builder.decimal (Prelude.length nameRecord) <> newlineBuilder + <> " Storage offset: " <> Text.Builder.decimal (nameStringOffset table') <> newlineBuilder + <> foldMap go (zip3 [0 :: Int ..] nameRecord variable) + where + go (index, NameRecord{ length = length', ..}, variable') + = "Name table " <> justifyNumber 3 index <> "." + <> " PlatformID: " <> Text.Builder.decimal platformID <> newlineBuilder + <> " EncodingID: " <> Text.Builder.decimal platformSpecificID <> newlineBuilder + <> " LanguageID: " <> Text.Builder.decimal languageID <> newlineBuilder + <> " NameID: " <> Text.Builder.decimal nameID <> newlineBuilder + <> " Length: " <> Text.Builder.decimal length' <> newlineBuilder + <> " Offset: " <> Text.Builder.decimal offset <> newlineBuilder + <> foldMap (" " <>) (dumpHexString $ ByteString.unpack variable') + +dumpHexString :: [Word8] -> [Text.Builder.Builder] +dumpHexString byteCodes + | null byteCodes = [dumpHexLine " > " byteCodes] + | Prelude.length byteCodes < 10 = [dumpHexLine " > " byteCodes] + | otherwise = dumpHexLine " > " byteCodes + : dumpHexString (drop 10 byteCodes) + where + dumpHexLine separator variable' = + let firstTen = take 10 variable' + digits = fold $ intersperse (Text.Builder.singleton ' ') $ hexByte <$> firstTen + printables = foldMap printableByte firstTen + in digits + <> Text.Builder.fromText (Text.replicate (10 - Prelude.length firstTen) " ") + <> separator + <> printables + <> newlineBuilder + printableByte :: Word8 -> Text.Builder.Builder + printableByte code + | code >= 0x20 + , code < 0x7f = Text.Builder.singleton $ toEnum $ fromIntegral code + | otherwise = Text.Builder.singleton '.' + +hexByte :: Integral a => a -> Text.Builder.Builder +hexByte = Text.Builder.fromLazyText + . Text.Lazy.justifyRight 2 '0' + . Text.Builder.toLazyText + . Text.Builder.hexadecimal + +dumpMaxp :: MaxpTable -> Text.Builder.Builder +dumpMaxp (TrueMaxp TrueMaxpTable{..}) + = dumpCaption "'maxp' Table - Maximum Profile" + <> " 'maxp' version: " <> dumpFixed32 version <> newlineBuilder + <> " numGlyphs: " <> Text.Builder.decimal numGlyphs <> newlineBuilder + <> " maxPoints: " <> Text.Builder.decimal maxPoints <> newlineBuilder + <> " maxContours: " <> Text.Builder.decimal maxContours <> newlineBuilder + <> " maxCompositePoints: " <> Text.Builder.decimal maxComponentPoints <> newlineBuilder + <> " maxCompositeContours: " <> Text.Builder.decimal maxComponentContours <> newlineBuilder + <> " maxZones: " <> Text.Builder.decimal maxZones <> newlineBuilder + <> " maxTwilightPoints: " <> Text.Builder.decimal maxTwilightPoints <> newlineBuilder + <> " maxStorage: " <> Text.Builder.decimal maxStorage <> newlineBuilder + <> " maxFunctionDefs: " <> Text.Builder.decimal maxFunctionDefs <> newlineBuilder + <> " maxInstructionDefs: " <> Text.Builder.decimal maxInstructionDefs <> newlineBuilder + <> " maxStackElements: " <> Text.Builder.decimal maxStackElements <> newlineBuilder + <> " maxSizeOfInstructions: " <> Text.Builder.decimal maxSizeOfInstructions <> newlineBuilder + <> " maxComponentElements: " <> Text.Builder.decimal maxComponentElements <> newlineBuilder + <> " maxCompoenetDepth: " <> Text.Builder.decimal maxComponentDepth <> newlineBuilder +dumpMaxp (OpenMaxp OpenMaxpTable{..}) + = dumpCaption "'maxp' Table - Maximum Profile" + <> " 'maxp' version: " <> dumpFixed32 version <> newlineBuilder <> newlineBuilder + <> " numGlyphs: " <> Text.Builder.decimal numGlyphs <> newlineBuilder + +dumpGASP :: GASPTable -> Text.Builder.Builder +dumpGASP GASPTable{..} = dumpCaption "'gasp' Table - Grid-fitting And Scan-conversion Procedure" + <> "'gasp' version: " <> Text.Builder.decimal version <> newlineBuilder + <> "numRanges: " <> Text.Builder.decimal (Prelude.length gaspRange) <> newlineBuilder + <> foldMap dumpGASPRange (zip [0..] gaspRange) + where + dumpGASPRange :: (Int, GASPRange) -> Text.Builder.Builder + dumpGASPRange (index', GASPRange{..}) = newlineBuilder + <> " gasp Range " <> Text.Builder.decimal index' <> newlineBuilder + <> " rangeMaxPPEM: " <> Text.Builder.decimal rangeMaxPPEM <> newlineBuilder + <> " rangeGaspBehavior: 0x" <> halfPaddedHexadecimal rangeGaspBehavior <> newlineBuilder + +dumpGlyf :: GlyfTable -> Text.Builder.Builder +dumpGlyf (GlyfTable glyfDescriptions) = dumpCaption "'glyf' Table - Glyf data" + <> foldMap go (Vector.indexed glyfDescriptions) + where + go (glyphIndex, GlyphDescription{..}) + = "Glyph " <> justifyNumber 6 glyphIndex <> Text.Builder.singleton '.' <> newlineBuilder + <> " numberOfContours: " <> Text.Builder.decimal numberOfContours <> newlineBuilder + <> " xMin: " <> Text.Builder.decimal xMin <> newlineBuilder + <> " yMin: " <> Text.Builder.decimal yMin <> newlineBuilder + <> " xMax: " <> Text.Builder.decimal xMax <> newlineBuilder + <> " yMax: " <> Text.Builder.decimal yMax <> newlineBuilder + <> newlineBuilder <> dumpGlyphDefinition definition <> newlineBuilder + dumpEndPoint (endPointIndex, endPoint) + = " " <> justifyNumber 2 endPointIndex + <> ": " <> Text.Builder.decimal endPoint <> newlineBuilder + dumpGlyphDefinition (SimpleGlyph SimpleGlyphDefinition{..}) + = " EndPoints" <> newlineBuilder + <> " ---------" <> newlineBuilder + <> foldMap dumpEndPoint (Vector.indexed endPtsOfContours) <> newlineBuilder + <> " Length of Instructions: " + <> Text.Builder.decimal (Vector.length instructions) <> newlineBuilder + <> newlineBuilder <> " Flags" <> newlineBuilder + <> " -----" <> newlineBuilder + <> fst (Vector.foldl' foldFlag ("", 0) flags) <> newlineBuilder + <> " Coordinates" <> newlineBuilder + <> " -----------" <> newlineBuilder + <> fst (Vector.ifoldl' foldCoordinate mempty coordinates) + dumpGlyphDefinition (CompoundGlyph CompoundGlyphDefinition{..}) + = foldMap (dumpCompoundGlyph $ Vector.length components) (Vector.indexed components) + <> newlineBuilder <> " Length of Instructions: " + <> Text.Builder.decimal (Vector.length instructions) <> newlineBuilder + dumpCompoundGlyph :: Int -> (Int, ComponentGlyphPartDescription) -> Text.Builder.Builder + dumpCompoundGlyph componentsLength (componentIndex, description) = + let moreComponents = succ componentIndex < componentsLength + compoundFlags = dumpCompoundFlags moreComponents description + ComponentGlyphPartDescription{..} = description + in " " <> Text.Builder.decimal componentIndex + <> ": Flags: 0x" <> compoundFlags <> newlineBuilder + <> " Glyf Index: " <> Text.Builder.decimal glyphIndex <> newlineBuilder + <> " X" <> dumpArgument argument1 <> newlineBuilder + <> " Y" <> dumpArgument argument2 <> newlineBuilder + <> dumpTransformationOption transformationOption + <> " Others: " <> dumpOtherFlags flags <> newlineBuilder + <> newlineBuilder -- TODO + dumpTransformationOption GlyphNoScale = "" + dumpTransformationOption (GlyphScale scale) = + " X,Y Scale: " <> Text.Builder.realFloat (fixed2Double scale) <> newlineBuilder + dumpTransformationOption (GlyphXyScale xScale yScale) + = " X Scale: " <> Text.Builder.realFloat (fixed2Double xScale) <> newlineBuilder + <> " Y Scale: " <> Text.Builder.realFloat (fixed2Double yScale) <> newlineBuilder + dumpTransformationOption (Glyph2By2Scale xScale scale01 scale10 yScale) + = " X Scale: " <> Text.Builder.realFloat (fixed2Double xScale) <> newlineBuilder + <> " X,Y Scale: " <> Text.Builder.realFloat (fixed2Double scale01) <> newlineBuilder + <> " Y,X Scale: " <> Text.Builder.realFloat (fixed2Double scale10) <> newlineBuilder + <> " Y Scale: " <> Text.Builder.realFloat (fixed2Double yScale) <> newlineBuilder + dumpOtherFlags ComponentGlyphFlags{..} = + let roundXyToGridText = if roundXyToGrid then "Round X,Y to Grid " else " " + useMyMetricsText = if useMyMetrics then "Use My Metrics " else " " + overlapCompoundText = if overlapCompound then "Overlap " else " " + in roundXyToGridText <> overlapCompoundText <> useMyMetricsText + dumpCompoundFlags :: Bool -> ComponentGlyphPartDescription -> Text.Builder.Builder + dumpCompoundFlags moreComponents ComponentGlyphPartDescription{..} = + let setBits = glyphArgumentBits argument1 + <> componentFlagBits flags + <> transformationOptionBits transformationOption + setBits' = if moreComponents then 5 : setBits else setBits + in Text.Builder.hexadecimal + $ foldr (flip setBit) (zeroBits :: Word16) setBits' + dumpArgument (GlyphInt8Argument argument) = + " BOffset: " <> Text.Builder.decimal argument + dumpArgument (GlyphInt16Argument argument) = + " WOffset: " <> Text.Builder.decimal argument + dumpArgument (GlyphWord8Argument argument) = + " BPoint: " <> Text.Builder.decimal argument + dumpArgument (GlyphWord16Argument argument) = + " WPoint: " <> Text.Builder.decimal argument + glyphArgumentBits (GlyphInt16Argument _) = [0, 1] + glyphArgumentBits (GlyphWord16Argument _) = [0] + glyphArgumentBits (GlyphInt8Argument _) = [1] + glyphArgumentBits (GlyphWord8Argument _) = [] + componentFlagBits ComponentGlyphFlags{..} = catMaybes + [ if roundXyToGrid then Just 2 else Nothing + , if weHaveInstructions then Just 8 else Nothing + , if useMyMetrics then Just 9 else Nothing + , if overlapCompound then Just 10 else Nothing + ] + transformationOptionBits GlyphScale{} = [3] + transformationOptionBits GlyphXyScale{} = [6] + transformationOptionBits Glyph2By2Scale{} = [7] + transformationOptionBits GlyphNoScale = [] + dumpFlag lineValue coordinateIndex + = " " <> justifyNumber 2 coordinateIndex <> lineValue + foldFlag :: (Text.Builder.Builder, Int) -> OutlineFlag -> (Text.Builder.Builder, Int) + foldFlag (accumulator, coordinateIndex) OutlineFlag{..} = + let lineValue = ": " + <> (if thisYIsSame then "YDual " else " ") + <> (if thisXIsSame then "XDual " else " ") + <> (if repeat > 0 then "Repeat " else " ") + <> (if yShortVector then "Y-Short " else " ") + <> (if xShortVector then "X-Short " else " ") + <> (if onCurve then "On" else "Off") + <> newlineBuilder + repeatN = succIntegral repeat + repeatedLines = fold + $ Vector.cons accumulator + $ dumpFlag lineValue + <$> Vector.enumFromN coordinateIndex repeatN + in (repeatedLines, coordinateIndex + repeatN) + foldCoordinate + :: (Text.Builder.Builder, GlyphCoordinate) + -> Int + -> GlyphCoordinate + -> (Text.Builder.Builder, GlyphCoordinate) + foldCoordinate (accumulator, absCoordinate) coordinateIndex relCoordinate = + let nextAbs = relCoordinate <> absCoordinate + newLine = " " <> justifyNumber 2 coordinateIndex + <> " Rel " <> dumpCoordinate relCoordinate + <> " -> Abs " <> dumpCoordinate nextAbs + <> newlineBuilder + in (accumulator <> newLine, nextAbs) + dumpCoordinate GlyphCoordinate{..} + = "(" <> justifyNumber 7 coordinateX <> ", " + <> justifyNumber 7 coordinateY <> ")" + +dumpTables + :: Megaparsec.State ByteString Void + -> FontDirectory + -> Either DumpError Text.Builder.Builder +dumpTables processedState directory@FontDirectory{..} + = parseRequired >>= traverseDirectory + where + traverseDirectory parsedRequired = + let initial = Right $ dumpOffsetTable directory + in foldl' (go parsedRequired) initial tableDirectory + parseRequired = do + requiredHhea <- findRequired "hhea" hheaTableP + requiredHead@HeadTable{ indexToLocFormat } <- + findRequired "head" headTableP + requiredLoca <- findRequired "loca" (locaTableP indexToLocFormat) + pure $ RequiredTables + { hheaTable = requiredHhea + , headTable = requiredHead + , locaTable = requiredLoca + } + findRequired tableName parser = + let missingError = Left $ DumpRequiredTableMissingError tableName + parseFound tableEntry = parseTable tableEntry parser processedState + in maybe missingError (first DumpParseError . parseFound) + $ find ((== Char8.pack tableName) . getField @"tag") tableDirectory + go _ (Left accumulator) _ = Left accumulator + go parsedRequired (Right accumulator) tableEntry + = maybe (Right accumulator) (concatDump accumulator . first DumpParseError) + $ dumpSubTable parsedRequired tableEntry + concatDump accumulator builderDump = ((accumulator <> newlineBuilder) <>) + <$> builderDump + dumpSubTable RequiredTables{..} tableEntry = + case getField @"tag" tableEntry of + "cmap" -> Just $ dumpCmap <$> parseTable tableEntry cmapTableP processedState + "head" -> Just $ Right $ dumpHead headTable + "hhea" -> Just $ Right $ dumpHhea hheaTable + "hmtx" -> Just $ dumpHmtx + <$> parseTable tableEntry (hmtxTableP $ getField @"numOfLongHorMetrics" hheaTable) processedState + "loca" -> Just $ Right $ dumpLoca locaTable + "maxp" -> Just $ dumpMaxp <$> parseTable tableEntry maxpTableP processedState + "name" -> Just $ dumpName <$> parseTable tableEntry nameTableP processedState + "post" -> Just $ dumpPost <$> parseTable tableEntry postTableP processedState + "OS/2" -> Just $ dumpOs2 <$> parseTable tableEntry os2TableP processedState + "cvt " -> Just $ dumpCVTable <$> parseTable tableEntry cvTableP processedState + "gasp" -> Just $ dumpGASP <$> parseTable tableEntry gaspTableP processedState + "glyf" -> Just $ dumpGlyf <$> parseTable tableEntry (glyfTableP locaTable) processedState + _ -> 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 |
