From e1a86f9defbcc3e9b87f988ffef5bfe9012eb7ac Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 1 Sep 2026 20:01:12 +1000 Subject: [PATCH] Finalize HEIF decoder support --- HEIF_IMPLEMENTATION_PLAN.md | 35 ++++++-- .../Formats/Heif/HeifConfigurationModule.cs | 3 +- .../Formats/Heif/HeifDecoderCore.cs | 73 +++++++++------ src/ImageSharp/Formats/Heif/HeifFormat.cs | 2 +- .../Formats/Heif/HeifImageFormatDetector.cs | 28 +++++- .../Formats/Heif/HeifPropertyParser.cs | 6 +- .../Formats/Heif/HeifSequenceParser.cs | 8 +- .../Metadata/Profiles/ICC/IccProfile.cs | 13 --- .../Formats/Heif/HeifDecoderTests.cs | 84 ++++++++++++++++- .../Formats/Heif/HeifSequenceParserTests.cs | 89 +++++++++++++++---- .../Formats/ImageFormatManagerTests.cs | 2 +- 11 files changed, 261 insertions(+), 82 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 7e08642f2..43a21f8f0 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -569,9 +569,9 @@ Previously verified algorithm checkpoints remain valuable evidence, but the fina - [x] The exact current-tree native-plane matrix passes through the production decoder on net10.0 and net11.0. The normal-dispatch and FeatureTestRunner fallback methods pass 2 of 2 focused tests on each target. - [x] The exact current-tree presentation matrix passes 12 of 12 cases through ImageSharp's established reference-image API on net10.0 and net11.0. - [x] Verify malformed/truncated data, frame IDs, reference slots, tile bounds, allocation limits, cancellation, and failure unwinding. -- [ ] Verify still items and bounded sequences from file, memory, non-seekable, and short-read streams. -- [ ] Verify ICC, CICP, alpha, grids, pixel aspect ratio, clean aperture, rotation, mirroring, metadata, and every presented sequence frame. -- [ ] Complete the public AVIF format/API review so registered capabilities match implemented behavior. +- [x] Verify still items and bounded sequences from file, memory, non-seekable, and short-read streams. +- [x] Verify ICC, CICP, alpha, grids, pixel aspect ratio, clean aperture, rotation, mirroring, metadata, and every presented sequence frame. +- [x] Complete the public AVIF format/API review so registered capabilities match implemented behavior. - [x] Remove or reject every valid in-scope AV1 syntax branch that remains silently ignored or unsupported. Verified negative-path and frame-identifier gate evidence on 2026-08-31: @@ -735,13 +735,32 @@ Final decoder allocation, lifetime, precision, architecture, and test-validity a Focused CDEF, restoration, film-grain, copy-ownership, and reference-isolation runs also pass 15 of 15 cases. No test-host crash or Windows application-error dialog occurred. +Final decoder stream, presentation, and public-registration evidence on 2026-09-01: + +- [x] Real AV1 still-item and timed-sequence files decode identically from a file stream, memory stream, + non-seekable stream, and a seekable stream limited to three bytes per read. All eight stream rows pass + through public format detection and production decoding, comparing every presented frame exactly. +- [x] A two-frame production sequence applies a centered clean-aperture crop, counter-clockwise rotation, + mirroring, pixel-aspect-ratio metadata, and CICP metadata to every frame. The complete five-frame real + auxiliary-alpha sequence composes non-opaque alpha and retains timing, Exif, and XMP for every frame. +- [x] The fixed-header detector accepts both compact and extended-size leading file-type boxes. Default + configuration registers the implemented HEIF decoder and detector but no longer advertises the + incomplete HEIF encoder. +- [x] Visual Studio 18.9 VSTest, serialized with stop-on-failure enabled, passes the 12 of 12 new + stream/presentation/registration cases and the complete current `HeifDecoderTests` plus + `HeifSequenceParserTests` set with the registration contract: 115 of 115. The final explicit + no-encoder registration assertion passes 1 of 1 after its final edit. +- [x] The net11.0 Release test project builds with zero errors, Roslynk reports zero compiler errors, + `git diff --check` passes, and `.gitattributes` is unchanged. Every VSTest invocation returned + normally with no surviving test host and no Windows application-error dialog. + Decoder exit gate: -- [ ] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. -- [ ] Every supported presentation behavior has established reference-image evidence at the correct output precision. -- [ ] No decoder path relies on a native codec, copied plane, per-block allocation, or contiguous memory-group accident. -- [ ] All allocator ownership is deterministic and exactly once. -- [ ] Full focused Release verification is recorded with no false coverage claims. +- [x] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. +- [x] Every supported presentation behavior has established reference-image evidence at the correct output precision. +- [x] No decoder path relies on a native codec, copied plane, per-block allocation, or contiguous memory-group accident. +- [x] All allocator ownership is deterministic and exactly once. +- [x] Full focused Release verification is recorded with no false coverage claims. ## AV1 encoder implementation diff --git a/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs b/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs index e08418bdb..f0ceb30e8 100644 --- a/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs +++ b/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs @@ -4,14 +4,13 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Registers the image encoders, decoders and mime type detectors for the HEIF format. +/// Configures HEIF image-format support. /// public sealed class HeifConfigurationModule : IImageFormatConfigurationModule { /// public void Configure(Configuration configuration) { - configuration.ImageFormatsManager.SetEncoder(HeifFormat.Instance, new HeifEncoder()); configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance); configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector()); } diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 0cf3db3dd..c83f5e85f 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -661,8 +661,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return; } - metadata.IccProfile = colorTrack.IccProfile?.DeepClone(); - metadata.CicpProfile = colorTrack.CicpProfile?.DeepClone(); + // The selected track is decoder-private and no longer mutates after parsing. Reuse its profiles so the + // returned metadata does not duplicate their storage. + metadata.IccProfile = colorTrack.IccProfile; + metadata.CicpProfile = colorTrack.CicpProfile; heifMetadata.ContentLightLevel = colorTrack.ContentLightLevel; heifMetadata.MasteringDisplayColorVolume = colorTrack.MasteringDisplayColorVolume; heifMetadata.ContentColorVolume = colorTrack.ContentColorVolume; @@ -1271,6 +1273,41 @@ internal sealed class HeifDecoderCore : ImageDecoderCore stream.Position -= 4; } + if (!this.Options.SkipMetadata && itemType == Heif4CharCode.Colr && itemLength is >= 4 and <= int.MaxValue) + { + Span profileTypeBuffer = this.boxHeaderScratch.AsSpan(0, 4); + HeifBoxReader.ReadExactly(stream, profileTypeBuffer, "The HEIF color-information property is truncated."); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(profileTypeBuffer); + + if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) + { + // Read directly into the array retained by IccProfile so the generic box buffer cannot create a + // second full-sized copy of the profile at this ownership boundary. + byte[] profileData = new byte[(int)itemLength - 4]; + HeifBoxReader.ReadExactly(stream, profileData, "Stream length is not sufficient for box content."); + IccProfile? iccProfile = null; + + try + { + iccProfile = HeifPropertyParser.ParseIccProfile(profileData); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + // A malformed ancillary profile can be ignored by policy while the physical property still + // occupies its ipco index and remains understood for essential-association handling. + properties.Add(new KeyValuePair( + Heif4CharCode.Colr, + iccProfile ?? IgnoredProperty)); + + continue; + } + + stream.Position -= 4; + } + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, itemLength); Span boxBuffer = boxMemory.GetSpan(); try @@ -1355,31 +1392,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore EnsureBufferRemaining(boxBuffer, 0, 4, "color information"); Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); object colorInformation = UnknownProperty; - if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) - { - if (!this.Options.SkipMetadata) - { - EnsureBufferRemaining(boxBuffer, 4, 1, "ICC color information"); - IccProfile? iccProfile = null; - try - { - iccProfile = HeifPropertyParser.ParseIccProfile(boxBuffer[4..]); - } - catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) - { - // Keep the understood property index without retaining invalid ancillary metadata. - } - - // A malformed ancillary profile can be ignored by policy while the physical property still - // occupies its ipco index and remains understood for essential-association handling. - colorInformation = iccProfile ?? IgnoredProperty; - } - else - { - colorInformation = IgnoredProperty; - } - } - else if (profileType == Heif4CharCode.Nclx) + if (profileType == Heif4CharCode.Nclx) { colorInformation = HeifPropertyParser.ParseCicpProfile(boxBuffer[4..]); } @@ -2346,16 +2359,18 @@ internal sealed class HeifDecoderCore : ImageDecoderCore ? this.FindDecodableGridTile(colorItem) : null; + // The associated item model is decoder-private and no longer mutates after property resolution. Reuse its + // profiles so the returned metadata does not duplicate their storage. IccProfile? iccProfile = colorItem.IccProfile ?? gridTile?.IccProfile; if (iccProfile is not null) { - metadata.IccProfile = iccProfile.DeepClone(); + metadata.IccProfile = iccProfile; } CicpProfile? cicpProfile = colorItem.CicpProfile ?? gridTile?.CicpProfile; if (cicpProfile is not null) { - metadata.CicpProfile = cicpProfile.DeepClone(); + metadata.CicpProfile = cicpProfile; } } diff --git a/src/ImageSharp/Formats/Heif/HeifFormat.cs b/src/ImageSharp/Formats/Heif/HeifFormat.cs index d97ecd0e7..55e57207e 100644 --- a/src/ImageSharp/Formats/Heif/HeifFormat.cs +++ b/src/ImageSharp/Formats/Heif/HeifFormat.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Registers the image encoders, decoders and mime type detectors for the HEIF format. +/// Represents the HEIF image format. /// public sealed class HeifFormat : IImageFormat { diff --git a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs index a8b07402f..968f810e0 100644 --- a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs @@ -35,16 +35,36 @@ public sealed class HeifImageFormatDetector : IImageFormatDetector return false; } - uint boxSize = BinaryPrimitives.ReadUInt32BigEndian(header); - if (boxSize < 16 || ((boxSize - 16) & 3) != 0) + uint compactBoxSize = BinaryPrimitives.ReadUInt32BigEndian(header); + int boxHeaderSize = 8; + ulong boxSize = compactBoxSize; + if (compactBoxSize == 1) + { + // An extended-size box inserts its 64-bit size before the normal ftyp payload. + if (header.Length < 24) + { + return false; + } + + boxHeaderSize = 16; + boxSize = BinaryPrimitives.ReadUInt64BigEndian(header[8..]); + } + + if (boxSize < (uint)(boxHeaderSize + 8)) + { + return false; + } + + ulong boxContentLength = boxSize - (uint)boxHeaderSize; + if ((boxContentLength & 3) != 0) { return false; } // HeaderSize may expose only a prefix of a longer ftyp box. Whole compatible-brand codes in that prefix are // sufficient for detection; the decoder validates the complete box before reading the rest of the container. - int availableContentLength = (int)Math.Min(boxSize - 8, (uint)header.Length - 8); + int availableContentLength = (int)Math.Min(boxContentLength, (ulong)(header.Length - boxHeaderSize)); availableContentLength &= ~3; - return HeifConstants.TryGetFileType(header.Slice(8, availableContentLength), out _); + return HeifConstants.TryGetFileType(header.Slice(boxHeaderSize, availableContentLength), out _); } } diff --git a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs index 482b7f701..31a431b0b 100644 --- a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs @@ -37,15 +37,15 @@ internal static class HeifPropertyParser /// /// The complete ICC profile bytes. /// The validated ICC profile. - public static IccProfile ParseIccProfile(ReadOnlySpan data) + public static IccProfile ParseIccProfile(byte[] data) { if (data.Length == 0) { throw new InvalidImageContentException("The HEIF ICC color property contains an empty profile."); } - // The source belongs to a pooled box-reader buffer. The span constructor performs the single ownership transfer - // required for the profile to retain its exact bytes after that buffer is returned and reused. + // The HEIF parser allocates this exact array as the profile's final storage, so IccProfile can adopt it without + // copying the potentially large profile payload. IccProfile profile = new(data); if (!profile.CheckIsValid()) { diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs index c9b2fb4a6..be39e4751 100644 --- a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -1137,9 +1137,11 @@ internal sealed class HeifSequenceParser throw new InvalidImageContentException("The ICC color-information property is empty or too large."); } - stream.Position -= 4; - using IMemoryOwner payload = this.boxReader.ReadPayload(stream, boxLength); - track.IccProfile = HeifPropertyParser.ParseIccProfile(payload.GetSpan()[4..]); + // Read directly into the array retained by IccProfile so the generic box buffer cannot create a + // second full-sized copy of the profile at this ownership boundary. + byte[] profileData = new byte[(int)boxLength - 4]; + HeifBoxReader.ReadExactly(stream, profileData, "Stream length is not sufficient for box content."); + track.IccProfile = HeifPropertyParser.ParseIccProfile(profileData); } catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index f0325e720..eaba0a045 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -40,19 +40,6 @@ public sealed partial class IccProfile : IDeepCloneable /// The raw ICC profile data public IccProfile(byte[] data) => this.data = data; - /// - /// Initializes a new instance of the class from raw ICC profile data whose source storage does not need to - /// remain valid for the lifetime of the profile. - /// - /// The raw ICC profile data. - public IccProfile(ReadOnlySpan data) - { - // A span cannot transfer ownership, while IccProfile retains the exact bytes for lazy parsing and byte-for-byte serialization. - // The destination has exactly data.Length elements, so CopyTo immediately overwrites every byte of the uninitialized array. - this.data = GC.AllocateUninitializedArray(data.Length); - data.CopyTo(this.data); - } - /// /// Initializes a new instance of the class. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 18b3be9e2..9147fdeaf 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -12,7 +12,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; -using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Formats.Heif; @@ -51,6 +50,49 @@ public class HeifDecoderTests Assert.Equal(height, imageInfo.Height); } + [Theory] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.File, 1, 4, 4)] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.Memory, 1, 4, 4)] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.NonSeekable, 1, 4, 4)] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.ShortRead, 1, 4, 4)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.File, 5, 150, 150)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.Memory, 5, 150, 150)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.NonSeekable, 5, 150, 150)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.ShortRead, 5, 150, 150)] + public void DecodeStillAndBoundedSequenceFromSupportedStream( + string imagePath, + DecoderStreamKind streamKind, + int expectedFrameCount, + int expectedWidth, + int expectedHeight) + { + TestFile testFile = TestFile.Create(imagePath); + using Image expected = Image.Load(testFile.Bytes); + using Stream stream = streamKind switch + { + DecoderStreamKind.File => File.OpenRead(testFile.FullPath), + DecoderStreamKind.Memory => new MemoryStream(testFile.Bytes, false), + DecoderStreamKind.NonSeekable => new NonSeekableStream(new MemoryStream(testFile.Bytes, false)), + DecoderStreamKind.ShortRead => new ShortReadMemoryStream(testFile.Bytes), + _ => throw new InvalidOperationException() + }; + + using Image actual = Image.Load(stream); + + Assert.Equal(new Size(expectedWidth, expectedHeight), actual.Size); + Assert.Equal(expectedFrameCount, actual.Frames.Count); + Assert.Equal(expected.Frames.Count, actual.Frames.Count); + for (int frameIndex = 0; frameIndex < actual.Frames.Count; frameIndex++) + { + for (int y = 0; y < actual.Height; y++) + { + Assert.True( + expected.Frames[frameIndex].PixelBuffer.DangerousGetRowSpan(y) + .SequenceEqual(actual.Frames[frameIndex].PixelBuffer.DangerousGetRowSpan(y))); + } + } + } + /// /// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes. /// @@ -661,6 +703,22 @@ public class HeifDecoderTests Assert.Same(HeifFormat.Instance, format); } + [Fact] + public void DetectorRecognizesExtendedSizeFileTypeBox() + { + byte[] data = new byte[24]; + BinaryPrimitives.WriteUInt32BigEndian(data, 1); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), (uint)Heif4CharCode.Ftyp); + BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan(8), (ulong)data.Length); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), (uint)Heif4CharCode.Avif); + HeifImageFormatDetector detector = new(); + + bool detected = detector.TryDetectFormat(data, out IImageFormat format); + + Assert.True(detected); + Assert.Same(HeifFormat.Instance, format); + } + [Theory] [InlineData(Heif4CharCode.Jpgs)] public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand) @@ -1186,4 +1244,28 @@ public class HeifDecoderTests uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset)); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), size + (uint)increment); } + + public enum DecoderStreamKind + { + File, + Memory, + NonSeekable, + ShortRead + } + + private sealed class ShortReadMemoryStream : MemoryStream + { + private const int MaximumReadLength = 3; + + public ShortReadMemoryStream(byte[] data) + : base(data, false) + { + } + + public override int Read(byte[] buffer, int offset, int count) + => base.Read(buffer, offset, Math.Min(count, MaximumReadLength)); + + public override int Read(Span buffer) + => base.Read(buffer[..Math.Min(buffer.Length, MaximumReadLength)]); + } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs index cffc34fa0..65e75cbb5 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs @@ -5,8 +5,10 @@ using System.Buffers.Binary; using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Tests.Formats.Heif; @@ -228,6 +230,48 @@ public class HeifSequenceParserTests } } + [Fact] + public void DecodeAppliesTrackPresentationPropertiesToEveryFrame() + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength), + trackProperties: true); + + int cleanApertureTypeOffset = data.AsSpan().IndexOf("clap"u8); + Assert.True(cleanApertureTypeOffset >= 0); + + // Narrow the synthetic full-frame aperture to its centered 2x2 region without changing the coded AV1 sample. + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(cleanApertureTypeOffset + 4), 2); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(cleanApertureTypeOffset + 12), 2); + + using Image expected = Image.Load(source); + expected.Mutate(context => context + .Crop(new Rectangle(1, 1, 2, 2)) + .Rotate(RotateMode.Rotate270) + .Flip(FlipMode.Horizontal)); + + using Image actual = Image.Load(data); + + Assert.Equal(expected.Size, actual.Size); + Assert.Equal(2, actual.Frames.Count); + Assert.Equal(4D, actual.Metadata.HorizontalResolution); + Assert.Equal(3D, actual.Metadata.VerticalResolution); + Assert.Equal(PixelResolutionUnit.AspectRatio, actual.Metadata.ResolutionUnits); + Assert.NotNull(actual.Metadata.CicpProfile); + for (int frameIndex = 0; frameIndex < actual.Frames.Count; frameIndex++) + { + Assert.NotNull(actual.Frames[frameIndex].Metadata.CicpProfile); + for (int y = 0; y < actual.Height; y++) + { + Assert.True( + expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y) + .SequenceEqual(actual.Frames[frameIndex].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. @@ -289,33 +333,39 @@ public class HeifSequenceParserTests } /// - /// Verifies that a genuine libavif alpha sequence composes its first retained frame from the linked monochrome - /// auxiliary track instead of returning the color frame as opaque. + /// Verifies that a genuine libavif alpha sequence composes every retained frame from the linked monochrome + /// auxiliary track instead of returning any color frame as opaque. /// [Fact] - public void DecodeComposesFirstRealLibavifAlphaSequenceFrame() + public void DecodeComposesEveryRealLibavifAlphaSequenceFrame() { - DecoderOptions options = new() { MaxFrames = 1 }; TestFile file = TestFile.Create(TestImages.Heif.Animated8BitWithAlphaExifXmp); - using Image image = Image.Load(options, file.Bytes); + using Image image = Image.Load(file.Bytes); - Assert.Single(image.Frames); + Assert.Equal(LibavifAnimationFrameCount, image.Frames.Count); Assert.True(image.Metadata.GetHeifMetadata().HasAlpha); - bool hasNonOpaqueSample = false; - for (int y = 0; y < image.Height && !hasNonOpaqueSample; y++) + Assert.NotNull(image.Metadata.ExifProfile); + Assert.NotNull(image.Metadata.XmpProfile); + foreach (ImageFrame frame in image.Frames) { - foreach (Rgba32 pixel in image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y)) + bool hasNonOpaqueSample = false; + for (int y = 0; y < frame.Height && !hasNonOpaqueSample; y++) { - if (pixel.A != byte.MaxValue) + foreach (Rgba32 pixel in frame.PixelBuffer.DangerousGetRowSpan(y)) { - hasNonOpaqueSample = true; - break; + if (pixel.A != byte.MaxValue) + { + hasNonOpaqueSample = true; + break; + } } } - } - Assert.True(hasNonOpaqueSample); + Assert.True(hasNonOpaqueSample); + Assert.True(frame.Metadata.GetHeifMetadata().FrameDelay.Numerator > 0); + Assert.True(frame.Metadata.GetHeifMetadata().FrameDelay.Denominator > 0); + } } /// @@ -963,8 +1013,11 @@ public class HeifSequenceParserTests /// 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); + private static byte[] CreateDecodableAv1SequenceContainer( + ReadOnlySpan sample, + ReadOnlySpan configuration, + bool trackProperties = false) + => CreateDecodableAv1SequenceContainer(sample, sample, configuration, true, trackProperties); /// /// Builds a two-frame AVIF sequence with caller-provided AV1 samples so integrity tests can corrupt one sample @@ -979,11 +1032,13 @@ public class HeifSequenceParserTests ReadOnlySpan firstSample, ReadOnlySpan secondSample, ReadOnlySpan configuration, - bool allSamplesSync) + bool allSamplesSync, + bool trackProperties = false) { uint chunkOffset = FileTypeBoxLength + SyntheticFileLength; byte[] movie = CreateSequenceFile( chunkOffset, + trackProperties: trackProperties, width: 4, height: 4, av1Configuration: configuration.ToArray(), diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 146898b46..3fd90b36a 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -38,7 +38,7 @@ public class ImageFormatManagerTests Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); - Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); + Assert.Empty(this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count());