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