diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,17 @@
 # Changelog for ktx-codec
 
+## [0.1.0.0] - 2025-12-11
+
+Making editing KTX2 files simpler.
+
+* Add `Codec.Ktx2` with the `Ktx2` type pulled from `Codec.Ktx2.Write` arguments.
+* Changed `Codec.Ktx2.Write` functions to use that `Ktx2` instead. Breaking.
+* Add `Codec.Ktx2.Read.fromFile` to read the whole file at once and put it into `Ktx2` envelope.
+* Add `Codec.Ktx.KeyValue.writerKtxCodec`/`writerCodecWith`/`editWriter` so you can brag about using this library when writing.
+* Add container-like functions for `Codec.Ktx.KeyValue`: `lookup`, `insertBytes`, `insertNumber`, and `insertText`.
+  
+[0.1.0.0]: https://gitlab.com/dpwiz/ktx/-/tree/v0.1.0.0-codec
+
 ## [0.0.2.1] - 2023-04-08
 
 * Add `Codec.Ktx2.Write`.
diff --git a/ktx-codec.cabal b/ktx-codec.cabal
--- a/ktx-codec.cabal
+++ b/ktx-codec.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.35.1.
+-- This file has been generated from package.yaml by hpack version 0.38.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           ktx-codec
-version:        0.0.2.1
+version:        0.1.0.0
 synopsis:       Khronos texture format
 description:    This package implements low-level encoding and decoding for .ktx and .ktx2 files.
                 .
@@ -18,7 +18,7 @@
 category:       Graphics
 author:         IC Rainbow
 maintainer:     aenor.realm@gmail.com
-copyright:      2023 IC Rainbow
+copyright:      2025 IC Rainbow
 license:        BSD3
 license-file:   LICENSE
 build-type:     Simple
@@ -37,6 +37,7 @@
   exposed-modules:
       Codec.Ktx
       Codec.Ktx.KeyValue
+      Codec.Ktx2
       Codec.Ktx2.DFD
       Codec.Ktx2.DFD.Khronos.BasicV2
       Codec.Ktx2.Header
@@ -65,10 +66,10 @@
   build-depends:
       base >=4.7 && <5
     , binary >=0.8.7 && <1
-    , bytestring >=0.10 && <0.12
-    , containers ==0.6.*
-    , text >=1.2 && <2.1
-    , vector ==0.12.*
+    , bytestring >=0.10 && <0.14
+    , containers >=0.6 && <1
+    , text >=1.2 && <3
+    , vector >=0.12 && <0.14
   default-language: Haskell2010
 
 test-suite ktx-codec-test
@@ -96,16 +97,16 @@
   build-depends:
       base >=4.7 && <5
     , binary >=0.8.7 && <1
-    , bytestring >=0.10 && <0.12
-    , containers ==0.6.*
-    , directory ==1.3.*
-    , filepath ==1.4.*
+    , bytestring >=0.10 && <0.14
+    , containers >=0.6 && <1
+    , directory >=1.3 && <2
+    , filepath >=1.4 && <2
     , ktx-codec
-    , shower ==0.2.*
+    , shower >=0.2 && <1
     , tasty
     , tasty-hunit
-    , text >=1.2 && <2.1
-    , vector ==0.12.*
+    , text >=1.2 && <3
+    , vector >=0.12 && <0.14
     , zstd
   default-language: Haskell2010
   if !flag(tests)
diff --git a/src/Codec/Ktx/KeyValue.hs b/src/Codec/Ktx/KeyValue.hs
--- a/src/Codec/Ktx/KeyValue.hs
+++ b/src/Codec/Ktx/KeyValue.hs
@@ -1,12 +1,21 @@
 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__
@@ -31,6 +40,8 @@
   , 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)
@@ -39,11 +50,15 @@
 import Data.Map.Strict (Map)
 import Data.Map.Strict qualified as Map
 import Data.Text (Text)
-import Data.Text qualified as Text (pack)
+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.
@@ -69,6 +84,13 @@
 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
 
@@ -78,6 +100,15 @@
 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
@@ -137,6 +168,8 @@
     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"
 
@@ -155,8 +188,25 @@
 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"
diff --git a/src/Codec/Ktx2.hs b/src/Codec/Ktx2.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Ktx2.hs
@@ -0,0 +1,24 @@
+module Codec.Ktx2
+  ( Ktx2(..)
+  ) where
+
+import Codec.Ktx.KeyValue (KeyValueData)
+import Codec.Ktx2.DFD qualified as DFD
+import Codec.Ktx2.Header (Header)
+import Data.ByteString (ByteString)
+import Data.Vector (Vector)
+import Data.Word (Word64)
+import GHC.Generics (Generic)
+
+{- | Shared wrapper for reading and writing files.
+
+When uploading textures to GPU prefer reading only the needed chunks and using direct transfers.
+-}
+data Ktx2 = Ktx2
+  { header :: Header
+  , dfdBlocks :: Vector DFD.Block
+  , kvd :: KeyValueData
+  , sgd :: ByteString
+  , levels :: [(Maybe Word64, ByteString)]
+  }
+  deriving (Eq, Show, Generic)
diff --git a/src/Codec/Ktx2/Read.hs b/src/Codec/Ktx2/Read.hs
--- a/src/Codec/Ktx2/Read.hs
+++ b/src/Codec/Ktx2/Read.hs
@@ -24,6 +24,10 @@
   , BytesContext
   , bytes
 
+  -- * Reading everything at once
+  , fromFile
+  , fromBytes
+
   -- * Reading blocks
 
   -- ** Image data
@@ -44,7 +48,7 @@
   , ReadLevel(..)
   ) where
 
-import Control.Exception (Exception, throwIO)
+import Control.Exception (Exception, bracket, throwIO)
 import Control.Monad.IO.Class (MonadIO(..))
 import Data.Binary (Binary(..))
 import Data.Binary.Get (Get, ByteOffset, getByteString, runGetOrFail)
@@ -54,6 +58,7 @@
 import Data.ByteString.Unsafe qualified as BSU
 import Data.String (fromString)
 import Data.Text (Text)
+import Data.Traversable (for)
 import Data.Vector (Vector)
 import Data.Vector qualified as Vector
 import Foreign (Ptr, plusPtr)
@@ -62,6 +67,8 @@
 
 import Codec.Ktx.KeyValue (KeyValueData)
 import Codec.Ktx.KeyValue qualified as KeyValue
+import Codec.Ktx2 (Ktx2)
+import Codec.Ktx2 qualified as Ktx2
 import Codec.Ktx2.Header (Header(..))
 import Codec.Ktx2.Level (Level(..))
 import Codec.Ktx2.DFD (DFD(..))
@@ -154,6 +161,24 @@
   pure $ Context src header
 
 -- * File contents
+
+fromFile :: MonadIO io => FilePath -> io Ktx2
+fromFile source = liftIO $ bracket (open source) close fromContext
+
+fromBytes :: MonadIO io => ByteString -> io Ktx2
+fromBytes source = bytes source >>= fromContext
+
+fromContext :: (MonadIO io, ReadChunk a) => Context a -> io Ktx2
+fromContext ktx@Context{header} = do
+  DFD{dfdBlocks} <- dataFormatDescriptor ktx
+  kvd <- keyValueData ktx
+  sgd <- supercompressionGlobalData ktx
+  levels' <- levels ktx
+  annLevels <-
+    for (Vector.toList levels') \l@Level{uncompressedByteLength} -> do
+      bs <- levelData ktx l
+      pure (Just uncompressedByteLength, bs)
+  pure Ktx2.Ktx2{levels=annLevels, ..}
 
 -- | Read the level index.
 levels
diff --git a/src/Codec/Ktx2/Write.hs b/src/Codec/Ktx2/Write.hs
--- a/src/Codec/Ktx2/Write.hs
+++ b/src/Codec/Ktx2/Write.hs
@@ -7,44 +7,25 @@
 import Control.Monad.IO.Class (MonadIO(..))
 import Data.Binary (Binary(..))
 import Data.Binary.Put (runPut, putLazyByteString, putByteString)
-import Data.ByteString (ByteString)
 import Data.ByteString qualified as BS
 import Data.ByteString.Lazy qualified as BSL
 import Data.Foldable (traverse_)
 import Data.Map qualified as Map
-import Data.Vector (Vector)
 import Data.Vector qualified as Vector
-import Data.Word (Word64)
 
-import Codec.Ktx.KeyValue (KeyValueData)
 import Codec.Ktx.KeyValue qualified as KeyValueData
+import Codec.Ktx2 (Ktx2)
+import Codec.Ktx2 qualified as Ktx2
 import Codec.Ktx2.DFD (DFD(..))
 import Codec.Ktx2.DFD qualified as DFD
-import Codec.Ktx2.Header (Header)
 import Codec.Ktx2.Header qualified as Header
 import Codec.Ktx2.Level qualified as Level
 
-toFile
-  :: MonadIO io
-  => FilePath
-  -> Header
-  -> Vector DFD.Block
-  -> KeyValueData
-  -> ByteString
-  -> [(Maybe Word64, ByteString)]
-  -> io ()
-toFile path headerBase dfdBlocks kvd sgd levels =
-  liftIO . BSL.writeFile path $
-    toChunks headerBase dfdBlocks kvd sgd levels
+toFile :: MonadIO io => FilePath -> Ktx2 -> io ()
+toFile path = liftIO . BSL.writeFile path . toChunks
 
-toChunks
-  :: Header
-  -> Vector DFD.Block
-  -> KeyValueData
-  -> ByteString
-  -> [(Maybe Word64, ByteString)]
-  -> BSL.ByteString
-toChunks headerBase dfdBlocks kvd sgd levels =
+toChunks :: Ktx2 -> BSL.ByteString
+toChunks Ktx2.Ktx2{header=headerBase, dfdBlocks, kvd, sgd, levels} =
   runPut do
     put header
     Vector.mapM_ put levelIndex
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -21,6 +21,7 @@
 
 import Codec.Ktx qualified as Ktx1
 import Codec.Ktx.KeyValue qualified as KeyValue
+import Codec.Ktx2 qualified as Ktx2
 import Codec.Ktx2.DFD (DFD(..))
 import Codec.Ktx2.DFD.Khronos.BasicV2 qualified as BasicV2
 import Codec.Ktx2.Header qualified as Header
@@ -127,6 +128,7 @@
         , BS.take 16 readLevel
         , BS.take 16 writeLevel
         )
+
 testKtx2Context
   :: ( Read.ReadChunk a
      , Read.ReadLevel a
@@ -206,13 +208,12 @@
       , bytes
       )
 
-  pure $
-    Write.toChunks
-      (Read.header ctx)
-      dfdBlocks
-      kvd
-      sgd
-      levelBuffers
+  pure $ Write.toChunks Ktx2.Ktx2
+    { header = Read.header ctx
+    , dfdBlocks
+    , levels = levelBuffers
+    , ..
+    }
 
 printer :: Show a => a -> IO ()
 printer x =
