diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 7d568ef83a..443eeb6891 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -62,6 +62,7 @@ This snapshot pins or classifies the available references and failures; it does | `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. | | `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. | | `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. | +| `HeifDecoderCore` and `HeifEncoderCore` item-property containers and associations | ISO/IEC 14496-12 section 8.11.14 item properties and `ipma` syntax | libavif `src/read.c` and `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Preserve the position of every property in `ipco`, associate properties by item ID, and read or write the essential bit plus one-based 7-bit or 15-bit property index according to the full-box flags. Independent HEIC, HIF, and AVIF fixtures provide the reader oracle; container-level identification of encoded output guards the writer independently of pixel roundtripping. | This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped. diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 8ef92ae031..642811bc1a 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -465,9 +465,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore this.metadata.IccProfile = new IccProfile(iccData); } + // Property indices refer to every box in ipco, including properties handled directly while parsing. + properties.Add(new KeyValuePair(Heif4CharCode.Colr, new object())); + break; case Heif4CharCode.Av1C: this.av1CodecConfiguration = new(boxBuffer); + properties.Add(new KeyValuePair(Heif4CharCode.Av1C, new object())); break; case Heif4CharCode.Altt: case Heif4CharCode.Imir: @@ -490,38 +494,72 @@ internal sealed class HeifDecoderCore : ImageDecoderCore using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); byte version = boxBuffer[0]; - byte flags = boxBuffer[3]; + bool largePropertyIndex = (boxBuffer[3] & 1) != 0; int bytesRead = 4; - int itemId = (int)ReadUInt16Or32(boxBuffer, version >= 1, ref bytesRead); - - int associationCount = boxBuffer[bytesRead++]; - for (int i = 0; i < associationCount; i++) + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[bytesRead..]); + bytesRead += 4; + for (uint entryIndex = 0; entryIndex < entryCount; entryIndex++) { - uint propId; - if (flags == 1) - { - propId = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]) & 0x4FFFU; - bytesRead += 2; - } - else + uint itemId = ReadUInt16Or32(boxBuffer, version >= 1, ref bytesRead); + HeifItem? item = this.FindItemById(itemId); + if (item is null) { - propId = boxBuffer[bytesRead++] & 0x4FU; + throw new InvalidImageContentException($"Item property association references unknown item ID {itemId}."); } - KeyValuePair prop = properties[(int)propId]; - switch (prop.Key) + int associationCount = boxBuffer[bytesRead++]; + for (int i = 0; i < associationCount; i++) { - case Heif4CharCode.Ispe: - this.items[itemId].SetExtent((Size)prop.Value); - break; - case Heif4CharCode.Pasp: - this.items[itemId].PixelAspectRatio = (Size)prop.Value; - break; - case Heif4CharCode.Pixi: - int[] values = (int[])prop.Value; - this.items[itemId].ChannelCount = values[0]; - this.items[itemId].BitsPerPixel = values[1]; - break; + uint association; + uint propertyIndexMask; + uint essentialMask; + if (largePropertyIndex) + { + association = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + bytesRead += 2; + propertyIndexMask = 0x7FFFU; + essentialMask = 0x8000U; + } + else + { + association = boxBuffer[bytesRead++]; + propertyIndexMask = 0x7FU; + essentialMask = 0x80U; + } + + uint propertyIndex = association & propertyIndexMask; + bool essential = (association & essentialMask) != 0; + if (propertyIndex == 0) + { + if (essential) + { + throw new InvalidImageContentException($"Item {itemId} associates essential property index 0."); + } + + continue; + } + + propertyIndex--; + if (propertyIndex >= properties.Count) + { + throw new InvalidImageContentException($"Item {itemId} references property index {propertyIndex + 1}, but only {properties.Count} properties exist."); + } + + KeyValuePair prop = properties[(int)propertyIndex]; + switch (prop.Key) + { + case Heif4CharCode.Ispe: + item.SetExtent((Size)prop.Value); + break; + case Heif4CharCode.Pasp: + item.PixelAspectRatio = (Size)prop.Value; + break; + case Heif4CharCode.Pixi: + int[] values = (int[])prop.Value; + item.ChannelCount = values[0]; + item.BitsPerPixel = values[1]; + break; + } } } } diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs index 8cee2f02c5..71726b245c 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs @@ -220,7 +220,6 @@ internal sealed class HeifEncoderCore private static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items) { - const ushort numPropPerItem = 1; Span buffer = memory.GetSpan(memoryOffset, 20); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp); @@ -233,22 +232,32 @@ internal sealed class HeifEncoderCore } BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset)); - buffer = memory.GetSpan(memoryOffset, bytesWritten + 16 + (5 * items.Count * numPropPerItem)); + bool largePropertyIndex = items.Count > 0x7F; + int propertyIndexSize = largePropertyIndex ? 2 : 1; + buffer = memory.GetSpan(memoryOffset, bytesWritten + 16 + ((3 + propertyIndexSize) * items.Count)); // Write 'ipma' box int ipmaLengthOffset = bytesWritten; - bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipma, 0, 0); - BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)(items.Count * numPropPerItem)); + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipma, 0, largePropertyIndex ? 1U : 0U); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)items.Count); bytesWritten += 4; - ushort propIndex = 0; + ushort propertyIndex = 1; foreach (HeifItem item in items) { BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); bytesWritten += 2; buffer[bytesWritten++] = 1; - BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propIndex); - bytesWritten += 2; - propIndex += numPropPerItem; + if (largePropertyIndex) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propertyIndex); + bytesWritten += 2; + } + else + { + buffer[bytesWritten++] = (byte)propertyIndex; + } + + propertyIndex++; } BinaryPrimitives.WriteUInt32BigEndian(buffer[ipmaLengthOffset..], (uint)(bytesWritten - ipmaLengthOffset)); diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 78ada561c5..1675801bc3 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -11,11 +11,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif; public class HeifDecoderTests { [Theory] - [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc)] - [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc)] - [InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg)] - [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1)] - public void Identify(string imagePath, HeifCompressionMethod compressionMethod) + [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, 3992, 2992)] + [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, 640, 428)] + [InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg, 7728, 5152)] + [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, 480, 640)] + public void Identify(string imagePath, HeifCompressionMethod compressionMethod, int width, int height) { TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); @@ -26,6 +26,8 @@ public class HeifDecoderTests Assert.NotNull(imageInfo); Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat); Assert.Equal(compressionMethod, heicMetadata.CompressionMethod); + Assert.Equal(width, imageInfo.Width); + Assert.Equal(height, imageInfo.Height); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs index 8826b2103f..61ceb6b14f 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs @@ -23,6 +23,10 @@ public class HeifEncoderTests image.Save(stream, encoder); stream.Position = 0; + ImageInfo imageInfo = Image.Identify(stream); + Assert.Equal(image.Size, imageInfo.Size); + + stream.Position = 0; using Image encodedImage = Image.Load(stream); HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata();