Browse Source

Handle HEIF container extensions

pull/2633/head
James Jackson-South 2 weeks ago
parent
commit
d401064cad
  1. 2
      HEIF_IMPLEMENTATION_PLAN.md
  2. 17
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  3. 127
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

2
HEIF_IMPLEMENTATION_PLAN.md

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

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

@ -19,6 +19,8 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// </summary>
internal sealed class HeifDecoderCore : ImageDecoderCore
{
private static readonly object UnknownProperty = new();
/// <summary>
/// The general configuration.
/// </summary>
@ -81,7 +83,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
stream.Skip((int)(stream.Length - stream.Position));
break;
default:
throw new ImageFormatException($"Unknown box type of '{PrettyPrint(boxType)}'");
SkipBox(stream, boxLength);
break;
}
}
@ -245,7 +248,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
SkipBox(stream, length);
break;
default:
throw new ImageFormatException($"Unknown metadata box type of '{PrettyPrint(boxType)}'");
SkipBox(stream, length);
break;
}
}
}
@ -484,7 +488,9 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
properties.Add(new KeyValuePair<Heif4CharCode, object>(itemType, new object()));
break;
default:
throw new ImageFormatException($"Unknown item type in property box of '{PrettyPrint(itemType)}'");
// Unknown properties still occupy an ipco index and become an error only when marked essential.
properties.Add(new KeyValuePair<Heif4CharCode, object>(itemType, UnknownProperty));
break;
}
}
}
@ -546,6 +552,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
}
KeyValuePair<Heif4CharCode, object> prop = properties[(int)propertyIndex];
if (essential && ReferenceEquals(prop.Value, UnknownProperty))
{
throw new InvalidImageContentException($"Item {itemId} associates unknown essential property '{PrettyPrint(prop.Key)}'.");
}
switch (prop.Key)
{
case Heif4CharCode.Ispe:

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.PixelFormats;
@ -10,6 +11,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif;
[ValidateDisposedMemoryAllocations]
public class HeifDecoderTests
{
private const uint UnknownBoxType = 0x74657374U;
[Theory]
[InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, 3992, 2992)]
[InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, 640, 428)]
@ -42,4 +45,128 @@ public class HeifDecoderTests
image.CompareToReferenceOutput(provider);
Assert.Equal(HeifCompressionMethod.LegacyJpeg, heicMetadata.CompressionMethod);
}
[Fact]
public void DecodeIgnoresUnknownTopLevelBox()
{
byte[] data = CreateEncodedContainer();
data = InsertBytes(data, data.Length, CreateUnknownBox());
using Image<Rgba32> image = Image.Load<Rgba32>(data);
Assert.Equal(new Size(2, 3), image.Size);
}
[Fact]
public void IdentifyIgnoresUnknownMetadataBox()
{
byte[] data = CreateEncodedContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox());
IncrementBoxSize(data, metaOffset, 8);
ImageInfo imageInfo = Image.Identify(data);
Assert.Equal(new Size(2, 3), imageInfo.Size);
}
[Fact]
public void IdentifyIgnoresUnknownNonEssentialProperty()
{
byte[] data = CreateContainerWithUnknownProperty(false);
ImageInfo imageInfo = Image.Identify(data);
Assert.Equal(new Size(2, 3), imageInfo.Size);
}
[Fact]
public void IdentifyRejectsUnknownEssentialProperty()
{
byte[] data = CreateContainerWithUnknownProperty(true);
InvalidImageContentException exception = Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
Assert.Contains("essential", exception.Message, StringComparison.OrdinalIgnoreCase);
}
private static byte[] CreateEncodedContainer()
{
using Image<Rgba32> image = new(2, 3);
using MemoryStream stream = new();
image.Save(stream, new HeifEncoder());
return stream.ToArray();
}
private static byte[] CreateContainerWithUnknownProperty(bool essential)
{
byte[] data = CreateEncodedContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset));
int ipcoOffset = FindBoxOffset(data, Heif4CharCode.Ipco, iprpOffset + 8, iprpSize - 8);
int ipcoSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipcoOffset));
int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8);
// Insert the property before ipma so its one-based index is 2 and all parent box sizes remain explicit.
data = InsertBytes(data, ipcoOffset + ipcoSize, CreateUnknownBox());
IncrementBoxSize(data, metaOffset, 8);
IncrementBoxSize(data, iprpOffset, 8);
IncrementBoxSize(data, ipcoOffset, 8);
ipmaOffset += 8;
// The generated container has one item with one property association; append the unknown property to that entry.
int associationCountOffset = ipmaOffset + 18;
data[associationCountOffset]++;
int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset));
byte association = (byte)(2 | (essential ? 0x80 : 0));
data = InsertBytes(data, associationOffset, new byte[] { association });
IncrementBoxSize(data, metaOffset, 1);
IncrementBoxSize(data, iprpOffset, 1);
IncrementBoxSize(data, ipmaOffset, 1);
return data;
}
private static byte[] CreateUnknownBox()
{
byte[] box = new byte[8];
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
return box;
}
private static int FindBoxOffset(ReadOnlySpan<byte> data, Heif4CharCode type, int offset, int length)
{
int endOffset = offset + length;
while (offset < endOffset)
{
int boxSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(data[(offset + 4)..]);
if (boxType == type)
{
return offset;
}
offset += boxSize;
}
return -1;
}
private static byte[] InsertBytes(byte[] data, int offset, ReadOnlySpan<byte> inserted)
{
byte[] result = new byte[data.Length + inserted.Length];
data.AsSpan(0, offset).CopyTo(result);
inserted.CopyTo(result.AsSpan(offset));
data.AsSpan(offset).CopyTo(result.AsSpan(offset + inserted.Length));
return result;
}
private static void IncrementBoxSize(byte[] data, int offset, int increment)
{
uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset));
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), size + (uint)increment);
}
}

Loading…
Cancel
Save