kb-text-shape-0.1.0.0: src/KB/Text/Shape/Font.hs
module KB.Text.Shape.Font
( -- * Distillation
extractBlob
-- * Loading
, FontData(..)
, createFont
, destroyFont
-- * Using
, withFontData
, Handles.Font
-- * Querying
, getFontInfo
, Info(..)
-- ** Font metrics
, emToCaps
, capHeight
, unitsPerEm
-- * Loading internals
, withLoader
, loadFont
, LoadFontResult(..)
, placeBlob
) where
import Prelude hiding (id)
import Foreign
import Control.Monad (when, zipWithM)
import Data.ByteString (ByteString)
import Data.ByteString.Internal qualified as ByteString
import Data.ByteString.Unsafe qualified as ByteString
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Data.Text.Foreign qualified as Text
import KB.Text.Shape.FFI.API.Direct qualified as ShapeDirect
import KB.Text.Shape.FFI.API.Other qualified as Other
import KB.Text.Shape.FFI.Flags qualified as Flags
import KB.Text.Shape.FFI.Enums qualified as Enums
import KB.Text.Shape.FFI.Handles qualified as Handles
import KB.Text.Shape.FFI.Structs qualified as Structs
-- | Extract and pre-process font data needed for shaping.
extractBlob
:: ByteString -- ^ TTF font data
-> Int -- ^ Font index (use 0 for the first/only font)
-> IO ByteString -- ^ KBTS blob data
extractBlob fontData fontIndex =
withLoader \font statePtr -> do
loadFont fontData fontIndex font statePtr >>= \case
Left err ->
error $ show err
Right LoadFontNeedsBlob{scratchSize, outputSize} ->
placeBlob font statePtr scratchSize outputSize
Right LoadFontReady ->
pure fontData
createFont :: ByteString -> Int -> IO FontData
createFont fontSource fontIndex =
withLoader \font statePtr -> do
loadFont fontSource fontIndex font statePtr >>= \case
Left err ->
error $ show err
Right LoadFontNeedsBlob{scratchSize, outputSize} -> do
blobData <- placeBlob font statePtr scratchSize outputSize
pure font{fontResources = [fontSource, blobData]}
Right LoadFontReady ->
pure font{fontResources = [fontSource]}
destroyFont :: FontData -> IO ()
destroyFont font = withFontData font ShapeDirect.kbts_FreeFont
-- | Haskell-owned font data
data FontData = FontData
{ fontData :: ForeignPtr Word8
, fontResources :: [ByteString]
}
deriving (Eq, Show)
-- | Use the font handle from a loaded font
withFontData :: FontData -> (Handles.Font -> IO r) -> IO r
withFontData FontData{fontData} action = withForeignPtr fontData (action . Handles.Font . castPtr)
getFontInfo :: Handles.Font -> IO Info
getFontInfo font =
alloca \fontInfoPtr -> do
ShapeDirect.kbts_GetFontInfo font fontInfoPtr
Structs.FontInfo{strings=stringsArray, ..} <- peek fontInfoPtr
strings <- catMaybes <$> zipWithM loadStrings (zip [0..Enums.FONT_INFO_STRING_ID_COUNT - 1] stringsArray) stringLengths
pure Info{..}
where
loadStrings (ix, ptr) = \case
0 -> pure Nothing
len -> Just . (Enums.FontInfoStringId ix,) <$> Text.peekCStringLen (ptr, fromIntegral len)
data Info = Info
{ strings :: [(Enums.FontInfoStringId, Text)]
, styleFlags :: Flags.FontStyleFlags
, weight :: Enums.FontWeight
, width :: Enums.FontWidth
}
deriving (Eq, Show)
{- | Scaling factor to go from font-specific Em units to a cap-height normalized units.
This results in more consistent font sizing when using multiple fonts.
You may even have a chance to align something vertically!
-}
{-# INLINE emToCaps #-}
emToCaps :: Fractional a => Handles.Font -> a
emToCaps font = unitsPerEm font / capHeight font
-- | Get the height of font's capital letters, in the font's logical units.
{-# INLINE capHeight #-}
capHeight :: Num a => Handles.Font -> a
capHeight = fromIntegral . Other.hs_GetCapHeight
-- | Get the font's "Em square" size, in the font's logical units.
{-# INLINE unitsPerEm #-}
unitsPerEm :: Num a => Handles.Font -> a
unitsPerEm = fromIntegral . Other.hs_GetUnitsPerEm
withLoader :: (FontData -> Ptr ShapeDirect.LoadFontState -> IO a) -> IO a
withLoader action = do
fontData <- mallocForeignPtrBytes Handles.sizeOfFontData
let font = FontData{fontData, fontResources = []}
withFontData font \(Handles.Font fontPtr) ->
fillBytes fontPtr 0x00 Handles.sizeOfFontData
alloca \statePtr -> do
fillBytes statePtr 0x00 $ sizeOf (undefined :: ShapeDirect.LoadFontState)
action font statePtr
data LoadFontResult
= LoadFontReady
| LoadFontNeedsBlob { scratchSize :: Int, outputSize :: Int}
loadFont
:: ByteString
-> Int
-> FontData
-> Ptr ShapeDirect.LoadFontState
-> IO (Either Enums.LoadFontError LoadFontResult)
loadFont ttfData fontIndex font statePtr =
withFontData font \fontPtr ->
alloca \scratchSizePtr ->
alloca \outputSizePtr ->
ByteString.unsafeUseAsCStringLen ttfData \(ttfDataPtr, ttfDataSize) -> do
err <- ShapeDirect.kbts_LoadFont
fontPtr
statePtr
(castPtr ttfDataPtr)
(fromIntegral ttfDataSize)
(fromIntegral fontIndex)
scratchSizePtr
outputSizePtr
case err of
Enums.LOAD_FONT_ERROR_NONE ->
pure $ Right LoadFontReady
Enums.LOAD_FONT_ERROR_NEED_TO_CREATE_BLOB -> do
scratchSize <- peek scratchSizePtr
outputSize <- peek outputSizePtr
pure $ Right LoadFontNeedsBlob{..}
_ ->
pure $ Left err
placeBlob :: FontData -> Ptr ShapeDirect.LoadFontState -> Int -> Int -> IO ByteString
placeBlob font statePtr scratchSize outputSize =
allocaBytes scratchSize \scratchPtr -> do
outputData <- mallocForeignPtrBytes outputSize
withForeignPtr outputData \outputPtr ->
withFontData font \fontPtr -> do
err <- ShapeDirect.kbts_PlaceBlob fontPtr statePtr scratchPtr (castPtr outputPtr)
when (err /= Enums.LOAD_FONT_ERROR_NONE) $
error $ show err
pure $! ByteString.fromForeignPtr0 outputData outputSize