ktx-codec-0.0.2.1: src/Codec/Ktx2/Read.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- | Block-by-block extraction of data from a KTX2 file.
* Acquire a "Context". Header data is available without reading the rest of the source.
* Read 'levels' index. An memory allocation information is available.
* Consult 'Codec.Ktx2.Header.supercompressionScheme' and copy level data to decompression staging buffer or GPU memory directly.
Extra information is available when needed:
* Image metadata.
* Data Format Descriptor. Khronos Basic descriptor block is usually present, but a file may contain more.
* Supercompression data shared between all the levels.
-}
module Codec.Ktx2.Read
( Context(..)
, FileContext
, open
, close
, BytesContext
, bytes
-- * Reading blocks
-- ** Image data
, levels
, levelToPtr
, levelData
-- ** Supplemental information
, dataFormatDescriptor
, keyValueData
, supercompressionGlobalData
-- * Decoding internals
, ReadChunk(..)
, ChunkError(..)
, decodeAt
, DecodeError(..)
, ReadLevel(..)
) where
import Control.Exception (Exception, throwIO)
import Control.Monad.IO.Class (MonadIO(..))
import Data.Binary (Binary(..))
import Data.Binary.Get (Get, ByteOffset, getByteString, runGetOrFail)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.ByteString.Unsafe qualified as BSU
import Data.String (fromString)
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Foreign (Ptr, plusPtr)
import Foreign qualified
import System.IO qualified as IO
import Codec.Ktx.KeyValue (KeyValueData)
import Codec.Ktx.KeyValue qualified as KeyValue
import Codec.Ktx2.Header (Header(..))
import Codec.Ktx2.Level (Level(..))
import Codec.Ktx2.DFD (DFD(..))
import Data.Typeable (Typeable, Proxy(..), typeRep)
-- * Context
-- | A bundle of source data and header information used by reader functions.
data Context a = Context
{ context :: a
, header :: Header
}
-- ** Reading from files
-- | Context for reading from a file. The file has to be seekable.
type FileContext = Context IO.Handle
instance ReadChunk IO.Handle where
readChunkAt handle offset size = liftIO do
IO.hSeek handle IO.AbsoluteSeek (fromIntegral offset)
BS.hGet handle size
instance ReadLevel IO.Handle where
readLevelTo handle Level{..} ptr = liftIO do
IO.hSeek
handle
IO.AbsoluteSeek
(fromIntegral byteOffset)
got <- IO.hGetBuf handle ptr (fromIntegral byteLength)
pure $ fromIntegral got == byteLength
instance Show (Context IO.Handle) where
show (Context handle header) = mconcat
[ "Context ("
, show handle
, ") "
, show header
]
open :: MonadIO io => FilePath -> io FileContext
open path = do
handle <- liftIO $ IO.openBinaryFile path IO.ReadMode
header <- decodeAt handle 0 80 get
pure $ Context handle header
close :: MonadIO io => FileContext -> io ()
close (Context handle _header) = liftIO $ IO.hClose handle
-- ** Reading from memory
-- | Context for reading from memory. Useful when the data is embedded in a module or otherwise already available in full.
type BytesContext = Context ByteString
instance ReadChunk ByteString where
readChunkAt bs offset size =
if offset + size > BS.length bs then
liftIO . throwIO . ChunkError $ mconcat
[ "Offset " <> fromString (show offset)
, " and size " <> fromString (show size)
, " is beyond the size of the buffer: "
, fromString (show $ BS.length bs)
]
else
pure $ BS.take size (BS.drop offset bs)
instance ReadLevel ByteString where
readLevelTo buf Level{..} dst =
liftIO $ BSU.unsafeUseAsCStringLen buf \(src, size) ->
if size < fromIntegral (byteOffset + byteLength) then
pure False
else do
Foreign.copyBytes
dst
(plusPtr src $ fromIntegral byteOffset)
(fromIntegral byteLength)
pure True
instance Show (Context ByteString) where
show (Context buf header) = mconcat
[ "Context ["
, show $ BS.length buf
, "] "
, show header
]
bytes :: MonadIO io => ByteString -> io BytesContext
bytes src = do
header <- decodeAt src 0 80 get
pure $ Context src header
-- * File contents
-- | Read the level index.
levels
:: ( ReadChunk src
, MonadIO io
)
=> Context src
-> io (Vector Level)
levels (Context handle Header{levelCount}) =
decodeAt handle 80 (numLevels * 8 * 3) $
Vector.replicateM numLevels get
where
numLevels = max 1 $ fromIntegral levelCount
{- | Copy level data into a provided pointer.
The buffer must be large enough for the 'byteLength' of the "Level" being accessed.
-}
{-# INLINE levelToPtr #-}
levelToPtr
:: ( ReadLevel src
, MonadIO io
)
=> Context src
-> Level
-> Ptr ()
-> io Bool
levelToPtr (Context handle _header) = readLevelTo handle
-- | Copy level data into a managed buffer.
levelData
:: ( ReadChunk src
, MonadIO io
)
=> Context src
-> Level
-> io ByteString
levelData (Context handle _header) Level{..} = do
readChunkAt
handle
(fromIntegral byteOffset)
(fromIntegral byteLength)
{- | Read DFD block data.
Further processing is performed according to descriptor vendor/type/version.
E.g. "Codec.Ktx2.DFD.Khronos.BasicV2".
-}
dataFormatDescriptor
:: ( ReadChunk src
, MonadIO io
)
=> Context src
-> io DFD
dataFormatDescriptor (Context handle Header{..}) =
if dfdByteLength == 0 then
pure DFD
{ dfdTotalSize = 0
, dfdBlocks = mempty
}
else
decodeAt
handle
(fromIntegral dfdByteOffset)
(fromIntegral dfdByteLength)
get
{- | Read and parse Key-Value Data block.
-}
keyValueData
:: ( ReadChunk src
, MonadIO io
)
=> Context src
-> io KeyValueData
keyValueData (Context handle Header{..}) =
if kvdByteLength == 0 then
pure mempty
else
decodeAt
handle
(fromIntegral kvdByteOffset)
(fromIntegral kvdByteLength)
(KeyValue.getDataLe $ fromIntegral kvdByteLength)
-- | Get a copy of global supercompression data.
supercompressionGlobalData
:: ( ReadChunk src
, MonadIO io
)
=> Context src
-> io ByteString
supercompressionGlobalData (Context handle Header{..}) =
decodeAt
handle
(fromIntegral sgdByteOffset)
(fromIntegral sgdByteLength)
(getByteString $ fromIntegral sgdByteLength)
-- * IO helpers
class ReadChunk a where
{- | Get a chunk of data.
The context handle must have enough information to check whether requested region is safe to access.
Throw "ChunkError" when it isn't possible to fullfill the request.
-}
readChunkAt :: MonadIO io => a -> Int -> Int -> io ByteString
newtype ChunkError = ChunkError Text
deriving (Eq, Show)
instance Exception ChunkError
-- | Get a chunk of data and run a decoder on it.
decodeAt
:: forall a src io
. ( ReadChunk src
, Show a
, Typeable a
, MonadIO io
)
=> src
-> Int
-> Int
-> Get a
-> io a
decodeAt src offset size action = do
chunk <- readChunkAt src offset size
case runGetOrFail action (BSL.fromChunks [chunk]) of
Right ("", used, ok) | fromIntegral used == size ->
pure ok
Right (remains, finalOffset, _okBut) ->
liftIO . throwIO $ DecodeError
(fromIntegral offset + finalOffset)
( "BUG: unused data " <>
fromString (show remains)
)
Left (_remains, errorOffset, message) ->
liftIO . throwIO $ DecodeError
(fromIntegral offset + errorOffset)
( fromString $ unwords
[ typeName
, "-"
, message
]
)
where
typeName = show $ typeRep (Proxy :: Proxy a)
data DecodeError = DecodeError ByteOffset Text
deriving (Eq, Show)
instance Exception DecodeError
class ReadLevel a where
readLevelTo :: MonadIO io => a -> Level -> Ptr () -> io Bool