Browse Source

Correct HEIF file type brand handling

pull/2633/head
James Jackson-South 1 week ago
parent
commit
7be9b15205
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 30
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  3. 6
      src/ImageSharp/Formats/Heif/Heif4CharCode.tt
  4. 48
      src/ImageSharp/Formats/Heif/HeifConstants.cs
  5. 22
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  6. 19
      src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs
  7. 58
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

1
HEIF_IMPLEMENTATION_PLAN.md

@ -63,6 +63,7 @@ This snapshot pins or classifies the available references and failures; it does
| `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` 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. |
This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped.

30
src/ImageSharp/Formats/Heif/Heif4CharCode.cs

@ -143,6 +143,26 @@ public enum Heif4CharCode : uint
/// </summary>
Heix = 0x68656978U,
/// <summary>
/// HEVC image sequence brand.
/// </summary>
Hevc = 0x68657663U,
/// <summary>
/// HEVC Main 10 image sequence brand.
/// </summary>
Hevx = 0x68657678U,
/// <summary>
/// Layered HEVC image sequence brand.
/// </summary>
Hevm = 0x6865766DU,
/// <summary>
/// Layered HEVC image sequence brand.
/// </summary>
Hevs = 0x68657673U,
/// <summary>
/// High Efficient File brand.
/// </summary>
@ -153,6 +173,11 @@ public enum Heif4CharCode : uint
/// </summary>
Avif = 0x61766966U,
/// <summary>
/// AVIF image sequence brand.
/// </summary>
Avis = 0x61766973U,
/// <summary>
/// High Efficiency Coding tile.
/// </summary>
@ -163,6 +188,11 @@ public enum Heif4CharCode : uint
/// </summary>
Jpeg = 0x6A706567U,
/// <summary>
/// JPEG image sequence brand.
/// </summary>
Jpgs = 0x6A706773U,
/// <summary>
/// AOMedia Video Coding tile.
/// </summary>

6
src/ImageSharp/Formats/Heif/Heif4CharCode.tt

@ -33,10 +33,16 @@
"ipma", "Item Property Association",
"heic", "High Efficient Image Coding brand",
"heix", "High Efficient Image Coding brand (legacy name)",
"hevc", "HEVC image sequence brand",
"hevx", "HEVC Main 10 image sequence brand",
"hevm", "Layered HEVC image sequence brand",
"hevs", "Layered HEVC image sequence brand",
"mif1", "High Efficient File brand",
"avif", "AVIF brand",
"avis", "AVIF image sequence brand",
"hvc1", "High Efficiency Coding tile",
"jpeg", "Legacy JPEG coded tile",
"jpgs", "JPEG image sequence brand",
"av01", "AOMedia Video Coding tile",
"dinf", "Data Information",
"grpl", "Group list",

48
src/ImageSharp/Formats/Heif/HeifConstants.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
@ -19,4 +21,50 @@ internal static class HeifConstants
/// The list of file extensions that equate to a HEIC.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "heic", "heif", "hif", "avif" };
public static bool IsSupportedFileType(ReadOnlySpan<byte> boxContent)
{
if (boxContent.Length < 8 || (boxContent.Length & 3) != 0)
{
return false;
}
Heif4CharCode majorBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent);
if (IsSequenceBrand(majorBrand))
{
return false;
}
if (IsSupportedStillImageBrand(majorBrand))
{
return true;
}
// The minor-version field follows the major brand; compatible brands start at byte eight.
for (int offset = 8; offset < boxContent.Length; offset += 4)
{
Heif4CharCode compatibleBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent[offset..]);
if (IsSupportedStillImageBrand(compatibleBrand))
{
return true;
}
}
return false;
}
private static bool IsSupportedStillImageBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Heic
or Heif4CharCode.Heix
or Heif4CharCode.Mif1
or Heif4CharCode.Avif
or Heif4CharCode.Jpeg;
private static bool IsSequenceBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Hevc
or Heif4CharCode.Hevx
or Heif4CharCode.Hevm
or Heif4CharCode.Hevs
or Heif4CharCode.Avis
or Heif4CharCode.Jpgs;
}

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

@ -106,7 +106,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <inheritdoc/>
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.CheckFileTypeBox(stream);
if (!this.CheckFileTypeBox(stream))
{
throw new ImageFormatException("Not an HEIF image.");
}
while (stream.Position < stream.Length)
{
@ -138,13 +141,20 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
private bool CheckFileTypeBox(BufferedReadStream stream)
{
long boxLength = this.ReadBoxHeader(stream, out Heif4CharCode boxType);
if (boxType != Heif4CharCode.Ftyp)
{
return false;
}
EnsureBoxBoundary(boxLength, stream);
if (boxLength < 8 || boxLength > int.MaxValue || (boxLength & 3) != 0)
{
return false;
}
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan();
uint majorBrand = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer);
bool correctBrand = majorBrand is (uint)Heif4CharCode.Heic or (uint)Heif4CharCode.Heix or (uint)Heif4CharCode.Avif;
// TODO: Interpret minorVersion and compatible brands.
return boxType == Heif4CharCode.Ftyp && correctBrand;
return HeifConstants.IsSupportedFileType(boxBuffer);
}
private void UpdateMetadata(ImageMetadata metadata, HeifItem item)

19
src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif;
public sealed class HeifImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 12;
public int HeaderSize => 32;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
@ -23,8 +23,19 @@ public sealed class HeifImageFormatDetector : IImageFormatDetector
private static bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
{
bool hasFtyp = BinaryPrimitives.ReadUInt32BigEndian(header.Slice(4)) == (uint)Heif4CharCode.Ftyp;
uint brand = BinaryPrimitives.ReadUInt32BigEndian(header.Slice(8));
return hasFtyp && (brand == (uint)Heif4CharCode.Heic || brand == (uint)Heif4CharCode.Heix || brand == (uint)Heif4CharCode.Avif);
if (header.Length < 16 || BinaryPrimitives.ReadUInt32BigEndian(header[4..]) != (uint)Heif4CharCode.Ftyp)
{
return false;
}
uint boxSize = BinaryPrimitives.ReadUInt32BigEndian(header);
if (boxSize < 16 || ((boxSize - 16) & 3) != 0)
{
return false;
}
int availableContentLength = (int)Math.Min(boxSize - 8, (uint)header.Length - 8);
availableContentLength &= ~3;
return HeifConstants.IsSupportedFileType(header.Slice(8, availableContentLength));
}
}

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.PixelFormats;
@ -91,6 +92,63 @@ public class HeifDecoderTests
Assert.Contains("essential", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData(Heif4CharCode.Heic)]
[InlineData(Heif4CharCode.Heix)]
[InlineData(Heif4CharCode.Mif1)]
[InlineData(Heif4CharCode.Avif)]
[InlineData(Heif4CharCode.Jpeg)]
public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand)
{
byte[] data = CreateEncodedContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new();
bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format);
Assert.True(detected);
Assert.Same(HeifFormat.Instance, format);
}
[Fact]
public void IdentifyAcceptsSupportedCompatibleBrand()
{
byte[] data = CreateEncodedContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
ImageInfo imageInfo = Image.Identify(data);
Assert.Equal(new Size(2, 3), imageInfo.Size);
}
[Theory]
[InlineData(Heif4CharCode.Hevc)]
[InlineData(Heif4CharCode.Hevx)]
[InlineData(Heif4CharCode.Hevm)]
[InlineData(Heif4CharCode.Hevs)]
[InlineData(Heif4CharCode.Avis)]
[InlineData(Heif4CharCode.Jpgs)]
public void DetectorRejectsSequenceMajorBrand(Heif4CharCode brand)
{
byte[] data = CreateEncodedContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new();
Assert.False(detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out _));
}
[Fact]
public void IdentifyRejectsUnsupportedBrands()
{
byte[] data = CreateEncodedContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType);
using MemoryStream stream = new(data, false);
Assert.Throws<ImageFormatException>(() => HeifDecoder.Instance.Identify(DecoderOptions.Default, stream));
}
private static byte[] CreateEncodedContainer()
{
using Image<Rgba32> image = new(2, 3);

Loading…
Cancel
Save