diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index c450c8869..233943981 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -61,6 +61,12 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [x] Document the default, valid range, special values, invalid-value behavior, and format-dependent restrictions of every retained option using observable API behavior only.
- [x] Pass `HeifEncoder` directly to `HeifEncoderCore`, matching the JPEG, PNG, and WebP encoder-core contracts and avoiding interface dispatch.
- [x] Verify construction-time range validation and legacy-JPEG codec-boundary restrictions with focused tests before the Phase 1 API-review gate is marked complete.
+- [x] **Completed:** lock public HEIF image metadata to the supported component bit-depth contract.
+ - [x] Replace the unrestricted integer bit depth with `HeifBitDepth` and preserve the 8-bit default.
+ - [x] Resolve format-connecting component precision to the nearest supported 8/10/12-bit output without widening the public value domain.
+ - [x] Reject HEVC configuration records outside the exposed 8/10/12-bit profile matrix at the external parse boundary.
+ - [x] Verify defaults, cloning, format-connecting conversion, pixel-type projection, and current HEIC/HIF/AVIF Identify results.
+ - Release build: 0 errors. Focused metadata and Identify tests: 42 passed, 0 failed.
Gain maps, progressive/layered images, sample transforms, and experimental extension brands require explicit conformance and API decisions. They do not create permission to omit any valid color, compression, or bit-depth path from the PR. The container reader must skip unsupported optional extensions safely and reject an unsupported essential property with a useful error.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
index c56801d1e..829649a83 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
@@ -139,7 +139,7 @@ internal sealed class Av1CodecConfiguration
///
/// Gets the coded image sample precision in bits.
///
- public int BitDepth => this.TwelveBit ? 12 : this.HighBitDepth ? 10 : 8;
+ public HeifBitDepth BitDepth => this.TwelveBit ? HeifBitDepth.Bit12 : this.HighBitDepth ? HeifBitDepth.Bit10 : HeifBitDepth.Bit8;
///
/// Gets a value indicating whether the coded image contains only a luma plane.
diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
index a6fe3a737..186db788c 100644
--- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
@@ -47,7 +47,7 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder
{
foreach (byte channelBitDepth in item.ChannelBitDepths)
{
- if (channelBitDepth != codecConfiguration.BitDepth)
+ if (channelBitDepth != (byte)codecConfiguration.BitDepth)
{
throw new InvalidImageContentException($"AV1 image item {item.Id} has mismatched pixel-information and codec-configuration bit depths.");
}
diff --git a/src/ImageSharp/Formats/Heif/HeifBitDepth.cs b/src/ImageSharp/Formats/Heif/HeifBitDepth.cs
index be11bd6ae..470e3ba87 100644
--- a/src/ImageSharp/Formats/Heif/HeifBitDepth.cs
+++ b/src/ImageSharp/Formats/Heif/HeifBitDepth.cs
@@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Formats.Heif;
///
-/// Enumerates the component bit depths supported for HEIF image encoding.
+/// Enumerates the supported HEIF image-component bit depths.
///
public enum HeifBitDepth : byte
{
diff --git a/src/ImageSharp/Formats/Heif/HeifMetadata.cs b/src/ImageSharp/Formats/Heif/HeifMetadata.cs
index 13076f557..0695ce1d4 100644
--- a/src/ImageSharp/Formats/Heif/HeifMetadata.cs
+++ b/src/ImageSharp/Formats/Heif/HeifMetadata.cs
@@ -39,12 +39,12 @@ public class HeifMetadata : IFormatMetadata
///
/// Gets or sets the compression method used for the primary frame.
///
- public HeifCompressionMethod CompressionMethod { get; set; }
+ public HeifCompressionMethod CompressionMethod { get; set; } = HeifCompressionMethod.LegacyJpeg;
///
- /// Gets or sets the encoded precision of each color component in bits.
+ /// Gets or sets the encoded precision of each color component. The default is .
///
- public int BitDepth { get; set; } = 8;
+ public HeifBitDepth BitDepth { get; set; } = HeifBitDepth.Bit8;
///
/// Gets or sets a value indicating whether the primary image contains a single luminance component.
@@ -93,34 +93,45 @@ public class HeifMetadata : IFormatMetadata
public HeifNominalDiffuseWhite? NominalDiffuseWhite { get; set; }
///
- public static HeifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => new()
+ public static HeifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
- CompressionMethod = HeifCompressionMethod.LegacyJpeg,
- BitDepth = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8,
- IsMonochrome = metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.Luminance)
- && !metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.ChrominanceBlue),
- HasAlpha = metadata.PixelTypeInfo.AlphaRepresentation != PixelAlphaRepresentation.None
- };
+ int componentPrecision = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8;
+ HeifBitDepth bitDepth = componentPrecision switch
+ {
+ <= 8 => HeifBitDepth.Bit8,
+ <= 10 => HeifBitDepth.Bit10,
+ _ => HeifBitDepth.Bit12
+ };
+
+ return new HeifMetadata
+ {
+ BitDepth = bitDepth,
+ IsMonochrome = metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.Luminance)
+ && !metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.ChrominanceBlue),
+ HasAlpha = metadata.PixelTypeInfo.AlphaRepresentation != PixelAlphaRepresentation.None
+ };
+ }
///
public PixelTypeInfo GetPixelTypeInfo()
{
+ int bitDepth = (int)this.BitDepth;
int colorComponentCount = this.IsMonochrome ? 1 : 3;
int componentCount = colorComponentCount + (this.HasAlpha ? 1 : 0);
- int bitsPerPixel = componentCount * this.BitDepth;
+ int bitsPerPixel = componentCount * bitDepth;
PixelColorType colorType = this.IsMonochrome ? PixelColorType.Luminance : PixelColorType.RGB;
PixelComponentInfo info;
if (this.IsMonochrome)
{
info = this.HasAlpha
- ? PixelComponentInfo.Create(2, bitsPerPixel, this.BitDepth, this.BitDepth)
- : PixelComponentInfo.Create(1, bitsPerPixel, this.BitDepth);
+ ? PixelComponentInfo.Create(2, bitsPerPixel, bitDepth, bitDepth)
+ : PixelComponentInfo.Create(1, bitsPerPixel, bitDepth);
}
else
{
info = this.HasAlpha
- ? PixelComponentInfo.Create(4, bitsPerPixel, this.BitDepth, this.BitDepth, this.BitDepth, this.BitDepth)
- : PixelComponentInfo.Create(3, bitsPerPixel, this.BitDepth, this.BitDepth, this.BitDepth);
+ ? PixelComponentInfo.Create(4, bitsPerPixel, bitDepth, bitDepth, bitDepth, bitDepth)
+ : PixelComponentInfo.Create(3, bitsPerPixel, bitDepth, bitDepth, bitDepth);
}
if (this.HasAlpha)
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs
index 0879cec01..c2aa475cf 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs
@@ -66,6 +66,14 @@ internal sealed class HevcCodecConfiguration
this.BitDepthLuma = 8 + (lumaBitDepth & 7);
this.BitDepthChroma = 8 + (chromaBitDepth & 7);
+ // Reject precisions outside the public HEIF profile matrix before an unrepresentable value can enter the
+ // typed image metadata or reach a sample pipeline that only implements 8, 10, and 12-bit arithmetic.
+ if (this.BitDepthLuma is not 8 and not 10 and not 12
+ || (this.ChromaFormat != 0 && this.BitDepthChroma is not 8 and not 10 and not 12))
+ {
+ throw new InvalidImageContentException("The HEVC codec configuration uses an unsupported component bit depth.");
+ }
+
// Average frame rate and temporal-layer signaling describe timed samples. Consume those fixed-record fields
// to reach the image item's NAL length width without retaining playback state in the still-image model.
offset += 2;
@@ -267,7 +275,8 @@ internal sealed class HevcCodecConfiguration
///
/// Gets the maximum coded color-component precision in bits.
///
- public int BitDepth => this.IsMonochrome ? this.BitDepthLuma : Math.Max(this.BitDepthLuma, this.BitDepthChroma);
+ public HeifBitDepth BitDepth
+ => (HeifBitDepth)(this.IsMonochrome ? this.BitDepthLuma : Math.Max(this.BitDepthLuma, this.BitDepthChroma));
///
/// Gets a value indicating whether the coded image contains only a luma plane.
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
index e312d015d..ceaa292c3 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
@@ -15,11 +15,11 @@ public class HeifDecoderTests
private const uint UnknownBoxType = 0x74657374U;
[Theory]
- [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, 3992, 2992)]
- [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, 640, 428)]
- [InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg, 7728, 5152)]
- [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, 480, 640)]
- public void Identify(string imagePath, HeifCompressionMethod compressionMethod, int width, int height)
+ [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 3992, 2992)]
+ [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 640, 428)]
+ [InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg, HeifBitDepth.Bit8, 7728, 5152)]
+ [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, HeifBitDepth.Bit8, 480, 640)]
+ public void Identify(string imagePath, HeifCompressionMethod compressionMethod, HeifBitDepth bitDepth, int width, int height)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
@@ -30,6 +30,7 @@ public class HeifDecoderTests
Assert.NotNull(imageInfo);
Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat);
Assert.Equal(compressionMethod, heicMetadata.CompressionMethod);
+ Assert.Equal(bitDepth, heicMetadata.BitDepth);
Assert.Equal(width, imageInfo.Width);
Assert.Equal(height, imageInfo.Height);
}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs
new file mode 100644
index 000000000..3b86e70ed
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Heif;
+using SixLabors.ImageSharp.Formats.Heif.Hevc;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Heif;
+
+[Trait("Format", "Heif")]
+public class HeifMetadataTests
+{
+ [Fact]
+ public void DefaultsMatchLegacyEightBitHeif()
+ {
+ HeifMetadata metadata = new();
+
+ Assert.Equal(HeifCompressionMethod.LegacyJpeg, metadata.CompressionMethod);
+ Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
+ Assert.False(metadata.IsMonochrome);
+ Assert.False(metadata.HasAlpha);
+ }
+
+ [Fact]
+ public void DeepCloneCopiesImageDescription()
+ {
+ HeifMetadata metadata = new()
+ {
+ CompressionMethod = HeifCompressionMethod.Av1,
+ BitDepth = HeifBitDepth.Bit12,
+ IsMonochrome = true,
+ HasAlpha = true
+ };
+
+ HeifMetadata clone = metadata.DeepClone();
+
+ Assert.Equal(metadata.CompressionMethod, clone.CompressionMethod);
+ Assert.Equal(metadata.BitDepth, clone.BitDepth);
+ Assert.Equal(metadata.IsMonochrome, clone.IsMonochrome);
+ Assert.Equal(metadata.HasAlpha, clone.HasAlpha);
+ }
+
+ [Theory]
+ [InlineData(1, HeifBitDepth.Bit8)]
+ [InlineData(8, HeifBitDepth.Bit8)]
+ [InlineData(9, HeifBitDepth.Bit10)]
+ [InlineData(10, HeifBitDepth.Bit10)]
+ [InlineData(11, HeifBitDepth.Bit12)]
+ [InlineData(16, HeifBitDepth.Bit12)]
+ public void FromFormatConnectingMetadataSelectsSupportedBitDepth(int componentPrecision, HeifBitDepth expected)
+ {
+ FormatConnectingMetadata connectingMetadata = new()
+ {
+ PixelTypeInfo = new PixelTypeInfo(componentPrecision)
+ {
+ ComponentInfo = PixelComponentInfo.Create(1, componentPrecision, componentPrecision)
+ }
+ };
+
+ HeifMetadata metadata = HeifMetadata.FromFormatConnectingMetadata(connectingMetadata);
+
+ Assert.Equal(expected, metadata.BitDepth);
+ }
+
+ [Theory]
+ [InlineData(HeifBitDepth.Bit8, false, false, 24, 3)]
+ [InlineData(HeifBitDepth.Bit10, false, true, 40, 4)]
+ [InlineData(HeifBitDepth.Bit12, true, false, 12, 1)]
+ [InlineData(HeifBitDepth.Bit12, true, true, 24, 2)]
+ public void GetPixelTypeInfoUsesComponentBitDepth(
+ HeifBitDepth bitDepth,
+ bool isMonochrome,
+ bool hasAlpha,
+ int expectedBitsPerPixel,
+ int expectedComponentCount)
+ {
+ HeifMetadata metadata = new()
+ {
+ BitDepth = bitDepth,
+ IsMonochrome = isMonochrome,
+ HasAlpha = hasAlpha
+ };
+
+ PixelTypeInfo pixelTypeInfo = metadata.GetPixelTypeInfo();
+ PixelComponentInfo componentInfo = pixelTypeInfo.ComponentInfo.Value;
+
+ Assert.Equal(expectedBitsPerPixel, pixelTypeInfo.BitsPerPixel);
+ Assert.Equal(expectedComponentCount, componentInfo.ComponentCount);
+ Assert.Equal((int)bitDepth, componentInfo.GetMaximumComponentPrecision());
+ }
+
+ [Theory]
+ [InlineData(8, HeifBitDepth.Bit8)]
+ [InlineData(10, HeifBitDepth.Bit10)]
+ [InlineData(12, HeifBitDepth.Bit12)]
+ public void HevcConfigurationAcceptsExposedBitDepths(int componentBitDepth, HeifBitDepth expected)
+ {
+ HevcCodecConfiguration configuration = new(CreateHevcCodecConfiguration(componentBitDepth));
+
+ Assert.Equal(expected, configuration.BitDepth);
+ }
+
+ [Theory]
+ [InlineData(9)]
+ [InlineData(11)]
+ [InlineData(13)]
+ [InlineData(14)]
+ [InlineData(15)]
+ public void HevcConfigurationRejectsUnexposedBitDepths(int componentBitDepth)
+ {
+ byte[] configuration = CreateHevcCodecConfiguration(componentBitDepth);
+
+ Assert.Throws(() => new HevcCodecConfiguration(configuration));
+ }
+
+ ///
+ /// Creates the fixed HEVC decoder-configuration record needed to exercise component bit-depth validation.
+ ///
+ /// The luma and chroma sample precision to encode in the record.
+ /// The complete configuration record without parameter-set arrays.
+ private static byte[] CreateHevcCodecConfiguration(int componentBitDepth)
+ {
+ byte[] data = new byte[23];
+ data[0] = 1;
+ data[13] = 0xF0;
+ data[15] = 0xFC;
+ data[16] = 0xFD;
+ data[17] = (byte)(0xF8 | (componentBitDepth - 8));
+ data[18] = (byte)(0xF8 | (componentBitDepth - 8));
+ data[21] = 3;
+
+ // An empty array list is sufficient here because bit-depth validation belongs to the fixed record and runs
+ // before parameter-set matching. Parameter-set conformance is covered separately by the container tests.
+ return data;
+ }
+}