Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7b41d97
Add basis for ASTC decoding
Erik-White Nov 12, 2025
ae2c4c4
Update to use AstcSharp
Erik-White Feb 3, 2026
52ff132
Fix comment tags
Erik-White Feb 3, 2026
48c9802
Merge branch 'main' into add-astc-decoding
Erik-White Feb 3, 2026
2563be5
Use strongly named AstcSharp
Erik-White Feb 4, 2026
5df747f
Remove temporary tests
Erik-White Feb 4, 2026
6db3033
Reduce code duplication for ASTC block sizes
Erik-White Feb 4, 2026
d8d9d44
Remove debug code
Erik-White Feb 4, 2026
8e86c7c
Add ASTC block size reference tests
Erik-White Feb 11, 2026
710f018
Update naming of ktx2 test files
Erik-White Feb 11, 2026
3d9fd53
Add ASTC 8x8 test for KTX1
Erik-White Feb 11, 2026
37e067f
Add unorm and srgb test cases
Erik-White Feb 12, 2026
0b1639a
UNORM -> Unorm
Erik-White Feb 12, 2026
f9c119c
Update block size test data
Erik-White Feb 12, 2026
a149d52
Use tighter comparison for KTX1 tests
Erik-White Feb 12, 2026
310f012
Add placeholder tests for supercompressed ASTC
Erik-White Feb 12, 2026
aceb57a
Add cubemap decoding test
Erik-White Feb 13, 2026
5e8a097
Add test for mipmap levels
Erik-White Feb 13, 2026
52058fc
Improve and reorganize flat tests
Erik-White Feb 13, 2026
e5c2e5e
Clean up test images list
Erik-White Feb 13, 2026
f26130b
Add large image test
Erik-White Feb 13, 2026
297f802
Tidy up test image naming
Erik-White Feb 13, 2026
1946b6c
Tidy up unused test files
Erik-White Feb 13, 2026
fc7387b
Restore existing test images
Erik-White Feb 13, 2026
1c6be57
Update ASTC types to follow style recommendations
Erik-White Feb 13, 2026
d8535e5
Remove unsupported test images
Erik-White Feb 13, 2026
bba744e
Tidy up AstcDecoder and add more guards
Erik-White Feb 13, 2026
7bc09d0
Correctly guard image dimensions
Erik-White Feb 13, 2026
4ca0d00
Update readme
Erik-White Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SixLabors.ImageSharp.Textures

[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp.Textures/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp.Textures/actions)
[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp.Textures/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)

</div>
Expand All @@ -33,6 +33,7 @@ with the following compressions:
- BC5
- BC6H
- BC7
- ASTC

Encoding textures is **not** yet supported. PR are of course very welcome.

Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
-->

<!-- Import the shared src .props file -->
<Import Project="$(MSBuildThisFileDirectory)..\shared-infrastructure\msbuild\props\SixLabors.Src.props" />
<Import Project="$(MSBuildThisFileDirectory)../shared-infrastructure/msbuild/props/SixLabors.Src.props" />

<!-- Import the solution .props file. -->
<Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" />
Expand Down
62 changes: 59 additions & 3 deletions src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal class KtxProcessor
/// <returns>The decoded mipmaps.</returns>
public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count)
{
if (this.KtxHeader.GlTypeSize == 1)
if (this.KtxHeader.GlTypeSize is 0 or 1)
{
switch (this.KtxHeader.GlFormat)
{
Expand Down Expand Up @@ -86,13 +86,41 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count)
return this.AllocateMipMaps<Etc1>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgb8Etc2:
return this.AllocateMipMaps<Etc2>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc4x4Khr:
return this.AllocateMipMaps<RgbaAstc4X4>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc5x4Khr:
return this.AllocateMipMaps<RgbaAstc5X4>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc5x5Khr:
return this.AllocateMipMaps<RgbaAstc5X5>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc6x5Khr:
return this.AllocateMipMaps<RgbaAstc6X5>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc6x6Khr:
return this.AllocateMipMaps<RgbaAstc6X6>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc8x5Khr:
return this.AllocateMipMaps<RgbaAstc8X5>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc8x6Khr:
return this.AllocateMipMaps<RgbaAstc8X6>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc8x8Khr:
return this.AllocateMipMaps<RgbaAstc8X8>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr:
return this.AllocateMipMaps<RgbaAstc10X5>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr:
return this.AllocateMipMaps<RgbaAstc10X6>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc10x8Khr:
return this.AllocateMipMaps<RgbaAstc10X8>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc10x10Khr:
return this.AllocateMipMaps<RgbaAstc10X10>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc12x10Khr:
return this.AllocateMipMaps<RgbaAstc12X10>(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgbaAstc12x12Khr:
return this.AllocateMipMaps<RgbaAstc12X12>(stream, width, height, count);
}

break;
}
}

if (this.KtxHeader.GlTypeSize == 2 || this.KtxHeader.GlTypeSize == 4)
if (this.KtxHeader.GlTypeSize is 2 or 4)
{
// TODO: endianess is not respected here. Use stream reader which respects endianess.
switch (this.KtxHeader.GlInternalFormat)
Expand Down Expand Up @@ -166,12 +194,40 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height)
return this.AllocateCubeMap<Etc1>(stream, width, height);
case GlInternalPixelFormat.CompressedRgb8Etc2:
return this.AllocateCubeMap<Etc2>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc4x4Khr:
return this.AllocateCubeMap<RgbaAstc4X4>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc5x4Khr:
return this.AllocateCubeMap<RgbaAstc5X4>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc5x5Khr:
return this.AllocateCubeMap<RgbaAstc5X5>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc6x5Khr:
return this.AllocateCubeMap<RgbaAstc6X5>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc6x6Khr:
return this.AllocateCubeMap<RgbaAstc6X6>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc8x5Khr:
return this.AllocateCubeMap<RgbaAstc8X5>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc8x6Khr:
return this.AllocateCubeMap<RgbaAstc8X6>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc8x8Khr:
return this.AllocateCubeMap<RgbaAstc8X8>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr:
return this.AllocateCubeMap<RgbaAstc10X5>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr:
return this.AllocateCubeMap<RgbaAstc10X6>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc10x8Khr:
return this.AllocateCubeMap<RgbaAstc10X8>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc10x10Khr:
return this.AllocateCubeMap<RgbaAstc10X10>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc12x10Khr:
return this.AllocateCubeMap<RgbaAstc12X10>(stream, width, height);
case GlInternalPixelFormat.CompressedRgbaAstc12x12Khr:
return this.AllocateCubeMap<RgbaAstc12X12>(stream, width, height);
}

break;
}

if (this.KtxHeader.GlTypeSize == 2 || this.KtxHeader.GlTypeSize == 4)
if (this.KtxHeader.GlTypeSize is 2 or 4)
{
// TODO: endianess is not respected here. Use stream reader which respects endianess.
switch (this.KtxHeader.GlInternalFormat)
Expand Down
35 changes: 30 additions & 5 deletions src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public Texture DecodeTexture(Stream stream)
int width = (int)this.ktxHeader.PixelWidth;
int height = (int)this.ktxHeader.PixelHeight;

// Level indices start immediately after the header
var levelIndices = new LevelIndex[this.ktxHeader.LevelCount];
for (int i = 0; i < levelIndices.Length; i++)
{
Expand All @@ -84,15 +85,39 @@ public Texture DecodeTexture(Stream stream)

var ktxProcessor = new Ktx2Processor(this.ktxHeader);

Texture texture;
if (this.ktxHeader.FaceCount == 6)
{
CubemapTexture cubeMapTexture = ktxProcessor.DecodeCubeMap(stream, width, height, levelIndices);
return cubeMapTexture;
texture = ktxProcessor.DecodeCubeMap(stream, width, height, levelIndices);
}
else
{
var flatTexture = new FlatTexture();
MipMap[] mipMaps = ktxProcessor.DecodeMipMaps(stream, width, height, levelIndices);
flatTexture.MipMaps.AddRange(mipMaps);
texture = flatTexture;
}

var texture = new FlatTexture();
MipMap[] mipMaps = ktxProcessor.DecodeMipMaps(stream, width, height, levelIndices);
texture.MipMaps.AddRange(mipMaps);
// Seek to the end of the file to ensure the entire stream is consumed.
// KTX2 files use byte offsets for mipmap data, so the stream position may not
// be at the end after reading. We need to find the furthest point read.
if (levelIndices.Length > 0)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to satisfy the tests that check the stream has been read to the end. The reader can jump around a bit in the file so it may not always end up positioned at the end otherwise.

{
long maxEndPosition = 0;
for (int i = 0; i < levelIndices.Length; i++)
{
long endPosition = (long)(levelIndices[i].ByteOffset + levelIndices[i].UncompressedByteLength);
if (endPosition > maxEndPosition)
{
maxEndPosition = endPosition;
}
}

if (stream.Position < maxEndPosition && stream.CanSeek)
{
stream.Position = maxEndPosition;
}
}

return texture;
}
Expand Down
84 changes: 84 additions & 0 deletions src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,48 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, LevelIndex[]
return AllocateMipMaps<Bc7>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK:
return AllocateMipMaps<Etc1>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc4X4>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc5X4>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc5X5>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc6X5>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc6X6>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc8X5>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc8X6>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc8X8>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc10X5>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc10X6>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc10X8>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc10X10>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc12X10>(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
return AllocateMipMaps<RgbaAstc12X12>(memoryStream, width, height, levelIndices);
}

throw new NotSupportedException("The pixel format is not supported");
Expand Down Expand Up @@ -286,6 +328,48 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height, LevelI
return AllocateCubeMap<Bc7>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK:
return AllocateCubeMap<Etc1>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc4X4>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc5X4>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc5X5>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc6X5>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc6X6>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc8X5>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc8X6>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc8X8>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc10X5>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc10X6>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc10X8>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc10X10>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc12X10>(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
case VkFormat.VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
return AllocateCubeMap<RgbaAstc12X12>(stream, width, height, levelIndices);
}

throw new NotSupportedException("The pixel format is not supported");
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp.Textures/ImageSharp.Textures.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AstcSharp" Version="0.9.6" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>

Expand Down
Loading
Loading