ktx-codec-0.1.0.0: src/Codec/Ktx/KeyValue.hs
module Codec.Ktx.KeyValue
( KeyValueData
, lookup
, insertBytes
, insertNumber
, insertText
-- * Predefined keys
-- $predefined
, pattern KTXcubemapIncomplete
, pattern KTXanimData
, pattern KTXastcDecodeMode
, pattern KTXwriterScParams
, pattern KTXwriter
, setWriterWith
, writerKtxCodecWith
, writerKtxCodec
, pattern KTXswizzle
, pattern KTXmetalPixelFormat
, pattern KTXdxgiFormat__
, pattern KTXglFormat
, pattern KTXorientation
-- * Writing
, Value(..)
, text
, bytes
, number
-- * Reading
, FromValue(..)
, textual
-- * Binary operations
, getDataLe
, getData
, putDataLe
, putData
) where
import Prelude hiding (lookup)
import Data.Binary.Get (Get, getWord32le, getByteString, isolate, skip)
import Data.Binary.Put (Put, putByteString, putWord32le)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Foldable (for_)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text (pack, unpack)
import Data.Text.Encoding qualified as Text
import Data.Version
import Data.Word (Word32)
import GHC.Generics (Generic)
import Text.Read (readMaybe)
import Paths_ktx_codec qualified as Paths
type KeyValueData = Map Text Value
{- | A wrapper for raw data.
Use "FromValue"/"ToValue" to process.
-}
newtype Value = Value ByteString
deriving (Eq, Show, Generic)
class FromValue a where
fromValue :: Value -> Maybe a
instance FromValue Text where
fromValue (Value bs)
| BS.null bs =
Nothing
| BS.last bs == 0x00 =
either (const Nothing) Just $
Text.decodeUtf8' $ BS.init bs
| otherwise =
Nothing
instance FromValue ByteString where
fromValue (Value bs) = Just bs
instance FromValue Integer where
fromValue val =
fromValue val >>= readMaybe . Text.unpack
lookup :: FromValue a => Text -> KeyValueData -> Maybe a
lookup key kvd = Map.lookup key kvd >>= fromValue
text :: Text -> Value
text t = Value $ BS.snoc (Text.encodeUtf8 t) 0x00
bytes :: ByteString -> Value
bytes = Value
number :: (Num a, Show a) => a -> Value
number = text . Text.pack . show
insertText :: Text -> Text -> KeyValueData -> KeyValueData
insertText key value = Map.insert key (text value)
insertBytes :: Text -> ByteString -> KeyValueData -> KeyValueData
insertBytes key value = Map.insert key (bytes value)
insertNumber :: (Num a, Show a) => Text -> a -> KeyValueData -> KeyValueData
insertNumber key value = Map.insert key (number value)
-- | Extract all valid (null-terminated utf8) values.
textual :: KeyValueData -> Map Text Text
textual = Map.mapMaybe fromValue
{-# INLINE getDataLe #-}
getDataLe :: Int -> Get KeyValueData
getDataLe = getData getWord32le
getData :: Get Word32 -> Int -> Get KeyValueData
getData getSize bytesOfKeyValueData =
isolate bytesOfKeyValueData $
go bytesOfKeyValueData []
where
go remains acc
| remains == 0 =
pure $ Map.fromList acc
| remains < 0 =
fail "Attempted to read beyond bytesOfKeyValueData"
| otherwise = do
keyAndValueByteSize <- fmap fromIntegral getSize
let paddingSize = 3 - ((keyAndValueByteSize + 3) `rem` 4)
keyAndValue <- getByteString keyAndValueByteSize
skip 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 = Text.decodeUtf8 keyBS
value = Value $ BS.drop 1 valueBS
go
(remains - keyAndValueByteSize - 4 - paddingSize)
((key, value) : acc)
{-# INLINE putDataLe #-}
putDataLe :: KeyValueData -> Put
putDataLe = putData putWord32le
putData :: (Word32 -> Put) -> KeyValueData -> Put
putData putSize kvs =
for_ (Map.toList kvs) \(key, Value value) -> do
let
keyAndValue = mconcat [Text.encodeUtf8 key, BS.singleton 0x00, value]
keyAndValueByteSize = BS.length keyAndValue
paddingSize = 3 - ((keyAndValueByteSize + 3) `rem` 4)
putSize (fromIntegral keyAndValueByteSize)
putByteString keyAndValue
putByteString $ BS.replicate paddingSize 0
-- $predefined https://github.khronos.org/KTX-Specification/ktxspec.v2.html#_predefined_keyvalue_pairs
pattern KTXcubemapIncomplete :: Text
pattern KTXcubemapIncomplete = "KTXcubemapIncomplete"
pattern KTXorientation :: Text
pattern KTXorientation = "KTXorientation"
pattern KTXglFormat :: Text
pattern KTXglFormat = "KTXglFormat"
pattern KTXdxgiFormat__ :: Text
pattern KTXdxgiFormat__ = "KTXdxgiFormat__"
pattern KTXmetalPixelFormat :: Text
pattern KTXmetalPixelFormat = "KTXmetalPixelFormat"
pattern KTXswizzle :: Text
pattern KTXswizzle = "KTXswizzle"
{- | KTX file writers may, and are strongly encouraged to, identify themselves by including a value with the key @KTXwriter@
The value is a NUL-terminated UTF-8 string that will uniquely identify the tool writing the file, for example: @AcmeCo TexTool v1.0@.
Only the most recent writer should be identified. Editing tools must overwrite this value when rewriting a file originally written by a different tool.
-}
pattern KTXwriter :: Text
pattern KTXwriter = "KTXwriter"
-- | Replace writer info with your own, using this package version as baseline.
setWriterWith :: (Text -> Text) -> KeyValueData -> KeyValueData
setWriterWith f = Map.insert KTXwriter (writerKtxCodecWith f)
-- | Attach your application/library version to the writer tag.
writerKtxCodecWith :: (Text -> Text) -> Value
writerKtxCodecWith f = text . f $ "ktx-codec " <> Text.pack (showVersion Paths.version)
-- | The value for the KTXwriter we ought to write when using this package to write or modify KTX files.
writerKtxCodec :: Value
writerKtxCodec = writerKtxCodecWith id
pattern KTXwriterScParams :: Text
pattern KTXwriterScParams = "KTXwriterScParams"
pattern KTXastcDecodeMode :: Text
pattern KTXastcDecodeMode = "KTXastcDecodeMode"
pattern KTXanimData :: Text
pattern KTXanimData = "KTXanimData"