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 +11/−0
- LICENSE +26/−0
- README.md +6/−0
- msdf-atlas.cabal +103/−0
- src/Graphics/MSDF/Atlas/CLI.hs +100/−0
- src/Graphics/MSDF/Atlas/Compact.hs +98/−0
- src/Graphics/MSDF/Atlas/Layout.hs +180/−0
- test/Spec.hs +32/−0
+ 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