From 32d5e3b5139cc4ee05f65448a5d21b5d39f1bffd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Aug 2026 15:05:31 +1000 Subject: [PATCH] Parse layered AV1 image properties --- HEIF_IMPLEMENTATION_PLAN.md | 9 +- .../Formats/Heif/Av1/Av1Constants.cs | 10 ++ .../Formats/Heif/Av1/Av1LayerSelector.cs | 21 +++ .../Formats/Heif/Av1/Av1LayeredImageIndex.cs | 97 +++++++++++++ .../Heif/Av1/Av1OperatingPointSelector.cs | 16 +++ .../Formats/Heif/Av1HeifItemDecoder.cs | 28 +++- src/ImageSharp/Formats/Heif/Heif4CharCode.cs | 15 ++ src/ImageSharp/Formats/Heif/Heif4CharCode.tt | 3 + .../Formats/Heif/HeifDecoderCore.cs | 110 +++++++++++++++ src/ImageSharp/Formats/Heif/HeifItem.cs | 18 +++ .../Formats/Heif/HeifPropertyParser.cs | 77 ++++++++++ .../Formats/Heif/HeifPropertyParserTests.cs | 133 ++++++++++++++++++ 12 files changed, 532 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs 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)); + } +}