├── tests ├── ktx_specs.ktx ├── etc2-rgba8.ktx ├── 16x16_colors_PVRTexTool.ktx ├── format_pvrtc1_4bpp_unorm.ktx ├── testimage_SIGNED_R11_EAC.ktx ├── smiling_ATC_RGBA_Explicit.ktx ├── 16x16_colors_Compressonator.ktx ├── smiling_etc_64x64_Compressonator.ktx ├── VersionInfoTests.cs ├── KtxCreatorTests.cs ├── KtxBitFiddlingTests.cs ├── MetadataValueTests.cs ├── tests.csproj ├── CommonSampleFiles.cs ├── KtxWriterTests.cs ├── KtxValidatorsTests.cs └── KtxLoaderTests.cs ├── create-nuget-release.ps1 ├── .github └── workflows │ ├── dotnet-core.yml │ └── dotnet.yml ├── lib ├── VersionInfo.cs ├── KtxErrors.cs ├── KtxBitFiddling.cs ├── KtxStructure.cs ├── KtxSharp.csproj ├── KtxCreator.cs ├── KtxWriter.cs ├── MetadataValue.cs ├── KtxLoader.cs ├── KtxTextureData.cs ├── KtxValidators.cs ├── KtxCommon.cs └── KtxHeader.cs ├── LICENSE ├── README.md └── .gitignore /tests/ktx_specs.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/ktx_specs.ktx -------------------------------------------------------------------------------- /tests/etc2-rgba8.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/etc2-rgba8.ktx -------------------------------------------------------------------------------- /tests/16x16_colors_PVRTexTool.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/16x16_colors_PVRTexTool.ktx -------------------------------------------------------------------------------- /tests/format_pvrtc1_4bpp_unorm.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/format_pvrtc1_4bpp_unorm.ktx -------------------------------------------------------------------------------- /tests/testimage_SIGNED_R11_EAC.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/testimage_SIGNED_R11_EAC.ktx -------------------------------------------------------------------------------- /tests/smiling_ATC_RGBA_Explicit.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/smiling_ATC_RGBA_Explicit.ktx -------------------------------------------------------------------------------- /tests/16x16_colors_Compressonator.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/16x16_colors_Compressonator.ktx -------------------------------------------------------------------------------- /tests/smiling_etc_64x64_Compressonator.ktx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcraiha/libktxsharp/HEAD/tests/smiling_etc_64x64_Compressonator.ktx -------------------------------------------------------------------------------- /create-nuget-release.ps1: -------------------------------------------------------------------------------- 1 | $currentDate = Get-Date 2 | $gitShortHash = ((git rev-parse --short HEAD) | Out-String).Trim() 3 | $finalCommand = "dotnet pack" + " " + "--configuration Release" + " " + "--include-source" + " " + "--include-symbols" + " " + "/p:InformationalVersion=""" + $currentDate.ToUniversalTime().ToString("yyyy-MM-dd HH.mm.ss") + " Short hash: " + $gitShortHash + """" 4 | Write-Host $finalCommand -------------------------------------------------------------------------------- /tests/VersionInfoTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | 4 | namespace Tests 5 | { 6 | public class VersionInfoTests 7 | { 8 | [Test] 9 | public void GetVersionTest() 10 | { 11 | // Arrange 12 | 13 | // Act 14 | string version = VersionInfo.GetVersion(); 15 | 16 | // Assert 17 | Assert.IsNotNull(version); 18 | Assert.Greater(version.Length, 1); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: CIBuild 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 10.0.x 20 | - name: Test 21 | run: dotnet test tests/tests.csproj 22 | -------------------------------------------------------------------------------- /lib/VersionInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace KtxSharp 4 | { 5 | /// 6 | /// Version info static class 7 | /// 8 | public static class VersionInfo 9 | { 10 | /// 11 | /// Get version 12 | /// 13 | /// Version string 14 | public static string GetVersion() 15 | { 16 | var version = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; 17 | return version; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /lib/KtxErrors.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace KtxSharp 3 | { 4 | /// 5 | /// Generic error generator 6 | /// 7 | public static class ErrorGen 8 | { 9 | /// 10 | /// Modulo 4 error 11 | /// 12 | /// Variable Name 13 | /// Value 14 | /// Error message 15 | public static string Modulo4Error(string variableName, uint value) 16 | { 17 | return $"{variableName} value is {value}, but it should be modulo 4!"; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /lib/KtxBitFiddling.cs: -------------------------------------------------------------------------------- 1 | // Bit fiddling 2 | 3 | namespace KtxSharp 4 | { 5 | /// 6 | /// Static class for bit fiddling operations 7 | /// 8 | public static class KtxBitFiddling 9 | { 10 | /// 11 | /// Swap endianness of uint 12 | /// 13 | /// https://stackoverflow.com/a/19560621/4886769 14 | /// Input value 15 | /// Endianness swapped value 16 | public static uint SwapEndian(uint inputValue) 17 | { 18 | // swap adjacent 16-bit blocks 19 | inputValue = (inputValue >> 16) | (inputValue << 16); 20 | // swap adjacent 8-bit blocks 21 | return ((inputValue & 0xFF00FF00) >> 8) | ((inputValue & 0x00FF00FF) << 8); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /lib/KtxStructure.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace KtxSharp 4 | { 5 | /// 6 | /// KtxStructure that has header and texture data 7 | /// 8 | public sealed class KtxStructure 9 | { 10 | /// 11 | /// Header 12 | /// 13 | public readonly KtxHeader header; 14 | 15 | /// 16 | /// Texture data 17 | /// 18 | public readonly KtxTextureData textureData; 19 | 20 | /// 21 | /// Constuctor for KtxStructure 22 | /// 23 | /// Header 24 | /// Texture data 25 | public KtxStructure(KtxHeader ktxHeader, KtxTextureData texData) 26 | { 27 | this.header = ktxHeader; 28 | this.textureData = texData; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /lib/KtxSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | LibKTX 6 | 0.9.2 7 | $(VersionSuffix) 8 | Kaarlo Räihä 9 | .Net Standard compatible managed library for handling KTX File Format (only KTX 1, not KTX 2) for texture data 10 | true 11 | https://github.com/mcraiha/libktxsharp 12 | https://github.com/mcraiha/libktxsharp.git 13 | Unlicense 14 | true 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/KtxCreatorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | using System.IO; 4 | using System.Collections.Generic; 5 | 6 | namespace Tests 7 | { 8 | public class KtxCreatorTests 9 | { 10 | [SetUp] 11 | public void Setup() 12 | { 13 | } 14 | 15 | [Test] 16 | public void KtxCreatorValidInputTest() 17 | { 18 | // Arange 19 | byte[] textureLevel0 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; // Not a valid ATSC block most likely! 20 | List textureDatas = new List() { textureLevel0 }; 21 | 22 | // Act 23 | 24 | // Assert 25 | Assert.DoesNotThrow(() => KtxCreator.Create(GlDataType.Compressed, GlPixelFormat.GL_RGBA, GlInternalFormat.GL_COMPRESSED_RGBA_ASTC_8x8_KHR, 8, 8, textureDatas, new System.Collections.Generic.Dictionary())); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/KtxBitFiddlingTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | using System.Collections.Generic; 4 | 5 | namespace Tests 6 | { 7 | public class KtxBitFiddlingTests 8 | { 9 | 10 | [Test] 11 | public void SwapEndianTest() 12 | { 13 | // Arrange 14 | var inputOutputExpectedPairs = new Dictionary() 15 | { 16 | // First some mirror matches 17 | { 0, 0 }, 18 | { 0xaaaa_aaaa, 0xaaaa_aaaa }, 19 | { 0xabcd_dcba, 0xbadc_cdab }, 20 | { uint.MaxValue, uint.MaxValue }, 21 | 22 | // Then some other values 23 | { 1, 16777216 }, 24 | { 2048, 524288 } 25 | }; 26 | 27 | // Act 28 | 29 | 30 | // Assert 31 | foreach (var pair in inputOutputExpectedPairs) 32 | { 33 | Assert.AreEqual(pair.Value, KtxBitFiddling.SwapEndian(pair.Key), $"{pair.Key} with endian swap should be {pair.Value}"); 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /tests/MetadataValueTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | 4 | namespace Tests 5 | { 6 | public class MetadataValueTests 7 | { 8 | [Test] 9 | public void ConstructorTest() 10 | { 11 | // Arrange 12 | byte[] val1 = new byte[] { 0x61 }; 13 | byte[] val2 = new byte[] { 0x61, 0 }; 14 | byte[] val3 = new byte[] { 0x6A, 0x6F, 0x6B, 0x65, 0x32, 0x00 }; // UTF8 v: 'joke2\0' 15 | 16 | 17 | // Act 18 | MetadataValue result1 = new MetadataValue(val1); 19 | MetadataValue result2 = new MetadataValue(val2); 20 | MetadataValue result3 = new MetadataValue(val3); 21 | 22 | // Assert 23 | CollectionAssert.AreNotEqual(val1, val2); 24 | CollectionAssert.AreNotEqual(val1, val3); 25 | 26 | Assert.IsFalse(result1.isString); 27 | Assert.AreEqual(val1[0], result1.bytesValue[0]); 28 | 29 | Assert.IsTrue(result2.isString); 30 | Assert.AreEqual("a", result2.stringValue); 31 | 32 | Assert.IsTrue(result3.isString); 33 | Assert.AreEqual("joke2", result3.stringValue); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will publish nuget package 2 | 3 | name: Publish-Nuget-Release 4 | 5 | on: 6 | push: 7 | tags: 8 | - v1.* # Push events to v1.0, v1.1, and v1.9 tags 9 | - v0.9* # Push events to v0.9xyz tags 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: 10.0.x 22 | - name: Add SHORT_SHA env property with commit short sha 23 | run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV 24 | - name: Add current ISO time to env property 25 | run: echo "CURRENT_TIME=`date -Iseconds`" >> $GITHUB_ENV 26 | - name: Pack 27 | run: | 28 | cd lib 29 | dotnet pack -o out --configuration Release --include-source --include-symbols /p:ContinuousIntegrationBuild=true /p:InformationalVersion="Build time: ${{env.CURRENT_TIME}} Short hash: ${{env.SHORT_SHA}}" 30 | - name: Push to Nuget 31 | run: dotnet nuget push ./lib/out/*.nupkg --skip-duplicate -k ${{secrets.NUGET_TOKEN}} --source https://api.nuget.org/v3/index.json 32 | -------------------------------------------------------------------------------- /lib/KtxCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace KtxSharp 5 | { 6 | /// 7 | /// Create a new KtxStructure 8 | /// 9 | public static class KtxCreator 10 | { 11 | /// 12 | /// Create new 2d KtxStructure from existing data 13 | /// 14 | /// GlDataType 15 | /// GlPixelFormat 16 | /// GlInternalFormat 17 | /// Width 18 | /// Height 19 | /// Texture datas 20 | /// metadata 21 | /// KtxStructure 22 | public static KtxStructure Create(GlDataType glDataType, GlPixelFormat glPixelFormat, GlInternalFormat glInternalFormat, uint width, uint height, List textureDatas, Dictionary metadata) 23 | { 24 | KtxHeader header = new KtxHeader(glDataType, glPixelFormat, glInternalFormat, width, height, (uint)textureDatas.Count, metadata); 25 | KtxTextureData textureData = new KtxTextureData(textureDatas); 26 | 27 | return new KtxStructure(header, textureData); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /tests/tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libktxsharp 2 | 3 | C# library for handling [KTX File Format](https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/). Only Version 1 is supported, so Version 2 is **NOT** supported. 4 | 5 | ## Build status 6 | 7 | ![](https://github.com/mcraiha/libktxsharp/workflows/CIBuild/badge.svg) 8 | 9 | ## Nuget 10 | 11 | [https://www.nuget.org/packages/LibKTX/](https://www.nuget.org/packages/LibKTX/) 12 | 13 | ## Why 14 | 15 | Because KTX specs are public and I need something like this for my upcoming projects 16 | 17 | ## How to use 18 | 1. Get nuget, build .dll or include [lib folder](lib) in your project 19 | 2. Use following code example 20 | ```csharp 21 | using KtxSharp; 22 | 23 | byte[] ktxBytes = File.ReadAllBytes("myImage.ktx"); 24 | 25 | KtxStructure ktxStructure = null; 26 | using (MemoryStream ms = new MemoryStream(ktxBytes)) 27 | { 28 | ktxStructure = KtxLoader.LoadInput(ms); 29 | } 30 | 31 | Console.WriteLine(ktxStructure.header.pixelWidth); 32 | ``` 33 | 34 | ## How do I build this 35 | 36 | ### Requirements 37 | 38 | Dotnet core 2.0 (or newer) environment 39 | 40 | ### Build .dll 41 | 42 | Move to lib folder and run 43 | ```bash 44 | dotnet build 45 | ``` 46 | 47 | ### Build nuget 48 | 49 | Move to lib folder and run 50 | ```bash 51 | dotnet pack -o out --configuration Release --include-source --include-symbols 52 | ``` 53 | 54 | ## Testing 55 | 56 | ### Requirements 57 | 58 | * nunit 59 | * NUnit3TestAdapter 60 | * Microsoft.NET.Test.Sdk 61 | 62 | All requirements are restored when you run 63 | ```bash 64 | dotnet restore 65 | ``` 66 | 67 | ### Run tests 68 | 69 | Just call 70 | ```bash 71 | dotnet test 72 | ``` 73 | 74 | ## What is in 75 | 76 | * Basic KTX read functionality 77 | * Some test cases 78 | 79 | ## What is partially in 80 | 81 | * KTX write support 82 | 83 | ## What is missing 84 | 85 | * More files for testing 86 | * Benchmarks 87 | 88 | ## License 89 | 90 | All code is released under *"Do whatever you want"* license aka [Unlicense](LICENSE) -------------------------------------------------------------------------------- /lib/KtxWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace KtxSharp 5 | { 6 | /// 7 | /// Write Ktx output static class 8 | /// 9 | public static class KtxWriter 10 | { 11 | /// 12 | /// Should writer override endianness 13 | /// 14 | public enum OverrideEndianness 15 | { 16 | /// 17 | /// Keep same endianness as input had 18 | /// 19 | KeepSame = 0, 20 | 21 | /// 22 | /// Write little endian 23 | /// 24 | WriteLittleEndian, 25 | 26 | /// 27 | /// Write big endian 28 | /// 29 | WriteBigEndian 30 | } 31 | 32 | /// 33 | /// Write KtxStructure to output stream 34 | /// 35 | /// KtxStructure 36 | /// Output stream 37 | /// Override endianness (optional) 38 | public static void WriteTo(KtxStructure structure, Stream output, OverrideEndianness overrideEndianness = OverrideEndianness.KeepSame) 39 | { 40 | if (structure == null) 41 | { 42 | throw new NullReferenceException("Structure cannot be null"); 43 | } 44 | 45 | if (output == null) 46 | { 47 | throw new NullReferenceException("Output stream cannot be null"); 48 | } 49 | else if (!output.CanWrite) 50 | { 51 | throw new ArgumentException("Output stream must be writable"); 52 | } 53 | 54 | bool writeLittleEndian = true; 55 | if (overrideEndianness == OverrideEndianness.KeepSame) 56 | { 57 | writeLittleEndian = structure.header.isInputLittleEndian; 58 | } 59 | else if (overrideEndianness == OverrideEndianness.WriteBigEndian) 60 | { 61 | writeLittleEndian = false; 62 | } 63 | 64 | output.Write(Common.onlyValidIdentifier, 0, Common.onlyValidIdentifier.Length); 65 | structure.header.WriteTo(output, writeLittleEndian); 66 | structure.textureData.WriteTo(output, writeLittleEndian); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /lib/MetadataValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace KtxSharp 5 | { 6 | /// 7 | /// Metadata value class 8 | /// 9 | public sealed class MetadataValue 10 | { 11 | /// 12 | /// Is string (false means byte array) 13 | /// 14 | public readonly bool isString; 15 | 16 | /// 17 | /// Bytes value, null if metadata is strings 18 | /// 19 | public readonly byte[] bytesValue; 20 | 21 | /// 22 | /// String value, null if metadata is byte array 23 | /// 24 | public readonly string stringValue; 25 | 26 | /// 27 | /// Constructor 28 | /// 29 | /// Input bytes 30 | public MetadataValue(byte[] input) 31 | { 32 | int indexOfNull = Array.FindIndex(input, b => b == Common.nulByte); 33 | if (indexOfNull > -1) 34 | { 35 | // Basically if input array contains any NUL byte, it means value is string 36 | this.isString = true; 37 | this.stringValue = System.Text.Encoding.UTF8.GetString(input, 0, indexOfNull); 38 | 39 | this.bytesValue = null; 40 | } 41 | else 42 | { 43 | // Otherwise it is byte array 44 | this.isString = false; 45 | this.bytesValue = input; 46 | 47 | this.stringValue = null; 48 | } 49 | } 50 | 51 | /// 52 | /// How many bytes this will take on memory 53 | /// 54 | /// Byte amount 55 | public uint GetSizeInBytes() 56 | { 57 | if (isString) 58 | { 59 | return Common.GetLengthOfUtf8StringAsBytes(this.stringValue) + 1 /* NUL byte length */; 60 | } 61 | else 62 | { 63 | return (uint)this.bytesValue.Length; 64 | } 65 | } 66 | 67 | /// 68 | /// Get value as byte array 69 | /// 70 | /// Byte array 71 | public byte[] GetAsBytes() 72 | { 73 | if (isString) 74 | { 75 | return Common.GetUtf8StringAsBytes(this.stringValue); 76 | } 77 | else 78 | { 79 | return this.bytesValue; 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/CommonSampleFiles.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Tests 3 | { 4 | public static class CommonFiles 5 | { 6 | // If you add any more test files here, remember to add them to tests.csproj ! 7 | 8 | /// 9 | /// First sample file is created with Compressonator https://github.com/GPUOpen-Tools/Compressonator 10 | /// 11 | public static readonly string validSample1Filename = "16x16_colors_Compressonator.ktx"; 12 | 13 | /// 14 | /// Second sample file is created with PVRTexTool https://community.imgtec.com/developers/powervr/tools/pvrtextool/ 15 | /// 16 | public static readonly string validSample2Filename = "16x16_colors_PVRTexTool.ktx"; 17 | 18 | /// 19 | /// Third sample file is created with ETCPACK https://github.com/Ericsson/ETCPACK 20 | /// 21 | public static readonly string validSample3Filename = "testimage_SIGNED_R11_EAC.ktx"; 22 | 23 | /// 24 | /// Fourth sample file is filled (missing parts filled with zeroes) from KTX specifications https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/#4 25 | /// 26 | public static readonly string validSample4Filename = "ktx_specs.ktx"; 27 | 28 | /// 29 | /// Fifth sample file is from Khronos GitHub repo https://github.com/KhronosGroup/KTX-Software/tree/master/tests/testimages 30 | /// 31 | public static readonly string validSample5Filename = "etc2-rgba8.ktx"; 32 | 33 | /// 34 | /// Sixth sample file is created with Compressonator https://github.com/GPUOpen-Tools/Compressonator 35 | /// 36 | public static readonly string validSample6Filename = "smiling_etc_64x64_Compressonator.ktx"; 37 | 38 | /// 39 | /// Seventh sample file is created with Compressonator https://github.com/GPUOpen-Tools/Compressonator 40 | /// 41 | public static readonly string validSample7Filename = "smiling_ATC_RGBA_Explicit.ktx"; 42 | 43 | /// 44 | /// Eight sample file is from ktxtest GitHub repo https://github.com/sevmeyer/ktxtest 45 | /// 46 | public static readonly string validSample8Filename = "format_pvrtc1_4bpp_unorm.ktx"; 47 | } 48 | } -------------------------------------------------------------------------------- /lib/KtxLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace KtxSharp 5 | { 6 | /// 7 | /// Load Ktx input static class 8 | /// 9 | public static class KtxLoader 10 | { 11 | 12 | /// 13 | /// Check if input is valid 14 | /// 15 | /// Stream to check (must be seekable stream) 16 | /// Tuple that tells if input is valid, and possible error message 17 | public static (bool isValid, string possibleError) CheckIfInputIsValid(Stream stream) 18 | { 19 | // Currently only header and metadata are validated properly, so texture data can still contain invalid values 20 | (bool isStreamValid, string possibleStreamError) = KtxValidators.GenericStreamValidation(stream); 21 | if (!isStreamValid) 22 | { 23 | return (isValid: false, possibleError: possibleStreamError); 24 | } 25 | 26 | // We have to duplicate the data, since we have to both validate it and keep it for texture data validation step 27 | long streamPos = stream.Position; 28 | 29 | (bool isIdentifierValid, string possibleIdentifierError) = KtxValidators.ValidateIdentifier(stream); 30 | if (!isIdentifierValid) 31 | { 32 | return (isValid: false, possibleError: possibleIdentifierError); 33 | } 34 | 35 | (bool isHeaderValid, string possibleHeaderError) = KtxValidators.ValidateHeaderData(stream); 36 | if (!isHeaderValid) 37 | { 38 | return (isValid: false, possibleError: possibleHeaderError); 39 | } 40 | 41 | stream.Position = streamPos; 42 | KtxHeader tempHeader = new KtxHeader(stream); 43 | 44 | (bool isTextureDataValid, string possibleTextureDataError) = KtxValidators.ValidateTextureData(stream, tempHeader, (uint)(stream.Length - stream.Position)); 45 | 46 | return (isValid: isTextureDataValid, possibleError: possibleTextureDataError); 47 | } 48 | 49 | /// 50 | /// Load KtxStructure from stream 51 | /// 52 | /// Stream to read (must be seekable stream) 53 | /// Seek from current position (use this if your stream is not a single .ktx file) 54 | /// 55 | public static KtxStructure LoadInput(Stream stream, bool seekFromCurrent = false) 56 | { 57 | // First we read the header 58 | KtxHeader header = new KtxHeader(stream, seekFromCurrent); 59 | // Then texture data 60 | KtxTextureData textureData = new KtxTextureData(header, stream); 61 | // And combine those to one structure 62 | return new KtxStructure(header, textureData); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/KtxWriterTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | using System; 4 | using System.IO; 5 | 6 | namespace Tests 7 | { 8 | public class KtxWriterTests 9 | { 10 | [Test] 11 | public void ValidityWithValidSamplesTest() 12 | { 13 | // Arrange 14 | byte[] inputBytes1 = File.ReadAllBytes(CommonFiles.validSample1Filename); 15 | byte[] inputBytes2 = File.ReadAllBytes(CommonFiles.validSample2Filename); 16 | byte[] inputBytes3 = File.ReadAllBytes(CommonFiles.validSample3Filename); 17 | byte[] inputBytes4 = File.ReadAllBytes(CommonFiles.validSample4Filename); 18 | 19 | MemoryStream msWriter1 = new MemoryStream(); 20 | MemoryStream msWriter2 = new MemoryStream(); 21 | MemoryStream msWriter3 = new MemoryStream(); 22 | MemoryStream msWriter4 = new MemoryStream(); 23 | 24 | // Act 25 | KtxStructure ktxStructure1 = null; 26 | KtxStructure ktxStructure2 = null; 27 | KtxStructure ktxStructure3 = null; 28 | KtxStructure ktxStructure4 = null; 29 | 30 | using (MemoryStream msReader = new MemoryStream(inputBytes1)) 31 | { 32 | ktxStructure1 = KtxLoader.LoadInput(msReader); 33 | } 34 | 35 | using (MemoryStream msReader = new MemoryStream(inputBytes2)) 36 | { 37 | ktxStructure2 = KtxLoader.LoadInput(msReader); 38 | } 39 | 40 | using (MemoryStream msReader = new MemoryStream(inputBytes3)) 41 | { 42 | ktxStructure3 = KtxLoader.LoadInput(msReader); 43 | } 44 | 45 | using (MemoryStream msReader = new MemoryStream(inputBytes4)) 46 | { 47 | ktxStructure4 = KtxLoader.LoadInput(msReader); 48 | } 49 | 50 | KtxWriter.WriteTo(ktxStructure1, msWriter1); 51 | KtxWriter.WriteTo(ktxStructure2, msWriter2); 52 | KtxWriter.WriteTo(ktxStructure3, msWriter3); 53 | KtxWriter.WriteTo(ktxStructure4, msWriter4); 54 | 55 | // Assert 56 | CollectionAssert.AreEqual(inputBytes1, msWriter1.ToArray()); 57 | CollectionAssert.AreEqual(inputBytes2, msWriter2.ToArray()); 58 | CollectionAssert.AreEqual(inputBytes3, msWriter3.ToArray()); 59 | CollectionAssert.AreEqual(inputBytes4, msWriter4.ToArray()); 60 | } 61 | 62 | [Test] 63 | public void NullOrInvalidInputsTest() 64 | { 65 | // Arrange 66 | KtxStructure structure = null; 67 | 68 | MemoryStream msWriter = new MemoryStream(); 69 | MemoryStream msWriterNonWriteable = new MemoryStream(new byte[] { 0 }, writable: false); 70 | 71 | // Act 72 | using (FileStream input = new FileStream(CommonFiles.validSample1Filename, FileMode.Open)) 73 | { 74 | structure = KtxLoader.LoadInput(input); 75 | } 76 | 77 | // Assert 78 | Assert.Throws(() => { KtxWriter.WriteTo(null, msWriter); }); 79 | Assert.Throws(() => { KtxWriter.WriteTo(structure, null); }); 80 | Assert.Throws(() => { KtxWriter.WriteTo(structure, msWriterNonWriteable); }); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/KtxValidatorsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | using System.IO; 4 | 5 | namespace Tests 6 | { 7 | public class KtxValidatorsTests 8 | { 9 | [SetUp] 10 | public void Setup() 11 | { 12 | } 13 | 14 | [Test] 15 | public void GenericStreamValidationTest() 16 | { 17 | // Arrange 18 | MemoryStream ms = new MemoryStream(new byte[] { 0 }); 19 | // Close MemoryStream since then it should have CanRead as false https://msdn.microsoft.com/en-us/library/system.io.memorystream.canread.aspx 20 | ms.Close(); 21 | 22 | MemoryStream notMuchContent = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); 23 | 24 | // Act 25 | var nullShouldError = KtxValidators.GenericStreamValidation(null); 26 | var closedMemoryStreamShouldError = KtxValidators.GenericStreamValidation(ms); 27 | var notEnoughContentShouldError = KtxValidators.GenericStreamValidation(notMuchContent); 28 | 29 | // Assert 30 | Assert.IsFalse(nullShouldError.isValid); 31 | Assert.IsTrue(nullShouldError.possibleError.Contains("is null")); 32 | 33 | Assert.IsFalse(closedMemoryStreamShouldError.isValid); 34 | Assert.IsTrue(closedMemoryStreamShouldError.possibleError.Contains("not readable")); 35 | 36 | Assert.IsFalse(notEnoughContentShouldError.isValid); 37 | Assert.IsTrue(notEnoughContentShouldError.possibleError.Contains("should have at")); 38 | } 39 | 40 | [Test] 41 | public void KtxHeaderGenerationValidation() 42 | { 43 | // Arrange 44 | KtxHeader header = new KtxHeader(GlDataType.Compressed, GlPixelFormat.GL_RGBA, GlInternalFormat.GL_COMPRESSED_RGBA_ASTC_10x10_KHR, 256, 256, 1, new System.Collections.Generic.Dictionary()); 45 | 46 | // Act 47 | MemoryStream ms1 = new MemoryStream(); 48 | header.WriteTo(ms1); 49 | MemoryStream ms2 = new MemoryStream(ms1.ToArray()); 50 | (bool valid, string possibleError) = KtxValidators.ValidateHeaderData(ms2); 51 | 52 | // Assert 53 | Assert.IsTrue(valid); 54 | Assert.IsTrue(string.IsNullOrEmpty(possibleError)); 55 | } 56 | 57 | [Test, Description("KTX2 identifier test support")] 58 | public void Ktx2IdentifierTest() 59 | { 60 | // Arrange 61 | byte[] bytes = new byte[] 62 | { 63 | // Sample data from https://github.khronos.org/KTX-Specification/ktxspec.v2.html 64 | 0xAB, 0x4B, 0x54, 0x58, // first four bytes of Byte[12] identifier 65 | 0x20, 0x32, 0x30, 0xBB, // next four bytes of Byte[12] identifier 66 | 0x0D, 0x0A, 0x1A, 0x0A, // final four bytes of Byte[12] identifier 67 | 0x00, 0x00, 0x00, 0x00, // UInt32 vkFormat = VK_FORMAT_UNDEFINED (0) 68 | 0x01, 0x00, 0x00, 0x00, // UInt32 typeSize = 1 69 | 0x08, 0x00, 0x00, 0x00, // UInt32 pixelWidth = 8 70 | 0x08, 0x00, 0x00, 0x00, // UInt32 pixelHeight = 8 71 | 0x00, 0x00, 0x00, 0x00, // UInt32 pixelDepth = 0 72 | 0x00, 0x00, 0x00, 0x00, // UInt32 layerCount = 0 73 | 0x01, 0x00, 0x00, 0x00, // UInt32 faceCount = 0 74 | 0x01, 0x00, 0x00, 0x00, // UInt32 levelCount = 0 75 | 0x01, 0x00, 0x00, 0x00, // UInt32 supercompressionScheme = 1 (BASISLZ) 76 | }; 77 | 78 | MemoryStream ktx2Header = new MemoryStream(bytes); 79 | 80 | // Act 81 | var kt2ContentError = KtxValidators.ValidateIdentifier(ktx2Header); 82 | 83 | // Assert 84 | Assert.IsFalse(kt2ContentError.isValid); 85 | Assert.IsTrue(kt2ContentError.possibleError.Contains("KTX version 2")); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /lib/KtxTextureData.cs: -------------------------------------------------------------------------------- 1 | // Class for storing texture data 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Collections.Generic; 6 | 7 | namespace KtxSharp 8 | { 9 | /// 10 | /// Texture data class 11 | /// 12 | public sealed class KtxTextureData 13 | { 14 | /// 15 | /// How many bytes of texture data there is 16 | /// 17 | public readonly uint totalTextureDataLength; 18 | 19 | /// 20 | /// Texture data as raw bytes 21 | /// 22 | public readonly byte[] textureDataAsRawBytes; 23 | 24 | /// 25 | /// Texture type (basic) 26 | /// 27 | public readonly TextureTypeBasic textureType; 28 | 29 | /// 30 | /// Texture data for each mip map level 31 | /// 32 | public List textureDataOfMipmapLevel; 33 | 34 | /// 35 | /// Constructor for texture data 36 | /// 37 | /// List of 2d texture bytes, one for each mipmap level 38 | public KtxTextureData(List textureDatas) 39 | { 40 | if (textureDatas == null || textureDatas.Count < 1) 41 | { 42 | throw new ArgumentException("Texturedatas must contain something"); 43 | } 44 | 45 | this.textureDataOfMipmapLevel = textureDatas; 46 | 47 | this.textureType = this.textureDataOfMipmapLevel.Count > 1 ? TextureTypeBasic.Basic2DWithMipmaps : TextureTypeBasic.Basic2DNoMipmaps; 48 | } 49 | 50 | /// 51 | /// Constructor for texture data 52 | /// 53 | /// Header 54 | /// Stream for reading 55 | public KtxTextureData(KtxHeader header, Stream stream) 56 | { 57 | //this.totalTextureDataLength = (uint)stream.Length; 58 | 59 | // Try to figure out texture type basic 60 | bool containsMipmaps = header.numberOfMipmapLevels > 1; 61 | 62 | if (header.numberOfArrayElements == 0 || header.numberOfArrayElements == 1) 63 | { 64 | // Is NOT texture array 65 | 66 | if (header.numberOfFaces == 0 || header.numberOfFaces == 1) 67 | { 68 | // Is NOT cube texture 69 | 70 | if (header.pixelDepth == 0 || header.pixelDepth == 1) 71 | { 72 | // Is not 3D texture 73 | 74 | if (header.pixelHeight == 0 || header.pixelHeight == 1) 75 | { 76 | // 1D texture 77 | this.textureType = containsMipmaps ? TextureTypeBasic.Basic1DWithMipmaps : TextureTypeBasic.Basic1DNoMipmaps; 78 | } 79 | else 80 | { 81 | // 2D texture 82 | this.textureType = containsMipmaps ? TextureTypeBasic.Basic2DWithMipmaps : TextureTypeBasic.Basic2DNoMipmaps; 83 | } 84 | } 85 | else 86 | { 87 | // Is 3D texture 88 | this.textureType = containsMipmaps ? TextureTypeBasic.Basic3DWithMipmaps : TextureTypeBasic.Basic3DNoMipmaps; 89 | } 90 | } 91 | else 92 | { 93 | // Is cube texture 94 | } 95 | } 96 | else 97 | { 98 | // Is Texture array 99 | } 100 | 101 | uint mipmapLevels = header.numberOfMipmapLevels; 102 | if (mipmapLevels == 0) 103 | { 104 | mipmapLevels = 1; 105 | } 106 | 107 | // Since we know how many mipmap levels there are, allocate the capacity 108 | this.textureDataOfMipmapLevel = new List((int)mipmapLevels); 109 | 110 | // Check if length reads should be endian swapped 111 | bool shouldSwapEndianness = (header.endiannessValue != Common.expectedEndianValue); 112 | 113 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true)) 114 | { 115 | for (int i = 0; i < mipmapLevels; i++) 116 | { 117 | uint amountOfDataInThisMipmapLevel = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 118 | this.textureDataOfMipmapLevel.Add(reader.ReadBytes((int)amountOfDataInThisMipmapLevel)); 119 | 120 | // Skip possible padding bytes 121 | while (amountOfDataInThisMipmapLevel % 4 != 0) 122 | { 123 | amountOfDataInThisMipmapLevel++; 124 | // Read but ignore values 125 | reader.ReadByte(); 126 | } 127 | } 128 | } 129 | } 130 | 131 | /// 132 | /// Write content to stream. Leaves stream open 133 | /// 134 | /// Output stream 135 | /// Write little endian output (default enabled) 136 | public void WriteTo(Stream output, bool writeLittleEndian = true) 137 | { 138 | using (BinaryWriter writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true)) 139 | { 140 | Action writeUint = writer.Write; 141 | if (!writeLittleEndian) 142 | { 143 | writeUint = (uint u) => WriteUintAsBigEndian(writer, u); 144 | } 145 | 146 | GenericWrite(this.textureDataOfMipmapLevel, writeUint, writer.Write); 147 | } 148 | } 149 | 150 | private static void GenericWrite(List textureDataOfMipmapLevel, Action writeUint, Action writeByteArray) 151 | { 152 | foreach (byte[] level in textureDataOfMipmapLevel) 153 | { 154 | writeUint((uint)level.Length); 155 | writeByteArray(level); 156 | } 157 | } 158 | 159 | private static void WriteUintAsBigEndian(BinaryWriter writer, uint value) 160 | { 161 | writer.Write(KtxBitFiddling.SwapEndian(value)); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /lib/KtxValidators.cs: -------------------------------------------------------------------------------- 1 | // Validators for headers and texture data 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Linq; 6 | 7 | namespace KtxSharp 8 | { 9 | /// 10 | /// Static class for Ktx validation 11 | /// 12 | public static class KtxValidators 13 | { 14 | // There must be at least 64 bytes of input 15 | private static readonly int minInputSizeInBytes = 64; 16 | 17 | /// 18 | /// Generic stream validation 19 | /// 20 | /// Input stream to read 21 | /// Tuple that tells if stream is valid, and possible error 22 | public static (bool isValid, string possibleError) GenericStreamValidation(Stream stream) 23 | { 24 | if (stream == null) 25 | { 26 | return (isValid: false, possibleError: "Stream is null!"); 27 | } 28 | 29 | if (!stream.CanRead) 30 | { 31 | return (isValid: false, possibleError: "Stream is not readable!"); 32 | } 33 | 34 | if (stream.Length < minInputSizeInBytes) 35 | { 36 | return (isValid: false, possibleError: $"KTX input should have at least { minInputSizeInBytes } bytes!"); 37 | } 38 | 39 | return (isValid: true, possibleError: ""); 40 | } 41 | 42 | /// 43 | /// Validate identifier 44 | /// 45 | /// Stream for reading 46 | /// Tuple that tells if stream has valid identifier, and possible error 47 | public static (bool isValid, string possibleError) ValidateIdentifier(Stream stream) 48 | { 49 | try 50 | { 51 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true)) 52 | { 53 | byte[] tempIdentifier = reader.ReadBytes(Common.onlyValidIdentifier.Length); 54 | 55 | if (!Common.onlyValidIdentifier.SequenceEqual(tempIdentifier)) 56 | { 57 | if (Common.ktx2ValidIdentifier.SequenceEqual(tempIdentifier)) 58 | { 59 | return (isValid: false, possibleError: "KTX version 2 is not supported!"); 60 | } 61 | 62 | return (isValid: false, possibleError: "Identifier does not match requirements!"); 63 | } 64 | } 65 | } 66 | catch (Exception e) 67 | { 68 | return (isValid: false, e.ToString()); 69 | } 70 | 71 | return (isValid: true, possibleError: ""); 72 | } 73 | 74 | /// 75 | /// Validate header data 76 | /// 77 | /// Stream for reading 78 | /// Tuple that tells if stream is valid, and possible error 79 | public static (bool isValid, string possibleError) ValidateHeaderData(Stream stream) 80 | { 81 | // Use the stream in a binary reader. 82 | try 83 | { 84 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true)) 85 | { 86 | // Start validating header 87 | uint tempEndian = reader.ReadUInt32(); 88 | 89 | if (Common.expectedEndianValue != tempEndian && Common.otherValidEndianValue != tempEndian) 90 | { 91 | return (isValid: false, possibleError: "Endianness does not match requirements!"); 92 | } 93 | 94 | bool shouldSwapEndianness = (tempEndian != Common.expectedEndianValue); 95 | 96 | uint glTypeTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 97 | // TODO: uint glType to enum 98 | 99 | // If glType is 0 it should mean that this is compressed texture 100 | bool assumeCompressedTexture = (glTypeTemp == 0); 101 | 102 | uint glTypeSizeTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 103 | 104 | if (assumeCompressedTexture && glTypeSizeTemp != 1) 105 | { 106 | return (isValid: false, possibleError: "glTypeSize should be 1 for compressed textures!"); 107 | } 108 | 109 | uint glFormatTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 110 | 111 | if (assumeCompressedTexture && glFormatTemp != 0) 112 | { 113 | return (isValid: false, possibleError: "glFormat should be 0 for compressed textures!"); 114 | } 115 | 116 | uint glInternalFormatTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 117 | 118 | uint glBaseInternalFormatTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 119 | 120 | uint pixelWidthTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 121 | 122 | uint pixelHeightTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 123 | 124 | uint pixelDepthTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 125 | 126 | uint numberOfArrayElementsTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 127 | 128 | uint numberOfFacesTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 129 | 130 | uint numberOfMipmapLevelsTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 131 | 132 | uint sizeOfKeyValueDataTemp = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 133 | if (sizeOfKeyValueDataTemp % 4 != 0) 134 | { 135 | return (isValid: false, possibleError: ErrorGen.Modulo4Error(nameof(sizeOfKeyValueDataTemp), sizeOfKeyValueDataTemp)); 136 | } 137 | 138 | // Validate metadata 139 | (bool validMedata, string possibleMetadataError) = ValidateMetadata(reader, sizeOfKeyValueDataTemp, shouldSwapEndianness); 140 | if (!validMedata) 141 | { 142 | return (isValid: false, possibleError: possibleMetadataError); 143 | } 144 | } 145 | } 146 | catch (Exception e) 147 | { 148 | return (isValid: false, e.ToString()); 149 | } 150 | 151 | return (isValid: true, possibleError: ""); 152 | } 153 | 154 | /// 155 | /// Validate texture data 156 | /// 157 | /// Stream for reading 158 | /// Header 159 | /// Expected texture data size 160 | /// Tuple that tells if stream is valid, and possible error 161 | public static (bool isValid, string possibleError) ValidateTextureData(Stream stream, KtxHeader header, uint expectedTextureDataSize) 162 | { 163 | // Use the stream in a binary reader. 164 | try 165 | { 166 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true)) 167 | { 168 | // Specs say that if value of certain things is zero (0) then it should be used as one (1) 169 | uint mipmapLevels = (header.numberOfMipmapLevels == 0) ? 1 : header.numberOfMipmapLevels; 170 | 171 | uint numberOfArrayElements = (header.numberOfArrayElements == 0) ? 1 : header.numberOfArrayElements; 172 | 173 | uint pixelDepth = (header.pixelDepth == 0) ? 1 : header.pixelDepth; 174 | 175 | uint pixelHeight = (header.pixelHeight == 0) ? 1 : header.pixelHeight; 176 | 177 | uint totalLengthOfTextureDataSection = 0; 178 | 179 | // Check if length reads should be endian swapped 180 | bool shouldSwapEndianness = (header.endiannessValue != Common.expectedEndianValue); 181 | 182 | // Check each mipmap level separately 183 | for (uint u = 0; u < mipmapLevels; u++) 184 | { 185 | uint imageSize = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 186 | totalLengthOfTextureDataSection += (imageSize + (uint)Common.sizeOfUint); 187 | if (imageSize > expectedTextureDataSize || totalLengthOfTextureDataSection > expectedTextureDataSize) 188 | { 189 | return (isValid: false, "Texture data: More data than expected!"); 190 | } 191 | 192 | // TODO: More checks! 193 | 194 | // Read but do not use data for anything 195 | reader.ReadBytes((int)imageSize); 196 | 197 | // Skip possible padding bytes 198 | while (imageSize % 4 != 0) 199 | { 200 | imageSize++; 201 | // Read but ignore values 202 | reader.ReadByte(); 203 | } 204 | } 205 | } 206 | } 207 | catch (Exception e) 208 | { 209 | return (isValid: false, e.ToString()); 210 | } 211 | 212 | return (isValid: true, possibleError: ""); 213 | } 214 | 215 | private static (bool isValid, string possibleError) ValidateMetadata(BinaryReader reader, uint bytesOfKeyValueData, bool shouldSwapEndianness) 216 | { 217 | uint currentPosition = 0; 218 | 219 | while (currentPosition < bytesOfKeyValueData) 220 | { 221 | uint combinedKeyAndValueSize = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 222 | currentPosition += (uint)Common.sizeOfUint; 223 | 224 | if ((currentPosition + combinedKeyAndValueSize) > bytesOfKeyValueData) 225 | { 226 | return (isValid: false, possibleError: "Metadata: combinedKeyAndValueSize would go beyond Metadata array!"); 227 | } 228 | 229 | // There should be at least NUL 230 | byte[] keyAndValueAsBytes = reader.ReadBytes((int)combinedKeyAndValueSize); 231 | 232 | if (!keyAndValueAsBytes.Contains(Common.nulByte)) 233 | { 234 | return (isValid: false, possibleError: "Metadata: KeyValue pair does not contain NUL byte!"); 235 | } 236 | 237 | // Check if key is valid UTF-8 byte combination 238 | try 239 | { 240 | UTF8Encoding utf8ThrowException = new UTF8Encoding(false, true); 241 | string notUsed = utf8ThrowException.GetString(keyAndValueAsBytes, 0, keyAndValueAsBytes.Length); 242 | } 243 | catch (Exception e) 244 | { 245 | return (isValid: false, possibleError: $"Byte array to UTF-8 failed: {e}!"); 246 | } 247 | 248 | currentPosition += combinedKeyAndValueSize; 249 | 250 | // Skip value paddings if there are any 251 | while (currentPosition % 4 != 0) 252 | { 253 | currentPosition++; 254 | reader.ReadByte(); 255 | } 256 | } 257 | 258 | return (isValid: true, possibleError: ""); 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /tests/KtxLoaderTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using KtxSharp; 3 | using System.IO; 4 | using System.Collections.Generic; 5 | 6 | namespace Tests 7 | { 8 | public class KtxLoaderTests 9 | { 10 | 11 | 12 | [Test] 13 | public void ValidityWithValidSamplesTest() 14 | { 15 | // Arrange 16 | byte[] inputBytes1 = File.ReadAllBytes(CommonFiles.validSample1Filename); 17 | byte[] inputBytes2 = File.ReadAllBytes(CommonFiles.validSample2Filename); 18 | byte[] inputBytes3 = File.ReadAllBytes(CommonFiles.validSample3Filename); 19 | byte[] inputBytes4 = File.ReadAllBytes(CommonFiles.validSample4Filename); 20 | byte[] inputBytes5 = File.ReadAllBytes(CommonFiles.validSample5Filename); 21 | byte[] inputBytes6 = File.ReadAllBytes(CommonFiles.validSample6Filename); 22 | byte[] inputBytes7 = File.ReadAllBytes(CommonFiles.validSample7Filename); 23 | byte[] inputBytes8 = File.ReadAllBytes(CommonFiles.validSample8Filename); 24 | 25 | // Act 26 | bool wasTest1Valid = false; 27 | string test1PossibleError = ""; 28 | using (MemoryStream ms1 = new MemoryStream(inputBytes1)) 29 | { 30 | (wasTest1Valid, test1PossibleError) = KtxLoader.CheckIfInputIsValid(ms1); 31 | } 32 | 33 | bool wasTest2Valid = false; 34 | string test2PossibleError = ""; 35 | using (MemoryStream ms2 = new MemoryStream(inputBytes2)) 36 | { 37 | (wasTest2Valid, test2PossibleError) = KtxLoader.CheckIfInputIsValid(ms2); 38 | } 39 | 40 | bool wasTest3Valid = false; 41 | string test3PossibleError = ""; 42 | using (MemoryStream ms3 = new MemoryStream(inputBytes3)) 43 | { 44 | (wasTest3Valid, test3PossibleError) = KtxLoader.CheckIfInputIsValid(ms3); 45 | } 46 | 47 | bool wasTest4Valid = false; 48 | string test4PossibleError = ""; 49 | using (MemoryStream ms4 = new MemoryStream(inputBytes4)) 50 | { 51 | (wasTest4Valid, test4PossibleError) = KtxLoader.CheckIfInputIsValid(ms4); 52 | } 53 | 54 | bool wasTest5Valid = false; 55 | string test5PossibleError = ""; 56 | using (MemoryStream ms5 = new MemoryStream(inputBytes5)) 57 | { 58 | (wasTest5Valid, test5PossibleError) = KtxLoader.CheckIfInputIsValid(ms5); 59 | } 60 | 61 | bool wasTest6Valid = false; 62 | string test6PossibleError = ""; 63 | using (MemoryStream ms6 = new MemoryStream(inputBytes6)) 64 | { 65 | (wasTest6Valid, test6PossibleError) = KtxLoader.CheckIfInputIsValid(ms6); 66 | } 67 | 68 | bool wasTest7Valid = false; 69 | string test7PossibleError = ""; 70 | using (MemoryStream ms7 = new MemoryStream(inputBytes7)) 71 | { 72 | (wasTest7Valid, test7PossibleError) = KtxLoader.CheckIfInputIsValid(ms7); 73 | } 74 | 75 | bool wasTest8Valid = false; 76 | string test8PossibleError = ""; 77 | using (MemoryStream ms8 = new MemoryStream(inputBytes8)) 78 | { 79 | (wasTest8Valid, test8PossibleError) = KtxLoader.CheckIfInputIsValid(ms8); 80 | } 81 | 82 | // Assert 83 | var allFiles = new List() { inputBytes1, inputBytes2, inputBytes3, inputBytes4, inputBytes5, inputBytes6, inputBytes7, inputBytes8 }; 84 | CollectionAssert.AllItemsAreUnique(allFiles, "All input should be unique"); 85 | 86 | Assert.IsTrue(wasTest1Valid); 87 | Assert.IsTrue(wasTest2Valid); 88 | Assert.IsTrue(wasTest3Valid); 89 | Assert.IsTrue(wasTest4Valid); 90 | Assert.IsTrue(wasTest5Valid); 91 | Assert.IsTrue(wasTest6Valid); 92 | Assert.IsTrue(wasTest7Valid); 93 | Assert.IsTrue(wasTest8Valid); 94 | 95 | Assert.AreEqual("", test1PossibleError, "There should NOT be any errors"); 96 | Assert.AreEqual("", test2PossibleError, "There should NOT be any errors"); 97 | Assert.AreEqual("", test3PossibleError, "There should NOT be any errors"); 98 | Assert.AreEqual("", test4PossibleError, "There should NOT be any errors"); 99 | Assert.AreEqual("", test5PossibleError, "There should NOT be any errors"); 100 | Assert.AreEqual("", test6PossibleError, "There should NOT be any errors"); 101 | Assert.AreEqual("", test7PossibleError, "There should NOT be any errors"); 102 | Assert.AreEqual("", test8PossibleError, "There should NOT be any errors"); 103 | } 104 | 105 | [Test] 106 | public void ValidityWithInvalidSamplesTest() 107 | { 108 | // Arrange 109 | byte[] inputBytes4 = File.ReadAllBytes(CommonFiles.validSample4Filename); 110 | 111 | // Act 112 | inputBytes4[73] = 0xC0; // Make string invalid UTF-8 113 | inputBytes4[74] = 0xB1; 114 | 115 | bool wasTest4Valid = false; 116 | string test4PossibleError = ""; 117 | using (MemoryStream ms4 = new MemoryStream(inputBytes4)) 118 | { 119 | (wasTest4Valid, test4PossibleError) = KtxLoader.CheckIfInputIsValid(ms4); 120 | } 121 | 122 | // Assert 123 | Assert.IsFalse(wasTest4Valid); 124 | Assert.IsTrue(test4PossibleError.Contains("Byte array to UTF-8 failed")); 125 | } 126 | 127 | [Test] 128 | public void CheckHeadersWithValidSamplesTest() 129 | { 130 | // Arrange 131 | byte[] inputBytes1 = File.ReadAllBytes(CommonFiles.validSample1Filename); 132 | byte[] inputBytes2 = File.ReadAllBytes(CommonFiles.validSample2Filename); 133 | byte[] inputBytes3 = File.ReadAllBytes(CommonFiles.validSample3Filename); 134 | byte[] inputBytes4 = File.ReadAllBytes(CommonFiles.validSample4Filename); 135 | byte[] inputBytes5 = File.ReadAllBytes(CommonFiles.validSample5Filename); 136 | byte[] inputBytes6 = File.ReadAllBytes(CommonFiles.validSample6Filename); 137 | byte[] inputBytes7 = File.ReadAllBytes(CommonFiles.validSample7Filename); 138 | byte[] inputBytes8 = File.ReadAllBytes(CommonFiles.validSample8Filename); 139 | 140 | // Act 141 | KtxStructure ktxStructure1 = null; 142 | using (MemoryStream ms1 = new MemoryStream(inputBytes1)) 143 | { 144 | ktxStructure1 = KtxLoader.LoadInput(ms1); 145 | } 146 | 147 | KtxStructure ktxStructure2 = null; 148 | using (MemoryStream ms2 = new MemoryStream(inputBytes2)) 149 | { 150 | ktxStructure2 = KtxLoader.LoadInput(ms2); 151 | } 152 | 153 | KtxStructure ktxStructure3 = null; 154 | using (MemoryStream ms3 = new MemoryStream(inputBytes3)) 155 | { 156 | ktxStructure3 = KtxLoader.LoadInput(ms3); 157 | } 158 | 159 | KtxStructure ktxStructure4 = null; 160 | using (MemoryStream ms4 = new MemoryStream(inputBytes4)) 161 | { 162 | ktxStructure4 = KtxLoader.LoadInput(ms4); 163 | } 164 | 165 | KtxStructure ktxStructure5 = null; 166 | using (MemoryStream ms5 = new MemoryStream(inputBytes5)) 167 | { 168 | ktxStructure5 = KtxLoader.LoadInput(ms5); 169 | } 170 | 171 | KtxStructure ktxStructure6 = null; 172 | using (MemoryStream ms6 = new MemoryStream(inputBytes6)) 173 | { 174 | ktxStructure6 = KtxLoader.LoadInput(ms6); 175 | } 176 | 177 | KtxStructure ktxStructure7 = null; 178 | using (MemoryStream ms7 = new MemoryStream(inputBytes7)) 179 | { 180 | ktxStructure7 = KtxLoader.LoadInput(ms7); 181 | } 182 | 183 | KtxStructure ktxStructure8 = null; 184 | using (MemoryStream ms8 = new MemoryStream(inputBytes8)) 185 | { 186 | ktxStructure8 = KtxLoader.LoadInput(ms8); 187 | } 188 | 189 | // Assert 190 | var allFiles = new List() { inputBytes1, inputBytes2, inputBytes3, inputBytes4, inputBytes5, inputBytes6, inputBytes7, inputBytes8 }; 191 | CollectionAssert.AllItemsAreUnique(allFiles, "All input should be unique"); 192 | 193 | // Compressonator sample file resolution 194 | Assert.AreEqual(16, ktxStructure1.header.pixelWidth); 195 | Assert.AreEqual(16, ktxStructure1.header.pixelHeight); 196 | 197 | // Compressonator sample file format 198 | Assert.AreEqual(GlPixelFormat.GL_RGBA, ktxStructure1.header.glFormat); 199 | Assert.AreEqual((uint)GlPixelFormat.GL_RGBA, ktxStructure1.header.glFormatAsUint); 200 | Assert.IsTrue(ktxStructure1.header.isInputLittleEndian); 201 | 202 | // Compressonator sample file Data type and internal format 203 | Assert.AreEqual(GlDataType.GL_UNSIGNED_BYTE, ktxStructure1.header.glDataType); 204 | Assert.AreEqual((uint)GlDataType.GL_UNSIGNED_BYTE, ktxStructure1.header.glTypeAsUint); 205 | Assert.AreEqual(GlInternalFormat.GL_RGBA8_OES, ktxStructure1.header.glInternalFormat); 206 | Assert.AreEqual((uint)GlInternalFormat.GL_RGBA8_OES, ktxStructure1.header.glInternalFormatAsUint); 207 | 208 | 209 | // PVRTexTool sample file resolution 210 | Assert.AreEqual(16, ktxStructure2.header.pixelWidth); 211 | Assert.AreEqual(16, ktxStructure2.header.pixelHeight); 212 | 213 | // PVRTexTool sample file format 214 | Assert.IsTrue(ktxStructure2.header.isInputLittleEndian); 215 | 216 | // PVRTexTool sample file Data type and internal format 217 | Assert.AreEqual(GlDataType.GL_UNSIGNED_BYTE, ktxStructure2.header.glDataType); 218 | Assert.AreEqual((uint)GlDataType.GL_UNSIGNED_BYTE, ktxStructure2.header.glTypeAsUint); 219 | Assert.AreEqual(GlInternalFormat.GL_RGBA8_OES, ktxStructure2.header.glInternalFormat); 220 | Assert.AreEqual((uint)GlInternalFormat.GL_RGBA8_OES, ktxStructure2.header.glInternalFormatAsUint); 221 | 222 | 223 | // ETCPACK sample file resolution 224 | Assert.AreEqual(2048, ktxStructure3.header.pixelWidth); 225 | Assert.AreEqual(32, ktxStructure3.header.pixelHeight); 226 | 227 | // ETCPACK sample file Data type and internal format 228 | Assert.AreEqual(GlDataType.Compressed, ktxStructure3.header.glDataType); 229 | Assert.AreEqual((uint)GlDataType.Compressed, ktxStructure3.header.glTypeAsUint); 230 | Assert.AreEqual(GlInternalFormat.GL_COMPRESSED_SIGNED_R11_EAC, ktxStructure3.header.glInternalFormat); 231 | Assert.AreEqual((uint)GlInternalFormat.GL_COMPRESSED_SIGNED_R11_EAC, ktxStructure3.header.glInternalFormatAsUint); 232 | 233 | // ktx_specs.ktx resolution 234 | Assert.AreEqual(32, ktxStructure4.header.pixelWidth); 235 | Assert.AreEqual(32, ktxStructure4.header.pixelHeight); 236 | 237 | // ktx_specs.ktx Data type and internal format 238 | Assert.AreEqual(GlDataType.Compressed, ktxStructure4.header.glDataType); 239 | Assert.AreEqual((uint)GlDataType.Compressed, ktxStructure4.header.glTypeAsUint); 240 | Assert.AreEqual(GlInternalFormat.GL_ETC1_RGB8_OES, ktxStructure4.header.glInternalFormat); 241 | Assert.AreEqual((uint)GlInternalFormat.GL_ETC1_RGB8_OES, ktxStructure4.header.glInternalFormatAsUint); 242 | Assert.IsFalse(ktxStructure4.header.isInputLittleEndian); 243 | 244 | // ktx_specs.ktx Metadata 245 | Assert.AreEqual(1, ktxStructure4.header.metadataDictionary.Count); 246 | Assert.IsTrue(ktxStructure4.header.metadataDictionary.ContainsKey("api")); 247 | Assert.IsTrue(ktxStructure4.header.metadataDictionary["api"].isString); 248 | Assert.AreEqual("joke2", ktxStructure4.header.metadataDictionary["api"].stringValue); 249 | 250 | // etc2-rgba8.ktx resolution 251 | Assert.AreEqual(128, ktxStructure5.header.pixelWidth); 252 | Assert.AreEqual(128, ktxStructure5.header.pixelHeight); 253 | 254 | // etc2-rgba8.ktx Data type and internal format 255 | Assert.AreEqual(GlDataType.Compressed, ktxStructure5.header.glDataType); 256 | Assert.AreEqual((uint)GlDataType.Compressed, ktxStructure5.header.glTypeAsUint); 257 | Assert.AreEqual(GlInternalFormat.GL_COMPRESSED_RGBA8_ETC2_EAC, ktxStructure5.header.glInternalFormat); 258 | Assert.AreEqual((uint)GlInternalFormat.GL_COMPRESSED_RGBA8_ETC2_EAC, ktxStructure5.header.glInternalFormatAsUint); 259 | 260 | // smiling_etc_64x64_Compressonator.ktx resolution 261 | Assert.AreEqual(64, ktxStructure6.header.pixelWidth); 262 | Assert.AreEqual(64, ktxStructure6.header.pixelHeight); 263 | 264 | // smiling_etc_64x64_Compressonator.ktx Data type and internal format 265 | Assert.AreEqual(GlDataType.Compressed, ktxStructure6.header.glDataType); 266 | Assert.AreEqual((uint)GlDataType.Compressed, ktxStructure6.header.glTypeAsUint); 267 | Assert.AreEqual(GlInternalFormat.GL_ETC1_RGB8_OES, ktxStructure6.header.glInternalFormat); 268 | Assert.AreEqual((uint)GlInternalFormat.GL_ETC1_RGB8_OES, ktxStructure6.header.glInternalFormatAsUint); 269 | 270 | // smiling_ATC_RGBA_Explicit.ktx resolution 271 | Assert.AreEqual(64, ktxStructure7.header.pixelWidth); 272 | Assert.AreEqual(64, ktxStructure7.header.pixelHeight); 273 | 274 | // smiling_ATC_RGBA_Explicit.ktx Data type and internal format 275 | Assert.AreEqual(GlDataType.Compressed, ktxStructure7.header.glDataType); 276 | Assert.AreEqual((uint)GlDataType.Compressed, ktxStructure7.header.glTypeAsUint); 277 | Assert.AreEqual(GlInternalFormat.GL_ATC_RGBA_EXPLICIT_ALPHA_AMD, ktxStructure7.header.glInternalFormat); 278 | Assert.AreEqual((uint)GlInternalFormat.GL_ATC_RGBA_EXPLICIT_ALPHA_AMD, ktxStructure7.header.glInternalFormatAsUint); 279 | 280 | // format_pvrtc1_4bpp_unorm.ktx resolution 281 | Assert.AreEqual(64, ktxStructure8.header.pixelWidth); 282 | Assert.AreEqual(64, ktxStructure8.header.pixelHeight); 283 | 284 | // format_pvrtc1_4bpp_unorm.ktx Data type and internal format 285 | Assert.AreEqual(GlDataType.Compressed, ktxStructure8.header.glDataType); 286 | Assert.AreEqual((uint)GlDataType.Compressed, ktxStructure8.header.glTypeAsUint); 287 | Assert.AreEqual(GlInternalFormat.GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, ktxStructure8.header.glInternalFormat); 288 | Assert.AreEqual((uint)GlInternalFormat.GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, ktxStructure8.header.glInternalFormatAsUint); 289 | } 290 | } 291 | } -------------------------------------------------------------------------------- /lib/KtxCommon.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Collections.Generic; 3 | 4 | // Common global values 5 | namespace KtxSharp 6 | { 7 | /// 8 | /// Common values 9 | /// 10 | public static class Common 11 | { 12 | /// 13 | /// There is only one valid file identifier for KTX 1 header, it is '«', 'K', 'T', 'X', ' ', '1', '1', '»', '\r', '\n', '\x1A', '\n' 14 | /// 15 | /// 16 | public static readonly byte[] onlyValidIdentifier = new byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; 17 | 18 | /// 19 | /// KTX 2 format is not supported, but it is needed for better error message 20 | /// 21 | public static readonly byte[] ktx2ValidIdentifier = new byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; 22 | 23 | /// 24 | /// Expected Endian value 25 | /// 26 | public static readonly uint expectedEndianValue = 0x04030201; 27 | 28 | /// 29 | /// Other valid Endian value 30 | /// 31 | public static readonly uint otherValidEndianValue = 0x01020304; 32 | 33 | /// 34 | /// Big endian value as bytes 35 | /// 36 | /// 37 | public static readonly byte[] bigEndianAsBytes = new byte[] { 0x04, 0x03, 0x02, 0x01 }; 38 | 39 | /// 40 | /// Little endian value as bytes 41 | /// 42 | /// 43 | public static readonly byte[] littleEndianAsBytes = new byte[] { 0x01, 0x02, 0x03, 0x04 }; 44 | 45 | /// 46 | /// NUL is used to terminate UTF-8 strings, and padd metadata 47 | /// 48 | public static readonly byte nulByte = 0; 49 | 50 | /// 51 | /// Sizeof(uint) 52 | /// 53 | /// 54 | public static readonly int sizeOfUint = sizeof(uint); 55 | 56 | /// 57 | /// Get length of UTF-8 string as bytes 58 | /// 59 | /// Input string 60 | /// Length in bytes 61 | public static uint GetLengthOfUtf8StringAsBytes(string inputString) 62 | { 63 | return (uint)Encoding.UTF8.GetByteCount(inputString); 64 | } 65 | 66 | /// 67 | /// Get UTF-8 string as byte array 68 | /// 69 | /// Input string 70 | /// Byte array 71 | public static byte[] GetUtf8StringAsBytes(string inputString) 72 | { 73 | return Encoding.UTF8.GetBytes(inputString); 74 | } 75 | 76 | /// 77 | /// GlType to size 78 | /// 79 | public static readonly Dictionary GlTypeToSize = new Dictionary() 80 | { 81 | { GlDataType.Compressed, 1 }, 82 | { GlDataType.GL_BYTE, 1 }, 83 | { GlDataType.GL_UNSIGNED_BYTE, 1 }, 84 | 85 | { GlDataType.GL_SHORT, 2 }, 86 | { GlDataType.GL_UNSIGNED_SHORT, 2 }, 87 | 88 | { GlDataType.GL_FLOAT, 4 }, 89 | { GlDataType.GL_FIXED, 4 } 90 | }; 91 | } 92 | 93 | /// 94 | /// Gl Data type 95 | /// 96 | public enum GlDataType : uint 97 | { 98 | /// 99 | /// Compressed 100 | /// 101 | Compressed = 0, 102 | 103 | /// 104 | /// GL_BYTE 105 | /// 106 | GL_BYTE = 0x1400, 107 | 108 | /// 109 | /// GL_UNSIGNED_BYTE 110 | /// 111 | GL_UNSIGNED_BYTE = 0x1401, 112 | 113 | /// 114 | /// GL_SHORT 115 | /// 116 | GL_SHORT = 0x1402, 117 | 118 | /// 119 | /// GL_UNSIGNED_SHORT 120 | /// 121 | GL_UNSIGNED_SHORT = 0x1403, 122 | 123 | /// 124 | /// GL_FLOAT 125 | /// 126 | GL_FLOAT = 0x1406, 127 | 128 | /// 129 | /// GL_FIXED 130 | /// 131 | GL_FIXED = 0x140C, 132 | 133 | /// 134 | /// Custom value for situation where parser cannot identify format 135 | /// 136 | NotKnown = 0xFFFF, 137 | } 138 | 139 | /// 140 | /// GlPixelFormat 141 | /// 142 | public enum GlPixelFormat : uint 143 | { 144 | /// 145 | /// GL_COLOR_INDEX 146 | /// 147 | GL_COLOR_INDEX = 0x1900, 148 | 149 | /// 150 | /// GL_STENCIL_INDEX 151 | /// 152 | GL_STENCIL_INDEX = 0x1901, 153 | 154 | /// 155 | /// GL_DEPTH_COMPONENT 156 | /// 157 | GL_DEPTH_COMPONENT = 0x1902, 158 | 159 | /// 160 | /// GL_RED 161 | /// 162 | GL_RED = 0x1903, 163 | 164 | /// 165 | /// GL_GREEN 166 | /// 167 | GL_GREEN = 0x1904, 168 | 169 | /// 170 | /// GL_BLUE 171 | /// 172 | GL_BLUE = 0x1905, 173 | 174 | /// 175 | /// GL_ALPHA 176 | /// 177 | GL_ALPHA = 0x1906, 178 | 179 | /// 180 | /// GL_RGB 181 | /// 182 | GL_RGB = 0x1907, 183 | 184 | /// 185 | /// GL_RGBA 186 | /// 187 | GL_RGBA = 0x1908, 188 | 189 | /// 190 | /// GL_LUMINANCE 191 | /// 192 | GL_LUMINANCE = 0x1909, 193 | 194 | /// 195 | /// GL_LUMINANCE_ALPHA 196 | /// 197 | GL_LUMINANCE_ALPHA = 0x190A, 198 | 199 | /// 200 | /// Custom value for situation where parser cannot identify format 201 | /// 202 | NotKnown = 0xFFFF, 203 | } 204 | 205 | /// 206 | /// GlInternalFormat 207 | /// 208 | public enum GlInternalFormat : uint 209 | { 210 | /// 211 | /// RGB8 212 | /// 213 | GL_RGB8_OES = 0x8051, 214 | 215 | /// 216 | /// RGBA8 217 | /// 218 | GL_RGBA8_OES = 0x8058, 219 | 220 | /// 221 | /// RGB S3TC DXT1 222 | /// 223 | GL_COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0, 224 | 225 | /// 226 | /// RGBA S3TC DXT1 227 | /// 228 | GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1, 229 | 230 | /// 231 | /// RGBA S3TC DXT3 232 | /// 233 | GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2, 234 | 235 | /// 236 | /// RGBA S3TC DXT5 237 | /// 238 | GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3, 239 | 240 | /// 241 | /// RGB PVRTC 4BPP V1 242 | /// 243 | GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00, 244 | 245 | /// 246 | /// RGB PVRTC 2BPP V1 247 | /// 248 | GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01, 249 | 250 | /// 251 | /// RGBA PVRTC 4BPP V1 252 | /// 253 | GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02, 254 | 255 | /// 256 | /// RGBA PVRTC 2BPP V1 257 | /// 258 | GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03, 259 | 260 | /// 261 | /// RGB ATC 262 | /// 263 | GL_ATC_RGB_AMD = 0x8C92, 264 | 265 | /// 266 | /// RGBA ATC Explicit Alpha 267 | /// 268 | GL_ATC_RGBA_EXPLICIT_ALPHA_AMD = 0x8C93, 269 | 270 | /// 271 | /// RGBA ATC Interpolated Alpha 272 | /// 273 | GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD = 0x87EE, 274 | 275 | /// 276 | /// ETC1 RGB8 277 | /// 278 | GL_ETC1_RGB8_OES = 0x8D64, 279 | 280 | /// 281 | /// R11 EAC 282 | /// 283 | GL_COMPRESSED_R11_EAC = 0x9270, 284 | 285 | /// 286 | /// SIGNED R11 EAC 287 | /// 288 | GL_COMPRESSED_SIGNED_R11_EAC = 0x9271, 289 | 290 | /// 291 | /// RG11 EAC 292 | /// 293 | GL_COMPRESSED_RG11_EAC = 0x9272, 294 | 295 | /// 296 | /// SIGNED RG11 EAC 297 | /// 298 | GL_COMPRESSED_SIGNED_RG11_EAC = 0x9273, 299 | 300 | /// 301 | /// RGB8 ETC2 302 | /// 303 | GL_COMPRESSED_RGB8_ETC2 = 0x9274, 304 | 305 | /// 306 | /// SRGB8 ETC2 307 | /// 308 | GL_COMPRESSED_SRGB8_ETC2 = 0x9275, 309 | 310 | /// 311 | /// RGB8 PUNCHTHROUGH ALPHA1 ETC2 312 | /// 313 | GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, 314 | 315 | /// 316 | /// SRGB8 PUNCHTHROUGH ALPHA1 ETC2 317 | /// 318 | GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, 319 | 320 | /// 321 | /// RGBA8 ETC2 EAC 322 | /// 323 | GL_COMPRESSED_RGBA8_ETC2_EAC = 0x9278, 324 | 325 | /// 326 | /// SRGB8 ALPHA8 ETC2 EAC 327 | /// 328 | GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, 329 | 330 | /// 331 | /// RGBA ASTC 4x4 KHR 332 | /// 333 | GL_COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0, 334 | 335 | /// 336 | /// RGBA ASTC 5x4 KHR 337 | /// 338 | GL_COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1, 339 | 340 | /// 341 | /// RGBA ASTC 5x5 KHR 342 | /// 343 | GL_COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2, 344 | 345 | /// 346 | /// RGBA ASTC 6x5 KHR 347 | /// 348 | GL_COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3, 349 | 350 | /// 351 | /// RGBA ASTC 6x6 KHR 352 | /// 353 | GL_COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4, 354 | 355 | /// 356 | /// RGBA ASTC 8x5 KHR 357 | /// 358 | GL_COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5, 359 | 360 | /// 361 | /// RGBA ASTC 8x6 KHR 362 | /// 363 | GL_COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6, 364 | 365 | /// 366 | /// RGBA ASTC 8x8 KHR 367 | /// 368 | GL_COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7, 369 | 370 | /// 371 | /// RGBA ASTC 10x5 KHR 372 | /// 373 | GL_COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8, 374 | 375 | /// 376 | /// RGBA ASTC 10x6 KHR 377 | /// 378 | GL_COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9, 379 | 380 | /// 381 | /// RGBA ASTC 10x8 KHR 382 | /// 383 | GL_COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA, 384 | 385 | /// 386 | /// RGBA ASTC 10x10 KHR 387 | /// 388 | GL_COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB, 389 | 390 | /// 391 | /// RGBA ASTC 12x10 KHR 392 | /// 393 | GL_COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC, 394 | 395 | /// 396 | /// RGBA ASTC 12x12 KHR 397 | /// 398 | GL_COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD, 399 | 400 | 401 | /// 402 | /// SRGB8 ALPHA8 ASTC 4x4 KHR 403 | /// 404 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0, 405 | 406 | /// 407 | /// SRGB8 ALPHA8 ASTC 5x4 KHR 408 | /// 409 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1, 410 | 411 | /// 412 | /// SRGB8 ALPHA8 ASTC 5x5 KHR 413 | /// 414 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2, 415 | 416 | /// 417 | /// SRGB8 ALPHA8 ASTC 6x5 KHR 418 | /// 419 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3, 420 | 421 | /// 422 | /// SRGB8 ALPHA8 ASTC 6x6 KHR 423 | /// 424 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4, 425 | 426 | /// 427 | /// SRGB8 ALPHA8 ASTC 8x5 KHR 428 | /// 429 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5, 430 | 431 | /// 432 | /// SRGB8 ALPHA8 ASTC 8x6 KHR 433 | /// 434 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6, 435 | 436 | /// 437 | /// SRGB8 ALPHA8 ASTC 8x8 KHR 438 | /// 439 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7, 440 | 441 | /// 442 | /// SRGB8 ALPHA8 ASTC 10x5 KHR 443 | /// 444 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8, 445 | 446 | /// 447 | /// SRGB8 ALPHA8 ASTC 10x6 KHR 448 | /// 449 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9, 450 | 451 | /// 452 | /// SRGB8 ALPHA8 ASTC 10x8 KHR 453 | /// 454 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA, 455 | 456 | /// 457 | /// SRGB8 ALPHA8 ASTC 10x10 KHR 458 | /// 459 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB, 460 | 461 | /// 462 | /// SRGB8 ALPHA8 ASTC 12x10 KHR 463 | /// 464 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC, 465 | 466 | /// 467 | /// SRGB8 ALPHA8 ASTC 12x12 KHR 468 | /// 469 | GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD, 470 | 471 | /// 472 | /// Custom value for situation where parser cannot identify format 473 | /// 474 | NotKnown = 0xFFFF, 475 | } 476 | 477 | /// 478 | /// Texture type (basic) 479 | /// 480 | public enum TextureTypeBasic 481 | { 482 | /// 483 | /// Basic 2d no mip maps 484 | /// 485 | Basic2DNoMipmaps = 1, 486 | 487 | /// 488 | /// Basic 2d with mip maps 489 | /// 490 | Basic2DWithMipmaps = 2, 491 | 492 | /// 493 | /// Basic 3d no mip maps 494 | /// 495 | Basic3DNoMipmaps = 3, 496 | 497 | /// 498 | /// Basic 3d with mip maps 499 | /// 500 | Basic3DWithMipmaps = 4, 501 | 502 | /// 503 | /// Basic 1d no mip maps 504 | /// 505 | Basic1DNoMipmaps = 5, 506 | 507 | /// 508 | /// Basic 1d with mip maps 509 | /// 510 | Basic1DWithMipmaps = 6, 511 | } 512 | } -------------------------------------------------------------------------------- /lib/KtxHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | 7 | namespace KtxSharp 8 | { 9 | /// 10 | /// KtxHeader class 11 | /// 12 | /// Based on specifications mentioned in https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ 13 | public sealed class KtxHeader 14 | { 15 | /// 16 | /// Is input little endian 17 | /// 18 | public readonly bool isInputLittleEndian; 19 | 20 | /// 21 | /// Endianness value 22 | /// 23 | public readonly uint endiannessValue; 24 | 25 | /// 26 | /// Is data uncompressed 27 | /// 28 | public readonly bool isUncompressed; 29 | 30 | /// 31 | /// GlType as uint 32 | /// 33 | public readonly uint glTypeAsUint; 34 | 35 | /// 36 | /// GlDataType 37 | /// 38 | public readonly GlDataType glDataType; 39 | 40 | /// 41 | /// GlType size as uint 42 | /// 43 | public readonly uint glTypeSizeAsUint; 44 | 45 | /// 46 | /// GlFormat as uint 47 | /// 48 | public readonly uint glFormatAsUint; 49 | 50 | /// 51 | /// GlFormat as GlPixelFormat 52 | /// 53 | public readonly GlPixelFormat glFormat; 54 | 55 | /// 56 | /// GlInternalFormat as uint 57 | /// 58 | public readonly uint glInternalFormatAsUint; 59 | 60 | /// 61 | /// GlInternalFormat 62 | /// 63 | public readonly GlInternalFormat glInternalFormat; 64 | 65 | /// 66 | /// GlBaseInternal as uint 67 | /// 68 | public readonly uint glBaseInternalFormatAsUint; 69 | 70 | /// 71 | /// GlPixelFormat 72 | /// 73 | public readonly GlPixelFormat glPixelFormat; 74 | 75 | /// 76 | /// Width in pixels 77 | /// 78 | public readonly uint pixelWidth; 79 | 80 | /// 81 | /// Height in pixels 82 | /// 83 | public readonly uint pixelHeight; 84 | 85 | /// 86 | /// Depth in pixels 87 | /// 88 | public readonly uint pixelDepth; 89 | 90 | /// 91 | /// Number of array elements 92 | /// 93 | public readonly uint numberOfArrayElements; 94 | 95 | /// 96 | /// Number of faces 97 | /// 98 | public readonly uint numberOfFaces; 99 | 100 | /// 101 | /// Number of mipmap levels 102 | /// 103 | public readonly uint numberOfMipmapLevels; 104 | 105 | /// 106 | /// How many bytes of key value data there is 107 | /// 108 | public readonly uint bytesOfKeyValueData; 109 | 110 | /// 111 | /// Metadata dictionary (key is string) 112 | /// 113 | public readonly Dictionary metadataDictionary; 114 | 115 | /// 116 | /// KtxHeader constructor for 2d texture 117 | /// 118 | /// GlDataType 119 | /// GlPixelFormat 120 | /// GlInternalFormat 121 | /// Width 122 | /// Height 123 | /// Mipmap count 124 | /// Metadata 125 | public KtxHeader(GlDataType glDataType, GlPixelFormat glPixelFormat, GlInternalFormat glInternalFormat, uint width, uint height, uint mipmapCount, Dictionary metadata) 126 | { 127 | this.isInputLittleEndian = true; 128 | 129 | this.glDataType = glDataType; 130 | this.glTypeAsUint = (uint)this.glDataType; 131 | 132 | this.glTypeSizeAsUint = Common.GlTypeToSize[glDataType]; 133 | 134 | this.glFormat = (glDataType != GlDataType.Compressed) ? glPixelFormat : 0; 135 | this.glFormatAsUint = (uint)this.glFormat; 136 | 137 | this.glInternalFormat = glInternalFormat; 138 | this.glInternalFormatAsUint = (uint)this.glInternalFormat; 139 | 140 | this.glPixelFormat = glPixelFormat; 141 | this.glBaseInternalFormatAsUint = (uint)this.glPixelFormat; 142 | 143 | this.pixelWidth = width; 144 | this.pixelHeight = height; 145 | 146 | // For 2d textures these values must be 0 147 | this.pixelDepth = 0; 148 | this.numberOfArrayElements = 0; 149 | 150 | // For non cubemaps this should be 1 151 | this.numberOfFaces = 1; 152 | 153 | this.numberOfMipmapLevels = mipmapCount; 154 | 155 | this.metadataDictionary = metadata; 156 | } 157 | 158 | /// 159 | /// KtxHeader constructor 160 | /// 161 | /// Stream for reading (must be seekable stream) 162 | /// Seek from current position (use this if your stream is not a single .ktx file) 163 | public KtxHeader(Stream stream, bool seekFromCurrent = false) 164 | { 165 | // Skip first 12 bytes since they only contain identifier (by default we assume that we are dealing with a single .ktx file) 166 | stream.Seek(12, seekFromCurrent ? SeekOrigin.Current : SeekOrigin.Begin); 167 | 168 | // Read endianness as bytes 169 | byte[] endiannessBytes = new byte[4]; 170 | int bytesRead = stream.Read(buffer: endiannessBytes, offset: 0, count: endiannessBytes.Length); 171 | 172 | if (bytesRead != 4) 173 | { 174 | throw new InvalidOperationException("Cannot read enough bytes from stream!"); 175 | } 176 | 177 | if (!Common.littleEndianAsBytes.SequenceEqual(endiannessBytes) && !Common.bigEndianAsBytes.SequenceEqual(endiannessBytes)) 178 | { 179 | throw new InvalidOperationException("Endianness info in header is not valid! You can use ValidateIdentifier method to check that you have supported KTX file."); 180 | } 181 | 182 | this.isInputLittleEndian = Common.littleEndianAsBytes.SequenceEqual(endiannessBytes); 183 | 184 | // Turn endianness as bytes to uint 185 | this.endiannessValue = BitConverter.ToUInt32(endiannessBytes, 0); 186 | 187 | // See if following uint reads need endian swap 188 | bool shouldSwapEndianness = (this.endiannessValue != Common.expectedEndianValue); 189 | 190 | // Use the stream in a binary reader. 191 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true)) 192 | { 193 | // Swap endianness for every KTX variable if needed 194 | 195 | this.glTypeAsUint = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 196 | if (GlDataType.IsDefined(typeof(GlDataType), this.glTypeAsUint)) 197 | { 198 | this.glDataType = (GlDataType)this.glTypeAsUint; 199 | } 200 | else 201 | { 202 | this.glDataType = GlDataType.NotKnown; 203 | } 204 | 205 | this.glTypeSizeAsUint = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 206 | 207 | this.glFormatAsUint = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 208 | if (GlPixelFormat.IsDefined(typeof(GlPixelFormat), this.glFormatAsUint)) 209 | { 210 | this.glFormat = (GlPixelFormat)this.glFormatAsUint; 211 | } 212 | else 213 | { 214 | this.glFormat = GlPixelFormat.NotKnown; 215 | } 216 | 217 | this.glInternalFormatAsUint = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 218 | if (GlInternalFormat.IsDefined(typeof(GlInternalFormat), this.glInternalFormatAsUint)) 219 | { 220 | this.glInternalFormat = (GlInternalFormat)this.glInternalFormatAsUint; 221 | } 222 | else 223 | { 224 | this.glInternalFormat = GlInternalFormat.NotKnown; 225 | } 226 | 227 | this.glBaseInternalFormatAsUint = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 228 | if (GlPixelFormat.IsDefined(typeof(GlPixelFormat), this.glBaseInternalFormatAsUint)) 229 | { 230 | this.glPixelFormat = (GlPixelFormat)this.glBaseInternalFormatAsUint; 231 | } 232 | else 233 | { 234 | this.glPixelFormat = GlPixelFormat.NotKnown; 235 | } 236 | 237 | this.pixelWidth = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 238 | 239 | this.pixelHeight = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 240 | 241 | this.pixelDepth = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 242 | 243 | this.numberOfArrayElements = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 244 | 245 | this.numberOfFaces = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 246 | 247 | this.numberOfMipmapLevels = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 248 | 249 | this.bytesOfKeyValueData = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(reader.ReadUInt32()) : reader.ReadUInt32(); 250 | 251 | // Check that bytesOfKeyValueData is mod 4 252 | if (this.bytesOfKeyValueData % 4 != 0) 253 | { 254 | throw new InvalidOperationException(ErrorGen.Modulo4Error(nameof(this.bytesOfKeyValueData), this.bytesOfKeyValueData)); 255 | } 256 | 257 | this.metadataDictionary = ParseMetadata(reader.ReadBytes((int)this.bytesOfKeyValueData), shouldSwapEndianness); 258 | } 259 | } 260 | 261 | /// 262 | /// Write content to stream. Leaves stream open 263 | /// 264 | /// Output stream 265 | /// Write little endian output (default enabled) 266 | public void WriteTo(Stream output, bool writeLittleEndian = true) 267 | { 268 | using (BinaryWriter writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true)) 269 | { 270 | Action writeUint = writer.Write; 271 | if (!writeLittleEndian) 272 | { 273 | writeUint = (uint u) => WriteUintAsBigEndian(writer, u); 274 | } 275 | GenericWrite(this, writeUint, writer.Write, writer.Write); 276 | } 277 | } 278 | 279 | private static void GenericWrite(KtxHeader header, Action writeUint, Action writeByte, Action writeByteArray) 280 | { 281 | writeUint(Common.expectedEndianValue); 282 | writeUint(header.glTypeAsUint); 283 | writeUint(header.glTypeSizeAsUint); 284 | writeUint(header.glFormatAsUint); 285 | writeUint(header.glInternalFormatAsUint); 286 | writeUint(header.glBaseInternalFormatAsUint); 287 | writeUint(header.pixelWidth); 288 | writeUint(header.pixelHeight); 289 | writeUint(header.pixelDepth); 290 | writeUint(header.numberOfArrayElements); 291 | writeUint(header.numberOfFaces); 292 | writeUint(header.numberOfMipmapLevels); 293 | writeUint(GetTotalSizeOfMetadata(header.metadataDictionary)); 294 | foreach (var pair in header.metadataDictionary) 295 | { 296 | uint keyLenght = Common.GetLengthOfUtf8StringAsBytes(pair.Key) + 1; 297 | uint valueLength = pair.Value.GetSizeInBytes(); 298 | uint totalLength = keyLenght + valueLength; 299 | writeUint(totalLength); 300 | writeByteArray(Common.GetUtf8StringAsBytes(pair.Key)); 301 | writeByte(Common.nulByte); 302 | writeByteArray(pair.Value.GetAsBytes()); 303 | if (pair.Value.isString) 304 | { 305 | writeByte(Common.nulByte); 306 | } 307 | 308 | // Write padding if needed 309 | while (totalLength % 4 != 0) 310 | { 311 | writeByte(Common.nulByte); 312 | totalLength++; 313 | } 314 | } 315 | } 316 | 317 | private static void WriteUintAsBigEndian(BinaryWriter writer, uint value) 318 | { 319 | writer.Write(KtxBitFiddling.SwapEndian(value)); 320 | } 321 | 322 | #region Parse metadata 323 | 324 | private static Dictionary ParseMetadata(byte[] inputArray, bool shouldSwapEndianness) 325 | { 326 | Dictionary returnDictionary = new Dictionary(); 327 | int position = 0; 328 | while (position < inputArray.Length) 329 | { 330 | uint combinedKeyAndValueSizeInBytes = shouldSwapEndianness ? KtxBitFiddling.SwapEndian(BitConverter.ToUInt32(inputArray, position)) : BitConverter.ToUInt32(inputArray, position); 331 | 332 | // Pair must be larger than 0 bytes 333 | if (combinedKeyAndValueSizeInBytes == 0) 334 | { 335 | throw new InvalidOperationException("Metadata: combinedKeyAndValueSize cannot be 0!"); 336 | } 337 | 338 | position += Common.sizeOfUint; 339 | 340 | // Error out in case size is larger than bytes left 341 | if (combinedKeyAndValueSizeInBytes + 4 > (uint) inputArray.Length) 342 | { 343 | throw new InvalidOperationException("Metadata: combinedKeyAndValueSize cannot be larger than whole metadata!"); 344 | } 345 | 346 | // Find NUL since key should always have it 347 | int indexOfFirstNul = Array.IndexOf(inputArray, Common.nulByte, position); 348 | 349 | if (indexOfFirstNul < 0) 350 | { 351 | throw new InvalidOperationException("Metadata: No Nul found when looking for key"); 352 | } 353 | 354 | int keyLength = indexOfFirstNul - position; 355 | 356 | if (keyLength > combinedKeyAndValueSizeInBytes) 357 | { 358 | throw new InvalidOperationException("Metadata: Key length is longer than combinedKeyAndValueSizeInBytes!"); 359 | } 360 | 361 | string key = System.Text.Encoding.UTF8.GetString(bytes: inputArray, index: position, count: keyLength); 362 | 363 | position += (keyLength + 1 /* Because we have to skip nul byte*/); 364 | 365 | int valueLength = (int)combinedKeyAndValueSizeInBytes - keyLength; 366 | byte[] bytesOfValue = new byte[valueLength]; 367 | Buffer.BlockCopy(src: inputArray, srcOffset: position, dst: bytesOfValue, dstOffset: 0, count: valueLength); 368 | 369 | returnDictionary[key] = new MetadataValue(bytesOfValue); 370 | 371 | position += valueLength; 372 | 373 | // Skip value paddings if there are any 374 | while (position % 4 != 0) 375 | { 376 | position++; 377 | } 378 | } 379 | 380 | return returnDictionary; 381 | } 382 | 383 | #endregion // Parse metadata 384 | 385 | #region Write metadata 386 | 387 | private static uint GetTotalSizeOfMetadata(Dictionary pairs) 388 | { 389 | uint totalCount = 0; 390 | 391 | foreach (var pair in pairs) 392 | { 393 | totalCount += 4; // Size is always 4 bytes uint 394 | totalCount += Common.GetLengthOfUtf8StringAsBytes(pair.Key) + 1; 395 | totalCount += pair.Value.GetSizeInBytes(); 396 | } 397 | 398 | // Add value padding if needed 399 | while (totalCount % 4 != 0) 400 | { 401 | totalCount++; 402 | } 403 | 404 | return totalCount; 405 | } 406 | 407 | #endregion // Write metadata 408 | 409 | 410 | #region ToString 411 | 412 | /// 413 | /// Print some info into string 414 | /// 415 | /// String 416 | public override string ToString() 417 | { 418 | StringBuilder sb = new StringBuilder(); 419 | 420 | sb.AppendLine($"isInputLittleEndian: {isInputLittleEndian}"); 421 | sb.AppendLine($"endiannessValue: {endiannessValue}"); 422 | sb.AppendLine($"isUncompressed: {isUncompressed}"); 423 | sb.AppendLine($"glTypeAsUint: {glTypeAsUint}"); 424 | sb.AppendLine($"glTypeSizeAsUint: {glTypeSizeAsUint}"); 425 | sb.AppendLine($"glFormatAsUint: {glFormatAsUint}"); 426 | sb.AppendLine($"glInternalFormatAsUint: {glInternalFormatAsUint}"); 427 | sb.AppendLine($"glBaseInternalFormatAsUint: {glBaseInternalFormatAsUint}"); 428 | sb.AppendLine($"pixelWidth: {pixelWidth}"); 429 | sb.AppendLine($"pixelHeight: {pixelHeight}"); 430 | sb.AppendLine($"pixelDepth: {pixelDepth}"); 431 | sb.AppendLine($"numberOfArrayElements: {numberOfArrayElements}"); 432 | sb.AppendLine($"numberOfFaces: {numberOfFaces}"); 433 | sb.AppendLine($"numberOfMipmapLevels: {numberOfMipmapLevels}"); 434 | sb.AppendLine($"bytesOfKeyValueData: {bytesOfKeyValueData}"); 435 | 436 | return sb.ToString(); 437 | } 438 | 439 | #endregion // ToString 440 | } 441 | } --------------------------------------------------------------------------------