Browse Source

Correct HEIF item property associations

pull/2633/head
James Jackson-South 2 months ago
parent
commit
eaa93a35f7
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 90
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  3. 25
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  4. 12
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  5. 4
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

1
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. | | `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. | | `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. | | `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. This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped.

90
src/ImageSharp/Formats/Heif/HeifDecoderCore.cs

@ -465,9 +465,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
this.metadata.IccProfile = new IccProfile(iccData); 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, object>(Heif4CharCode.Colr, new object()));
break; break;
case Heif4CharCode.Av1C: case Heif4CharCode.Av1C:
this.av1CodecConfiguration = new(boxBuffer); this.av1CodecConfiguration = new(boxBuffer);
properties.Add(new KeyValuePair<Heif4CharCode, object>(Heif4CharCode.Av1C, new object()));
break; break;
case Heif4CharCode.Altt: case Heif4CharCode.Altt:
case Heif4CharCode.Imir: case Heif4CharCode.Imir:
@ -490,38 +494,72 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
byte version = boxBuffer[0]; byte version = boxBuffer[0];
byte flags = boxBuffer[3]; bool largePropertyIndex = (boxBuffer[3] & 1) != 0;
int bytesRead = 4; int bytesRead = 4;
int itemId = (int)ReadUInt16Or32(boxBuffer, version >= 1, ref bytesRead); uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[bytesRead..]);
bytesRead += 4;
int associationCount = boxBuffer[bytesRead++]; for (uint entryIndex = 0; entryIndex < entryCount; entryIndex++)
for (int i = 0; i < associationCount; i++)
{ {
uint propId; uint itemId = ReadUInt16Or32(boxBuffer, version >= 1, ref bytesRead);
if (flags == 1) HeifItem? item = this.FindItemById(itemId);
{ if (item is null)
propId = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]) & 0x4FFFU;
bytesRead += 2;
}
else
{ {
propId = boxBuffer[bytesRead++] & 0x4FU; throw new InvalidImageContentException($"Item property association references unknown item ID {itemId}.");
} }
KeyValuePair<Heif4CharCode, object> prop = properties[(int)propId]; int associationCount = boxBuffer[bytesRead++];
switch (prop.Key) for (int i = 0; i < associationCount; i++)
{ {
case Heif4CharCode.Ispe: uint association;
this.items[itemId].SetExtent((Size)prop.Value); uint propertyIndexMask;
break; uint essentialMask;
case Heif4CharCode.Pasp: if (largePropertyIndex)
this.items[itemId].PixelAspectRatio = (Size)prop.Value; {
break; association = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]);
case Heif4CharCode.Pixi: bytesRead += 2;
int[] values = (int[])prop.Value; propertyIndexMask = 0x7FFFU;
this.items[itemId].ChannelCount = values[0]; essentialMask = 0x8000U;
this.items[itemId].BitsPerPixel = values[1]; }
break; 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<Heif4CharCode, object> 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;
}
} }
} }
} }

25
src/ImageSharp/Formats/Heif/HeifEncoderCore.cs

@ -220,7 +220,6 @@ internal sealed class HeifEncoderCore
private static int WriteItemPropertiesBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items) private static int WriteItemPropertiesBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items)
{ {
const ushort numPropPerItem = 1;
Span<byte> buffer = memory.GetSpan(memoryOffset, 20); Span<byte> buffer = memory.GetSpan(memoryOffset, 20);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp);
@ -233,22 +232,32 @@ internal sealed class HeifEncoderCore
} }
BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset)); 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 // Write 'ipma' box
int ipmaLengthOffset = bytesWritten; int ipmaLengthOffset = bytesWritten;
bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipma, 0, 0); bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipma, 0, largePropertyIndex ? 1U : 0U);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)(items.Count * numPropPerItem)); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)items.Count);
bytesWritten += 4; bytesWritten += 4;
ushort propIndex = 0; ushort propertyIndex = 1;
foreach (HeifItem item in items) foreach (HeifItem item in items)
{ {
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id);
bytesWritten += 2; bytesWritten += 2;
buffer[bytesWritten++] = 1; buffer[bytesWritten++] = 1;
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propIndex); if (largePropertyIndex)
bytesWritten += 2; {
propIndex += numPropPerItem; BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propertyIndex);
bytesWritten += 2;
}
else
{
buffer[bytesWritten++] = (byte)propertyIndex;
}
propertyIndex++;
} }
BinaryPrimitives.WriteUInt32BigEndian(buffer[ipmaLengthOffset..], (uint)(bytesWritten - ipmaLengthOffset)); BinaryPrimitives.WriteUInt32BigEndian(buffer[ipmaLengthOffset..], (uint)(bytesWritten - ipmaLengthOffset));

12
tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

@ -11,11 +11,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif;
public class HeifDecoderTests public class HeifDecoderTests
{ {
[Theory] [Theory]
[InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc)] [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, 3992, 2992)]
[InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc)] [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, 640, 428)]
[InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg)] [InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg, 7728, 5152)]
[InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1)] [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, 480, 640)]
public void Identify(string imagePath, HeifCompressionMethod compressionMethod) public void Identify(string imagePath, HeifCompressionMethod compressionMethod, int width, int height)
{ {
TestFile testFile = TestFile.Create(imagePath); TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false); using MemoryStream stream = new(testFile.Bytes, false);
@ -26,6 +26,8 @@ public class HeifDecoderTests
Assert.NotNull(imageInfo); Assert.NotNull(imageInfo);
Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat); Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat);
Assert.Equal(compressionMethod, heicMetadata.CompressionMethod); Assert.Equal(compressionMethod, heicMetadata.CompressionMethod);
Assert.Equal(width, imageInfo.Width);
Assert.Equal(height, imageInfo.Height);
} }
[Theory] [Theory]

4
tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

@ -23,6 +23,10 @@ public class HeifEncoderTests
image.Save(stream, encoder); image.Save(stream, encoder);
stream.Position = 0; 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); using Image<TPixel> encodedImage = Image.Load<TPixel>(stream);
HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata(); HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata();

Loading…
Cancel
Save