// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers.Binary; using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Formats.Heif; [Trait("Format", "Heif")] [ValidateDisposedMemoryAllocations] public class HeifDecoderTests { private const uint UnknownBoxType = 0x74657374U; private const HwIntrinsics HevcPresentationConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; private static ReadOnlySpan MalformedJpegApp13 => [ 0xFF, 0xED, 0x00, 0x1D, (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', 0x00, (byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', (byte)'!' ]; [Theory] [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 3992, 2992)] [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 640, 428)] [InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg, HeifBitDepth.Bit8, 7728, 5152)] [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, HeifBitDepth.Bit8, 480, 640)] public void Identify(string imagePath, HeifCompressionMethod compressionMethod, HeifBitDepth bitDepth, int width, int height) { TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); ImageInfo imageInfo = Image.Identify(stream); HeifMetadata heicMetadata = imageInfo.Metadata.GetHeifMetadata(); Assert.NotNull(imageInfo); Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat); Assert.Equal(compressionMethod, heicMetadata.CompressionMethod); Assert.Equal(bitDepth, heicMetadata.BitDepth); Assert.Equal(width, imageInfo.Width); Assert.Equal(height, imageInfo.Height); } [Theory] [WithFile(TestImages.Heif.FujiFilmHif, PixelTypes.Rgba32)] public void Decode(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); HeifMetadata heicMetadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); image.CompareToReferenceOutput(provider); Assert.Equal(HeifCompressionMethod.LegacyJpeg, heicMetadata.CompressionMethod); } /// /// Verifies complete HEVC still-image decoding for real grid, auxiliary-alpha, 4:2:0, and 4:4:4 HEIC inputs. /// /// The real HEIC input and matching output naming context. /// The independently reported presented width. /// The independently reported presented height. /// Whether the presented image carries an ICC profile. [Theory] [WithFile(TestImages.Heif.Image1, PixelTypes.Rgba32, 3992, 2992, true)] [WithFile(TestImages.Heif.Image2, PixelTypes.Rgba32, 3464, 2130, true)] [WithFile(TestImages.Heif.Image3, PixelTypes.Rgba32, 4242, 2828, true)] [WithFile(TestImages.Heif.Image4, PixelTypes.Rgba32, 700, 476, true)] [WithFile(TestImages.Heif.Sample640x427, PixelTypes.Rgba32, 640, 428, false)] public void DecodeHevcStillImage(TestImageProvider provider, int width, int height, bool hasIccProfile) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); Assert.Equal(new Size(width, height), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(hasIccProfile, image.Metadata.IccProfile is not null); } /// /// Verifies genuine HEIC presentation for the official HEVC profile and Range Extensions matrix against /// independently pinned presentation references. /// /// The genuine HEIC input and matching reference-output naming context. /// The independently reported presented width. /// The independently reported presented height. /// The maximum coded component precision. [Theory] [WithFile(TestImages.Heif.General8Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.General10Bit420Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.General10Bit422Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.General10Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.General12Bit420Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.General12Bit422Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.General12Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.RangeExtensionChromaAngle422Heic, PixelTypes.Rgba32, 1920, 1080, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.RangeExtensionCrossComponent8Bit444Heic, PixelTypes.Rgba32, 1280, 720, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.RangeExtensionCrossComponent10Bit444Heic, PixelTypes.Rgba32, 1920, 1080, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.RangeExtensionCrossComponent12Bit444Heic, PixelTypes.Rgba32, 2560, 1600, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.ExtendedPrecision8Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.ExtendedPrecision10Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.ExtendedPrecision12Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.RangeExtensionPcm10Bit422Heic, PixelTypes.Rgba32, 416, 240, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.RangeExtensionPcm12Bit444Heic, PixelTypes.Rgba32, 416, 240, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.PersistentRice12Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.TransformSkipContext8Bit444Heic, PixelTypes.Rgba32, 1920, 1080, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.TransformSkipContext10Bit444Heic, PixelTypes.Rgba32, 1920, 1080, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.TransformSkipContext12Bit444Heic, PixelTypes.Rgba32, 2560, 1600, HeifBitDepth.Bit12)] [WithFile(TestImages.Heif.Main42210AHeic, PixelTypes.Rgba32, 1920, 1080, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.Main42210BHeic, PixelTypes.Rgba32, 2560, 1600, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.HighThroughput8Bit420WavefrontHeic, PixelTypes.Rgba32, 1024, 768, HeifBitDepth.Bit8)] public void DecodeHevcRangeExtensionStillImageMatchesPinnedPresentation( TestImageProvider provider, int width, int height, HeifBitDepth bitDepth) { using Image image = provider.GetImage(); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); Assert.Equal(new Size(width, height), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); Assert.Equal(bitDepth, metadata.BitDepth); Assert.Null(image.Metadata.IccProfile); } /// /// Verifies genuine monochrome HEIC presentation against pinned libheif output. /// /// The image provider. /// The coded luma precision. [Theory] [WithFile(TestImages.Heif.General8BitMonochromeHeic, PixelTypes.Rgba32, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.General12BitMonochromeHeic, PixelTypes.Rgba32, HeifBitDepth.Bit12)] public void DecodeHevcMonochromeStillImageMatchesPinnedLibheif( TestImageProvider provider, HeifBitDepth bitDepth) { using Image image = provider.GetImage(); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); Assert.Equal(new Size(400, 384), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); Assert.Equal(bitDepth, metadata.BitDepth); Assert.Null(image.Metadata.IccProfile); } /// /// Verifies that high-bit-depth monochrome HEIC presentation preserves its source precision. /// /// The image provider. [Theory] [WithFile(TestImages.Heif.General12BitMonochromeHeic, PixelTypes.Rgba64)] public void DecodeHevcMonochromeHighBitDepthPreservesSourcePrecision(TestImageProvider provider) { using Image image = provider.GetImage(); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); Assert.Equal(new Size(400, 384), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); Assert.Equal(HeifBitDepth.Bit12, metadata.BitDepth); Assert.Null(image.Metadata.IccProfile); } /// /// Verifies genuine HEIC presentation for the official high-throughput Range Extensions streams against /// independently pinned presentation references. /// /// The genuine HEIC input and matching reference-output naming context. /// The independently reported presented width. /// The independently reported presented height. /// The maximum coded component precision. [Theory] [WithFile(TestImages.Heif.HighThroughputExtendedPrecision8Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.HighThroughputExtendedPrecision10Bit444Heic, PixelTypes.Rgba32, 400, 384, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.HighThroughput10Bit422TilesWavefrontHeic, PixelTypes.Rgba32, 1920, 1080, HeifBitDepth.Bit10)] [WithFile(TestImages.Heif.HighThroughput8Bit420TilesWavefrontHeic, PixelTypes.Rgba32, 1024, 768, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.HighThroughput8Bit420CabacBypassAlignmentHeic, PixelTypes.Rgba32, 1024, 768, HeifBitDepth.Bit8)] [WithFile(TestImages.Heif.HighThroughput8Bit420ExtendedPrecisionHeic, PixelTypes.Rgba32, 1024, 768, HeifBitDepth.Bit8)] public void DecodeHevcHighThroughputStillImageMatchesPinnedPresentation( TestImageProvider provider, int width, int height, HeifBitDepth bitDepth) { using Image image = provider.GetImage(); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); Assert.Equal(new Size(width, height), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); Assert.Equal(bitDepth, metadata.BitDepth); Assert.Null(image.Metadata.IccProfile); } /// /// Verifies genuine HEIC presentation for unequal luma and chroma precision, which independent HEIF decoders /// currently reject after reconstructing the native planes. /// /// The retained genuine HEIC input. [Theory] [WithFile(TestImages.Heif.RangeExtensionLuma12Chroma8Heic, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.RangeExtensionLuma8Chroma12Heic, PixelTypes.Rgba32)] public void DecodeHevcRangeExtensionUnequalBitDepthStillImage(TestImageProvider provider) { using Image image = provider.GetImage(); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); Assert.Equal(new Size(1920, 1080), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); Assert.Equal(HeifBitDepth.Bit12, metadata.BitDepth); Assert.Null(image.Metadata.IccProfile); } /// /// Verifies representative genuine HEIC Range Extensions presentation with allocator-split buffers and /// exactly-once final disposal. /// /// The genuine HEIC input and allocator configuration. /// The independently reported presented width. /// The independently reported presented height. [Theory] [WithFile(TestImages.Heif.General8Bit420Heic, PixelTypes.Rgba32, 400, 384)] [WithFile(TestImages.Heif.General10Bit422Heic, PixelTypes.Rgba32, 400, 384)] [WithFile(TestImages.Heif.General12Bit444Heic, PixelTypes.Rgba32, 400, 384)] [WithFile(TestImages.Heif.General12BitMonochromeHeic, PixelTypes.Rgba32, 400, 384)] [WithFile(TestImages.Heif.HighThroughput8Bit420ExtendedPrecisionHeic, PixelTypes.Rgba32, 1024, 768)] public void DecodeHevcRangeExtensionStillImageWithConstrainedAllocator( TestImageProvider provider, int width, int height) { TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 4_096 }; allocator.EnableNonThreadSafeLogging(); Configuration configuration = Configuration.Default.Clone(); configuration.MemoryAllocator = allocator; provider.Configuration = configuration; using (Image image = provider.GetImage()) { Assert.Equal(new Size(width, height), image.Size); } Assert.NotEmpty(allocator.AllocationLog); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.All( allocator.AllocationLog, allocation => Assert.Single( allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId)); } /// /// Verifies representative eight-bit subsampled and high-bit-depth full-resolution presentation through every /// available vector width and the scalar fallback. /// /// The genuine HEIC input and matching reference-output naming context. [Theory] [WithFile(TestImages.Heif.General8Bit420Heic, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.HighThroughputExtendedPrecision12Bit444Heic, PixelTypes.Rgba32)] public void DecodeHevcRangeExtensionPresentationMatchesAcrossIntrinsicWidths( TestImageProvider provider) => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateHevcRangeExtensionPresentation, HevcPresentationConfigurations, provider); /// /// Verifies high-bit-depth HEVC presentation precision through every available vector width and the scalar fallback. /// /// The genuine HEIC input and matching reference-output naming context. [Theory] [WithFile(TestImages.Heif.General12Bit444Heic, PixelTypes.Rgba64)] public void DecodeHevcHighBitDepthPresentationPreservesPrecisionAcrossIntrinsicWidths( TestImageProvider provider) { using Image image = provider.GetImage(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateHevcHighBitDepthPresentation, HevcPresentationConfigurations, provider); } /// /// Verifies one HEVC presentation with the feature-runner configuration and the repository reference API. /// /// The serialized input provider and reference-output naming context. private static void ValidateHevcRangeExtensionPresentation(string providerDump) { TestImageProvider provider = FeatureTestRunner.DeserializeForXunit>(providerDump); using Image image = provider.GetImage(); image.DebugSave(provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider); } /// /// Verifies one high-bit-depth HEVC presentation with the feature-runner configuration and the repository reference API. /// /// The serialized input provider and reference-output naming context. private static void ValidateHevcHighBitDepthPresentation(string providerDump) { TestImageProvider provider = FeatureTestRunner.DeserializeForXunit>(providerDump); using Image image = provider.GetImage(); image.CompareToReferenceOutput(ImageComparer.Exact, provider); } /// /// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes. /// [Theory] [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] public void DecodeAvifPreservesEmbeddedIccProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Heif.ParisIccExifXmpPng).Bytes); Assert.NotNull(preserved.Metadata.IccProfile); Assert.NotNull(expectedPreserved.Metadata.IccProfile); Assert.Equal(expectedPreserved.Metadata.IccProfile.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); } /// /// Verifies that AVIF decoding converts pixels from an embedded non-sRGB ICC profile to sRGB. /// [Theory] [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)] public void DecodeAvifConvertsEmbeddedNonSrgbIccProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); Assert.NotNull(preserved.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile); Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); converted.DebugSave(provider, testOutputDetails: "IccConverted"); // The PNG is the independent RGB source used by libavif's avifenc. A tolerant comparison accounts for the // AV1 loss while proving the AVIF ICC stage produces the same target-profile interpretation. ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); } /// /// Verifies that AVIF grid composition retains and converts the presented image's non-sRGB ICC profile. /// [Theory] [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] public void DecodeAvifGridConvertsEmbeddedNonSrgbIccProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); Assert.NotNull(preserved.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile); Assert.Equal(expectedPreserved.Metadata.IccProfile!.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); } /// /// Verifies that AVIF sequence ICC conversion is applied to every presented frame. /// [Theory] [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] public void DecodeAvifSequenceConvertsEveryFrameWithEmbeddedNonSrgbIccProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); Assert.Equal(2, preserved.Frames.Count); Assert.Equal(preserved.Frames.Count, converted.Frames.Count); Assert.NotNull(preserved.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile); Assert.Equal(expectedPreserved.Metadata.IccProfile!.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); for (int i = 0; i < converted.Frames.Count; i++) { Assert.False(ImageComparer.Exact.CompareImagesOrFrames(i, preserved.Frames[i], converted.Frames[i]).IsEmpty); Assert.True(ImageComparer.TolerantPercentage(1F, 20).CompareImagesOrFrames(i, expected.Frames.RootFrame, converted.Frames[i]).IsEmpty); } } /// /// Verifies that non-sRGB ICC conversion follows auxiliary-alpha composition and preserves the composed alpha values. /// [Fact] public void DecodeAvifAlphaImageConvertsEmbeddedIccProfileWithoutChangingAlpha() { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; byte[] encoded = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; using Image preserved = Image.Load(preserveOptions, encoded); using Image converted = Image.Load(convertOptions, encoded); using Image expected = preserved.Clone(); ColorProfileConverter converter = new(new ColorConversionOptions { SourceIccProfile = expected.Metadata.IccProfile, TargetIccProfile = CompactSrgbV4Profile.Profile, MemoryAllocator = expected.Configuration.MemoryAllocator, }); // Build the oracle from the fully composed preserved decode so that only ICC ordering and alpha retention // are under test; the independently encoded AV1 color and alpha payloads remain identical in both paths. converter.Convert(expected); Assert.NotNull(preserved.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile); Assert.Equal(TestIccProfiles.GetProfile(TestIccProfiles.RommRgb).ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); for (int y = 0; y < converted.Height; y++) { Span preservedRow = preserved.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); Span convertedRow = converted.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < convertedRow.Length; x++) { Assert.Equal(preservedRow[x].A, convertedRow[x].A); } } ImageComparer.Exact.VerifySimilarity(expected, converted); } /// /// Verifies that compact profile handling retains non-sRGB ICC profiles and leaves their pixels unconverted. /// [Theory] [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)] public void DecodeAvifRetainsNonSrgbIccProfileWhenCompacting(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact }; using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image compact = provider.GetImage(HeifDecoder.Instance, compactOptions); Assert.NotNull(preserved.Metadata.IccProfile); Assert.NotNull(compact.Metadata.IccProfile); Assert.Equal(preserved.Metadata.IccProfile.ToByteArray(), compact.Metadata.IccProfile.ToByteArray()); Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); } /// /// Verifies that compact profile handling removes a canonical sRGB ICC profile without changing pixels. /// [Theory] [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] public void DecodeAvifCompactsCanonicalSrgbIccProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact }; using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image compact = provider.GetImage(HeifDecoder.Instance, compactOptions); Assert.NotNull(preserved.Metadata.IccProfile); Assert.Null(compact.Metadata.IccProfile); Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); } /// /// Verifies that metadata skipping omits the embedded AVIF ICC profile. /// [Theory] [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)] public void DecodeAvifSkipsEmbeddedIccProfileWithMetadata(TestImageProvider provider) where TPixel : unmanaged, IPixel { DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Preserve, SkipMetadata = true }; using Image image = provider.GetImage(HeifDecoder.Instance, options); Assert.Null(image.Metadata.IccProfile); } [Fact] public void DecodeIgnoresUnknownTopLevelBox() { byte[] data = CreateEncodedContainer(); data = InsertBytes(data, data.Length, CreateUnknownBox()); using Image image = Image.Load(data); Assert.Equal(new Size(2, 3), image.Size); } [Fact] public void DecodeAppliesTargetSizeOnceToThePresentedHeifImage() { using Image source = new(64, 48); for (int y = 0; y < source.Height; y++) { Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < row.Length; x++) { row[x] = new Rgba32((byte)(x * 3), (byte)(y * 5), (byte)((x * 7) + (y * 11))); } } using MemoryStream stream = new(); source.Save(stream, new HeifEncoder()); byte[] data = stream.ToArray(); Size targetSize = new(17, 17); DecoderOptions options = new() { TargetSize = targetSize }; using Image expected = Image.Load(data); expected.Mutate(context => context.Resize(new ResizeOptions { Size = targetSize, Mode = ResizeMode.Max, Sampler = options.Sampler })); using Image image = Image.Load(options, data); Assert.Equal(expected.Size, image.Size); for (int y = 0; y < image.Height; y++) { Assert.True(image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y).SequenceEqual( expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))); } } [Fact] public void DecodePropagatesStrictValidationToLegacyJpegItems() { byte[] data = CreateContainerWithMalformedJpegMetadata(); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; Assert.Throws(() => { using Image image = Image.Load(options, data); }); } [Theory] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(SegmentIntegrityHandling.IgnoreImageData)] public void DecodePropagatesRecoverableMetadataValidationToLegacyJpegItems(SegmentIntegrityHandling handling) { byte[] data = CreateContainerWithMalformedJpegMetadata(); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; using Image image = Image.Load(options, data); Assert.Equal(new Size(2, 3), image.Size); } [Fact] public void DecodePropagatesSkipMetadataToLegacyJpegItems() { byte[] data = CreateContainerWithMalformedJpegMetadata(); DecoderOptions options = new() { SkipMetadata = true, SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; using Image image = Image.Load(options, data); Assert.Equal(new Size(2, 3), image.Size); } [Fact] public void DecodePropagatesConfigurationToLegacyJpegItems() { byte[] data = CreateEncodedContainer(); Configuration configuration = Configuration.CreateDefaultInstance(); DecoderOptions options = new() { Configuration = configuration }; using Image image = Image.Load(options, data); Assert.Same(configuration, image.Configuration); } /// /// Verifies that invalid optional alpha payloads remain fatal when image-data errors cannot be ignored. /// [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void DecodeRejectsInvalidAlphaPayloadUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) { byte[] data = [.. TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes]; uint alphaItemId = FindFirstItemReferenceSourceId(data, Heif4CharCode.Auxl); ClearItemPayload(data, alphaItemId); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; Assert.ThrowsAny(() => { using Image image = Image.Load(options, data); }); } /// /// Verifies that omits a corrupt optional alpha item while /// retaining the independently decodable color item. /// [Fact] public void DecodeOmitsInvalidAlphaPayloadWhenImageDataErrorsAreIgnored() { byte[] source = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; byte[] data = [.. source]; uint alphaItemId = FindFirstItemReferenceSourceId(data, Heif4CharCode.Auxl); ClearItemPayload(data, alphaItemId); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; using Image expected = Image.Load(source); using Image actual = Image.Load(options, data); AssertOpaqueRgbMatches(expected, actual); } /// /// Verifies that a malformed alpha relationship remains fatal when image-data errors cannot be ignored. /// [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void DecodeRejectsMalformedAlphaReferenceUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) { byte[] data = [.. TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes]; InvalidateFirstItemReferenceSource(data, Heif4CharCode.Auxl); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; Assert.Throws(() => { using Image image = Image.Load(options, data); }); } /// /// Verifies that omits a malformed optional alpha /// relationship while retaining the independently decodable color item. /// [Fact] public void DecodeOmitsMalformedAlphaReferenceWhenImageDataErrorsAreIgnored() { byte[] source = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; byte[] data = [.. source]; InvalidateFirstItemReferenceSource(data, Heif4CharCode.Auxl); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; using Image expected = Image.Load(source); using Image actual = Image.Load(options, data); AssertOpaqueRgbMatches(expected, actual); } /// /// Verifies that strict validation rejects a malformed descriptive metadata relationship. /// [Fact] public void DecodeRejectsMalformedMetadataReferenceInStrictMode() { byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes]; InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; Assert.Throws(() => { using Image image = Image.Load(options, data); }); } /// /// Verifies that non-strict validation omits a malformed descriptive relationship without weakening image-data /// validation. /// [Theory] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(SegmentIntegrityHandling.IgnoreImageData)] public void DecodeOmitsMalformedMetadataReferenceWhenAncillaryErrorsAreIgnored(SegmentIntegrityHandling handling) { byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes]; InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; using Image image = Image.Load(options, data); Assert.Null(image.Metadata.ExifProfile); Assert.NotNull(image.Metadata.XmpProfile); Assert.NotNull(image.Metadata.IccProfile); } /// /// Verifies that skipped metadata is neither retained nor validated through its optional descriptive links. /// [Fact] public void DecodeDoesNotValidateSkippedMetadataReference() { byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes]; InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc); DecoderOptions options = new() { SkipMetadata = true, SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; using Image image = Image.Load(options, data); Assert.Null(image.Metadata.ExifProfile); Assert.Null(image.Metadata.XmpProfile); Assert.Null(image.Metadata.IccProfile); } [Fact] public void IdentifyIgnoresUnknownMetadataBox() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox()); IncrementBoxSize(data, metaOffset, 8); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyIgnoresUnknownNonEssentialProperty() { byte[] data = CreateContainerWithUnknownProperty(false); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyRejectsUnknownEssentialProperty() { byte[] data = CreateContainerWithUnknownProperty(true); InvalidImageContentException exception = Assert.Throws(() => Image.Identify(data)); Assert.Contains("essential", exception.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void IdentifyRejectsMalformedAncillaryPropertyInStrictMode() { byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; Assert.Throws(() => Image.Identify(options, data)); } [Theory] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(SegmentIntegrityHandling.IgnoreImageData)] public void IdentifyIgnoresMalformedAncillaryPropertyWhenPermitted(SegmentIntegrityHandling handling) { byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; ImageInfo imageInfo = Image.Identify(options, data); Assert.Equal(new Size(2, 3), imageInfo.Size); Assert.Equal(PixelResolutionUnit.PixelsPerInch, imageInfo.Metadata.ResolutionUnits); Assert.Equal(96D, imageInfo.Metadata.HorizontalResolution); Assert.Equal(96D, imageInfo.Metadata.VerticalResolution); } [Fact] public void IdentifyDoesNotValidateSkippedAncillaryPropertyMetadata() { byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); DecoderOptions options = new() { SkipMetadata = true, SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; ImageInfo imageInfo = Image.Identify(options, data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void IdentifyRejectsMalformedImagePropertyUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) { byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; Assert.Throws(() => Image.Identify(options, data)); } [Fact] public void IdentifyIgnoresMalformedImagePropertyWhenImageDataErrorsAreIgnored() { byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; ImageInfo imageInfo = Image.Identify(options, data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyRejectsDuplicateAncillaryPropertyAssociationInStrictMode() { byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; Assert.Throws(() => Image.Identify(options, data)); } [Theory] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(SegmentIntegrityHandling.IgnoreImageData)] public void IdentifyIgnoresDuplicateAncillaryPropertyAssociationWhenPermitted(SegmentIntegrityHandling handling) { byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; ImageInfo imageInfo = Image.Identify(options, data); Assert.Equal(new Size(2, 3), imageInfo.Size); Assert.Equal(PixelResolutionUnit.AspectRatio, imageInfo.Metadata.ResolutionUnits); Assert.Equal(1D, imageInfo.Metadata.HorizontalResolution); Assert.Equal(2D, imageInfo.Metadata.VerticalResolution); } [Theory] [InlineData(SegmentIntegrityHandling.Strict)] [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] public void IdentifyRejectsDuplicateImagePropertyAssociationUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) { byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true); DecoderOptions options = new() { SegmentIntegrityHandling = handling }; Assert.Throws(() => Image.Identify(options, data)); } [Fact] public void IdentifyIgnoresDuplicateImagePropertyAssociationWhenImageDataErrorsAreIgnored() { byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true); DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; ImageInfo imageInfo = Image.Identify(options, data); Assert.Equal(new Size(3, 2), imageInfo.Size); } [Theory] [InlineData(Heif4CharCode.Heic)] [InlineData(Heif4CharCode.Heix)] [InlineData(Heif4CharCode.Mif1)] [InlineData(Heif4CharCode.Avif)] [InlineData(Heif4CharCode.Jpeg)] public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand) { byte[] data = CreateEncodedContainer(); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); HeifImageFormatDetector detector = new(); bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format); Assert.True(detected); Assert.Same(HeifFormat.Instance, format); } [Fact] public void IdentifyAcceptsSupportedCompatibleBrand() { byte[] data = CreateEncodedContainer(); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Theory] [InlineData(Heif4CharCode.Hevc)] [InlineData(Heif4CharCode.Hevx)] [InlineData(Heif4CharCode.Avis)] public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand) { byte[] data = CreateEncodedContainer(); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); HeifImageFormatDetector detector = new(); bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format); Assert.True(detected); Assert.Same(HeifFormat.Instance, format); } [Theory] [InlineData(Heif4CharCode.Hevm)] [InlineData(Heif4CharCode.Hevs)] [InlineData(Heif4CharCode.Jpgs)] public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand) { byte[] data = CreateEncodedContainer(); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); HeifImageFormatDetector detector = new(); Assert.False(detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out _)); } [Fact] public void IdentifyRejectsUnsupportedBrands() { byte[] data = CreateEncodedContainer(); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType); using MemoryStream stream = new(data, false); Assert.Throws(() => HeifDecoder.Instance.Identify(DecoderOptions.Default, stream)); } [Fact] public void IdentifyAcceptsExtendedSizeTopLevelBox() { byte[] data = CreateEncodedContainer(); byte[] box = new byte[16]; BinaryPrimitives.WriteUInt32BigEndian(box, 1); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); BinaryPrimitives.WriteUInt64BigEndian(box.AsSpan(8), (ulong)box.Length); data = InsertBytes(data, data.Length, box); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyAcceptsUuidTopLevelBox() { byte[] data = CreateEncodedContainer(); byte[] box = new byte[24]; BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); data = InsertBytes(data, data.Length, box); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyAcceptsSizeZeroTopLevelBox() { byte[] data = CreateEncodedContainer(); byte[] box = CreateUnknownBox(); BinaryPrimitives.WriteUInt32BigEndian(box, 0); data = InsertBytes(data, data.Length, box); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyAcceptsExtendedSizeItemInfoEntry() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int infeOffset = iinfOffset + 14; uint infeSize = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(infeOffset)); data = InsertBytes(data, infeOffset + 8, new byte[8]); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(infeOffset), 1); BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan(infeOffset + 8), infeSize + 8); IncrementBoxSize(data, metaOffset, 8); IncrementBoxSize(data, iinfOffset, 8); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyRejectsSizeZeroMetadataChild() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); byte[] box = CreateUnknownBox(); BinaryPrimitives.WriteUInt32BigEndian(box, 0); data = InsertBytes(data, metaOffset + metaSize, box); IncrementBoxSize(data, metaOffset, box.Length); Assert.Throws(() => Image.Identify(data)); } [Fact] public void IdentifyRejectsMetadataChildBeyondParent() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); byte[] box = CreateUnknownBox(); BinaryPrimitives.WriteUInt32BigEndian(box, 16); data = InsertBytes(data, metaOffset + metaSize, box); IncrementBoxSize(data, metaOffset, box.Length); Assert.Throws(() => Image.Identify(data)); } [Fact] public void IdentifyRejectsItemInfoEntryBeyondParent() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); uint iinfSize = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iinfOffset)); int infeOffset = iinfOffset + 14; BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(infeOffset), iinfSize); Assert.Throws(() => Image.Identify(data)); } [Fact] public void IdentifyRejectsBoxSmallerThanHeader() { byte[] data = CreateEncodedContainer(); byte[] box = CreateUnknownBox(); BinaryPrimitives.WriteUInt32BigEndian(box, 4); data = InsertBytes(data, data.Length, box); Assert.Throws(() => Image.Identify(data)); } [Fact] public void IdentifyRejectsTruncatedExtendedSizeHeader() { byte[] data = CreateEncodedContainer(); byte[] box = new byte[12]; BinaryPrimitives.WriteUInt32BigEndian(box, 1); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); data = InsertBytes(data, data.Length, box); Assert.Throws(() => Image.Identify(data)); } [Fact] public void IdentifyRejectsTruncatedUuidHeader() { byte[] data = CreateEncodedContainer(); byte[] box = new byte[16]; BinaryPrimitives.WriteUInt32BigEndian(box, 24); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); data = InsertBytes(data, data.Length, box); Assert.Throws(() => Image.Identify(data)); } [Fact] public void IdentifyAcceptsItemPropertiesBeforeItemInfo() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); data = MoveBoxBefore(data, iprpOffset, iprpSize, iinfOffset); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyAcceptsItemLocationBeforeItemInfo() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int ilocOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); int ilocSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ilocOffset)); data = MoveBoxBefore(data, ilocOffset, ilocSize, iinfOffset); ImageInfo imageInfo = Image.Identify(data); Assert.Equal(new Size(2, 3), imageInfo.Size); } [Fact] public void IdentifyRejectsDuplicateUniqueMetadataBox() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12); int pitmSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(pitmOffset)); data = InsertBytes(data, metaOffset + metaSize, data.AsSpan(pitmOffset, pitmSize)); IncrementBoxSize(data, metaOffset, pitmSize); Assert.Throws(() => Image.Identify(data)); } private static byte[] CreateEncodedContainer() { using Image image = new(2, 3); using MemoryStream stream = new(); image.Save(stream, new HeifEncoder()); return stream.ToArray(); } private static byte[] CreateContainerWithUnknownProperty(bool essential) => CreateContainerWithProperty(CreateUnknownBox(), essential); private static byte[] CreateContainerWithProperty(ReadOnlySpan property, bool essential) { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); int ipcoOffset = FindBoxOffset(data, Heif4CharCode.Ipco, iprpOffset + 8, iprpSize - 8); int ipcoSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipcoOffset)); int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); // Insert the property before ipma so its one-based index is 2 and all parent box sizes remain explicit. data = InsertBytes(data, ipcoOffset + ipcoSize, property); IncrementBoxSize(data, metaOffset, property.Length); IncrementBoxSize(data, iprpOffset, property.Length); IncrementBoxSize(data, ipcoOffset, property.Length); ipmaOffset += property.Length; // The generated container has one item with one property association; append the inserted property to that entry. int associationCountOffset = ipmaOffset + 18; data[associationCountOffset]++; int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); byte association = (byte)(2 | (essential ? 0x80 : 0)); data = InsertBytes(data, associationOffset, new byte[] { association }); IncrementBoxSize(data, metaOffset, 1); IncrementBoxSize(data, iprpOffset, 1); IncrementBoxSize(data, ipmaOffset, 1); return data; } private static byte[] CreateContainerWithMalformedJpegMetadata() { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); int mediaDataOffset = FindBoxOffset(data, Heif4CharCode.Mdat, 0, data.Length); // The generated item uses one file-relative extent. Insert the malformed JPEG application segment after its // start-of-image marker, then update the enclosing media-data size and the exact declared extent length. data = InsertBytes(data, mediaDataOffset + 10, MalformedJpegApp13); IncrementBoxSize(data, mediaDataOffset, MalformedJpegApp13.Length); uint extentLength = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(itemLocationOffset + 32)); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(itemLocationOffset + 32), extentLength + (uint)MalformedJpegApp13.Length); return data; } private static byte[] CreateContainerWithDuplicatePropertyAssociation(ReadOnlySpan property, bool essential) { byte[] data = CreateContainerWithProperty(property, essential); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); int associationCountOffset = ipmaOffset + 18; // Repeat the inserted property's one-based index in the existing item entry without changing box structure. data[associationCountOffset]++; int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); byte association = (byte)(2 | (essential ? 0x80 : 0)); data = InsertBytes(data, associationOffset, new byte[] { association }); IncrementBoxSize(data, metaOffset, 1); IncrementBoxSize(data, iprpOffset, 1); IncrementBoxSize(data, ipmaOffset, 1); return data; } private static byte[] CreateUnknownBox() => CreateEmptyBox((Heif4CharCode)UnknownBoxType); private static byte[] CreateEmptyBox(Heif4CharCode type) => CreateBox(type, []); private static byte[] CreatePixelAspectRatioBox() { byte[] payload = new byte[8]; BinaryPrimitives.WriteUInt32BigEndian(payload, 2); BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(4), 1); return CreateBox(Heif4CharCode.Pasp, payload); } private static byte[] CreateRotationBox() => CreateBox(Heif4CharCode.Irot, [1]); private static byte[] CreateBox(Heif4CharCode type, ReadOnlySpan payload) { byte[] box = new byte[8 + payload.Length]; BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)type); payload.CopyTo(box.AsSpan(8)); return box; } private static int FindBoxOffset(ReadOnlySpan data, Heif4CharCode type, int offset, int length) { int endOffset = offset + length; while (offset < endOffset) { int boxSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[offset..]); Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(data[(offset + 4)..]); if (boxType == type) { return offset; } offset += boxSize; } return -1; } /// /// Reads the source item identifier from the first registered relationship of the requested type. /// /// The complete HEIF container. /// The item-reference child type. /// The source item identifier. private static uint FindFirstItemReferenceSourceId(ReadOnlySpan data, Heif4CharCode referenceType) { int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); Assert.True(metaOffset >= 0); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]); int itemReferenceOffset = FindBoxOffset(data, Heif4CharCode.Iref, metaOffset + 12, metaSize - 12); Assert.True(itemReferenceOffset >= 0); int itemReferenceSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[itemReferenceOffset..]); int relationshipOffset = FindBoxOffset(data, referenceType, itemReferenceOffset + 12, itemReferenceSize - 12); Assert.True(relationshipOffset >= 0); byte version = data[itemReferenceOffset + 8]; Assert.InRange(version, (byte)0, (byte)1); return version == 0 ? BinaryPrimitives.ReadUInt16BigEndian(data[(relationshipOffset + 8)..]) : BinaryPrimitives.ReadUInt32BigEndian(data[(relationshipOffset + 8)..]); } /// /// Replaces the source item identifier of the first requested relationship with an undeclared value. /// /// The complete mutable HEIF container. /// The item-reference child type. private static void InvalidateFirstItemReferenceSource(Span data, Heif4CharCode referenceType) { int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); Assert.True(metaOffset >= 0); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]); int itemReferenceOffset = FindBoxOffset(data, Heif4CharCode.Iref, metaOffset + 12, metaSize - 12); Assert.True(itemReferenceOffset >= 0); int itemReferenceSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[itemReferenceOffset..]); int relationshipOffset = FindBoxOffset(data, referenceType, itemReferenceOffset + 12, itemReferenceSize - 12); Assert.True(relationshipOffset >= 0); byte version = data[itemReferenceOffset + 8]; Assert.InRange(version, (byte)0, (byte)1); if (version == 0) { BinaryPrimitives.WriteUInt16BigEndian(data[(relationshipOffset + 8)..], ushort.MaxValue); } else { BinaryPrimitives.WriteUInt32BigEndian(data[(relationshipOffset + 8)..], uint.MaxValue); } } /// /// Clears every file-relative extent belonging to the requested item while retaining the container structure. /// /// The complete mutable HEIF container. /// The item whose coded payload is cleared. private static void ClearItemPayload(Span data, uint itemId) { int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); Assert.True(metaOffset >= 0); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]); int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); Assert.True(itemLocationOffset >= 0); int offset = itemLocationOffset + 8; byte version = data[offset]; offset += 4; int extentOffsetSize = data[offset] >> 4; int extentLengthSize = data[offset] & 0x0F; offset++; int baseOffsetSize = data[offset] >> 4; int extentIndexSize = version is 1 or 2 ? data[offset] & 0x0F : 0; offset++; uint itemCount = version == 2 ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); offset += version == 2 ? 4 : 2; bool found = false; for (uint itemIndex = 0; itemIndex < itemCount; itemIndex++) { uint currentItemId = version == 2 ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); offset += version == 2 ? 4 : 2; if (version is 1 or 2) { ushort constructionMethod = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); Assert.Equal(0, constructionMethod & 0x0F); offset += 2; } // The data-reference index is zero for the self-contained image items used by the fixture. Assert.Equal(0, BinaryPrimitives.ReadUInt16BigEndian(data[offset..])); offset += 2; ulong baseOffset = ReadVariableUnsigned(data, baseOffsetSize, ref offset); int extentCount = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); offset += 2; for (int extentIndex = 0; extentIndex < extentCount; extentIndex++) { _ = ReadVariableUnsigned(data, extentIndexSize, ref offset); ulong extentOffset = ReadVariableUnsigned(data, extentOffsetSize, ref offset); ulong extentLength = ReadVariableUnsigned(data, extentLengthSize, ref offset); if (currentItemId == itemId) { data.Slice(checked((int)(baseOffset + extentOffset)), checked((int)extentLength)).Clear(); found = true; } } } Assert.True(found); } /// /// Reads one zero-width, 32-bit, or 64-bit unsigned item-location field. /// /// The complete HEIF container. /// The field width in bytes. /// The current read offset, advanced past the field. /// The decoded field value. private static ulong ReadVariableUnsigned(ReadOnlySpan data, int size, ref int offset) { ulong value = size switch { 0 => 0, 4 => BinaryPrimitives.ReadUInt32BigEndian(data[offset..]), 8 => BinaryPrimitives.ReadUInt64BigEndian(data[offset..]), _ => throw new InvalidOperationException($"Unexpected item-location field width {size} in the test fixture.") }; offset += size; return value; } /// /// Verifies that omitting an invalid alpha item preserves color channels and produces opaque output. /// /// The image decoded with its valid alpha item. /// The image decoded after the alpha item or relationship was invalidated. private static void AssertOpaqueRgbMatches(Image expected, Image actual) { Assert.False(actual.Metadata.GetHeifMetadata().HasAlpha); Assert.Equal(expected.Size, actual.Size); for (int y = 0; y < actual.Height; y++) { Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); Span actualRow = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < actualRow.Length; x++) { Assert.Equal(expectedRow[x].R, actualRow[x].R); Assert.Equal(expectedRow[x].G, actualRow[x].G); Assert.Equal(expectedRow[x].B, actualRow[x].B); Assert.Equal(byte.MaxValue, actualRow[x].A); } } } private static byte[] InsertBytes(byte[] data, int offset, ReadOnlySpan inserted) { byte[] result = new byte[data.Length + inserted.Length]; data.AsSpan(0, offset).CopyTo(result); inserted.CopyTo(result.AsSpan(offset)); data.AsSpan(offset).CopyTo(result.AsSpan(offset + inserted.Length)); return result; } private static byte[] MoveBoxBefore(byte[] data, int boxOffset, int boxSize, int beforeOffset) { byte[] result = new byte[data.Length]; data.AsSpan(0, beforeOffset).CopyTo(result); data.AsSpan(boxOffset, boxSize).CopyTo(result.AsSpan(beforeOffset)); data.AsSpan(beforeOffset, boxOffset - beforeOffset).CopyTo(result.AsSpan(beforeOffset + boxSize)); data.AsSpan(boxOffset + boxSize).CopyTo(result.AsSpan(boxOffset + boxSize)); return result; } private static void IncrementBoxSize(byte[] data, int offset, int increment) { uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset)); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), size + (uint)increment); } }