diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Schell Scivally (c) 2016
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * 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.
+
+    * Neither the name of Schell Scivally nor the names of other
+      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
+OWNER 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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/gelatin-freetype2.cabal b/gelatin-freetype2.cabal
new file mode 100644
--- /dev/null
+++ b/gelatin-freetype2.cabal
@@ -0,0 +1,67 @@
+name:                gelatin-freetype2
+version:             0.1.0.0
+synopsis:            FreeType2 based text rendering for the gelatin realtime
+                     rendering system.
+description:         FreeType2 based text rendering for the gelatin realtime
+                     rendering system. Please see README.md.
+homepage:            https://github.com/schell/gelatin/gelatin-freetype2#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Schell Scivally
+maintainer:          schell@zyghost.com
+copyright:           Schell Scivally
+category:            Web
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+stability:           experimental
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Gelatin.FreeType2.Internal
+                     , Gelatin.FreeType2.Utils
+                     , Gelatin.FreeType2
+
+  build-depends:       base               >=4.8 && <4.11
+                     , gelatin            >=0.1 && <0.2
+                     , gelatin-gl         >=0.1 && <0.2
+                     , freetype2          >=0.1 && <0.2
+                     , transformers       >=0.4 && <0.6
+                     , mtl                >=2.2 && <2.3
+                     , containers         >=0.5 && <0.6
+                     , either             >=4.4 && <4.6
+
+  default-language:    Haskell2010
+
+--executable gelatin-freetype2-exe
+--  hs-source-dirs:      app
+--  main-is:             Main.hs
+--  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
+--  build-depends:       base >=4.8 && <4.11
+--                     , gelatin
+--                     , gelatin-freetype2
+--                     , gelatin-sdl2
+--                     , gelatin-gl
+--                     , freetype2
+--                     , sdl2
+--                     , halive
+--                     , transformers
+--                     , mtl
+--                     , containers
+--                     , pretty-show
+--                     , vector
+--
+--  default-language:    Haskell2010
+
+test-suite gelatin-freetype2-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       base                  >=4.8 && <4.11
+                     , gelatin-freetype2
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/schell/gelatin-freetype2
diff --git a/src/Gelatin/FreeType2.hs b/src/Gelatin/FreeType2.hs
new file mode 100644
--- /dev/null
+++ b/src/Gelatin/FreeType2.hs
@@ -0,0 +1,30 @@
+-- |
+-- Module:     Gelatin.GL
+-- Copyright:  (c) 2017 Schell Scivally
+-- License:    MIT
+-- Maintainer: Schell Scivally <schell@takt.com>
+--
+-- This module provides font string rendering through the legendary freetype2.
+-- It automatically manages a texture atlas and word atlas to speed up rendering.
+module Gelatin.FreeType2
+  (-- * Getting straight to rendering
+    freetypeRenderer2
+    -- * Creating a gelatin picture
+  , freetypePicture
+    -- * Creating an Atlas
+  , allocAtlas
+  , asciiChars
+  , freeAtlas
+  , loadWords
+  , unloadMissingWords
+  , Atlas(..)
+    -- * Going deeper
+    -- ** Glyphs
+  , GlyphSize(..)
+  , glyphWidth
+  , glyphHeight
+    -- ** Measuring glyphs
+  , GlyphMetrics(..)
+  ) where
+
+import Gelatin.FreeType2.Internal
diff --git a/src/Gelatin/FreeType2/Internal.hs b/src/Gelatin/FreeType2/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Gelatin/FreeType2/Internal.hs
@@ -0,0 +1,381 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+-- |
+-- Module:     Gelatin.FreeType2
+-- Copyright:  (c) 2017 Schell Scivally
+-- License:    MIT
+-- Maintainer: Schell Scivally <schell@takt.com>
+--
+-- This module provides easy freetype2 font rendering using gelatin's
+-- graphics primitives.
+--
+module Gelatin.FreeType2.Internal where
+
+import           Gelatin.GL
+import           Gelatin.FreeType2.Utils
+import           Gelatin.Picture.Internal
+import           Control.Monad
+import           Control.Monad.IO.Class
+import           Control.Monad.State.Strict
+import           Data.Maybe (fromMaybe)
+import           Data.IntMap (IntMap)
+import qualified Data.IntMap as IM
+import           Data.Map (Map)
+import qualified Data.Map as M
+import           Foreign.Marshal.Utils (with)
+import           Graphics.Rendering.FreeType.Internal.GlyphMetrics as GM
+import           Graphics.Rendering.FreeType.Internal.Bitmap as BM
+--------------------------------------------------------------------------------
+-- WordMap
+--------------------------------------------------------------------------------
+type WordMap = Map String (V2 Float, Renderer2)
+
+-- | Load a string of words into the 'Atlas'.
+loadWords
+  :: MonadIO m
+  => Backend GLuint e V2V2 (V2 Float) Float Raster
+  -- ^ The V2V2 backend needed to render font glyphs.
+  -> Atlas
+  -- ^ The atlas to load the words into.
+  -> String
+  -- ^ The string of words to load, with each word separated by spaces.
+  -> m Atlas
+loadWords b atlas str = do
+  wm <- liftIO $ foldM loadWord (atlasWordMap atlas) $ words str
+  return atlas{atlasWordMap=wm}
+  where loadWord wm word
+          | Just _ <- M.lookup word wm = return wm
+          | otherwise = do
+              let pic = do freetypePicture atlas word
+                           pictureSize2 fst
+              (sz,r) <- compilePictureT b pic
+              return $ M.insert word (sz,r) wm
+
+-- | Unload any words not contained in the source string.
+unloadMissingWords
+  :: MonadIO m
+  => Atlas
+  -- ^ The 'Atlas' to unload words from.
+  -> String
+  -- ^ The source string.
+  -> m Atlas
+unloadMissingWords atlas str = do
+  let wm = atlasWordMap atlas
+      ws = M.fromList $ zip (words str) [(0::Int)..]
+      missing = M.difference wm ws
+      retain  = M.difference wm missing
+      dealoc  = (liftIO . fst . snd) <$> missing
+  sequence_ dealoc
+  return atlas{atlasWordMap=retain}
+--------------------------------------------------------------------------------
+-- Glyph
+--------------------------------------------------------------------------------
+data GlyphSize = CharSize Float Float Int Int
+               | PixelSize Int Int
+               deriving (Show, Eq, Ord)
+
+glyphWidth :: GlyphSize -> Float
+glyphWidth (CharSize x y _ _) = if x == 0 then y else x
+glyphWidth (PixelSize x y) = fromIntegral $ if x == 0 then y else x
+
+glyphHeight :: GlyphSize -> Float
+glyphHeight (CharSize x y _ _) = if y == 0 then x else y
+glyphHeight (PixelSize x y) = fromIntegral $ if y == 0 then x else y
+
+-- | https://www.freetype.org/freetype2/docs/tutorial/step2.html
+data GlyphMetrics = GlyphMetrics { glyphTexBB       :: (V2 Int, V2 Int)
+                                 , glyphTexSize     :: V2 Int
+                                 , glyphSize        :: V2 Int
+                                 , glyphHoriBearing :: V2 Int
+                                 , glyphVertBearing :: V2 Int
+                                 , glyphAdvance     :: V2 Int
+                                 } deriving (Show, Eq)
+--------------------------------------------------------------------------------
+-- Atlas
+--------------------------------------------------------------------------------
+data Atlas = Atlas { atlasTexture     :: GLuint
+                   , atlasTextureSize :: V2 Int
+                   , atlasLibrary     :: FT_Library
+                   , atlasFontFace    :: FT_Face
+                   , atlasMetrics     :: IntMap GlyphMetrics
+                   , atlasGlyphSize   :: GlyphSize
+                   , atlasWordMap     :: WordMap
+                   , atlasFilePath    :: FilePath
+                   }
+
+emptyAtlas :: FT_Library -> FT_Face -> GLuint -> Atlas
+emptyAtlas lib face t = Atlas t 0 lib face mempty (PixelSize 0 0) mempty ""
+
+data AtlasMeasure = AM { amWH :: V2 Int
+                       , amXY :: V2 Int
+                       , rowHeight :: Int
+                       , amMap :: IntMap (V2 Int)
+                       } deriving (Show, Eq)
+
+emptyAM :: AtlasMeasure
+emptyAM = AM 0 (V2 1 1) 0 mempty
+
+spacing :: Int
+spacing = 1
+
+measure :: FT_Face -> Int -> AtlasMeasure -> Char -> FreeTypeIO AtlasMeasure
+measure face maxw am@AM{..} char
+  | Just _ <- IM.lookup (fromEnum char) amMap = return am
+  | otherwise = do
+    let V2 x y = amXY
+        V2 w h = amWH
+    -- Load the char, replacing the glyph according to
+    -- https://www.freetype.org/freetype2/docs/tutorial/step1.html
+    loadChar face (fromIntegral $ fromEnum char) ft_LOAD_RENDER
+    -- Get the glyph slot
+    slot <- liftIO $ peek $ glyph face
+    -- Get the bitmap
+    bmp   <- liftIO $ peek $ bitmap slot
+    let bw = fromIntegral $ BM.width bmp
+        bh = fromIntegral $ rows bmp
+        gotoNextRow = (x + bw + spacing) >= maxw
+        rh = if gotoNextRow then 0 else max bh rowHeight
+        nx = if gotoNextRow then 0 else x + bw + spacing
+        nw = max w (x + bw + spacing)
+        nh = max h (y + rh + spacing)
+        ny = if gotoNextRow then nh else y
+        am = AM { amWH = V2 nw nh
+                , amXY = V2 nx ny
+                , rowHeight = rh
+                , amMap = IM.insert (fromEnum char) amXY amMap
+                }
+    return am
+
+texturize :: IntMap (V2 Int) -> Atlas -> Char -> FreeTypeIO Atlas
+texturize xymap atlas@Atlas{..} char
+  | Just pos@(V2 x y) <- IM.lookup (fromEnum char) xymap = do
+    -- Load the char
+    loadChar atlasFontFace (fromIntegral $ fromEnum char) ft_LOAD_RENDER
+    -- Get the slot and bitmap
+    slot  <- liftIO $ peek $ glyph atlasFontFace
+    bmp   <- liftIO $ peek $ bitmap slot
+    -- Update our texture by adding the bitmap
+    glTexSubImage2D GL_TEXTURE_2D 0
+                    (fromIntegral x) (fromIntegral y)
+                    (fromIntegral $ BM.width bmp) (fromIntegral $ rows bmp)
+                    GL_RED GL_UNSIGNED_BYTE
+                    (castPtr $ buffer bmp)
+    -- Get the glyph metrics
+    ftms  <- liftIO $ peek $ metrics slot
+    -- Add the metrics to the atlas
+    let vecwh = fromIntegral <$> V2 (BM.width bmp) (rows bmp)
+        canon = floor . (* 0.5) . (* 0.015625) . realToFrac . fromIntegral
+        vecsz = canon <$> V2 (GM.width ftms) (GM.height ftms)
+        vecxb = canon <$> V2 (horiBearingX ftms) (horiBearingY ftms)
+        vecyb = canon <$> V2 (vertBearingX ftms) (vertBearingY ftms)
+        vecad = canon <$> V2 (horiAdvance ftms) (vertAdvance ftms)
+        mtrcs = GlyphMetrics { glyphTexBB = (pos, pos + vecwh)
+                             , glyphTexSize = vecwh
+                             , glyphSize = vecsz
+                             , glyphHoriBearing = vecxb
+                             , glyphVertBearing = vecyb
+                             , glyphAdvance = vecad
+                             }
+    return atlas{ atlasMetrics = IM.insert (fromEnum char) mtrcs atlasMetrics }
+
+  | otherwise = do
+    liftIO $ putStrLn "could not find xy"
+    return atlas
+
+-- | Allocate a new 'Atlas'.
+-- When creating a new 'Atlas' you must pass all the characters that you
+-- might need during the life of the 'Atlas'. Character texturization only
+-- happens here.
+allocAtlas
+  :: MonadIO m
+  => FilePath
+  -- ^ 'FilePath' of the 'Font' to use for this 'Atlas'.
+  -> GlyphSize
+  -- ^ Size of glyphs in this 'Atlas'
+  -> String
+  -- ^ The characters to include in this 'Atlas'.
+  -> m (Maybe Atlas)
+allocAtlas fontFilePath gs str = do
+  e <- liftIO $ runFreeType $ do
+    fce <- newFace fontFilePath
+    case gs of
+      PixelSize w h -> setPixelSizes fce (2*w) (2*h)
+      CharSize w h dpix dpiy -> setCharSize fce (floor $ 26.6 * 2 * w)
+                                                (floor $ 26.6 * 2 * h)
+                                                dpix dpiy
+
+    AM{..} <- foldM (measure fce 512) emptyAM str
+
+    let V2 w h = amWH
+        xymap  = amMap
+
+    t <- liftIO $ do
+      t <- allocAndActivateTex GL_TEXTURE0
+      glPixelStorei GL_UNPACK_ALIGNMENT 1
+      withCString (replicate (w * h) $ toEnum 0) $
+        glTexImage2D GL_TEXTURE_2D 0 GL_RED (fromIntegral w) (fromIntegral h)
+                     0 GL_RED GL_UNSIGNED_BYTE . castPtr
+      return t
+
+    lib   <- getLibrary
+    atlas <- foldM (texturize xymap) (emptyAtlas lib fce t) str
+
+    glGenerateMipmap GL_TEXTURE_2D
+    glTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_S GL_REPEAT
+    glTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_T GL_REPEAT
+    glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MAG_FILTER GL_LINEAR
+    glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MIN_FILTER GL_LINEAR
+    glBindTexture GL_TEXTURE_2D 0
+    glPixelStorei GL_UNPACK_ALIGNMENT 4
+    return
+      atlas{ atlasTextureSize = V2 w h
+           , atlasGlyphSize = gs
+           , atlasFilePath = fontFilePath
+           }
+  case e of
+    Left err -> liftIO (print err) >> return Nothing
+    Right (atlas,_) -> return $ Just atlas
+
+-- | Releases all resources associated with the given 'Atlas'.
+freeAtlas :: MonadIO m => Atlas -> m ()
+freeAtlas a = liftIO $ do
+  _ <- ft_Done_FreeType (atlasLibrary a)
+  _ <- unloadMissingWords a ""
+  with (atlasTexture a) $ \ptr -> glDeleteTextures 1 ptr
+
+makeCharQuad :: MonadIO m => Atlas -> Bool -> (Int, Maybe FT_UInt) -> Char
+             -> VerticesT (V2 Float, V2 Float) m (Int, Maybe FT_UInt)
+makeCharQuad Atlas{..} useKerning (penx, mLast) char = do
+  let ichar = fromEnum char
+  eNdx <- withFreeType (Just atlasLibrary) $ getCharIndex atlasFontFace ichar
+  let mndx = either (const Nothing) Just eNdx
+  px <- case (,,) <$> mndx <*> mLast <*> Just useKerning of
+    Just (ndx,lndx,True) -> do
+      e <- withFreeType (Just atlasLibrary) $
+        getKerning atlasFontFace lndx ndx ft_KERNING_DEFAULT
+      return $ either (const penx) ((+penx) . floor . (/(64::Double)) . fromIntegral . fst) e
+    _  -> return $ fromIntegral penx
+  case IM.lookup ichar atlasMetrics of
+    Nothing -> return (penx, mndx)
+    Just GlyphMetrics{..} -> do
+      let V2 dx dy = fromIntegral <$> glyphHoriBearing
+          x = (fromIntegral px) + dx
+          y = -dy
+          V2 w h = fromIntegral <$> glyphSize
+          V2 aszW aszH = fromIntegral <$> atlasTextureSize
+          V2 texL texT = fromIntegral <$> fst glyphTexBB
+          V2 texR texB = fromIntegral <$> snd glyphTexBB
+
+          tl = (V2 (x)    y   , V2 (texL/aszW) (texT/aszH))
+          tr = (V2 (x+w)  y   , V2 (texR/aszW) (texT/aszH))
+          br = (V2 (x+w) (y+h), V2 (texR/aszW) (texB/aszH))
+          bl = (V2 (x)   (y+h), V2 (texL/aszW) (texB/aszH))
+      tri tl tr br
+      tri tl br bl
+      let V2 ax _ = glyphAdvance
+
+      return (px + ax, mndx)
+
+-- | A string containing all standard ASCII characters.
+-- This is often passed as the 'String' parameter in 'allocAtlas'.
+asciiChars :: String
+asciiChars = map toEnum [32..126]
+
+stringTris :: MonadIO m => Atlas -> Bool -> String -> VerticesT (V2 Float, V2 Float) m ()
+stringTris atlas useKerning =
+  foldM_ (makeCharQuad atlas useKerning) (0, Nothing)
+--------------------------------------------------------------------------------
+-- Picture
+--------------------------------------------------------------------------------
+-- | Constructs a 'TexturePictureT' of one word in all red.
+-- Colorization can then be done using 'setReplacementColor' in the picture
+-- computation, or by using 'redChannelReplacement' and passing that to the
+-- renderer after compilation, at render time. Keep in mind that any new word
+-- geometry will be discarded, since this computation does not return a new 'Atlas'.
+-- For that reason it is advised that you load the needed words before using this
+-- function. For loading words, see 'loadWords'.
+--
+-- This is used in 'freetypeRenderer2' to construct the geometry of each word.
+-- 'freetypeRenderer2' goes further and stores these geometries, looking them up
+-- and constructing a string of word renderers for each input 'String'.
+freetypePicture
+  :: MonadIO m
+  => Atlas
+  -- ^ The 'Atlas' from which to read font textures word geometry.
+  -> String
+  -- ^ The word to render.
+  -> TexturePictureT m ()
+  -- ^ Returns a textured picture computation representing the texture and
+  -- geometry of the input word.
+freetypePicture atlas@Atlas{..} str = do
+  eKerning <- withFreeType (Just atlasLibrary) $ hasKerning atlasFontFace
+  setTextures [atlasTexture]
+  let useKerning = either (const False) id eKerning
+  setGeometry $ triangles $ stringTris atlas useKerning str
+--------------------------------------------------------------------------------
+-- Performance Rendering
+--------------------------------------------------------------------------------
+-- | Constructs a 'Renderer2' from the given color and string. The 'WordMap'
+-- record of the given 'Atlas' is used to construct the string geometry, greatly
+-- improving performance and allowing longer strings to be compiled and renderered
+-- in real time. To create a new 'Atlas' see 'allocAtlas'.
+--
+-- Note that since word geometries are stored in the 'Atlas' 'WordMap' and multiple
+-- renderers can reference the same 'Atlas', the returned 'Renderer2' contains a
+-- clean up operation that does nothing. It is expected that the programmer
+-- will call 'freeAtlas' manually when the 'Atlas' is no longer needed.
+freetypeRenderer2
+  :: MonadIO m
+  => Backend GLuint e V2V2 (V2 Float) Float Raster
+  -- ^ The V2V2 backend to use for compilation.
+  -> Atlas
+  -- ^ The 'Atlas' to read character textures from and load word geometry
+  -- into.
+  -> V4 Float
+  -- ^ The solid color to render the string with.
+  -> String
+  -- ^ The string to render.
+  -- This string can contain newlines, which will be respected.
+  -> m (Renderer2, V2 Float, Atlas)
+  -- ^ Returns the 'Renderer2', the size of the text and the new
+  -- 'Atlas' with the loaded geometry of the string.
+freetypeRenderer2 b atlas0 color str = do
+  atlas <- loadWords b atlas0 str
+  let glyphw  = glyphWidth $ atlasGlyphSize atlas
+      spacew  = fromMaybe glyphw $ do
+        metrcs <- IM.lookup (fromEnum ' ') $ atlasMetrics atlas
+        let V2 x _ = glyphAdvance metrcs
+        return $ fromIntegral x
+      glyphh = glyphHeight $ atlasGlyphSize atlas
+      spaceh = glyphh
+      isWhiteSpace c = c == ' ' || c == '\n' || c == '\t'
+      renderWord :: [RenderTransform2] -> V2 Float -> String -> IO ()
+      renderWord _ _ ""       = return ()
+      renderWord rs (V2 _ y) ('\n':cs) = renderWord rs (V2 0 (y + spaceh)) cs
+      renderWord rs (V2 x y) (' ':cs) = renderWord rs (V2 (x + spacew) y) cs
+      renderWord rs (V2 x y) cs       = do
+        let word = takeWhile (not . isWhiteSpace) cs
+            rest = drop (length word) cs
+        case M.lookup word (atlasWordMap atlas) of
+          Nothing          -> renderWord rs (V2 x y) rest
+          Just (V2 w _, r) -> do
+            let ts = [move x y, redChannelReplacementV4 color]
+            snd r $ ts ++ rs
+            renderWord rs (V2 (x + w) y) rest
+      rr t = renderWord t 0 str
+      measureString :: (V2 Float, V2 Float) -> String -> (V2 Float, V2 Float)
+      measureString (V2 x y, V2 w h) ""        = (V2 x y, V2 w h)
+      measureString (V2 x y, V2 w _) (' ':cs)  =
+        let nx = x + spacew in measureString (V2 nx y, V2 (max w nx) y) cs
+      measureString (V2 x y, V2 w h) ('\n':cs) =
+        let ny = y + spaceh in measureString (V2 x ny, V2 w (max h ny)) cs
+      measureString (V2 x y, V2 w h) cs        =
+        let word = takeWhile (not . isWhiteSpace) cs
+            rest = drop (length word) cs
+            n    = case M.lookup word (atlasWordMap atlas) of
+                     Nothing          -> (V2 x y, V2 w h)
+                     Just (V2 ww _, _) -> let nx = x + ww
+                                          in (V2 nx y, V2 (max w nx) y)
+        in measureString n rest
+      V2 szw szh = snd $ measureString (0,0) str
+  return ((return (), rr), V2 szw (max spaceh szh), atlas)
diff --git a/src/Gelatin/FreeType2/Utils.hs b/src/Gelatin/FreeType2/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Gelatin/FreeType2/Utils.hs
@@ -0,0 +1,123 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE TupleSections #-}
+module Gelatin.FreeType2.Utils (
+   module FT
+ , FreeTypeT
+ , FreeTypeIO
+ , getAdvance
+ , getCharIndex
+ , getLibrary
+ , getKerning
+ , glyphFormatString
+ , hasKerning
+ , loadChar
+ , loadGlyph
+ , newFace
+ , setCharSize
+ , setPixelSizes
+ , withFreeType
+ , runFreeType
+) where
+
+import           Control.Monad.IO.Class (MonadIO, liftIO)
+import           Control.Monad.Trans.Either
+import           Control.Monad.Trans.Class
+import           Control.Monad.Trans.State.Strict
+import           Control.Monad (unless)
+import           Graphics.Rendering.FreeType.Internal                   as FT
+import           Graphics.Rendering.FreeType.Internal.PrimitiveTypes    as FT
+import           Graphics.Rendering.FreeType.Internal.Library           as FT
+import           Graphics.Rendering.FreeType.Internal.FaceType          as FT
+import           Graphics.Rendering.FreeType.Internal.Face as FT hiding (generic)
+import           Graphics.Rendering.FreeType.Internal.GlyphSlot         as FT
+import           Graphics.Rendering.FreeType.Internal.Bitmap            as FT
+import           Graphics.Rendering.FreeType.Internal.Vector            as FT
+import           Foreign                                                as FT
+import           Foreign.C.String                                       as FT
+
+type FreeTypeT m = EitherT String (StateT FT_Library m)
+type FreeTypeIO = FreeTypeT IO
+
+glyphFormatString :: FT_Glyph_Format -> String
+glyphFormatString fmt
+    | fmt == ft_GLYPH_FORMAT_COMPOSITE = "ft_GLYPH_FORMAT_COMPOSITE"
+    | fmt == ft_GLYPH_FORMAT_OUTLINE = "ft_GLYPH_FORMAT_OUTLINE"
+    | fmt == ft_GLYPH_FORMAT_PLOTTER = "ft_GLYPH_FORMAT_PLOTTER"
+    | fmt == ft_GLYPH_FORMAT_BITMAP = "ft_GLYPH_FORMAT_BITMAP"
+    | otherwise = "ft_GLYPH_FORMAT_NONE"
+
+liftE :: MonadIO m => IO (Either FT_Error a) -> FreeTypeT m a
+liftE f = (liftIO f) >>= \case
+  Left e  -> left $ "FreeType2 error:" ++ (show e)
+  Right a -> right a
+
+runIOErr :: MonadIO m => IO FT_Error -> FreeTypeT m ()
+runIOErr f = do
+  e <- liftIO f
+  unless (e == 0) $ fail $ "FreeType2 error:" ++ (show e)
+
+runFreeType :: MonadIO m => FreeTypeT m a -> m (Either String (a, FT_Library))
+runFreeType f = do
+  (e,lib) <- liftIO $ alloca $ \p -> do
+    e <- ft_Init_FreeType p
+    lib <- peek p
+    return (e,lib)
+  if e /= 0
+    then do
+      _ <- liftIO $ ft_Done_FreeType lib
+      return $ Left $ "Error initializing FreeType2:" ++ show e
+    else (fmap (,lib)) <$> evalStateT (runEitherT f) lib
+
+withFreeType :: MonadIO m => Maybe FT_Library -> FreeTypeT m a -> m (Either String a)
+withFreeType Nothing f = runFreeType f >>= \case
+  Left e -> return $ Left e
+  Right (a,lib) -> do
+    _ <- liftIO $ ft_Done_FreeType lib
+    return $ Right a
+withFreeType (Just lib) f = evalStateT (runEitherT f) lib
+
+getLibrary :: MonadIO m => FreeTypeT m FT_Library
+getLibrary = lift get
+
+newFace :: MonadIO m => FilePath -> FreeTypeT m FT_Face
+newFace fp = do
+  ft <- lift get
+  liftE $ withCString fp $ \str ->
+    alloca $ \ptr -> ft_New_Face ft str 0 ptr >>= \case
+      0 -> Right <$> peek ptr
+      e -> return $ Left e
+
+setCharSize :: (MonadIO m, Integral i) => FT_Face -> i -> i -> i -> i -> FreeTypeT m ()
+setCharSize ff w h dpix dpiy = runIOErr $
+  ft_Set_Char_Size ff (fromIntegral w)    (fromIntegral h)
+                      (fromIntegral dpix) (fromIntegral dpiy)
+
+setPixelSizes :: (MonadIO m, Integral i) => FT_Face -> i -> i -> FreeTypeT m ()
+setPixelSizes ff w h =
+  runIOErr $ ft_Set_Pixel_Sizes ff (fromIntegral w) (fromIntegral h)
+
+getCharIndex :: (MonadIO m, Integral i)
+             => FT_Face -> i -> FreeTypeT m FT_UInt
+getCharIndex ff ndx = liftIO $ ft_Get_Char_Index ff $ fromIntegral ndx
+
+loadGlyph :: MonadIO m => FT_Face -> FT_UInt -> FT_Int32 -> FreeTypeT m ()
+loadGlyph ff fg flags = runIOErr $ ft_Load_Glyph ff fg flags
+
+loadChar :: MonadIO m => FT_Face -> FT_ULong -> FT_Int32 -> FreeTypeT m ()
+loadChar ff char flags = runIOErr $ ft_Load_Char ff char flags
+
+hasKerning :: MonadIO m => FT_Face -> FreeTypeT m Bool
+hasKerning = liftIO . ft_HAS_KERNING
+
+getKerning :: MonadIO m => FT_Face -> FT_UInt -> FT_UInt -> FT_Kerning_Mode -> FreeTypeT m (Int,Int)
+getKerning ff prevNdx curNdx flags = liftE $ alloca $ \ptr -> do
+  ft_Get_Kerning ff prevNdx curNdx (fromIntegral flags) ptr >>= \case
+    0 -> do FT_Vector x y <- peek ptr
+            return $ Right (fromIntegral x, fromIntegral y)
+    e -> return $ Left e
+
+getAdvance :: MonadIO m => FT_GlyphSlot -> FreeTypeT m (Int,Int)
+getAdvance slot = do
+  FT_Vector x y <- liftIO $ peek $ advance slot
+  liftIO $ print ("v",x,y)
+  return (fromIntegral x, fromIntegral y)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,2 @@
+main :: IO ()
+main = putStrLn "Test suite not yet implemented"
