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);