Browse Source

Finalize HEIF decoder support

pull/2633/head
James Jackson-South 1 day ago
parent
commit
e1a86f9def
  1. 35
      HEIF_IMPLEMENTATION_PLAN.md
  2. 3
      src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs
  3. 73
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  4. 2
      src/ImageSharp/Formats/Heif/HeifFormat.cs
  5. 28
      src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs
  6. 6
      src/ImageSharp/Formats/Heif/HeifPropertyParser.cs
  7. 8
      src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
  8. 13
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  9. 84
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  10. 89
      tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs
  11. 2
      tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

35
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 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] 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. - [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. - [x] 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. - [x] 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] 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. - [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: 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 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. 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: Decoder exit gate:
- [ ] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. - [x] 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. - [x] 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. - [x] 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. - [x] All allocator ownership is deterministic and exactly once.
- [ ] Full focused Release verification is recorded with no false coverage claims. - [x] Full focused Release verification is recorded with no false coverage claims.
## AV1 encoder implementation ## AV1 encoder implementation

3
src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs

@ -4,14 +4,13 @@
namespace SixLabors.ImageSharp.Formats.Heif; namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the HEIF format. /// Configures HEIF image-format support.
/// </summary> /// </summary>
public sealed class HeifConfigurationModule : IImageFormatConfigurationModule public sealed class HeifConfigurationModule : IImageFormatConfigurationModule
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(Configuration configuration) public void Configure(Configuration configuration)
{ {
configuration.ImageFormatsManager.SetEncoder(HeifFormat.Instance, new HeifEncoder());
configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance); configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector()); configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector());
} }

73
src/ImageSharp/Formats/Heif/HeifDecoderCore.cs

@ -661,8 +661,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
return; return;
} }
metadata.IccProfile = colorTrack.IccProfile?.DeepClone(); // The selected track is decoder-private and no longer mutates after parsing. Reuse its profiles so the
metadata.CicpProfile = colorTrack.CicpProfile?.DeepClone(); // returned metadata does not duplicate their storage.
metadata.IccProfile = colorTrack.IccProfile;
metadata.CicpProfile = colorTrack.CicpProfile;
heifMetadata.ContentLightLevel = colorTrack.ContentLightLevel; heifMetadata.ContentLightLevel = colorTrack.ContentLightLevel;
heifMetadata.MasteringDisplayColorVolume = colorTrack.MasteringDisplayColorVolume; heifMetadata.MasteringDisplayColorVolume = colorTrack.MasteringDisplayColorVolume;
heifMetadata.ContentColorVolume = colorTrack.ContentColorVolume; heifMetadata.ContentColorVolume = colorTrack.ContentColorVolume;
@ -1271,6 +1273,41 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
stream.Position -= 4; stream.Position -= 4;
} }
if (!this.Options.SkipMetadata && itemType == Heif4CharCode.Colr && itemLength is >= 4 and <= int.MaxValue)
{
Span<byte> 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, object>(
Heif4CharCode.Colr,
iccProfile ?? IgnoredProperty));
continue;
}
stream.Position -= 4;
}
using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, itemLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, itemLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
try try
@ -1355,31 +1392,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
EnsureBufferRemaining(boxBuffer, 0, 4, "color information"); EnsureBufferRemaining(boxBuffer, 0, 4, "color information");
Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer);
object colorInformation = UnknownProperty; object colorInformation = UnknownProperty;
if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) if (profileType == Heif4CharCode.Nclx)
{
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)
{ {
colorInformation = HeifPropertyParser.ParseCicpProfile(boxBuffer[4..]); colorInformation = HeifPropertyParser.ParseCicpProfile(boxBuffer[4..]);
} }
@ -2346,16 +2359,18 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
? this.FindDecodableGridTile<Rgba32>(colorItem) ? this.FindDecodableGridTile<Rgba32>(colorItem)
: null; : 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; IccProfile? iccProfile = colorItem.IccProfile ?? gridTile?.IccProfile;
if (iccProfile is not null) if (iccProfile is not null)
{ {
metadata.IccProfile = iccProfile.DeepClone(); metadata.IccProfile = iccProfile;
} }
CicpProfile? cicpProfile = colorItem.CicpProfile ?? gridTile?.CicpProfile; CicpProfile? cicpProfile = colorItem.CicpProfile ?? gridTile?.CicpProfile;
if (cicpProfile is not null) if (cicpProfile is not null)
{ {
metadata.CicpProfile = cicpProfile.DeepClone(); metadata.CicpProfile = cicpProfile;
} }
} }

2
src/ImageSharp/Formats/Heif/HeifFormat.cs

@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Formats.Heif; namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the HEIF format. /// Represents the HEIF image format.
/// </summary> /// </summary>
public sealed class HeifFormat : IImageFormat<HeifMetadata, HeifFrameMetadata> public sealed class HeifFormat : IImageFormat<HeifMetadata, HeifFrameMetadata>
{ {

28
src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs

@ -35,16 +35,36 @@ public sealed class HeifImageFormatDetector : IImageFormatDetector
return false; return false;
} }
uint boxSize = BinaryPrimitives.ReadUInt32BigEndian(header); uint compactBoxSize = BinaryPrimitives.ReadUInt32BigEndian(header);
if (boxSize < 16 || ((boxSize - 16) & 3) != 0) 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; return false;
} }
// HeaderSize may expose only a prefix of a longer ftyp box. Whole compatible-brand codes in that prefix are // 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. // 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; availableContentLength &= ~3;
return HeifConstants.TryGetFileType(header.Slice(8, availableContentLength), out _); return HeifConstants.TryGetFileType(header.Slice(boxHeaderSize, availableContentLength), out _);
} }
} }

6
src/ImageSharp/Formats/Heif/HeifPropertyParser.cs

@ -37,15 +37,15 @@ internal static class HeifPropertyParser
/// </summary> /// </summary>
/// <param name="data">The complete ICC profile bytes.</param> /// <param name="data">The complete ICC profile bytes.</param>
/// <returns>The validated ICC profile.</returns> /// <returns>The validated ICC profile.</returns>
public static IccProfile ParseIccProfile(ReadOnlySpan<byte> data) public static IccProfile ParseIccProfile(byte[] data)
{ {
if (data.Length == 0) if (data.Length == 0)
{ {
throw new InvalidImageContentException("The HEIF ICC color property contains an empty profile."); 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 // The HEIF parser allocates this exact array as the profile's final storage, so IccProfile can adopt it without
// required for the profile to retain its exact bytes after that buffer is returned and reused. // copying the potentially large profile payload.
IccProfile profile = new(data); IccProfile profile = new(data);
if (!profile.CheckIsValid()) if (!profile.CheckIsValid())
{ {

8
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."); throw new InvalidImageContentException("The ICC color-information property is empty or too large.");
} }
stream.Position -= 4; // Read directly into the array retained by IccProfile so the generic box buffer cannot create a
using IMemoryOwner<byte> payload = this.boxReader.ReadPayload(stream, boxLength); // second full-sized copy of the profile at this ownership boundary.
track.IccProfile = HeifPropertyParser.ParseIccProfile(payload.GetSpan()[4..]); 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)) catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex))
{ {

13
src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs

@ -40,19 +40,6 @@ public sealed partial class IccProfile : IDeepCloneable<IccProfile>
/// <param name="data">The raw ICC profile data</param> /// <param name="data">The raw ICC profile data</param>
public IccProfile(byte[] data) => this.data = data; public IccProfile(byte[] data) => this.data = data;
/// <summary>
/// Initializes a new instance of the <see cref="IccProfile"/> class from raw ICC profile data whose source storage does not need to
/// remain valid for the lifetime of the profile.
/// </summary>
/// <param name="data">The raw ICC profile data.</param>
public IccProfile(ReadOnlySpan<byte> 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<byte>(data.Length);
data.CopyTo(this.data);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="IccProfile"/> class. /// Initializes a new instance of the <see cref="IccProfile"/> class.
/// </summary> /// </summary>

84
tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

@ -12,7 +12,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; using SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
namespace SixLabors.ImageSharp.Tests.Formats.Heif; namespace SixLabors.ImageSharp.Tests.Formats.Heif;
@ -51,6 +50,49 @@ public class HeifDecoderTests
Assert.Equal(height, imageInfo.Height); 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<Rgba32> expected = Image.Load<Rgba32>(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<Rgba32> actual = Image.Load<Rgba32>(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)));
}
}
}
/// <summary> /// <summary>
/// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes. /// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes.
/// </summary> /// </summary>
@ -661,6 +703,22 @@ public class HeifDecoderTests
Assert.Same(HeifFormat.Instance, format); 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] [Theory]
[InlineData(Heif4CharCode.Jpgs)] [InlineData(Heif4CharCode.Jpgs)]
public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand) public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand)
@ -1186,4 +1244,28 @@ public class HeifDecoderTests
uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset)); uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset));
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), size + (uint)increment); 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<byte> buffer)
=> base.Read(buffer[..Math.Min(buffer.Length, MaximumReadLength)]);
}
} }

89
tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs

@ -5,8 +5,10 @@ using System.Buffers.Binary;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Tests.Formats.Heif; 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<Rgba32> expected = Image.Load<Rgba32>(source);
expected.Mutate(context => context
.Crop(new Rectangle(1, 1, 2, 2))
.Rotate(RotateMode.Rotate270)
.Flip(FlipMode.Horizontal));
using Image<Rgba32> actual = Image.Load<Rgba32>(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)));
}
}
}
/// <summary> /// <summary>
/// Verifies that strict and ancillary-tolerant decoding both reject corrupt coded image data because neither /// 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. /// integrity mode permits recovery from errors in a retained AV1 sample.
@ -289,33 +333,39 @@ public class HeifSequenceParserTests
} }
/// <summary> /// <summary>
/// Verifies that a genuine libavif alpha sequence composes its first retained frame from the linked monochrome /// Verifies that a genuine libavif alpha sequence composes every retained frame from the linked monochrome
/// auxiliary track instead of returning the color frame as opaque. /// auxiliary track instead of returning any color frame as opaque.
/// </summary> /// </summary>
[Fact] [Fact]
public void DecodeComposesFirstRealLibavifAlphaSequenceFrame() public void DecodeComposesEveryRealLibavifAlphaSequenceFrame()
{ {
DecoderOptions options = new() { MaxFrames = 1 };
TestFile file = TestFile.Create(TestImages.Heif.Animated8BitWithAlphaExifXmp); TestFile file = TestFile.Create(TestImages.Heif.Animated8BitWithAlphaExifXmp);
using Image<Rgba32> image = Image.Load<Rgba32>(options, file.Bytes); using Image<Rgba32> image = Image.Load<Rgba32>(file.Bytes);
Assert.Single(image.Frames); Assert.Equal(LibavifAnimationFrameCount, image.Frames.Count);
Assert.True(image.Metadata.GetHeifMetadata().HasAlpha); Assert.True(image.Metadata.GetHeifMetadata().HasAlpha);
bool hasNonOpaqueSample = false; Assert.NotNull(image.Metadata.ExifProfile);
for (int y = 0; y < image.Height && !hasNonOpaqueSample; y++) Assert.NotNull(image.Metadata.XmpProfile);
foreach (ImageFrame<Rgba32> 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; if (pixel.A != byte.MaxValue)
break; {
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);
}
} }
/// <summary> /// <summary>
@ -963,8 +1013,11 @@ public class HeifSequenceParserTests
/// <param name="sample">The complete AV1 sample payload.</param> /// <param name="sample">The complete AV1 sample payload.</param>
/// <param name="configuration">The AV1CodecConfigurationBox payload describing the sample.</param> /// <param name="configuration">The AV1CodecConfigurationBox payload describing the sample.</param>
/// <returns>The complete synthetic AVIF byte stream.</returns> /// <returns>The complete synthetic AVIF byte stream.</returns>
private static byte[] CreateDecodableAv1SequenceContainer(ReadOnlySpan<byte> sample, ReadOnlySpan<byte> configuration) private static byte[] CreateDecodableAv1SequenceContainer(
=> CreateDecodableAv1SequenceContainer(sample, sample, configuration, true); ReadOnlySpan<byte> sample,
ReadOnlySpan<byte> configuration,
bool trackProperties = false)
=> CreateDecodableAv1SequenceContainer(sample, sample, configuration, true, trackProperties);
/// <summary> /// <summary>
/// Builds a two-frame AVIF sequence with caller-provided AV1 samples so integrity tests can corrupt one sample /// 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<byte> firstSample, ReadOnlySpan<byte> firstSample,
ReadOnlySpan<byte> secondSample, ReadOnlySpan<byte> secondSample,
ReadOnlySpan<byte> configuration, ReadOnlySpan<byte> configuration,
bool allSamplesSync) bool allSamplesSync,
bool trackProperties = false)
{ {
uint chunkOffset = FileTypeBoxLength + SyntheticFileLength; uint chunkOffset = FileTypeBoxLength + SyntheticFileLength;
byte[] movie = CreateSequenceFile( byte[] movie = CreateSequenceFile(
chunkOffset, chunkOffset,
trackProperties: trackProperties,
width: 4, width: 4,
height: 4, height: 4,
av1Configuration: configuration.ToArray(), av1Configuration: configuration.ToArray(),

2
tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

@ -38,7 +38,7 @@ public class ImageFormatManagerTests
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<BmpEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<BmpEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<JpegEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<JpegEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<GifEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<GifEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<HeifEncoder>().Count()); Assert.Empty(this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<HeifEncoder>());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TgaEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TgaEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TiffEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TiffEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<WebpEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<WebpEncoder>().Count());

Loading…
Cancel
Save