📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1685 lines
71 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
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.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
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.Av1, 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<ArgumentException>(() => new HeifEncoder { Quality = quality });
[Theory]
[InlineData(-1)]
[InlineData(101)]
public void AlphaQualityOutsideRangeThrows(int quality)
=> Assert.Throws<ArgumentException>(() => new HeifEncoder { AlphaQuality = quality });
[Theory]
[InlineData(-1)]
[InlineData(11)]
public void EffortOutsideRangeThrows(int effort)
=> Assert.Throws<ArgumentException>(() => 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<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Quality = 0
};
image.Save(stream, encoder);
Assert.NotEqual(0, stream.Length);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void LegacyJpegWritesNonSeekableStream()
{
using Image<Rgba32> 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 { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
Assert.NotEqual(0, storage.Length);
storage.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(storage);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void LegacyJpegWritesAtCurrentStreamPosition()
{
using Image<Rgba32> 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 { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
stream.Position = fileStart;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(image.Size, decoded.Size);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, false)]
public void LegacyJpegHonorsSkipMetadataForEmbeddedProfiles(bool skipMetadata, bool expectedIccProfile)
{
using Image<Rgb24> image = new(8, 8);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
SkipMetadata = skipMetadata
};
image.Save(stream, encoder);
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(preserveOptions, stream);
Assert.Equal(expectedIccProfile, decoded.Metadata.IccProfile is not null);
if (expectedIccProfile)
{
Assert.Equal(
IccTestDataProfiles.ProfileRandomArray,
Assert.IsType<IccProfile>(decoded.Metadata.IccProfile).ToByteArray());
}
}
[Fact]
public void LegacyJpegIgnoresLosslessOption()
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Lossless = true
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(image.Size, decoded.Size);
Assert.Equal(
HeifCompressionMethod.LegacyJpeg,
decoded.Metadata.GetHeifMetadata().CompressionMethod);
}
[Theory]
[InlineData(HeifBitDepth.Bit10)]
[InlineData(HeifBitDepth.Bit12)]
public void LegacyJpegNormalizesHighBitDepthToEightBit(HeifBitDepth bitDepth)
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
BitDepth = bitDepth
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(HeifBitDepth.Bit8, decoded.Metadata.GetHeifMetadata().BitDepth);
}
[Fact]
public void LegacyJpegEncodesRootFrameFromImageSequence()
{
using Image<Rgba32> image = new(1, 1);
image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgba32(255, 255, 255);
image.Frames.AddFrame(image.Frames.RootFrame);
image.Frames[1].PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgba32(0, 0, 0);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Quality = 100
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Single(decoded.Frames);
Assert.Equal(new Rgba32(255, 255, 255), decoded.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
}
[Fact]
public void Av1ImageSequencePreservesSeparateRootFrame()
{
const int width = 8;
const int height = 8;
using Image<Rgb24> image = new(width, height);
for (int row = 0; row < height; row++)
{
image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row).Fill(new Rgb24(255, 255, 255));
}
image.Frames.AddFrame(image.Frames.RootFrame);
for (int row = 0; row < height; row++)
{
image.Frames[1].PixelBuffer.DangerousGetRowSpan(row).Fill(new Rgb24(0, 0, 0));
}
image.Frames[1].Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 20);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
AnimateRootFrame = false,
Lossless = true,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> fileType = GetTopLevelBox(file, Heif4CharCode.Ftyp);
Assert.Equal(Heif4CharCode.Miaf, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[^sizeof(uint)..]));
Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(GetMetadataChild(file, Heif4CharCode.Pitm)[12..]));
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream);
Assert.Equal(2, decoded.Frames.Count);
Assert.False(decoded.Metadata.GetHeifMetadata().AnimateRootFrame);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
Assert.Equal(
image.Frames[1].Metadata.GetHeifMetadata().FrameDelay,
decoded.Frames[1].Metadata.GetHeifMetadata().FrameDelay);
}
[Fact]
public void GridDecoderAcceptsSmallerRightAndBottomColorAndAlphaCells()
{
const int tileWidth = 64;
const int tileHeight = 64;
const int outputWidth = 96;
const int outputHeight = 96;
Size[] tileSizes =
[
new(tileWidth, tileHeight),
new(outputWidth - tileWidth, tileHeight),
new(tileWidth, outputHeight - tileHeight),
new(outputWidth - tileWidth, outputHeight - tileHeight)
];
Rgba32[] tileColors =
[
new(32, 32, 32),
new(64, 64, 64),
new(96, 96, 96),
new(128, 128, 128)
];
ObuColorConfig colorConfig = new()
{
IsColorDescriptionPresent = true,
ColorPrimaries = ObuColorPrimaries.Bt709,
TransferCharacteristics = ObuTransferCharacteristics.Srgb,
MatrixCoefficients = ObuMatrixCoefficients.Bt709,
ColorRange = true,
IsMonochrome = true,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = Av1BitDepth.EightBit
};
List<HeifItem> items = [];
Dictionary<uint, byte[]> payloads = [];
HeifItem gridItem = new(Heif4CharCode.Grid, 1);
gridItem.SetExtent(new Size(outputWidth, outputHeight));
items.Add(gridItem);
HeifItemLink gridLink = new(Heif4CharCode.Dimg, gridItem.Id);
for (int tileIndex = 0; tileIndex < tileSizes.Length; tileIndex++)
{
Size tileSize = tileSizes[tileIndex];
using Image<Rgba32> tile = new(tileSize.Width, tileSize.Height, tileColors[tileIndex]);
using MemoryStream payload = new();
ObuSequenceHeader header = Av1FrameEncoder.Encode(
Configuration.Default,
tile.Frames.RootFrame,
payload,
colorConfig,
qIndex: 0,
effort: 0);
uint itemId = (uint)tileIndex + 2;
HeifItem tileItem = new(Heif4CharCode.Av01, itemId)
{
Av1CodecConfiguration = new Av1CodecConfiguration(header)
};
tileItem.SetExtent(tileSize);
items.Add(tileItem);
gridLink.DestinationIds.Add(itemId);
payloads.Add(itemId, payload.ToArray());
}
List<HeifItemLink> links = [gridLink];
GridHeifItemDecoder<Rgba32> decoder = new(items, links, ReadItem);
Span<byte> descriptor = [0, 0, 1, 1, 0, outputWidth, 0, outputHeight];
using Image<Rgba32> result = decoder.DecodeItemData(
new DecoderOptions { Configuration = Configuration.Default },
gridItem,
descriptor,
null,
TestContext.Current.CancellationToken);
for (int y = 0; y < outputHeight; y++)
{
for (int x = 0; x < outputWidth; x++)
{
int tileIndex = (y < tileHeight ? 0 : 2) + (x < tileWidth ? 0 : 1);
Assert.Equal(tileColors[tileIndex], result[x, y]);
}
}
Rgba32 opaqueColor = new(7, 11, 13);
using Image<Rgba32> alphaResult = new(outputWidth, outputHeight, opaqueColor);
decoder.DecodeAlphaItemData(
new DecoderOptions { Configuration = Configuration.Default },
gridItem,
descriptor,
alphaResult.Frames.RootFrame,
alphaResult.Size,
new Rectangle(Point.Empty, alphaResult.Size),
false,
TestContext.Current.CancellationToken);
for (int y = 0; y < outputHeight; y++)
{
for (int x = 0; x < outputWidth; x++)
{
int tileIndex = (y < tileHeight ? 0 : 2) + (x < tileWidth ? 0 : 1);
Rgba32 expected = opaqueColor;
expected.A = tileColors[tileIndex].R;
Assert.Equal(expected, alphaResult[x, y]);
}
}
IMemoryOwner<byte> ReadItem(HeifItem item)
{
byte[] payload = payloads[item.Id];
IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(payload.Length);
payload.CopyTo(owner.Memory.Span);
return owner;
}
}
[Fact]
public void GridDecoderRejectsCellSmallerThanMiafMinimum()
{
ObuColorConfig colorConfig = new()
{
IsColorDescriptionPresent = true,
ColorPrimaries = ObuColorPrimaries.Bt709,
TransferCharacteristics = ObuTransferCharacteristics.Srgb,
MatrixCoefficients = ObuMatrixCoefficients.Bt709,
ColorRange = true,
IsMonochrome = true,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = Av1BitDepth.EightBit
};
InvalidImageContentException exception = Assert.Throws<InvalidImageContentException>(
() =>
{
using Image<Rgba32> decoded = DecodeSingleCellGrid(63, 64, colorConfig);
});
Assert.Contains("grid cells must be at least 64 samples", exception.Message, StringComparison.Ordinal);
}
/// <summary>
/// Verifies grid dimensions preserve chroma alignment for AV1's 4:2:2 and 4:2:0 layouts.
/// </summary>
[Theory]
[InlineData(65, 64, true, false)]
[InlineData(65, 64, true, true)]
[InlineData(64, 65, true, true)]
public void GridDecoderRejectsOddSubsampledDimension(
int width,
int height,
bool subsamplingX,
bool subsamplingY)
{
ObuColorConfig colorConfig = new()
{
IsColorDescriptionPresent = true,
ColorPrimaries = ObuColorPrimaries.Bt709,
TransferCharacteristics = ObuTransferCharacteristics.Srgb,
MatrixCoefficients = ObuMatrixCoefficients.Bt709,
ColorRange = true,
BitDepth = Av1BitDepth.EightBit,
SubSamplingX = subsamplingX,
SubSamplingY = subsamplingY
};
InvalidImageContentException exception = Assert.Throws<InvalidImageContentException>(
() =>
{
using Image<Rgba32> decoded = DecodeSingleCellGrid(width, height, colorConfig);
});
Assert.Contains("must be even", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Av1OversizedStillImageWritesAndDecodesGrid()
{
const int width = 65537;
using Image<Rgb24> image = new(width, 1);
image[0, 0] = new Rgb24(1, 2, 3);
image[32768, 0] = new Rgb24(11, 13, 17);
image[32769, 0] = new Rgb24(19, 23, 29);
image[width - 1, 0] = new Rgb24(31, 37, 41);
// Identity-matrix 4:4:4 makes the lossless AV1 cells preserve the packed RGB channels exactly.
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Assert.Equal(
[0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1],
GetItemPayload(file, 1).ToArray());
using Av1Decoder firstCellDecoder = new(Configuration.Default);
using Image<Rgb24> firstCell = firstCellDecoder.Decode<Rgb24>(GetItemPayload(file, 2));
using Av1Decoder secondCellDecoder = new(Configuration.Default);
using Image<Rgb24> secondCell = secondCellDecoder.Decode<Rgb24>(GetItemPayload(file, 3));
// AVIF requires the first grid cell to be at least 64 samples on both axes. The derived grid trims
// the replicated right and bottom edges back to the presentation encoded in its descriptor.
Assert.Equal(new Size(32769, 64), firstCell.Size);
Assert.Equal(new Size(32769, 64), secondCell.Size);
Assert.Equal(image[32768, 0], firstCell[32768, 0]);
Assert.Equal(image[32769, 0], secondCell[0, 0]);
Assert.Equal(image[width - 1, 0], secondCell[32767, 0]);
Assert.Equal(secondCell[32767, 0], secondCell[32768, 0]);
Assert.Equal(secondCell[0, 0], secondCell[0, 63]);
Assert.Equal(1U, GetItemInfoFlags(file, 2));
Assert.Equal(1U, GetItemInfoFlags(file, 3));
ReadOnlySpan<byte> references = GetMetadataChild(file, Heif4CharCode.Iref);
const int FirstReferenceOffset = 12;
Assert.Equal(
Heif4CharCode.Dimg,
(Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(references[(FirstReferenceOffset + 4)..]));
Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(references[(FirstReferenceOffset + 8)..]));
Assert.Equal(2, BinaryPrimitives.ReadUInt16BigEndian(references[(FirstReferenceOffset + 10)..]));
Assert.Equal(2, BinaryPrimitives.ReadUInt16BigEndian(references[(FirstReferenceOffset + 12)..]));
Assert.Equal(3, BinaryPrimitives.ReadUInt16BigEndian(references[(FirstReferenceOffset + 14)..]));
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
}
[Fact]
public void Av1LosslessRoundTripPreservesColorAndAlpha()
{
const int width = 8;
const int height = 8;
using Image<Rgba32> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgba32(
(byte)((column * 31) + row),
(byte)((row * 29) + column),
(byte)((column * 17) + (row * 11)),
(byte)((column * 23) + (row * 7)));
}
}
// Identity-matrix 4:4:4 maps the packed RGB channels directly onto AV1 planes, so codec losslessness
// can be asserted against the original pixels without a separate color-conversion tolerance.
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> colorPayload = GetItemPayload(file, 1);
using Av1Decoder colorDecoder = new(Configuration.Default);
using Image<Rgba32> colorImage = colorDecoder.Decode<Rgba32>(colorPayload);
ObuSequenceHeader colorSequenceHeader = Assert.IsType<ObuSequenceHeader>(colorDecoder.SequenceHeader);
ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader);
Assert.Equal(Av1ColorFormat.Yuv444, colorSequenceHeader.ColorConfig.GetColorFormat());
Assert.Equal(0, colorFrameHeader.QuantizationParameters.BaseQIndex);
Assert.True(colorFrameHeader.CodedLossless);
Assert.True(colorFrameHeader.AllLossless);
Assert.Equal(Av1TransformMode.Only4x4, colorFrameHeader.TransformMode);
Span<byte> alphaPayload = GetItemPayload(file, 2);
using Av1Decoder alphaDecoder = new(Configuration.Default);
using Image<L8> alphaImage = alphaDecoder.Decode<L8>(alphaPayload);
ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader);
Assert.Equal(0, alphaFrameHeader.QuantizationParameters.BaseQIndex);
Assert.True(alphaFrameHeader.CodedLossless);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-lossless-color.obu"), colorPayload.ToArray());
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-lossless-alpha.obu"), alphaPayload.ToArray());
}
[Theory]
[InlineData(HeifBitDepth.Bit10)]
[InlineData(HeifBitDepth.Bit12)]
public void Av1LosslessRoundTripPreservesHighBitDepthSourcePixels(HeifBitDepth bitDepth)
{
const int width = 8;
const int height = 8;
int codedMaximum = (1 << (int)bitDepth) - 1;
using Image<Rgba64> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba64> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
int red = ((column * 131) + (row * 37) + 1) & codedMaximum;
int green = ((column * 61) + (row * 173) + 3) & codedMaximum;
int blue = ((column * 211) + (row * 47) + 5) & codedMaximum;
int alpha = ((column * 127) + (row * 89)) & codedMaximum;
pixels[column] = new Rgba64(
ExpandToUShort(red, codedMaximum),
ExpandToUShort(green, codedMaximum),
ExpandToUShort(blue, codedMaximum),
ExpandToUShort(alpha, codedMaximum));
}
}
// Full-range identity 4:4:4 preserves the requested sample lattice, isolating source precision from a
// deliberately lossy color matrix or chroma subsampling step.
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
BitDepth = bitDepth,
ChromaSubsampling = HeifChromaSubsampling.Yuv444,
Lossless = true,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> colorPayload = GetItemPayload(file, 1);
Span<byte> alphaPayload = GetItemPayload(file, 2);
stream.Position = 0;
using Image<Rgba64> decoded = Image.Load<Rgba64>(stream);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-public-lossless-{(int)bitDepth}b-color.obu"),
colorPayload.ToArray());
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-public-lossless-{(int)bitDepth}b-alpha.obu"),
alphaPayload.ToArray());
}
private static ushort ExpandToUShort(int sample, int maximum)
=> (ushort)(((sample * (long)ushort.MaxValue) + (maximum / 2)) / maximum);
[Theory]
[InlineData((ushort)0, null, (ushort)0)]
[InlineData((ushort)3, null, (ushort)3)]
[InlineData((ushort)3, (ushort)7, (ushort)7)]
public void Av1LosslessImageSequencePreservesFramesTimingAndAlpha(
ushort metadataRepeatCount,
ushort? encoderRepeatCount,
ushort expectedRepeatCount)
{
const int width = 8;
const int height = 8;
const int frameCount = 3;
using Image<Rgba32> image = new(width, height);
image.Frames.AddFrame(image.Frames.RootFrame);
image.Frames.AddFrame(image.Frames.RootFrame);
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
ImageFrame<Rgba32> frame = image.Frames[frameIndex];
frame.Metadata.GetHeifMetadata().FrameDelay = frameIndex switch
{
0 => new Rational(1, 24),
1 => new Rational(1, 25),
_ => new Rational(1, 30)
};
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = frame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgba32(
(byte)((frameIndex * 53) + (column * 19) + row),
(byte)((frameIndex * 31) + (row * 23) + column),
(byte)((frameIndex * 71) + (column * 7) + (row * 13)),
(byte)((frameIndex * 47) + (column * 17) + (row * 11)));
}
}
}
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
ExifProfile exifProfile = new();
exifProfile.SetValue(ExifTag.Software, "ImageSharp AV1 sequence");
image.Metadata.ExifProfile = exifProfile;
byte[] xmpData = Encoding.UTF8.GetBytes("<xmp>ImageSharp AV1 sequence</xmp>");
image.Metadata.XmpProfile = new XmpProfile(xmpData);
image.Metadata.GetHeifMetadata().RepeatCount = metadataRepeatCount;
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true,
Effort = 0,
RepeatCount = encoderRepeatCount
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Assert.Equal((uint)Heif4CharCode.Avis, BinaryPrimitives.ReadUInt32BigEndian(file.AsSpan(8)));
Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(GetMetadataChild(file, Heif4CharCode.Pitm)[12..]));
using (Av1Decoder sampleDecoder = new(Configuration.Default))
using (Image<Rgba32> decodedSample = sampleDecoder.Decode<Rgba32>(GetItemPayload(file, 1)))
{
ObuSequenceHeader sampleHeader = sampleDecoder.SequenceHeader;
Assert.NotNull(sampleHeader);
Assert.False(sampleHeader.IsStillPicture);
Assert.False(sampleHeader.IsReducedStillPictureHeader);
}
stream.Position = 0;
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
using Image<Rgba32> decoded = Image.Load<Rgba32>(preserveOptions, stream);
Assert.Equal(frameCount, decoded.Frames.Count);
Assert.Equal(expectedRepeatCount, decoded.Metadata.GetHeifMetadata().RepeatCount);
Assert.True(decoded.Metadata.GetHeifMetadata().AnimateRootFrame);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
Assert.Equal(
IccTestDataProfiles.ProfileRandomArray,
Assert.IsType<IccProfile>(decoded.Metadata.IccProfile).ToByteArray());
ExifProfile decodedExif = Assert.IsType<ExifProfile>(decoded.Metadata.ExifProfile);
Assert.True(decodedExif.TryGetValue(ExifTag.Software, out IExifValue<string> software));
Assert.Equal("ImageSharp AV1 sequence", software.Value);
Assert.Equal(xmpData, Assert.IsType<XmpProfile>(decoded.Metadata.XmpProfile).ToByteArray());
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
Assert.Equal(
image.Frames[frameIndex].Metadata.GetHeifMetadata().FrameDelay,
decoded.Frames[frameIndex].Metadata.GetHeifMetadata().FrameDelay);
}
}
[Fact]
public void Av1ImageSequenceWritesToPrefixedNonSeekableStream()
{
using Image<Rgb24> image = new(8, 8);
image.Frames.AddFrame(image.Frames.RootFrame);
image.Frames.RootFrame.Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 10);
image.Frames[1].Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 20);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
image.Metadata.ExifProfile = new ExifProfile();
image.Metadata.ExifProfile.SetValue(ExifTag.Software, "suppressed");
image.Metadata.XmpProfile = new XmpProfile(Encoding.UTF8.GetBytes("<xmp>suppressed</xmp>"));
using MemoryStream storage = new();
storage.Write([1, 2, 3, 4]);
long fileStart = storage.Position;
using NonSeekableStream destination = new(storage);
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0,
SkipMetadata = true
};
image.Save(destination, encoder);
storage.Position = fileStart;
using Image<Rgb24> decoded = Image.Load<Rgb24>(storage);
Assert.Equal(image.Size, decoded.Size);
Assert.Equal(image.Frames.Count, decoded.Frames.Count);
Assert.Null(decoded.Metadata.IccProfile);
Assert.Null(decoded.Metadata.ExifProfile);
Assert.Null(decoded.Metadata.XmpProfile);
}
[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<Rgb24> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgb24> 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<byte> fileType = GetTopLevelBox(file, Heif4CharCode.Ftyp);
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<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb24> payloadImage = payloadDecoder.Decode<Rgb24>(payload);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(payloadDecoder.FrameHeader);
Assert.Equal(64, frameHeader.QuantizationParameters.BaseQIndex);
Assert.Equal(image.Size, payloadImage.Size);
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(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<CicpProfile>(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<Rgba32> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> 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<byte> colorPayload = GetItemPayload(file, 1);
Span<byte> alphaPayload = GetItemPayload(file, 2);
using Av1Decoder colorDecoder = new(Configuration.Default);
using Image<Rgba32> colorImage = colorDecoder.Decode<Rgba32>(colorPayload);
ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader);
Assert.Equal(64, colorFrameHeader.QuantizationParameters.BaseQIndex);
using Av1Decoder alphaDecoder = new(Configuration.Default);
using Image<L8> alphaImage = alphaDecoder.Decode<L8>(alphaPayload);
ObuSequenceHeader alphaSequenceHeader = Assert.IsType<ObuSequenceHeader>(alphaDecoder.SequenceHeader);
ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader);
Assert.True(alphaSequenceHeader.ColorConfig.IsMonochrome);
Assert.Equal(4, alphaFrameHeader.QuantizationParameters.BaseQIndex);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(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<Rgb24> image = new(8, 8);
for (int row = 0; row < image.Height; row++)
{
Span<Rgb24> 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<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb48> payloadImage = payloadDecoder.Decode<Rgb48>(payload);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(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<Rgb48> decoded = Image.Load<Rgb48>(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<Rgb24> 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<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb48> payloadImage = payloadDecoder.Decode<Rgb48>(payload);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader);
Assert.Equal(Av1BitDepth.TenBit, sequenceHeader.ColorConfig.BitDepth);
}
[Theory]
[InlineData(CicpMatrixCoefficients.Identity, HeifChromaSubsampling.Yuv420)]
[InlineData(CicpMatrixCoefficients.YCgCoRe, null)]
[InlineData(CicpMatrixCoefficients.YCgCoRe, HeifChromaSubsampling.Yuv420)]
[InlineData(CicpMatrixCoefficients.YCgCoRe, HeifChromaSubsampling.Yuv422)]
[InlineData(CicpMatrixCoefficients.YCgCoRo, null)]
[InlineData(CicpMatrixCoefficients.YCgCoRo, HeifChromaSubsampling.Yuv420)]
[InlineData(CicpMatrixCoefficients.YCgCoRo, HeifChromaSubsampling.Yuv422)]
public void Av1SanitizesIncompatibleMatrixWithoutMutatingSourceMetadata(
CicpMatrixCoefficients matrix,
HeifChromaSubsampling? subsampling)
{
using Image<Rgb24> image = new(8, 8, new Rgb24(32, 96, 192));
CicpProfile sourceProfile = new(1, 13, (byte)matrix, false);
image.Metadata.CicpProfile = sourceProfile;
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
ChromaSubsampling = subsampling,
Effort = 0
};
image.Save(stream, encoder);
Assert.Same(sourceProfile, image.Metadata.CicpProfile);
Assert.Equal(matrix, sourceProfile.MatrixCoefficients);
Assert.False(sourceProfile.FullRange);
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream);
CicpProfile decodedProfile = Assert.IsType<CicpProfile>(decoded.Metadata.CicpProfile);
Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, decodedProfile.MatrixCoefficients);
Assert.False(decodedProfile.FullRange);
// A fallback must change the actual encoded conversion as well as its metadata. Compare with the
// same packed pixels explicitly encoded using that fallback matrix and the requested sampling.
using Image<Rgb24> explicitConversion = image.Clone();
explicitConversion.Metadata.CicpProfile = new CicpProfile(1, 13, (byte)CicpMatrixCoefficients.ItuRBt601_7_525, false);
using MemoryStream expected = new();
explicitConversion.Save(expected, encoder);
Assert.Equal(expected.ToArray(), stream.ToArray());
}
[Fact]
public void Av1PreservesIccExifAndXmpMetadata()
{
using Image<Rgb24> 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("<xmp>ImageSharp HEIF</xmp>");
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<byte> 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<byte> 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<byte> 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<Rgb24> decoded = Image.Load<Rgb24>(preserveOptions, file);
Assert.Equal(
IccTestDataProfiles.ProfileRandomArray,
Assert.IsType<IccProfile>(decoded.Metadata.IccProfile).ToByteArray());
ExifProfile decodedExif = Assert.IsType<ExifProfile>(decoded.Metadata.ExifProfile);
Assert.True(decodedExif.TryGetValue(ExifTag.Software, out IExifValue<string> software));
Assert.Equal("ImageSharp HEIF", software.Value);
Assert.Equal(xmpData, Assert.IsType<XmpProfile>(decoded.Metadata.XmpProfile).ToByteArray());
}
[Fact]
public void Av1SkipMetadataSuppressesIccExifAndXmp()
{
using Image<Rgb24> 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("<xmp>ImageSharp HEIF</xmp>"));
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0,
SkipMetadata = true
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> itemInfo = GetMetadataChild(file, Heif4CharCode.Iinf);
Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(itemInfo[12..]));
Span<byte> 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<Rgb24> decoded = Image.Load<Rgb24>(preserveOptions, file);
Assert.Null(decoded.Metadata.IccProfile);
Assert.Null(decoded.Metadata.ExifProfile);
Assert.Null(decoded.Metadata.XmpProfile);
}
[Fact]
public void Av1WritesNonSeekableStream()
{
using Image<Rgb24> 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<Rgb24> decoded = Image.Load<Rgb24>(storage);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void Av1WritesAtCurrentStreamPosition()
{
using Image<Rgb24> 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<Rgb24> decoded = Image.Load<Rgb24>(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<HeifItem> items = [colorItem, alphaItem];
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
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<byte> 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<byte> 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 ItemPropertiesReuseAnEarlierIdenticalCellPropertySet()
{
ObuSequenceHeader sequenceHeader = new()
{
SequenceProfile = ObuSequenceProfile.Main,
OperatingPoint = [new ObuOperatingPoint { SequenceLevelIndex = 31 }],
ColorConfig = new ObuColorConfig
{
BitDepth = Av1BitDepth.EightBit,
SubSamplingX = false,
SubSamplingY = false
}
};
HeifItem firstCell = new(Heif4CharCode.Av01, 1)
{
ChannelBitDepths = [8, 8, 8],
Av1CodecConfiguration = new Av1CodecConfiguration(sequenceHeader)
};
firstCell.SetExtent(new Size(64, 64));
HeifItem secondCell = new(Heif4CharCode.Av01, 2)
{
PropertySource = firstCell
};
secondCell.SetExtent(firstCell.Extent);
List<HeifItem> items = [firstCell, secondCell];
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
Assert.Equal(92, length);
const int IpcoOffset = 8;
int ipmaOffset = IpcoOffset + BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]);
ReadOnlySpan<byte> ipmaPayload = propertyBox[(ipmaOffset + 8)..];
Assert.Equal(
[0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 3, 1, 2, 0x83, 0, 2, 3, 1, 2, 0x83],
ipmaPayload.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<HeifItem> items =
[
colorItem,
new HeifItem(Heif4CharCode.Exif, 2),
new HeifItem(Heif4CharCode.Mime, 3)
];
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
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<byte> 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<HeifItem> 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);
}
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
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<Rgba32> 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() { CompressionMethod = HeifCompressionMethod.LegacyJpeg };
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<TPixel>(TestImageProvider<TPixel> provider, HeifCompressionMethod compressionMethod)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance));
using MemoryStream stream = new();
HeifEncoder encoder = new() { CompressionMethod = compressionMethod };
image.Save(stream, encoder);
stream.Position = 0;
ImageInfo imageInfo = Image.Identify(stream);
Assert.Equal(image.Size, imageInfo.Size);
stream.Position = 0;
using Image<TPixel> encodedImage = Image.Load<TPixel>(stream);
HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata();
ImageComparer.Exact.CompareImages(image, encodedImage);
Assert.Equal(compressionMethod, heifMetadata.CompressionMethod);
}
private static Span<byte> GetItemPayload(Span<byte> 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 uint GetItemInfoFlags(Span<byte> file, ushort itemId)
{
ReadOnlySpan<byte> itemInformation = GetMetadataChild(file, Heif4CharCode.Iinf);
int entryOffset = 14;
while (entryOffset < itemInformation.Length)
{
int entrySize = BinaryPrimitives.ReadInt32BigEndian(itemInformation[entryOffset..]);
ReadOnlySpan<byte> entry = itemInformation.Slice(entryOffset, entrySize);
ushort currentItemId = BinaryPrimitives.ReadUInt16BigEndian(entry[12..]);
if (currentItemId == itemId)
{
return (uint)((entry[9] << 16) | (entry[10] << 8) | entry[11]);
}
entryOffset += entrySize;
}
throw new InvalidImageContentException($"The encoded file has no item-information entry for item {itemId}.");
}
private static Image<Rgba32> DecodeSingleCellGrid(int width, int height, ObuColorConfig colorConfig)
{
using Image<Rgba32> tile = new(width, height, new Rgba32(127, 127, 127));
using MemoryStream payloadStream = new();
ObuSequenceHeader header = Av1FrameEncoder.Encode(
Configuration.Default,
tile.Frames.RootFrame,
payloadStream,
colorConfig,
qIndex: 0,
effort: 0);
byte[] payload = payloadStream.ToArray();
HeifItem gridItem = new(Heif4CharCode.Grid, 1);
gridItem.SetExtent(new Size(width, height));
HeifItem tileItem = new(Heif4CharCode.Av01, 2)
{
Av1CodecConfiguration = new Av1CodecConfiguration(header)
};
tileItem.SetExtent(new Size(width, height));
HeifItemLink gridLink = new(Heif4CharCode.Dimg, gridItem.Id);
gridLink.DestinationIds.Add(tileItem.Id);
GridHeifItemDecoder<Rgba32> decoder = new([gridItem, tileItem], [gridLink], ReadItem);
byte[] descriptor = new byte[8];
BinaryPrimitives.WriteUInt16BigEndian(descriptor.AsSpan(4), (ushort)width);
BinaryPrimitives.WriteUInt16BigEndian(descriptor.AsSpan(6), (ushort)height);
return decoder.DecodeItemData(
new DecoderOptions { Configuration = Configuration.Default },
gridItem,
descriptor,
null,
TestContext.Current.CancellationToken);
IMemoryOwner<byte> ReadItem(HeifItem item)
{
Assert.Equal(tileItem.Id, item.Id);
IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(payload.Length);
payload.CopyTo(owner.Memory.Span);
return owner;
}
}
private static Span<byte> GetTopLevelBox(Span<byte> file, Heif4CharCode requestedType)
{
int offset = 0;
while (offset < file.Length)
{
int boxSize = BinaryPrimitives.ReadInt32BigEndian(file[offset..]);
Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(offset + sizeof(uint))..]);
if (boxType == requestedType)
{
return file.Slice(offset, boxSize);
}
offset += boxSize;
}
throw new InvalidImageContentException($"The encoded file has no {requestedType} top-level box.");
}
private static Span<byte> GetMetadataChild(Span<byte> 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.");
}
}