From 1b4ec92730568d4d4d23a2a5b9d73a822913dc81 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 24 Aug 2026 21:38:42 +1000 Subject: [PATCH] Harden and document HEIF still-image container --- .../Formats/Heif/Av1HeifItemDecoder.cs | 13 +- .../Formats/Heif/GridHeifItemDecoder.cs | 45 +- .../Formats/Heif/HeifCompressionFactory.cs | 9 +- .../Formats/Heif/HeifCompressionMethod.cs | 16 +- src/ImageSharp/Formats/Heif/HeifConstants.cs | 26 + src/ImageSharp/Formats/Heif/HeifDecoder.cs | 3 + .../Formats/Heif/HeifDecoderCore.cs | 754 ++++++++++++++---- .../Formats/Heif/HeifEncoderCore.cs | 194 ++++- src/ImageSharp/Formats/Heif/HeifFormat.cs | 3 + .../Formats/Heif/HeifImageFormatDetector.cs | 11 +- src/ImageSharp/Formats/Heif/HeifItem.cs | 8 +- src/ImageSharp/Formats/Heif/HeifItemLink.cs | 2 + src/ImageSharp/Formats/Heif/HeifLocation.cs | 21 +- .../Formats/Heif/HeifLocationComparer.cs | 30 +- .../Formats/Heif/HeifLocationOffsetOrigin.cs | 14 + .../Formats/Heif/IHeifItemDecoder.cs | 16 +- .../Formats/Heif/JpegHeifItemDecoder.cs | 13 +- 17 files changed, 942 insertions(+), 236 deletions(-) diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index 3b96a3b46c..f5caf15ce9 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -7,24 +7,29 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Decoder for a single into a AVIF image. +/// Decodes a single AV1-coded HEIF image item. /// +/// The destination pixel type. internal class Av1HeifItemDecoder : IHeifItemDecoder where TPixel : unmanaged, IPixel { /// - /// Gets the item type this decoder decodes, which is . + /// Gets the AV1-coded image item type. /// public Heif4CharCode Type => Heif4CharCode.Av01; /// - /// Gets the compression method this doceder uses, which is . + /// Gets the AV1 compression method. /// public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.Av1; /// - /// Decode the specified item as AVIF. + /// Decodes the encoded AV1 payload of an image item. /// + /// The configuration that supplies memory allocation and codec services. + /// The HEIF item whose encoded payload is being decoded. + /// The encoded AV1 payload. + /// The decoded image. public Image DecodeItemData(Configuration configuration, HeifItem item, Span data) { Av1Decoder decoder = new(configuration); diff --git a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs index 4a447e7636..ca82f53a81 100644 --- a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs @@ -9,16 +9,39 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Decoder for a grid of several into a single image. +/// Decodes the image items referenced by a HEIF grid derived-image item. /// +/// The destination pixel type. internal class GridHeifItemDecoder : IHeifItemDecoder where TPixel : unmanaged, IPixel { + /// + /// The configuration used to decode each compressed grid tile. + /// private readonly Configuration configuration; + + /// + /// The item definitions available to the grid. + /// private readonly IList items; + + /// + /// The item-reference relationships used to locate the grid's tiles. + /// private readonly IList itemLinks; + + /// + /// The assembled encoded payload for each referenced image item. + /// private readonly IDictionary> buffers; + /// + /// Initializes a new instance of the class. + /// + /// The configuration used to decode compressed grid tiles. + /// The item definitions in the containing HEIF file. + /// The item-reference relationships in the containing HEIF file. + /// The assembled encoded payload for each image item. public GridHeifItemDecoder(Configuration configuration, IList items, IList itemLinks, IDictionary> buffers) { this.configuration = configuration; @@ -28,29 +51,37 @@ internal class GridHeifItemDecoder : IHeifItemDecoder } /// - /// Gets the item type this decoder decodes, which is . + /// Gets the grid derived-image item type. /// public Heif4CharCode Type => Heif4CharCode.Grid; /// - /// Gets the compression method this doceder uses. + /// Gets the compression method used by the decoded grid tiles. /// public HeifCompressionMethod CompressionMethod { get; private set; } /// - /// Decode the specified item as single image. + /// Decodes the tiles referenced by a grid derived-image item. /// + /// The configuration associated with the containing HEIF decode. + /// The grid derived-image item. + /// The grid descriptor payload. + /// The image reconstructed from the referenced grid tiles. public Image DecodeItemData(Configuration configuration, HeifItem gridItem, Span data) { - List linked = this.itemLinks.First(l => l.SourceId == gridItem.Id).DestinationIds; + List linked = this.itemLinks.First( + link => link.Type == Heif4CharCode.Dimg && link.SourceId == gridItem.Id).DestinationIds; + + // Each compressed tile decoder returns an owned Image. Keep every tile alive until + // the final grid has copied its pixels, then dispose all intermediates together. using DisposableList> gridTiles = new(linked.Count); foreach (uint id in linked) { HeifItem? item = this.items.FirstOrDefault(item => item.Id == id); - if (item != null) + if (item is not null) { IHeifItemDecoder? decoder = HeifCompressionFactory.GetDecoder(item.Type); - if (decoder != null) + if (decoder is not null) { this.CompressionMethod = decoder.CompressionMethod; IMemoryOwner itemMemory = this.buffers[item.Id]; diff --git a/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs b/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs index cb874d5bc3..df00957036 100644 --- a/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs +++ b/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs @@ -6,13 +6,16 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Factory for item decoders inside the HEIF container format. +/// Selects the still-image decoder for a compressed HEIF image item. /// -internal class HeifCompressionFactory +internal static class HeifCompressionFactory { /// - /// Get a decoder implementation. + /// Gets a decoder for the specified compressed image item type. /// + /// The destination pixel type. + /// The image item type. + /// A matching item decoder, or when the item type is not supported. public static IHeifItemDecoder? GetDecoder(Heif4CharCode type) where TPixel : unmanaged, IPixel => type switch { diff --git a/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs b/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs index 18c438f057..6789f94c99 100644 --- a/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs +++ b/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs @@ -4,42 +4,42 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Compression algorithms possible inside an HEIF (High Efficiency Image Format) based file. +/// Identifies the compression method used by a coded image item in a HEIF file. /// public enum HeifCompressionMethod { /// - /// High Efficiency Video Coding + /// High Efficiency Video Coding (HEVC). /// Hevc, /// - /// Legact JPEG + /// Legacy JPEG coding. /// LegacyJpeg, /// - /// JPEG 2000 + /// JPEG 2000 coding. /// Jpeg2000, /// - /// JPEG-XR + /// JPEG XR coding. /// JpegXR, /// - /// JPEG-XS + /// JPEG XS coding. /// JpegXS, /// - /// AOMedia's Video 1 coding + /// AOMedia Video 1 (AV1) coding. /// Av1, /// - /// Advanced Video Coding + /// Advanced Video Coding (AVC). /// Avc, } diff --git a/src/ImageSharp/Formats/Heif/HeifConstants.cs b/src/ImageSharp/Formats/Heif/HeifConstants.cs index 21cca4f68e..9052b007c0 100644 --- a/src/ImageSharp/Formats/Heif/HeifConstants.cs +++ b/src/ImageSharp/Formats/Heif/HeifConstants.cs @@ -10,6 +10,9 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// internal static class HeifConstants { + /// + /// The HEIC still-image brand written by the encoder. + /// public const Heif4CharCode HeicBrand = Heif4CharCode.Heic; /// @@ -22,8 +25,20 @@ internal static class HeifConstants /// public static readonly IEnumerable FileExtensions = new[] { "heic", "heif", "hif", "avif" }; + /// + /// Determines whether a file-type box describes a supported still-image container. + /// + /// + /// The file-type box payload, beginning with the major brand and minor version and followed by compatible brands. + /// + /// + /// when the major brand is a supported still-image brand, or when an otherwise unknown + /// major brand declares a supported compatible still-image brand; otherwise, . + /// public static bool IsSupportedFileType(ReadOnlySpan boxContent) { + // Every brand is a four-character code. The payload must contain the major brand and minor version before + // any compatible brands, otherwise accepting a partial trailing code could produce a false detection. if (boxContent.Length < 8 || (boxContent.Length & 3) != 0) { return false; @@ -32,6 +47,7 @@ internal static class HeifConstants Heif4CharCode majorBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent); if (IsSequenceBrand(majorBrand)) { + // Sequence major brands describe timed image sequences, which the still-image decoder cannot expose. return false; } @@ -53,6 +69,11 @@ internal static class HeifConstants return false; } + /// + /// Determines whether identifies a still-image container supported by this codec. + /// + /// The registered file-type brand. + /// when the brand identifies a supported still-image container. private static bool IsSupportedStillImageBrand(Heif4CharCode brand) => brand is Heif4CharCode.Heic or Heif4CharCode.Heix @@ -60,6 +81,11 @@ internal static class HeifConstants or Heif4CharCode.Avif or Heif4CharCode.Jpeg; + /// + /// Determines whether identifies a timed image sequence. + /// + /// The registered file-type brand. + /// when the brand identifies a timed image sequence. private static bool IsSequenceBrand(Heif4CharCode brand) => brand is Heif4CharCode.Hevc or Heif4CharCode.Hevx diff --git a/src/ImageSharp/Formats/Heif/HeifDecoder.cs b/src/ImageSharp/Formats/Heif/HeifDecoder.cs index c1d79b1096..9cac84fe75 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoder.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoder.cs @@ -10,6 +10,9 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// public sealed class HeifDecoder : ImageDecoder { + /// + /// Initializes a new instance of the class. + /// private HeifDecoder() { } diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 2a2c77abae..b3d94332b7 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -19,8 +19,14 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// internal sealed class HeifDecoderCore : ImageDecoderCore { + /// + /// Marks an item property whose box type is not understood by this decoder. + /// private static readonly object UnknownProperty = new(); + /// + /// Defines the dependency order in which recognized metadata children are interpreted. + /// private static readonly Heif4CharCode[] MetadataParseOrder = [ Heif4CharCode.Hdlr, @@ -28,7 +34,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore Heif4CharCode.Pitm, Heif4CharCode.Iref, Heif4CharCode.Iloc, - Heif4CharCode.Iprp + Heif4CharCode.Iprp, + Heif4CharCode.Idat ]; /// @@ -41,14 +48,36 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// private readonly ImageMetadata metadata; + /// + /// The item identifier selected by the primary-item box. + /// private uint primaryItem; + /// + /// The item declarations parsed from the item-information box. + /// private readonly List items; + /// + /// The typed relationships parsed from the item-reference box. + /// private readonly List itemLinks; + /// + /// The codec configuration associated with the current AV1 item. + /// private Av1CodecConfiguration av1CodecConfiguration; + /// + /// The absolute stream offset of the item-data box payload, or -1 when no item-data box exists. + /// + private long itemDataOffset = -1; + + /// + /// The number of bytes in the item-data box payload. + /// + private long itemDataLength; + /// /// Initializes a new instance of the class. /// @@ -72,7 +101,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore this.items.Clear(); this.itemLinks.Clear(); - Image? image = null; + this.itemDataOffset = -1; + this.itemDataLength = 0; + + // Item locations are absolute file offsets or idat-relative offsets, so payload bytes need not be adjacent to + // the metadata box. Complete the top-level scan before resolving and decoding the primary item. while (stream.Position < stream.Length) { long boxLength = this.ReadBoxHeader(stream, stream.Length, out Heif4CharCode boxType, true); @@ -82,8 +115,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore this.ParseMetadata(stream, boxLength); break; case Heif4CharCode.Mdat: - image = this.ParseMediaData(stream, boxLength); - break; case Heif4CharCode.Free: SkipBox(stream, boxLength); break; @@ -97,19 +128,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } - HeifItem? item = this.FindItemById(this.primaryItem); - if (item == null) - { - throw new ImageFormatException("No primary item found"); - } - - if (image == null) - { - throw new NotImplementedException("No JPEG image decoded"); - } - - this.UpdateMetadata(image.Metadata, item); - return image; + return this.DecodePrimaryItem(stream); } /// @@ -120,6 +139,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore throw new ImageFormatException("Not an HEIF image."); } + this.items.Clear(); + this.itemLinks.Clear(); + this.itemDataOffset = -1; + this.itemDataLength = 0; + + // Identification reads only the container model. Payload boxes remain skipped because dimensions and format + // metadata come from item declarations and associated properties rather than reconstructed pixels. while (stream.Position < stream.Length) { long boxLength = this.ReadBoxHeader(stream, stream.Length, out Heif4CharCode boxType, true); @@ -136,7 +162,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } HeifItem? item = this.FindItemById(this.primaryItem); - if (item == null) + if (item is null) { throw new ImageFormatException("No primary item found"); } @@ -146,6 +172,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return new ImageInfo(new(item.Extent.Width, item.Extent.Height), this.metadata); } + /// + /// Reads and validates the leading file-type box against the still-image brands supported by this decoder. + /// + /// The container stream positioned at its first top-level box. + /// when the complete file-type payload advertises a supported still-image brand. private bool CheckFileTypeBox(BufferedReadStream stream) { long boxLength = this.ReadBoxHeader(stream, stream.Length, out Heif4CharCode boxType, true); @@ -164,15 +195,57 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return HeifConstants.IsSupportedFileType(boxBuffer); } + /// + /// Updates identification metadata from the primary item or its decodable thumbnail fallback. + /// + /// The destination image metadata. + /// The primary item whose visible representation is being identified. private void UpdateMetadata(ImageMetadata metadata, HeifItem item) { + HeifItem metadataItem = item; + if (item.Type == Heif4CharCode.Grid) + { + // A grid is a derived image rather than a compression method. Its dimg references identify the coded + // tile items whose decoder determines the compression reported for the primary presentation. + HeifItemLink? derivedImageReference = this.itemLinks.FirstOrDefault( + link => link.Type == Heif4CharCode.Dimg && link.SourceId == item.Id); + + if (derivedImageReference is not null) + { + HeifItem? tileItem = derivedImageReference.DestinationIds + .Select(this.FindItemById) + .FirstOrDefault(candidate => candidate is not null && HeifCompressionFactory.GetDecoder(candidate.Type) is not null); + + if (tileItem is not null) + { + metadataItem = tileItem; + } + } + } + else if (HeifCompressionFactory.GetDecoder(item.Type) is null) + { + // A thumbnail reference points from the thumbnail item to the master image. Restrict fallback metadata + // to a thumbnail of this primary item rather than allowing an unrelated thumbnail to relabel it. + HeifItemLink? thumbnailReference = this.itemLinks.FirstOrDefault( + link => link.Type == Heif4CharCode.Thmb && link.DestinationIds.Contains(item.Id)); + + if (thumbnailReference is not null) + { + HeifItem? thumbnailItem = this.FindItemById(thumbnailReference.SourceId); + if (thumbnailItem is not null && HeifCompressionFactory.GetDecoder(thumbnailItem.Type) is not null) + { + metadataItem = thumbnailItem; + } + } + } + HeifMetadata meta = metadata.GetHeifMetadata(); HeifCompressionMethod compressionMethod = HeifCompressionMethod.Hevc; - if (item.Type == Heif4CharCode.Av01) + if (metadataItem.Type == Heif4CharCode.Av01) { compressionMethod = HeifCompressionMethod.Av1; } - else if (item.Type == Heif4CharCode.Jpeg || this.itemLinks.Any(link => link.Type == Heif4CharCode.Thmb)) + else if (metadataItem.Type == Heif4CharCode.Jpeg) { compressionMethod = HeifCompressionMethod.LegacyJpeg; } @@ -180,6 +253,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore meta.CompressionMethod = compressionMethod; } + /// + /// Reads an ISO BMFF box header and resolves its validated payload length. + /// + /// The stream positioned at the box size field. + /// The absolute end position of the containing box or file. + /// Receives the box four-character code. + /// Indicates whether a size-zero box may extend to the end of the file. + /// The number of payload bytes following the complete variable-length header. private long ReadBoxHeader(BufferedReadStream stream, long parentEndPosition, out Heif4CharCode boxType, bool topLevel = false) { if (parentEndPosition - stream.Position < 8) @@ -200,6 +281,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore if (boxSize == 1) { + // A 32-bit size value of one replaces the size field with the following unsigned 64-bit largesize value. if (parentEndPosition - stream.Position < 8) { throw new InvalidImageContentException("Not enough data to read the extended box size."); @@ -252,6 +334,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return (long)contentLength; } + /// + /// Parses an ISO BMFF child-box header from a bounded parent payload. + /// + /// The remaining bytes in the parent payload, beginning at the child size field. + /// Receives the validated child payload length. + /// Receives the child box four-character code. + /// The number of bytes occupied by the complete child header. private static int ParseBoxHeader(Span buffer, out long length, out Heif4CharCode boxType) { if (buffer.Length < 8) @@ -303,6 +392,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return bytesRead; } + /// + /// Indexes and parses the recognized children of a metadata box. + /// + /// The stream positioned at the metadata full-box header. + /// The bounded metadata payload length. private void ParseMetadata(BufferedReadStream stream, long boxLength) { if (boxLength < 4) @@ -312,6 +406,9 @@ internal sealed class HeifDecoderCore : ImageDecoderCore long endPosition = stream.Position + boxLength; stream.Skip(4); + + // Physical child order is not a dependency order. Record bounded payload positions first, then parse item + // declarations before the locations, references, and properties that resolve those identifiers. Dictionary boxes = []; while (stream.Position < endPosition) { @@ -356,18 +453,36 @@ internal sealed class HeifDecoderCore : ImageDecoderCore case Heif4CharCode.Iprp: this.ParseItemProperties(stream, box.Length); break; + case Heif4CharCode.Idat: + if (box.Length == 0) + { + throw new InvalidImageContentException("The item data box is empty."); + } + + // iloc construction method one addresses bytes from the start of the idat payload, not its header. + this.itemDataOffset = box.Offset; + this.itemDataLength = box.Length; + break; } } stream.Position = endPosition; } + /// + /// Validates that the metadata handler describes picture items rather than a timed media track. + /// + /// The stream positioned at the handler full-box payload. + /// The bounded handler payload length. private void ParseHandler(BufferedReadStream stream, long boxLength) { using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); - // Only read the handler type, to check if this is not a movie file. + EnsureBufferRemaining(boxBuffer, 0, 12, "handler"); + + // The full-box header and pre_defined field precede the handler type. A picture + // handler keeps this bounded parser in the still-image metadata model. int bytesRead = 8; Heif4CharCode handlerType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[bytesRead..]); if (handlerType != Heif4CharCode.Pict) @@ -376,162 +491,298 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } + /// + /// Parses the item-information box and its item-information entries. + /// + /// The stream positioned at the item-information full-box payload. + /// The bounded item-information payload length. private void ParseItemInfo(BufferedReadStream stream, long boxLength) { using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); - uint entryCount; + EnsureBufferRemaining(boxBuffer, 0, 4, "item info"); + int bytesRead = 0; byte version = boxBuffer[bytesRead]; + if (version > 1) + { + throw new InvalidImageContentException($"The item info box has unsupported version {version}."); + } + bytesRead += 4; - entryCount = ReadUInt16Or32(boxBuffer, version != 0, ref bytesRead); + uint entryCount = ReadUInt16Or32(boxBuffer, version != 0, ref bytesRead); for (uint i = 0; i < entryCount; i++) { bytesRead += this.ParseItemInfoEntry(boxBuffer[bytesRead..]); } + + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The item info entry count does not consume the item info box."); + } } + /// + /// Parses one versioned item-information entry from a bounded item-information payload. + /// + /// The bytes beginning at the item-information-entry box header. + /// The complete item-information-entry box length. private int ParseItemInfoEntry(Span buffer) { - int bytesRead = ParseBoxHeader(buffer, out long boxLength, out Heif4CharCode boxType); - byte version = buffer[bytesRead]; + int headerLength = ParseBoxHeader(buffer, out long boxLength, out Heif4CharCode boxType); + if (boxType != Heif4CharCode.Infe) + { + throw new InvalidImageContentException($"The item info box contains unexpected child '{PrettyPrint(boxType)}'."); + } + + int totalLength = checked(headerLength + (int)boxLength); + Span entryBuffer = buffer[..totalLength]; + int bytesRead = headerLength; + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + byte version = entryBuffer[bytesRead]; + if (version > 3) + { + throw new InvalidImageContentException($"The item info entry has unsupported version {version}."); + } + bytesRead += 4; HeifItem? item = null; if (version is 0 or 1) { - uint itemId = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + uint itemId = BinaryPrimitives.ReadUInt16BigEndian(entryBuffer[bytesRead..]); bytesRead += 2; item = new HeifItem(boxType, itemId); - // Skip Protection Index, not sure what that means... + uint protectionIndex = BinaryPrimitives.ReadUInt16BigEndian(entryBuffer[bytesRead..]); bytesRead += 2; - item.Name = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.Name.Length + 1; - item.ContentType = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.ContentType.Length + 1; + if (protectionIndex != 0) + { + throw new InvalidImageContentException($"Item {itemId} uses unsupported item protection."); + } - // Optional field. - if (bytesRead < boxLength) + item.Name = ReadNullTerminatedString(entryBuffer[bytesRead..], out int nameLength); + bytesRead += nameLength; + item.ContentType = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentTypeLength); + bytesRead += contentTypeLength; + + if (bytesRead < totalLength) { - item.ContentEncoding = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.ContentEncoding.Length + 1; + item.ContentEncoding = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentEncodingLength); + bytesRead += contentEncodingLength; } } if (version == 1) { - // Optional fields. - if (bytesRead < boxLength) + if (bytesRead < totalLength) { - item!.ExtensionType = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + item!.ExtensionType = BinaryPrimitives.ReadUInt32BigEndian(entryBuffer[bytesRead..]); bytesRead += 4; } - if (bytesRead < boxLength) + if (bytesRead < totalLength) { - // TODO: Parse item.Extension + // Version-one extension payloads are outside the image item types currently + // consumed by this decoder, but remain bounded within this entry. + bytesRead = totalLength; } } if (version >= 2) { - uint itemId = ReadUInt16Or32(buffer, version == 3, ref bytesRead); + uint itemId = ReadUInt16Or32(entryBuffer, version == 3, ref bytesRead); + + EnsureBufferRemaining(entryBuffer, bytesRead, 6, "item info entry"); - // Skip Protection Index, not sure what that means... + uint protectionIndex = BinaryPrimitives.ReadUInt16BigEndian(entryBuffer[bytesRead..]); bytesRead += 2; - Heif4CharCode itemType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); + if (protectionIndex != 0) + { + throw new InvalidImageContentException($"Item {itemId} uses unsupported item protection."); + } + + Heif4CharCode itemType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(entryBuffer[bytesRead..]); bytesRead += 4; item = new HeifItem(itemType, itemId); - item.Name = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.Name.Length + 1; + item.Name = ReadNullTerminatedString(entryBuffer[bytesRead..], out int nameLength); + bytesRead += nameLength; if (item.Type == Heif4CharCode.Mime) { - item.ContentType = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.ContentType.Length + 1; + item.ContentType = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentTypeLength); + bytesRead += contentTypeLength; - // Optional field. - if (bytesRead < boxLength) + if (bytesRead < totalLength) { - item.ContentEncoding = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.ContentEncoding.Length + 1; + item.ContentEncoding = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentEncodingLength); + bytesRead += contentEncodingLength; } } else if (item.Type == Heif4CharCode.Uri) { - item.UriType = ReadNullTerminatedString(buffer[bytesRead..]); - bytesRead += item.UriType.Length + 1; + item.UriType = ReadNullTerminatedString(entryBuffer[bytesRead..], out int uriLength); + bytesRead += uriLength; } } - if (item != null) + if (item is not null) { + if (this.FindItemById(item.Id) is not null) + { + throw new InvalidImageContentException($"The item info box contains duplicate item ID {item.Id}."); + } + this.items.Add(item); } - return bytesRead; + if (bytesRead != totalLength) + { + throw new InvalidImageContentException("The item info entry contains unexpected trailing data."); + } + + return totalLength; } + /// + /// Parses typed relationships between source and destination items. + /// + /// The stream positioned at the item-reference full-box payload. + /// The bounded item-reference payload length. private void ParseItemReference(BufferedReadStream stream, long boxLength) { using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 4, "item reference"); + int bytesRead = 0; - bool largeIds = boxBuffer[bytesRead] != 0; + byte version = boxBuffer[bytesRead]; + if (version > 1) + { + throw new InvalidImageContentException($"The item reference box has unsupported version {version}."); + } + + bool largeIds = version == 1; bytesRead += 4; while (bytesRead < boxLength) { - bytesRead += ParseBoxHeader(boxBuffer[bytesRead..], out long subBoxLength, out Heif4CharCode linkType); - uint sourceId = ReadUInt16Or32(boxBuffer, largeIds, ref bytesRead); + int referenceHeaderLength = ParseBoxHeader(boxBuffer[bytesRead..], out long referenceLength, out Heif4CharCode linkType); + int referenceEnd = checked(bytesRead + referenceHeaderLength + (int)referenceLength); + Span referenceBuffer = boxBuffer[..referenceEnd]; + bytesRead += referenceHeaderLength; + uint sourceId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead); + if (this.FindItemById(sourceId) is null) + { + throw new InvalidImageContentException($"The item reference box references unknown source item ID {sourceId}."); + } + HeifItemLink link = new(linkType, sourceId); - int count = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + EnsureBufferRemaining(referenceBuffer, bytesRead, 2, "item reference"); + int count = BinaryPrimitives.ReadUInt16BigEndian(referenceBuffer[bytesRead..]); bytesRead += 2; for (uint i = 0; i < count; i++) { - uint destId = ReadUInt16Or32(boxBuffer, largeIds, ref bytesRead); + uint destId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead); + if (this.FindItemById(destId) is null) + { + throw new InvalidImageContentException($"The item reference box references unknown destination item ID {destId}."); + } + link.DestinationIds.Add(destId); } - this.itemLinks!.Add(link); + if (bytesRead != referenceEnd) + { + throw new InvalidImageContentException($"The '{PrettyPrint(linkType)}' item reference length does not match its entry count."); + } + + this.itemLinks.Add(link); } } + /// + /// Reads the identifier of the presentation's primary item. + /// + /// The stream positioned at the primary-item full-box payload. + /// The bounded primary-item payload length. private void ParsePrimaryItem(BufferedReadStream stream, long boxLength) { - // BoxLength should be 6 or 8. using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 4, "primary item"); + byte version = boxBuffer[0]; + if (version > 1) + { + throw new InvalidImageContentException($"The primary item box has unsupported version {version}."); + } + int bytesRead = 4; - this.primaryItem = ReadUInt16Or32(boxBuffer, version != 0, ref bytesRead); + this.primaryItem = ReadUInt16Or32(boxBuffer, version == 1, ref bytesRead); + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The primary item box has an invalid length."); + } } + /// + /// Parses the ordered item-property table and applies its item associations. + /// + /// The stream positioned at the item-properties payload. + /// The bounded item-properties payload length. private void ParseItemProperties(BufferedReadStream stream, long boxLength) { - // Cannot use Dictionary here, Properties can have multiple instances with the same key. + // Property types may repeat, and ipma can physically precede ipco. Index the bounded + // children first so associations are always resolved after the ordered property table. List> properties = new(); long endBoxPosition = stream.Position + boxLength; + (long Offset, long Length)? propertyContainer = null; + List<(long Offset, long Length)> associations = []; while (stream.Position < endBoxPosition) { long containerLength = this.ReadBoxHeader(stream, endBoxPosition, out Heif4CharCode containerType); if (containerType == Heif4CharCode.Ipco) { - // Parse Item Property Container, which is just an array of property boxes. - this.ParsePropertyContainer(stream, containerLength, properties); + if (propertyContainer.HasValue) + { + throw new InvalidImageContentException("The item properties box contains duplicate property containers."); + } + + propertyContainer = (stream.Position, containerLength); } else if (containerType == Heif4CharCode.Ipma) { - // Parse Item Property Association - this.ParsePropertyAssociation(stream, containerLength, properties); - } - else - { - throw new ImageFormatException($"Unknown container type in property box of '{PrettyPrint(containerType)}'"); + associations.Add((stream.Position, containerLength)); } + + // Unknown optional children remain bounded by iprp and do not expand the still-image model. + SkipBox(stream, containerLength); } + + if (!propertyContainer.HasValue) + { + throw new InvalidImageContentException("The item properties box does not contain a property container."); + } + + stream.Position = propertyContainer.Value.Offset; + this.ParsePropertyContainer(stream, propertyContainer.Value.Length, properties); + foreach ((long Offset, long Length) association in associations) + { + stream.Position = association.Offset; + this.ParsePropertyAssociation(stream, association.Length, properties); + } + + stream.Position = endBoxPosition; } + /// + /// Parses the ordered property boxes contained by an item-property container. + /// + /// The stream positioned at the first property box. + /// The bounded item-property-container payload length. + /// The one-based association table in physical property order. private void ParsePropertyContainer(BufferedReadStream stream, long boxLength, List> properties) { long endPosition = stream.Position + boxLength; @@ -543,20 +794,26 @@ internal sealed class HeifDecoderCore : ImageDecoderCore switch (itemType) { case Heif4CharCode.Ispe: - // Skip over version (8 bits) and flags (24 bits). + EnsureBufferRemaining(boxBuffer, 0, 12, "image spatial extents"); + + // The full-box header precedes the unsigned display width and height. int width = (int)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); int height = (int)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[8..]); properties.Add(new KeyValuePair(Heif4CharCode.Ispe, new Size(width, height))); break; case Heif4CharCode.Pasp: + EnsureBufferRemaining(boxBuffer, 0, 8, "pixel aspect ratio"); int horizontalSpacing = (int)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); int verticalSpacing = (int)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); properties.Add(new KeyValuePair(Heif4CharCode.Pasp, new Size(horizontalSpacing, verticalSpacing))); break; case Heif4CharCode.Pixi: - // Skip over version (8 bits) and flags (24 bits). + EnsureBufferRemaining(boxBuffer, 0, 5, "pixel information"); + + // The full-box header precedes one bit-depth byte for each channel. int channelCount = boxBuffer[4]; int offset = 5; + EnsureBufferRemaining(boxBuffer, offset, channelCount, "pixel information"); int bitsPerPixel = 0; for (int i = 0; i < channelCount; i++) { @@ -567,6 +824,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Colr: + EnsureBufferRemaining(boxBuffer, 0, 4, "color information"); Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) { @@ -580,6 +838,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Av1C: + EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration"); this.av1CodecConfiguration = new(boxBuffer); properties.Add(new KeyValuePair(Heif4CharCode.Av1C, new object())); break; @@ -590,7 +849,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore case Heif4CharCode.HvcC: case Heif4CharCode.Rloc: case Heif4CharCode.Udes: - // TODO: Implement + // These registered image properties are not arbitrary unknown boxes. Preserve their indices so + // container identification remains available while their owning image stage handles the value. properties.Add(new KeyValuePair(itemType, new object())); break; default: @@ -601,24 +861,37 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } + /// + /// Applies one-based property indices and essential flags to their referenced items. + /// + /// The stream positioned at the property-association full-box payload. + /// The bounded property-association payload length. + /// The properties in the order used by association indices. private void ParsePropertyAssociation(BufferedReadStream stream, long boxLength, List> properties) { using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 8, "item property association"); byte version = boxBuffer[0]; + if (version > 1) + { + throw new InvalidImageContentException($"The item property association box has unsupported version {version}."); + } + bool largePropertyIndex = (boxBuffer[3] & 1) != 0; int bytesRead = 4; uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[bytesRead..]); bytesRead += 4; for (uint entryIndex = 0; entryIndex < entryCount; entryIndex++) { - uint itemId = ReadUInt16Or32(boxBuffer, version >= 1, ref bytesRead); + uint itemId = ReadUInt16Or32(boxBuffer, version == 1, ref bytesRead); HeifItem? item = this.FindItemById(itemId); if (item is null) { throw new InvalidImageContentException($"Item property association references unknown item ID {itemId}."); } + EnsureBufferRemaining(boxBuffer, bytesRead, 1, "item property association"); int associationCount = boxBuffer[bytesRead++]; for (int i = 0; i < associationCount; i++) { @@ -627,6 +900,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore uint essentialMask; if (largePropertyIndex) { + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item property association"); association = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); bytesRead += 2; propertyIndexMask = 0x7FFFU; @@ -634,6 +908,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } else { + EnsureBufferRemaining(boxBuffer, bytesRead, 1, "item property association"); association = boxBuffer[bytesRead++]; propertyIndexMask = 0x7FU; essentialMask = 0x80U; @@ -679,15 +954,34 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } } + + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The item property association box contains unexpected trailing data."); + } } + /// + /// Parses the construction method, base offset, and ordered extents for every declared item. + /// + /// The stream positioned at the item-location full-box payload. + /// The bounded item-location payload length. private void ParseItemLocation(BufferedReadStream stream, long boxLength) { using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); int bytesRead = 0; + EnsureBufferRemaining(boxBuffer, bytesRead, 6, "item location"); byte version = boxBuffer[bytesRead]; + if (version > 2) + { + throw new InvalidImageContentException($"The item location box has unsupported version {version}."); + } + bytesRead += 4; + + // The first two payload bytes pack four-bit integer widths for extent offset, extent length, base offset, + // and, for versions one and two, extent index. A zero width represents an implicit zero value. byte b1 = boxBuffer[bytesRead]; bytesRead++; byte b2 = boxBuffer[bytesRead]; @@ -701,43 +995,103 @@ internal sealed class HeifDecoderCore : ImageDecoderCore indexSize = b2 & 0x0f; } + if (!IsSupportedFieldSize(offsetSize) + || !IsSupportedFieldSize(lengthSize) + || !IsSupportedFieldSize(baseOffsetSize) + || !IsSupportedFieldSize(indexSize)) + { + throw new InvalidImageContentException("The item location box uses an invalid integer field size."); + } + + EnsureBufferRemaining(boxBuffer, bytesRead, version == 2 ? 4 : 2, "item location"); uint itemCount = ReadUInt16Or32(boxBuffer, version == 2, ref bytesRead); + HashSet locatedItemIds = []; for (uint i = 0; i < itemCount; i++) { + EnsureBufferRemaining(boxBuffer, bytesRead, version == 2 ? 4 : 2, "item location"); uint itemId = ReadUInt16Or32(boxBuffer, version == 2, ref bytesRead); HeifItem? item = this.FindItemById(itemId); + if (item is null) + { + throw new InvalidImageContentException($"The item location box references unknown item ID {itemId}."); + } + + if (!locatedItemIds.Add(itemId)) + { + throw new InvalidImageContentException($"The item location box contains duplicate locations for item ID {itemId}."); + } + HeifLocationOffsetOrigin constructionMethod = HeifLocationOffsetOrigin.FileOffset; if (version is 1 or 2) { - bytesRead++; - byte b3 = boxBuffer[bytesRead]; - bytesRead++; - constructionMethod = (HeifLocationOffsetOrigin)(b3 & 0x0f); + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item location"); + ushort constructionField = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + bytesRead += 2; + if ((constructionField & 0xFFF0) != 0) + { + throw new InvalidImageContentException("The item location box has nonzero reserved construction bits."); + } + + constructionMethod = (HeifLocationOffsetOrigin)(constructionField & 0x0F); + if (constructionMethod is not HeifLocationOffsetOrigin.FileOffset and not HeifLocationOffsetOrigin.ItemDataOffset) + { + throw new InvalidImageContentException($"The item location box uses unsupported construction method {(int)constructionMethod}."); + } } + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item location"); uint dataReferenceIndex = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); bytesRead += 2; + if (dataReferenceIndex != 0) + { + throw new InvalidImageContentException("External item data references are not supported."); + } + long baseOffset = ReadUIntVariable(boxBuffer, baseOffsetSize, ref bytesRead); + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item location"); uint extentCount = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); bytesRead += 2; for (uint j = 0; j < extentCount; j++) { - uint extentIndex = 0; if (version is 1 or 2 && indexSize > 0) { - extentIndex = (uint)ReadUIntVariable(boxBuffer, indexSize, ref bytesRead); + // Extent indices select referenced-item extents only for construction method two. Methods zero + // and one still carry the field when configured, so consume it to preserve the following offsets. + ReadUIntVariable(boxBuffer, indexSize, ref bytesRead); } long extentOffset = ReadUIntVariable(boxBuffer, offsetSize, ref bytesRead); long extentLength = ReadUIntVariable(boxBuffer, lengthSize, ref bytesRead); HeifLocation loc = new(constructionMethod, baseOffset, extentOffset, extentLength); - item?.DataLocations.Add(loc); + item.DataLocations.Add(loc); } } + + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The item location box contains unexpected trailing data."); + } } + /// + /// Determines whether an item-location integer width can be represented by the supported reader primitives. + /// + /// The width in bytes from an item-location size nibble. + /// for the registered zero, 32-bit, and 64-bit widths. + private static bool IsSupportedFieldSize(int size) => size is 0 or 4 or 8; + + /// + /// Reads a version-selected 16-bit or 32-bit unsigned identifier or count. + /// + /// The bounded box payload. + /// Indicates that the field is 32 bits rather than 16 bits. + /// The running payload offset, advanced past the field. + /// The decoded unsigned value. private static uint ReadUInt16Or32(Span buffer, bool isLarge, ref int bytesRead) { + int fieldLength = isLarge ? 4 : 2; + EnsureBufferRemaining(buffer, bytesRead, fieldLength, "versioned integer field"); + uint result; if (isLarge) { @@ -753,91 +1107,136 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return result; } + /// + /// Reads a zero-width, 32-bit, or 64-bit unsigned item-location field into the supported stream range. + /// + /// The bounded item-location payload. + /// The field width selected by the item-location size nibble. + /// The running payload offset, advanced past the field. + /// The decoded nonnegative stream offset or length. private static long ReadUIntVariable(Span buffer, int numBytes, ref int bytesRead) { - long result = 0L; - int shift = 0; - if (numBytes > 8) - { - throw new InvalidImageContentException($"Can't store large integer of {numBytes * 8} bits."); - } - else - if (numBytes > 4) + EnsureBufferRemaining(buffer, bytesRead, numBytes, "item location"); + ulong result = numBytes switch { - result = (long)BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]); - shift = 8 - numBytes; - } - else if (numBytes > 2) - { - result = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); - shift = 4 - numBytes; - } - else if (numBytes > 1) - { - result = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); - } - else if (numBytes == 1) + 0 => 0, + 4 => BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]), + 8 => BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]), + _ => throw new InvalidImageContentException("The item location box uses an invalid integer field size.") + }; + + if (result > long.MaxValue) { - result = buffer[bytesRead]; + throw new InvalidImageContentException("An item location offset exceeds the supported stream range."); } bytesRead += numBytes; - result >>= shift << 3; - return result; + return (long)result; } - private Image ParseMediaData(Stream stream, long boxLength) + /// + /// Resolves item extents, selects the primary or supported thumbnail decoder, and reconstructs the image. + /// + /// The destination pixel format. + /// The complete seekable HEIF container stream. + /// The image reconstructed from the selected item. + private Image DecodePrimaryItem(BufferedReadStream stream) where TPixel : unmanaged, IPixel { - EnsureBoxBoundary(boxLength, stream); - - IComparer comparer = new HeifLocationComparer(stream.Position, stream.Position); - SortedList locations = new(comparer); + using DisposableDictionary> buffers = new(this.items.Count); foreach (HeifItem item in this.items) { - HeifLocation loc = item.DataLocations[0]; - if (loc.Length != 0) + long itemLength = 0; + foreach (HeifLocation loc in item.DataLocations) { - locations[loc] = item; + if (loc.Length < 0 || itemLength > int.MaxValue - loc.Length) + { + throw new InvalidImageContentException($"Item {item.Id} data is too large to buffer."); + } + + itemLength += loc.Length; } - } - using DisposableDictionary> buffers = new(locations.Count); - foreach (HeifLocation loc in locations.Keys) - { - HeifItem item = locations[loc]; - long streamPosition = loc.GetStreamPosition(stream.Position, stream.Position); - long dataLength = loc.Length; - stream.Skip((int)(streamPosition - stream.Position)); - EnsureBoxBoundary(dataLength, stream); - buffers.Add(item.Id, this.ReadIntoBuffer(stream, dataLength)); + if (itemLength == 0) + { + continue; + } + + // One logical item is the concatenation of its extents in declared order. Materialize only that item data, + // never the enclosing file or mdat box, so codec readers receive the contiguous payload they expect. + int bufferLength = (int)itemLength; + IMemoryOwner extentMemory = this.configuration.MemoryAllocator.Allocate(bufferLength); + buffers.Add(item.Id, extentMemory); + Span itemBuffer = extentMemory.GetSpan()[..bufferLength]; + int writeOffset = 0; + foreach (HeifLocation loc in item.DataLocations) + { + if (loc.BaseOffset < 0 || loc.Offset < 0 || loc.BaseOffset > long.MaxValue - loc.Offset) + { + throw new InvalidImageContentException($"Item {item.Id} has an invalid extent offset."); + } + + long relativeOffset = loc.BaseOffset + loc.Offset; + long sourceOffset; + long sourceBytesRemaining; + if (loc.Origin == HeifLocationOffsetOrigin.FileOffset) + { + // Construction method zero resolves base_offset + extent_offset from the start of the file. + sourceOffset = relativeOffset; + sourceBytesRemaining = stream.Length - sourceOffset; + } + else if (loc.Origin == HeifLocationOffsetOrigin.ItemDataOffset) + { + if (this.itemDataOffset < 0 || relativeOffset > this.itemDataLength) + { + throw new InvalidImageContentException($"Item {item.Id} has an extent outside its item data box."); + } + + // Construction method one resolves the same relative value from the idat payload start. + sourceOffset = this.itemDataOffset + relativeOffset; + sourceBytesRemaining = this.itemDataLength - relativeOffset; + } + else + { + throw new InvalidImageContentException($"Item {item.Id} uses an unsupported location origin."); + } + + EnsureBoxInsideParent(loc.Length, sourceBytesRemaining); + stream.Position = sourceOffset; + int extentLength = (int)loc.Length; + int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength)); + if (bytesRead != extentLength) + { + throw new InvalidImageContentException($"Item {item.Id} extent is truncated."); + } + + writeOffset += extentLength; + } } HeifItem? rootItem = this.FindItemById(this.primaryItem); - if (rootItem == null) + if (rootItem is null) { throw new ImageFormatException("No primary HEIF item defined."); } - IHeifItemDecoder? itemDecoder; - if (rootItem.Type == Heif4CharCode.Grid) - { - itemDecoder = new GridHeifItemDecoder(this.configuration, this.items, this.itemLinks, buffers); - } - - itemDecoder = HeifCompressionFactory.GetDecoder(rootItem.Type); + IHeifItemDecoder? itemDecoder = rootItem.Type == Heif4CharCode.Grid + ? new GridHeifItemDecoder(this.configuration, this.items, this.itemLinks, buffers) + : HeifCompressionFactory.GetDecoder(rootItem.Type); HeifItem itemToDecode = rootItem; - if (itemDecoder == null) + if (itemDecoder is null) { // Unable to decode the primary image, decode the thumbnail instead. - HeifItemLink? thumbLink = this.itemLinks.FirstOrDefault(link => link.Type == Heif4CharCode.Thmb); - if (thumbLink != null) + HeifItemLink? thumbLink = this.itemLinks.FirstOrDefault( + link => link.Type == Heif4CharCode.Thmb && link.DestinationIds.Contains(rootItem.Id)); + + if (thumbLink is not null) { HeifItem? thumbItem = this.FindItemById(thumbLink.SourceId); - if (thumbItem != null) + if (thumbItem is not null) { itemDecoder = HeifCompressionFactory.GetDecoder(thumbItem.Type); - if (itemDecoder != null) + if (itemDecoder is not null) { itemToDecode = thumbItem; } @@ -845,21 +1244,54 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } - if (itemDecoder == null) + if (itemDecoder is null) { throw new ImageFormatException("No decodable item found inside this HEIF container."); } - HeifMetadata meta = this.metadata.GetHeifMetadata(); + if (!buffers.TryGetValue(itemToDecode.Id, out IMemoryOwner? itemMemory)) + { + throw new InvalidImageContentException($"Item {itemToDecode.Id} has no data extents."); + } + + Image image = itemDecoder.DecodeItemData(this.configuration, itemToDecode, itemMemory.GetSpan()); + + // The decoder determines the compression of the pixels that were actually returned, including grid tiles + // and a thumbnail fallback when the primary image compression is not available. + HeifMetadata meta = image.Metadata.GetHeifMetadata(); meta.CompressionMethod = itemDecoder.CompressionMethod; + return image; + } - IMemoryOwner itemMemory = buffers[itemToDecode.Id]; - return itemDecoder.DecodeItemData(this.configuration, itemToDecode, itemMemory.GetSpan()); + /// + /// Validates that a fixed-width field remains within a buffered box payload. + /// + /// The bounded box payload. + /// The zero-based field offset. + /// The field width in bytes. + /// The diagnostic name used for malformed input errors. + private static void EnsureBufferRemaining(Span buffer, int offset, int count, string boxName) + { + if ((uint)offset > (uint)buffer.Length || (uint)count > (uint)(buffer.Length - offset)) + { + throw new InvalidImageContentException($"The {boxName} box is truncated."); + } } + /// + /// Advances over a box payload without narrowing its 64-bit length. + /// + /// The seekable container stream. + /// The validated payload length. private static void SkipBox(Stream stream, long boxLength) => stream.Seek(boxLength, SeekOrigin.Current); + /// + /// Reads a complete bounded box payload into allocator-owned memory. + /// + /// The stream positioned at the payload start. + /// The validated payload length. + /// An owner containing exactly the requested payload bytes. private IMemoryOwner ReadIntoBuffer(Stream stream, long length) { if ((ulong)length > int.MaxValue) @@ -878,9 +1310,19 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return buffer; } + /// + /// Validates a box payload length against the bytes remaining in the file. + /// + /// The declared box payload length. + /// The stream positioned at the payload start. private static void EnsureBoxBoundary(long boxLength, Stream stream) => EnsureBoxInsideParent(boxLength, stream.Length - stream.Position); + /// + /// Validates a child payload length against its remaining parent payload. + /// + /// The declared child payload length. + /// The number of bytes remaining in the parent. private static void EnsureBoxInsideParent(long boxLength, long parentLength) { if (boxLength < 0 || parentLength < 0 || boxLength > parentLength) @@ -889,15 +1331,37 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } + /// + /// Finds an item by its file-defined identifier. + /// + /// The item identifier. + /// The matching item, or when it has not been declared. private HeifItem? FindItemById(uint itemId) => this.items.FirstOrDefault(item => item.Id == itemId); - private static string ReadNullTerminatedString(Span span) + /// + /// Decodes the UTF-8 bytes preceding the first null terminator. + /// + /// The bytes beginning at a required null-terminated string. + /// The number of source bytes consumed, including the terminator. + /// The decoded string without its terminator. + private static string ReadNullTerminatedString(Span span, out int bytesRead) { - Span bytes = span[..span.IndexOf((byte)0)]; - return Encoding.UTF8.GetString(bytes); + int terminator = span.IndexOf((byte)0); + if (terminator < 0) + { + throw new InvalidImageContentException("A null-terminated item information string is truncated."); + } + + bytesRead = terminator + 1; + return Encoding.UTF8.GetString(span[..terminator]); } + /// + /// Formats a known enum name or an unknown four-character code for diagnostics. + /// + /// The box, property, brand, or item code. + /// A readable enum name or four-character ASCII value. private static string PrettyPrint(Heif4CharCode code) { string? pretty = Enum.GetName(code); diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs index 71726b245c..7d4684c5e2 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs @@ -41,16 +41,16 @@ internal sealed class HeifEncoderCore /// The to encode from. /// The to encode the image data to. /// The token to request cancellation. - public async void Encode(Image image, Stream stream, CancellationToken cancellationToken) + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - byte[] pixels = await CompressPixels(image, cancellationToken); + byte[] pixels = CompressPixels(image, cancellationToken); List items = new(); List links = new(); - GenerateItems(image, pixels, items, links); + GenerateItems(image, pixels, items); // Write out the generated header and pixels. this.WriteFileTypeBox(stream); @@ -62,22 +62,33 @@ internal sealed class HeifEncoderCore meta.CompressionMethod = HeifCompressionMethod.LegacyJpeg; } - private static void GenerateItems(Image image, byte[] pixels, List items, List links) + /// + /// Builds the item declarations and relationships for the encoded image payload. + /// + /// The source pixel format. + /// The source image. + /// The encoded primary-item payload. + /// The destination item collection. + private static void GenerateItems(Image image, byte[] pixels, List items) where TPixel : unmanaged, IPixel { HeifItem primaryItem = new(Heif4CharCode.Jpeg, 1u); - primaryItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.ItemDataOffset, 0L, 0L, pixels.LongLength)); + primaryItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, 0L, pixels.LongLength)); primaryItem.BitsPerPixel = 24; primaryItem.ChannelCount = 3; primaryItem.SetExtent(image.Size); items.Add(primaryItem); - // Create a fake thumbnail, to make our own Decoder happy. - HeifItemLink thumbnail = new(Heif4CharCode.Thmb, 1u); - thumbnail.DestinationIds.Add(1u); - links.Add(thumbnail); + // No item relationship is emitted until the writer has a distinct derived image, + // thumbnail, auxiliary image, or metadata item to reference. } + /// + /// Writes an eight-byte ISO BMFF basic box header with a placeholder size. + /// + /// The destination beginning at the box size field. + /// The box four-character code. + /// The number of header bytes written. private static int WriteBoxHeader(Span buffer, Heif4CharCode type) { int bytesWritten = 0; @@ -89,6 +100,14 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes a 12-byte ISO BMFF full-box header with a placeholder size. + /// + /// The destination beginning at the box size field. + /// The box four-character code. + /// The full-box syntax version. + /// The 24-bit full-box flags value. + /// The number of header bytes written. private static int WriteBoxHeader(Span buffer, Heif4CharCode type, byte version, uint flags) { int bytesWritten = 0; @@ -97,7 +116,8 @@ internal sealed class HeifEncoderCore BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)type); bytesWritten += 4; - // Layout in memory is 4 bytes, 1 version byte followed by 3 flag bytes. + // Writing the 24-bit flags as a big-endian 32-bit value establishes the three flag bytes, after which the + // version overwrites the leading byte to form the full-box version-and-flags word. BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], flags); buffer[bytesWritten] = version; bytesWritten += 4; @@ -105,6 +125,10 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes the major brand, minor version, and compatible brands for the current HEIF output. + /// + /// The destination stream. private void WriteFileTypeBox(Stream stream) { Span buffer = stackalloc byte[24]; @@ -122,6 +146,12 @@ internal sealed class HeifEncoderCore stream.Write(buffer); } + /// + /// Writes the metadata box containing item declarations, relationships, properties, and file locations. + /// + /// The declared image and metadata items. + /// The typed relationships between items. + /// The destination stream positioned after the file-type box. private void WriteMetadataBox(List items, List links, Stream stream) { using AutoExpandingMemory memory = new(this.configuration, 0x1000); @@ -130,16 +160,34 @@ internal sealed class HeifEncoderCore bytesWritten += WriteHandlerBox(memory, bytesWritten); bytesWritten += WritePrimaryItemBox(memory, bytesWritten); bytesWritten += WriteItemInfoBox(memory, bytesWritten, items); - bytesWritten += WriteItemReferenceBox(memory, bytesWritten, items, links); + if (links.Count > 0) + { + // iref is optional and has no meaning without at least one typed item relationship. + bytesWritten += WriteItemReferenceBox(memory, bytesWritten, items, links); + } + bytesWritten += WriteItemPropertiesBox(memory, bytesWritten, items); - bytesWritten += WriteItemDataBox(memory, bytesWritten); - bytesWritten += WriteItemLocationBox(memory, bytesWritten, items); + + // iloc needs the absolute mdat payload position, but that position depends on the final meta length. Emit it + // once to establish the stable box size, calculate the following mdat position, then patch the same bytes. + int itemLocationOffset = bytesWritten; + bytesWritten += WriteItemLocationBox(memory, bytesWritten, items, 0); + + // The mdat payload immediately follows the completed meta box and its own eight-byte header. + long mediaDataOffset = checked(stream.Position + bytesWritten + 8); + WriteItemLocationBox(memory, itemLocationOffset, items, mediaDataOffset); buffer = memory.GetSpan(bytesWritten); BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); stream.Write(buffer); } + /// + /// Writes the picture metadata handler box. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The complete handler-box length. private static int WriteHandlerBox(AutoExpandingMemory memory, int memoryOffset) { Span buffer = memory.GetSpan(memoryOffset, 33); @@ -157,6 +205,12 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes the identifier of the primary presentation item. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The complete primary-item-box length. private static int WritePrimaryItemBox(AutoExpandingMemory memory, int memoryOffset) { Span buffer = memory.GetSpan(memoryOffset, 14); @@ -168,6 +222,13 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes the item-information box and one version-two entry for each item. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The items to declare. + /// The complete item-information-box length. private static int WriteItemInfoBox(AutoExpandingMemory memory, int memoryOffset, List items) { Span buffer = memory.GetSpan(memoryOffset, 14 + (items.Count * 21)); @@ -193,6 +254,14 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes typed item-reference child boxes using 16-bit item identifiers. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The declared items used to size the destination. + /// The relationships to write. + /// The complete item-reference-box length. private static int WriteItemReferenceBox(AutoExpandingMemory memory, int memoryOffset, List items, List links) { Span buffer = memory.GetSpan(memoryOffset, 12 + (links.Count * (12 + (items.Count * 2)))); @@ -218,12 +287,19 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes spatial-extent properties and their one-based item associations. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The items whose dimensions are written and associated. + /// The complete item-properties-box length. private static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items) { Span buffer = memory.GetSpan(memoryOffset, 20); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp); - // Write 'ipco' box + // ipco order defines the one-based property indices written later in ipma. int ipcoLengthOffset = bytesWritten; bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipco); foreach (HeifItem item in items) @@ -236,7 +312,7 @@ internal sealed class HeifEncoderCore int propertyIndexSize = largePropertyIndex ? 2 : 1; buffer = memory.GetSpan(memoryOffset, bytesWritten + 16 + ((3 + propertyIndexSize) * items.Count)); - // Write 'ipma' box + // ipma uses a 15-bit index only when the property table cannot fit in the compact seven-bit form. int ipmaLengthOffset = bytesWritten; bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipma, 0, largePropertyIndex ? 1U : 0U); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)items.Count); @@ -267,6 +343,13 @@ internal sealed class HeifEncoderCore return bytesWritten; } + /// + /// Writes an item's display width and height as an image-spatial-extents property. + /// + /// The expanding metadata buffer. + /// The destination offset within the property container. + /// The item whose extent is written. + /// The complete image-spatial-extents-box length. private static int WriteSpatialExtentPropertyBox(AutoExpandingMemory memory, int memoryOffset, HeifItem item) { Span buffer = memory.GetSpan(memoryOffset, 20); @@ -280,45 +363,58 @@ internal sealed class HeifEncoderCore return bytesWritten; } - private static int WriteItemDataBox(AutoExpandingMemory memory, int memoryOffset) - { - Span buffer = memory.GetSpan(memoryOffset, 10); - int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Idat); - - BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); - return bytesWritten; - } - - private static int WriteItemLocationBox(AutoExpandingMemory memory, int memoryOffset, List items) + /// + /// Writes version-one file-relative locations for every ordered item extent. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The items and relative payload extents to locate. + /// The absolute stream offset of the media-data payload. + /// The complete item-location-box length. + private static int WriteItemLocationBox(AutoExpandingMemory memory, int memoryOffset, List items, long mediaDataOffset) { - Span buffer = memory.GetSpan(memoryOffset, 30 + (items.Count * 8)); + int extentCount = items.Sum(item => item.DataLocations.Count); + Span buffer = memory.GetSpan(memoryOffset, 16 + (items.Count * 8) + (extentCount * 12)); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iloc, 1, 0); - buffer[bytesWritten++] = 0x44; + + // The high and low nibbles select eight-byte offsets and four-byte lengths. Base offsets and extent indices + // are omitted, because every generated extent is written as one absolute file offset into mdat. + buffer[bytesWritten++] = 0x84; buffer[bytesWritten++] = 0; - BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1); - bytesWritten += 2; - BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items[0].Id); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count); bytesWritten += 2; - for (int i = 0; i < 4; i++) + foreach (HeifItem item in items) { - buffer[bytesWritten++] = 0; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); + bytesWritten += 2; - IEnumerable itemLocs = items.SelectMany(item => item.DataLocations).Where(loc => loc != null); - BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)itemLocs.Count()); - bytesWritten += 2; - foreach (HeifLocation loc in itemLocs) - { - BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)loc.Offset); - bytesWritten += 4; - BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)loc.Length); - bytesWritten += 4; + // Version 1 stores twelve reserved bits followed by the four-bit construction method. + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)HeifLocationOffsetOrigin.FileOffset); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.DataLocations.Count); + bytesWritten += 2; + foreach (HeifLocation loc in item.DataLocations) + { + // Generated locations are relative to the mdat payload until the enclosing meta size is known. + long absoluteOffset = checked(mediaDataOffset + loc.BaseOffset + loc.Offset); + BinaryPrimitives.WriteUInt64BigEndian(buffer[bytesWritten..], (ulong)absoluteOffset); + bytesWritten += 8; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)loc.Length); + bytesWritten += 4; + } } BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); return bytesWritten; } + /// + /// Writes the encoded primary-item bytes in a media-data box. + /// + /// The encoded item payload. + /// The destination stream. private void WriteMediaDataBox(Span data, Stream stream) { Span buf = stackalloc byte[12]; @@ -329,7 +425,14 @@ internal sealed class HeifEncoderCore stream.Write(data); } - private static async Task CompressPixels(Image image, CancellationToken cancellationToken) + /// + /// Encodes the source pixels as the current legacy JPEG item payload. + /// + /// The source pixel format. + /// The source image. + /// The token used to cancel payload encoding. + /// The encoded JPEG item bytes. + private static byte[] CompressPixels(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using MemoryStream stream = new(); @@ -337,7 +440,10 @@ internal sealed class HeifEncoderCore { ColorType = JpegColorType.YCbCrRatio420 }; - await image.SaveAsJpegAsync(stream, encoder, cancellationToken); + + // ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation + // so HEIF encoding cannot return while its temporary item payload is still being produced. + image.SaveAsJpegAsync(stream, encoder, cancellationToken).GetAwaiter().GetResult(); return stream.ToArray(); } } diff --git a/src/ImageSharp/Formats/Heif/HeifFormat.cs b/src/ImageSharp/Formats/Heif/HeifFormat.cs index 6a1cdcce3c..6a89c4adc1 100644 --- a/src/ImageSharp/Formats/Heif/HeifFormat.cs +++ b/src/ImageSharp/Formats/Heif/HeifFormat.cs @@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// public sealed class HeifFormat : IImageFormat { + /// + /// Initializes a new instance of the class. + /// private HeifFormat() { } diff --git a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs index 513767740c..dffa45ee19 100644 --- a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs @@ -18,11 +18,18 @@ public sealed class HeifImageFormatDetector : IImageFormatDetector public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) { format = IsSupportedFileFormat(header) ? HeifFormat.Instance : null; - return format != null; + return format is not null; } + /// + /// Determines whether the available header begins with a supported still-image HEIF file-type box. + /// + /// The fixed-size header prefix supplied by format detection. + /// when the prefix declares a supported still-image brand; otherwise, . private static bool IsSupportedFileFormat(ReadOnlySpan header) { + // Detection is intentionally limited to files beginning with ftyp. Other valid top-level boxes can precede + // ftyp in ISO BMFF, but scanning arbitrary input is outside the fixed-header detector contract. if (header.Length < 16 || BinaryPrimitives.ReadUInt32BigEndian(header[4..]) != (uint)Heif4CharCode.Ftyp) { return false; @@ -34,6 +41,8 @@ public sealed class HeifImageFormatDetector : IImageFormatDetector return false; } + // HeaderSize may expose only a prefix of a longer ftyp box. Whole compatible-brand codes in that prefix are + // sufficient for detection; the decoder validates the complete box before reading the rest of the container. int availableContentLength = (int)Math.Min(boxSize - 8, (uint)header.Length - 8); availableContentLength &= ~3; return HeifConstants.IsSupportedFileType(header.Slice(8, availableContentLength)); diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs index 3c4cafe3d0..3fb2499837 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -4,8 +4,10 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Provides definition for a HEIF Item. +/// Describes a metadata or image item in a HEIF still-image container. /// +/// The four-character item type. +/// The item identifier used by locations, properties, and references. internal class HeifItem(Heif4CharCode type, uint id) { /// @@ -92,5 +94,9 @@ internal class HeifItem(Heif4CharCode type, uint id) } } + /// + /// Returns the item type and identifier. + /// + /// The item type and identifier separated by a colon. public override string ToString() => $"{this.Type}:{this.Id}"; } diff --git a/src/ImageSharp/Formats/Heif/HeifItemLink.cs b/src/ImageSharp/Formats/Heif/HeifItemLink.cs index 7ed2f45402..80e6ebdaff 100644 --- a/src/ImageSharp/Formats/Heif/HeifItemLink.cs +++ b/src/ImageSharp/Formats/Heif/HeifItemLink.cs @@ -6,6 +6,8 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// /// Link between instances within the same HEIF file. /// +/// The four-character reference type. +/// The identifier of the item that owns the references. internal class HeifItemLink(Heif4CharCode type, uint sourceId) { /// diff --git a/src/ImageSharp/Formats/Heif/HeifLocation.cs b/src/ImageSharp/Formats/Heif/HeifLocation.cs index 1f2f35787f..c027fb49aa 100644 --- a/src/ImageSharp/Formats/Heif/HeifLocation.cs +++ b/src/ImageSharp/Formats/Heif/HeifLocation.cs @@ -4,8 +4,12 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Location within the file of an . +/// Describes one contiguous extent of an item's encoded data. /// +/// The origin from which the base and extent offsets are measured. +/// The item-location base offset. +/// The extent offset relative to the base offset. +/// The length of the extent in bytes. internal class HeifLocation(HeifLocationOffsetOrigin origin, long baseOffset, long offset, long length) { /// @@ -14,25 +18,26 @@ internal class HeifLocation(HeifLocationOffsetOrigin origin, long baseOffset, lo public HeifLocationOffsetOrigin Origin { get; } = origin; /// - /// Gets the base offset of this location. + /// Gets the item-location base offset in bytes. /// public long BaseOffset { get; } = baseOffset; /// - /// Gets the offset of this location. + /// Gets the extent offset relative to in bytes. /// public long Offset { get; } = offset; /// - /// Gets the length of this location. + /// Gets the extent length in bytes. /// public long Length { get; } = length; /// - /// Gets the stream position of this location. + /// Resolves the absolute stream position of this extent. /// - /// Stream position of the MediaData box. - /// Stream position of the previous box. + /// The absolute origin of the item-data payload. + /// The absolute origin of the referenced item payload. + /// The absolute byte position of the extent in the input stream. public long GetStreamPosition(long positionOfMediaData, long positionOfItem) => this.Origin switch { HeifLocationOffsetOrigin.FileOffset => this.BaseOffset + this.Offset, @@ -40,8 +45,10 @@ internal class HeifLocation(HeifLocationOffsetOrigin origin, long baseOffset, lo _ => positionOfItem + this.BaseOffset + this.Offset }; + /// public override int GetHashCode() => HashCode.Combine(this.Origin, this.Offset, this.Length, this.BaseOffset); + /// public override bool Equals(object? obj) { if (obj is not HeifLocation other) diff --git a/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs b/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs index e249114ced..c8aaba3af4 100644 --- a/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs +++ b/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs @@ -3,22 +3,43 @@ namespace SixLabors.ImageSharp.Formats.Heif; +/// +/// Orders item extents by their resolved absolute stream position. +/// internal class HeifLocationComparer : IComparer { + /// + /// The absolute origin of item-data-relative extents. + /// private readonly long positionOfMediaData; + + /// + /// The absolute origin of item-relative extents. + /// private readonly long positionOfItem; + /// + /// Initializes a new instance of the class. + /// + /// The absolute origin of the item-data payload. + /// The absolute origin of the referenced item payload. public HeifLocationComparer(long positionOfMediaData, long positionOfItem) { this.positionOfMediaData = positionOfMediaData; this.positionOfItem = positionOfItem; } + /// + /// Compares two extents by their resolved absolute stream positions. + /// + /// The first extent. + /// The second extent. + /// A negative value when precedes , zero when their positions match, or a positive value otherwise. public int Compare(HeifLocation? x, HeifLocation? y) { - if (x == null) + if (x is null) { - if (y == null) + if (y is null) { return 0; } @@ -26,7 +47,7 @@ internal class HeifLocationComparer : IComparer return 1; } - if (y == null) + if (y is null) { return -1; } @@ -34,6 +55,7 @@ internal class HeifLocationComparer : IComparer long xPos = x.GetStreamPosition(this.positionOfMediaData, this.positionOfItem); long yPos = y.GetStreamPosition(this.positionOfMediaData, this.positionOfItem); - return Math.Sign(xPos - yPos); + // CompareTo avoids overflowing when valid 64-bit offsets lie near opposite numeric limits. + return xPos.CompareTo(yPos); } } diff --git a/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs b/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs index accceb380b..e3011d0229 100644 --- a/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs +++ b/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs @@ -3,9 +3,23 @@ namespace SixLabors.ImageSharp.Formats.Heif; +/// +/// Identifies the origin used to resolve an item-location extent offset. +/// internal enum HeifLocationOffsetOrigin { + /// + /// The base and extent offsets are absolute file offsets. + /// FileOffset = 0, + + /// + /// The base and extent offsets are relative to the item-data box payload. + /// ItemDataOffset = 1, + + /// + /// The base and extent offsets are relative to another item payload. + /// ItemOffset = 2 } diff --git a/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs index b5f66afe4a..f1f106de85 100644 --- a/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs @@ -6,28 +6,28 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Decoder for a single . +/// Decodes the compressed payload of a single HEIF image item. /// -/// The pixel type to use. +/// The destination pixel type. internal interface IHeifItemDecoder where TPixel : unmanaged, IPixel { /// - /// Gets the type of item this decoder can decode. + /// Gets the image item type decoded by this implementation. /// public Heif4CharCode Type { get; } /// - /// Gets the tis decoder uses. + /// Gets the compression method used by the image item. /// public HeifCompressionMethod CompressionMethod { get; } /// - /// Decode the specified item, given encoded data. + /// Decodes the compressed payload of an image item. /// - /// The configuration to used. - /// The item to decode. - /// The encoded data. + /// The configuration that supplies memory allocation and codec services. + /// The HEIF item whose encoded payload is being decoded. + /// The encoded image payload. /// The decoded image. public Image DecodeItemData(Configuration configuration, HeifItem item, Span data); } diff --git a/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs index b095f6eb14..823f5bfd94 100644 --- a/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs @@ -6,24 +6,29 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif; /// -/// Decoder for a single into a JPEG image. +/// Decodes a single JPEG-coded HEIF image item. /// +/// The destination pixel type. internal class JpegHeifItemDecoder : IHeifItemDecoder where TPixel : unmanaged, IPixel { /// - /// Gets the item type this decoder decodes, which is . + /// Gets the JPEG-coded image item type. /// public Heif4CharCode Type => Heif4CharCode.Jpeg; /// - /// Gets the compression method this doceder uses, which is . + /// Gets the legacy JPEG compression method. /// public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.LegacyJpeg; /// - /// Decode the specified item as JPEG. + /// Decodes the encoded JPEG payload of an image item. /// + /// The configuration associated with the containing HEIF decode. + /// The HEIF item whose encoded payload is being decoded. + /// The encoded JPEG payload. + /// The decoded image. public Image DecodeItemData(Configuration configuration, HeifItem item, Span data) { Image image = Image.Load(data);