├── 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 | 
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 | }
--------------------------------------------------------------------------------