ktx-font-0.1.0.1: src/Codec/Ktx2/Font.hs
module Codec.Ktx2.Font
( -- * KTX font bundles
Bundle(..)
, loadBundle
, loadBundleBytes
, loadBundleFile
, freeBundle
, bundleFont
-- * Shaping context
, StackContext(..)
, createStackContext
, pushBundleFont
, destroyStackContext
-- ** Associated data utilities
, lookupAtlas
, lookupBundled
, mapWithBundle
, mapBundled
-- * Format internals
, pattern KTX_KEY_atlas
, pattern KTX_KEY_kbts
, pattern KTX_KEY_kbts_version
, kbtsVersion
) where
import Codec.Compression.Zstd qualified as Zstd
import Codec.Ktx.KeyValue qualified as KVD
import Codec.Ktx2 qualified as Ktx2
import Codec.Ktx2.Read qualified as Ktx2
import Codec.Ktx2.Write qualified as Ktx2
import Control.Concurrent (MVar, newMVar, takeMVar)
import Control.Exception (bracket)
import Control.Monad
import Data.Aeson (encode, eitherDecodeFileStrict, eitherDecodeStrict)
import Data.ByteString (ByteString)
import Data.ByteString qualified as ByteString
import Data.Foldable (toList)
import Data.IntMap (IntMap)
import Data.IntMap qualified as IntMap
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Traversable (for)
import Data.Vector.Generic qualified as Vector
import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
import GHC.Generics (Generic, Generic1)
import Graphics.MSDF.Atlas.Compact (Compact, compact)
import Graphics.MSDF.Atlas.Compact qualified as Compact
import Graphics.MSDF.Atlas.Layout qualified as Layout
import KB.Text.Shape qualified as TextShape
import KB.Text.Shape.FFI.Enums qualified as KBTS
import KB.Text.Shape.FFI.Handles (Font(..), intHandle)
import KB.Text.Shape.Font (FontData(..), createFont, destroyFont, withFontData)
import KB.Text.Shape.Font qualified as Font
pattern KTX_KEY_atlas :: Text
pattern KTX_KEY_atlas = "msdf-atlas"
pattern KTX_KEY_kbts :: Text
pattern KTX_KEY_kbts = "kbts-blob"
pattern KTX_KEY_kbts_version :: Text
pattern KTX_KEY_kbts_version = "kbts-version"
kbtsVersion :: Text
kbtsVersion = Text.pack $ show KBTS.VERSION_CURRENT <> "," <> show KBTS.BLOB_VERSION_CURRENT
bundleFont
:: FilePath -- ^ Source font (ttf etc)
-> FilePath -- ^ Atlas JSON
-> FilePath -- ^ KTX2 texture
-> FilePath -- ^ Output
-> IO ()
bundleFont pathTtf pathJson pathKtx2 pathKtxf = do
layout <- eitherDecodeFileStrict pathJson >>= either fail pure
ttfData <- ByteString.readFile pathTtf
kbtsData <- Font.extractBlob ttfData 0
font <- createFont kbtsData 0
rescale <-
case layout.atlas.sizeUnit of
Just Layout.CapsHeight -> pure id
Nothing -> do
factor <- withFontData font $ pure . Font.emToCaps
pure $ scalePlanes factor
destroyFont font
sourceKtx <- Ktx2.fromFile pathKtx2
let
atlas = rescale $ compact layout
atlasData = ByteString.toStrict $ encode atlas
kvd =
KVD.insertBytes KTX_KEY_atlas (Zstd.compress 19 atlasData) $
KVD.insertBytes KTX_KEY_kbts (Zstd.compress 19 kbtsData) $
KVD.insertText KTX_KEY_kbts_version kbtsVersion $
KVD.setWriterWith ("ktx-font 0.1.0.1 / " <>) sourceKtx.kvd
Ktx2.toFile pathKtxf sourceKtx{Ktx2.kvd}
-- | Rescale planes to caps height instead of ems.
--
-- With 1.4 factor, 1em targeting 10px would become 14px.
-- That means you need smaller font sizes in caps that in ems.
scalePlanes :: Float -> Compact -> Compact
scalePlanes s a = a
{ Compact._size = a._size * s
, Compact.planes = Vector.map (Compact.scaleBox s) a.planes
}
{- | Put already-loaded font to context.
The context will NOT keep a fontData reference.
-}
pushBundleFont :: TextShape.Context -> Bundle -> IO Int
pushBundleFont ctx Bundle{fontData} =
withFontData fontData $ TextShape.pushFont ctx
data Bundle = Bundle
{ fontData :: FontData -- ^ The bundle holds the font memory too.
, atlas :: Compact -- ^ Glyph atlas data for the texture.
}
deriving (Eq)
loadBundleFile :: FilePath -> IO Bundle
loadBundleFile fontFile =
bracket (Ktx2.open fontFile) Ktx2.close loadBundle
loadBundleBytes :: ByteString -> IO Bundle
loadBundleBytes bytes =
Ktx2.bytes bytes >>= loadBundle
-- | Load bundle data from already opened KTX2 reader context
loadBundle :: Ktx2.ReadChunk a => Ktx2.Context a -> IO Bundle
loadBundle ktx = do
kvd <- Ktx2.keyValueData ktx
let
bundled = (,,)
<$> (Map.lookup KTX_KEY_atlas kvd >>= KVD.fromValue)
<*> (Map.lookup KTX_KEY_kbts kvd >>= KVD.fromValue)
<*> (Map.lookup KTX_KEY_kbts_version kvd >>= KVD.fromValue)
(mtsdfZstd, kbtsZstd, kbtsVer) <-
case bundled of
Nothing ->
error "Missing KTXF metadata"
Just compressed ->
pure compressed
unless (kbtsVer == kbtsVersion) $
error $ "KBTS blob version mismatch: " <> show kbtsVer <> ", expected " <> show kbtsVersion
atlas <-
case Zstd.decompress mtsdfZstd of
Zstd.Decompress json ->
either error pure $ eitherDecodeStrict json
Zstd.Skip ->
error "Empty atlas data"
Zstd.Error err ->
error $ "Atlas decompression error: " <> err
fontData <-
case Zstd.decompress kbtsZstd of
Zstd.Decompress fontData ->
createFont fontData 0
Zstd.Skip ->
error $ "Empty font data"
Zstd.Error err ->
error $ "Font decompression error: " <> err
pure Bundle{..}
freeBundle :: Bundle -> IO ()
freeBundle Bundle{fontData} = destroyFont fontData
-- * Shaping contexts
data StackContext a = StackContext
{ shapeContext :: MVar TextShape.Context
, bundled :: IntMap a
, atlases :: IntMap Compact -- XXX: Font.Handle ~ Ptr Font.Handle ~ Int
}
deriving stock (Functor, Foldable, Traversable, Generic, Generic1)
{- | Create shaping context and push fonts from all the bundles.
-}
createStackContext :: Foldable t => t Bundle -> IO (StackContext ())
createStackContext bundles = do
ctx <- TextShape.createContext
locals <- for (toList bundles) \Bundle{..} ->
withFontData fontData \font -> do
_refs <- TextShape.pushFont ctx font
pure (intHandle font, atlas)
let atlases = IntMap.fromList locals
let bundled = IntMap.map (const ()) atlases
shapeContext <- newMVar ctx
pure StackContext{..}
{- | Update font annotations with the bundle collection annotated with update functions.
This may be used to attach more information, e.g. after all fonts were assigned texture slots.
The update collection should be a superset of what was initially bundled.
Otherwise you may see missing things downstream.
-}
mapWithBundle :: Foldable t => t (Bundle, Maybe a -> b) -> StackContext a -> StackContext b
mapWithBundle bundles StackContext{bundled = old, ..} = StackContext{bundled = new, ..}
where
new = IntMap.fromList do
(Bundle{fontData=Font.FontData{fontData}}, f) <- toList bundles
let key = intHandle $ unsafeForeignPtrToPtr fontData
pure (key, f $ IntMap.lookup key old)
{- | Destroy shaping context.
NB: Does NOT free the fonts as they may be shared with other stacks. Use `freeBundle` for that.
-}
destroyStackContext :: StackContext a -> IO ()
destroyStackContext StackContext{shapeContext} = takeMVar shapeContext >>= TextShape.destroyContext
{-# INLINE lookupBundled #-}
lookupBundled :: Font -> StackContext a -> Maybe a
lookupBundled font StackContext{..} = IntMap.lookup (intHandle font) bundled
{-# INLINE lookupAtlas #-}
lookupAtlas :: Font -> StackContext a -> Maybe Compact
lookupAtlas font StackContext{atlases} = IntMap.lookup (intHandle font) atlases
mapBundled :: (a -> b) -> StackContext a -> StackContext b
mapBundled f ctx@StackContext{bundled} = ctx
{ bundled = IntMap.map f bundled
}