Browse Source

Define HEIF encoder options

pull/2633/head
James Jackson-South 1 week ago
parent
commit
0468441220
  1. 12
      HEIF_IMPLEMENTATION_PLAN.md
  2. 25
      src/ImageSharp/Formats/Heif/HeifBitDepth.cs
  3. 30
      src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs
  4. 20
      src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs
  5. 105
      src/ImageSharp/Formats/Heif/HeifEncoder.cs
  6. 42
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  7. 12
      src/ImageSharp/Formats/Heif/IHeifEncoderOptions.cs
  8. 81
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

12
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. - [ ] 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. - [ ] 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. - [ ] 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 `<inheritdoc/>` consistently from `HeifEncoder` if the interface remains justified. - [x] **Completed:** define and document the HEIF encoder option contract using the established ImageSharp encoder pattern.
- [ ] 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. - [x] Confirm that `IHeifEncoderOptions` has only one concrete implementation and remove the unnecessary interface.
- [ ] Document the default, valid range, special values, invalid-value behavior, and format-dependent restrictions of every retained option using observable API behavior only. - [x] 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 `<inheritdoc/>` on matching `HeifEncoder` members instead of maintaining duplicate documentation. - [x] Pass `HeifEncoder` directly to `HeifEncoderCore`, matching the JPEG, PNG, and WebP encoder-core contracts and avoiding interface dispatch.
- [ ] Verify option validation and API shape against the established ImageSharp encoder patterns before the Phase 1 API-review gate is marked complete. - [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. 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. - `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. - `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. - `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. - 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. - Configuration registration exists, but it currently registers capabilities broader than the implementation provides.

25
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;
/// <summary>
/// Enumerates the component bit depths supported for HEIF image encoding.
/// </summary>
public enum HeifBitDepth : byte
{
/// <summary>
/// Eight bits per image component.
/// </summary>
Bit8 = 8,
/// <summary>
/// Ten bits per image component.
/// </summary>
Bit10 = 10,
/// <summary>
/// Twelve bits per image component.
/// </summary>
Bit12 = 12
}

30
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;
/// <summary>
/// Enumerates the chroma sampling layouts supported for HEIF image encoding.
/// </summary>
public enum HeifChromaSubsampling : byte
{
/// <summary>
/// A single luminance plane without chroma planes.
/// </summary>
Monochrome,
/// <summary>
/// Chroma sampled at half the luma resolution horizontally and vertically.
/// </summary>
Yuv420,
/// <summary>
/// Chroma sampled at half the luma resolution horizontally and full resolution vertically.
/// </summary>
Yuv422,
/// <summary>
/// Chroma sampled at full luma resolution horizontally and vertically.
/// </summary>
Yuv444
}

20
src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs

@ -18,28 +18,8 @@ public enum HeifCompressionMethod
/// </summary> /// </summary>
LegacyJpeg, LegacyJpeg,
/// <summary>
/// JPEG 2000 coding.
/// </summary>
Jpeg2000,
/// <summary>
/// JPEG XR coding.
/// </summary>
JpegXR,
/// <summary>
/// JPEG XS coding.
/// </summary>
JpegXS,
/// <summary> /// <summary>
/// AOMedia Video 1 (AV1) coding. /// AOMedia Video 1 (AV1) coding.
/// </summary> /// </summary>
Av1, Av1,
/// <summary>
/// Advanced Video Coding (AVC).
/// </summary>
Avc,
} }

105
src/ImageSharp/Formats/Heif/HeifEncoder.cs

@ -4,10 +4,111 @@
namespace SixLabors.ImageSharp.Formats.Heif; namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public sealed class HeifEncoder : ImageEncoder public sealed class HeifEncoder : AlphaAwareImageEncoder
{ {
/// <summary>
/// Backing field for <see cref="Quality"/>.
/// </summary>
private int? quality;
/// <summary>
/// Backing field for <see cref="AlphaQuality"/>.
/// </summary>
private int? alphaQuality;
/// <summary>
/// Backing field for <see cref="Effort"/>.
/// </summary>
private int effort = 5;
/// <summary>
/// Gets the compression method used for the primary image item.
/// The default is <see cref="HeifCompressionMethod.LegacyJpeg"/>.
/// </summary>
public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.LegacyJpeg;
/// <summary>
/// Gets the lossy compression quality, or <see langword="null"/> 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 <see cref="Lossless"/> encoding.
/// </summary>
/// <exception cref="ArgumentException">The quality is outside the range 0 to 100.</exception>
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;
}
}
/// <summary>
/// Gets the lossy compression quality for the auxiliary alpha image, or <see langword="null"/> to use the
/// effective <see cref="Quality"/>. 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.
/// </summary>
/// <exception cref="ArgumentException">The alpha quality is outside the range 0 to 100.</exception>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <exception cref="ArgumentException">The effort is outside the range 0 to 10.</exception>
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;
}
}
/// <summary>
/// Gets a value indicating whether the primary and auxiliary alpha images are encoded without loss. When
/// <see langword="true"/>, <see cref="Quality"/> and <see cref="AlphaQuality"/> do not affect the encoded image.
/// Legacy JPEG image items do not support lossless encoding. The default is <see langword="false"/>.
/// </summary>
public bool Lossless { get; init; }
/// <summary>
/// Gets the encoded precision of each image component, or <see langword="null"/> to use the HEIF metadata bit
/// depth. Metadata that does not specify a bit depth defaults to <see cref="HeifBitDepth.Bit8"/>. Legacy JPEG
/// image items support only <see cref="HeifBitDepth.Bit8"/>.
/// </summary>
public HeifBitDepth? BitDepth { get; init; }
/// <summary>
/// Gets the encoded chroma sampling, or <see langword="null"/> to use <see cref="HeifChromaSubsampling.Yuv420"/>
/// for lossy encoding and <see cref="HeifChromaSubsampling.Yuv444"/> for lossless encoding.
/// </summary>
public HeifChromaSubsampling? ChromaSubsampling { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {

42
src/ImageSharp/Formats/Heif/HeifEncoderCore.cs

@ -47,7 +47,14 @@ internal sealed class HeifEncoderCore
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); 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<HeifItem> items = new(); List<HeifItem> items = new();
List<HeifItemLink> links = new(); List<HeifItemLink> links = new();
GenerateItems(image, pixels, items); GenerateItems(image, pixels, items);
@ -59,7 +66,7 @@ internal sealed class HeifEncoderCore
stream.Flush(); stream.Flush();
HeifMetadata meta = image.Metadata.GetHeifMetadata(); HeifMetadata meta = image.Metadata.GetHeifMetadata();
meta.CompressionMethod = HeifCompressionMethod.LegacyJpeg; meta.CompressionMethod = this.encoder.CompressionMethod;
} }
/// <summary> /// <summary>
@ -432,13 +439,40 @@ internal sealed class HeifEncoderCore
/// <param name="image">The source image.</param> /// <param name="image">The source image.</param>
/// <param name="cancellationToken">The token used to cancel payload encoding.</param> /// <param name="cancellationToken">The token used to cancel payload encoding.</param>
/// <returns>The encoded JPEG item bytes.</returns> /// <returns>The encoded JPEG item bytes.</returns>
private static byte[] CompressPixels<TPixel>(Image<TPixel> image, CancellationToken cancellationToken) private byte[] CompressPixels<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
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(); using MemoryStream stream = new();
JpegEncoder encoder = new() JpegEncoder encoder = new()
{ {
ColorType = JpegColorType.YCbCrRatio420 Quality = this.encoder.Quality,
ColorType = colorType
}; };
// ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation // ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation

12
src/ImageSharp/Formats/Heif/IHeifEncoderOptions.cs

@ -1,12 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Configuration options for use during HEIF encoding.
/// </summary>
internal interface IHeifEncoderOptions
{
// None defined yet.
}

81
tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

@ -12,6 +12,87 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif;
[ValidateDisposedMemoryAllocations] [ValidateDisposedMemoryAllocations]
public class HeifEncoderTests 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<ArgumentException>(() => new HeifEncoder { Quality = quality });
[Theory]
[InlineData(-1)]
[InlineData(101)]
public void AlphaQualityOutsideRangeThrows(int quality)
=> Assert.Throws<ArgumentException>(() => new HeifEncoder { AlphaQuality = quality });
[Theory]
[InlineData(-1)]
[InlineData(11)]
public void EffortOutsideRangeThrows(int effort)
=> Assert.Throws<ArgumentException>(() => 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<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new() { Quality = 0 };
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
}
[Fact]
public void LegacyJpegRejectsLosslessEncoding()
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new() { Lossless = true };
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
}
[Theory]
[InlineData(HeifBitDepth.Bit10)]
[InlineData(HeifBitDepth.Bit12)]
public void LegacyJpegRejectsHighBitDepth(HeifBitDepth bitDepth)
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new() { BitDepth = bitDepth };
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
}
[Theory] [Theory]
[WithFile(TestImages.Heif.Sample640x427, PixelTypes.Rgba32, HeifCompressionMethod.LegacyJpeg)] [WithFile(TestImages.Heif.Sample640x427, PixelTypes.Rgba32, HeifCompressionMethod.LegacyJpeg)]
public static void Encode<TPixel>(TestImageProvider<TPixel> provider, HeifCompressionMethod compressionMethod) public static void Encode<TPixel>(TestImageProvider<TPixel> provider, HeifCompressionMethod compressionMethod)

Loading…
Cancel
Save