From b7f694258792036bae117c73e251e9db3dfedb08 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 14:53:25 +1000 Subject: [PATCH] Parse HEIF sequence sample dependencies --- HEIF_IMPLEMENTATION_PLAN.md | 4 +- src/ImageSharp/Formats/Heif/Heif4CharCode.cs | 5 + src/ImageSharp/Formats/Heif/Heif4CharCode.tt | 1 + .../Formats/Heif/HeifSequenceParser.cs | 439 ++++++++++++++++++ .../Formats/Heif/HeifSequenceSample.cs | 15 + .../Formats/Heif/HeifSequenceTrack.cs | 5 + .../Formats/Heif/HeifSequenceParserTests.cs | 78 +++- 7 files changed, 543 insertions(+), 4 deletions(-) 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);