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() {