diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index cebc5b15f..54b1ae4aa 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -50,9 +50,9 @@ Checkboxes may be marked complete only when the implementation and the verificat - The bounded syntax matrix below is derived from ISO/IEC 23008-12:2022 clauses 7 through 10, the codec-specific HEVC Annex B, AVIF 1.2 section 3, AV1-ISOBMFF 1.2 section 2, and the pinned libavif `read.c` and `write.c` sequence paths. - [ ] Parse and write only that bounded image-sequence syntax; do not add audio, arbitrary media, editing, fragmentation, streaming, or general presentation APIs. - [x] Share one bounded 32/64-bit box-header reader between the existing item path and the image-sequence path, including short-read handling and parent-boundary validation. - - [x] Select one enabled `pict` master track without materializing unrelated tracks, then parse its `mvhd`/`tkhd`/`mdhd`/`hdlr`, self-contained `dref`, `av01` or `hvc1` sample entry, codec configuration, mandatory `ccst`, and bounded repetition edit. + - [x] Select one enabled `pict` master track without materializing unrelated tracks, accepting conforming image-sequence tracks that omit `track_in_movie`, then parse its `mvhd`/`tkhd`/`mdhd`/`hdlr`, self-contained `dref`, `av01` or `hvc1` sample entry, codec configuration, mandatory `ccst`, and bounded repetition edit. - [x] Resolve `stsc`, `stco`/`co64`, `stsz`/`stz2`, `stts`, and `stss` into one exact value-type descriptor array capped by `DecoderOptions.MaxFrames`; validate complete run/count syntax through one allocator-owned sequential scratch buffer. - - 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. + - Release verification passes the libavif-shaped two-sample parser fixture, the one-frame retention boundary, and a sample offset/length beyond the file. Pinned upstream libavif files independently verify 8-bit and 12-bit sequence identification, alpha-track and Exif/XMP retention, infinite and finite repetition, and safe omission of an unrelated audio track. The parser performs no per-entry allocation and does not buffer `moov`, `mdat`, unrelated track payloads, 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. - [x] Parse optional direct dependencies from `refs` sample groups into a compact retained-sample dependency graph. @@ -68,7 +68,7 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Match auxiliary alpha samples by exact decode duration, visibility, and presentation time, and validate premultiplication track identity. - [x] Require unity movie and track matrices so image presentation remains on the optimized `clap`/`irot`/`imir` path without a movie compositor. - [x] Apply the shared `DecoderOptions` contract consistently to still items, nested payload codecs, grids, metadata properties, and sequence samples. - - `Strict` rejects recoverable ancillary and image-data errors, `IgnoreAncillary` suppresses only ancillary failures, and `IgnoreImageData` additionally permits failed image properties or samples to be omitted. `SkipMetadata` avoids optional property and item-payload validation, while cancellation and the caller configuration flow into nested JPEG and AV1 decoders. Target scaling and ICC conversion remain presentation-level operations after item or grid composition. The focused Release matrix passes all 19 new still-image policy cases, all 3 new sequence-sample cases, and the complete 32-test sequence-parser suite; the Release test-project build completes with zero errors. + - `Strict` rejects recoverable ancillary and image-data errors, `IgnoreAncillary` suppresses only ancillary failures, and `IgnoreImageData` additionally permits failed image properties or samples to be omitted. `SkipMetadata` avoids optional property and item-payload validation, while cancellation and the caller configuration flow into nested JPEG and AV1 decoders. Target scaling and ICC conversion remain presentation-level operations after item or grid composition. The focused Release matrix passes all 19 new still-image policy cases, all 3 new sequence-sample cases, and the complete 37-test sequence-parser suite; the Release test-project build completes with zero errors. - [ ] Complete reference-dependent AV1 and HEVC sample reconstruction and independent sequence vectors. - [ ] 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. @@ -126,7 +126,7 @@ The sequence reader and writer may retain only the following syntax and the reso | Syntax | Required image behavior | | --- | --- | | `ftyp` sequence and structural brands | Recognize `avis` AV1 sequences and the non-layered `hevc`/`hevx` HEVC sequence profiles. `avio` can additionally signal an all-sync AV1 sequence. Layered `hevm`/`hevs`, JPEG `jpgs`, arbitrary video brands, and brands for unimplemented codecs remain unsupported until their image payload and presentation requirements are implemented. | -| `moov`/`mvhd`, `trak`/`tkhd`, and `mdia`/`mdhd`/`hdlr` | Select an enabled, in-movie `pict` master image-sequence track; retain its displayed dimensions, media time scale, media duration, and movie-time-scale track duration. Require unity movie and track matrices because arbitrary movie-canvas composition is outside image-format scope; use `clap`/`irot`/`imir` for supported image presentation. Ignore unrelated tracks rather than exposing them. | +| `moov`/`mvhd`, `trak`/`tkhd`, and `mdia`/`mdhd`/`hdlr` | Select an enabled `pict` master image-sequence track without requiring the optional `track_in_movie` flag; retain its displayed dimensions, media time scale, media duration, and movie-time-scale track duration. Require unity movie and track matrices because arbitrary movie-canvas composition is outside image-format scope; use `clap`/`irot`/`imir` for supported image presentation. Inspect unrelated tracks only far enough to identify and skip their non-image handlers. | | `minf`/`dinf`/`dref` and `stbl` | Accept only self-contained sample data references and own the bounded sample-table state for one selected image sequence plus an optional linked auxiliary-alpha sequence. No reusable data-reference, media-information, or sample-table API is created. | | `stsd` and one supported visual sample entry | Require exactly one `av01` entry for AVIF or one non-layered `hvc1` entry for HEIC. Retain only its dimensions, codec configuration (`av1C` or `hvcC`), image presentation/color/HDR properties, and mandatory version-zero `ccst` coding constraints. Reject an unsupported essential configuration rather than treating it as generic video. | | `stsc`, `stco`/`co64`, and `stsz`/`stz2` | Resolve each declared image sample directly to a validated file offset and length. Expand run tables once into a compact frame-owned descriptor array bounded by `DecoderOptions.MaxFrames`; never buffer the movie or complete `mdat`. | @@ -285,7 +285,7 @@ This assessment is based on the current source after the upstream ImageSharp mer ### Tests -- The repository contains HEIC, HIF, and AVIF assets, but only the legacy JPEG HIF path reaches a full reference-image comparison. +- The repository contains HEIC, HIF, and AVIF assets, but only the legacy JPEG HIF path reaches a full reference-image comparison. Pinned upstream libavif animations now verify real 8-bit, 12-bit, alpha, Exif/XMP, repetition, and unrelated-audio sequence parsing without claiming inter-frame pixel reconstruction. - HEVC fixtures are identified but not decoded, and there are no HEVC algorithm tests. - The strongest AV1 integration test now drives the single-still decoder through tile parsing, reconstruction, and pixel conversion, but only verifies non-zero output rather than independent reference pixels. - Several full-image, inverse-transform, entropy, and frame-header cases are disabled or commented out. diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs index 1424b3d8c..0c9e58db5 100644 --- a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -89,7 +89,7 @@ internal sealed class HeifSequenceParser { TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch); if (colorTrackId == 0 - && identity.IsEnabledInMovie + && identity.IsEnabled && identity.HandlerType == Heif4CharCode.Pict && identity.AuxiliaryForTrackId == 0) { @@ -201,9 +201,25 @@ internal sealed class HeifSequenceParser stream.Position = checked(childStart + childLength); } - if (!trackHeader.IsPresent || !media.IsPresent) + if (!media.IsPresent) + { + throw new InvalidImageContentException("A HEIF track is missing its media box."); + } + + stream.Position = media.Offset; + Heif4CharCode handlerType = ScanMediaHandler(stream, media.Length, scratch); + if (handlerType is not Heif4CharCode.Pict and not Heif4CharCode.Auxv) { - throw new InvalidImageContentException("A HEIF image-sequence track is missing its header or media box."); + // Non-image tracks are outside this parser's retained ISOBMFF surface. Do not impose image dimensions, + // matrices, or reference semantics on audio and other unrelated media carried by a valid image file. + TrackIdentity unrelatedIdentity = default; + unrelatedIdentity.HandlerType = handlerType; + return unrelatedIdentity; + } + + if (!trackHeader.IsPresent) + { + throw new InvalidImageContentException("A HEIF image-sequence track is missing its track header."); } stream.Position = trackHeader.Offset; @@ -214,8 +230,7 @@ internal sealed class HeifSequenceParser ParseTrackReferences(stream, trackReferences.Length, ref identity, scratch); } - stream.Position = media.Offset; - identity.HandlerType = ScanMediaHandler(stream, media.Length, scratch); + identity.HandlerType = handlerType; return identity; } @@ -351,7 +366,6 @@ internal sealed class HeifSequenceParser const int dimensionsLength = 2 * sizeof(uint); const int fixedPointFractionalBits = 16; const uint trackEnabledFlag = 1 << 0; - const uint trackInMovieFlag = 1 << 1; ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, fullBoxHeaderLength, "track header"); byte version = prefix[0]; @@ -418,13 +432,11 @@ internal sealed class HeifSequenceParser throw new NotSupportedException("The HEIF image-sequence track requires an unsupported movie presentation matrix."); } - // Bits 0 and 1 are track_enabled and track_in_movie respectively. A primary image-sequence candidate must - // participate in movie playback as well as being enabled; preview-only and disabled tracks remain available - // for explicit references but are not selected as the primary color track. - uint enabledInMovieFlags = trackEnabledFlag | trackInMovieFlag; - bool isEnabledInMovie = (flags & enabledInMovieFlags) == enabledInMovieFlags; + // ISO/IEC 14496-12 Section 8.3.2 assigns bit zero to track_enabled. Image sequences, including files written + // by libavif, do not require track_in_movie to be set, so only the enabled bit participates in selection. + bool isEnabled = (flags & trackEnabledFlag) != 0; - return new TrackIdentity(id, isEnabledInMovie, width, height, duration); + return new TrackIdentity(id, isEnabled, width, height, duration); } /// @@ -2384,14 +2396,14 @@ internal sealed class HeifSequenceParser /// Initializes a new instance of the struct. /// /// The file-defined track identifier. - /// Whether the track is enabled and used in the movie presentation. + /// Whether the track is enabled. /// The displayed track width. /// The displayed track height. /// The track duration in movie-time-scale units. - public TrackIdentity(uint id, bool isEnabledInMovie, int width, int height, ulong trackDuration) + public TrackIdentity(uint id, bool isEnabled, int width, int height, ulong trackDuration) { this.Id = id; - this.IsEnabledInMovie = isEnabledInMovie; + this.IsEnabled = isEnabled; this.Width = width; this.Height = height; this.TrackDuration = trackDuration; @@ -2406,9 +2418,9 @@ internal sealed class HeifSequenceParser public uint Id { get; } /// - /// Gets a value indicating whether the track is enabled and used in the movie presentation. + /// Gets a value indicating whether the track is enabled. /// - public bool IsEnabledInMovie { get; } + public bool IsEnabled { get; } /// /// Gets the displayed track width. diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs index babb08c76..5d12a13a9 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs @@ -5,21 +5,145 @@ using System.Buffers.Binary; using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.Formats.Heif; +/// +/// Verifies HEIF image-sequence parsing with upstream libavif files and narrowly constructed invalid containers. +/// [Trait("Format", "Heif")] [ValidateDisposedMemoryAllocations] public class HeifSequenceParserTests { + private const int BoxHeaderLength = 8; + private const int FileTypeBoxLength = 24; + private const int SyntheticFileLength = 2048; + private const uint SyntheticChunkOffset = 1024; + private const uint SyntheticMovieTimescale = 1000; + private const uint SyntheticTrackDuration = 600; + private const uint SyntheticMediaDuration = 200; + private const uint SyntheticSampleDuration = 100; + private const uint SyntheticSampleCount = 2; + private const int SyntheticWidth = 320; + private const int SyntheticHeight = 240; + private const int FirstSyntheticSampleLength = 10; + private const int SecondSyntheticSampleLength = 12; + private const uint ColorTrackId = 1; + private const uint AlphaTrackId = 2; + private const uint UnrelatedTrackId = 3; + private const uint MismatchedAlphaTimescale = 2000; + private const uint UnityFixed16Point16 = 1U << 16; + private const uint DoubleFixed16Point16 = 2U << 16; + private const uint UnityFixed2Point30 = 1U << 30; + private const ushort UnityFixed8Point8 = 1 << 8; + private const ushort PackedUndeterminedLanguage = 0x55C4; + private const uint SyntheticHorizontalResolution = 72U << 16; + private const ushort SyntheticPixelDepth = 24; + private const ushort SyntheticMaximumContentLightLevel = 1000; + private const ushort SyntheticMaximumFrameAverageLightLevel = 400; + private const uint SyntheticHorizontalPixelSpacing = 4; + private const uint SyntheticVerticalPixelSpacing = 3; + private const ushort TrackExifItemId = 1; + private const ushort TrackXmpItemId = 2; + private const int OrangeAv1ConfigurationOffset = 0xC7; + private const int OrangeAv1ConfigurationLength = 4; + private const int OrangeAv1SampleOffset = 0x10E; + private const int OrangeAv1SampleLength = 0x1D; private const int TrackExifOffset = 1800; private const int TrackXmpOffset = 1840; - + private const int LibavifAnimationFrameCount = 5; + private const int LibavifAnimationSize = 150; + private const int LibavifKeyframeAnimationSize = 64; + private const int FinitePlayCount = 1; + private const int InfinitePlayCount = 0; + private const byte InvalidAv1SampleByte = 0x80; + + /// + /// Gets the minimal little-endian TIFF payload stored in the synthetic track-level Exif item. + /// private static ReadOnlySpan TrackExifData => [0, 0, 0, 0, 0x49, 0x49, 0x2A, 0, 8, 0, 0, 0]; + /// + /// Gets the minimal XMP packet stored in the synthetic track-level MIME item. + /// private static ReadOnlySpan TrackXmpData => ""u8; + /// + /// Gets the minimal AV1CodecConfigurationBox payload for profile zero, level zero, and an absent initial + /// presentation-delay field. The high marker and version bits encode marker one and configuration version one. + /// + private static ReadOnlySpan DefaultAv1Configuration => [0x81, 0, 0, 0]; + + /// + /// Verifies that genuine libavif animation files are identified from their image-sequence tracks rather than + /// from the fallback primary item. The audio variant must produce the same image description because non-image + /// tracks are deliberately outside the decoder's retained ISOBMFF surface. + /// + /// The libavif animation fixture to identify. + [Theory] + [InlineData(TestImages.Heif.Animated8Bit)] + [InlineData(TestImages.Heif.Animated8BitWithAudio)] + public void IdentifyReadsRealLibavifSequence(string imagePath) + { + TestFile file = TestFile.Create(imagePath); + + ImageInfo info = Image.Identify(file.Bytes); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size); + Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.Equal(FinitePlayCount, metadata.RepeatCount); + Assert.False(metadata.HasAlpha); + } + + /// + /// Verifies that a genuine libavif animation carries its linked alpha track, infinite repetition, and metadata + /// items through the public sequence metadata boundary. + /// + [Fact] + public void IdentifyReadsRealLibavifSequenceWithAlphaAndMetadata() + { + TestFile file = TestFile.Create(TestImages.Heif.Animated8BitWithAlphaExifXmp); + + ImageInfo info = Image.Identify(file.Bytes); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size); + Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.Equal(InfinitePlayCount, metadata.RepeatCount); + Assert.True(metadata.HasAlpha); + Assert.NotNull(info.Metadata.ExifProfile); + Assert.NotNull(info.Metadata.XmpProfile); + } + + /// + /// Verifies that a genuine 12-bit libavif sequence with inter-frame dependencies is identified as all five frames + /// instead of falling back to its primary image item. + /// + [Fact] + public void IdentifyReadsReal12BitLibavifSequence() + { + TestFile file = TestFile.Create(TestImages.Heif.Animated12BitWithKeyframes); + + ImageInfo info = Image.Identify(file.Bytes); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(LibavifKeyframeAnimationSize, LibavifKeyframeAnimationSize), info.Size); + Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit12, metadata.BitDepth); + } + + /// + /// Verifies that identification transfers bounded sequence, track property, metadata-item, and frame-timing + /// information from the parser into the public image metadata model. + /// [Fact] public void IdentifyReturnsBoundedSequenceAndFrameMetadata() { @@ -28,7 +152,7 @@ public class HeifSequenceParserTests ImageInfo info = Image.Identify(data); HeifMetadata metadata = info.Metadata.GetHeifMetadata(); - Assert.Equal(new Size(240, 320), info.Size); + Assert.Equal(new Size(SyntheticHeight, SyntheticWidth), info.Size); Assert.Equal(2, info.FrameCount); Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); @@ -42,9 +166,13 @@ public class HeifSequenceParserTests Assert.Equal(3D, info.Metadata.VerticalResolution); Assert.All( info.FrameMetadataCollection, - frame => Assert.Equal(new Rational(1, 10), frame.GetHeifMetadata().FrameDelay)); + frame => Assert.Equal(new Rational(SyntheticSampleDuration, SyntheticMovieTimescale), frame.GetHeifMetadata().FrameDelay)); } + /// + /// Verifies that omits ancillary sequence profiles and item metadata + /// without discarding structural codec, repetition, or frame-timing information required to describe the image. + /// [Fact] public void IdentifySkipsAncillarySequenceMetadataWithoutDroppingImageMetadata() { @@ -65,14 +193,20 @@ public class HeifSequenceParserTests Assert.Null(metadata.ContentLightLevel); Assert.All( info.FrameMetadataCollection, - frame => Assert.Equal(new Rational(1, 10), frame.GetHeifMetadata().FrameDelay)); + frame => Assert.Equal(new Rational(SyntheticSampleDuration, SyntheticMovieTimescale), frame.GetHeifMetadata().FrameDelay)); } + /// + /// Verifies that two independently addressable AV1 samples become two complete ImageSharp frames with the + /// expected pixels and per-frame duration. + /// [Fact] public void DecodeAdoptsIndependentAv1SamplesAsImageFrames() { byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; - byte[] data = CreateDecodableAv1SequenceContainer(source.AsSpan(0x10E, 0x1D), source.AsSpan(0xC7, 4)); + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength)); using Image expected = Image.Load(source); using Image actual = Image.Load(data); @@ -81,32 +215,52 @@ public class HeifSequenceParserTests Assert.Equal(2, actual.Frames.Count); foreach (ImageFrame frame in actual.Frames) { - Assert.Equal(new Rational(1, 10), frame.Metadata.GetHeifMetadata().FrameDelay); + Assert.Equal( + new Rational(SyntheticSampleDuration, SyntheticMovieTimescale), + frame.Metadata.GetHeifMetadata().FrameDelay); for (int y = 0; y < frame.Height; y++) { - Assert.True(frame.PixelBuffer.DangerousGetRowSpan(y).SequenceEqual(expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))); + Assert.True( + frame.PixelBuffer.DangerousGetRowSpan(y) + .SequenceEqual(expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))); } } } + /// + /// Verifies that strict and ancillary-tolerant decoding both reject corrupt coded image data because neither + /// integrity mode permits recovery from errors in a retained AV1 sample. + /// + /// The segment-integrity policy applied at the decoder boundary. [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void DecodeRejectsInvalidAv1SampleUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) { byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; - byte[] data = CreateDecodableAv1SequenceContainer(source.AsSpan(0x10E, 0x1D), [0x80], source.AsSpan(0xC7, 4), false); + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + [InvalidAv1SampleByte], + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength), + false); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; Assert.Throws(() => Image.Load(options, data)); } + /// + /// Verifies that image-data tolerance drops an invalid non-root AV1 sample while preserving the valid frame. + /// [Fact] public void DecodeSkipsInvalidAv1SampleWhenImageDataErrorsAreIgnored() { byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; - byte[] data = CreateDecodableAv1SequenceContainer(source.AsSpan(0x10E, 0x1D), [0x80], source.AsSpan(0xC7, 4), false); + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + [InvalidAv1SampleByte], + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength), + false); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; using Image image = Image.Load(options, data); @@ -115,11 +269,17 @@ public class HeifSequenceParserTests Assert.Single(image.Frames); } + /// + /// Verifies that frame-aligned color and auxiliary AV1 samples are decoded together and the auxiliary luma + /// samples become the alpha channel of each presented color frame. + /// [Fact] public void DecodeComposesFrameAlignedAv1AlphaSamples() { byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; - byte[] data = CreateDecodableAv1SequenceWithAlphaContainer(source.AsSpan(0x10E, 0x1D), source.AsSpan(0xC7, 4)); + byte[] data = CreateDecodableAv1SequenceWithAlphaContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength)); using Image expectedColor = Image.Load(source); using Image expectedAlpha = Image.Load(source); @@ -141,24 +301,28 @@ public class HeifSequenceParserTests } } + /// + /// Verifies the complete libavif-shaped sample-table mapping, including timing, chunk offsets, sync status, + /// coding constraints, and the normalized sequence play count. + /// [Fact] public void ParseResolvesLibavifShapedSampleTable() { - byte[] data = CreateSequenceFile(1024); + byte[] data = CreateSequenceFile(SyntheticChunkOffset); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); - Assert.Equal(1000U, sequence.MovieTimescale); + Assert.Equal(SyntheticMovieTimescale, sequence.MovieTimescale); Assert.Null(sequence.AlphaTrack); - Assert.Equal(1U, sequence.ColorTrack.Id); - Assert.Equal(320, sequence.ColorTrack.Width); - Assert.Equal(240, sequence.ColorTrack.Height); + Assert.Equal(ColorTrackId, sequence.ColorTrack.Id); + Assert.Equal(SyntheticWidth, sequence.ColorTrack.Width); + Assert.Equal(SyntheticHeight, sequence.ColorTrack.Height); Assert.Equal(Heif4CharCode.Av01, sequence.ColorTrack.CodecType); Assert.NotNull(sequence.ColorTrack.Av1CodecConfiguration); - Assert.Equal(2U, sequence.ColorTrack.TotalSampleCount); + Assert.Equal(SyntheticSampleCount, sequence.ColorTrack.TotalSampleCount); Assert.Equal(3, sequence.ColorTrack.RepeatCount); Assert.False(sequence.ColorTrack.AllReferencePicturesIntra); Assert.True(sequence.ColorTrack.IntraPicturePredictionUsed); @@ -167,72 +331,106 @@ public class HeifSequenceParserTests sequence.ColorTrack.Samples, sample => { - Assert.Equal(1024, sample.Offset); - Assert.Equal(10, sample.Length); - Assert.Equal(100U, sample.Duration); + Assert.Equal(SyntheticChunkOffset, sample.Offset); + Assert.Equal(FirstSyntheticSampleLength, sample.Length); + Assert.Equal(SyntheticSampleDuration, sample.Duration); Assert.True(sample.IsSync); }, sample => { - Assert.Equal(1034, sample.Offset); - Assert.Equal(12, sample.Length); - Assert.Equal(100U, sample.Duration); + Assert.Equal((long)SyntheticChunkOffset + FirstSyntheticSampleLength, sample.Offset); + Assert.Equal(SecondSyntheticSampleLength, sample.Length); + Assert.Equal(SyntheticSampleDuration, sample.Duration); Assert.False(sample.IsSync); }); } + /// + /// Verifies that the parser does not select a picture track whose TrackHeaderBox clears the enabled flag. + /// + [Fact] + public void ParseRejectsDisabledPictureTrack() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackEnabled: false); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that the parser retains only the configured maximum number of frames while preserving the track's + /// declared total sample count. + /// [Fact] public void ParseRetainsOnlyConfiguredFrameCount() { - byte[] data = CreateSequenceFile(1024); + const uint retainedFrameLimit = 1; + + byte[] data = CreateSequenceFile(SyntheticChunkOffset); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(1); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(retainedFrameLimit); + stream.Position = BoxHeaderLength; HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); - Assert.Equal(2U, sequence.ColorTrack.TotalSampleCount); + Assert.Equal(SyntheticSampleCount, sequence.ColorTrack.TotalSampleCount); HeifSequenceSample sample = Assert.Single(sequence.ColorTrack.Samples); - Assert.Equal(1024, sample.Offset); - Assert.Equal(10, sample.Length); - Assert.Equal(100U, sample.Duration); + Assert.Equal(SyntheticChunkOffset, sample.Offset); + Assert.Equal(FirstSyntheticSampleLength, sample.Length); + Assert.Equal(SyntheticSampleDuration, sample.Duration); } + /// + /// Verifies that a retained sample whose declared byte range extends beyond the source stream is rejected. + /// [Fact] public void ParseRejectsRetainedSampleBeyondFile() { - byte[] data = CreateSequenceFile(2040); + uint truncatedChunkOffset = SyntheticFileLength - BoxHeaderLength; + byte[] data = CreateSequenceFile(truncatedChunkOffset); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that presentation transforms at either the movie or track level are rejected until the decoder can + /// apply those matrices to the emitted raster. + /// + /// Whether the movie header contains a non-unity matrix. + /// Whether the track header contains a non-unity matrix. [Theory] [InlineData(true, false)] [InlineData(false, true)] public void ParseRejectsNonIdentityMoviePresentationMatrix(bool nonIdentityMovieMatrix, bool nonIdentityTrackMatrix) { byte[] data = CreateSequenceFile( - 1024, + SyntheticChunkOffset, nonIdentityMovieMatrix: nonIdentityMovieMatrix, nonIdentityTrackMatrix: nonIdentityTrackMatrix); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that HEVC composition offsets and composition-to-decode information identify hidden samples and + /// assign the expected presentation time to the visible sample. + /// [Fact] public void ParseMarksHiddenHevcSamples() { - byte[] data = CreateSequenceFile(1024, hevc: true, compositionOffsets: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, hevc: true, compositionOffsets: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); @@ -244,45 +442,69 @@ public class HeifSequenceParserTests Assert.Equal(100, sequence.ColorTrack.Samples[1].CompositionTime); } + /// + /// Verifies that auxiliary-track and premultiplication references are resolved by track identifier and remain + /// valid when matching presentation properties are present on either track. + /// + /// Whether the color sample entry carries presentation properties. + /// Whether the alpha sample entry carries matching presentation properties. [Theory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(true, true)] public void ParseMatchesAlphaTrackAndPremultiplicationByTrackId(bool colorTransforms, bool alphaTransforms) { - byte[] data = CreateSequenceFileWithAlpha(1024, 1000, 2, colorTransforms, alphaTransforms); + byte[] data = CreateSequenceFileWithAlpha( + SyntheticChunkOffset, + SyntheticMovieTimescale, + AlphaTrackId, + colorTransforms, + alphaTransforms); + using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); Assert.NotNull(sequence.AlphaTrack); - Assert.Equal(2U, sequence.AlphaTrack.Id); + Assert.Equal(AlphaTrackId, sequence.AlphaTrack.Id); Assert.True(sequence.AlphaTrack.IsAlpha); Assert.True(sequence.ColorTrack.IsPremultiplied); } + /// + /// Verifies that an alpha track with a different media timescale is rejected under policies that do not permit + /// recovery from image-data inconsistencies. + /// + /// The segment-integrity policy applied at the parser boundary. [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void ParseRejectsAlphaTrackWithDifferentDecodeTiming(SegmentIntegrityHandling handling) { - byte[] data = CreateSequenceFileWithAlpha(1024, 2000, 0); + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, MismatchedAlphaTimescale, 0); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2, segmentIntegrityHandling: handling); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount, segmentIntegrityHandling: handling); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that image-data tolerance drops a timing-incompatible alpha track and clears the color track's + /// premultiplication state. + /// [Fact] public void ParseDropsAlphaTrackWithDifferentDecodeTimingWhenImageDataErrorsAreIgnored() { - byte[] data = CreateSequenceFileWithAlpha(1024, 2000, 0); + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, MismatchedAlphaTimescale, 0); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2, segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreImageData); - stream.Position = 8; + HeifSequenceParser parser = CreateParser( + SyntheticSampleCount, + segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreImageData); + + stream.Position = BoxHeaderLength; HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); @@ -290,46 +512,61 @@ public class HeifSequenceParserTests Assert.False(sequence.ColorTrack.IsPremultiplied); } + /// + /// Verifies that a premultiplication reference cannot name a track other than the selected linked alpha track. + /// [Fact] public void ParseRejectsPremultiplicationReferenceToUnrelatedTrack() { - byte[] data = CreateSequenceFileWithAlpha(1024, 1000, 3); + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, SyntheticMovieTimescale, UnrelatedTrackId); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that linked color and alpha tracks with different presentation properties are rejected because they + /// cannot be composed frame-for-frame into one image sequence. + /// [Fact] public void ParseRejectsMismatchedAlphaPresentationTransforms() { - byte[] data = CreateSequenceFileWithAlpha(1024, 1000, 0, false, true); + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, SyntheticMovieTimescale, 0, false, true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that AV1 image-sequence tracks reject composition-offset tables, which are only supported for the + /// HEVC hidden-sample presentation model. + /// [Fact] public void ParseRejectsCompositionOffsetsForAv1() { - byte[] data = CreateSequenceFile(1024, compositionOffsets: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, compositionOffsets: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that AV1 direct-reference sample groups resolve file-defined sample identifiers into compact + /// zero-based indices retained by each dependent sample. + /// [Fact] public void ParseResolvesDirectReferenceSamples() { - byte[] data = CreateSequenceFile(1024, directReferences: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, directReferences: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); @@ -341,34 +578,48 @@ public class HeifSequenceParserTests Assert.Equal(1, sequence.ColorTrack.Samples[1].DirectReferenceCount); } + /// + /// Verifies that a direct-reference group cannot name a sample identifier absent from the retained description + /// table. + /// [Fact] public void ParseRejectsUnknownDirectReferenceSampleId() { - byte[] data = CreateSequenceFile(1024, directReferences: true, directReferenceSampleId: 2); + byte[] data = CreateSequenceFile( + SyntheticChunkOffset, + directReferences: true, + directReferenceSampleId: AlphaTrackId); + using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that image properties nested in the visual sample entry are retained with their specified color, + /// geometry, orientation, light-level, and viewing-environment values. + /// [Fact] public void ParseRetainsTrackImageProperties() { - byte[] data = CreateSequenceFile(1024, trackProperties: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackProperties: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; Assert.NotNull(track.CicpProfile); - Assert.Equal(4U, track.PixelAspectRatio!.HorizontalSpacing); - Assert.Equal(3U, track.PixelAspectRatio.VerticalSpacing); - Assert.Equal(new Rectangle(0, 0, 320, 240), track.CleanAperture!.Value.ToRectangle(new Size(320, 240))); + Assert.Equal(SyntheticHorizontalPixelSpacing, track.PixelAspectRatio!.HorizontalSpacing); + Assert.Equal(SyntheticVerticalPixelSpacing, track.PixelAspectRatio.VerticalSpacing); + Size codedSize = new(SyntheticWidth, SyntheticHeight); + + Assert.Equal(new Rectangle(Point.Empty, codedSize), track.CleanAperture!.Value.ToRectangle(codedSize)); Assert.Equal((byte)1, track.RotationAngle); Assert.Equal((byte)1, track.MirrorAxis); - Assert.Equal((ushort)1000, track.ContentLightLevel!.Value.MaximumContentLightLevel); + Assert.Equal(SyntheticMaximumContentLightLevel, track.ContentLightLevel!.Value.MaximumContentLightLevel); Assert.NotNull(track.MasteringDisplayColorVolume); Assert.NotNull(track.ContentColorVolume); Assert.NotNull(track.AmbientViewingEnvironment); @@ -376,15 +627,20 @@ public class HeifSequenceParserTests Assert.NotNull(track.NominalDiffuseWhite); } + /// + /// Verifies that track-level Exif and XMP items are bounded and retained whether their extents address the file + /// or the metadata box's item-data payload. + /// + /// Whether metadata extents use construction method one and address the item-data box. [Theory] [InlineData(false)] [InlineData(true)] public void ParseRetainsBoundedTrackMetadata(bool useItemData) { - byte[] data = CreateSequenceFile(1024, trackMetadata: true, metadataInItemData: useItemData); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackMetadata: true, metadataInItemData: useItemData); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; @@ -393,110 +649,163 @@ public class HeifSequenceParserTests Assert.Equal(TrackXmpData.ToArray(), track.Metadata.XmpData); } + /// + /// Verifies that metadata skipping avoids both validation and retention of malformed optional track metadata + /// while leaving image samples available. + /// [Fact] public void ParseDoesNotValidateOrRetainSkippedTrackMetadata() { - byte[] data = CreateSequenceFile(1024, trackMetadata: true, invalidTrackMetadata: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackMetadata: true, invalidTrackMetadata: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2, skipMetadata: true); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount, skipMetadata: true); + stream.Position = BoxHeaderLength; HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; Assert.Null(track.Metadata); - Assert.Equal(2, track.Samples.Length); + Assert.Equal(SyntheticSampleCount, (uint)track.Samples.Length); } + /// + /// Verifies that malformed track metadata is fatal under strict validation but is omitted under ancillary-error + /// tolerance without affecting the retained image samples. + /// [Fact] public void ParseUsesAncillaryIntegrityPolicyForTrackMetadata() { - byte[] data = CreateSequenceFile(1024, trackMetadata: true, invalidTrackMetadata: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackMetadata: true, invalidTrackMetadata: true); using MemoryStream strictStream = new(data, false); - HeifSequenceParser strictParser = CreateParser(2); - strictStream.Position = 8; + HeifSequenceParser strictParser = CreateParser(SyntheticSampleCount); + strictStream.Position = BoxHeaderLength; Assert.Throws(() => strictParser.Parse(strictStream, GetMoviePayloadLength(data))); using MemoryStream tolerantStream = new(data, false); - HeifSequenceParser tolerantParser = CreateParser(2, segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreAncillary); - tolerantStream.Position = 8; + HeifSequenceParser tolerantParser = CreateParser( + SyntheticSampleCount, + segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreAncillary); + + tolerantStream.Position = BoxHeaderLength; HeifSequenceTrack track = tolerantParser.Parse(tolerantStream, GetMoviePayloadLength(data)).ColorTrack; Assert.Null(track.Metadata); - Assert.Equal(2, track.Samples.Length); + Assert.Equal(SyntheticSampleCount, (uint)track.Samples.Length); } + /// + /// Verifies that malformed presentation properties remain image-data errors under strict and ancillary-tolerant + /// policies because they affect the rendered image geometry. + /// + /// The segment-integrity policy applied at the parser boundary. [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void ParseRejectsInvalidPresentationPropertyUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) { - byte[] data = CreateSequenceFile(1024, trackProperties: true, invalidRotation: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackProperties: true, invalidRotation: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2, segmentIntegrityHandling: handling); - stream.Position = 8; + HeifSequenceParser parser = CreateParser(SyntheticSampleCount, segmentIntegrityHandling: handling); + stream.Position = BoxHeaderLength; Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); } + /// + /// Verifies that image-data tolerance omits only the malformed presentation property while retaining independent + /// valid properties and all image samples. + /// [Fact] public void ParseOmitsInvalidPresentationPropertyWhenImageDataErrorsAreIgnored() { - byte[] data = CreateSequenceFile(1024, trackProperties: true, invalidRotation: true); + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackProperties: true, invalidRotation: true); using MemoryStream stream = new(data, false); - HeifSequenceParser parser = CreateParser(2, segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreImageData); - stream.Position = 8; + HeifSequenceParser parser = CreateParser( + SyntheticSampleCount, + segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreImageData); + + stream.Position = BoxHeaderLength; HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; Assert.Null(track.RotationAngle); Assert.NotNull(track.PixelAspectRatio); - Assert.Equal(2, track.Samples.Length); + Assert.Equal(SyntheticSampleCount, (uint)track.Samples.Length); } + /// + /// Builds a bounded movie box containing one configurable image-sequence track and leaves sample payload space + /// outside the movie so tests can independently control sample offsets and source-length validation. + /// + /// The absolute file offset of the track's single sample chunk. + /// Whether the visual sample entry describes HEVC instead of AV1. + /// Whether to write HEVC composition-offset and shift boxes. + /// Whether to write AV1 direct-reference sample groups. + /// The sample identifier named by the dependent sample. + /// Whether to write image presentation properties in the sample entry. + /// Whether to write track-level Exif and XMP metadata items. + /// Whether metadata extents address an item-data box instead of file offsets. + /// Whether the metadata handler is intentionally invalid. + /// Whether the rotation property contains reserved high bits. + /// The displayed and coded sample width in pixels. + /// The displayed and coded sample height in pixels. + /// The AV1CodecConfigurationBox payload, or the valid default payload. + /// The first sample size, or the synthetic default size. + /// The second sample size, or the first/default sample size. + /// Whether both samples are declared as sync samples. + /// The alpha track identifier named by the premultiplication reference. + /// Whether the movie header matrix contains horizontal scaling. + /// Whether the track header matrix contains horizontal scaling. + /// Whether the picture track is eligible for sequence presentation. + /// The fixed-length synthetic file containing the serialized movie box. private static byte[] CreateSequenceFile( uint chunkOffset, bool hevc = false, bool compositionOffsets = false, bool directReferences = false, - uint directReferenceSampleId = 1, + uint directReferenceSampleId = ColorTrackId, bool trackProperties = false, bool trackMetadata = false, bool metadataInItemData = false, bool invalidTrackMetadata = false, bool invalidRotation = false, - int width = 320, - int height = 240, + int width = SyntheticWidth, + int height = SyntheticHeight, byte[] av1Configuration = null, int? sampleSize = null, int? secondSampleSize = null, bool allSamplesSync = false, uint premultipliedByTrackId = 0, bool nonIdentityMovieMatrix = false, - bool nonIdentityTrackMatrix = false) + bool nonIdentityTrackMatrix = false, + bool trackEnabled = true) { using MemoryStream stream = new(); using BinaryWriter writer = new(stream, Encoding.UTF8, true); long movie = BeginBox(writer, Heif4CharCode.Moov); long movieHeader = BeginBox(writer, Heif4CharCode.Mvhd); + + // ISO/IEC 14496-12 Section 8.2.2 orders the version-zero fields as creation time, modification time, + // timescale, duration, preferred 16.16 rate, preferred 8.8 volume, reserved words, matrix, predefined words, + // and the next available track identifier. WriteFullBoxHeader(writer, 0, 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); - WriteUInt32(writer, 1000); - WriteUInt32(writer, 600); - WriteUInt32(writer, 0x00010000); - WriteUInt16(writer, 0x0100); + WriteUInt32(writer, SyntheticMovieTimescale); + WriteUInt32(writer, SyntheticTrackDuration); + WriteUInt32(writer, UnityFixed16Point16); + WriteUInt16(writer, UnityFixed8Point8); WriteUInt16(writer, 0); - WriteZeros(writer, 8); + WriteZeros(writer, 2 * sizeof(uint)); WritePresentationMatrix(writer, nonIdentityMovieMatrix); - WriteZeros(writer, 24); - WriteUInt32(writer, 3); + WriteZeros(writer, 6 * sizeof(uint)); + WriteUInt32(writer, UnrelatedTrackId); EndBox(writer, movieHeader); long track = BeginBox(writer, Heif4CharCode.Trak); - WriteTrackHeader(writer, width, height, 1, nonIdentityTrackMatrix); + WriteTrackHeader(writer, width, height, ColorTrackId, nonIdentityTrackMatrix, trackEnabled); if (premultipliedByTrackId != 0) { WriteTrackReference(writer, Heif4CharCode.Prem, premultipliedByTrackId); @@ -536,7 +845,10 @@ public class HeifSequenceParserTests EndBox(writer, movie); byte[] movieBytes = stream.ToArray(); - byte[] file = new byte[2048]; + + // The fixed outer length leaves deterministic space for file-addressed metadata and sample extents while + // allowing individual tests to place an extent deliberately beyond the source boundary. + byte[] file = new byte[SyntheticFileLength]; movieBytes.CopyTo(file, 0); if (trackMetadata && !metadataInItemData) @@ -548,6 +860,21 @@ public class HeifSequenceParserTests return file; } + /// + /// Appends an auxiliary alpha track to a synthetic color-track movie and links it by track identifier. + /// + /// The absolute file offset of the color sample chunk. + /// The alpha track's media time scale in units per second. + /// The track identifier named by the color premultiplication reference. + /// Whether the color sample entry carries image presentation properties. + /// Whether the alpha sample entry carries image presentation properties. + /// The absolute alpha sample-chunk offset, or the color chunk offset when omitted. + /// The displayed and coded width of both tracks in pixels. + /// The displayed and coded height of both tracks in pixels. + /// The AV1CodecConfigurationBox payload shared by the tracks. + /// The size of each sample in both tracks. + /// Whether all color and alpha samples are sync samples. + /// The fixed-length synthetic file containing the color and alpha tracks. private static byte[] CreateSequenceFileWithAlpha( uint chunkOffset, uint alphaTimescale, @@ -555,8 +882,8 @@ public class HeifSequenceParserTests bool colorTransforms = false, bool alphaTransforms = false, uint? alphaChunkOffset = null, - int width = 320, - int height = 240, + int width = SyntheticWidth, + int height = SyntheticHeight, byte[] av1Configuration = null, int? sampleSize = null, bool allSamplesSync = false) @@ -575,8 +902,8 @@ public class HeifSequenceParserTests using MemoryStream stream = new(); using BinaryWriter writer = new(stream, Encoding.UTF8, true); long track = BeginBox(writer, Heif4CharCode.Trak); - WriteTrackHeader(writer, width, height, 2); - WriteTrackReference(writer, Heif4CharCode.Auxl, 1); + WriteTrackHeader(writer, width, height, AlphaTrackId, false, true); + WriteTrackReference(writer, Heif4CharCode.Auxl, ColorTrackId); long media = BeginBox(writer, Heif4CharCode.Mdia); WriteMediaHeader(writer, alphaTimescale); @@ -590,7 +917,7 @@ public class HeifSequenceParserTests false, false, false, - 1, + ColorTrackId, alphaTransforms, false, width, @@ -606,44 +933,60 @@ public class HeifSequenceParserTests EndBox(writer, track); byte[] alphaTrack = stream.ToArray(); - byte[] file = new byte[2048]; + byte[] file = new byte[SyntheticFileLength]; + + // The alpha TrackBox is appended inside the existing MovieBox, so patch the movie size after concatenation. colorFile.AsSpan(0, movieLength).CopyTo(file); alphaTrack.CopyTo(file, movieLength); BinaryPrimitives.WriteUInt32BigEndian(file, (uint)(movieLength + alphaTrack.Length)); return file; } + /// + /// Prefixes a synthetic movie with the AVIF image-sequence FileTypeBox used by the public decoder entry points. + /// + /// Whether the sequence carries image presentation properties. + /// Whether the sequence carries track-level Exif and XMP items. + /// The complete synthetic AVIF byte stream. private static byte[] CreateSequenceContainer(bool trackProperties, bool trackMetadata) { byte[] movie = CreateSequenceFile( - 1024, + SyntheticChunkOffset, trackProperties: trackProperties, trackMetadata: trackMetadata, metadataInItemData: true); - byte[] data = new byte[movie.Length + 24]; - BinaryPrimitives.WriteUInt32BigEndian(data, 24); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), (uint)Heif4CharCode.Ftyp); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)Heif4CharCode.Avis); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(12), 0); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), (uint)Heif4CharCode.Avif); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), (uint)Heif4CharCode.Mif1); - movie.CopyTo(data, 24); + byte[] data = new byte[movie.Length + FileTypeBoxLength]; + WriteSequenceFileTypeBox(data); + movie.CopyTo(data, FileTypeBoxLength); return data; } + /// + /// Builds a two-frame AVIF sequence that stores the same independently decodable AV1 sample in both frames. + /// + /// The complete AV1 sample payload. + /// The AV1CodecConfigurationBox payload describing the sample. + /// The complete synthetic AVIF byte stream. private static byte[] CreateDecodableAv1SequenceContainer(ReadOnlySpan sample, ReadOnlySpan configuration) => CreateDecodableAv1SequenceContainer(sample, sample, configuration, true); + /// + /// Builds a two-frame AVIF sequence with caller-provided AV1 samples so integrity tests can corrupt one sample + /// without also corrupting the parser-owned container structures. + /// + /// The first AV1 sample payload. + /// The second AV1 sample payload. + /// The AV1CodecConfigurationBox payload describing both samples. + /// Whether both samples are marked independently decodable. + /// The complete synthetic AVIF byte stream. private static byte[] CreateDecodableAv1SequenceContainer( ReadOnlySpan firstSample, ReadOnlySpan secondSample, ReadOnlySpan configuration, bool allSamplesSync) { - const int fileTypeLength = 24; - const int movieStorageLength = 2048; - uint chunkOffset = fileTypeLength + movieStorageLength; + uint chunkOffset = FileTypeBoxLength + SyntheticFileLength; byte[] movie = CreateSequenceFile( chunkOffset, width: 4, @@ -654,27 +997,27 @@ public class HeifSequenceParserTests allSamplesSync: allSamplesSync); byte[] data = new byte[chunkOffset + firstSample.Length + secondSample.Length]; - BinaryPrimitives.WriteUInt32BigEndian(data, fileTypeLength); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), (uint)Heif4CharCode.Ftyp); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)Heif4CharCode.Avis); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(12), 0); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), (uint)Heif4CharCode.Avif); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), (uint)Heif4CharCode.Mif1); - movie.CopyTo(data, fileTypeLength); + WriteSequenceFileTypeBox(data); + movie.CopyTo(data, FileTypeBoxLength); firstSample.CopyTo(data.AsSpan((int)chunkOffset)); secondSample.CopyTo(data.AsSpan((int)chunkOffset + firstSample.Length)); return data; } + /// + /// Builds two frame-aligned color and alpha AV1 tracks using a sample that is independently decodable as both + /// color and monochrome luma, allowing alpha composition to be tested without an encoder dependency. + /// + /// The AV1 sample payload stored in every color and alpha frame. + /// The AV1CodecConfigurationBox payload describing the sample. + /// The complete synthetic AVIF byte stream. private static byte[] CreateDecodableAv1SequenceWithAlphaContainer(ReadOnlySpan sample, ReadOnlySpan configuration) { - const int fileTypeLength = 24; - const int movieStorageLength = 2048; - uint colorChunkOffset = fileTypeLength + movieStorageLength; + uint colorChunkOffset = FileTypeBoxLength + SyntheticFileLength; uint alphaChunkOffset = colorChunkOffset + (uint)(sample.Length * 2); byte[] movie = CreateSequenceFileWithAlpha( colorChunkOffset, - 1000, + SyntheticMovieTimescale, 0, alphaChunkOffset: alphaChunkOffset, width: 4, @@ -684,13 +1027,8 @@ public class HeifSequenceParserTests allSamplesSync: true); byte[] data = new byte[alphaChunkOffset + (sample.Length * 2)]; - BinaryPrimitives.WriteUInt32BigEndian(data, fileTypeLength); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), (uint)Heif4CharCode.Ftyp); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)Heif4CharCode.Avis); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(12), 0); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), (uint)Heif4CharCode.Avif); - BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), (uint)Heif4CharCode.Mif1); - movie.CopyTo(data, fileTypeLength); + WriteSequenceFileTypeBox(data); + movie.CopyTo(data, FileTypeBoxLength); sample.CopyTo(data.AsSpan((int)colorChunkOffset)); sample.CopyTo(data.AsSpan((int)colorChunkOffset + sample.Length)); sample.CopyTo(data.AsSpan((int)alphaChunkOffset)); @@ -698,35 +1036,94 @@ public class HeifSequenceParserTests return data; } - private static void WriteTrackHeader(BinaryWriter writer, int width, int height, uint trackId = 1, bool nonIdentityMatrix = false) + /// + /// Writes the AVIF image-sequence FileTypeBox shared by all complete synthetic decoder inputs. + /// + /// The destination whose first 24 bytes receive the box. + private static void WriteSequenceFileTypeBox(Span destination) + { + int fieldOffset = 0; + + // ISO/IEC 14496-12 Section 4.3 stores the box size and type first, followed by the major brand, minor + // version, and compatible brands. 'avis' selects the sequence presentation while 'avif' and 'mif1' declare + // compatibility with the AVIF and HEIF image-item structures also present in these files. + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], FileTypeBoxLength); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Ftyp); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Avis); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], 0); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Avif); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Mif1); + } + + /// + /// Writes a version-zero TrackHeaderBox with an enabled movie track, integral 16.16 dimensions, and a selectable + /// presentation matrix. + /// + /// The writer receiving big-endian box fields. + /// The displayed track width in pixels. + /// The displayed track height in pixels. + /// The nonzero file-defined track identifier. + /// Whether to encode horizontal scaling instead of the unity matrix. + /// Whether to set the TrackHeaderBox enabled flag. + private static void WriteTrackHeader( + BinaryWriter writer, + int width, + int height, + uint trackId, + bool nonIdentityMatrix, + bool isEnabled) { + const uint trackEnabledFlag = 1U << 0; + const int fixedPointFractionalBits = 16; + long trackHeader = BeginBox(writer, Heif4CharCode.Tkhd); - WriteFullBoxHeader(writer, 0, 3); + + // ISO/IEC 14496-12 Section 8.3.2 assigns bit zero to track_enabled. libavif sequence tracks set that bit without + // requiring track_in_movie. The following fields hold times, identifier, duration, matrix, and 16.16 dimensions. + WriteFullBoxHeader(writer, 0, isEnabled ? trackEnabledFlag : 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); WriteUInt32(writer, trackId); WriteUInt32(writer, 0); - WriteUInt32(writer, 600); - WriteZeros(writer, 16); + WriteUInt32(writer, SyntheticTrackDuration); + WriteZeros(writer, (2 * sizeof(uint)) + (4 * sizeof(ushort))); WritePresentationMatrix(writer, nonIdentityMatrix); - WriteUInt32(writer, (uint)width << 16); - WriteUInt32(writer, (uint)height << 16); + WriteUInt32(writer, (uint)width << fixedPointFractionalBits); + WriteUInt32(writer, (uint)height << fixedPointFractionalBits); EndBox(writer, trackHeader); } + /// + /// Writes the nine fixed-point coefficients of an ISO base media presentation matrix. + /// + /// The writer receiving big-endian matrix coefficients. + /// Whether the horizontal 16.16 scale is two instead of one. private static void WritePresentationMatrix(BinaryWriter writer, bool nonIdentityMatrix) { - WriteUInt32(writer, nonIdentityMatrix ? 0x00020000U : 0x00010000U); + // The first six coefficients use 16.16 fixed point and the final perspective column uses 2.30. Altering only + // the horizontal scale gives matrix-validation tests one controlled departure from the unity matrix. + WriteUInt32(writer, nonIdentityMatrix ? DoubleFixed16Point16 : UnityFixed16Point16); WriteUInt32(writer, 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); - WriteUInt32(writer, 0x00010000); + WriteUInt32(writer, UnityFixed16Point16); WriteUInt32(writer, 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); - WriteUInt32(writer, 0x40000000); + WriteUInt32(writer, UnityFixed2Point30); } + /// + /// Writes one TrackReferenceBox child that links the owning track to a single referenced track identifier. + /// + /// The writer receiving the reference boxes. + /// The image-sequence reference relationship. + /// The referenced track identifier. private static void WriteTrackReference(BinaryWriter writer, Heif4CharCode referenceType, uint trackId) { long references = BeginBox(writer, Heif4CharCode.Tref); @@ -736,33 +1133,54 @@ public class HeifSequenceParserTests EndBox(writer, references); } + /// + /// Writes a repeating single-entry EditListBox whose 200 movie-time-scale units are repeated to fill the + /// 600-unit track duration, producing three total plays. + /// + /// The writer receiving the edit boxes. private static void WriteEditList(BinaryWriter writer) { + const uint repeatEditListFlag = 1U << 0; + const uint editEntryCount = 1; + const uint mediaStartTime = 0; + const ushort unityMediaRateInteger = 1; + const ushort unityMediaRateFraction = 0; + long edit = BeginBox(writer, Heif4CharCode.Edts); long editList = BeginBox(writer, Heif4CharCode.Elst); - WriteFullBoxHeader(writer, 0, 1); - WriteUInt32(writer, 1); - WriteUInt32(writer, 200); - WriteUInt32(writer, 0); - WriteUInt16(writer, 1); - WriteUInt16(writer, 0); + WriteFullBoxHeader(writer, 0, repeatEditListFlag); + WriteUInt32(writer, editEntryCount); + WriteUInt32(writer, SyntheticMediaDuration); + WriteUInt32(writer, mediaStartTime); + WriteUInt16(writer, unityMediaRateInteger); + WriteUInt16(writer, unityMediaRateFraction); EndBox(writer, editList); EndBox(writer, edit); } - private static void WriteMediaHeader(BinaryWriter writer, uint timescale = 1000) + /// + /// Writes a version-zero MediaHeaderBox for two 100-unit samples and the packed ISO-639 language code "und". + /// + /// The writer receiving the media header. + /// The media time scale in units per second. + private static void WriteMediaHeader(BinaryWriter writer, uint timescale = SyntheticMovieTimescale) { long mediaHeader = BeginBox(writer, Heif4CharCode.Mdhd); WriteFullBoxHeader(writer, 0, 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); WriteUInt32(writer, timescale); - WriteUInt32(writer, 200); - WriteUInt16(writer, 21956); + WriteUInt32(writer, SyntheticMediaDuration); + WriteUInt16(writer, PackedUndeterminedLanguage); WriteUInt16(writer, 0); EndBox(writer, mediaHeader); } + /// + /// Writes a HandlerBox with the requested track role and an empty null-terminated handler name. + /// + /// The writer receiving the handler box. + /// The four-character handler role. private static void WriteHandler(BinaryWriter writer, Heif4CharCode handlerType) { long handler = BeginBox(writer, Heif4CharCode.Hdlr); @@ -774,6 +1192,10 @@ public class HeifSequenceParserTests EndBox(writer, handler); } + /// + /// Writes a self-contained DataInformationBox whose single DataEntryUrlBox resolves sample offsets in this file. + /// + /// The writer receiving the data-reference hierarchy. private static void WriteDataInformation(BinaryWriter writer) { long dataInformation = BeginBox(writer, Heif4CharCode.Dinf); @@ -787,21 +1209,40 @@ public class HeifSequenceParserTests EndBox(writer, dataInformation); } + /// + /// Writes a track-level MetaBox containing one Exif item and one XMP MIME item, with extents addressing either + /// the enclosing file or an ItemDataBox. + /// + /// The writer receiving the metadata hierarchy. + /// Whether item extents use construction method one. + /// Whether to write an invalid video handler instead of the picture handler. private static void WriteTrackMetadata(BinaryWriter writer, bool useItemData, bool invalidHandler) { + const byte fourByteOffsetAndLengthSizes = 0x44; + const ushort metadataItemCount = 2; + long metadata = BeginBox(writer, Heif4CharCode.Meta); WriteFullBoxHeader(writer, 0, 0); WriteHandler(writer, invalidHandler ? Heif4CharCode.Vide : Heif4CharCode.Pict); long itemLocations = BeginBox(writer, Heif4CharCode.Iloc); WriteFullBoxHeader(writer, useItemData ? (byte)1 : (byte)0, 0); - writer.Write((byte)0x44); + + // ISO/IEC 14496-12 Section 8.11.3 packs offset_size and length_size into the high and low nibbles. Four-byte + // fields cover the synthetic file while keeping the encoded records identical to normal HEIF metadata. + writer.Write(fourByteOffsetAndLengthSizes); writer.Write((byte)0); - WriteUInt16(writer, 2); - WriteTrackMetadataLocation(writer, 1, useItemData, useItemData ? 0U : TrackExifOffset, (uint)TrackExifData.Length); + WriteUInt16(writer, metadataItemCount); WriteTrackMetadataLocation( writer, - 2, + TrackExifItemId, + useItemData, + useItemData ? 0U : TrackExifOffset, + (uint)TrackExifData.Length); + + WriteTrackMetadataLocation( + writer, + TrackXmpItemId, useItemData, useItemData ? (uint)TrackExifData.Length : TrackXmpOffset, (uint)TrackXmpData.Length); @@ -810,9 +1251,9 @@ public class HeifSequenceParserTests long itemInformation = BeginBox(writer, Heif4CharCode.Iinf); WriteFullBoxHeader(writer, 0, 0); - WriteUInt16(writer, 2); - WriteTrackMetadataItem(writer, 1, Heif4CharCode.Exif); - WriteTrackMetadataItem(writer, 2, Heif4CharCode.Mime); + WriteUInt16(writer, metadataItemCount); + WriteTrackMetadataItem(writer, TrackExifItemId, Heif4CharCode.Exif); + WriteTrackMetadataItem(writer, TrackXmpItemId, Heif4CharCode.Mime); EndBox(writer, itemInformation); if (useItemData) @@ -826,26 +1267,47 @@ public class HeifSequenceParserTests EndBox(writer, metadata); } + /// + /// Writes one ItemLocationBox record with a single extent encoded using four-byte offsets and lengths. + /// + /// The writer receiving the item-location record. + /// The item identifier linked to an ItemInfoEntry. + /// Whether the extent addresses ItemDataBox bytes. + /// The extent offset relative to the selected construction method. + /// The extent length in bytes. private static void WriteTrackMetadataLocation(BinaryWriter writer, ushort itemId, bool useItemData, uint offset, uint length) { + const ushort itemDataConstructionMethod = 1; + const ushort localDataReferenceIndex = 0; + const ushort extentCount = 1; + WriteUInt16(writer, itemId); if (useItemData) { - WriteUInt16(writer, 1); + WriteUInt16(writer, itemDataConstructionMethod); } - WriteUInt16(writer, 0); - WriteUInt16(writer, 1); + WriteUInt16(writer, localDataReferenceIndex); + WriteUInt16(writer, extentCount); WriteUInt32(writer, offset); WriteUInt32(writer, length); } + /// + /// Writes a version-two ItemInfoEntry for an Exif item or an XMP item using the registered RDF MIME type. + /// + /// The writer receiving the item information entry. + /// The identifier matched by the location record. + /// The item's four-character type. private static void WriteTrackMetadataItem(BinaryWriter writer, ushort itemId, Heif4CharCode itemType) { + const byte itemInfoVersion = 2; + const ushort noItemProtection = 0; + long itemInformationEntry = BeginBox(writer, Heif4CharCode.Infe); - WriteFullBoxHeader(writer, 2, 0); + WriteFullBoxHeader(writer, itemInfoVersion, 0); WriteUInt16(writer, itemId); - WriteUInt16(writer, 0); + WriteUInt16(writer, noItemProtection); WriteUInt32(writer, (uint)itemType); writer.Write((byte)0); if (itemType == Heif4CharCode.Mime) @@ -857,6 +1319,25 @@ public class HeifSequenceParserTests EndBox(writer, itemInformationEntry); } + /// + /// Writes the timing, sample-to-chunk, size, chunk-offset, sync-sample, optional composition, and optional direct + /// reference boxes for exactly two image-sequence samples. + /// + /// The writer receiving the SampleTableBox. + /// The absolute file offset of the single sample chunk. + /// Whether the sample description uses HEVC instead of AV1. + /// Whether to write composition-offset and shift boxes. + /// Whether to write AV1 direct-reference grouping. + /// The sample identifier referenced by the second sample. + /// Whether the visual sample entry contains presentation properties. + /// Whether the rotation property contains reserved high bits. + /// The coded sample width in pixels. + /// The coded sample height in pixels. + /// The AV1CodecConfigurationBox payload. + /// The first sample length, or the synthetic default. + /// The second sample length, or the first/default length. + /// Whether both samples are listed as sync samples. + /// Whether the sample entry describes an auxiliary alpha track. private static void WriteSampleTable( BinaryWriter writer, uint chunkOffset, @@ -874,67 +1355,79 @@ public class HeifSequenceParserTests bool allSamplesSync, bool alpha = false) { + const uint singleEntry = 1; + const uint firstChunk = 1; + const uint sampleDescriptionIndex = 1; + const uint variableSampleSizes = 0; + long sampleTable = BeginBox(writer, Heif4CharCode.Stbl); WriteSampleDescription(writer, hevc, trackProperties, invalidRotation, width, height, av1Configuration, allSamplesSync, alpha); long timing = BeginBox(writer, Heif4CharCode.Stts); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, 1); - WriteUInt32(writer, 2); - WriteUInt32(writer, 100); + WriteUInt32(writer, singleEntry); + WriteUInt32(writer, SyntheticSampleCount); + WriteUInt32(writer, SyntheticSampleDuration); EndBox(writer, timing); long sampleToChunk = BeginBox(writer, Heif4CharCode.Stsc); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, 1); - WriteUInt32(writer, 1); - WriteUInt32(writer, 2); - WriteUInt32(writer, 1); + WriteUInt32(writer, singleEntry); + WriteUInt32(writer, firstChunk); + WriteUInt32(writer, SyntheticSampleCount); + WriteUInt32(writer, sampleDescriptionIndex); EndBox(writer, sampleToChunk); long sampleSizes = BeginBox(writer, Heif4CharCode.Stsz); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, 0); - WriteUInt32(writer, 2); - WriteUInt32(writer, (uint)(sampleSize ?? 10)); - WriteUInt32(writer, (uint)(secondSampleSize ?? sampleSize ?? 12)); + WriteUInt32(writer, variableSampleSizes); + WriteUInt32(writer, SyntheticSampleCount); + WriteUInt32(writer, (uint)(sampleSize ?? FirstSyntheticSampleLength)); + WriteUInt32(writer, (uint)(secondSampleSize ?? sampleSize ?? SecondSyntheticSampleLength)); EndBox(writer, sampleSizes); long chunkOffsets = BeginBox(writer, Heif4CharCode.Stco); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, 1); + WriteUInt32(writer, singleEntry); WriteUInt32(writer, chunkOffset); EndBox(writer, chunkOffsets); long syncSamples = BeginBox(writer, Heif4CharCode.Stss); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, allSamplesSync ? 2U : 1U); - WriteUInt32(writer, 1); + WriteUInt32(writer, allSamplesSync ? SyntheticSampleCount : singleEntry); + WriteUInt32(writer, firstChunk); if (allSamplesSync) { - WriteUInt32(writer, 2); + WriteUInt32(writer, SyntheticSampleCount); } EndBox(writer, syncSamples); if (compositionOffsets) { + const byte signedCompositionOffsetVersion = 1; + const uint compositionEntryCount = SyntheticSampleCount; + const uint oneSample = 1; + uint hiddenSampleOffset = unchecked((uint)int.MinValue); + long offsets = BeginBox(writer, Heif4CharCode.Ctts); - WriteFullBoxHeader(writer, 1, 0); - WriteUInt32(writer, 2); - WriteUInt32(writer, 1); - WriteUInt32(writer, 0x80000000); - WriteUInt32(writer, 1); + WriteFullBoxHeader(writer, signedCompositionOffsetVersion, 0); + WriteUInt32(writer, compositionEntryCount); + WriteUInt32(writer, oneSample); + WriteUInt32(writer, hiddenSampleOffset); + WriteUInt32(writer, oneSample); WriteUInt32(writer, 0); EndBox(writer, offsets); + // The CompositionToDecodeBox declares that visible presentation begins after the hidden sample's + // 100-unit slot and ends at the two-sample media duration. long compositionToDecode = BeginBox(writer, Heif4CharCode.Cslg); WriteFullBoxHeader(writer, 0, 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); WriteUInt32(writer, 0); - WriteUInt32(writer, 100); - WriteUInt32(writer, 200); + WriteUInt32(writer, SyntheticSampleDuration); + WriteUInt32(writer, SyntheticMediaDuration); EndBox(writer, compositionToDecode); } @@ -946,33 +1439,67 @@ public class HeifSequenceParserTests EndBox(writer, sampleTable); } + /// + /// Writes AV1 "refs" sample-group descriptions and maps the first sample to an independent description and the + /// second sample to a description containing one direct reference. + /// + /// The writer receiving the sample-group boxes. + /// The sample identifier named by the dependent description. private static void WriteDirectReferenceSampleGroup(BinaryWriter writer, uint directReferenceSampleId) { + const byte variableLengthDescriptionVersion = 1; + const uint variableDescriptionLength = 0; + const uint descriptionCount = SyntheticSampleCount; + const uint independentDescriptionLength = sizeof(uint) + sizeof(byte); + const uint dependentDescriptionLength = independentDescriptionLength + sizeof(uint); + const uint independentSampleId = ColorTrackId; + const uint dependentSampleId = 0; + const byte noDirectReferences = 0; + const byte oneDirectReference = 1; + long descriptions = BeginBox(writer, Heif4CharCode.Sgpd); - WriteFullBoxHeader(writer, 1, 0); + WriteFullBoxHeader(writer, variableLengthDescriptionVersion, 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, variableDescriptionLength); + WriteUInt32(writer, descriptionCount); + WriteUInt32(writer, independentDescriptionLength); + WriteUInt32(writer, independentSampleId); + writer.Write(noDirectReferences); + WriteUInt32(writer, dependentDescriptionLength); + WriteUInt32(writer, dependentSampleId); + writer.Write(oneDirectReference); WriteUInt32(writer, directReferenceSampleId); EndBox(writer, descriptions); + const uint sampleGroupRunCount = SyntheticSampleCount; + const uint oneSamplePerRun = 1; + const uint independentDescriptionIndex = 1; + const uint dependentDescriptionIndex = 2; + 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); + WriteUInt32(writer, sampleGroupRunCount); + WriteUInt32(writer, oneSamplePerRun); + WriteUInt32(writer, independentDescriptionIndex); + WriteUInt32(writer, oneSamplePerRun); + WriteUInt32(writer, dependentDescriptionIndex); EndBox(writer, sampleMap); } + /// + /// Writes one VisualSampleEntry and its codec configuration, optional alpha role, optional presentation + /// properties, and CodingConstraintsBox. + /// + /// The writer receiving the SampleDescriptionBox. + /// Whether the entry uses HEVC instead of AV1. + /// Whether to append image presentation properties. + /// Whether the rotation property contains reserved high bits. + /// The coded sample width in pixels. + /// The coded sample height in pixels. + /// The AV1CodecConfigurationBox payload. + /// Whether coding constraints declare every reference picture intra coded. + /// Whether the entry carries the HEIF alpha auxiliary type. private static void WriteSampleDescription( BinaryWriter writer, bool hevc, @@ -984,22 +1511,35 @@ public class HeifSequenceParserTests bool allSamplesSync, bool alpha) { + const ushort localDataReferenceIndex = 1; + const ushort visualSampleFrameCount = 1; + const int compressorNameLength = 32; + const ushort noColorTable = ushort.MaxValue; + const uint allReferencePicturesIntraMask = 1U << 31; + const uint intraPicturePredictionUsedMask = 1U << 30; + const int maximumReferencesShift = 26; + const uint maximumReferencesPerPicture = 15; + long description = BeginBox(writer, Heif4CharCode.Stsd); WriteFullBoxHeader(writer, 0, 0); WriteUInt32(writer, 1); long sampleEntry = BeginBox(writer, hevc ? Heif4CharCode.Hvc1 : Heif4CharCode.Av01); + + // ISO/IEC 14496-12 Section 12.1.3 defines six reserved bytes and a data-reference index before the visual + // sample entry's predefined words, dimensions, 16.16 resolution, frame count, fixed compressor-name field, + // pixel depth, and the -1 sentinel indicating that no color table is present. WriteZeros(writer, 6); - WriteUInt16(writer, 1); - WriteZeros(writer, 16); + WriteUInt16(writer, localDataReferenceIndex); + WriteZeros(writer, (2 * sizeof(ushort)) + (3 * sizeof(uint))); WriteUInt16(writer, (ushort)width); WriteUInt16(writer, (ushort)height); - WriteUInt32(writer, 0x00480000); - WriteUInt32(writer, 0x00480000); + WriteUInt32(writer, SyntheticHorizontalResolution); + WriteUInt32(writer, SyntheticHorizontalResolution); WriteUInt32(writer, 0); - WriteUInt16(writer, 1); - WriteZeros(writer, 32); - WriteUInt16(writer, 0x18); - WriteUInt16(writer, ushort.MaxValue); + WriteUInt16(writer, visualSampleFrameCount); + WriteZeros(writer, compressorNameLength); + WriteUInt16(writer, SyntheticPixelDepth); + WriteUInt16(writer, noColorTable); if (hevc) { @@ -1008,7 +1548,9 @@ public class HeifSequenceParserTests else { long configuration = BeginBox(writer, Heif4CharCode.Av1C); - writer.Write(av1Configuration ?? [0x81, 0, 0, 0]); + ReadOnlySpan configurationPayload = av1Configuration is null ? DefaultAv1Configuration : av1Configuration; + + writer.Write(configurationPayload); EndBox(writer, configuration); } @@ -1026,114 +1568,192 @@ public class HeifSequenceParserTests WriteTrackImageProperties(writer, invalidRotation, width, height); } - long codingConstraints = BeginBox(writer, Heif4CharCode.Ccst); + long codingConstraintsBox = BeginBox(writer, Heif4CharCode.Ccst); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, allSamplesSync ? 0xFC000000U : 0x7C000000U); - EndBox(writer, codingConstraints); + + // HEIF CodingConstraintsBox places the two boolean constraints in bits 31 and 30 and the four-bit maximum + // reference count in bits 29..26. The synthetic dependency path deliberately permits the maximum of fifteen. + uint codingConstraints = intraPicturePredictionUsedMask | (maximumReferencesPerPicture << maximumReferencesShift); + if (allSamplesSync) + { + codingConstraints |= allReferencePicturesIntraMask; + } + + WriteUInt32(writer, codingConstraints); + EndBox(writer, codingConstraintsBox); EndBox(writer, sampleEntry); EndBox(writer, description); } + /// + /// Writes representative color, aspect-ratio, clean-aperture, orientation, HDR, and viewing-environment + /// properties inside a visual sample entry. + /// + /// The writer receiving the property boxes. + /// Whether the rotation byte contains reserved high bits. + /// The clean-aperture width in pixels. + /// The clean-aperture height in pixels. private static void WriteTrackImageProperties(BinaryWriter writer, bool invalidRotation, int width, int height) { + const byte fullRangeFlag = 1 << 7; + const byte reservedRotationBits = 0xFC; + const byte rotateCounterClockwise90Degrees = 1; + const byte verticalMirrorAxis = 1; + long color = BeginBox(writer, Heif4CharCode.Colr); WriteUInt32(writer, (uint)Heif4CharCode.Nclx); - WriteUInt16(writer, 1); - WriteUInt16(writer, 13); - WriteUInt16(writer, 6); - writer.Write((byte)0x80); + WriteUInt16(writer, (ushort)CicpColorPrimaries.ItuRBt709_6); + WriteUInt16(writer, (ushort)CicpTransferCharacteristics.Iec61966_2_1); + WriteUInt16(writer, (ushort)CicpMatrixCoefficients.ItuRBt601_7_525); + writer.Write(fullRangeFlag); EndBox(writer, color); long pixelAspectRatio = BeginBox(writer, Heif4CharCode.Pasp); - WriteUInt32(writer, 4); - WriteUInt32(writer, 3); + WriteUInt32(writer, SyntheticHorizontalPixelSpacing); + WriteUInt32(writer, SyntheticVerticalPixelSpacing); EndBox(writer, pixelAspectRatio); + const uint cleanApertureDenominator = 1; + const uint centeredCleanApertureOffset = 0; + long cleanAperture = BeginBox(writer, Heif4CharCode.Clap); WriteUInt32(writer, (uint)width); - WriteUInt32(writer, 1); + WriteUInt32(writer, cleanApertureDenominator); WriteUInt32(writer, (uint)height); - WriteUInt32(writer, 1); - WriteUInt32(writer, 0); - WriteUInt32(writer, 1); - WriteUInt32(writer, 0); - WriteUInt32(writer, 1); + WriteUInt32(writer, cleanApertureDenominator); + WriteUInt32(writer, centeredCleanApertureOffset); + WriteUInt32(writer, cleanApertureDenominator); + WriteUInt32(writer, centeredCleanApertureOffset); + WriteUInt32(writer, cleanApertureDenominator); EndBox(writer, cleanAperture); long rotation = BeginBox(writer, Heif4CharCode.Irot); - writer.Write(invalidRotation ? (byte)0xFC : (byte)1); + writer.Write(invalidRotation ? reservedRotationBits : rotateCounterClockwise90Degrees); EndBox(writer, rotation); long mirror = BeginBox(writer, Heif4CharCode.Imir); - writer.Write((byte)1); + writer.Write(verticalMirrorAxis); EndBox(writer, mirror); long contentLightLevel = BeginBox(writer, Heif4CharCode.Clli); - WriteUInt16(writer, 1000); - WriteUInt16(writer, 400); + WriteUInt16(writer, SyntheticMaximumContentLightLevel); + WriteUInt16(writer, SyntheticMaximumFrameAverageLightLevel); EndBox(writer, contentLightLevel); + // MasteringDisplayColorVolume stores chromaticity in 0.00002 increments and luminance in 0.0001 cd/m2. + const ushort redPrimaryX = 15_000; + const ushort redPrimaryY = 30_000; + const ushort greenPrimaryX = 7_500; + const ushort greenPrimaryY = 3_000; + const ushort bluePrimaryX = 34_000; + const ushort bluePrimaryY = 16_000; + const ushort whitePointX = 15_635; + const ushort whitePointY = 16_450; + const uint maximumDisplayLuminance = 10_000_000; + const uint minimumDisplayLuminance = 50; + long masteringDisplay = BeginBox(writer, Heif4CharCode.Mdcv); - WriteUInt16(writer, 15000); - WriteUInt16(writer, 30000); - WriteUInt16(writer, 7500); - WriteUInt16(writer, 3000); - WriteUInt16(writer, 34000); - WriteUInt16(writer, 16000); - WriteUInt16(writer, 15635); - WriteUInt16(writer, 16450); - WriteUInt32(writer, 10_000_000); - WriteUInt32(writer, 50); + WriteUInt16(writer, redPrimaryX); + WriteUInt16(writer, redPrimaryY); + WriteUInt16(writer, greenPrimaryX); + WriteUInt16(writer, greenPrimaryY); + WriteUInt16(writer, bluePrimaryX); + WriteUInt16(writer, bluePrimaryY); + WriteUInt16(writer, whitePointX); + WriteUInt16(writer, whitePointY); + WriteUInt32(writer, maximumDisplayLuminance); + WriteUInt32(writer, minimumDisplayLuminance); EndBox(writer, masteringDisplay); + const byte minimumLuminancePresentMask = 1 << 4; + const byte maximumLuminancePresentMask = 1 << 3; + const byte averageLuminancePresentMask = 1 << 2; + const uint minimumLuminanceValue = 1_000_000; + const uint maximumLuminanceValue = 10_000_000; + const uint averageLuminanceValue = 5_000_000; + long contentColorVolume = BeginBox(writer, Heif4CharCode.Cclv); - writer.Write((byte)0x1C); - WriteUInt32(writer, 1_000_000); - WriteUInt32(writer, 10_000_000); - WriteUInt32(writer, 5_000_000); + + // ISO/IEC 23000-22 declares the optional content-color-volume fields through bits 5..2. This payload carries + // all three luminance values and deliberately omits the much larger primary-chromaticity field set. + writer.Write((byte)(minimumLuminancePresentMask | maximumLuminancePresentMask | averageLuminancePresentMask)); + WriteUInt32(writer, minimumLuminanceValue); + WriteUInt32(writer, maximumLuminanceValue); + WriteUInt32(writer, averageLuminanceValue); EndBox(writer, contentColorVolume); + const uint ambientIlluminance = 10_000; + long ambientViewing = BeginBox(writer, Heif4CharCode.Amve); - WriteUInt32(writer, 10_000); - WriteUInt16(writer, 15_635); - WriteUInt16(writer, 16_450); + WriteUInt32(writer, ambientIlluminance); + WriteUInt16(writer, whitePointX); + WriteUInt16(writer, whitePointY); EndBox(writer, ambientViewing); + const uint referenceViewingIlluminance = 10_000; + const ushort referenceWhiteX = 3_127; + const ushort referenceWhiteY = 3_290; + const uint referenceBlackLuminance = 5_000; + long referenceViewing = BeginBox(writer, Heif4CharCode.Reve); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, 10_000); - WriteUInt16(writer, 3_127); - WriteUInt16(writer, 3_290); - WriteUInt32(writer, 5_000); - WriteUInt16(writer, 3_127); - WriteUInt16(writer, 3_290); + WriteUInt32(writer, referenceViewingIlluminance); + WriteUInt16(writer, referenceWhiteX); + WriteUInt16(writer, referenceWhiteY); + WriteUInt32(writer, referenceBlackLuminance); + WriteUInt16(writer, referenceWhiteX); + WriteUInt16(writer, referenceWhiteY); EndBox(writer, referenceViewing); + const uint nominalDiffuseWhiteLuminance = 2_030_000; + long nominalDiffuseWhite = BeginBox(writer, Heif4CharCode.Ndwt); WriteFullBoxHeader(writer, 0, 0); - WriteUInt32(writer, 2_030_000); + WriteUInt32(writer, nominalDiffuseWhiteLuminance); EndBox(writer, nominalDiffuseWhite); } + /// + /// Writes the minimal HEVCDecoderConfigurationRecord accepted by the parser, with no parameter-set arrays, + /// because sample-table tests exercise container timing rather than HEVC bitstream decoding. + /// + /// The writer receiving the HEVCConfigurationBox. private static void WriteHevcConfiguration(BinaryWriter writer) { + const byte configurationVersion = 1; + const byte mainProfileIdc = 1; + const ushort reservedMinSpatialSegmentationIdc = 0xF000; + const byte reservedParallelismType = 0xFC; + const byte reservedChromaFormat420 = 0xFD; + const byte reservedEightBitDepth = 0xF8; + const byte fourByteNalUnitLength = 3; + long configuration = BeginBox(writer, Heif4CharCode.HvcC); - writer.Write((byte)1); - writer.Write((byte)1); + + // ISO/IEC 14496-15 defines this HEVCDecoderConfigurationRecord layout. Only the profile and NAL-unit length + // fields are material to these container tests; compatibility, constraints, level, timing, and arrays are empty. + writer.Write(configurationVersion); + writer.Write(mainProfileIdc); WriteUInt32(writer, 0); WriteZeros(writer, 6); writer.Write((byte)0); - WriteUInt16(writer, 0xF000); - writer.Write((byte)0xFC); - writer.Write((byte)0xFD); - writer.Write((byte)0xF8); - writer.Write((byte)0xF8); + WriteUInt16(writer, reservedMinSpatialSegmentationIdc); + writer.Write(reservedParallelismType); + writer.Write(reservedChromaFormat420); + writer.Write(reservedEightBitDepth); + writer.Write(reservedEightBitDepth); WriteUInt16(writer, 0); - writer.Write((byte)3); + writer.Write(fourByteNalUnitLength); writer.Write((byte)0); EndBox(writer, configuration); } + /// + /// Starts a small ISO base media box with a placeholder 32-bit size that patches later. + /// + /// The writer receiving the box header. + /// The box's four-character type. + /// The stream position of the size field. private static long BeginBox(BinaryWriter writer, Heif4CharCode type) { long start = writer.BaseStream.Position; @@ -1142,6 +1762,11 @@ public class HeifSequenceParserTests return start; } + /// + /// Completes a box by patching its total byte length and restoring the writer to the end of the payload. + /// + /// The writer containing the box. + /// The stream position returned by . private static void EndBox(BinaryWriter writer, long start) { long end = writer.BaseStream.Position; @@ -1150,20 +1775,54 @@ public class HeifSequenceParserTests writer.BaseStream.Position = end; } + /// + /// Writes the FullBox version byte and low 24 flag bits as one big-endian word. + /// + /// The writer receiving the FullBox header. + /// The box syntax version. + /// The box-specific low 24 flag bits. private static void WriteFullBoxHeader(BinaryWriter writer, byte version, uint flags) => WriteUInt32(writer, ((uint)version << 24) | flags); + /// + /// Writes an unsigned 16-bit ISO base media field in network byte order. + /// + /// The little-endian binary writer receiving the field. + /// The host-order value. private static void WriteUInt16(BinaryWriter writer, ushort value) => writer.Write(BinaryPrimitives.ReverseEndianness(value)); + /// + /// Writes an unsigned 32-bit ISO base media field in network byte order. + /// + /// The little-endian binary writer receiving the field. + /// The host-order value. private static void WriteUInt32(BinaryWriter writer, uint value) => writer.Write(BinaryPrimitives.ReverseEndianness(value)); + /// + /// Writes reserved bytes whose governing box syntax requires all bits to be zero. + /// + /// The writer receiving the reserved bytes. + /// The number of reserved bytes. private static void WriteZeros(BinaryWriter writer, int count) => writer.Write(new byte[count]); + /// + /// Reads the synthetic movie box size and removes its standard eight-byte header to obtain the parser payload + /// boundary expected by . + /// + /// The synthetic file beginning with a MovieBox. + /// The validated movie payload length. private static int GetMoviePayloadLength(byte[] data) - => checked((int)BinaryPrimitives.ReadUInt32BigEndian(data) - 8); - + => checked((int)BinaryPrimitives.ReadUInt32BigEndian(data) - BoxHeaderLength); + + /// + /// Creates a sequence parser with the decoder limits and integrity policy exercised by a test. + /// + /// The maximum number of sequence frames to retain. + /// Whether optional metadata parsing is disabled. + /// The malformed-segment recovery policy. + /// The configured parser. private static HeifSequenceParser CreateParser( uint maxFrames, bool skipMetadata = false, diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 31d12cda3..2468d3722 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1295,6 +1295,12 @@ public static class TestImages // Extremely small image, 4x4 pixels with a single solid color. public const string Orange4x4 = "Heif/Orange4x4.avif"; + + // Sourced from libavif/tests/data under libavif's BSD-2-Clause license. + public const string Animated8Bit = "Heif/colors-animated-8bpc.avif"; + public const string Animated8BitWithAudio = "Heif/colors-animated-8bpc-audio.avif"; + public const string Animated8BitWithAlphaExifXmp = "Heif/colors-animated-8bpc-alpha-exif-xmp.avif"; + public const string Animated12BitWithKeyframes = "Heif/colors-animated-12bpc-keyframes-0-2-3.avif"; } public static class Ico diff --git a/tests/Images/Input/Heif/colors-animated-12bpc-keyframes-0-2-3.avif b/tests/Images/Input/Heif/colors-animated-12bpc-keyframes-0-2-3.avif new file mode 100644 index 000000000..1550d842f --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-12bpc-keyframes-0-2-3.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bf9f91da471749e7df639ba7945d4d94c1c3e3968c26f3619fbbcfc92790576 +size 2267 diff --git a/tests/Images/Input/Heif/colors-animated-8bpc-alpha-exif-xmp.avif b/tests/Images/Input/Heif/colors-animated-8bpc-alpha-exif-xmp.avif new file mode 100644 index 000000000..a05d5438b --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-8bpc-alpha-exif-xmp.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2e38681057c15009c4b76ea08cea68cdde80806abd41d42a646f697bf5aabb2 +size 7506 diff --git a/tests/Images/Input/Heif/colors-animated-8bpc-audio.avif b/tests/Images/Input/Heif/colors-animated-8bpc-audio.avif new file mode 100644 index 000000000..af83fa779 --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-8bpc-audio.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:624f3bfe78b6bd75e9e12fe9b36c6132e3effaf82aa2a443f1b2a207a7d3561b +size 3505 diff --git a/tests/Images/Input/Heif/colors-animated-8bpc.avif b/tests/Images/Input/Heif/colors-animated-8bpc.avif new file mode 100644 index 000000000..ac90131fc --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-8bpc.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f8683d21725261f37f86e115f0c212cc52d0fefd3a2ddfcc4fa648c1859906d +size 1235