Browse Source

Lock HEIF metadata bit depth

pull/2633/head
James Jackson-South 1 week ago
parent
commit
afa38644c2
  1. 6
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  3. 2
      src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
  4. 2
      src/ImageSharp/Formats/Heif/HeifBitDepth.cs
  5. 41
      src/ImageSharp/Formats/Heif/HeifMetadata.cs
  6. 11
      src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs
  7. 11
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  8. 137
      tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs

6
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.

2
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -139,7 +139,7 @@ internal sealed class Av1CodecConfiguration
/// <summary>
/// Gets the coded image sample precision in bits.
/// </summary>
public int BitDepth => this.TwelveBit ? 12 : this.HighBitDepth ? 10 : 8;
public HeifBitDepth BitDepth => this.TwelveBit ? HeifBitDepth.Bit12 : this.HighBitDepth ? HeifBitDepth.Bit10 : HeifBitDepth.Bit8;
/// <summary>
/// Gets a value indicating whether the coded image contains only a luma plane.

2
src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs

@ -47,7 +47,7 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
{
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.");
}

2
src/ImageSharp/Formats/Heif/HeifBitDepth.cs

@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Enumerates the component bit depths supported for HEIF image encoding.
/// Enumerates the supported HEIF image-component bit depths.
/// </summary>
public enum HeifBitDepth : byte
{

41
src/ImageSharp/Formats/Heif/HeifMetadata.cs

@ -39,12 +39,12 @@ public class HeifMetadata : IFormatMetadata<HeifMetadata>
/// <summary>
/// Gets or sets the compression method used for the primary frame.
/// </summary>
public HeifCompressionMethod CompressionMethod { get; set; }
public HeifCompressionMethod CompressionMethod { get; set; } = HeifCompressionMethod.LegacyJpeg;
/// <summary>
/// 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 <see cref="HeifBitDepth.Bit8"/>.
/// </summary>
public int BitDepth { get; set; } = 8;
public HeifBitDepth BitDepth { get; set; } = HeifBitDepth.Bit8;
/// <summary>
/// Gets or sets a value indicating whether the primary image contains a single luminance component.
@ -93,34 +93,45 @@ public class HeifMetadata : IFormatMetadata<HeifMetadata>
public HeifNominalDiffuseWhite? NominalDiffuseWhite { get; set; }
/// <inheritdoc/>
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
};
}
/// <inheritdoc/>
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)

11
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
/// <summary>
/// Gets the maximum coded color-component precision in bits.
/// </summary>
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));
/// <summary>
/// Gets a value indicating whether the coded image contains only a luma plane.

11
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);
}

137
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<InvalidImageContentException>(() => new HevcCodecConfiguration(configuration));
}
/// <summary>
/// Creates the fixed HEVC decoder-configuration record needed to exercise component bit-depth validation.
/// </summary>
/// <param name="componentBitDepth">The luma and chroma sample precision to encode in the record.</param>
/// <returns>The complete configuration record without parameter-set arrays.</returns>
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;
}
}
Loading…
Cancel
Save