packages feed

kb-text-shape-0.1.0.1: src/KB/Text/Shape.hs

module KB.Text.Shape
  ( -- * The slow part
    withContext
  , Context(..)
  , createContext
  , destroyContext

    -- ** Adding fonts
    -- $stack
  , Handles.Font
  , pushFontFromFile
  , pushFontFromMemory
  , pushFont
  , popFont

    -- * Turning texts into glyphs
  , run
  , Run(..)
  , Glyph(..)
  , gpos
  , GPOS(..)
    -- ** Feeding input
  , Handles.ShapeContext
  , text_
  , char_
  , withFont_
    -- $features
  , withFeature_
  , pushFeature_
  , popFeature_
    -- * Internals
  , stripGlyph
  ) where

import Control.Monad
import Data.IORef
import Foreign hiding (void)
import Foreign.C
import Prelude hiding (id)

import Control.Exception (bracket)
import Data.ByteString (ByteString)
import Data.ByteString.Unsafe qualified as ByteString
import Data.Char (chr, ord)
import Data.IntMap (IntMap)
import Data.IntMap.Strict qualified as IntMap
import Data.Text (Text)
import Data.Text.Foreign qualified as Text
import GHC.Records (HasField(..))

import KB.Text.Shape.FFI.API.Context qualified as ShapeContext
import KB.Text.Shape.FFI.Enums qualified as Enums
import KB.Text.Shape.FFI.Flags qualified as Flags
import KB.Text.Shape.FFI.Handles qualified as Handles
import KB.Text.Shape.FFI.Iterators qualified as Iterators
import KB.Text.Shape.FFI.Structs qualified as Structs

withContext :: (Context -> IO r) -> IO r
withContext = bracket createContext destroyContext

data Context = Context
  { handle :: Handles.ShapeContext
  , fonts :: IORef (IntMap ByteStringRC)
  }

data ByteStringRC = ByteStringRC ByteString Int

createContext :: IO Context
createContext = do
  handle <- ShapeContext.kbts_CreateShapeContext nullFunPtr nullPtr
  when (handle == Handles.ShapeContext nullPtr) $
    -- XXX: assuming the default allocator didn't allocate anything yet
    error "kbts_CreateShapeContext: failed to init"
  fonts <- newIORef mempty
  pure Context{handle, fonts}

destroyContext :: Context -> IO ()
destroyContext Context{handle} = ShapeContext.kbts_DestroyShapeContext handle

{- $stack

The context is capable of managing multiple fonts through a font stack.
The font stack will hold references to all fonts in use by the context. Whenever
you try to shape some text, the context will check to see if it is supported by
the font at the top of the stack. If it is not, it will try the next font down,
and so on, until all fonts have been tried. As such, you should push your fallback
fonts first, and your preferred fonts last.

-}

pushFontFromFile :: Context -> FilePath -> Int -> IO Handles.Font
pushFontFromFile ctx path fontIndex = do
  font <- withCString path \pathPtr ->
    ShapeContext.kbts_ShapePushFontFromFile ctx.handle pathPtr (fromIntegral fontIndex)
  let err = ShapeContext.kbts_ShapeError ctx.handle
  when (err /= Enums.SHAPE_ERROR_NONE) $
      error $ "kbts_ShapePushFontFromFile: failed to load font. " <> show err
  _ <- keepFont ctx font mempty -- register the empty blob so the counters would look nicer
  pure font

pushFontFromMemory :: Context -> ByteString -> Int -> IO Handles.Font
pushFontFromMemory ctx fontData fontIndex =
  ByteString.unsafeUseAsCStringLen fontData \(memoryPtr, memorySize) -> do
    font <- ShapeContext.kbts_ShapePushFontFromMemory ctx.handle (castPtr memoryPtr) (fromIntegral memorySize)  (fromIntegral fontIndex)
    let err = ShapeContext.kbts_ShapeError ctx.handle
    when (err /= Enums.SHAPE_ERROR_NONE) $
        error $ "kbts_ShapePushFontFromMemory: failed to load font. " <> show err
    _ <- keepFont ctx font fontData
    pure font

keepFont :: Context -> Handles.Font -> ByteString -> IO Int
keepFont ctx font bytes = atomicModifyIORef' ctx.fonts $ swap . IntMap.alterF addRef (Handles.intHandle font)
  where
    swap (a, b) = (b, a)
    addRef :: Maybe ByteStringRC -> (Int, Maybe ByteStringRC)
    addRef = \case
      Nothing -> (1, Just $ ByteStringRC bytes 1)
      Just (ByteStringRC oldBytes oldC) -> (newC, Just $ ByteStringRC oldBytes newC)
        where
          newC = oldC + 1

releaseFont :: Context -> Handles.Font -> IO Int
releaseFont ctx font = atomicModifyIORef' ctx.fonts $ swap . IntMap.alterF delRef (Handles.intHandle font)
  where
    swap (a, b) = (b, a)
    delRef :: Maybe ByteStringRC -> (Int, Maybe ByteStringRC)
    delRef = \case
      Nothing -> (-1, Nothing)
      Just (ByteStringRC _old 1) -> (0, Nothing)
      Just (ByteStringRC stillUsed oldC) -> (newC, Just $ ByteStringRC stillUsed newC)
        where newC = oldC - 1

pushFont :: Context -> Handles.Font -> IO Int
pushFont ctx font = do
  font' <- ShapeContext.kbts_ShapePushFont ctx.handle font
  keepFont ctx font' mempty

popFont :: Context -> IO (Int, Handles.Font)
popFont ctx = do
  font <- ShapeContext.kbts_ShapePopFont ctx.handle
  kept <- releaseFont ctx font
  pure (kept, font)

{- | Run the segmentation and shaping.

NB: Make sure you did load some fonts and text data!

Add content with 'text_' and 'char_', which you may wrap in feature tag sections.
-}
run :: Context -> ((?shapeContext :: Handles.ShapeContext) => IO ()) -> IO [(Run, [Glyph])]
run ctx action = do
  ShapeContext.kbts_ShapeBegin ctx.handle Enums.DIRECTION_DONT_KNOW Enums.LANGUAGE_DONT_KNOW
  shapeAction
  ShapeContext.kbts_ShapeEnd ctx.handle

  iterateRun ctx
  where
    shapeAction = let ?shapeContext = ctx.handle in action

-- | Add one codepoint to the shaping run.
char_ :: (?shapeContext :: Handles.ShapeContext) => Char -> IO ()
char_ c = ShapeContext.kbts_ShapeCodepoint ?shapeContext (fromIntegral (ord c))

{- | Add a chunk of text to the shaping run.

This will not add extra characters like newlines or whitespace.
You may want to call this multiple times instead of concatentating everything.
-}
text_ :: (?shapeContext :: Handles.ShapeContext) => Text -> IO ()
text_ t =
  Text.withCStringLen t \(strPtr, strLen) ->
    ShapeContext.kbts_ShapeUtf8
      ?shapeContext
      strPtr
      (fromIntegral strLen)
      Enums.USER_ID_GENERATION_MODE_CODEPOINT_INDEX

{- | Run a section with extra fonts on the stack.

The text added here will first consider the new font.
This may be used to implement text styles by pushing bold/italic variants.
-}
withFont_ :: (?shapeContext :: Handles.ShapeContext) => Handles.Font -> IO () -> IO ()
withFont_ f action = do
  void $! ShapeContext.kbts_ShapePushFont ?shapeContext f
  action
  void $! ShapeContext.kbts_ShapePopFont ?shapeContext

{- $features

The context has a feature stack that allows you to manipulate font features hierarchically.
When you give text to the context, it will apply all feature overrides that are on the
stack at the time.

If two feature overrides use the same tag, then only the latest one, i.e. the one higher
in the stack, is applied.
-}

withFeature_ :: (?shapeContext :: Handles.ShapeContext) => Enums.FeatureTag -> Int -> IO r -> IO r
withFeature_ tag value action = do
  pushFeature_ tag value
  r <- action
  _ <- popFeature_ tag
  pure r

pushFeature_ :: (?shapeContext :: Handles.ShapeContext) => Enums.FeatureTag -> Int -> IO ()
pushFeature_ tag value = ShapeContext.kbts_ShapePushFeature ?shapeContext tag (fromIntegral value)

popFeature_ :: (?shapeContext :: Handles.ShapeContext) => Enums.FeatureTag -> IO Int
popFeature_ tag = fromIntegral <$> ShapeContext.kbts_ShapePopFeature ?shapeContext tag

{- | Text runs with uniform direction and script.

The result of a text segmentation work.
-}
data Run = Run
  { font :: Handles.Font
  , script :: Enums.Script
  , paragraphDirection :: Enums.Direction
  , direction :: Enums.Direction
  , flags :: Flags.BreakFlags
  } deriving (Eq, Show)

iterateRun :: Context -> IO [(Run, [Glyph])]
iterateRun ctx =
  alloca \runPtr ->
    alloca \glyphOutPtr ->
      stepWhile (step runPtr) (collect runPtr glyphOutPtr)
  where
    step runPtr = (/= 0) <$> ShapeContext.kbts_ShapeRun ctx.handle runPtr
    collect runPtr glyphOutPtr = do
      Structs.Run{..} <- peek runPtr
      (Run{..},) <$> iterateGlyphs glyphOutPtr (Structs.runGlyphIterator runPtr)

iterateGlyphs :: Ptr (Ptr Structs.Glyph) -> Ptr Structs.GlyphIterator -> IO [Glyph]
iterateGlyphs glyphOutPtr it = stepWhile step fetch
  where
    step = (/= 0) <$> Iterators.kbts_GlyphIteratorNext it glyphOutPtr
    fetch = do
      peek glyphOutPtr >>= peek >>= stripGlyph

stepWhile :: Monad m => m Bool -> m a -> m [a]
stepWhile step fetch = do
  result <- step
  if result then do
    x <- fetch
    (x :) <$> stepWhile step fetch
  else
    pure []

{- | Glyphs ready to rasterize.

The result of a text shaping work.
-}
data Glyph = Glyph
  { -- omitted: prev :: Ptr Glyph
    -- omitted: next :: Ptr Glyph
   codepoint :: Char -- was: Word32
  , id :: Word16 -- ^ Glyph index. This is what you want to use to query outline data.
  , uid :: Word16

  , codepointIndex :: Int -- was: userIdOrCodepointIndex

  , offsetX :: Int
    {- ^ This, and the next few are in the "font units".
    Those have to be scaled appropriately using font metrics.
    To get the consistent results when using multiple fonts divide by 'KB.Text.Shape.Font.capHeight',
    then scale to the desired height.
    -}
  , offsetY :: Int
  , advanceX :: Int
  , advanceY :: Int

  , attachGlyph :: Maybe Glyph -- was: Ptr Glyph

  -- omitted: config :: Ptr () -- kbts_glyph_config *Config;

  , decomposition :: Word64

  , classes :: Word32 -- kbts_glyph_classes Classes;
  , flags :: Flags.GlyphFlags
  -- omitted: parentInfo :: Word32

  -- omitted: ligatureUid :: Word16
  -- omitted: ligatureComponentIndexPlusOne :: Word16
  -- omitted: ligatureComponentCount :: Word16

  -- omitted: joiningFeature :: Word8 -- kbts_joining_feature JoiningFeature;

  -- Unicode properties filled in by CodepointToGlyph.
  , joiningType :: Enums.UnicodeJoiningType
  , unicodeFlags :: Word8
  , syllabicClass :: Word8
  , syllabicPosition :: Word8
  , useClass :: Word8
  , combiningClass :: Word8
  }
  deriving (Eq, Show)

instance HasField "gpos" Glyph (GPOS Int) where
  {-# INLINE getField #-}
  getField = gpos

-- | Extract glyph positioning information.
{-# INLINE gpos #-}
gpos :: Glyph -> GPOS Int
gpos Glyph{offsetX, offsetY, advanceX, advanceY} = GPOS{offsetX, offsetY, advanceX, advanceY}

{- | A container for glyph positioning information.

You may want to use its Functor instance to convert between different units.
-}
data GPOS a = GPOS
  { offsetX, offsetY, advanceX, advanceY :: a
  }
  deriving (Eq, Show, Functor)

-- | Remove the internals and pointer data
stripGlyph :: Structs.Glyph -> IO Glyph
stripGlyph Structs.Glyph{..} = do
  attached <-
    if attachGlyph == nullPtr then
      pure Nothing
    else
      Just <$> (peek attachGlyph >>= stripGlyph)
  pure Glyph
    { codepoint = chr (fromIntegral codepoint)
    , codepointIndex = userIdOrCodepointIndex -- XXX: always codepoint when using context api
    , attachGlyph = attached
    , offsetX = fromIntegral offsetX
    , offsetY = fromIntegral offsetY
    , advanceX = fromIntegral advanceX
    , advanceY = fromIntegral advanceY
    , ..
    }