Browse Source

Parse HEIF metadata independent of order

pull/2633/head
James Jackson-South 1 week ago
parent
commit
d498e9f48c
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 68
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  3. 56
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

1
HEIF_IMPLEMENTATION_PLAN.md

@ -65,6 +65,7 @@ This snapshot pins or classifies the available references and failures; it does
| `HeifDecoderCore` box extension handling and `HeifDecoderCore`/`HeifEncoderCore` item-property associations | ISO/IEC 14496-12 box extensibility and section 8.11.14 item properties and `ipma` syntax | libavif `src/read.c` and `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Skip unrecognized top-level and metadata child boxes, preserve the position of every property in `ipco`, reject an unrecognized property only when its item association marks it essential, 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. |
| `HeifConstants.IsSupportedFileType`, `HeifImageFormatDetector`, and `HeifDecoderCore.CheckFileTypeBox` | ISO/IEC 14496-12 `FileTypeBox` syntax and the MP4 Registration Authority HEIF/AVIF still-image and sequence brand registrations | libavif `src/read.c` functions `avifParseFileTypeBox`, `avifFileTypeHasBrand`, and `avifFileTypeIsCompatible` at `092276ce89098ead06db80975173191e5fee1826` | Apply one rule to the major and compatible brands, accept the implemented still-image container and payload brands, and reject registered HEVC, AVIF, and JPEG sequence major brands until sequence decoding is implemented. The decoder validates the complete `ftyp` payload; the fixed-size format detector inspects the available prefix. |
| `HeifDecoderCore.ReadBoxHeader` and `HeifDecoderCore.ParseBoxHeader` | ISO/IEC 14496-12 section 4.2.2 basic box syntax | libavif `src/stream.c` functions `avifROStreamReadBoxHeaderPartial` and `avifROStreamReadBoxHeader` at `092276ce89098ead06db80975173191e5fee1826` | Resolve 32-bit, 64-bit, UUID, and top-level size-zero boxes into content lengths only after validating the complete variable-sized header and the remaining parent boundary. Nested size-zero boxes are invalid; large skips retain 64-bit offsets. |
| `HeifDecoderCore.ParseMetadata` | ISO/IEC 14496-12 `MetaBox` and HEIF item declarations, locations, properties, and associations | libavif `src/read.c` functions `avifParseMetaBox`, `avifMetaFindOrCreateItem`, `avifParseItemLocationBox`, and `avifParseItemPropertiesBox` at `092276ce89098ead06db80975173191e5fee1826` | Index unique recognized metadata children by type and payload location, then parse them in dependency order so physical placement does not control item lookup or property association. Duplicate unique children and truncated full-box headers are invalid. |
This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped.

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

@ -21,6 +21,16 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
{
private static readonly object UnknownProperty = new();
private static readonly Heif4CharCode[] MetadataParseOrder =
[
Heif4CharCode.Hdlr,
Heif4CharCode.Iinf,
Heif4CharCode.Pitm,
Heif4CharCode.Iref,
Heif4CharCode.Iloc,
Heif4CharCode.Iprp
];
/// <summary>
/// The general configuration.
/// </summary>
@ -295,45 +305,61 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
private void ParseMetadata(BufferedReadStream stream, long boxLength)
{
if (boxLength < 4)
{
throw new InvalidImageContentException("The metadata box is missing its version and flags.");
}
long endPosition = stream.Position + boxLength;
stream.Skip(4);
Dictionary<Heif4CharCode, (long Offset, long Length)> boxes = [];
while (stream.Position < endPosition)
{
long length = this.ReadBoxHeader(stream, endPosition, out Heif4CharCode boxType);
if (Array.IndexOf(MetadataParseOrder, boxType) >= 0)
{
// Association and location boxes can precede the item declarations they reference.
if (!boxes.TryAdd(boxType, (stream.Position, length)))
{
throw new InvalidImageContentException($"The metadata box contains duplicate '{PrettyPrint(boxType)}' boxes.");
}
}
SkipBox(stream, length);
}
foreach (Heif4CharCode boxType in MetadataParseOrder)
{
if (!boxes.TryGetValue(boxType, out (long Offset, long Length) box))
{
continue;
}
stream.Position = box.Offset;
switch (boxType)
{
case Heif4CharCode.Iprp:
this.ParseItemProperties(stream, length);
case Heif4CharCode.Hdlr:
this.ParseHandler(stream, box.Length);
break;
case Heif4CharCode.Iinf:
this.ParseItemInfo(stream, length);
break;
case Heif4CharCode.Iref:
this.ParseItemReference(stream, length);
this.ParseItemInfo(stream, box.Length);
break;
case Heif4CharCode.Pitm:
this.ParsePrimaryItem(stream, length);
this.ParsePrimaryItem(stream, box.Length);
break;
case Heif4CharCode.Hdlr:
this.ParseHandler(stream, length);
case Heif4CharCode.Iref:
this.ParseItemReference(stream, box.Length);
break;
case Heif4CharCode.Iloc:
this.ParseItemLocation(stream, length);
break;
case Heif4CharCode.Dinf:
case Heif4CharCode.Idat:
case Heif4CharCode.Grpl:
case Heif4CharCode.Ipro:
case Heif4CharCode.Uuid:
case Heif4CharCode.Ipmc:
// Silently skip these boxes.
SkipBox(stream, length);
this.ParseItemLocation(stream, box.Length);
break;
default:
SkipBox(stream, length);
case Heif4CharCode.Iprp:
this.ParseItemProperties(stream, box.Length);
break;
}
}
stream.Position = endPosition;
}
private void ParseHandler(BufferedReadStream stream, long boxLength)

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

@ -288,6 +288,52 @@ public class HeifDecoderTests
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
}
[Fact]
public void IdentifyAcceptsItemPropertiesBeforeItemInfo()
{
byte[] data = CreateEncodedContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset));
data = MoveBoxBefore(data, iprpOffset, iprpSize, iinfOffset);
ImageInfo imageInfo = Image.Identify(data);
Assert.Equal(new Size(2, 3), imageInfo.Size);
}
[Fact]
public void IdentifyAcceptsItemLocationBeforeItemInfo()
{
byte[] data = CreateEncodedContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
int ilocOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12);
int ilocSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ilocOffset));
data = MoveBoxBefore(data, ilocOffset, ilocSize, iinfOffset);
ImageInfo imageInfo = Image.Identify(data);
Assert.Equal(new Size(2, 3), imageInfo.Size);
}
[Fact]
public void IdentifyRejectsDuplicateUniqueMetadataBox()
{
byte[] data = CreateEncodedContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12);
int pitmSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(pitmOffset));
data = InsertBytes(data, metaOffset + metaSize, data.AsSpan(pitmOffset, pitmSize));
IncrementBoxSize(data, metaOffset, pitmSize);
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
}
private static byte[] CreateEncodedContainer()
{
using Image<Rgba32> image = new(2, 3);
@ -361,6 +407,16 @@ public class HeifDecoderTests
return result;
}
private static byte[] MoveBoxBefore(byte[] data, int boxOffset, int boxSize, int beforeOffset)
{
byte[] result = new byte[data.Length];
data.AsSpan(0, beforeOffset).CopyTo(result);
data.AsSpan(boxOffset, boxSize).CopyTo(result.AsSpan(beforeOffset));
data.AsSpan(beforeOffset, boxOffset - beforeOffset).CopyTo(result.AsSpan(beforeOffset + boxSize));
data.AsSpan(boxOffset + boxSize).CopyTo(result.AsSpan(boxOffset + boxSize));
return result;
}
private static void IncrementBoxSize(byte[] data, int offset, int increment)
{
uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset));

Loading…
Cancel
Save