diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 11327e4d5..15de2638a 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/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.
diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
index f259b2492..475cc86e7 100644
--- a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
+++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs
@@ -143,6 +143,26 @@ public enum Heif4CharCode : uint
///
Heix = 0x68656978U,
+ ///
+ /// HEVC image sequence brand.
+ ///
+ Hevc = 0x68657663U,
+
+ ///
+ /// HEVC Main 10 image sequence brand.
+ ///
+ Hevx = 0x68657678U,
+
+ ///
+ /// Layered HEVC image sequence brand.
+ ///
+ Hevm = 0x6865766DU,
+
+ ///
+ /// Layered HEVC image sequence brand.
+ ///
+ Hevs = 0x68657673U,
+
///
/// High Efficient File brand.
///
@@ -153,6 +173,11 @@ public enum Heif4CharCode : uint
///
Avif = 0x61766966U,
+ ///
+ /// AVIF image sequence brand.
+ ///
+ Avis = 0x61766973U,
+
///
/// High Efficiency Coding tile.
///
@@ -163,6 +188,11 @@ public enum Heif4CharCode : uint
///
Jpeg = 0x6A706567U,
+ ///
+ /// JPEG image sequence brand.
+ ///
+ Jpgs = 0x6A706773U,
+
///
/// AOMedia Video Coding tile.
///
diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
index c537e4dbb..1006e9d26 100644
--- a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt
+++ b/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",
diff --git a/src/ImageSharp/Formats/Heif/HeifConstants.cs b/src/ImageSharp/Formats/Heif/HeifConstants.cs
index f59fd5641..21cca4f68 100644
--- a/src/ImageSharp/Formats/Heif/HeifConstants.cs
+++ b/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;
///
@@ -19,4 +21,50 @@ internal static class HeifConstants
/// The list of file extensions that equate to a HEIC.
///
public static readonly IEnumerable FileExtensions = new[] { "heic", "heif", "hif", "avif" };
+
+ public static bool IsSupportedFileType(ReadOnlySpan 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;
}
diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
index 7f881520a..b60e07901 100644
--- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
@@ -106,7 +106,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
///
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 boxMemory = this.ReadIntoBuffer(stream, boxLength);
Span 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)
diff --git a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs
index b53aacc87..513767740 100644
--- a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs
+++ b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif;
public sealed class HeifImageFormatDetector : IImageFormatDetector
{
///
- public int HeaderSize => 12;
+ public int HeaderSize => 32;
///
public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format)
@@ -23,8 +23,19 @@ public sealed class HeifImageFormatDetector : IImageFormatDetector
private static bool IsSupportedFileFormat(ReadOnlySpan 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));
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
index 0aca10f07..7366fde31 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
+++ b/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(() => HeifDecoder.Instance.Identify(DecoderOptions.Default, stream));
+ }
+
private static byte[] CreateEncodedContainer()
{
using Image image = new(2, 3);