packages feed

msdf-atlas (empty) → 0.1.0.0

raw patch · 8 files changed

+556/−0 lines, 8 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, containers, msdf-atlas, text, vector, zstd

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `msdf-atlas`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 0.1.0.0 - 2025-12-30++Initial release.
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2025 IC Rainbow++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1.  Redistributions of source code must retain the above copyright notice, this+    list of conditions and the following disclaimer.++2.  Redistributions in binary form must reproduce the above copyright notice,+    this list of conditions and the following disclaimer in the documentation+    and/or other materials provided with the distribution.++3.  Neither the name of the copyright holder nor the names of its contributors+    may be used to endorse or promote products derived from this software+    without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,6 @@+# msdf-atlas++https://github.com/Chlumsky/msdf-atlas-gen++The package alsoo provides a compacted representation of the atlas+to use with text shaping packages like ktx-font / kb-text-shape.
+ msdf-atlas.cabal view
@@ -0,0 +1,103 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.38.1.+--+-- see: https://github.com/sol/hpack++name:           msdf-atlas+version:        0.1.0.0+synopsis:       Types and parser for the MSDF atlas layout+category:       Graphics+author:         IC Rainbow+maintainer:     aenor.realm@gmail.com+copyright:      2025 IC Rainbow+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+extra-doc-files:+    CHANGELOG.md++source-repository head+  type: git+  location: https://gitlab.com/dpwiz/msdf-atlas++library+  exposed-modules:+      Graphics.MSDF.Atlas.CLI+      Graphics.MSDF.Atlas.Compact+      Graphics.MSDF.Atlas.Layout+  other-modules:+      Paths_msdf_atlas+  autogen-modules:+      Paths_msdf_atlas+  hs-source-dirs:+      src+  default-extensions:+      BlockArguments+      DeriveAnyClass+      DeriveGeneric+      DerivingStrategies+      DerivingVia+      DuplicateRecordFields+      ImportQualifiedPost+      LambdaCase+      NamedFieldPuns+      OverloadedRecordDot+      OverloadedStrings+      PatternSynonyms+      RecordWildCards+      StandaloneDeriving+      StrictData+      TupleSections+      TypeApplications+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      aeson+    , base >=4.7 && <5+    , containers+    , text+    , vector+  default-language: Haskell2010++test-suite msdf-atlas-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_msdf_atlas+  autogen-modules:+      Paths_msdf_atlas+  hs-source-dirs:+      test+  default-extensions:+      BlockArguments+      DeriveAnyClass+      DeriveGeneric+      DerivingStrategies+      DerivingVia+      DuplicateRecordFields+      ImportQualifiedPost+      LambdaCase+      NamedFieldPuns+      OverloadedRecordDot+      OverloadedStrings+      PatternSynonyms+      RecordWildCards+      StandaloneDeriving+      StrictData+      TupleSections+      TypeApplications+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >=4.7 && <5+    , bytestring+    , containers+    , msdf-atlas+    , text+    , vector+    , zstd+  default-language: Haskell2010
+ src/Graphics/MSDF/Atlas/CLI.hs view
@@ -0,0 +1,100 @@+module Graphics.MSDF.Atlas.CLI where++import Data.Text (Text)++type Inputs = [Input] -- grouped together with `-and`++data Input = Input+  { font :: FilePath+  , variation :: [(Text, Int)] -- optional variations, switching to varfont+  , range :: Maybe Range+  , fontscale :: Maybe Float+  , fontname :: Maybe Text+  }+  deriving (Eq, Show)++data Range+  = Charset FilePath+  | Chars SetSpec+  | Glyphs SetSpec+  | Allglyphs+  deriving (Eq, Show)++data SetSpec+  = Single Char+  | Range Char Char+  | String [Char]+  deriving (Eq, Show)++data Format+  = PNG+  | BMP+  | TIFF+  | RGBA+  | FL32+  | TEXT+  | TEXTFLOAT+  | BIN+  | BINFLOAT+  | BINFLOATLE+  deriving (Eq, Ord, Show, Enum, Bounded)++data Dimensions+  = Fixed Int Int+  | Auto Constraint+  deriving (Eq, Show)++data Constraint+  = PoTS+  | PoTR+  | Square+  | Square2+  | Square4 -- default+  deriving (Eq, Show)++data Uniform+  = UniformCols Int+  | UniformCell Int Int+  | UniformCellConstraint Constraint+  | UniformOrigin OriginConstraint+  deriving (Eq, Show)++data OriginConstraint+  = Off+  | On+  | Horizontal+  | Vertical+  deriving (Eq, Show)++type Outputs = [Output]++data Output+  = ImageOut FilePath+  | JSON FilePath+  | CSV FilePath+  | ARFont FilePath+  | ShadronPreview FilePath Text+  deriving (Eq, Show)++data Glyph = Glyph+  { size :: Maybe Float -- pixels per em+  , minsize :: Maybe Float+  , emrange :: Maybe Float+  , pxrange :: Maybe Int+  , aemrange :: Maybe (Float, Float)+  , pxalign :: OriginConstraint+  , empadding :: Maybe Float+  , pxpadding :: Maybe Int+  , outerempadding :: Maybe Float+  , outerpxpadding :: Maybe Int+  , aempadding :: Maybe (LBTR Float)+  , apxpadding :: Maybe (LBTR Int)+  , aouterempadding :: Maybe (LBTR Float)+  , aouterpxpadding :: Maybe (LBTR Int)+  }+  deriving (Eq, Show)++data LBTR a = LBTR+  { left, bottom, right, top :: a+  }+  deriving (Eq, Show)
+ src/Graphics/MSDF/Atlas/Compact.hs view
@@ -0,0 +1,98 @@+module Graphics.MSDF.Atlas.Compact+  ( compact+  , lookupGlyph+  , Compact(..)+  , Box(..)+  , nullBox+  , moveBox+  , scaleBox+  ) where++import Data.Aeson.Types (FromJSON(..), ToJSON(..))+import Data.Vector.Storable qualified as Storable+import Data.Vector.Generic qualified as Vector+import Foreign (Storable(..))+import GHC.Generics (Generic)++import Graphics.MSDF.Atlas.Layout (AtlasType, Layout (..), Atlas (..))+import qualified Graphics.MSDF.Atlas.Layout as Atlas++data Compact = Compact+  { _atlasSize :: (Int, Int) -- ^ Atlas image size in pixels+  , _size :: Float -- ^ Font size in pixels+  , _type :: AtlasType+  , _yOrigin :: Atlas.YOrigin+  , glyphs :: Storable.Vector Box -- ^ Glyph boxes in the atlas UV space, normalized+  , planes :: Storable.Vector Box -- ^ Quad boxes in word space+  }+  deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON)++data Box = Box {x, y, w, h :: Float}+  deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON)++{-# INLINEABLE nullBox #-}+nullBox :: Box+nullBox = Box 0 0 0 0++{-# INLINEABLE moveBox #-}+moveBox :: Float -> Float -> Box -> Box+moveBox tx ty box@Box{x, y} = box {x = x + tx, y = y + ty}++{-# INLINEABLE scaleBox #-}+scaleBox :: Float -> Box -> Box+scaleBox s Box{x, y, w, h} = Box {x = x * s, y = y * s, w = w * s, h = h * s}++instance Storable Box where+  alignment ~_ = 16+  sizeOf ~_ = 16+  peek ptr = Box+    <$> peekByteOff ptr 0+    <*> peekByteOff ptr 4+    <*> peekByteOff ptr 8+    <*> peekByteOff ptr 12+  poke ptr Box{..} = do+    pokeByteOff ptr 0 x+    pokeByteOff ptr 4 y+    pokeByteOff ptr 8 w+    pokeByteOff ptr 12 h++compact :: Layout -> Compact+compact Layout{atlas=Atlas{..}, glyphs=aGlyphs} =+  Compact+    { _atlasSize = (width, height)+    , _size = size+    , _type = aType+    , _yOrigin = yOrigin+    , glyphs = Storable.convert as+    , planes = Storable.convert ps+    }+  where+    u = 1 / fromIntegral width+    v = 1 / fromIntegral height++    (as, ps) = Vector.unzip do+      Atlas.Glyph{planeBounds, atlasBounds} <- aGlyphs+      pure+        ( maybe nullBox atlasBox atlasBounds+        , maybe nullBox planeBox planeBounds+        )++    atlasBox Atlas.Bounds{..} = Box{..}+      where+        x = left * u+        y = (if yOrigin == Atlas.Top then top else bottom) * v+        w = abs (right - left) * u+        h = abs (top - bottom) * v++    planeBox Atlas.Bounds{..} = Box{..}+      where+        x = left * 0.5 + right * 0.5+        y = ySign (top * 0.5 + bottom * 0.5)+        w = abs $ right - left+        h = abs $ top - bottom+        ySign =+          if yOrigin == Atlas.Top then negate else id++lookupGlyph :: Int -> Compact -> Maybe (Box, Box)+lookupGlyph ix Compact{glyphs, planes} =+  (,) <$> Vector.indexM glyphs ix <*> Vector.indexM planes ix
+ src/Graphics/MSDF/Atlas/Layout.hs view
@@ -0,0 +1,180 @@+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).
+ test/Spec.hs view
@@ -0,0 +1,32 @@+import Data.Aeson (eitherDecodeFileStrict, encodeFile)+import Data.Char (chr)+import Data.IntMap qualified as IntMap++import Graphics.MSDF.Atlas.Layout+import Graphics.MSDF.Atlas.Compact (compact)++main :: IO ()+main = do+  testCompact+  testAtlasCodepoints++testCompact :: IO ()+testCompact = do+  layout <- eitherDecodeFileStrict "test/Ubuntu-R-codepoints.json" >>= either fail pure+  encodeFile "test/compact.json" (compact layout)++testAtlasCodepoints :: IO ()+testAtlasCodepoints = do+  Layout{glyphs, kerning} <- eitherDecodeFileStrict "test/Ubuntu-R-codepoints.json" >>= either fail pure+  mapM_ (matchCodepoints $ byCodepoint glyphs) kerning++matchCodepoints :: ByCodepoint -> Kerning -> IO ()+matchCodepoints (ByCodepoint codepoints) Kerning{..} = sequence_ pair_+  where+    pair_ = do+      a <- unicode1+      b <- unicode2+      Just do+        Glyph{} <- maybe (fail "unicode1 codepoint not found in glyphs") pure $ IntMap.lookup a codepoints+        Glyph{} <- maybe (fail "unicode2 codepoint not found in glyphs") pure $ IntMap.lookup b codepoints+        putStrLn $ chr a : chr b : ' ' : show advance