diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index acbce5ece..c450c8869 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -56,11 +56,11 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [ ] Connect both payload encoders to the bounded HEIF writer with the selected bit depth, chroma layout, range, color signaling, alpha, metadata, and animation state.
- [ ] Replace each `NotSupportedException` branch only when the corresponding payload is accepted by the pinned independent decoder and the ImageSharp decoder.
- [ ] Verify that every public quality, effort, lossless, bit-depth, chroma, alpha, and metadata option changes or constrains the encoded output exactly as documented.
-- [ ] **Queued:** complete `IHeifEncoderOptions` documentation, including all observable limits, and use `` consistently from `HeifEncoder` if the interface remains justified.
- - [ ] Confirm that the interface is required by multiple concrete HEIF-family encoders and remove it if it does not represent a genuine shared public contract.
- - [ ] Document the default, valid range, special values, invalid-value behavior, and format-dependent restrictions of every retained option using observable API behavior only.
- - [ ] Keep the complete contract on `IHeifEncoderOptions` and use `` on matching `HeifEncoder` members instead of maintaining duplicate documentation.
- - [ ] Verify option validation and API shape against the established ImageSharp encoder patterns before the Phase 1 API-review gate is marked complete.
+- [x] **Completed:** define and document the HEIF encoder option contract using the established ImageSharp encoder pattern.
+ - [x] Confirm that `IHeifEncoderOptions` has only one concrete implementation and remove the unnecessary interface.
+ - [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.
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.
@@ -176,7 +176,7 @@ This assessment is based on the current source after the upstream ImageSharp mer
- `HeifFormat` combines the HEIF, HEIC, HIF, and AVIF identities and extensions, but the implementation does not yet decode all payloads that contract implies.
- `HeifDecoder` now defaults to `Rgba32`, preserving decoded auxiliary alpha for non-generic loads.
- `HeifMetadata` now reports alpha presence and the corresponding 24/32-bit RGB pixel shape, but complete decoded HEVC/AV1 bit depth, monochrome/chroma layout, color signaling, and profiles remain absent.
-- `IHeifEncoderOptions` is empty, and the encoder exposes no meaningful quality, speed, lossless, subsampling, bit-depth, or alpha policy.
+- `HeifEncoder` defines quality, alpha quality, effort, lossless, chroma-subsampling, and bit-depth contracts directly, without a single-implementation options interface. The legacy JPEG path applies its supported quality, bit-depth, and chroma options and rejects unsupported combinations; AV1 and HEVC must implement the same public contracts before the Phase 1 API-review gate can pass.
- HEIF/HEIC/AVIF is absent from the format source-generation list in `_Formats.ttinclude`, so the standard ImageSharp save extensions are not generated.
- Configuration registration exists, but it currently registers capabilities broader than the implementation provides.
diff --git a/src/ImageSharp/Formats/Heif/HeifBitDepth.cs b/src/ImageSharp/Formats/Heif/HeifBitDepth.cs
new file mode 100644
index 000000000..be11bd6ae
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/HeifBitDepth.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif;
+
+///
+/// Enumerates the component bit depths supported for HEIF image encoding.
+///
+public enum HeifBitDepth : byte
+{
+ ///
+ /// Eight bits per image component.
+ ///
+ Bit8 = 8,
+
+ ///
+ /// Ten bits per image component.
+ ///
+ Bit10 = 10,
+
+ ///
+ /// Twelve bits per image component.
+ ///
+ Bit12 = 12
+}
diff --git a/src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs b/src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs
new file mode 100644
index 000000000..099430c7c
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif;
+
+///
+/// Enumerates the chroma sampling layouts supported for HEIF image encoding.
+///
+public enum HeifChromaSubsampling : byte
+{
+ ///
+ /// A single luminance plane without chroma planes.
+ ///
+ Monochrome,
+
+ ///
+ /// Chroma sampled at half the luma resolution horizontally and vertically.
+ ///
+ Yuv420,
+
+ ///
+ /// Chroma sampled at half the luma resolution horizontally and full resolution vertically.
+ ///
+ Yuv422,
+
+ ///
+ /// Chroma sampled at full luma resolution horizontally and vertically.
+ ///
+ Yuv444
+}
diff --git a/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs b/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs
index 6789f94c9..f317301b4 100644
--- a/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs
+++ b/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs
@@ -18,28 +18,8 @@ public enum HeifCompressionMethod
///
LegacyJpeg,
- ///
- /// JPEG 2000 coding.
- ///
- Jpeg2000,
-
- ///
- /// JPEG XR coding.
- ///
- JpegXR,
-
- ///
- /// JPEG XS coding.
- ///
- JpegXS,
-
///
/// AOMedia Video 1 (AV1) coding.
///
Av1,
-
- ///
- /// Advanced Video Coding (AVC).
- ///
- Avc,
}
diff --git a/src/ImageSharp/Formats/Heif/HeifEncoder.cs b/src/ImageSharp/Formats/Heif/HeifEncoder.cs
index 170dc7bd7..b440fc393 100644
--- a/src/ImageSharp/Formats/Heif/HeifEncoder.cs
+++ b/src/ImageSharp/Formats/Heif/HeifEncoder.cs
@@ -4,10 +4,111 @@
namespace SixLabors.ImageSharp.Formats.Heif;
///
-/// Image encoder for writing an image to a stream as HEIF images.
+/// Image encoder for writing image data to a stream in a HEIF container.
///
-public sealed class HeifEncoder : ImageEncoder
+public sealed class HeifEncoder : AlphaAwareImageEncoder
{
+ ///
+ /// Backing field for .
+ ///
+ private int? quality;
+
+ ///
+ /// Backing field for .
+ ///
+ private int? alphaQuality;
+
+ ///
+ /// Backing field for .
+ ///
+ private int effort = 5;
+
+ ///
+ /// Gets the compression method used for the primary image item.
+ /// The default is .
+ ///
+ public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.LegacyJpeg;
+
+ ///
+ /// Gets the lossy compression quality, or to use the compression method's default quality.
+ /// Valid values range from 0 for the lowest quality to 100 for the highest quality. Legacy JPEG image items
+ /// support values from 1 through 100. A value of 100 does not enable encoding.
+ ///
+ /// The quality is outside the range 0 to 100.
+ public int? Quality
+ {
+ get => this.quality;
+ init
+ {
+ if (value is < 0 or > 100)
+ {
+ throw new ArgumentException("Quality must be in the range [0..100].");
+ }
+
+ this.quality = value;
+ }
+ }
+
+ ///
+ /// Gets the lossy compression quality for the auxiliary alpha image, or to use the
+ /// effective . Valid values range from 0 for the lowest quality to 100 for the highest
+ /// quality. This option has no effect when the encoded image does not require an auxiliary alpha image.
+ ///
+ /// The alpha quality is outside the range 0 to 100.
+ public int? AlphaQuality
+ {
+ get => this.alphaQuality;
+ init
+ {
+ if (value is < 0 or > 100)
+ {
+ throw new ArgumentException("Alpha quality must be in the range [0..100].");
+ }
+
+ this.alphaQuality = value;
+ }
+ }
+
+ ///
+ /// Gets the encoding effort in the range 0 to 10. A value of 0 selects the fastest encoding and 10 selects the
+ /// slowest encoding with the greatest compression effort. The default is 5. Legacy JPEG image items use a fixed
+ /// encoding effort, so this option does not affect them.
+ ///
+ /// The effort is outside the range 0 to 10.
+ public int Effort
+ {
+ get => this.effort;
+ init
+ {
+ if (value is < 0 or > 10)
+ {
+ throw new ArgumentException("Effort must be in the range [0..10].");
+ }
+
+ this.effort = value;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the primary and auxiliary alpha images are encoded without loss. When
+ /// , and do not affect the encoded image.
+ /// Legacy JPEG image items do not support lossless encoding. The default is .
+ ///
+ public bool Lossless { get; init; }
+
+ ///
+ /// Gets the encoded precision of each image component, or to use the HEIF metadata bit
+ /// depth. Metadata that does not specify a bit depth defaults to . Legacy JPEG
+ /// image items support only .
+ ///
+ public HeifBitDepth? BitDepth { get; init; }
+
+ ///
+ /// Gets the encoded chroma sampling, or to use
+ /// for lossy encoding and for lossless encoding.
+ ///
+ public HeifChromaSubsampling? ChromaSubsampling { get; init; }
+
///
protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
{
diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
index 7d4684c5e..8a28213fe 100644
--- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
@@ -47,7 +47,14 @@ internal sealed class HeifEncoderCore
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
- byte[] pixels = CompressPixels(image, cancellationToken);
+ byte[] pixels = this.encoder.CompressionMethod switch
+ {
+ HeifCompressionMethod.LegacyJpeg => this.CompressPixels(image, cancellationToken),
+ HeifCompressionMethod.Av1 => throw new NotSupportedException("AV1 encoding is not implemented."),
+ HeifCompressionMethod.Hevc => throw new NotSupportedException("HEVC encoding is not implemented."),
+ _ => throw new NotSupportedException($"HEIF compression method '{this.encoder.CompressionMethod}' is not supported.")
+ };
+
List items = new();
List links = new();
GenerateItems(image, pixels, items);
@@ -59,7 +66,7 @@ internal sealed class HeifEncoderCore
stream.Flush();
HeifMetadata meta = image.Metadata.GetHeifMetadata();
- meta.CompressionMethod = HeifCompressionMethod.LegacyJpeg;
+ meta.CompressionMethod = this.encoder.CompressionMethod;
}
///
@@ -432,13 +439,40 @@ internal sealed class HeifEncoderCore
/// The source image.
/// The token used to cancel payload encoding.
/// The encoded JPEG item bytes.
- private static byte[] CompressPixels(Image image, CancellationToken cancellationToken)
+ private byte[] CompressPixels(Image image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ if (this.encoder.Lossless)
+ {
+ throw new NotSupportedException("Legacy JPEG image items do not support lossless encoding.");
+ }
+
+ if (this.encoder.BitDepth is not null && this.encoder.BitDepth != HeifBitDepth.Bit8)
+ {
+ throw new NotSupportedException("Legacy JPEG image items support only 8-bit component encoding.");
+ }
+
+ if (this.encoder.Quality == 0)
+ {
+ // Zero is meaningful to the AV1 and HEVC quality scales, but ImageSharp's JPEG encoder deliberately
+ // exposes the JPEG quality scale as 1 through 100. Reject the codec-specific mismatch at this boundary.
+ throw new NotSupportedException("Legacy JPEG image items support quality values in the range [1..100].");
+ }
+
+ JpegColorType colorType = this.encoder.ChromaSubsampling switch
+ {
+ null or HeifChromaSubsampling.Yuv420 => JpegColorType.YCbCrRatio420,
+ HeifChromaSubsampling.Yuv422 => JpegColorType.YCbCrRatio422,
+ HeifChromaSubsampling.Yuv444 => JpegColorType.YCbCrRatio444,
+ HeifChromaSubsampling.Monochrome => JpegColorType.Luminance,
+ _ => throw new NotSupportedException($"HEIF chroma sampling '{this.encoder.ChromaSubsampling}' is not supported.")
+ };
+
using MemoryStream stream = new();
JpegEncoder encoder = new()
{
- ColorType = JpegColorType.YCbCrRatio420
+ Quality = this.encoder.Quality,
+ ColorType = colorType
};
// ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation
diff --git a/src/ImageSharp/Formats/Heif/IHeifEncoderOptions.cs b/src/ImageSharp/Formats/Heif/IHeifEncoderOptions.cs
deleted file mode 100644
index 700ccc1e0..000000000
--- a/src/ImageSharp/Formats/Heif/IHeifEncoderOptions.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats.Heif;
-
-///
-/// Configuration options for use during HEIF encoding.
-///
-internal interface IHeifEncoderOptions
-{
- // None defined yet.
-}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
index 61ceb6b14..8b1f2d804 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
@@ -12,6 +12,87 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif;
[ValidateDisposedMemoryAllocations]
public class HeifEncoderTests
{
+ [Fact]
+ public void OptionsHaveExpectedDefaults()
+ {
+ HeifEncoder encoder = new();
+
+ Assert.Equal(HeifCompressionMethod.LegacyJpeg, encoder.CompressionMethod);
+ Assert.Null(encoder.Quality);
+ Assert.Null(encoder.AlphaQuality);
+ Assert.Equal(5, encoder.Effort);
+ Assert.False(encoder.Lossless);
+ Assert.Null(encoder.BitDepth);
+ Assert.Null(encoder.ChromaSubsampling);
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(101)]
+ public void QualityOutsideRangeThrows(int quality)
+ => Assert.Throws(() => new HeifEncoder { Quality = quality });
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(101)]
+ public void AlphaQualityOutsideRangeThrows(int quality)
+ => Assert.Throws(() => new HeifEncoder { AlphaQuality = quality });
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(11)]
+ public void EffortOutsideRangeThrows(int effort)
+ => Assert.Throws(() => new HeifEncoder { Effort = effort });
+
+ [Theory]
+ [InlineData(0, 0, 0)]
+ [InlineData(100, 100, 10)]
+ public void OptionRangeBoundariesAreAccepted(int quality, int alphaQuality, int effort)
+ {
+ HeifEncoder encoder = new()
+ {
+ Quality = quality,
+ AlphaQuality = alphaQuality,
+ Effort = effort
+ };
+
+ Assert.Equal(quality, encoder.Quality);
+ Assert.Equal(alphaQuality, encoder.AlphaQuality);
+ Assert.Equal(effort, encoder.Effort);
+ }
+
+ [Fact]
+ public void LegacyJpegRejectsZeroQuality()
+ {
+ using Image image = new(1, 1);
+ using MemoryStream stream = new();
+ HeifEncoder encoder = new() { Quality = 0 };
+
+ Assert.Throws(() => image.Save(stream, encoder));
+ }
+
+ [Fact]
+ public void LegacyJpegRejectsLosslessEncoding()
+ {
+ using Image image = new(1, 1);
+ using MemoryStream stream = new();
+ HeifEncoder encoder = new() { Lossless = true };
+
+ Assert.Throws(() => image.Save(stream, encoder));
+ }
+
+ [Theory]
+ [InlineData(HeifBitDepth.Bit10)]
+ [InlineData(HeifBitDepth.Bit12)]
+ public void LegacyJpegRejectsHighBitDepth(HeifBitDepth bitDepth)
+ {
+ using Image image = new(1, 1);
+ using MemoryStream stream = new();
+ HeifEncoder encoder = new() { BitDepth = bitDepth };
+
+ Assert.Throws(() => image.Save(stream, encoder));
+ }
+
[Theory]
[WithFile(TestImages.Heif.Sample640x427, PixelTypes.Rgba32, HeifCompressionMethod.LegacyJpeg)]
public static void Encode(TestImageProvider provider, HeifCompressionMethod compressionMethod)