Browse Source

Write AVIF item properties

pull/2633/head
James Jackson-South 1 month ago
parent
commit
fe44469b1c
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 210
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  3. 186
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

1
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.

210
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
/// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <param name="items">The items whose dimensions are written and associated.</param>
/// <returns>The complete item-properties-box length.</returns>
private static int WriteItemPropertiesBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items)
public static int WriteItemPropertiesBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items)
{
Span<byte> 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;
}
/// <summary>
/// Gets the number of properties emitted for an item.
/// </summary>
/// <param name="item">The item whose populated properties are counted.</param>
/// <returns>The number of emitted properties.</returns>
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;
}
/// <summary>
/// Writes one compact or extended property association.
/// </summary>
/// <param name="buffer">The item-property-association destination.</param>
/// <param name="offset">The current destination offset, advanced past the association.</param>
/// <param name="propertyIndex">The one-based property index.</param>
/// <param name="largePropertyIndex">Whether the association uses a 15-bit property index.</param>
/// <param name="essential">Whether decoding the item requires understanding this property.</param>
private static void WritePropertyAssociation(
Span<byte> 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;
}
}
/// <summary>
/// Writes the encoded precision of each image channel.
/// </summary>
/// <param name="memory">The expanding metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="channelBitDepths">The encoded precision of each channel.</param>
/// <returns>The complete pixel-information-box length.</returns>
private static int WritePixelInformationPropertyBox(
AutoExpandingMemory<byte> memory,
int memoryOffset,
ReadOnlySpan<byte> channelBitDepths)
{
Span<byte> 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;
}
/// <summary>
/// Writes an AV1 codec-configuration property.
/// </summary>
/// <param name="memory">The expanding metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="configuration">The fixed image configuration.</param>
/// <returns>The complete AV1 codec-configuration-box length.</returns>
private static int WriteAv1CodecConfigurationPropertyBox(
AutoExpandingMemory<byte> memory,
int memoryOffset,
Av1CodecConfiguration configuration)
{
Span<byte> 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;
}
/// <summary>
/// Writes the registered type of an auxiliary image item.
/// </summary>
/// <param name="memory">The expanding metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="auxiliaryType">The null-terminated registered auxiliary type.</param>
/// <returns>The complete auxiliary-type-box length.</returns>
private static int WriteAuxiliaryTypePropertyBox(
AutoExpandingMemory<byte> memory,
int memoryOffset,
string auxiliaryType)
{
int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(auxiliaryType);
Span<byte> 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;
}
/// <summary>
/// Writes an H.273 color description for a color image item.
/// </summary>
/// <param name="memory">The expanding metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="profile">The color description to write.</param>
/// <returns>The complete color-information-box length.</returns>
private static int WriteColorInformationPropertyBox(
AutoExpandingMemory<byte> memory,
int memoryOffset,
CicpProfile profile)
{
Span<byte> 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;
}
/// <summary>
/// Writes an item's display width and height as an image-spatial-extents property.
/// </summary>

186
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<HeifItem> items = [colorItem, alphaItem];
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items);
ReadOnlySpan<byte> 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<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 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);
}
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items);
ReadOnlySpan<byte> 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()
{

Loading…
Cancel
Save