diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index bbda34a71..0e50c724d 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat
## Delivery dashboard
-Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `91d79f771`, including the completed AV1 transform, OBU-framing, intra-block-copy, and 12-profile reconstruction checkpoints. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead.
+Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `63c4d302a`, including the completed AV1 transform, OBU-framing, intra-block-copy, and 12-profile reconstruction checkpoints. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead.
Status meanings:
@@ -59,6 +59,13 @@ Immediate checkpoint: **complete layered AV1 image-item decoding through the exi
- [x] Close the base AV1 profile matrix with exact native-plane and presented-image comparisons for 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 fixtures under normal, AVX2, 128-bit, and scalar dispatch.
- [x] Accept the AV1-ISOBMFF final low-overhead OBU form that omits its payload-size field and uses the bounded image-item remainder; focused Release coverage reconstructs a valid combined frame in that form.
- [ ] **Current:** implement layered AV1 image-item properties and stateful dependency reconstruction, then verify default final-layer output against the two pinned libavif progressive fixtures.
+ - [x] Parse and associate `a1op`, `lsel`, and `a1lx` through the bounded image-item property model, including normative essential flags, duplicate handling, exact property lengths, and the four-layer limit.
+ - [x] Validate `a1lx` layer boundaries against the logical item size and restrict concrete `lsel` decoding to the cumulative payload through the selected spatial layer without copying item bytes.
+ - [ ] Apply the selected `a1op` operating-point mask while consuming extended OBUs and validate the selected index against the parsed sequence header.
+ - [ ] Preserve reconstruction, reference-frame, primary-CDF, segmentation, loop-filter, and motion state across every dependent layer in one image-item decoder session.
+ - [ ] Implement the complete inter-frame entropy, mode, motion-vector, compound-prediction, inter-prediction, and warped/global-motion paths permitted by the image profile.
+ - [ ] Return the explicitly selected spatial layer or the final displayed layer, keeping reference reconstruction separate from display-only film grain.
+ - [ ] Verify color and auxiliary-alpha output exactly against both pinned libavif progressive fixtures under normal SIMD dispatch and all required `FeatureTestRunner` fallbacks.
- [ ] Correct the audited 12-bit inverse ADST4, Identity4, and Identity16 SIMD arithmetic by widening only the libaom-widened multiply/accumulate operations, with exact conformant-range vectors and `FeatureTestRunner` coverage.
- [ ] Continue inventorying and removing every remaining valid AV1 still-image unsupported branch, adding exact independent compression-tool fixtures to the profile-matrix regression gate.
- [ ] Complete the remaining HEVC still-image profile and Range Extensions matrix with exact independent native-plane and presentation evidence.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
index e9f8b86b4..82f11fb41 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
@@ -21,6 +21,16 @@ internal static class Av1Constants
///
public const int LevelBits = 5;
+ ///
+ /// The maximum number of operating points declared by one AV1 sequence header.
+ ///
+ public const int MaxOperatingPointCount = 32;
+
+ ///
+ /// The maximum number of spatial layers identified by an AV1 OBU extension header.
+ ///
+ public const int MaxSpatialLayerCount = 4;
+
///
/// The number of bits used to signal a super-resolution denominator offset.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs b/src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs
new file mode 100644
index 000000000..27cec2dca
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1;
+
+///
+/// Identifies the AV1 spatial layer selected by an AVIF image item.
+///
+/// The spatial-layer identifier, or for progressive or final-layer decoding.
+internal readonly struct Av1LayerSelector(ushort layerId)
+{
+ ///
+ /// The layer identifier that selects progressive exposure or the final layer rather than one specific spatial layer.
+ ///
+ public const ushort AllLayers = ushort.MaxValue;
+
+ ///
+ /// Gets the spatial-layer identifier, or when no individual layer is selected.
+ ///
+ public ushort LayerId { get; } = layerId;
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs b/src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs
new file mode 100644
index 000000000..fd19f0444
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1;
+
+///
+/// Describes the explicit payload sizes that delimit the first three layers of a layered AV1 image item.
+///
+/// The first layer size in bytes.
+/// The second layer size in bytes.
+/// The third layer size in bytes.
+internal readonly struct Av1LayeredImageIndex(uint firstLayerSize, uint secondLayerSize, uint thirdLayerSize)
+{
+ ///
+ /// Gets the first layer size in bytes.
+ ///
+ public uint FirstLayerSize { get; } = firstLayerSize;
+
+ ///
+ /// Gets the second layer size in bytes.
+ ///
+ public uint SecondLayerSize { get; } = secondLayerSize;
+
+ ///
+ /// Gets the third layer size in bytes.
+ ///
+ public uint ThirdLayerSize { get; } = thirdLayerSize;
+
+ ///
+ /// Gets the number of item bytes needed to decode the selected spatial layer.
+ ///
+ /// The complete logical image-item payload size.
+ /// The requested spatial layer, or to decode the final layer.
+ /// The cumulative payload size through the selected layer, or the complete item size for final-layer decoding.
+ public int GetPayloadLength(int itemSize, Av1LayerSelector? selector)
+ {
+ int selectedLayer = selector is null || selector.Value.LayerId == Av1LayerSelector.AllLayers
+ ? -1
+ : selector.Value.LayerId;
+
+ uint remainingSize = (uint)itemSize;
+ uint selectedPayloadSize = 0;
+ int layerCount = 0;
+ for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1; layer++)
+ {
+ uint layerSize = layer switch
+ {
+ 0 => this.FirstLayerSize,
+ 1 => this.SecondLayerSize,
+ _ => this.ThirdLayerSize
+ };
+
+ layerCount++;
+ if (layerSize == 0)
+ {
+ if (selectedLayer < 0 || selectedLayer == layer)
+ {
+ selectedPayloadSize += remainingSize;
+ }
+
+ remainingSize = 0;
+ break;
+ }
+
+ if (layerSize >= remainingSize)
+ {
+ // Every explicit layer must leave at least one byte for the final implicit layer. A zero entry instead
+ // identifies the current layer as final and consumes the complete remainder.
+ throw new InvalidImageContentException($"AV1 layered-image layer {layer} does not fit within the item payload.");
+ }
+
+ if (selectedLayer < 0 || layer <= selectedLayer)
+ {
+ selectedPayloadSize += layerSize;
+ }
+
+ remainingSize -= layerSize;
+ }
+
+ if (remainingSize != 0)
+ {
+ if (selectedLayer < 0 || selectedLayer == layerCount)
+ {
+ selectedPayloadSize += remainingSize;
+ }
+
+ layerCount++;
+ }
+
+ if (selectedLayer >= layerCount)
+ {
+ throw new InvalidImageContentException($"AV1 layer selector requests layer {selectedLayer}, but the item contains {layerCount} layers.");
+ }
+
+ return selectedLayer < 0 ? itemSize : (int)selectedPayloadSize;
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs b/src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs
new file mode 100644
index 000000000..4d48e0118
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1;
+
+///
+/// Identifies the AV1 sequence-header operating point selected by an AVIF image item.
+///
+/// The zero-based operating-point index.
+internal readonly struct Av1OperatingPointSelector(byte index)
+{
+ ///
+ /// Gets the zero-based operating-point index.
+ ///
+ public byte Index { get; } = index;
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
index cc8741f05..4441c3a8c 100644
--- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
@@ -44,15 +44,16 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlpha
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
+ Span itemData = GetItemData(item, data);
Av1CodecConfiguration codecConfiguration = ValidateItemData(
options,
item,
- data,
+ itemData,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
using Av1Decoder decoder = new(options.Configuration);
- Image image = decoder.Decode(data, colorProfile, codecConfiguration);
+ Image image = decoder.Decode(itemData, colorProfile, codecConfiguration);
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = this.CompressionMethod;
metadata.BitDepth = codecConfiguration.BitDepth;
@@ -74,7 +75,8 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlpha
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
- Av1CodecConfiguration codecConfiguration = ValidateItemData(options, item, data, out _, out _);
+ Span itemData = GetItemData(item, data);
+ Av1CodecConfiguration codecConfiguration = ValidateItemData(options, item, itemData, out _, out _);
if (!codecConfiguration.IsMonochrome)
{
throw new InvalidImageContentException($"AV1 alpha image item {item.Id} is not monochrome.");
@@ -82,7 +84,7 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlpha
using Av1Decoder decoder = new(options.Configuration);
decoder.DecodeAlpha(
- data,
+ itemData,
item.CicpProfile,
codecConfiguration,
default,
@@ -92,6 +94,24 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlpha
premultiplied);
}
+ ///
+ /// Gets the cumulative item bytes required by an explicit AV1 spatial-layer selection.
+ ///
+ /// The AV1 image item containing optional layered-image properties.
+ /// The complete logical image-item payload.
+ /// The complete payload for final-layer decoding, or the cumulative prefix through the selected layer.
+ private static Span GetItemData(HeifItem item, Span data)
+ {
+ Av1LayeredImageIndex? layeredImageIndex = item.Av1LayeredImageIndex;
+ if (layeredImageIndex is null)
+ {
+ return data;
+ }
+
+ int payloadLength = layeredImageIndex.Value.GetPayloadLength(data.Length, item.Av1LayerSelector);
+ return data[..payloadLength];
+ }
+
///
/// Validates an AV1 item description against its encoded payload and returns the required codec configuration.
///
diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
index 6dab5cb5a..3afe3c9b7 100644
--- a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
+++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
@@ -248,6 +248,21 @@ public enum Heif4CharCode : uint
///
Av1C = 0x61763143U,
+ ///
+ /// AV1 operating-point selector.
+ ///
+ A1op = 0x61316F70U,
+
+ ///
+ /// AV1 layer selector.
+ ///
+ Lsel = 0x6C73656CU,
+
+ ///
+ /// AV1 layered-image indexing.
+ ///
+ A1lx = 0x61316C78U,
+
///
/// Image Mirror.
///
diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
index 8f1d43526..810a14ed9 100644
--- a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
+++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
@@ -54,6 +54,9 @@
"ndwt", "Nominal diffuse white",
"hvcC", "HVC configuration",
"av1C", "AV1 configuration",
+ "a1op", "AV1 operating-point selector",
+ "lsel", "AV1 layer selector",
+ "a1lx", "AV1 layered-image indexing",
"imir", "Image Mirror",
"irot", "Image Rotation",
"clap", "Clean Aperture",
diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
index 926e40a46..c588e341d 100644
--- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
@@ -1442,6 +1442,27 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
Heif4CharCode.Av1C,
new Av1CodecConfiguration(boxBuffer, this.Options)));
+ break;
+ case Heif4CharCode.A1op:
+ properties.Add(
+ new KeyValuePair(
+ Heif4CharCode.A1op,
+ HeifPropertyParser.ParseAv1OperatingPointSelector(boxBuffer)));
+
+ break;
+ case Heif4CharCode.Lsel:
+ properties.Add(
+ new KeyValuePair(
+ Heif4CharCode.Lsel,
+ HeifPropertyParser.ParseAv1LayerSelector(boxBuffer)));
+
+ break;
+ case Heif4CharCode.A1lx:
+ properties.Add(
+ new KeyValuePair(
+ Heif4CharCode.A1lx,
+ HeifPropertyParser.ParseAv1LayeredImageIndex(boxBuffer)));
+
break;
case Heif4CharCode.HvcC:
properties.Add(
@@ -1572,6 +1593,26 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
continue;
}
+ // AVIF 1.1 section 2.3.2.1.1 requires a1op to be essential, while HEIF section 6.5.11.1
+ // imposes the same requirement on lsel because ignoring either selector changes the decoded image.
+ if (!essential && prop.Key is Heif4CharCode.A1op or Heif4CharCode.Lsel)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates AV1 selector property '{prop.Key}' without marking it essential.");
+
+ continue;
+ }
+
+ // AVIF 1.1 section 2.3.2.3.2 requires a1lx to be nonessential; decoders may consume the complete
+ // item payload without using its optional layer-boundary optimization.
+ if (essential && prop.Key == Heif4CharCode.A1lx)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} marks AV1 layered-image indexing property '{prop.Key}' as essential.");
+
+ continue;
+ }
+
switch (prop.Key)
{
case Heif4CharCode.Ispe:
@@ -1641,6 +1682,75 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
item.Av1CodecConfiguration = av1CodecConfiguration;
}
+ break;
+ case Heif4CharCode.A1op:
+ if (prop.Value is Av1OperatingPointSelector operatingPointSelector)
+ {
+ if (item.Type != Heif4CharCode.Av01)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates an AV1 operating-point selector with non-AV1 item type '{item.Type}'.");
+
+ break;
+ }
+
+ if (item.Av1OperatingPointSelector is not null)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates more than one AV1 operating-point selector property.");
+
+ break;
+ }
+
+ item.Av1OperatingPointSelector = operatingPointSelector;
+ }
+
+ break;
+ case Heif4CharCode.Lsel:
+ if (prop.Value is Av1LayerSelector layerSelector)
+ {
+ if (item.Type != Heif4CharCode.Av01)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates an AV1 layer selector with non-AV1 item type '{item.Type}'.");
+
+ break;
+ }
+
+ if (item.Av1LayerSelector is not null)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates more than one AV1 layer selector property.");
+
+ break;
+ }
+
+ item.Av1LayerSelector = layerSelector;
+ }
+
+ break;
+ case Heif4CharCode.A1lx:
+ if (prop.Value is Av1LayeredImageIndex layeredImageIndex)
+ {
+ if (item.Type != Heif4CharCode.Av01)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates AV1 layered-image indexing with non-AV1 item type '{item.Type}'.");
+
+ break;
+ }
+
+ if (item.Av1LayeredImageIndex is not null)
+ {
+ this.ThrowOrIgnoreImageDataSegmentError(
+ $"Item {itemId} associates more than one AV1 layered-image indexing property.");
+
+ break;
+ }
+
+ item.Av1LayeredImageIndex = layeredImageIndex;
+ }
+
break;
case Heif4CharCode.HvcC:
if (prop.Value is HevcCodecConfiguration hevcCodecConfiguration)
diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs
index e26467cda..dd4ecd9c4 100644
--- a/src/ImageSharp/Formats/Heif/HeifItem.cs
+++ b/src/ImageSharp/Formats/Heif/HeifItem.cs
@@ -109,6 +109,24 @@ internal class HeifItem(Heif4CharCode type, uint id)
///
public Av1CodecConfiguration? Av1CodecConfiguration { get; set; }
+ ///
+ /// Gets or sets the operating-point selector associated with this AV1 image item, or when
+ /// the item uses the default operating-point index zero.
+ ///
+ public Av1OperatingPointSelector? Av1OperatingPointSelector { get; set; }
+
+ ///
+ /// Gets or sets the spatial-layer selector associated with this AV1 image item, or when
+ /// no explicit layer selection is present.
+ ///
+ public Av1LayerSelector? Av1LayerSelector { get; set; }
+
+ ///
+ /// Gets or sets the layered-image payload index associated with this AV1 image item, or
+ /// when the payload does not provide explicit layer boundaries.
+ ///
+ public Av1LayeredImageIndex? Av1LayeredImageIndex { get; set; }
+
///
/// Gets or sets the HEVC codec configuration associated with this coded image item, or
/// when the item has no HEVC codec-configuration property.
diff --git a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs
index 73ee5dbf3..482b7f701 100644
--- a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs
+++ b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs
@@ -3,6 +3,7 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@@ -304,6 +305,82 @@ internal static class HeifPropertyParser
return new HeifNominalDiffuseWhite(luminance == 0 ? null : luminance * luminanceScale);
}
+ ///
+ /// Parses the sequence-header operating point selected by an AV1 image item.
+ ///
+ /// The complete AV1 operating-point-selector payload.
+ /// The selected zero-based operating-point index.
+ public static Av1OperatingPointSelector ParseAv1OperatingPointSelector(ReadOnlySpan data)
+ {
+ EnsureExactLength(data, 1, "AV1 operating-point selector");
+ byte index = data[0];
+ if (index >= Av1Constants.MaxOperatingPointCount)
+ {
+ // AV1 signals operating_points_cnt_minus_1 in five bits, so a sequence header cannot contain
+ // an operating point whose zero-based index is greater than 31.
+ throw new InvalidImageContentException($"The AV1 operating-point selector requests unsupported index {index}.");
+ }
+
+ return new Av1OperatingPointSelector(index);
+ }
+
+ ///
+ /// Parses the spatial layer selected by an AV1 image item.
+ ///
+ /// The complete AV1 layer-selector payload.
+ /// The selected spatial-layer identifier.
+ public static Av1LayerSelector ParseAv1LayerSelector(ReadOnlySpan data)
+ {
+ EnsureExactLength(data, 2, "AV1 layer selector");
+ ushort layerId = BinaryPrimitives.ReadUInt16BigEndian(data);
+ if (layerId != Av1LayerSelector.AllLayers && layerId >= Av1Constants.MaxSpatialLayerCount)
+ {
+ // AV1 OBU extension headers carry spatial_id in two bits. AVIF reserves 0xFFFF to request
+ // progressive exposure or final-layer decoding instead of selecting one of those four IDs.
+ throw new InvalidImageContentException($"The AV1 layer selector requests unsupported layer {layerId}.");
+ }
+
+ return new Av1LayerSelector(layerId);
+ }
+
+ ///
+ /// Parses the explicit payload boundaries of a layered AV1 image item.
+ ///
+ /// The complete AV1 layered-image-indexing payload.
+ /// The three explicit layer sizes.
+ public static Av1LayeredImageIndex ParseAv1LayeredImageIndex(ReadOnlySpan data)
+ {
+ if (data.IsEmpty)
+ {
+ throw new InvalidImageContentException("The AV1 layered-image indexing property has an invalid length.");
+ }
+
+ byte sizeFlags = data[0];
+ if ((sizeFlags & 0xFE) != 0)
+ {
+ throw new InvalidImageContentException("The AV1 layered-image indexing property has nonzero reserved bits.");
+ }
+
+ bool usesLargeSizes = (sizeFlags & 1) != 0;
+ int layerSizeWidth = usesLargeSizes ? 4 : 2;
+
+ // a1lx stores the first three sizes explicitly. A fourth layer, when present, consumes the
+ // remaining item payload and therefore has no stored size field.
+ EnsureExactLength(data, 1 + (3 * layerSizeWidth), "AV1 layered-image indexing");
+ if (usesLargeSizes)
+ {
+ return new Av1LayeredImageIndex(
+ BinaryPrimitives.ReadUInt32BigEndian(data[1..]),
+ BinaryPrimitives.ReadUInt32BigEndian(data[5..]),
+ BinaryPrimitives.ReadUInt32BigEndian(data[9..]));
+ }
+
+ return new Av1LayeredImageIndex(
+ BinaryPrimitives.ReadUInt16BigEndian(data[1..]),
+ BinaryPrimitives.ReadUInt16BigEndian(data[3..]),
+ BinaryPrimitives.ReadUInt16BigEndian(data[5..]));
+ }
+
///
/// Parses a clean-aperture crop description.
///
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs
new file mode 100644
index 000000000..50ca1d93a
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif;
+using SixLabors.ImageSharp.Formats.Heif.Av1;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Heif;
+
+///
+/// Verifies parsing and interpretation of HEIF image-item property payloads.
+///
+[Trait("Format", "Heif")]
+public class HeifPropertyParserTests
+{
+ ///
+ /// Verifies that every AV1 operating-point index representable by a sequence header is accepted.
+ ///
+ /// The zero-based operating-point index.
+ [Theory]
+ [InlineData(0)]
+ [InlineData(31)]
+ public void ParseAv1OperatingPointSelectorAcceptsSequenceHeaderRange(byte index)
+ {
+ Av1OperatingPointSelector selector = HeifPropertyParser.ParseAv1OperatingPointSelector([index]);
+
+ Assert.Equal(index, selector.Index);
+ }
+
+ ///
+ /// Verifies that an AV1 operating-point index outside the sequence-header range is rejected.
+ ///
+ [Fact]
+ public void ParseAv1OperatingPointSelectorRejectsOutOfRangeIndex()
+ => Assert.Throws(() => HeifPropertyParser.ParseAv1OperatingPointSelector([32]));
+
+ ///
+ /// Verifies the four AV1 spatial-layer identifiers and the progressive final-layer selector.
+ ///
+ /// The most-significant selector byte.
+ /// The least-significant selector byte.
+ /// The parsed layer identifier.
+ [Theory]
+ [InlineData(0, 0, 0)]
+ [InlineData(0, 3, 3)]
+ [InlineData(255, 255, Av1LayerSelector.AllLayers)]
+ public void ParseAv1LayerSelectorAcceptsDefinedValues(byte highByte, byte lowByte, ushort expected)
+ {
+ Av1LayerSelector selector = HeifPropertyParser.ParseAv1LayerSelector([highByte, lowByte]);
+
+ Assert.Equal(expected, selector.LayerId);
+ }
+
+ ///
+ /// Verifies that an AV1 spatial-layer identifier wider than the OBU extension field is rejected.
+ ///
+ [Fact]
+ public void ParseAv1LayerSelectorRejectsOutOfRangeLayer()
+ => Assert.Throws(() => HeifPropertyParser.ParseAv1LayerSelector([0, 4]));
+
+ ///
+ /// Verifies the compact 16-bit representation of the three explicit layered-image boundaries.
+ ///
+ [Fact]
+ public void ParseAv1LayeredImageIndexReadsSmallSizes()
+ {
+ Av1LayeredImageIndex index = HeifPropertyParser.ParseAv1LayeredImageIndex([0, 0, 55, 0, 17, 1, 2]);
+
+ Assert.Equal(55U, index.FirstLayerSize);
+ Assert.Equal(17U, index.SecondLayerSize);
+ Assert.Equal(258U, index.ThirdLayerSize);
+ }
+
+ ///
+ /// Verifies the 32-bit representation used when a layered-image boundary exceeds 16 bits.
+ ///
+ [Fact]
+ public void ParseAv1LayeredImageIndexReadsLargeSizes()
+ {
+ Av1LayeredImageIndex index = HeifPropertyParser.ParseAv1LayeredImageIndex(
+ [1, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0]);
+
+ Assert.Equal(65536U, index.FirstLayerSize);
+ Assert.Equal(131072U, index.SecondLayerSize);
+ Assert.Equal(196608U, index.ThirdLayerSize);
+ }
+
+ ///
+ /// Verifies that reserved layered-image flag bits cannot alter the payload interpretation.
+ ///
+ [Fact]
+ public void ParseAv1LayeredImageIndexRejectsReservedBits()
+ => Assert.Throws(() => HeifPropertyParser.ParseAv1LayeredImageIndex([2, 0, 1, 0, 0, 0, 0]));
+
+ ///
+ /// Verifies cumulative selection through the two-layer payload used by the libavif progressive fixture.
+ ///
+ /// The selected spatial layer.
+ /// The cumulative payload length through that layer.
+ [Theory]
+ [InlineData(0, 55)]
+ [InlineData(1, 72)]
+ [InlineData(Av1LayerSelector.AllLayers, 72)]
+ public void GetPayloadLengthSelectsCumulativeLayerBytes(ushort layerId, int expectedLength)
+ {
+ Av1LayeredImageIndex index = new(55, 0, 0);
+ Av1LayerSelector selector = new(layerId);
+
+ Assert.Equal(expectedLength, index.GetPayloadLength(72, selector));
+ }
+
+ ///
+ /// Verifies that a selector cannot address a layer absent from the indexed item payload.
+ ///
+ [Fact]
+ public void GetPayloadLengthRejectsAbsentLayer()
+ {
+ Av1LayeredImageIndex index = new(55, 0, 0);
+ Av1LayerSelector selector = new(2);
+
+ Assert.Throws(() => index.GetPayloadLength(72, selector));
+ }
+
+ ///
+ /// Verifies that every explicit layer boundary leaves bytes for the following implicit layer.
+ ///
+ [Fact]
+ public void GetPayloadLengthRejectsBoundaryAtItemEnd()
+ {
+ Av1LayeredImageIndex index = new(72, 0, 0);
+
+ Assert.Throws(() => index.GetPayloadLength(72, null));
+ }
+}