packages feed

ktx-codec-0.0.1.3: src/Codec/Ktx.hs

module Codec.Ktx where

import Data.Binary (Binary(..), decodeFileOrFail, decodeOrFail)
import Data.Binary.Get (Get, ByteOffset, getWord32le, getWord32be, getByteString, isolate, skip)
import Data.Binary.Put (Put, execPut, putByteString, putWord32le, putWord32be)
import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder, hPutBuilder)
import Data.Coerce (coerce)
import Data.Foldable (for_)
import Data.Map.Strict (Map)
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Word (Word32)
import GHC.Generics (Generic)
import System.IO (IOMode(..), withBinaryFile)

import qualified Data.Text.Encoding as Text
import qualified Data.Map.Strict as Map
import qualified Data.Vector as Vector
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL

fromByteStringLazy :: BSL.ByteString -> Either (ByteOffset, String) Ktx
fromByteStringLazy bsl =
  case decodeOrFail bsl of
    Right (_leftovers, _bytesLeft, ktx) ->
      Right ktx
    Left (_leftovers, bytesLeft, err) ->
      Left (bytesLeft, err)

fromByteString :: ByteString -> Either (ByteOffset, String) Ktx
fromByteString = fromByteStringLazy . BSL.fromStrict

fromFile :: FilePath -> IO (Either (ByteOffset, String) Ktx)
fromFile = decodeFileOrFail

toBuilder :: Ktx -> Builder
toBuilder = execPut . put

toFile :: FilePath -> Ktx -> IO ()
toFile dest ktx =
  withBinaryFile dest WriteMode $ \handle ->
    hPutBuilder handle (toBuilder ktx)

data Ktx = Ktx
  { header :: Header
  , kvs    :: KeyValueData
  , images :: MipLevels
  } deriving (Show, Generic)

instance Binary Ktx where
  get = do
    header <- get
    kvs <- getKeyValueData header
    images <- getImages header
    pure Ktx{..}

  put Ktx{..} = do
    put header
    putKeyValueData putWord32 kvs
    putImages putWord32 images
    where
      putWord32 = mkPutWord32 $ endianness header

-- * Header

data Header = Header
  { identifier            :: ByteString
  , endianness            :: Word32
  , glType                :: Word32
  , glTypeSize            :: Word32
  , glFormat              :: Word32
  , glInternalFormat      :: Word32
  , glBaseInternalFormat  :: Word32
  , pixelWidth            :: Word32
  , pixelHeight           :: Word32
  , pixelDepth            :: Word32
  , numberOfArrayElements :: Word32
  , numberOfFaces         :: Word32
  , numberOfMipmapLevels  :: Word32
  , bytesOfKeyValueData   :: Word32
  } deriving (Show, Generic)

instance Binary Header where
  get = do
    identifier <- getByteString 12
    if identifier == canonicalIdentifier then
      pure ()
    else
      fail $ "KTX identifier mismatch: " <> show identifier

    endianness <- getWord32le
    let
      getNext =
        if endianness == endiannessLE then
          getWord32le
        else
          getWord32be

    glType                <- getNext
    glTypeSize            <- getNext
    glFormat              <- getNext
    glInternalFormat      <- getNext
    glBaseInternalFormat  <- getNext
    pixelWidth            <- getNext
    pixelHeight           <- getNext
    pixelDepth            <- getNext
    numberOfArrayElements <- getNext
    numberOfFaces         <- getNext
    numberOfMipmapLevels  <- getNext
    bytesOfKeyValueData   <- getNext

    pure Header{..}

  put Header{..} = do
    putByteString identifier
    let putWord32 = mkPutWord32 endianness

    putWord32 endianness
    putWord32 glType
    putWord32 glTypeSize
    putWord32 glFormat
    putWord32 glInternalFormat
    putWord32 glBaseInternalFormat
    putWord32 pixelWidth
    putWord32 pixelHeight
    putWord32 pixelDepth
    putWord32 numberOfArrayElements
    putWord32 numberOfFaces
    putWord32 numberOfMipmapLevels
    putWord32 bytesOfKeyValueData

endiannessLE :: Word32
endiannessLE = 0x04030201

canonicalIdentifier :: ByteString
canonicalIdentifier = BS.pack
  [ 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB -- «KTX 11»
  , 0x0D, 0x0A, 0x1A, 0x0A                         -- \r\n\x1A\n
  ]

-- * Key-value data

type KeyValueData = Map Key Value

newtype Key = Key Text
  deriving (Eq, Ord, Show, Generic)

newtype Value = Value ByteString
  deriving (Show, Generic)

getKeyValueData :: Header -> Get KeyValueData
getKeyValueData Header{..} =
  isolate (fromIntegral bytesOfKeyValueData) $
    go (toInteger bytesOfKeyValueData) []
  where
    go remains acc
      | remains == 0 =
          pure $ Map.fromList acc

      | remains < 0 =
          fail "Attempted to read beyond bytesOfKeyValueData"

      | otherwise = do
          keyAndValueByteSize <- fmap toInteger getSize
          let paddingSize = 3 - ((keyAndValueByteSize + 3) `rem` 4)

          keyAndValue <- getByteString (fromInteger keyAndValueByteSize)
          skip (fromInteger paddingSize)

          {- XXX: Spec says:
              Any byte value is allowed.
              It is encouraged that the value be a NUL terminated UTF-8 string but this is not required.
              If the Value data is a string of bytes then the NUL termination
              should be included in the keyAndValueByteSize byte count
              (but programs that read KTX files must not rely on this).
          -}
          let
            (keyBS, valueBS) = BS.span (/= 0x00) keyAndValue
            key   = Key $ Text.decodeUtf8 keyBS
            value = Value $ BS.drop 1 valueBS

          go
            (remains - 4 - keyAndValueByteSize - paddingSize)
            ((key, value) : acc)

    getSize =
      if endianness == endiannessLE then
        getWord32le
      else
        getWord32be

putKeyValueData :: (Word32 -> Put) -> Map Key Value -> Put
putKeyValueData endianPut kvs =
  for_ (Map.toList kvs) \(Key key, Value value) -> do
    let
      keyAndValue = Text.encodeUtf8 key <> BS.singleton 0x00 <> value
      keyAndValueByteSize = BS.length keyAndValue
      paddingSize = 3 - ((keyAndValueByteSize + 3) `rem` 4)

    endianPut (fromIntegral keyAndValueByteSize)
    putByteString keyAndValue
    putByteString $ BS.replicate paddingSize 0

-- * Images

type MipLevels = Vector MipLevel

data MipLevel = MipLevel
  { imageSize     :: Word32
  , arrayElements :: Vector ArrayElement
  }
  deriving (Show, Generic)

newtype ArrayElement = ArrayElement
  { faces :: Vector Face
  }
  deriving (Show, Generic)

newtype Face = Face
  { zSlices :: Vector ZSlice
  }
  deriving (Show, Generic)

newtype ZSlice = ZSlice
  { block :: ByteString
  }
  deriving (Generic)

instance Show ZSlice where
  show ZSlice{..} =
    let
      size = BS.length block
    in
      mconcat
        [ "ZSlice ("
        , show size
        , ") "
        , show (BS.take 32 block)
        ]

getImages :: Header -> Get MipLevels
getImages Header{..} =
  some_ numberOfMipmapLevels' do
    imageSize <- getImageSize

    let
      sliceSize = fromIntegral $
        if numberOfFaces == 6 then
          imageSize
        else
          imageSize
            `div` numberOfArrayElements'
            `div` numberOfFaces
            `div` pixelDepth'

    elements <- some_ numberOfArrayElements' $
      some_ numberOfFaces $
        some_ pixelDepth' $
          ZSlice <$> getByteString sliceSize

    pure MipLevel
      { imageSize     = imageSize
      , arrayElements = coerce elements
      }

  where
    some_ n action = Vector.forM (Vector.fromList [1..n]) \_ix -> action

    numberOfMipmapLevels'
      | numberOfMipmapLevels == 0 = 1
      | otherwise                 = numberOfMipmapLevels

    numberOfArrayElements'
      | numberOfArrayElements == 0 = 1
      | otherwise                  = numberOfArrayElements

    pixelDepth'
      | pixelDepth == 0 = 1
      | otherwise       = pixelDepth

    getImageSize =
      if endianness == endiannessLE then
        getWord32le
      else
        getWord32be

putImages :: (Word32 -> Put) -> MipLevels -> Put
putImages putWord32 mipLevels = Vector.forM_ mipLevels \MipLevel{..} -> do
  putWord32 imageSize
  Vector.forM_ arrayElements \ArrayElement{..} ->
    Vector.forM_ faces \Face{..} ->
      Vector.forM_ zSlices \ZSlice{..} ->
        putByteString block

-- * Utils

mkPutWord32 :: Word32 -> (Word32 -> Put)
mkPutWord32 someEndianness =
  if someEndianness == endiannessLE then
    putWord32le
  else
    putWord32be