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; + } +}