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());