diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 5b261f806..be5948ef0 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -55,7 +55,9 @@ Checkboxes may be marked complete only when the implementation and the verificat
- Release verification passes the libavif-shaped two-sample parser fixture, the one-frame retention boundary, and a sample offset/length beyond the file. The parser performs no per-entry allocation and does not buffer `moov`, `mdat`, or complete attacker-sized tables.
- [x] Parse HEVC composition offsets and hidden samples from `ctts`/`cslg`, while rejecting the `ctts` box prohibited for AV1 tracks.
- Release verification covers signed and unsigned composition-offset syntax, hidden-sample visibility, composition-time calculation, required `cslg` and edit-list signaling, complete run counts, and the AV1 prohibition without buffering either table.
- - [ ] Parse optional direct dependencies from `refs` sample groups, track presentation/color/HDR properties, and bounded sequence metadata items.
+ - [x] Parse optional direct dependencies from `refs` sample groups into a compact retained-sample dependency graph.
+ - The parser validates complete `sbgp` runs and `sgpd` entry lengths, versions, indices, positive identifier uniqueness, decode order, sync-sample independence, and `ccst` reference limits. It uses pooled value-type sort indexes plus one exact flat reference array, with no per-sample collections or attacker-sized table materialization.
+ - [ ] Parse track presentation/color/HDR properties and bounded sequence metadata items.
- [ ] Connect the parsed sequence index to HEIF detection, Identify, frame decode, alpha matching, and frame metadata without changing still-image source selection.
- [ ] Write the same bounded movie, track, sample-description, location, dependency, timing, repetition, alpha, and metadata syntax from ImageSharp frames.
- [ ] Decode frame dependencies, durations, repetition, frame-local auxiliary images, and frame-local metadata into the existing ImageSharp multi-frame model.
diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
index d7205315e..6dab5cb5a 100644
--- a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
+++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
@@ -143,6 +143,11 @@ public enum Heif4CharCode : uint
///
Sbgp = 0x73626770U,
+ ///
+ /// Direct reference samples group type.
+ ///
+ Refs = 0x72656673U,
+
///
/// Image sequence coding constraints.
///
diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
index f013b50f9..8f1d43526 100644
--- a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
+++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
@@ -33,6 +33,7 @@
"stss", "Image sequence sync samples",
"sgpd", "Image sequence sample group descriptions",
"sbgp", "Image sequence sample-to-group map",
+ "refs", "Direct reference samples group type",
"ccst", "Image sequence coding constraints",
"auxi", "Auxiliary image sequence type",
"url ", "Self-contained data location",
diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
index 949087d43..592f6a25b 100644
--- a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
+++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
@@ -647,6 +647,8 @@ internal sealed class HeifSequenceParser
BoxReference syncSamples = default;
BoxReference compositionOffsets = default;
BoxReference compositionToDecode = default;
+ BoxReference sampleGroupDescriptions = default;
+ BoxReference sampleToGroup = default;
while (stream.Position < tableEnd)
{
@@ -681,6 +683,20 @@ internal sealed class HeifSequenceParser
break;
case Heif4CharCode.Cslg:
SetUnique(ref compositionToDecode, childStart, childLength, "sample table", childType);
+ break;
+ case Heif4CharCode.Sgpd:
+ if (ReadSampleGroupType(stream, childLength, scratch) == Heif4CharCode.Refs)
+ {
+ SetUnique(ref sampleGroupDescriptions, childStart, childLength, "sample table", childType);
+ }
+
+ break;
+ case Heif4CharCode.Sbgp:
+ if (ReadSampleGroupType(stream, childLength, scratch) == Heif4CharCode.Refs)
+ {
+ SetUnique(ref sampleToGroup, childStart, childLength, "sample table", childType);
+ }
+
break;
}
@@ -735,6 +751,16 @@ internal sealed class HeifSequenceParser
}
}
+ if (sampleGroupDescriptions.IsPresent != sampleToGroup.IsPresent)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group is missing its description or sample map.");
+ }
+
+ if (sampleGroupDescriptions.IsPresent)
+ {
+ this.ParseDirectReferences(stream, sampleGroupDescriptions, sampleToGroup, track, scratch);
+ }
+
if (compositionOffsets.IsPresent)
{
if (track.CodecType == Heif4CharCode.Av01)
@@ -1322,6 +1348,347 @@ internal sealed class HeifSequenceParser
}
}
+ ///
+ /// Resolves the image-specific direct-reference sample group into compact zero-based sample indices.
+ ///
+ /// The seekable source stream.
+ /// The validated direct-reference group-description payload.
+ /// The validated direct-reference sample-map payload.
+ /// The selected image track receiving its dependency graph.
+ /// The parser-owned reusable scratch span.
+ private void ParseDirectReferences(
+ Stream stream,
+ BoxReference descriptions,
+ BoxReference sampleMap,
+ HeifSequenceTrack track,
+ Span scratch)
+ {
+ using IMemoryOwner assignmentOwner = this.allocator.Allocate(track.Samples.Length);
+ Span assignments = assignmentOwner.GetSpan()[..track.Samples.Length];
+ stream.Position = sampleMap.Offset;
+ uint greatestGroupIndex = ParseSampleToGroup(stream, sampleMap.Length, track, assignments, scratch);
+
+ // Sorting the retained value-type assignments lets each group description be applied in one sequential pass.
+ // This avoids a dictionary and prevents attacker-controlled group counts from causing quadratic lookup work.
+ assignments.Sort();
+ stream.Position = descriptions.Offset;
+ int directReferenceCount = ParseDirectReferenceDescriptions(
+ stream,
+ descriptions.Length,
+ greatestGroupIndex,
+ track,
+ assignments,
+ [],
+ false,
+ scratch);
+
+ using IMemoryOwner sampleIdOwner = this.allocator.Allocate(track.Samples.Length);
+ Span sampleIds = sampleIdOwner.GetSpan()[..track.Samples.Length];
+ int sampleIdCount = 0;
+ Span samples = track.Samples;
+ for (int i = 0; i < samples.Length; i++)
+ {
+ uint sampleId = samples[i].SampleId;
+ if (sampleId != 0)
+ {
+ sampleIds[sampleIdCount++] = new SampleIdIndexEntry(sampleId, i);
+ }
+ }
+
+ sampleIds = sampleIds[..sampleIdCount];
+ sampleIds.Sort();
+ for (int i = 1; i < sampleIds.Length; i++)
+ {
+ if (sampleIds[i - 1].Id == sampleIds[i].Id)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group contains a duplicate positive sample identifier.");
+ }
+ }
+
+ if (directReferenceCount == 0)
+ {
+ return;
+ }
+
+ track.DirectReferenceSampleIndices = new int[directReferenceCount];
+ stream.Position = descriptions.Offset;
+ _ = ParseDirectReferenceDescriptions(
+ stream,
+ descriptions.Length,
+ greatestGroupIndex,
+ track,
+ assignments,
+ sampleIds,
+ true,
+ scratch);
+
+ if (track.AllReferencePicturesIntra)
+ {
+ ReadOnlySpan referenceIndices = track.DirectReferenceSampleIndices;
+ for (int i = 0; i < referenceIndices.Length; i++)
+ {
+ if (samples[referenceIndices[i]].DirectReferenceCount != 0)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group contradicts its all-reference-pictures-intra constraint.");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Expands the run-length sample-to-group map only for samples retained by the decoder.
+ ///
+ /// The stream positioned at the sample-to-group payload.
+ /// The validated sample-to-group payload length.
+ /// The selected track whose complete sample count is validated.
+ /// The exact retained assignment span.
+ /// The parser-owned reusable scratch span.
+ /// The greatest group-description index used by any declared sample.
+ private static uint ParseSampleToGroup(
+ Stream stream,
+ long boxLength,
+ HeifSequenceTrack track,
+ Span assignments,
+ Span scratch)
+ {
+ long payloadStart = stream.Position;
+ ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-to-group");
+ byte version = prefix[0];
+ if (version is not 0 and not 1 || ReadFlags(prefix) != 0
+ || (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != Heif4CharCode.Refs)
+ {
+ throw new InvalidImageContentException("The direct-reference sample-to-group box has an unsupported version, flags, or grouping type.");
+ }
+
+ int headerLength = version == 0 ? 12 : 16;
+ stream.Position = payloadStart;
+ prefix = ReadPrefix(stream, boxLength, scratch, headerLength, "sample-to-group");
+ if (version == 1 && BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]) != 0)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group has a nonzero grouping-type parameter.");
+ }
+
+ int entryCountOffset = version == 0 ? 8 : 12;
+ uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[entryCountOffset..]);
+ long entryBytes = checked((long)entryCount * 8);
+ if (entryCount == 0 || boxLength != headerLength + entryBytes)
+ {
+ throw new InvalidImageContentException("The direct-reference sample map is empty or has an invalid length.");
+ }
+
+ TableReader reader = new(stream, entryBytes, scratch, "direct-reference sample map");
+ ulong describedSamples = 0;
+ int retainedOffset = 0;
+ uint greatestGroupIndex = 0;
+ for (uint i = 0; i < entryCount; i++)
+ {
+ uint sampleCount = reader.ReadUInt32();
+ uint groupDescriptionIndex = reader.ReadUInt32();
+ if (sampleCount == 0)
+ {
+ throw new InvalidImageContentException("The direct-reference sample map contains a zero-length run.");
+ }
+
+ describedSamples = checked(describedSamples + sampleCount);
+ greatestGroupIndex = Math.Max(greatestGroupIndex, groupDescriptionIndex);
+ int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), assignments.Length - retainedOffset);
+ for (int j = 0; j < retainedRun; j++)
+ {
+ assignments[retainedOffset + j] = new SampleGroupAssignment(groupDescriptionIndex, retainedOffset + j);
+ }
+
+ retainedOffset += retainedRun;
+ }
+
+ if (describedSamples != track.TotalSampleCount || retainedOffset != assignments.Length)
+ {
+ throw new InvalidImageContentException("The direct-reference sample map does not describe every sample.");
+ }
+
+ return greatestGroupIndex;
+ }
+
+ ///
+ /// Parses direct-reference descriptions, first sizing and then resolving the retained dependency graph.
+ ///
+ /// The stream positioned at the sample-group-description payload.
+ /// The validated sample-group-description payload length.
+ /// The greatest description index used by the complete sample map.
+ /// The selected track receiving sample identifiers and dependency indices.
+ /// The retained sample assignments sorted by group-description index.
+ /// The sorted positive sample identifiers, or an empty span during the sizing pass.
+ /// Whether this pass resolves reference identifiers into compact sample indices.
+ /// The parser-owned reusable scratch span.
+ /// The exact number of retained direct-reference indices.
+ private static int ParseDirectReferenceDescriptions(
+ Stream stream,
+ long boxLength,
+ uint greatestGroupIndex,
+ HeifSequenceTrack track,
+ ReadOnlySpan assignments,
+ ReadOnlySpan sampleIds,
+ bool resolveReferences,
+ Span scratch)
+ {
+ long payloadStart = stream.Position;
+ ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-group descriptions");
+ byte version = prefix[0];
+ if (version is not 1 and not 2 || ReadFlags(prefix) != 0
+ || (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != Heif4CharCode.Refs)
+ {
+ throw new InvalidImageContentException("The direct-reference sample-group-description box has an unsupported version, flags, or grouping type.");
+ }
+
+ int headerLength = version == 1 ? 16 : 20;
+ stream.Position = payloadStart;
+ prefix = ReadPrefix(stream, boxLength, scratch, headerLength, "sample-group descriptions");
+ uint defaultLength = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]);
+ uint defaultGroupIndex = version == 2 ? BinaryPrimitives.ReadUInt32BigEndian(prefix[12..]) : 0;
+ int entryCountOffset = version == 1 ? 12 : 16;
+ uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[entryCountOffset..]);
+ if (greatestGroupIndex > entryCount || defaultGroupIndex > entryCount)
+ {
+ throw new InvalidImageContentException("The direct-reference sample map uses an undefined group-description index.");
+ }
+
+ long entryBytes = boxLength - headerLength;
+ TableReader reader = new(stream, entryBytes, scratch, "direct-reference descriptions");
+ long consumedBytes = 0;
+ int assignmentOffset = 0;
+ int directReferenceCount = 0;
+ while (assignmentOffset < assignments.Length && assignments[assignmentOffset].GroupDescriptionIndex == 0)
+ {
+ assignmentOffset++;
+ }
+
+ for (uint entry = 0; entry < entryCount; entry++)
+ {
+ uint descriptionIndex = entry + 1;
+ uint descriptionLength = defaultLength;
+ if (descriptionLength == 0)
+ {
+ if (entryBytes - consumedBytes < 4)
+ {
+ throw new InvalidImageContentException("The direct-reference sample-group description is truncated.");
+ }
+
+ descriptionLength = reader.ReadUInt32();
+ consumedBytes += 4;
+ }
+
+ if (descriptionLength < 5 || descriptionLength > entryBytes - consumedBytes)
+ {
+ throw new InvalidImageContentException("The direct-reference sample-group description has an invalid length.");
+ }
+
+ uint sampleId = reader.ReadUInt32();
+ byte referenceCount = reader.ReadByte();
+ uint requiredLength = 5U + ((uint)referenceCount * 4U);
+ if (descriptionLength != requiredLength)
+ {
+ throw new InvalidImageContentException("The direct-reference sample-group entry has an invalid length.");
+ }
+
+ int firstAssignment = assignmentOffset;
+ while (assignmentOffset < assignments.Length && assignments[assignmentOffset].GroupDescriptionIndex == descriptionIndex)
+ {
+ int sampleIndex = assignments[assignmentOffset].SampleIndex;
+ ref HeifSequenceSample sample = ref track.Samples[sampleIndex];
+ if (!resolveReferences)
+ {
+ if (referenceCount > track.MaximumReferencesPerPicture || directReferenceCount > int.MaxValue - referenceCount)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group exceeds its coding constraints or supported size.");
+ }
+
+ sample.SampleId = sampleId;
+ sample.DirectReferenceOffset = directReferenceCount;
+ sample.DirectReferenceCount = referenceCount;
+ directReferenceCount += referenceCount;
+ }
+ else if (sample.SampleId != sampleId || sample.DirectReferenceCount != referenceCount)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group changed between parser passes.");
+ }
+
+ assignmentOffset++;
+ }
+
+ for (int reference = 0; reference < referenceCount; reference++)
+ {
+ uint referenceSampleId = reader.ReadUInt32();
+ if (referenceSampleId == 0)
+ {
+ throw new InvalidImageContentException("The direct-reference sample group contains identifier zero in a reference list.");
+ }
+
+ if (resolveReferences)
+ {
+ int low = 0;
+ int high = sampleIds.Length - 1;
+ while (low <= high)
+ {
+ int middle = low + ((high - low) >> 1);
+ uint candidate = sampleIds[middle].Id;
+ if (candidate < referenceSampleId)
+ {
+ low = middle + 1;
+ }
+ else if (candidate > referenceSampleId)
+ {
+ high = middle - 1;
+ }
+ else
+ {
+ low = middle;
+ break;
+ }
+ }
+
+ if (low >= sampleIds.Length || sampleIds[low].Id != referenceSampleId)
+ {
+ throw new InvalidImageContentException("A direct-reference sample identifier does not name a retained sample.");
+ }
+
+ int referencedSampleIndex = sampleIds[low].SampleIndex;
+ for (int assignment = firstAssignment; assignment < assignmentOffset; assignment++)
+ {
+ int sampleIndex = assignments[assignment].SampleIndex;
+ HeifSequenceSample sample = track.Samples[sampleIndex];
+ if (sample.IsSync || referencedSampleIndex >= sampleIndex)
+ {
+ throw new InvalidImageContentException("A direct-reference sample is not earlier in decode order or is attached to a sync sample.");
+ }
+
+ track.DirectReferenceSampleIndices[sample.DirectReferenceOffset + reference] = referencedSampleIndex;
+ }
+ }
+ }
+
+ consumedBytes += descriptionLength;
+ }
+
+ if (consumedBytes != entryBytes || assignmentOffset != assignments.Length)
+ {
+ throw new InvalidImageContentException("The direct-reference sample-group descriptions do not cover the retained sample map.");
+ }
+
+ return directReferenceCount;
+ }
+
+ ///
+ /// Reads the grouping type shared by a sample map or sample-group-description box.
+ ///
+ /// The stream positioned at the full-box payload.
+ /// The validated payload length.
+ /// The parser-owned reusable scratch span.
+ /// The declared grouping type.
+ private static Heif4CharCode ReadSampleGroupType(Stream stream, long boxLength, Span scratch)
+ {
+ ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample group");
+ return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
+ }
+
///
/// Parses HEVC decode-to-composition offsets and marks non-output reference samples.
///
@@ -1819,6 +2186,78 @@ internal sealed class HeifSequenceParser
public uint SamplesPerChunk { get; }
}
+ ///
+ /// Associates one retained sample with its one-based direct-reference group description.
+ ///
+ private readonly struct SampleGroupAssignment : IComparable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The one-based group-description index, or zero for no group.
+ /// The zero-based retained sample index.
+ public SampleGroupAssignment(uint groupDescriptionIndex, int sampleIndex)
+ {
+ this.GroupDescriptionIndex = groupDescriptionIndex;
+ this.SampleIndex = sampleIndex;
+ }
+
+ ///
+ /// Gets the one-based group-description index, or zero for no group.
+ ///
+ public uint GroupDescriptionIndex { get; }
+
+ ///
+ /// Gets the zero-based retained sample index.
+ ///
+ public int SampleIndex { get; }
+
+ ///
+ /// Compares this assignment with another assignment in group-description and sample order.
+ ///
+ /// The other assignment.
+ /// A value indicating the relative sort order.
+ public int CompareTo(SampleGroupAssignment other)
+ {
+ int result = this.GroupDescriptionIndex.CompareTo(other.GroupDescriptionIndex);
+ return result != 0 ? result : this.SampleIndex.CompareTo(other.SampleIndex);
+ }
+ }
+
+ ///
+ /// Maps one positive direct-reference identifier to its retained decode-order sample index.
+ ///
+ private readonly struct SampleIdIndexEntry : IComparable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The positive file-defined sample identifier.
+ /// The zero-based retained sample index.
+ public SampleIdIndexEntry(uint id, int sampleIndex)
+ {
+ this.Id = id;
+ this.SampleIndex = sampleIndex;
+ }
+
+ ///
+ /// Gets the positive file-defined sample identifier.
+ ///
+ public uint Id { get; }
+
+ ///
+ /// Gets the zero-based retained sample index.
+ ///
+ public int SampleIndex { get; }
+
+ ///
+ /// Compares this entry with another entry by sample identifier.
+ ///
+ /// The other identifier entry.
+ /// A value indicating the relative sort order.
+ public int CompareTo(SampleIdIndexEntry other) => this.Id.CompareTo(other.Id);
+ }
+
///
/// Contains the visible composition-offset range derived from a complete HEVC track.
///
diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs b/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs
index c5a8c2207..3353ba8eb 100644
--- a/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs
+++ b/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs
@@ -42,4 +42,19 @@ internal struct HeifSequenceSample
/// Gets or sets a value indicating whether decoding can begin at this sample.
///
public bool IsSync { get; set; }
+
+ ///
+ /// Gets or sets the positive identifier used when another retained sample directly references this sample.
+ ///
+ public uint SampleId { get; set; }
+
+ ///
+ /// Gets or sets the first direct reference in the owning track's compact reference-index array.
+ ///
+ public int DirectReferenceOffset { get; set; }
+
+ ///
+ /// Gets or sets the number of direct reference indices belonging to this sample.
+ ///
+ public byte DirectReferenceCount { get; set; }
}
diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs b/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs
index ced4d846d..f762fc745 100644
--- a/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs
+++ b/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs
@@ -83,6 +83,11 @@ internal sealed class HeifSequenceTrack
///
public HeifSequenceSample[] Samples { get; set; } = [];
+ ///
+ /// Gets or sets the compact zero-based sample indices referenced by retained samples.
+ ///
+ public int[] DirectReferenceSampleIndices { get; set; } = [];
+
///
/// Gets or sets the identifier of the master track served by this auxiliary track, or zero for a master track.
///
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs
index 54634b9f7..78cfc197c 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs
@@ -108,7 +108,41 @@ public class HeifSequenceParserTests
Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data)));
}
- private static byte[] CreateSequenceFile(uint chunkOffset, bool hevc = false, bool compositionOffsets = false)
+ [Fact]
+ public void ParseResolvesDirectReferenceSamples()
+ {
+ byte[] data = CreateSequenceFile(1024, directReferences: true);
+ using MemoryStream stream = new(data, false);
+ HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 2);
+ stream.Position = 8;
+
+ HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data));
+
+ Assert.Equal(new[] { 0 }, sequence.ColorTrack.DirectReferenceSampleIndices);
+ Assert.Equal(1U, sequence.ColorTrack.Samples[0].SampleId);
+ Assert.Equal(0, sequence.ColorTrack.Samples[0].DirectReferenceCount);
+ Assert.Equal(0U, sequence.ColorTrack.Samples[1].SampleId);
+ Assert.Equal(0, sequence.ColorTrack.Samples[1].DirectReferenceOffset);
+ Assert.Equal(1, sequence.ColorTrack.Samples[1].DirectReferenceCount);
+ }
+
+ [Fact]
+ public void ParseRejectsUnknownDirectReferenceSampleId()
+ {
+ byte[] data = CreateSequenceFile(1024, directReferences: true, directReferenceSampleId: 2);
+ using MemoryStream stream = new(data, false);
+ HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 2);
+ stream.Position = 8;
+
+ Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data)));
+ }
+
+ private static byte[] CreateSequenceFile(
+ uint chunkOffset,
+ bool hevc = false,
+ bool compositionOffsets = false,
+ bool directReferences = false,
+ uint directReferenceSampleId = 1)
{
using MemoryStream stream = new();
using BinaryWriter writer = new(stream, Encoding.UTF8, true);
@@ -133,7 +167,7 @@ public class HeifSequenceParserTests
long mediaInformation = BeginBox(writer, Heif4CharCode.Minf);
WriteDataInformation(writer);
- WriteSampleTable(writer, chunkOffset, hevc, compositionOffsets);
+ WriteSampleTable(writer, chunkOffset, hevc, compositionOffsets, directReferences, directReferenceSampleId);
EndBox(writer, mediaInformation);
EndBox(writer, media);
EndBox(writer, track);
@@ -221,7 +255,13 @@ public class HeifSequenceParserTests
EndBox(writer, dataInformation);
}
- private static void WriteSampleTable(BinaryWriter writer, uint chunkOffset, bool hevc, bool compositionOffsets)
+ private static void WriteSampleTable(
+ BinaryWriter writer,
+ uint chunkOffset,
+ bool hevc,
+ bool compositionOffsets,
+ bool directReferences,
+ uint directReferenceSampleId)
{
long sampleTable = BeginBox(writer, Heif4CharCode.Stbl);
WriteSampleDescription(writer, hevc);
@@ -282,9 +322,41 @@ public class HeifSequenceParserTests
EndBox(writer, compositionToDecode);
}
+ if (directReferences)
+ {
+ WriteDirectReferenceSampleGroup(writer, directReferenceSampleId);
+ }
+
EndBox(writer, sampleTable);
}
+ private static void WriteDirectReferenceSampleGroup(BinaryWriter writer, uint directReferenceSampleId)
+ {
+ long descriptions = BeginBox(writer, Heif4CharCode.Sgpd);
+ WriteFullBoxHeader(writer, 1, 0);
+ WriteUInt32(writer, (uint)Heif4CharCode.Refs);
+ WriteUInt32(writer, 0);
+ WriteUInt32(writer, 2);
+ WriteUInt32(writer, 5);
+ WriteUInt32(writer, 1);
+ writer.Write((byte)0);
+ WriteUInt32(writer, 9);
+ WriteUInt32(writer, 0);
+ writer.Write((byte)1);
+ WriteUInt32(writer, directReferenceSampleId);
+ EndBox(writer, descriptions);
+
+ long sampleMap = BeginBox(writer, Heif4CharCode.Sbgp);
+ WriteFullBoxHeader(writer, 0, 0);
+ WriteUInt32(writer, (uint)Heif4CharCode.Refs);
+ WriteUInt32(writer, 2);
+ WriteUInt32(writer, 1);
+ WriteUInt32(writer, 1);
+ WriteUInt32(writer, 1);
+ WriteUInt32(writer, 2);
+ EndBox(writer, sampleMap);
+ }
+
private static void WriteSampleDescription(BinaryWriter writer, bool hevc)
{
long description = BeginBox(writer, Heif4CharCode.Stsd);