// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers.Binary; using System.Text; using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestDataIcc; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests.Formats.Heif; [Trait("Format", "Heif")] [ValidateDisposedMemoryAllocations] public class HeifEncoderTests { private const int Av1EightBit = (int)Av1BitDepth.EightBit; private const int Av1TenBit = (int)Av1BitDepth.TenBit; private const int Av1TwelveBit = (int)Av1BitDepth.TwelveBit; private const int Yuv400 = (int)Av1ColorFormat.Yuv400; private const int Yuv420 = (int)Av1ColorFormat.Yuv420; private const int Yuv422 = (int)Av1ColorFormat.Yuv422; private const int Yuv444 = (int)Av1ColorFormat.Yuv444; [Fact] public void OptionsHaveExpectedDefaults() { HeifEncoder encoder = new(); Assert.Equal(HeifCompressionMethod.LegacyJpeg, encoder.CompressionMethod); Assert.Null(encoder.Quality); Assert.Null(encoder.AlphaQuality); Assert.Equal(5, encoder.Effort); Assert.False(encoder.Lossless); Assert.Null(encoder.BitDepth); Assert.Null(encoder.ChromaSubsampling); Assert.Null(encoder.RepeatCount); Assert.True(encoder.AnimateRootFrame); } [Theory] [InlineData(-1)] [InlineData(101)] public void QualityOutsideRangeThrows(int quality) => Assert.Throws(() => new HeifEncoder { Quality = quality }); [Theory] [InlineData(-1)] [InlineData(101)] public void AlphaQualityOutsideRangeThrows(int quality) => Assert.Throws(() => new HeifEncoder { AlphaQuality = quality }); [Theory] [InlineData(-1)] [InlineData(11)] public void EffortOutsideRangeThrows(int effort) => Assert.Throws(() => new HeifEncoder { Effort = effort }); [Theory] [InlineData(0, 0, 0)] [InlineData(100, 100, 10)] public void OptionRangeBoundariesAreAccepted(int quality, int alphaQuality, int effort) { HeifEncoder encoder = new() { Quality = quality, AlphaQuality = alphaQuality, Effort = effort }; Assert.Equal(quality, encoder.Quality); Assert.Equal(alphaQuality, encoder.AlphaQuality); Assert.Equal(effort, encoder.Effort); } [Fact] public void LegacyJpegAcceptsZeroQuality() { using Image image = new(1, 1); image[0, 0] = new Rgba32(10, 20, 30); using MemoryStream stream = new(); HeifEncoder encoder = new() { Quality = 0 }; image.Save(stream, encoder); Assert.NotEqual(0, stream.Length); stream.Position = 0; using Image decoded = Image.Load(stream); Assert.Equal(image.Size, decoded.Size); } [Fact] public void LegacyJpegWritesNonSeekableStream() { using Image image = new(1, 1); image[0, 0] = new Rgba32(10, 20, 30); using MemoryStream storage = new(); using NonSeekableStream destination = new(storage); image.Save(destination, new HeifEncoder()); Assert.NotEqual(0, storage.Length); storage.Position = 0; using Image decoded = Image.Load(storage); Assert.Equal(image.Size, decoded.Size); } [Fact] public void LegacyJpegWritesAtCurrentStreamPosition() { using Image image = new(1, 1); image[0, 0] = new Rgba32(10, 20, 30); using MemoryStream stream = new(); stream.Write([1, 2, 3, 4]); long fileStart = stream.Position; image.Save(stream, new HeifEncoder()); stream.Position = fileStart; using Image decoded = Image.Load(stream); Assert.Equal(image.Size, decoded.Size); } [Theory] [InlineData(false, true)] [InlineData(true, false)] public void LegacyJpegHonorsSkipMetadataForEmbeddedProfiles(bool skipMetadata, bool expectedIccProfile) { using Image image = new(8, 8); image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray); using MemoryStream stream = new(); HeifEncoder encoder = new() { SkipMetadata = skipMetadata }; image.Save(stream, encoder); DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; stream.Position = 0; using Image decoded = Image.Load(preserveOptions, stream); Assert.Equal(expectedIccProfile, decoded.Metadata.IccProfile is not null); if (expectedIccProfile) { Assert.Equal( IccTestDataProfiles.ProfileRandomArray, Assert.IsType(decoded.Metadata.IccProfile).ToByteArray()); } } [Fact] public void LegacyJpegRejectsLosslessEncoding() { using Image image = new(1, 1); using MemoryStream stream = new(); HeifEncoder encoder = new() { Lossless = true }; Assert.Throws(() => image.Save(stream, encoder)); Assert.Equal(0, stream.Length); } [Theory] [InlineData(HeifBitDepth.Bit10)] [InlineData(HeifBitDepth.Bit12)] public void LegacyJpegRejectsHighBitDepth(HeifBitDepth bitDepth) { using Image image = new(1, 1); using MemoryStream stream = new(); HeifEncoder encoder = new() { BitDepth = bitDepth }; Assert.Throws(() => image.Save(stream, encoder)); Assert.Equal(0, stream.Length); } [Fact] public void Av1RejectsLosslessEncodingBeforeWritingOutput() { using Image image = new(1, 1); using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Lossless = true }; Assert.Throws(() => image.Save(stream, encoder)); Assert.Equal(0, stream.Length); } [Fact] public void Av1RejectsImageSequenceBeforeWritingOutput() { using Image image = new(1, 1); image.Frames.AddFrame(image.Frames.RootFrame); using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1 }; Assert.Throws(() => image.Save(stream, encoder)); Assert.Equal(0, stream.Length); } [Theory] [InlineData(0, 255)] [InlineData(1, 249)] [InlineData(2, 249)] [InlineData(50, 128)] [InlineData(60, 100)] [InlineData(75, 64)] [InlineData(99, 4)] [InlineData(100, 4)] public void Av1QualityMapsThroughLibaomQuantizers(int quality, int expectedQIndex) => Assert.Equal(expectedQIndex, HeifEncoderCore.GetAv1QuantizerIndex(quality)); [Fact] public void Av1WritesStillImageWithRequiredBrandsAndProductionPayload() { const int width = 16; const int height = 16; using Image image = new(width, height); for (int row = 0; row < height; row++) { Span pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row); for (int column = 0; column < width; column++) { pixels[column] = new Rgb24( (byte)(column * 11), (byte)(row * 13), (byte)((column + row) * 7)); } } using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Quality = 75, Effort = 0 }; image.Save(stream, encoder); byte[] file = stream.ToArray(); ReadOnlySpan fileType = file.AsSpan(0, 28); Assert.Equal(28, BinaryPrimitives.ReadInt32BigEndian(fileType)); Assert.Equal(Heif4CharCode.Ftyp, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[4..])); Assert.Equal(Heif4CharCode.Avif, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[8..])); Assert.Equal(0, BinaryPrimitives.ReadInt32BigEndian(fileType[12..])); Assert.Equal(Heif4CharCode.Avif, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[16..])); Assert.Equal(Heif4CharCode.Mif1, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[20..])); Assert.Equal(Heif4CharCode.Miaf, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[24..])); Span payload = GetItemPayload(file, 1); using Av1Decoder payloadDecoder = new(Configuration.Default); using Image payloadImage = payloadDecoder.Decode(payload); ObuFrameHeader frameHeader = Assert.IsType(payloadDecoder.FrameHeader); Assert.Equal(64, frameHeader.QuantizationParameters.BaseQIndex); Assert.Equal(image.Size, payloadImage.Size); stream.Position = 0; using Image decoded = Image.Load(stream); HeifMetadata metadata = decoded.Metadata.GetHeifMetadata(); Assert.Equal(image.Size, decoded.Size); Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.False(metadata.IsMonochrome); Assert.False(metadata.HasAlpha); CicpProfile colorProfile = Assert.IsType(decoded.Metadata.CicpProfile); Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, colorProfile.MatrixCoefficients); Assert.False(colorProfile.FullRange); string outputDirectory = Path.Combine( TestEnvironment.ActualOutputDirectoryFullPath, "Formats", "Heif", "Av1"); Directory.CreateDirectory(outputDirectory); File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-16x16-8b-420.avif"), file); File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-16x16-8b-420.obu"), payload.ToArray()); } [Fact] public void Av1WritesAuxiliaryAlphaFromSourcePixelType() { const int width = 16; const int height = 8; using Image image = new(width, height); for (int row = 0; row < height; row++) { Span pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row); for (int column = 0; column < width; column++) { pixels[column] = column < 8 ? new Rgba32(40, 80, 120, 0) : new Rgba32(40, 80, 120, 255); } } using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Quality = 75, AlphaQuality = 100, Effort = 0 }; image.Save(stream, encoder); byte[] file = stream.ToArray(); Span colorPayload = GetItemPayload(file, 1); Span alphaPayload = GetItemPayload(file, 2); using Av1Decoder colorDecoder = new(Configuration.Default); using Image colorImage = colorDecoder.Decode(colorPayload); ObuFrameHeader colorFrameHeader = Assert.IsType(colorDecoder.FrameHeader); Assert.Equal(64, colorFrameHeader.QuantizationParameters.BaseQIndex); using Av1Decoder alphaDecoder = new(Configuration.Default); using Image alphaImage = alphaDecoder.Decode(alphaPayload); ObuSequenceHeader alphaSequenceHeader = Assert.IsType(alphaDecoder.SequenceHeader); ObuFrameHeader alphaFrameHeader = Assert.IsType(alphaDecoder.FrameHeader); Assert.True(alphaSequenceHeader.ColorConfig.IsMonochrome); Assert.Equal(4, alphaFrameHeader.QuantizationParameters.BaseQIndex); stream.Position = 0; using Image decoded = Image.Load(stream); HeifMetadata metadata = decoded.Metadata.GetHeifMetadata(); Assert.True(metadata.HasAlpha); Assert.InRange(decoded[0, 0].A, (byte)0, (byte)8); Assert.InRange(decoded[width - 1, 0].A, (byte)247, byte.MaxValue); string outputDirectory = Path.Combine( TestEnvironment.ActualOutputDirectoryFullPath, "Formats", "Heif", "Av1"); Directory.CreateDirectory(outputDirectory); File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-alpha-color.obu"), colorPayload.ToArray()); File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-alpha-auxiliary.obu"), alphaPayload.ToArray()); } [Theory] [InlineData(HeifBitDepth.Bit8, HeifChromaSubsampling.Monochrome, Av1EightBit, Yuv400)] [InlineData(HeifBitDepth.Bit10, HeifChromaSubsampling.Yuv420, Av1TenBit, Yuv420)] [InlineData(HeifBitDepth.Bit10, HeifChromaSubsampling.Yuv422, Av1TenBit, Yuv422)] [InlineData(HeifBitDepth.Bit12, HeifChromaSubsampling.Yuv444, Av1TwelveBit, Yuv444)] public void Av1ExplicitPrecisionAndSamplingReachPayload( HeifBitDepth bitDepth, HeifChromaSubsampling chromaSubsampling, int expectedAv1BitDepthValue, int expectedColorFormatValue) { Av1BitDepth expectedAv1BitDepth = (Av1BitDepth)expectedAv1BitDepthValue; Av1ColorFormat expectedColorFormat = (Av1ColorFormat)expectedColorFormatValue; using Image image = new(8, 8); for (int row = 0; row < image.Height; row++) { Span pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row); for (int column = 0; column < image.Width; column++) { pixels[column] = new Rgb24( (byte)(column * 29), (byte)(row * 29), (byte)((column + row) * 13)); } } image.Metadata.GetHeifMetadata().BitDepth = HeifBitDepth.Bit10; using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, BitDepth = bitDepth, ChromaSubsampling = chromaSubsampling, Effort = 0 }; image.Save(stream, encoder); byte[] file = stream.ToArray(); Span payload = GetItemPayload(file, 1); using Av1Decoder payloadDecoder = new(Configuration.Default); using Image payloadImage = payloadDecoder.Decode(payload); ObuSequenceHeader sequenceHeader = Assert.IsType(payloadDecoder.SequenceHeader); Assert.Equal(expectedAv1BitDepth, sequenceHeader.ColorConfig.BitDepth); Assert.Equal(expectedColorFormat, sequenceHeader.ColorConfig.GetColorFormat()); Assert.Equal(image.Size, payloadImage.Size); stream.Position = 0; using Image decoded = Image.Load(stream); HeifMetadata metadata = decoded.Metadata.GetHeifMetadata(); Assert.Equal(bitDepth, metadata.BitDepth); Assert.Equal(chromaSubsampling == HeifChromaSubsampling.Monochrome, metadata.IsMonochrome); string outputDirectory = Path.Combine( TestEnvironment.ActualOutputDirectoryFullPath, "Formats", "Heif", "Av1"); Directory.CreateDirectory(outputDirectory); File.WriteAllBytes( Path.Combine(outputDirectory, $"encoder-public-8x8-{(int)bitDepth}b-{chromaSubsampling}.obu"), payload.ToArray()); } [Fact] public void Av1UsesMetadataBitDepthWhenOptionIsNull() { using Image image = new(8, 8); image.Metadata.GetHeifMetadata().BitDepth = HeifBitDepth.Bit10; using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, ChromaSubsampling = HeifChromaSubsampling.Yuv444, Effort = 0 }; image.Save(stream, encoder); byte[] file = stream.ToArray(); Span payload = GetItemPayload(file, 1); using Av1Decoder payloadDecoder = new(Configuration.Default); using Image payloadImage = payloadDecoder.Decode(payload); ObuSequenceHeader sequenceHeader = Assert.IsType(payloadDecoder.SequenceHeader); Assert.Equal(Av1BitDepth.TenBit, sequenceHeader.ColorConfig.BitDepth); } [Fact] public void Av1SanitizesIncompatibleIdentityMatrixWithoutMutatingSourceMetadata() { using Image image = new(8, 8); CicpProfile sourceProfile = new(1, 13, 0, false); image.Metadata.CicpProfile = sourceProfile; using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, ChromaSubsampling = HeifChromaSubsampling.Yuv420, Effort = 0 }; image.Save(stream, encoder); Assert.Same(sourceProfile, image.Metadata.CicpProfile); Assert.Equal(CicpMatrixCoefficients.Identity, sourceProfile.MatrixCoefficients); Assert.False(sourceProfile.FullRange); stream.Position = 0; using Image decoded = Image.Load(stream); CicpProfile decodedProfile = Assert.IsType(decoded.Metadata.CicpProfile); Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, decodedProfile.MatrixCoefficients); Assert.False(decodedProfile.FullRange); } [Fact] public void Av1PreservesIccExifAndXmpMetadata() { using Image image = new(8, 8); image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray); ExifProfile generatedExif = new(); generatedExif.SetValue(ExifTag.Software, "ImageSharp HEIF"); byte[] exifData = generatedExif.ToByteArray(); image.Metadata.ExifProfile = generatedExif; byte[] xmpData = Encoding.UTF8.GetBytes("ImageSharp HEIF"); image.Metadata.XmpProfile = new XmpProfile(xmpData); using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Effort = 0 }; image.Save(stream, encoder); byte[] file = stream.ToArray(); Span itemInfo = GetMetadataChild(file, Heif4CharCode.Iinf); Assert.Equal(3, BinaryPrimitives.ReadUInt16BigEndian(itemInfo[12..])); int entryOffset = 14; int colorEntryLength = BinaryPrimitives.ReadInt32BigEndian(itemInfo[entryOffset..]); Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(itemInfo[(entryOffset + 12)..])); Assert.Equal(Heif4CharCode.Av01, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(itemInfo[(entryOffset + 16)..])); Assert.Equal([0], itemInfo.Slice(entryOffset + 20, colorEntryLength - 20).ToArray()); entryOffset += colorEntryLength; int exifEntryLength = BinaryPrimitives.ReadInt32BigEndian(itemInfo[entryOffset..]); Assert.Equal(2, BinaryPrimitives.ReadUInt16BigEndian(itemInfo[(entryOffset + 12)..])); Assert.Equal(Heif4CharCode.Exif, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(itemInfo[(entryOffset + 16)..])); Assert.Equal("Exif\0", Encoding.UTF8.GetString(itemInfo.Slice(entryOffset + 20, exifEntryLength - 20))); entryOffset += exifEntryLength; int xmpEntryLength = BinaryPrimitives.ReadInt32BigEndian(itemInfo[entryOffset..]); Assert.Equal(3, BinaryPrimitives.ReadUInt16BigEndian(itemInfo[(entryOffset + 12)..])); Assert.Equal(Heif4CharCode.Mime, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(itemInfo[(entryOffset + 16)..])); Assert.Equal( "XMP\0application/rdf+xml\0", Encoding.UTF8.GetString(itemInfo.Slice(entryOffset + 20, xmpEntryLength - 20))); entryOffset += xmpEntryLength; Assert.Equal(itemInfo.Length, entryOffset); Span itemReferences = GetMetadataChild(file, Heif4CharCode.Iref); int referenceOffset = 12; for (ushort sourceId = 2; sourceId <= 3; sourceId++) { int referenceLength = BinaryPrimitives.ReadInt32BigEndian(itemReferences[referenceOffset..]); Assert.Equal(14, referenceLength); Assert.Equal( Heif4CharCode.Cdsc, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(itemReferences[(referenceOffset + 4)..])); Assert.Equal(sourceId, BinaryPrimitives.ReadUInt16BigEndian(itemReferences[(referenceOffset + 8)..])); Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(itemReferences[(referenceOffset + 10)..])); Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(itemReferences[(referenceOffset + 12)..])); referenceOffset += referenceLength; } Assert.Equal(itemReferences.Length, referenceOffset); Span encodedExif = GetItemPayload(file, 2); Assert.Equal(0U, BinaryPrimitives.ReadUInt32BigEndian(encodedExif)); Assert.Equal(exifData, encodedExif[4..].ToArray()); Assert.Equal(xmpData, GetItemPayload(file, 3).ToArray()); DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; using Image decoded = Image.Load(preserveOptions, file); Assert.Equal( IccTestDataProfiles.ProfileRandomArray, Assert.IsType(decoded.Metadata.IccProfile).ToByteArray()); ExifProfile decodedExif = Assert.IsType(decoded.Metadata.ExifProfile); Assert.True(decodedExif.TryGetValue(ExifTag.Software, out IExifValue software)); Assert.Equal("ImageSharp HEIF", software.Value); Assert.Equal(xmpData, Assert.IsType(decoded.Metadata.XmpProfile).ToByteArray()); } [Fact] public void Av1SkipMetadataSuppressesIccExifAndXmp() { using Image image = new(8, 8); image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray); image.Metadata.ExifProfile = new ExifProfile(); image.Metadata.ExifProfile.SetValue(ExifTag.Software, "ImageSharp HEIF"); image.Metadata.XmpProfile = new XmpProfile(Encoding.UTF8.GetBytes("ImageSharp HEIF")); using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Effort = 0, SkipMetadata = true }; image.Save(stream, encoder); byte[] file = stream.ToArray(); Span itemInfo = GetMetadataChild(file, Heif4CharCode.Iinf); Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(itemInfo[12..])); Span itemProperties = GetMetadataChild(file, Heif4CharCode.Iprp); const int IpcoOffset = 8; int ipcoEnd = IpcoOffset + BinaryPrimitives.ReadInt32BigEndian(itemProperties[IpcoOffset..]); int propertyOffset = IpcoOffset + 8; while (propertyOffset < ipcoEnd) { int propertyLength = BinaryPrimitives.ReadInt32BigEndian(itemProperties[propertyOffset..]); Heif4CharCode propertyType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(itemProperties[(propertyOffset + 4)..]); if (propertyType == Heif4CharCode.Colr) { Assert.Equal( Heif4CharCode.Nclx, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(itemProperties[(propertyOffset + 8)..])); } propertyOffset += propertyLength; } Assert.Equal(ipcoEnd, propertyOffset); DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; using Image decoded = Image.Load(preserveOptions, file); Assert.Null(decoded.Metadata.IccProfile); Assert.Null(decoded.Metadata.ExifProfile); Assert.Null(decoded.Metadata.XmpProfile); } [Fact] public void Av1WritesNonSeekableStream() { using Image image = new(8, 8); using MemoryStream storage = new(); using NonSeekableStream destination = new(storage); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Effort = 0 }; image.Save(destination, encoder); Assert.NotEqual(0, storage.Length); storage.Position = 0; using Image decoded = Image.Load(storage); Assert.Equal(image.Size, decoded.Size); } [Fact] public void Av1WritesAtCurrentStreamPosition() { using Image image = new(8, 8); using MemoryStream stream = new(); stream.Write([1, 2, 3, 4]); long fileStart = stream.Position; HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1, Effort = 0 }; image.Save(stream, encoder); stream.Position = fileStart; using Image decoded = Image.Load(stream); Assert.Equal(image.Size, decoded.Size); } [Fact] public void Av1ItemPropertiesWriteRequiredTypesAndEssentialConfiguration() { ObuSequenceHeader colorHeader = new() { SequenceProfile = ObuSequenceProfile.Main, OperatingPoint = [new ObuOperatingPoint { SequenceLevelIndex = 31 }], ColorConfig = new ObuColorConfig { BitDepth = Av1BitDepth.TenBit, SubSamplingX = true, SubSamplingY = true } }; ObuSequenceHeader alphaHeader = new() { SequenceProfile = ObuSequenceProfile.Main, OperatingPoint = [new ObuOperatingPoint { SequenceLevelIndex = 31 }], ColorConfig = new ObuColorConfig { BitDepth = Av1BitDepth.TenBit, IsMonochrome = true, SubSamplingX = true, SubSamplingY = true } }; HeifItem colorItem = new(Heif4CharCode.Av01, 1) { ChannelBitDepths = [10, 10, 10], Av1CodecConfiguration = new Av1CodecConfiguration(colorHeader), CicpProfile = new CicpProfile(1, 13, 6, true) }; colorItem.SetExtent(new Size(64, 48)); HeifItem alphaItem = new(Heif4CharCode.Av01, 2) { ChannelBitDepths = [10], Av1CodecConfiguration = new Av1CodecConfiguration(alphaHeader), AuxiliaryType = HeifConstants.AlphaAuxiliaryType }; alphaItem.SetExtent(new Size(64, 48)); List items = [colorItem, alphaItem]; using AutoExpandingMemory memory = new(Configuration.Default, 16); int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); ReadOnlySpan propertyBox = memory.GetSpan(length); Assert.Equal(length, BinaryPrimitives.ReadInt32BigEndian(propertyBox)); Assert.Equal(Heif4CharCode.Iprp, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[4..])); const int IpcoOffset = 8; int ipcoSize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]); Assert.Equal(Heif4CharCode.Ipco, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(IpcoOffset + 4)..])); Heif4CharCode[] expectedTypes = [ Heif4CharCode.Ispe, Heif4CharCode.Pixi, Heif4CharCode.Av1C, Heif4CharCode.Colr, Heif4CharCode.Ispe, Heif4CharCode.Pixi, Heif4CharCode.Av1C, Heif4CharCode.AuxC ]; int propertyOffset = IpcoOffset + 8; int ipcoEnd = IpcoOffset + ipcoSize; int propertyIndex = 0; while (propertyOffset < ipcoEnd) { int propertySize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[propertyOffset..]); Heif4CharCode propertyType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(propertyOffset + 4)..]); ReadOnlySpan payload = propertyBox.Slice(propertyOffset + 8, propertySize - 8); Assert.Equal(expectedTypes[propertyIndex], propertyType); switch (propertyIndex) { case 0: case 4: Assert.Equal(0, BinaryPrimitives.ReadInt32BigEndian(payload)); Assert.Equal(64, BinaryPrimitives.ReadInt32BigEndian(payload[4..])); Assert.Equal(48, BinaryPrimitives.ReadInt32BigEndian(payload[8..])); break; case 1: Assert.Equal([0, 0, 0, 0, 3, 10, 10, 10], payload.ToArray()); break; case 2: Assert.Equal([0x81, 0x1F, 0x4C, 0], payload.ToArray()); break; case 3: Assert.Equal(Heif4CharCode.Nclx, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(payload)); Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(payload[4..])); Assert.Equal(13, BinaryPrimitives.ReadUInt16BigEndian(payload[6..])); Assert.Equal(6, BinaryPrimitives.ReadUInt16BigEndian(payload[8..])); Assert.Equal(0x80, payload[10]); break; case 5: Assert.Equal([0, 0, 0, 0, 1, 10], payload.ToArray()); break; case 6: Assert.Equal([0x81, 0x1F, 0x5C, 0], payload.ToArray()); break; case 7: Assert.Equal(0, BinaryPrimitives.ReadInt32BigEndian(payload)); Assert.Equal(HeifConstants.AlphaAuxiliaryType, Encoding.UTF8.GetString(payload[4..^1])); Assert.Equal(0, payload[^1]); break; } propertyOffset += propertySize; propertyIndex++; } Assert.Equal(expectedTypes.Length, propertyIndex); Assert.Equal(ipcoEnd, propertyOffset); int ipmaOffset = ipcoEnd; int ipmaSize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[ipmaOffset..]); Assert.Equal(length - ipmaOffset, ipmaSize); Assert.Equal(Heif4CharCode.Ipma, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(ipmaOffset + 4)..])); ReadOnlySpan ipmaPayload = propertyBox.Slice(ipmaOffset + 8, ipmaSize - 8); Assert.Equal(0, BinaryPrimitives.ReadInt32BigEndian(ipmaPayload)); Assert.Equal(2, BinaryPrimitives.ReadInt32BigEndian(ipmaPayload[4..])); Assert.Equal( [0, 1, 4, 1, 2, 0x83, 4, 0, 2, 4, 5, 6, 0x87, 8], ipmaPayload[8..].ToArray()); } [Fact] public void Av1ItemPropertiesWriteIccBeforeCicpAndExcludeMetadataItemsFromAssociations() { IccProfile iccProfile = new(IccTestDataProfiles.ProfileRandomArray); HeifItem colorItem = new(Heif4CharCode.Av01, 1) { IccProfile = iccProfile, CicpProfile = new CicpProfile(1, 13, 6, true) }; colorItem.SetExtent(new Size(64, 48)); List items = [ colorItem, new HeifItem(Heif4CharCode.Exif, 2), new HeifItem(Heif4CharCode.Mime, 3) ]; using AutoExpandingMemory memory = new(Configuration.Default, 16); int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); ReadOnlySpan propertyBox = memory.GetSpan(length); const int IpcoOffset = 8; int ipcoEnd = IpcoOffset + BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]); int propertyOffset = IpcoOffset + 8; Assert.Equal(Heif4CharCode.Ispe, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(propertyOffset + 4)..])); propertyOffset += BinaryPrimitives.ReadInt32BigEndian(propertyBox[propertyOffset..]); int iccPropertyLength = BinaryPrimitives.ReadInt32BigEndian(propertyBox[propertyOffset..]); Assert.Equal(Heif4CharCode.Colr, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(propertyOffset + 4)..])); Assert.Equal(Heif4CharCode.Prof, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(propertyOffset + 8)..])); Assert.Equal( IccTestDataProfiles.ProfileRandomArray, propertyBox.Slice(propertyOffset + 12, iccPropertyLength - 12).ToArray()); propertyOffset += iccPropertyLength; int cicpPropertyLength = BinaryPrimitives.ReadInt32BigEndian(propertyBox[propertyOffset..]); Assert.Equal(Heif4CharCode.Colr, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(propertyOffset + 4)..])); Assert.Equal(Heif4CharCode.Nclx, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[(propertyOffset + 8)..])); propertyOffset += cicpPropertyLength; Assert.Equal(ipcoEnd, propertyOffset); int ipmaOffset = ipcoEnd; int ipmaLength = BinaryPrimitives.ReadInt32BigEndian(propertyBox[ipmaOffset..]); ReadOnlySpan ipmaPayload = propertyBox.Slice(ipmaOffset + 8, ipmaLength - 8); Assert.Equal(0, BinaryPrimitives.ReadInt32BigEndian(ipmaPayload)); Assert.Equal(1, BinaryPrimitives.ReadInt32BigEndian(ipmaPayload[4..])); Assert.Equal([0, 1, 3, 1, 2, 3], ipmaPayload[8..].ToArray()); } [Fact] public void ItemPropertiesUseLargeAssociationsWhenPropertyCountExceedsCompactRange() { const int ItemCount = 43; ObuSequenceHeader sequenceHeader = new() { SequenceProfile = ObuSequenceProfile.Main, OperatingPoint = [new ObuOperatingPoint { SequenceLevelIndex = 31 }], ColorConfig = new ObuColorConfig { BitDepth = Av1BitDepth.EightBit, SubSamplingX = true, SubSamplingY = true } }; Av1CodecConfiguration codecConfiguration = new(sequenceHeader); byte[] channelBitDepths = [8, 8, 8]; List items = new(ItemCount); for (uint itemId = 1; itemId <= ItemCount; itemId++) { HeifItem item = new(Heif4CharCode.Av01, itemId) { ChannelBitDepths = channelBitDepths, Av1CodecConfiguration = codecConfiguration }; item.SetExtent(new Size(1, 1)); items.Add(item); } using AutoExpandingMemory memory = new(Configuration.Default, 16); int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); ReadOnlySpan propertyBox = memory.GetSpan(length); const int IpcoOffset = 8; int ipcoSize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]); int ipmaOffset = IpcoOffset + ipcoSize; Assert.Equal(1, BinaryPrimitives.ReadInt32BigEndian(propertyBox[(ipmaOffset + 8)..])); Assert.Equal(ItemCount, BinaryPrimitives.ReadInt32BigEndian(propertyBox[(ipmaOffset + 12)..])); const int AssociationEntrySize = 9; int finalEntryOffset = ipmaOffset + 16 + ((ItemCount - 1) * AssociationEntrySize); Assert.Equal(ItemCount, BinaryPrimitives.ReadUInt16BigEndian(propertyBox[finalEntryOffset..])); Assert.Equal(3, propertyBox[finalEntryOffset + 2]); Assert.Equal(127, BinaryPrimitives.ReadUInt16BigEndian(propertyBox[(finalEntryOffset + 3)..])); Assert.Equal(128, BinaryPrimitives.ReadUInt16BigEndian(propertyBox[(finalEntryOffset + 5)..])); Assert.Equal(0x8081, BinaryPrimitives.ReadUInt16BigEndian(propertyBox[(finalEntryOffset + 7)..])); } [Fact] public void LegacyJpegEncodingDoesNotMutateSourceHeifMetadata() { using Image image = new(1, 1); image[0, 0] = new Rgba32(10, 20, 30, 255); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); metadata.CompressionMethod = HeifCompressionMethod.Av1; using MemoryStream stream = new(); HeifEncoder encoder = new(); image.Save(stream, encoder); Assert.Same(metadata, image.Metadata.GetHeifMetadata()); Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); } [Theory] [WithFile(TestImages.Heif.IrvineAvif, PixelTypes.Rgba32, HeifCompressionMethod.LegacyJpeg)] public static void Encode(TestImageProvider provider, HeifCompressionMethod compressionMethod) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance)); using MemoryStream stream = new(); HeifEncoder encoder = new(); image.Save(stream, encoder); stream.Position = 0; ImageInfo imageInfo = Image.Identify(stream); Assert.Equal(image.Size, imageInfo.Size); stream.Position = 0; using Image encodedImage = Image.Load(stream); HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata(); ImageComparer.Exact.CompareImages(image, encodedImage); Assert.Equal(compressionMethod, heifMetadata.CompressionMethod); } private static Span GetItemPayload(Span file, ushort itemId) { int offset = 0; while (offset < file.Length) { int boxSize = BinaryPrimitives.ReadInt32BigEndian(file[offset..]); Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(offset + 4)..]); if (boxType == Heif4CharCode.Meta) { int childOffset = offset + 12; int boxEnd = offset + boxSize; while (childOffset < boxEnd) { int childSize = BinaryPrimitives.ReadInt32BigEndian(file[childOffset..]); Heif4CharCode childType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(childOffset + 4)..]); if (childType == Heif4CharCode.Iloc) { int locationOffset = childOffset + 14; int itemCount = BinaryPrimitives.ReadUInt16BigEndian(file[locationOffset..]); locationOffset += 2; for (int itemIndex = 0; itemIndex < itemCount; itemIndex++) { ushort currentItemId = BinaryPrimitives.ReadUInt16BigEndian(file[locationOffset..]); locationOffset += 6; int extentCount = BinaryPrimitives.ReadUInt16BigEndian(file[locationOffset..]); locationOffset += 2; for (int extentIndex = 0; extentIndex < extentCount; extentIndex++) { int itemOffset = checked((int)BinaryPrimitives.ReadUInt64BigEndian(file[locationOffset..])); locationOffset += 8; int itemLength = BinaryPrimitives.ReadInt32BigEndian(file[locationOffset..]); locationOffset += 4; if (currentItemId == itemId) { return file.Slice(itemOffset, itemLength); } } } } childOffset += childSize; } } offset += boxSize; } throw new InvalidImageContentException($"The encoded file has no payload for item {itemId}."); } private static Span GetMetadataChild(Span file, Heif4CharCode childType) { int offset = 0; while (offset < file.Length) { int boxSize = BinaryPrimitives.ReadInt32BigEndian(file[offset..]); Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(offset + 4)..]); if (boxType == Heif4CharCode.Meta) { int childOffset = offset + 12; int boxEnd = offset + boxSize; while (childOffset < boxEnd) { int childSize = BinaryPrimitives.ReadInt32BigEndian(file[childOffset..]); Heif4CharCode currentChildType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(childOffset + 4)..]); if (currentChildType == childType) { return file.Slice(childOffset, childSize); } childOffset += childSize; } } offset += boxSize; } throw new InvalidImageContentException($"The encoded file has no {childType} metadata child."); } }