packages feed

msdf-atlas-0.1.0.0: src/Graphics/MSDF/Atlas/Layout.hs

module Graphics.MSDF.Atlas.Layout where

import Data.Aeson
import Data.Char (ord)
import Data.IntMap (IntMap)
import Data.IntMap qualified as IntMap
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import GHC.Generics (Generic)

data Layout = Layout
  { atlas :: Atlas
  , metrics :: Metrics
  , glyphs :: Vector Glyph
  , kerning :: Vector Kerning
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Layout
instance ToJSON Layout

data Atlas = Atlas
  { aType :: AtlasType
  , distanceRange :: Maybe Float
  , distanceRangeMiddle :: Maybe Float
  , size :: Float -- ^ The `size` field represents the font size in pixels per em.
  , width :: Int -- ^ Atlas image width in pixels
  , height :: Int -- ^ Atlas image height in pixels
  , yOrigin :: YOrigin
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Atlas where
  parseJSON = withObject "Atlas" \o -> do
    aType <- o .: "type"
    distanceRange <- o .:? "distanceRange"
    distanceRangeMiddle <- o .:? "distanceRangeMiddle"
    size <- o .: "size"
    width <- o .: "width"
    height <- o .: "height"
    yOrigin <- o .: "yOrigin"
    pure Atlas{..}

instance ToJSON Atlas where
  toJSON Atlas{..} = object
    [ "type" .= aType
    , "distanceRange" .= distanceRange
    , "distanceRangeMiddle" .= distanceRangeMiddle
    , "size" .= size
    , "width" .= width
    , "height" .= height
    , "yOrigin" .= yOrigin
    ]

data AtlasType
  = Hardmask -- ^ non-anti-aliased binary image
  | Softmask -- ^ anti-aliased image
  | SDF -- ^ true signed distance field (SDF)
  | PSDF -- ^ signed perpendicular distance field (PSDF)
  | MSDF -- ^ multi-channel signed distance field (MSDF)
  | MTSDF -- ^ combination of MSDF and true SDF in the alpha channel
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

instance FromJSON AtlasType where
  parseJSON = withText "AtlasType" \case
    "hardmask" -> pure Hardmask
    "softmask" -> pure Softmask
    "sdf" -> pure SDF
    "psdf" -> pure PSDF
    "msdf" -> pure MSDF
    "mtsdf" -> pure MTSDF
    huh -> fail $ "Unexpected AtlasType: " <> show huh

instance ToJSON AtlasType where
  toJSON = \case
    Hardmask -> "hardmask"
    Softmask -> "softmask"
    SDF -> "sdf"
    PSDF -> "psdf"
    MSDF -> "msdf"
    MTSDF -> "mtsdf"

data YOrigin
  = Top
  | Bottom
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

instance FromJSON YOrigin where
  parseJSON = withText "yOrigin" \case
    "top" -> pure Top
    "bottom" -> pure Bottom
    huh -> fail $ "Unexpected yOrigin: " <> show huh

instance ToJSON YOrigin where
  toJSON = \case
    Top -> String "top"
    Bottom -> String "bottom"

    -- - If there are multiple input fonts (`-and` parameter), the remaining data are grouped into `variants`, each representing an input font.
    --

-- | Useful font metric values retrieved from the font. All values are in em's.
data Metrics = Metrics
  { emSize :: Float
  , lineHeight :: Float
  , ascender :: Float
  , descender :: Float
  , underlineY :: Float
  , underlineThickness :: Float
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Metrics
instance ToJSON Metrics

data Glyph = Glyph
  { index :: Maybe Int
  , unicode :: Maybe Int
  , advance :: Float
  , planeBounds :: Maybe Bounds
  , atlasBounds :: Maybe Bounds
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Glyph
instance ToJSON Glyph

newtype ByCodepoint = ByCodepoint (IntMap Glyph)
  deriving (Eq, Ord, Show, Generic)

instance FromJSON ByCodepoint
instance ToJSON ByCodepoint

byCodepoint :: Foldable t => t Glyph -> ByCodepoint
byCodepoint = ByCodepoint . foldr (\g@Glyph{unicode} rest -> maybe rest (\i -> IntMap.insert i g rest) unicode) mempty

lookupCodepoint :: Char -> ByCodepoint -> Maybe Glyph
lookupCodepoint c (ByCodepoint gs) = IntMap.lookup (ord c) gs

newtype ByIndex = ByIndex (Vector Glyph)
  deriving (Eq, Ord, Show, Generic)

lookupGlyph :: Int -> ByIndex -> Maybe Glyph
lookupGlyph ix (ByIndex gs) = Vector.indexM gs ix

byIndex :: Foldable t => t Glyph -> Either String ByIndex
byIndex = extract . foldr collect mempty
  where
    collect g@Glyph{index} next = maybe next (\i -> IntMap.insert i g next) index
    extract m =
      if IntMap.keys m == [0 .. IntMap.size m - 1] then
        Right . ByIndex $ Vector.fromList (IntMap.elems m)
      else
        Left "Glyph indices aren't contiguous"

data Bounds = Bounds
  { left, top, right, bottom :: Float
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Bounds
instance ToJSON Bounds

data Kerning = Kerning
  { advance :: Float
  , unicode1 :: Maybe Int
  , unicode2 :: Maybe Int
  , index1 :: Maybe Int
  , index2 :: Maybe Int
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON Kerning
instance ToJSON Kerning

    -- - `glyphs` is an array of individual glyphs identified by Unicode character index (`unicode`) or glyph index (`index`), depending on whether character set or glyph set mode is used.
    --     - `advance` is the horizontal advance in em's.
    --     - `planeBounds` represents the glyph quad's bounds in em's relative to the baseline and horizontal cursor position.
    --     - `atlasBounds` represents the glyph's bounds in the atlas in pixels.
    -- - If available, `kerning` lists all kerning pairs and their advance adjustment (which needs to be added to the base advance of the first glyph in the pair).