diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index fe99ed305f..4306d1ff4d 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -870,6 +870,7 @@ Encoder verification contract:
### 7. Write complete AVIF output
- [~] The encoder-side AV1 codec configuration is now derived directly from the encoded sequence header and writes the fixed four-byte `av1C` record with empty `configOBUs`. The image payload retains the required sequence header, so the property introduces no sequence-header allocation, retention, or copy. Four production-header cases cover main, high, and professional profiles; 8-, 10-, and 12-bit precision; monochrome, 4:2:0, 4:2:2, and 4:4:4 sampling; exact fixed bytes; decoder reparsing; and header/property equivalence through direct net11 Release VSTest. Property-container emission and public AVIF activation remain open.
+- [~] AV1 image properties now write `ispe`, `pixi`, `av1C`, `colr`, and `auxC` in current AVIF item order. Only `av1C` is essential; color and alpha items retain independent property sets and the registered alpha auxiliary type. The property container reacquires its span after nested expansion before patching `ipco`, removing the prior stale-buffer write, and selects compact or 15-bit `ipma` indices from the property count rather than the unrelated item count. A forced-growth color-plus-alpha case validates every property payload and association byte; a separate 43-item, 129-property case proves indices 127 through 129 and the extended essential bit. Both pass direct foreground net11 Release VSTest. Complete AVIF assembly remains open.
- [ ] Write the correct AVIF file type, item information, locations, references, properties, AV1 configuration, dimensions, color, alpha, metadata, and media data.
- [ ] Support single images, alpha auxiliary images, grids, multiple extents, and bounded image sequences in the final public scope.
- [ ] Preserve ICC, Exif, and XMP according to encoder options.
diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
index cc3e955492..5fc2d18d61 100644
--- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
@@ -2,9 +2,12 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
+using System.Text;
+using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif;
@@ -303,7 +306,7 @@ internal sealed class HeifEncoderCore
/// The destination offset within the metadata box.
/// The items whose dimensions are written and associated.
/// The complete item-properties-box length.
- private static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items)
+ public static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items)
{
Span buffer = memory.GetSpan(memoryOffset, 20);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp);
@@ -314,12 +317,50 @@ internal sealed class HeifEncoderCore
foreach (HeifItem item in items)
{
bytesWritten += WriteSpatialExtentPropertyBox(memory, memoryOffset + bytesWritten, item);
+
+ byte[]? channelBitDepths = item.ChannelBitDepths;
+ if (channelBitDepths is not null)
+ {
+ bytesWritten += WritePixelInformationPropertyBox(memory, memoryOffset + bytesWritten, channelBitDepths);
+ }
+
+ Av1CodecConfiguration? codecConfiguration = item.Av1CodecConfiguration;
+ if (codecConfiguration is not null)
+ {
+ bytesWritten += WriteAv1CodecConfigurationPropertyBox(memory, memoryOffset + bytesWritten, codecConfiguration);
+ }
+
+ string? auxiliaryType = item.AuxiliaryType;
+ if (auxiliaryType is not null)
+ {
+ bytesWritten += WriteAuxiliaryTypePropertyBox(memory, memoryOffset + bytesWritten, auxiliaryType);
+ }
+
+ CicpProfile? cicpProfile = item.CicpProfile;
+ if (cicpProfile is not null)
+ {
+ bytesWritten += WriteColorInformationPropertyBox(memory, memoryOffset + bytesWritten, cicpProfile);
+ }
}
+ buffer = memory.GetSpan(memoryOffset, bytesWritten);
BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset));
- bool largePropertyIndex = items.Count > 0x7F;
- int propertyIndexSize = largePropertyIndex ? 2 : 1;
- buffer = memory.GetSpan(memoryOffset, bytesWritten + 16 + ((3 + propertyIndexSize) * items.Count));
+ int propertyCount = 0;
+ int associationBoxCapacity = 16;
+ foreach (HeifItem item in items)
+ {
+ int itemPropertyCount = GetPropertyCount(item);
+ propertyCount += itemPropertyCount;
+ associationBoxCapacity += 3 + itemPropertyCount;
+ }
+
+ bool largePropertyIndex = propertyCount > 0x7F;
+ if (largePropertyIndex)
+ {
+ associationBoxCapacity += propertyCount;
+ }
+
+ buffer = memory.GetSpan(memoryOffset, bytesWritten + associationBoxCapacity);
// ipma uses a 15-bit index only when the property table cannot fit in the compact seven-bit form.
int ipmaLengthOffset = bytesWritten;
@@ -331,18 +372,30 @@ internal sealed class HeifEncoderCore
{
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id);
bytesWritten += 2;
- buffer[bytesWritten++] = 1;
- if (largePropertyIndex)
+
+ int itemPropertyCount = GetPropertyCount(item);
+ buffer[bytesWritten++] = (byte)itemPropertyCount;
+ WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, false);
+
+ if (item.ChannelBitDepths is not null)
{
- BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propertyIndex);
- bytesWritten += 2;
+ WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, false);
}
- else
+
+ if (item.Av1CodecConfiguration is not null)
+ {
+ WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, true);
+ }
+
+ if (item.AuxiliaryType is not null)
{
- buffer[bytesWritten++] = (byte)propertyIndex;
+ WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, false);
}
- propertyIndex++;
+ if (item.CicpProfile is not null)
+ {
+ WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, false);
+ }
}
BinaryPrimitives.WriteUInt32BigEndian(buffer[ipmaLengthOffset..], (uint)(bytesWritten - ipmaLengthOffset));
@@ -352,6 +405,141 @@ internal sealed class HeifEncoderCore
return bytesWritten;
}
+ ///
+ /// Gets the number of properties emitted for an item.
+ ///
+ /// The item whose populated properties are counted.
+ /// The number of emitted properties.
+ private static int GetPropertyCount(HeifItem item)
+ {
+ int count = 1;
+ count += item.ChannelBitDepths is not null ? 1 : 0;
+ count += item.Av1CodecConfiguration is not null ? 1 : 0;
+ count += item.AuxiliaryType is not null ? 1 : 0;
+ count += item.CicpProfile is not null ? 1 : 0;
+ return count;
+ }
+
+ ///
+ /// Writes one compact or extended property association.
+ ///
+ /// The item-property-association destination.
+ /// The current destination offset, advanced past the association.
+ /// The one-based property index.
+ /// Whether the association uses a 15-bit property index.
+ /// Whether decoding the item requires understanding this property.
+ private static void WritePropertyAssociation(
+ Span buffer,
+ ref int offset,
+ ushort propertyIndex,
+ bool largePropertyIndex,
+ bool essential)
+ {
+ if (largePropertyIndex)
+ {
+ ushort association = essential ? (ushort)(propertyIndex | 0x8000) : propertyIndex;
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[offset..], association);
+ offset += 2;
+ }
+ else
+ {
+ buffer[offset++] = essential ? (byte)(propertyIndex | 0x80) : (byte)propertyIndex;
+ }
+ }
+
+ ///
+ /// Writes the encoded precision of each image channel.
+ ///
+ /// The expanding metadata buffer.
+ /// The destination offset within the property container.
+ /// The encoded precision of each channel.
+ /// The complete pixel-information-box length.
+ private static int WritePixelInformationPropertyBox(
+ AutoExpandingMemory memory,
+ int memoryOffset,
+ ReadOnlySpan channelBitDepths)
+ {
+ Span buffer = memory.GetSpan(memoryOffset, 13 + channelBitDepths.Length);
+ int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pixi, 0, 0);
+ buffer[bytesWritten++] = (byte)channelBitDepths.Length;
+ channelBitDepths.CopyTo(buffer[bytesWritten..]);
+ bytesWritten += channelBitDepths.Length;
+
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
+ return bytesWritten;
+ }
+
+ ///
+ /// Writes an AV1 codec-configuration property.
+ ///
+ /// The expanding metadata buffer.
+ /// The destination offset within the property container.
+ /// The fixed image configuration.
+ /// The complete AV1 codec-configuration-box length.
+ private static int WriteAv1CodecConfigurationPropertyBox(
+ AutoExpandingMemory memory,
+ int memoryOffset,
+ Av1CodecConfiguration configuration)
+ {
+ Span buffer = memory.GetSpan(memoryOffset, 8 + Av1CodecConfiguration.FixedHeaderSize);
+ int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Av1C);
+ configuration.WriteFixedHeader(buffer.Slice(bytesWritten, Av1CodecConfiguration.FixedHeaderSize));
+ bytesWritten += Av1CodecConfiguration.FixedHeaderSize;
+
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
+ return bytesWritten;
+ }
+
+ ///
+ /// Writes the registered type of an auxiliary image item.
+ ///
+ /// The expanding metadata buffer.
+ /// The destination offset within the property container.
+ /// The null-terminated registered auxiliary type.
+ /// The complete auxiliary-type-box length.
+ private static int WriteAuxiliaryTypePropertyBox(
+ AutoExpandingMemory memory,
+ int memoryOffset,
+ string auxiliaryType)
+ {
+ int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(auxiliaryType);
+ Span buffer = memory.GetSpan(memoryOffset, 13 + auxiliaryTypeLength);
+ int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.AuxC, 0, 0);
+ bytesWritten += Encoding.UTF8.GetBytes(auxiliaryType, buffer[bytesWritten..]);
+ buffer[bytesWritten++] = 0;
+
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
+ return bytesWritten;
+ }
+
+ ///
+ /// Writes an H.273 color description for a color image item.
+ ///
+ /// The expanding metadata buffer.
+ /// The destination offset within the property container.
+ /// The color description to write.
+ /// The complete color-information-box length.
+ private static int WriteColorInformationPropertyBox(
+ AutoExpandingMemory memory,
+ int memoryOffset,
+ CicpProfile profile)
+ {
+ Span buffer = memory.GetSpan(memoryOffset, 19);
+ int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Nclx);
+ bytesWritten += 4;
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)profile.ColorPrimaries);
+ bytesWritten += 2;
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)profile.TransferCharacteristics);
+ bytesWritten += 2;
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)profile.MatrixCoefficients);
+ bytesWritten += 2;
+ buffer[bytesWritten++] = profile.FullRange ? (byte)0x80 : (byte)0;
+
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
+ return bytesWritten;
+ }
+
///
/// Writes an item's display width and height as an image-spatial-extents property.
///
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
index 6d6a8766bf..43eb593d3b 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
@@ -1,7 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers.Binary;
+using System.Text;
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.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@@ -146,6 +152,186 @@ public class HeifEncoderTests
Assert.Equal(0, stream.Length);
}
+ [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 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()
{