Browse Source

Parse layered AV1 image properties

pull/2633/head
James Jackson-South 6 days ago
parent
commit
32d5e3b513
  1. 9
      HEIF_IMPLEMENTATION_PLAN.md
  2. 10
      src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
  3. 21
      src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs
  4. 97
      src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs
  5. 16
      src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs
  6. 28
      src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
  7. 15
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  8. 3
      src/ImageSharp/Formats/Heif/Heif4CharCode.tt
  9. 110
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  10. 18
      src/ImageSharp/Formats/Heif/HeifItem.cs
  11. 77
      src/ImageSharp/Formats/Heif/HeifPropertyParser.cs
  12. 133
      tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs

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

10
src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs

@ -21,6 +21,16 @@ internal static class Av1Constants
/// </summary>
public const int LevelBits = 5;
/// <summary>
/// The maximum number of operating points declared by one AV1 sequence header.
/// </summary>
public const int MaxOperatingPointCount = 32;
/// <summary>
/// The maximum number of spatial layers identified by an AV1 OBU extension header.
/// </summary>
public const int MaxSpatialLayerCount = 4;
/// <summary>
/// The number of bits used to signal a super-resolution denominator offset.
/// </summary>

21
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;
/// <summary>
/// Identifies the AV1 spatial layer selected by an AVIF image item.
/// </summary>
/// <param name="layerId">The spatial-layer identifier, or <see cref="AllLayers"/> for progressive or final-layer decoding.</param>
internal readonly struct Av1LayerSelector(ushort layerId)
{
/// <summary>
/// The layer identifier that selects progressive exposure or the final layer rather than one specific spatial layer.
/// </summary>
public const ushort AllLayers = ushort.MaxValue;
/// <summary>
/// Gets the spatial-layer identifier, or <see cref="AllLayers"/> when no individual layer is selected.
/// </summary>
public ushort LayerId { get; } = layerId;
}

97
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;
/// <summary>
/// Describes the explicit payload sizes that delimit the first three layers of a layered AV1 image item.
/// </summary>
/// <param name="firstLayerSize">The first layer size in bytes.</param>
/// <param name="secondLayerSize">The second layer size in bytes.</param>
/// <param name="thirdLayerSize">The third layer size in bytes.</param>
internal readonly struct Av1LayeredImageIndex(uint firstLayerSize, uint secondLayerSize, uint thirdLayerSize)
{
/// <summary>
/// Gets the first layer size in bytes.
/// </summary>
public uint FirstLayerSize { get; } = firstLayerSize;
/// <summary>
/// Gets the second layer size in bytes.
/// </summary>
public uint SecondLayerSize { get; } = secondLayerSize;
/// <summary>
/// Gets the third layer size in bytes.
/// </summary>
public uint ThirdLayerSize { get; } = thirdLayerSize;
/// <summary>
/// Gets the number of item bytes needed to decode the selected spatial layer.
/// </summary>
/// <param name="itemSize">The complete logical image-item payload size.</param>
/// <param name="selector">The requested spatial layer, or <see langword="null"/> to decode the final layer.</param>
/// <returns>The cumulative payload size through the selected layer, or the complete item size for final-layer decoding.</returns>
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;
}
}

16
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;
/// <summary>
/// Identifies the AV1 sequence-header operating point selected by an AVIF image item.
/// </summary>
/// <param name="index">The zero-based operating-point index.</param>
internal readonly struct Av1OperatingPointSelector(byte index)
{
/// <summary>
/// Gets the zero-based operating-point index.
/// </summary>
public byte Index { get; } = index;
}

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

@ -44,15 +44,16 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlpha
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Span<byte> itemData = GetItemData(item, data);
Av1CodecConfiguration codecConfiguration = ValidateItemData(
options,
item,
data,
itemData,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
using Av1Decoder decoder = new(options.Configuration);
Image<TPixel> image = decoder.Decode<TPixel>(data, colorProfile, codecConfiguration);
Image<TPixel> image = decoder.Decode<TPixel>(itemData, colorProfile, codecConfiguration);
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = this.CompressionMethod;
metadata.BitDepth = codecConfiguration.BitDepth;
@ -74,7 +75,8 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlpha
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Av1CodecConfiguration codecConfiguration = ValidateItemData(options, item, data, out _, out _);
Span<byte> 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<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlpha
using Av1Decoder decoder = new(options.Configuration);
decoder.DecodeAlpha(
data,
itemData,
item.CicpProfile,
codecConfiguration,
default,
@ -92,6 +94,24 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlpha
premultiplied);
}
/// <summary>
/// Gets the cumulative item bytes required by an explicit AV1 spatial-layer selection.
/// </summary>
/// <param name="item">The AV1 image item containing optional layered-image properties.</param>
/// <param name="data">The complete logical image-item payload.</param>
/// <returns>The complete payload for final-layer decoding, or the cumulative prefix through the selected layer.</returns>
private static Span<byte> GetItemData(HeifItem item, Span<byte> data)
{
Av1LayeredImageIndex? layeredImageIndex = item.Av1LayeredImageIndex;
if (layeredImageIndex is null)
{
return data;
}
int payloadLength = layeredImageIndex.Value.GetPayloadLength(data.Length, item.Av1LayerSelector);
return data[..payloadLength];
}
/// <summary>
/// Validates an AV1 item description against its encoded payload and returns the required codec configuration.
/// </summary>

15
src/ImageSharp/Formats/Heif/Heif4CharCode.cs

@ -248,6 +248,21 @@ public enum Heif4CharCode : uint
/// </summary>
Av1C = 0x61763143U,
/// <summary>
/// AV1 operating-point selector.
/// </summary>
A1op = 0x61316F70U,
/// <summary>
/// AV1 layer selector.
/// </summary>
Lsel = 0x6C73656CU,
/// <summary>
/// AV1 layered-image indexing.
/// </summary>
A1lx = 0x61316C78U,
/// <summary>
/// Image Mirror.
/// </summary>

3
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",

110
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, object>(
Heif4CharCode.A1op,
HeifPropertyParser.ParseAv1OperatingPointSelector(boxBuffer)));
break;
case Heif4CharCode.Lsel:
properties.Add(
new KeyValuePair<Heif4CharCode, object>(
Heif4CharCode.Lsel,
HeifPropertyParser.ParseAv1LayerSelector(boxBuffer)));
break;
case Heif4CharCode.A1lx:
properties.Add(
new KeyValuePair<Heif4CharCode, object>(
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)

18
src/ImageSharp/Formats/Heif/HeifItem.cs

@ -109,6 +109,24 @@ internal class HeifItem(Heif4CharCode type, uint id)
/// </summary>
public Av1CodecConfiguration? Av1CodecConfiguration { get; set; }
/// <summary>
/// Gets or sets the operating-point selector associated with this AV1 image item, or <see langword="null"/> when
/// the item uses the default operating-point index zero.
/// </summary>
public Av1OperatingPointSelector? Av1OperatingPointSelector { get; set; }
/// <summary>
/// Gets or sets the spatial-layer selector associated with this AV1 image item, or <see langword="null"/> when
/// no explicit layer selection is present.
/// </summary>
public Av1LayerSelector? Av1LayerSelector { get; set; }
/// <summary>
/// Gets or sets the layered-image payload index associated with this AV1 image item, or <see langword="null"/>
/// when the payload does not provide explicit layer boundaries.
/// </summary>
public Av1LayeredImageIndex? Av1LayeredImageIndex { get; set; }
/// <summary>
/// Gets or sets the HEVC codec configuration associated with this coded image item, or <see langword="null"/>
/// when the item has no HEVC codec-configuration property.

77
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);
}
/// <summary>
/// Parses the sequence-header operating point selected by an AV1 image item.
/// </summary>
/// <param name="data">The complete AV1 operating-point-selector payload.</param>
/// <returns>The selected zero-based operating-point index.</returns>
public static Av1OperatingPointSelector ParseAv1OperatingPointSelector(ReadOnlySpan<byte> 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);
}
/// <summary>
/// Parses the spatial layer selected by an AV1 image item.
/// </summary>
/// <param name="data">The complete AV1 layer-selector payload.</param>
/// <returns>The selected spatial-layer identifier.</returns>
public static Av1LayerSelector ParseAv1LayerSelector(ReadOnlySpan<byte> 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);
}
/// <summary>
/// Parses the explicit payload boundaries of a layered AV1 image item.
/// </summary>
/// <param name="data">The complete AV1 layered-image-indexing payload.</param>
/// <returns>The three explicit layer sizes.</returns>
public static Av1LayeredImageIndex ParseAv1LayeredImageIndex(ReadOnlySpan<byte> 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..]));
}
/// <summary>
/// Parses a clean-aperture crop description.
/// </summary>

133
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;
/// <summary>
/// Verifies parsing and interpretation of HEIF image-item property payloads.
/// </summary>
[Trait("Format", "Heif")]
public class HeifPropertyParserTests
{
/// <summary>
/// Verifies that every AV1 operating-point index representable by a sequence header is accepted.
/// </summary>
/// <param name="index">The zero-based operating-point index.</param>
[Theory]
[InlineData(0)]
[InlineData(31)]
public void ParseAv1OperatingPointSelectorAcceptsSequenceHeaderRange(byte index)
{
Av1OperatingPointSelector selector = HeifPropertyParser.ParseAv1OperatingPointSelector([index]);
Assert.Equal(index, selector.Index);
}
/// <summary>
/// Verifies that an AV1 operating-point index outside the sequence-header range is rejected.
/// </summary>
[Fact]
public void ParseAv1OperatingPointSelectorRejectsOutOfRangeIndex()
=> Assert.Throws<InvalidImageContentException>(() => HeifPropertyParser.ParseAv1OperatingPointSelector([32]));
/// <summary>
/// Verifies the four AV1 spatial-layer identifiers and the progressive final-layer selector.
/// </summary>
/// <param name="highByte">The most-significant selector byte.</param>
/// <param name="lowByte">The least-significant selector byte.</param>
/// <param name="expected">The parsed layer identifier.</param>
[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);
}
/// <summary>
/// Verifies that an AV1 spatial-layer identifier wider than the OBU extension field is rejected.
/// </summary>
[Fact]
public void ParseAv1LayerSelectorRejectsOutOfRangeLayer()
=> Assert.Throws<InvalidImageContentException>(() => HeifPropertyParser.ParseAv1LayerSelector([0, 4]));
/// <summary>
/// Verifies the compact 16-bit representation of the three explicit layered-image boundaries.
/// </summary>
[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);
}
/// <summary>
/// Verifies the 32-bit representation used when a layered-image boundary exceeds 16 bits.
/// </summary>
[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);
}
/// <summary>
/// Verifies that reserved layered-image flag bits cannot alter the payload interpretation.
/// </summary>
[Fact]
public void ParseAv1LayeredImageIndexRejectsReservedBits()
=> Assert.Throws<InvalidImageContentException>(() => HeifPropertyParser.ParseAv1LayeredImageIndex([2, 0, 1, 0, 0, 0, 0]));
/// <summary>
/// Verifies cumulative selection through the two-layer payload used by the libavif progressive fixture.
/// </summary>
/// <param name="layerId">The selected spatial layer.</param>
/// <param name="expectedLength">The cumulative payload length through that layer.</param>
[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));
}
/// <summary>
/// Verifies that a selector cannot address a layer absent from the indexed item payload.
/// </summary>
[Fact]
public void GetPayloadLengthRejectsAbsentLayer()
{
Av1LayeredImageIndex index = new(55, 0, 0);
Av1LayerSelector selector = new(2);
Assert.Throws<InvalidImageContentException>(() => index.GetPayloadLength(72, selector));
}
/// <summary>
/// Verifies that every explicit layer boundary leaves bytes for the following implicit layer.
/// </summary>
[Fact]
public void GetPayloadLengthRejectsBoundaryAtItemEnd()
{
Av1LayeredImageIndex index = new(72, 0, 0);
Assert.Throws<InvalidImageContentException>(() => index.GetPayloadLength(72, null));
}
}
Loading…
Cancel
Save