diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 232301f61..cebc5b15f 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -67,6 +67,8 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Decode all-sync independently decodable AV1 samples into directly adopted ImageSharp frames without cloning complete pixel buffers. - [x] Match auxiliary alpha samples by exact decode duration, visibility, and presentation time, and validate premultiplication track identity. - [x] Require unity movie and track matrices so image presentation remains on the optimized `clap`/`irot`/`imir` path without a movie compositor. + - [x] Apply the shared `DecoderOptions` contract consistently to still items, nested payload codecs, grids, metadata properties, and sequence samples. + - `Strict` rejects recoverable ancillary and image-data errors, `IgnoreAncillary` suppresses only ancillary failures, and `IgnoreImageData` additionally permits failed image properties or samples to be omitted. `SkipMetadata` avoids optional property and item-payload validation, while cancellation and the caller configuration flow into nested JPEG and AV1 decoders. Target scaling and ICC conversion remain presentation-level operations after item or grid composition. The focused Release matrix passes all 19 new still-image policy cases, all 3 new sequence-sample cases, and the complete 32-test sequence-parser suite; the Release test-project build completes with zero errors. - [ ] Complete reference-dependent AV1 and HEVC sample reconstruction and independent sequence vectors. - [ ] Write the same bounded movie, track, sample-description, location, dependency, timing, repetition, alpha, and metadata syntax from ImageSharp frames. - [ ] Decode frame dependencies, durations, repetition, frame-local auxiliary images, and frame-local metadata into the existing ImageSharp multi-frame model. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs index 829649a83..e96d935e3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -48,7 +48,8 @@ internal sealed class Av1CodecConfiguration /// item-property payload. /// /// The configuration payload beginning with the marker and version fields. - public Av1CodecConfiguration(Span boxBuffer) + /// The general options governing metadata validation. + public Av1CodecConfiguration(Span boxBuffer, DecoderOptions options) { if (boxBuffer.Length < 4) { @@ -99,6 +100,7 @@ internal sealed class Av1CodecConfiguration true, true, "AV1 codec configuration", + options, out this.configSequenceHeaderOffset, out this.configSequenceHeaderLength, out this.configSequenceHeaderExtension, @@ -176,6 +178,7 @@ internal sealed class Av1CodecConfiguration /// /// The mastering-display property associated with the image item, or when absent. /// + /// The general options governing metadata validation. /// /// Receives the content light-level metadata carried by the combined configuration and item OBUs. /// @@ -186,83 +189,161 @@ internal sealed class Av1CodecConfiguration ReadOnlySpan itemData, HeifContentLightLevel? itemContentLightLevel, HeifMasteringDisplayColorVolume? itemMasteringDisplayColorVolume, + DecoderOptions options, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + => this.ValidateData( + itemData, + true, + "AV1 image item", + itemContentLightLevel, + itemMasteringDisplayColorVolume, + options, + out contentLightLevel, + out masteringDisplayColorVolume); + + /// + /// Validates one AV1 track sample against its sync-sample declaration, sample-entry metadata, and configuration record. + /// + /// The complete AV1 sample payload. + /// Indicates that the sample is declared as a random-access point. + /// + /// The content light-level property associated with the sample entry, or when absent. + /// + /// + /// The mastering-display property associated with the sample entry, or when absent. + /// + /// The general options governing metadata validation. + /// + /// Receives the content light-level metadata carried by the combined configuration and sample OBUs. + /// + /// + /// Receives the mastering-display metadata carried by the combined configuration and sample OBUs. + /// + public void ValidateSampleData( + ReadOnlySpan sampleData, + bool isSyncSample, + HeifContentLightLevel? sampleContentLightLevel, + HeifMasteringDisplayColorVolume? sampleMasteringDisplayColorVolume, + DecoderOptions options, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + => this.ValidateData( + sampleData, + isSyncSample, + "AV1 track sample", + sampleContentLightLevel, + sampleMasteringDisplayColorVolume, + options, + out contentLightLevel, + out masteringDisplayColorVolume); + + /// + /// Validates one bounded AV1 payload while applying the item or track sequence-header requirement. + /// + /// The complete bounded AV1 payload. + /// Indicates that exactly one sequence header is required. + /// The source description used by invalid-content errors. + /// The content light-level property associated with the payload. + /// The mastering-display property associated with the payload. + /// The general options governing metadata validation. + /// Receives validated OBU content light-level metadata. + /// Receives validated OBU mastering-display metadata. + private void ValidateData( + ReadOnlySpan data, + bool sequenceHeaderRequired, + string sourceName, + HeifContentLightLevel? containerContentLightLevel, + HeifMasteringDisplayColorVolume? containerMasteringDisplayColorVolume, + DecoderOptions options, out HeifContentLightLevel? contentLightLevel, out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) { int sequenceHeaderCount = ScanObus( - itemData, + data, false, false, - "AV1 image item", - out int itemSequenceHeaderOffset, - out int itemSequenceHeaderLength, - out int itemSequenceHeaderExtension, - out HeifContentLightLevel? itemObuContentLightLevel, - out HeifMasteringDisplayColorVolume? itemObuMasteringDisplayColorVolume); - - if (sequenceHeaderCount != 1) + sourceName, + options, + out int dataSequenceHeaderOffset, + out int dataSequenceHeaderLength, + out int dataSequenceHeaderExtension, + out HeifContentLightLevel? dataObuContentLightLevel, + out HeifMasteringDisplayColorVolume? dataObuMasteringDisplayColorVolume); + + if (sequenceHeaderCount > 1 || (sequenceHeaderRequired && sequenceHeaderCount != 1)) { - throw new InvalidImageContentException($"The AV1 image item contains {sequenceHeaderCount} sequence header OBUs instead of exactly one."); + string requirement = sequenceHeaderRequired ? "exactly one" : "at most one"; + throw new InvalidImageContentException($"The {sourceName} contains {sequenceHeaderCount} sequence header OBUs instead of {requirement}."); } - if (this.configSequenceHeaderOffset >= 0) + if (this.configSequenceHeaderOffset >= 0 && dataSequenceHeaderOffset >= 0) { ReadOnlySpan configSequenceHeader = this.configObus.AsSpan( this.configSequenceHeaderOffset, this.configSequenceHeaderLength); - ReadOnlySpan itemSequenceHeader = itemData.Slice( - itemSequenceHeaderOffset, - itemSequenceHeaderLength); + ReadOnlySpan dataSequenceHeader = data.Slice( + dataSequenceHeaderOffset, + dataSequenceHeaderLength); // Compare the extension and payload rather than the encoded OBU size. Configuration OBUs must carry a - // size field while an image item's final OBU may omit one, and different legal LEB128 widths do not alter + // size field while a payload's final OBU may omit one, and different legal LEB128 widths do not alter // the Sequence Header OBU being repeated. - if (this.configSequenceHeaderExtension != itemSequenceHeaderExtension - || !configSequenceHeader.SequenceEqual(itemSequenceHeader)) + if (this.configSequenceHeaderExtension != dataSequenceHeaderExtension + || !configSequenceHeader.SequenceEqual(dataSequenceHeader)) { - throw new InvalidImageContentException("The AV1 codec configuration sequence header does not match the image item sequence header."); + throw new InvalidImageContentException( + $"The AV1 codec configuration sequence header does not match the {sourceName} sequence header."); } } - ValidateContentLightLevel( - this.configContentLightLevel, - itemContentLightLevel, - "AV1 codec configuration"); - - ValidateContentLightLevel( - itemObuContentLightLevel, - itemContentLightLevel, - "AV1 image item"); + contentLightLevel = null; + masteringDisplayColorVolume = null; + if (options.SkipMetadata) + { + return; + } - ValidateMasteringDisplayColorVolume( - this.configMasteringDisplayColorVolume, - itemMasteringDisplayColorVolume, - "AV1 codec configuration"); + try + { + ValidateContentLightLevel(this.configContentLightLevel, containerContentLightLevel, "AV1 codec configuration"); + ValidateContentLightLevel(dataObuContentLightLevel, containerContentLightLevel, sourceName); + ValidateMasteringDisplayColorVolume( + this.configMasteringDisplayColorVolume, + containerMasteringDisplayColorVolume, + "AV1 codec configuration"); + + ValidateMasteringDisplayColorVolume( + dataObuMasteringDisplayColorVolume, + containerMasteringDisplayColorVolume, + sourceName); + + if (this.configContentLightLevel is not null + && dataObuContentLightLevel is not null + && !ContentLightLevelsMatch(this.configContentLightLevel.Value, dataObuContentLightLevel.Value)) + { + throw new InvalidImageContentException( + $"The AV1 codec configuration and {sourceName} contain conflicting content light-level metadata."); + } - ValidateMasteringDisplayColorVolume( - itemObuMasteringDisplayColorVolume, - itemMasteringDisplayColorVolume, - "AV1 image item"); + if (this.configMasteringDisplayColorVolume is not null + && dataObuMasteringDisplayColorVolume is not null + && this.configMasteringDisplayColorVolume.Value != dataObuMasteringDisplayColorVolume.Value) + { + throw new InvalidImageContentException( + $"The AV1 codec configuration and {sourceName} contain conflicting mastering-display metadata."); + } - if (this.configContentLightLevel is not null - && itemObuContentLightLevel is not null - && !ContentLightLevelsMatch(this.configContentLightLevel.Value, itemObuContentLightLevel.Value)) - { - throw new InvalidImageContentException("The AV1 codec configuration and image item contain conflicting content light-level metadata."); + // Configuration OBUs precede the payload OBUs, so a payload OBU supplies the effective value when both + // sequences repeat the same metadata type. + contentLightLevel = dataObuContentLightLevel ?? this.configContentLightLevel; + masteringDisplayColorVolume = dataObuMasteringDisplayColorVolume ?? this.configMasteringDisplayColorVolume; } - - if (this.configMasteringDisplayColorVolume is not null - && itemObuMasteringDisplayColorVolume is not null - && this.configMasteringDisplayColorVolume.Value != itemObuMasteringDisplayColorVolume.Value) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex)) { - throw new InvalidImageContentException("The AV1 codec configuration and image item contain conflicting mastering-display metadata."); + // Conflicting optional OBU metadata is discarded without weakening OBU framing or sequence-header checks. } - - // Configuration OBUs precede the image-item OBUs in the combined AV1 stream, so an item OBU supplies the - // effective value when both sequences repeat the same metadata type. - contentLightLevel = itemObuContentLightLevel ?? this.configContentLightLevel; - masteringDisplayColorVolume = itemObuMasteringDisplayColorVolume ?? this.configMasteringDisplayColorVolume; } /// @@ -315,6 +396,7 @@ internal sealed class Av1CodecConfiguration /// Indicates that a sequence-header OBU, when present, must be the first OBU in the sequence. /// /// The source description used by invalid-content errors. + /// The general options governing metadata validation. /// Receives the first sequence-header payload offset, or -1. /// Receives the first sequence-header payload length. /// Receives the first sequence-header extension byte, or -1. @@ -330,6 +412,7 @@ internal sealed class Av1CodecConfiguration bool requireSizeFields, bool sequenceHeaderMustBeFirst, string sourceName, + DecoderOptions options, out int sequenceHeaderOffset, out int sequenceHeaderLength, out int sequenceHeaderExtension, @@ -407,34 +490,41 @@ internal sealed class Av1CodecConfiguration sequenceHeaderExtension = extension; } } - else if (type == ObuType.Metadata) + else if (type == ObuType.Metadata && !options.SkipMetadata) { - ReadHdrMetadata( - data.Slice(offset, payloadLength), - sourceName, - out HeifContentLightLevel? obuContentLightLevel, - out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); - - if (obuContentLightLevel is not null) + try { - if (contentLightLevel is not null - && !ContentLightLevelsMatch(contentLightLevel.Value, obuContentLightLevel.Value)) + ReadHdrMetadata( + data.Slice(offset, payloadLength), + sourceName, + out HeifContentLightLevel? obuContentLightLevel, + out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); + + if (obuContentLightLevel is not null) { - throw new InvalidImageContentException($"The {sourceName} contains conflicting content light-level metadata OBUs."); - } + if (contentLightLevel is not null + && !ContentLightLevelsMatch(contentLightLevel.Value, obuContentLightLevel.Value)) + { + throw new InvalidImageContentException($"The {sourceName} contains conflicting content light-level metadata OBUs."); + } - contentLightLevel = obuContentLightLevel; - } + contentLightLevel = obuContentLightLevel; + } - if (obuMasteringDisplayColorVolume is not null) - { - if (masteringDisplayColorVolume is not null - && masteringDisplayColorVolume.Value != obuMasteringDisplayColorVolume.Value) + if (obuMasteringDisplayColorVolume is not null) { - throw new InvalidImageContentException($"The {sourceName} contains conflicting mastering-display metadata OBUs."); - } + if (masteringDisplayColorVolume is not null + && masteringDisplayColorVolume.Value != obuMasteringDisplayColorVolume.Value) + { + throw new InvalidImageContentException($"The {sourceName} contains conflicting mastering-display metadata OBUs."); + } - masteringDisplayColorVolume = obuMasteringDisplayColorVolume; + masteringDisplayColorVolume = obuMasteringDisplayColorVolume; + } + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex)) + { + // The OBU payload remains bounded by the image-data scan; only its invalid optional metadata is discarded. } } diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index 186db788c..96b21f3f2 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -27,19 +27,22 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder /// /// Decodes the encoded AV1 payload of an image item. /// - /// The configuration that supplies memory allocation and codec services. + /// The general options governing the containing HEIF decode. /// The HEIF item whose encoded payload is being decoded. /// The encoded AV1 payload. /// /// The container color description that supplies unspecified color information in the AV1 sequence header. /// + /// The token used to cancel the payload decode. /// The decoded image. public Image DecodeItemData( - Configuration configuration, + DecoderOptions options, HeifItem item, Span data, - CicpProfile? colorProfile) + CicpProfile? colorProfile, + CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); Av1CodecConfiguration codecConfiguration = item.Av1CodecConfiguration ?? throw new InvalidImageContentException($"AV1 image item {item.Id} has no codec configuration property."); @@ -49,7 +52,8 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder { if (channelBitDepth != (byte)codecConfiguration.BitDepth) { - throw new InvalidImageContentException($"AV1 image item {item.Id} has mismatched pixel-information and codec-configuration bit depths."); + throw new InvalidImageContentException( + $"AV1 image item {item.Id} has mismatched pixel-information and codec-configuration bit depths."); } } } @@ -58,10 +62,11 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder data, item.ContentLightLevel, item.MasteringDisplayColorVolume, + options, out HeifContentLightLevel? obuContentLightLevel, out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); - using Av1Decoder decoder = new(configuration); + using Av1Decoder decoder = new(options.Configuration); Image image = decoder.Decode(data, colorProfile, codecConfiguration); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); metadata.CompressionMethod = this.CompressionMethod; diff --git a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs index 4d74420f4..93b66f5b1 100644 --- a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs @@ -18,11 +18,6 @@ namespace SixLabors.ImageSharp.Formats.Heif; 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. /// @@ -46,7 +41,6 @@ internal class GridHeifItemDecoder : IHeifItemDecoder /// /// 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. @@ -54,13 +48,11 @@ internal class GridHeifItemDecoder : IHeifItemDecoder /// Optional row-major tile identifiers that replace the grid item's own derived-image references. /// public GridHeifItemDecoder( - Configuration configuration, IList items, IList itemLinks, IDictionary> buffers, IReadOnlyList? tileItemIds = null) { - this.configuration = configuration; this.items = items; this.itemLinks = itemLinks; this.buffers = buffers; @@ -80,16 +72,18 @@ internal class GridHeifItemDecoder : IHeifItemDecoder /// /// Decodes the tiles referenced by a grid derived-image item. /// - /// The configuration associated with the containing HEIF decode. + /// The general options governing the containing HEIF decode. /// The grid derived-image item. /// The grid descriptor payload. /// The container color description inherited by tiles that do not declare one. + /// The token used to cancel between tile payloads. /// The image reconstructed from the referenced grid tiles. public Image DecodeItemData( - Configuration configuration, + DecoderOptions options, HeifItem gridItem, Span data, - CicpProfile? colorProfile) + CicpProfile? colorProfile, + CancellationToken cancellationToken) { if (data.Length < 8) { @@ -157,6 +151,7 @@ internal class GridHeifItemDecoder : IHeifItemDecoder Av1CodecConfiguration? av1GridConfiguration = null; foreach (uint id in linked) { + cancellationToken.ThrowIfCancellationRequested(); HeifItem item = this.items.First(item => item.Id == id); if (tileType == default) { @@ -197,10 +192,11 @@ internal class GridHeifItemDecoder : IHeifItemDecoder this.CompressionMethod = decoder.CompressionMethod; Image tile = decoder.DecodeItemData( - this.configuration, + options, item, itemMemory.GetSpan(), - item.CicpProfile ?? colorProfile); + item.CicpProfile ?? colorProfile, + cancellationToken); try { @@ -227,7 +223,7 @@ internal class GridHeifItemDecoder : IHeifItemDecoder throw new InvalidImageContentException("The HEIF image grid edge tiles do not overlap the output canvas."); } - Image result = new(configuration, (int)outputWidth, (int)outputHeight, firstTile.Metadata.DeepClone()); + Image result = new(options.Configuration, (int)outputWidth, (int)outputHeight, firstTile.Metadata.DeepClone()); ImageFrame destination = result.Frames.RootFrame; for (int tileIndex = 0; tileIndex < gridTiles.Count; tileIndex++) { diff --git a/src/ImageSharp/Formats/Heif/HeifDecoder.cs b/src/ImageSharp/Formats/Heif/HeifDecoder.cs index 9769ff1f0..8f7e7a2c3 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoder.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoder.cs @@ -39,6 +39,7 @@ public sealed class HeifDecoder : ImageDecoder HeifDecoderCore decoder = new(options); Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + ScaleToTargetSize(options, image); return image; } diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index d87df9055..fe415b60e 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -30,24 +30,20 @@ internal sealed class HeifDecoderCore : ImageDecoderCore private static readonly object UnknownProperty = new(); /// - /// Defines the dependency order in which recognized metadata children are interpreted. + /// Marks an understood item property whose value was skipped or discarded by decoder policy. /// - private static readonly Heif4CharCode[] MetadataParseOrder = - [ - Heif4CharCode.Hdlr, - Heif4CharCode.Iinf, - Heif4CharCode.Pitm, - Heif4CharCode.Iref, - Heif4CharCode.Iloc, - Heif4CharCode.Iprp, - Heif4CharCode.Idat - ]; + private static readonly object IgnoredProperty = new(); /// /// The general configuration. /// private readonly Configuration configuration; + /// + /// The general options passed to nested coded-image decoders without presentation-level target scaling. + /// + private readonly DecoderOptions payloadOptions; + /// /// The decoded by this decoder instance. /// @@ -101,6 +97,21 @@ internal sealed class HeifDecoderCore : ImageDecoderCore : base(options) { this.configuration = options.Configuration; + + // HEIF owns final presentation resizing and ICC conversion after item/grid composition and container-profile + // selection. Nested codecs retain every other general policy but must not apply either operation independently. + this.payloadOptions = options.TargetSize is null && options.ColorProfileHandling == ColorProfileHandling.Preserve + ? options + : new DecoderOptions + { + Configuration = options.Configuration, + Sampler = options.Sampler, + SkipMetadata = options.SkipMetadata, + MaxFrames = options.MaxFrames, + SegmentIntegrityHandling = options.SegmentIntegrityHandling, + ColorProfileHandling = ColorProfileHandling.Preserve + }; + this.metadata = new ImageMetadata(); this.boxReader = new HeifBoxReader(this.configuration.MemoryAllocator); this.sequenceParser = new HeifSequenceParser(options); @@ -109,6 +120,20 @@ internal sealed class HeifDecoderCore : ImageDecoderCore this.itemLinks = []; } + /// + /// Gets the dependency order in which recognized metadata children are interpreted. + /// + private static ReadOnlySpan MetadataParseOrder => + [ + Heif4CharCode.Hdlr, + Heif4CharCode.Iinf, + Heif4CharCode.Pitm, + Heif4CharCode.Iref, + Heif4CharCode.Iloc, + Heif4CharCode.Iprp, + Heif4CharCode.Idat + ]; + /// protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { @@ -153,7 +178,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } - return this.DecodePrimaryItem(stream); + return this.DecodePrimaryItem(stream, cancellationToken); } /// @@ -401,41 +426,55 @@ internal sealed class HeifDecoderCore : ImageDecoderCore ImageFrame[] frames = new ImageFrame[visibleFrameCount]; sampleIndices = new int[visibleFrameCount]; int decodedFrameCount = 0; - for (int sampleIndex = 0; sampleIndex < track.Samples.Length; sampleIndex++) + try { - HeifSequenceSample sample = track.Samples[sampleIndex]; - if (sample.IsHidden) + for (int sampleIndex = 0; sampleIndex < track.Samples.Length; sampleIndex++) { - continue; + HeifSequenceSample sample = track.Samples[sampleIndex]; + if (sample.IsHidden) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + ImageFrame? frame = null; + this.ExecuteImageDataSegmentAction(() => frame = this.DecodeSequenceFrame(stream, track, sample)); + if (frame is null) + { + continue; + } + + frame.Metadata.GetHeifMetadata().FrameDelay = new Rational(sample.Duration, track.MediaTimescale); + frames[decodedFrameCount] = frame; + sampleIndices[decodedFrameCount] = sampleIndex; + decodedFrameCount++; } - cancellationToken.ThrowIfCancellationRequested(); - ImageFrame? frame = null; - this.ExecuteImageDataSegmentAction(() => frame = this.DecodeSequenceFrame(stream, track, sample)); - if (frame is null) + if (decodedFrameCount == 0) { - continue; + throw new InvalidImageContentException("The HEIF image sequence contains no decodable visible samples."); } - frame.Metadata.GetHeifMetadata().FrameDelay = new Rational(sample.Duration, track.MediaTimescale); - frames[decodedFrameCount] = frame; - sampleIndices[decodedFrameCount] = sampleIndex; - decodedFrameCount++; - } + if (decodedFrameCount != frames.Length) + { + // Compaction occurs only in IgnoreImageData mode after a recoverable coded-sample failure. + Array.Resize(ref frames, decodedFrameCount); + Array.Resize(ref sampleIndices, decodedFrameCount); + } - if (decodedFrameCount == 0) - { - throw new InvalidImageContentException("The HEIF image sequence contains no decodable visible samples."); + return frames; } - - if (decodedFrameCount != frames.Length) + catch { - // Compaction occurs only in IgnoreImageData mode after a recoverable coded-sample failure. - Array.Resize(ref frames, decodedFrameCount); - Array.Resize(ref sampleIndices, decodedFrameCount); - } + // Frames are independently allocated before the final Image adopts them. Retain ownership until this + // method returns so a later sample failure cannot leak the successfully decoded prefix. + for (int frameIndex = 0; frameIndex < decodedFrameCount; frameIndex++) + { + frames[frameIndex].Dispose(); + } - return frames; + throw; + } } /// @@ -465,10 +504,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore stream.Position = sample.Offset; HeifBoxReader.ReadExactly(stream, sampleData, "The HEIF image-sequence sample is truncated."); - codecConfiguration.ValidateItemData( + codecConfiguration.ValidateSampleData( sampleData, + sample.IsSync, track.ContentLightLevel, track.MasteringDisplayColorVolume, + this.Options, out _, out _); @@ -703,7 +744,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore while (stream.Position < endPosition) { long length = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode boxType); - if (Array.IndexOf(MetadataParseOrder, boxType) >= 0) + if (MetadataParseOrder.Contains(boxType)) { // Association and location boxes can precede the item declarations they reference. if (!boxes.TryAdd(boxType, (stream.Position, length))) @@ -1079,180 +1120,277 @@ internal sealed class HeifDecoderCore : ImageDecoderCore while (stream.Position < endPosition) { long itemLength = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode itemType); + if (this.Options.SkipMetadata && itemType is Heif4CharCode.Pasp + or Heif4CharCode.Clli + or Heif4CharCode.Mdcv + or Heif4CharCode.Cclv + or Heif4CharCode.Amve + or Heif4CharCode.Reve + or Heif4CharCode.Ndwt) + { + // These properties affect only exposed image metadata. Preserve their physical ipco positions while + // avoiding payload allocation and validation when the caller requested no metadata. + HeifBoxReader.Skip(stream, itemLength); + properties.Add(new KeyValuePair(itemType, IgnoredProperty)); + continue; + } + + if (this.Options.SkipMetadata && itemType == Heif4CharCode.Colr && itemLength >= 4) + { + Span profileTypeBuffer = this.boxHeaderScratch.AsSpan(0, 4); + HeifBoxReader.ReadExactly(stream, profileTypeBuffer, "The HEIF color-information property is truncated."); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(profileTypeBuffer); + if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) + { + // ICC bytes cannot affect reconstruction when metadata is skipped. Retain only the property index + // and leave the potentially large profile payload out of the allocator entirely. + HeifBoxReader.Skip(stream, itemLength - 4); + properties.Add(new KeyValuePair(Heif4CharCode.Colr, IgnoredProperty)); + continue; + } + + stream.Position -= 4; + } + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, itemLength); Span boxBuffer = boxMemory.GetSpan(); - switch (itemType) + try { - case Heif4CharCode.Ispe: - EnsureBufferRemaining(boxBuffer, 0, 12, "image spatial extents"); + switch (itemType) + { + case Heif4CharCode.Ispe: + EnsureBufferRemaining(boxBuffer, 0, 12, "image spatial extents"); - // The full-box header precedes the unsigned display width and height. - uint width = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); - uint height = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[8..]); - if (width is 0 or > int.MaxValue || height is 0 or > int.MaxValue) - { - throw new InvalidImageContentException("The image spatial extents property has invalid dimensions."); - } + // The full-box header precedes the unsigned display width and height. + uint width = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); + uint height = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[8..]); + if (width is 0 or > int.MaxValue || height is 0 or > int.MaxValue) + { + throw new InvalidImageContentException("The image spatial extents property has invalid dimensions."); + } - properties.Add(new KeyValuePair(Heif4CharCode.Ispe, new Size((int)width, (int)height))); - break; - case Heif4CharCode.Pasp: - properties.Add( - new KeyValuePair( - Heif4CharCode.Pasp, - HeifPropertyParser.ParsePixelAspectRatio(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Ispe, new Size((int)width, (int)height))); + break; + case Heif4CharCode.Pasp: + object pixelAspectRatio = IgnoredProperty; + try + { + pixelAspectRatio = HeifPropertyParser.ParsePixelAspectRatio(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Pixi: - EnsureBufferRemaining(boxBuffer, 0, 5, "pixel information"); - if (boxBuffer[0] != 0 || boxBuffer[1] != 0 || boxBuffer[2] != 0 || boxBuffer[3] != 0) - { - throw new InvalidImageContentException("The pixel information property has an unsupported version or flags."); - } + properties.Add(new KeyValuePair(Heif4CharCode.Pasp, pixelAspectRatio)); + break; + case Heif4CharCode.Pixi: + EnsureBufferRemaining(boxBuffer, 0, 5, "pixel information"); + if (boxBuffer[0] != 0 || boxBuffer[1] != 0 || boxBuffer[2] != 0 || boxBuffer[3] != 0) + { + throw new InvalidImageContentException("The pixel information property has an unsupported version or flags."); + } - // The full-box header precedes one bit-depth byte for each channel. - int channelCount = boxBuffer[4]; - if (channelCount == 0) - { - throw new InvalidImageContentException("The pixel information property has no channels."); - } + // The full-box header precedes one bit-depth byte for each channel. + int channelCount = boxBuffer[4]; + if (channelCount == 0) + { + throw new InvalidImageContentException("The pixel information property has no channels."); + } - int offset = 5; - EnsureBufferRemaining(boxBuffer, offset, channelCount, "pixel information"); - if (boxBuffer.Length != offset + channelCount) - { - throw new InvalidImageContentException("The pixel information property contains unexpected trailing data."); - } + int offset = 5; + EnsureBufferRemaining(boxBuffer, offset, channelCount, "pixel information"); + if (boxBuffer.Length != offset + channelCount) + { + throw new InvalidImageContentException("The pixel information property contains unexpected trailing data."); + } - byte[] channelBitDepths = boxBuffer.Slice(offset, channelCount).ToArray(); - for (int i = 0; i < channelBitDepths.Length; i++) - { - if (channelBitDepths[i] == 0) + byte[] channelBitDepths = boxBuffer.Slice(offset, channelCount).ToArray(); + for (int i = 0; i < channelBitDepths.Length; i++) { - throw new InvalidImageContentException($"The pixel information property declares zero precision for channel {i}."); + if (channelBitDepths[i] == 0) + { + throw new InvalidImageContentException($"The pixel information property declares zero precision for channel {i}."); + } } - } - properties.Add(new KeyValuePair(Heif4CharCode.Pixi, channelBitDepths)); + properties.Add(new KeyValuePair(Heif4CharCode.Pixi, channelBitDepths)); - break; - case Heif4CharCode.AuxC: - EnsureBufferRemaining(boxBuffer, 0, 5, "auxiliary type"); - if (boxBuffer[0] != 0) - { - throw new InvalidImageContentException($"The auxiliary type property has unsupported version {boxBuffer[0]}."); - } + break; + case Heif4CharCode.AuxC: + EnsureBufferRemaining(boxBuffer, 0, 5, "auxiliary type"); + if (boxBuffer[0] != 0) + { + throw new InvalidImageContentException($"The auxiliary type property has unsupported version {boxBuffer[0]}."); + } - // aux_type is a required null-terminated string. Any remaining bytes are the registered - // auxiliary subtype payload, which is not needed to identify an alpha image plane. - string auxiliaryType = ReadNullTerminatedString(boxBuffer[4..], out _); - properties.Add(new KeyValuePair(Heif4CharCode.AuxC, auxiliaryType)); - break; - case Heif4CharCode.Colr: - EnsureBufferRemaining(boxBuffer, 0, 4, "color information"); - Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); - object colorInformation = UnknownProperty; - if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) - { - EnsureBufferRemaining(boxBuffer, 4, 1, "ICC color information"); - byte[] iccData = boxBuffer[4..].ToArray(); - IccProfile? iccProfile = null; - this.ExecuteAncillarySegmentAction(() => iccProfile = HeifPropertyParser.ParseIccProfile(iccData)); - - // A malformed ancillary profile can be ignored by policy while the physical property still - // occupies its ipco index and remains understood for essential-association handling. - colorInformation = iccProfile ?? new object(); - } - else if (profileType == Heif4CharCode.Nclx) - { - colorInformation = HeifPropertyParser.ParseCicpProfile(boxBuffer[4..]); - } + // aux_type is a required null-terminated string. Any remaining bytes are the registered + // auxiliary subtype payload, which is not needed to identify an alpha image plane. + string auxiliaryType = ReadNullTerminatedString(boxBuffer[4..], out _); + properties.Add(new KeyValuePair(Heif4CharCode.AuxC, auxiliaryType)); + break; + case Heif4CharCode.Colr: + EnsureBufferRemaining(boxBuffer, 0, 4, "color information"); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); + object colorInformation = UnknownProperty; + if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) + { + if (!this.Options.SkipMetadata) + { + EnsureBufferRemaining(boxBuffer, 4, 1, "ICC color information"); + byte[] iccData = boxBuffer[4..].ToArray(); + IccProfile? iccProfile = null; + try + { + iccProfile = HeifPropertyParser.ParseIccProfile(iccData); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + // A malformed ancillary profile can be ignored by policy while the physical property still + // occupies its ipco index and remains understood for essential-association handling. + colorInformation = iccProfile ?? IgnoredProperty; + } + else + { + colorInformation = IgnoredProperty; + } + } + else if (profileType == Heif4CharCode.Nclx) + { + colorInformation = HeifPropertyParser.ParseCicpProfile(boxBuffer[4..]); + } - properties.Add(new KeyValuePair(Heif4CharCode.Colr, colorInformation)); + properties.Add(new KeyValuePair(Heif4CharCode.Colr, colorInformation)); - break; - case Heif4CharCode.Clli: - properties.Add( - new KeyValuePair( - Heif4CharCode.Clli, - HeifPropertyParser.ParseContentLightLevel(boxBuffer))); + break; + case Heif4CharCode.Clli: + object contentLightLevel = IgnoredProperty; + try + { + contentLightLevel = HeifPropertyParser.ParseContentLightLevel(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Mdcv: - properties.Add( - new KeyValuePair( - Heif4CharCode.Mdcv, - HeifPropertyParser.ParseMasteringDisplayColorVolume(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Clli, contentLightLevel)); + break; + case Heif4CharCode.Mdcv: + object masteringDisplayColorVolume = IgnoredProperty; + try + { + masteringDisplayColorVolume = HeifPropertyParser.ParseMasteringDisplayColorVolume(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Cclv: - properties.Add( - new KeyValuePair( - Heif4CharCode.Cclv, - HeifPropertyParser.ParseContentColorVolume(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Mdcv, masteringDisplayColorVolume)); + break; + case Heif4CharCode.Cclv: + object contentColorVolume = IgnoredProperty; + try + { + contentColorVolume = HeifPropertyParser.ParseContentColorVolume(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Amve: - properties.Add( - new KeyValuePair( - Heif4CharCode.Amve, - HeifPropertyParser.ParseAmbientViewingEnvironment(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Cclv, contentColorVolume)); + break; + case Heif4CharCode.Amve: + object ambientViewingEnvironment = IgnoredProperty; + try + { + ambientViewingEnvironment = HeifPropertyParser.ParseAmbientViewingEnvironment(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Reve: - properties.Add( - new KeyValuePair( - Heif4CharCode.Reve, - HeifPropertyParser.ParseReferenceViewingEnvironment(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Amve, ambientViewingEnvironment)); + break; + case Heif4CharCode.Reve: + object referenceViewingEnvironment = IgnoredProperty; + try + { + referenceViewingEnvironment = HeifPropertyParser.ParseReferenceViewingEnvironment(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Ndwt: - properties.Add( - new KeyValuePair( - Heif4CharCode.Ndwt, - HeifPropertyParser.ParseNominalDiffuseWhite(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Reve, referenceViewingEnvironment)); + break; + case Heif4CharCode.Ndwt: + object nominalDiffuseWhite = IgnoredProperty; + try + { + nominalDiffuseWhite = HeifPropertyParser.ParseNominalDiffuseWhite(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } - break; - case Heif4CharCode.Av1C: - EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration"); - properties.Add( - new KeyValuePair( - Heif4CharCode.Av1C, - new Av1CodecConfiguration(boxBuffer))); + properties.Add(new KeyValuePair(Heif4CharCode.Ndwt, nominalDiffuseWhite)); + break; + case Heif4CharCode.Av1C: + EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration"); + properties.Add( + new KeyValuePair( + Heif4CharCode.Av1C, + new Av1CodecConfiguration(boxBuffer, this.Options))); - break; - case Heif4CharCode.HvcC: - properties.Add( - new KeyValuePair( - Heif4CharCode.HvcC, - new HevcCodecConfiguration(boxBuffer))); + break; + case Heif4CharCode.HvcC: + properties.Add( + new KeyValuePair( + Heif4CharCode.HvcC, + new HevcCodecConfiguration(boxBuffer))); - break; - case Heif4CharCode.Clap: - properties.Add( - new KeyValuePair( - Heif4CharCode.Clap, - HeifPropertyParser.ParseCleanAperture(boxBuffer))); + break; + case Heif4CharCode.Clap: + properties.Add( + new KeyValuePair( + Heif4CharCode.Clap, + HeifPropertyParser.ParseCleanAperture(boxBuffer))); - break; - case Heif4CharCode.Irot: - properties.Add(new KeyValuePair(Heif4CharCode.Irot, HeifPropertyParser.ParseRotation(boxBuffer))); - break; - case Heif4CharCode.Imir: - properties.Add(new KeyValuePair(Heif4CharCode.Imir, HeifPropertyParser.ParseMirrorAxis(boxBuffer))); - break; - case Heif4CharCode.Altt: - case Heif4CharCode.Iscl: - case Heif4CharCode.Rloc: - case Heif4CharCode.Udes: - // 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: - // Unknown properties still occupy an ipco index and become an error only when marked essential. - properties.Add(new KeyValuePair(itemType, UnknownProperty)); - break; + break; + case Heif4CharCode.Irot: + properties.Add(new KeyValuePair(Heif4CharCode.Irot, HeifPropertyParser.ParseRotation(boxBuffer))); + break; + case Heif4CharCode.Imir: + properties.Add(new KeyValuePair(Heif4CharCode.Imir, HeifPropertyParser.ParseMirrorAxis(boxBuffer))); + break; + case Heif4CharCode.Altt: + case Heif4CharCode.Iscl: + case Heif4CharCode.Rloc: + case Heif4CharCode.Udes: + // 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, IgnoredProperty)); + break; + default: + // Unknown properties still occupy an ipco index and become an error only when marked essential. + properties.Add(new KeyValuePair(itemType, UnknownProperty)); + break; + } + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.Options, ex)) + { + // Invalid image properties retain their physical association index. Typed association handling ignores + // the placeholder so another decodable item or the coded-image defaults can remain usable. + properties.Add(new KeyValuePair(itemType, IgnoredProperty)); } } } @@ -1337,81 +1475,129 @@ internal sealed class HeifDecoderCore : ImageDecoderCore if (!essential && prop.Key is Heif4CharCode.Clap or Heif4CharCode.Irot or Heif4CharCode.Imir) { - throw new InvalidImageContentException($"Item {itemId} associates nonessential transformative property '{prop.Key}'."); + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates nonessential transformative property '{prop.Key}'."); + + continue; } switch (prop.Key) { case Heif4CharCode.Ispe: - item.SetExtent((Size)prop.Value); + if (prop.Value is Size extent) + { + item.SetExtent(extent); + } + break; case Heif4CharCode.Pasp: - if (item.PixelAspectRatio is not null) + if (prop.Value is HeifPixelAspectRatio pixelAspectRatio) { - throw new InvalidImageContentException($"Item {itemId} associates more than one pixel aspect ratio property."); + if (item.PixelAspectRatio is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one pixel aspect ratio property."); + + break; + } + + item.PixelAspectRatio = pixelAspectRatio; } - item.PixelAspectRatio = (HeifPixelAspectRatio)prop.Value; break; case Heif4CharCode.Pixi: - if (item.ChannelBitDepths is not null) + if (prop.Value is byte[] channelBitDepths) { - throw new InvalidImageContentException($"Item {itemId} associates more than one pixel information property."); - } + if (item.ChannelBitDepths is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one pixel information property."); - byte[] channelBitDepths = (byte[])prop.Value; - int bitsPerPixel = 0; - for (int channel = 0; channel < channelBitDepths.Length; channel++) - { - bitsPerPixel += channelBitDepths[channel]; + break; + } + + int bitsPerPixel = 0; + for (int channel = 0; channel < channelBitDepths.Length; channel++) + { + bitsPerPixel += channelBitDepths[channel]; + } + + item.ChannelCount = channelBitDepths.Length; + item.ChannelBitDepths = channelBitDepths; + item.BitsPerPixel = bitsPerPixel; } - item.ChannelCount = channelBitDepths.Length; - item.ChannelBitDepths = channelBitDepths; - item.BitsPerPixel = bitsPerPixel; break; case Heif4CharCode.Av1C: - if (item.Type != Heif4CharCode.Av01) + if (prop.Value is Av1CodecConfiguration av1CodecConfiguration) { - throw new InvalidImageContentException( - $"Item {itemId} associates an AV1 codec configuration with non-AV1 item type '{item.Type}'."); - } + if (item.Type != Heif4CharCode.Av01) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates an AV1 codec configuration with non-AV1 item type '{item.Type}'."); - if (item.Av1CodecConfiguration is not null) - { - throw new InvalidImageContentException($"Item {itemId} associates more than one AV1 codec configuration property."); + break; + } + + if (item.Av1CodecConfiguration is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one AV1 codec configuration property."); + + break; + } + + item.Av1CodecConfiguration = av1CodecConfiguration; } - item.Av1CodecConfiguration = (Av1CodecConfiguration)prop.Value; break; case Heif4CharCode.HvcC: - if (item.Type != Heif4CharCode.Hvc1) + if (prop.Value is HevcCodecConfiguration hevcCodecConfiguration) { - throw new InvalidImageContentException( - $"Item {itemId} associates an HEVC codec configuration with non-HEVC item type '{item.Type}'."); - } + if (item.Type != Heif4CharCode.Hvc1) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates an HEVC codec configuration with non-HEVC item type '{item.Type}'."); - if (item.HevcCodecConfiguration is not null) - { - throw new InvalidImageContentException($"Item {itemId} associates more than one HEVC codec configuration property."); + break; + } + + if (item.HevcCodecConfiguration is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one HEVC codec configuration property."); + + break; + } + + item.HevcCodecConfiguration = hevcCodecConfiguration; } - item.HevcCodecConfiguration = (HevcCodecConfiguration)prop.Value; break; case Heif4CharCode.AuxC: - if (item.AuxiliaryType is not null) + if (prop.Value is string auxiliaryType) { - throw new InvalidImageContentException($"Item {itemId} associates more than one auxiliary type property."); + if (item.AuxiliaryType is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one auxiliary type property."); + + break; + } + + item.AuxiliaryType = auxiliaryType; } - item.AuxiliaryType = (string)prop.Value; break; case Heif4CharCode.Colr: if (prop.Value is IccProfile iccProfile) { if (item.IccProfile is not null) { - throw new InvalidImageContentException($"Item {itemId} associates more than one ICC color property."); + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one ICC color property."); + + break; } item.IccProfile = iccProfile; @@ -1420,7 +1606,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore { if (item.CicpProfile is not null) { - throw new InvalidImageContentException($"Item {itemId} associates more than one CICP color property."); + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one CICP color property."); + + break; } item.CicpProfile = cicpProfile; @@ -1428,76 +1617,139 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Clli: - if (item.ContentLightLevel is not null) + if (prop.Value is HeifContentLightLevel contentLightLevel) { - throw new InvalidImageContentException($"Item {itemId} associates more than one content light level property."); + if (item.ContentLightLevel is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one content light level property."); + + break; + } + + item.ContentLightLevel = contentLightLevel; } - item.ContentLightLevel = (HeifContentLightLevel)prop.Value; break; case Heif4CharCode.Mdcv: - if (item.MasteringDisplayColorVolume is not null) + if (prop.Value is HeifMasteringDisplayColorVolume masteringDisplayColorVolume) { - throw new InvalidImageContentException($"Item {itemId} associates more than one mastering display color-volume property."); + if (item.MasteringDisplayColorVolume is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one mastering display color-volume property."); + + break; + } + + item.MasteringDisplayColorVolume = masteringDisplayColorVolume; } - item.MasteringDisplayColorVolume = (HeifMasteringDisplayColorVolume)prop.Value; break; case Heif4CharCode.Cclv: - if (item.ContentColorVolume is not null) + if (prop.Value is HeifContentColorVolume contentColorVolume) { - throw new InvalidImageContentException($"Item {itemId} associates more than one content color-volume property."); + if (item.ContentColorVolume is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one content color-volume property."); + + break; + } + + item.ContentColorVolume = contentColorVolume; } - item.ContentColorVolume = (HeifContentColorVolume)prop.Value; break; case Heif4CharCode.Amve: - if (item.AmbientViewingEnvironment is not null) + if (prop.Value is HeifAmbientViewingEnvironment ambientViewingEnvironment) { - throw new InvalidImageContentException($"Item {itemId} associates more than one ambient viewing-environment property."); + if (item.AmbientViewingEnvironment is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one ambient viewing-environment property."); + + break; + } + + item.AmbientViewingEnvironment = ambientViewingEnvironment; } - item.AmbientViewingEnvironment = (HeifAmbientViewingEnvironment)prop.Value; break; case Heif4CharCode.Reve: - if (item.ReferenceViewingEnvironment is not null) + if (prop.Value is HeifReferenceViewingEnvironment referenceViewingEnvironment) { - throw new InvalidImageContentException($"Item {itemId} associates more than one reference viewing-environment property."); + if (item.ReferenceViewingEnvironment is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one reference viewing-environment property."); + + break; + } + + item.ReferenceViewingEnvironment = referenceViewingEnvironment; } - item.ReferenceViewingEnvironment = (HeifReferenceViewingEnvironment)prop.Value; break; case Heif4CharCode.Ndwt: - if (item.NominalDiffuseWhite is not null) + if (prop.Value is HeifNominalDiffuseWhite nominalDiffuseWhite) { - throw new InvalidImageContentException($"Item {itemId} associates more than one nominal diffuse-white property."); + if (item.NominalDiffuseWhite is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one nominal diffuse-white property."); + + break; + } + + item.NominalDiffuseWhite = nominalDiffuseWhite; } - item.NominalDiffuseWhite = (HeifNominalDiffuseWhite)prop.Value; break; case Heif4CharCode.Clap: - if (item.CleanAperture is not null) + if (prop.Value is HeifCleanAperture cleanAperture) { - throw new InvalidImageContentException($"Item {itemId} associates more than one clean aperture property."); + if (item.CleanAperture is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one clean aperture property."); + + break; + } + + item.CleanAperture = cleanAperture; } - item.CleanAperture = (HeifCleanAperture)prop.Value; break; case Heif4CharCode.Irot: - if (item.RotationAngle is not null) + if (prop.Value is byte rotationAngle) { - throw new InvalidImageContentException($"Item {itemId} associates more than one image rotation property."); + if (item.RotationAngle is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one image rotation property."); + + break; + } + + item.RotationAngle = rotationAngle; } - item.RotationAngle = (byte)prop.Value; break; case Heif4CharCode.Imir: - if (item.MirrorAxis is not null) + if (prop.Value is byte mirrorAxis) { - throw new InvalidImageContentException($"Item {itemId} associates more than one image mirror property."); + if (item.MirrorAxis is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one image mirror property."); + + break; + } + + item.MirrorAxis = mirrorAxis; } - item.MirrorAxis = (byte)prop.Value; break; } } @@ -1687,78 +1939,104 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// /// The destination pixel format. /// The complete seekable HEIF container stream. + /// The token used to cancel item assembly and payload decoding. /// The image reconstructed from the selected item. - private Image DecodePrimaryItem(BufferedReadStream stream) + private Image DecodePrimaryItem(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using DisposableDictionary> buffers = new(this.items.Count); foreach (HeifItem item in this.items) { - long itemLength = 0; - foreach (HeifLocation loc in item.DataLocations) - { - if (loc.Length < 0 || itemLength > int.MaxValue - loc.Length) - { - throw new InvalidImageContentException($"Item {item.Id} data is too large to buffer."); - } - - itemLength += loc.Length; - } - - if (itemLength == 0) + cancellationToken.ThrowIfCancellationRequested(); + bool isMetadataItem = item.Type is Heif4CharCode.Exif or Heif4CharCode.Mime; + if (this.Options.SkipMetadata && isMetadataItem) { + // Metadata items are not codec inputs. Leave their extents on the stream when metadata loading is disabled. 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) + IMemoryOwner? extentMemory = null; + try { - if (loc.BaseOffset < 0 || loc.Offset < 0 || loc.BaseOffset > long.MaxValue - loc.Offset) + long itemLength = 0; + foreach (HeifLocation loc in item.DataLocations) { - throw new InvalidImageContentException($"Item {item.Id} has an invalid extent offset."); + if (loc.Length < 0 || itemLength > int.MaxValue - loc.Length) + { + throw new InvalidImageContentException($"Item {item.Id} data is too large to buffer."); + } + + itemLength += loc.Length; } - long relativeOffset = loc.BaseOffset + loc.Offset; - long sourceOffset; - long sourceBytesRemaining; - if (loc.Origin == HeifLocationOffsetOrigin.FileOffset) + if (itemLength == 0) { - // Construction method zero resolves base_offset + extent_offset from the start of the file. - sourceOffset = relativeOffset; - sourceBytesRemaining = stream.Length - sourceOffset; + continue; } - else if (loc.Origin == HeifLocationOffsetOrigin.ItemDataOffset) + + // 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; + extentMemory = this.configuration.MemoryAllocator.Allocate(bufferLength); + Span itemBuffer = extentMemory.GetSpan()[..bufferLength]; + int writeOffset = 0; + foreach (HeifLocation loc in item.DataLocations) { - if (this.itemDataOffset < 0 || relativeOffset > this.itemDataLength) + if (loc.BaseOffset < 0 || loc.Offset < 0 || loc.BaseOffset > long.MaxValue - loc.Offset) { - throw new InvalidImageContentException($"Item {item.Id} has an extent outside its item data box."); + throw new InvalidImageContentException($"Item {item.Id} has an invalid extent offset."); } - // 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."); - } + 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."); + } - HeifBoxReader.EnsureInsideParent(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."); + // 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."); + } + + HeifBoxReader.EnsureInsideParent(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; } - writeOffset += extentLength; + buffers.Add(item.Id, extentMemory); + extentMemory = null; + } + catch (Exception ex) when (isMetadataItem && ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // A failed optional metadata extent is discarded without weakening image-item extent validation. + extentMemory?.Dispose(); + } + catch + { + // The dictionary takes ownership only after every declared extent has been assembled successfully. + extentMemory?.Dispose(); + throw; } } @@ -1787,10 +2065,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore throw new ImageFormatException("No decodable item found inside this HEIF container."); } - Image image = this.DecodeImageItem(itemToDecode, itemDecoder, buffers); + Image image = this.DecodeImageItem(itemToDecode, itemDecoder, buffers, cancellationToken); try { - using Image? alphaImage = this.DecodeAlphaPlane(itemToDecode, buffers, out bool alphaPremultiplied); + using Image? alphaImage = this.DecodeAlphaPlane(itemToDecode, buffers, cancellationToken, out bool alphaPremultiplied); if (alphaImage is not null) { this.ApplyAlpha(image, alphaImage, alphaPremultiplied); @@ -2065,7 +2343,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore private IHeifItemDecoder? GetItemDecoder(HeifItem item, DisposableDictionary> buffers) where TPixel : unmanaged, IPixel => item.Type == Heif4CharCode.Grid && this.FindDecodableGridTile(item) is not null - ? new GridHeifItemDecoder(this.configuration, this.items, this.itemLinks, buffers) + ? new GridHeifItemDecoder(this.items, this.itemLinks, buffers) : HeifCompressionFactory.GetDecoder(item.Type); /// @@ -2075,11 +2353,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The image item to decode. /// The decoder selected for the item. /// The assembled item payloads. + /// The token used to cancel the payload decode. /// The decoded image. private Image DecodeImageItem( HeifItem item, IHeifItemDecoder decoder, - DisposableDictionary> buffers) + DisposableDictionary> buffers, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (!buffers.TryGetValue(item.Id, out IMemoryOwner? itemMemory)) @@ -2088,10 +2368,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } Image image = decoder.DecodeItemData( - this.configuration, + this.payloadOptions, item, itemMemory.GetSpan(), - item.CicpProfile); + item.CicpProfile, + cancellationToken); try { @@ -2196,11 +2477,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// /// The color image item whose alpha plane is requested. /// The assembled item payloads. + /// The token used to cancel the auxiliary payload decode. /// Indicates whether the color samples are premultiplied by the decoded alpha. /// The normalized 16-bit alpha plane, or when the item has no alpha auxiliary. private Image? DecodeAlphaPlane( HeifItem colorItem, DisposableDictionary> buffers, + CancellationToken cancellationToken, out bool premultiplied) { premultiplied = false; @@ -2234,7 +2517,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore && link.SourceId == colorItem.Id && link.DestinationIds.Contains(alphaItem.Id)); - return this.DecodeImageItem(alphaItem, decoder, buffers); + return this.DecodeImageItem(alphaItem, decoder, buffers, cancellationToken); } if (colorItem.Type != Heif4CharCode.Grid) @@ -2256,13 +2539,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore // The color grid descriptor defines the same row/column layout and output canvas for per-tile alpha // auxiliaries. Supplying their IDs lets the existing grid compositor preserve that normative ordering. GridHeifItemDecoder gridDecoder = new( - this.configuration, this.items, this.itemLinks, buffers, alphaTileIds); - return gridDecoder.DecodeItemData(this.configuration, colorItem, gridMemory.GetSpan(), null); + return gridDecoder.DecodeItemData(this.payloadOptions, colorItem, gridMemory.GetSpan(), null, cancellationToken); } /// diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs index d6371ec3a..4bafb3bbb 100644 --- a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -156,7 +156,7 @@ internal sealed class HeifSequenceParser { ValidateAlphaTrack(colorTrack, alphaTrack); } - catch (Exception ex) when (this.ShouldIgnoreImageDataSegmentError(ex)) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex)) { // Alpha is optional image data. IgnoreImageData permits a malformed auxiliary sequence to be omitted while // retaining the independently decodable color presentation. @@ -283,7 +283,7 @@ internal sealed class HeifSequenceParser stream.Position = metadata.Offset; track.Metadata = this.metadataParser.Parse(stream, metadata.Length, scratch); } - catch (Exception ex) when (this.ShouldIgnoreAncillarySegmentError(ex)) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) { // The validated parent range lets decoding continue safely without this optional metadata box. } @@ -889,7 +889,7 @@ internal sealed class HeifSequenceParser using (IMemoryOwner configuration = this.boxReader.ReadPayload(stream, childLength)) { - track.Av1CodecConfiguration = new Av1CodecConfiguration(configuration.GetSpan()); + track.Av1CodecConfiguration = new Av1CodecConfiguration(configuration.GetSpan(), this.options); } configurationSeen = true; @@ -941,7 +941,7 @@ internal sealed class HeifSequenceParser { ParseTrackImageProperty(stream, childLength, childType, track, scratch); } - catch (Exception ex) when (this.ShouldIgnoreAncillarySegmentError(ex)) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) { // The complete child range remains known, so optional metadata can be discarded safely. } @@ -955,7 +955,7 @@ internal sealed class HeifSequenceParser { ParseTrackImageProperty(stream, childLength, childType, track, scratch); } - catch (Exception ex) when (this.ShouldIgnoreImageDataSegmentError(ex)) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex)) { // IgnoreImageData permits a recoverable presentation property to be omitted. } @@ -1103,7 +1103,7 @@ internal sealed class HeifSequenceParser prefix = ReadPrefixFromStart(stream, boxLength, scratch, 11, "color information"); track.CicpProfile = HeifPropertyParser.ParseCicpProfile(prefix[4..]); } - catch (Exception ex) when (this.ShouldIgnoreImageDataSegmentError(ex)) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex)) { // IgnoreImageData permits the decoder to fall back to the coded sequence's color description. } @@ -1127,7 +1127,7 @@ internal sealed class HeifSequenceParser byte[] profileData = payload.GetSpan()[4..].ToArray(); track.IccProfile = HeifPropertyParser.ParseIccProfile(profileData); } - catch (Exception ex) when (this.ShouldIgnoreAncillarySegmentError(ex)) + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) { // A malformed optional ICC profile does not invalidate the coded image outside strict mode. } @@ -2249,22 +2249,6 @@ internal sealed class HeifSequenceParser return (int)size; } - /// - /// Determines whether a recoverable ancillary-segment error should be ignored by the configured decoder policy. - /// - /// The exception raised while parsing the ancillary segment. - /// when decoding may continue without the segment. - private bool ShouldIgnoreAncillarySegmentError(Exception exception) - => this.options.SegmentIntegrityHandling is not SegmentIntegrityHandling.Strict && ImageDecoderCore.IsRecoverableSegmentError(exception); - - /// - /// Determines whether a recoverable image-data-segment error should be ignored by the configured decoder policy. - /// - /// The exception raised while parsing the image-data segment. - /// when decoding may continue without the segment. - private bool ShouldIgnoreImageDataSegmentError(Exception exception) - => this.options.SegmentIntegrityHandling is SegmentIntegrityHandling.IgnoreImageData && ImageDecoderCore.IsRecoverableSegmentError(exception); - /// /// Records one unique child box while retaining only its stream range. /// diff --git a/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs index 1c096c803..9835a4708 100644 --- a/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs @@ -26,16 +26,18 @@ internal interface IHeifItemDecoder /// /// Decodes the compressed payload of an image item. /// - /// The configuration that supplies memory allocation and codec services. + /// The general options governing the containing HEIF decode. /// The HEIF item whose encoded payload is being decoded. /// The encoded image payload. /// /// The container color description that overrides matching color information in the encoded image payload. /// + /// The token used to cancel the payload decode. /// The decoded image. public Image DecodeItemData( - Configuration configuration, + DecoderOptions options, HeifItem item, Span data, - CicpProfile? colorProfile); + CicpProfile? colorProfile, + CancellationToken cancellationToken); } diff --git a/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs index c83b6bc81..831f62c16 100644 --- a/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; @@ -26,20 +27,26 @@ internal class JpegHeifItemDecoder : IHeifItemDecoder /// /// Decodes the encoded JPEG payload of an image item. /// - /// The configuration associated with the containing HEIF decode. + /// The general options governing the containing HEIF decode. /// The HEIF item whose encoded payload is being decoded. /// The encoded JPEG payload. /// The container color description associated with the image item. + /// The token used to cancel the payload decode. /// The decoded image. - public Image DecodeItemData( - Configuration configuration, + public unsafe Image DecodeItemData( + DecoderOptions options, HeifItem item, Span data, - CicpProfile? colorProfile) + CicpProfile? colorProfile, + CancellationToken cancellationToken) { // The JPEG decoder owns the payload's JPEG color coding. The containing decoder attaches HEIF CICP as // presentation metadata after payload decode, so it must not be mistaken for JPEG component-transform syntax. - Image image = Image.Load(data); - return image; + fixed (byte* dataPointer = data) + { + using UnmanagedMemoryStream stream = new(dataPointer, data.Length); + using JpegDecoderCore decoder = new(new JpegDecoderOptions { GeneralOptions = options }); + return decoder.Decode(options.Configuration, stream, cancellationToken); + } } } diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs index a3338f12d..a5b238c58 100644 --- a/src/ImageSharp/Formats/ImageDecoderCore.cs +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -39,7 +39,7 @@ internal abstract class ImageDecoderCore /// The action. protected void ExecuteAncillarySegmentAction(Action action) { - if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict) + if (!ShouldIgnoreAncillarySegmentErrors(this.Options)) { action(); return; @@ -61,7 +61,7 @@ internal abstract class ImageDecoderCore /// The action. protected void ExecuteImageDataSegmentAction(Action action) { - if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData) + if (!ShouldIgnoreImageDataSegmentErrors(this.Options)) { action(); return; @@ -89,10 +89,44 @@ internal abstract class ImageDecoderCore or InvalidOperationException or NotSupportedException; + /// + /// Determines whether the configured policy permits recoverable ancillary-segment errors to be ignored. + /// + /// The general decoder options. + /// when recoverable ancillary-segment errors may be ignored. + public static bool ShouldIgnoreAncillarySegmentErrors(DecoderOptions options) + => options.SegmentIntegrityHandling is not SegmentIntegrityHandling.Strict; + + /// + /// Determines whether the configured policy permits recoverable image-data-segment errors to be ignored. + /// + /// The general decoder options. + /// when recoverable image-data-segment errors may be ignored. + public static bool ShouldIgnoreImageDataSegmentErrors(DecoderOptions options) + => options.SegmentIntegrityHandling is SegmentIntegrityHandling.IgnoreImageData; + + /// + /// Determines whether an ancillary-segment exception may be ignored by the configured decoder policy. + /// + /// The general decoder options. + /// The exception raised while processing an ancillary segment. + /// when decoding may continue without the ancillary segment. + public static bool ShouldIgnoreAncillarySegmentError(DecoderOptions options, Exception exception) + => ShouldIgnoreAncillarySegmentErrors(options) && IsRecoverableSegmentError(exception); + + /// + /// Determines whether an image-data-segment exception may be ignored by the configured decoder policy. + /// + /// The general decoder options. + /// The exception raised while processing an image-data segment. + /// when decoding may continue without the image-data segment. + public static bool ShouldIgnoreImageDataSegmentError(DecoderOptions options, Exception exception) + => ShouldIgnoreImageDataSegmentErrors(options) && IsRecoverableSegmentError(exception); + /// /// Throws unless the decoder is running in a non-strict segment integrity mode. - /// Use this only from within when local control flow - /// must continue after the error. + /// Use this when ancillary parsing must continue locally after the error rather than returning through + /// . /// /// The exception message. protected void ThrowOrIgnoreNonStrictSegmentError(string message) @@ -103,6 +137,18 @@ internal abstract class ImageDecoderCore } } + /// + /// Throws unless the decoder permits recoverable image-data segment errors to be ignored. + /// + /// The exception message. + protected void ThrowOrIgnoreImageDataSegmentError(string message) + { + if (!ShouldIgnoreImageDataSegmentErrors(this.Options)) + { + throw new InvalidImageContentException(message); + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 0762aebcc..8f12ab78e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -4,7 +4,9 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Tests.Formats.Heif; @@ -14,6 +16,16 @@ public class HeifDecoderTests { private const uint UnknownBoxType = 0x74657374U; + private static ReadOnlySpan MalformedJpegApp13 => + [ + 0xFF, 0xED, + 0x00, 0x1D, + (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', + (byte)'0', 0x00, + (byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', + (byte)'!' + ]; + [Theory] [InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 3992, 2992)] [InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 640, 428)] @@ -59,6 +71,90 @@ public class HeifDecoderTests Assert.Equal(new Size(2, 3), image.Size); } + [Fact] + public void DecodeAppliesTargetSizeOnceToThePresentedHeifImage() + { + using Image source = new(64, 48); + for (int y = 0; y < source.Height; y++) + { + Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + row[x] = new Rgba32((byte)(x * 3), (byte)(y * 5), (byte)((x * 7) + (y * 11))); + } + } + + using MemoryStream stream = new(); + source.Save(stream, new HeifEncoder()); + byte[] data = stream.ToArray(); + Size targetSize = new(17, 17); + DecoderOptions options = new() { TargetSize = targetSize }; + + using Image expected = Image.Load(data); + expected.Mutate(context => context.Resize(new ResizeOptions { Size = targetSize, Mode = ResizeMode.Max, Sampler = options.Sampler })); + + using Image image = Image.Load(options, data); + + Assert.Equal(expected.Size, image.Size); + for (int y = 0; y < image.Height; y++) + { + Assert.True(image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y).SequenceEqual( + expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))); + } + } + + [Fact] + public void DecodePropagatesStrictValidationToLegacyJpegItems() + { + byte[] data = CreateContainerWithMalformedJpegMetadata(); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => + { + using Image image = Image.Load(options, data); + }); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void DecodePropagatesRecoverableMetadataValidationToLegacyJpegItems(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithMalformedJpegMetadata(); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + using Image image = Image.Load(options, data); + + Assert.Equal(new Size(2, 3), image.Size); + } + + [Fact] + public void DecodePropagatesSkipMetadataToLegacyJpegItems() + { + byte[] data = CreateContainerWithMalformedJpegMetadata(); + DecoderOptions options = new() + { + SkipMetadata = true, + SegmentIntegrityHandling = SegmentIntegrityHandling.Strict + }; + + using Image image = Image.Load(options, data); + + Assert.Equal(new Size(2, 3), image.Size); + } + + [Fact] + public void DecodePropagatesConfigurationToLegacyJpegItems() + { + byte[] data = CreateEncodedContainer(); + Configuration configuration = Configuration.CreateDefaultInstance(); + DecoderOptions options = new() { Configuration = configuration }; + + using Image image = Image.Load(options, data); + + Assert.Same(configuration, image.Configuration); + } + [Fact] public void IdentifyIgnoresUnknownMetadataBox() { @@ -93,6 +189,115 @@ public class HeifDecoderTests Assert.Contains("essential", exception.Message, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void IdentifyRejectsMalformedAncillaryPropertyInStrictMode() + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void IdentifyIgnoresMalformedAncillaryPropertyWhenPermitted(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + Assert.Equal(PixelResolutionUnit.PixelsPerInch, imageInfo.Metadata.ResolutionUnits); + Assert.Equal(96D, imageInfo.Metadata.HorizontalResolution); + Assert.Equal(96D, imageInfo.Metadata.VerticalResolution); + } + + [Fact] + public void IdentifyDoesNotValidateSkippedAncillaryPropertyMetadata() + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); + DecoderOptions options = new() + { + SkipMetadata = true, + SegmentIntegrityHandling = SegmentIntegrityHandling.Strict + }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void IdentifyRejectsMalformedImagePropertyUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Fact] + public void IdentifyIgnoresMalformedImagePropertyWhenImageDataErrorsAreIgnored() + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyRejectsDuplicateAncillaryPropertyAssociationInStrictMode() + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void IdentifyIgnoresDuplicateAncillaryPropertyAssociationWhenPermitted(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + Assert.Equal(PixelResolutionUnit.AspectRatio, imageInfo.Metadata.ResolutionUnits); + Assert.Equal(1D, imageInfo.Metadata.HorizontalResolution); + Assert.Equal(2D, imageInfo.Metadata.VerticalResolution); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void IdentifyRejectsDuplicateImagePropertyAssociationUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Fact] + public void IdentifyIgnoresDuplicateImagePropertyAssociationWhenImageDataErrorsAreIgnored() + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(3, 2), imageInfo.Size); + } + [Theory] [InlineData(Heif4CharCode.Heic)] [InlineData(Heif4CharCode.Heix)] @@ -357,6 +562,9 @@ public class HeifDecoderTests } private static byte[] CreateContainerWithUnknownProperty(bool essential) + => CreateContainerWithProperty(CreateUnknownBox(), essential); + + private static byte[] CreateContainerWithProperty(ReadOnlySpan property, bool essential) { byte[] data = CreateEncodedContainer(); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); @@ -368,13 +576,13 @@ public class HeifDecoderTests int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); // Insert the property before ipma so its one-based index is 2 and all parent box sizes remain explicit. - data = InsertBytes(data, ipcoOffset + ipcoSize, CreateUnknownBox()); - IncrementBoxSize(data, metaOffset, 8); - IncrementBoxSize(data, iprpOffset, 8); - IncrementBoxSize(data, ipcoOffset, 8); - ipmaOffset += 8; + data = InsertBytes(data, ipcoOffset + ipcoSize, property); + IncrementBoxSize(data, metaOffset, property.Length); + IncrementBoxSize(data, iprpOffset, property.Length); + IncrementBoxSize(data, ipcoOffset, property.Length); + ipmaOffset += property.Length; - // The generated container has one item with one property association; append the unknown property to that entry. + // The generated container has one item with one property association; append the inserted property to that entry. int associationCountOffset = ipmaOffset + 18; data[associationCountOffset]++; int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); @@ -386,11 +594,66 @@ public class HeifDecoderTests return data; } + private static byte[] CreateContainerWithMalformedJpegMetadata() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); + int mediaDataOffset = FindBoxOffset(data, Heif4CharCode.Mdat, 0, data.Length); + + // The generated item uses one file-relative extent. Insert the malformed JPEG application segment after its + // start-of-image marker, then update the enclosing media-data size and the exact declared extent length. + data = InsertBytes(data, mediaDataOffset + 10, MalformedJpegApp13); + IncrementBoxSize(data, mediaDataOffset, MalformedJpegApp13.Length); + uint extentLength = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(itemLocationOffset + 32)); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(itemLocationOffset + 32), extentLength + (uint)MalformedJpegApp13.Length); + return data; + } + + private static byte[] CreateContainerWithDuplicatePropertyAssociation(ReadOnlySpan property, bool essential) + { + byte[] data = CreateContainerWithProperty(property, essential); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); + int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); + int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); + int associationCountOffset = ipmaOffset + 18; + + // Repeat the inserted property's one-based index in the existing item entry without changing box structure. + data[associationCountOffset]++; + int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); + byte association = (byte)(2 | (essential ? 0x80 : 0)); + data = InsertBytes(data, associationOffset, new byte[] { association }); + IncrementBoxSize(data, metaOffset, 1); + IncrementBoxSize(data, iprpOffset, 1); + IncrementBoxSize(data, ipmaOffset, 1); + return data; + } + private static byte[] CreateUnknownBox() + => CreateEmptyBox((Heif4CharCode)UnknownBoxType); + + private static byte[] CreateEmptyBox(Heif4CharCode type) + => CreateBox(type, []); + + private static byte[] CreatePixelAspectRatioBox() + { + byte[] payload = new byte[8]; + BinaryPrimitives.WriteUInt32BigEndian(payload, 2); + BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(4), 1); + return CreateBox(Heif4CharCode.Pasp, payload); + } + + private static byte[] CreateRotationBox() => CreateBox(Heif4CharCode.Irot, [1]); + + private static byte[] CreateBox(Heif4CharCode type, ReadOnlySpan payload) { - byte[] box = new byte[8]; + byte[] box = new byte[8 + payload.Length]; BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); - BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); + BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)type); + payload.CopyTo(box.AsSpan(8)); return box; } diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs index 0c996d434..babb08c76 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs @@ -90,6 +90,31 @@ public class HeifSequenceParserTests } } + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void DecodeRejectsInvalidAv1SampleUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer(source.AsSpan(0x10E, 0x1D), [0x80], source.AsSpan(0xC7, 4), false); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => Image.Load(options, data)); + } + + [Fact] + public void DecodeSkipsInvalidAv1SampleWhenImageDataErrorsAreIgnored() + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer(source.AsSpan(0x10E, 0x1D), [0x80], source.AsSpan(0xC7, 4), false); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using Image image = Image.Load(options, data); + + Assert.Equal(new Size(4, 4), image.Size); + Assert.Single(image.Frames); + } + [Fact] public void DecodeComposesFrameAlignedAv1AlphaSamples() { @@ -445,6 +470,7 @@ public class HeifSequenceParserTests int height = 240, byte[] av1Configuration = null, int? sampleSize = null, + int? secondSampleSize = null, bool allSamplesSync = false, uint premultipliedByTrackId = 0, bool nonIdentityMovieMatrix = false, @@ -501,6 +527,7 @@ public class HeifSequenceParserTests height, av1Configuration, sampleSize, + secondSampleSize, allSamplesSync); EndBox(writer, mediaInformation); @@ -558,8 +585,21 @@ public class HeifSequenceParserTests long mediaInformation = BeginBox(writer, Heif4CharCode.Minf); WriteDataInformation(writer); WriteSampleTable( - writer, alphaChunkOffset ?? chunkOffset, false, false, false, 1, alphaTransforms, false, - width, height, av1Configuration, sampleSize, allSamplesSync, true); + writer, + alphaChunkOffset ?? chunkOffset, + false, + false, + false, + 1, + alphaTransforms, + false, + width, + height, + av1Configuration, + sampleSize, + null, + allSamplesSync, + true); EndBox(writer, mediaInformation); EndBox(writer, media); @@ -593,6 +633,13 @@ public class HeifSequenceParserTests } private static byte[] CreateDecodableAv1SequenceContainer(ReadOnlySpan sample, ReadOnlySpan configuration) + => CreateDecodableAv1SequenceContainer(sample, sample, configuration, true); + + private static byte[] CreateDecodableAv1SequenceContainer( + ReadOnlySpan firstSample, + ReadOnlySpan secondSample, + ReadOnlySpan configuration, + bool allSamplesSync) { const int fileTypeLength = 24; const int movieStorageLength = 2048; @@ -602,10 +649,11 @@ public class HeifSequenceParserTests width: 4, height: 4, av1Configuration: configuration.ToArray(), - sampleSize: sample.Length, - allSamplesSync: true); + sampleSize: firstSample.Length, + secondSampleSize: secondSample.Length, + allSamplesSync: allSamplesSync); - byte[] data = new byte[chunkOffset + (sample.Length * 2)]; + byte[] data = new byte[chunkOffset + firstSample.Length + secondSample.Length]; BinaryPrimitives.WriteUInt32BigEndian(data, fileTypeLength); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), (uint)Heif4CharCode.Ftyp); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)Heif4CharCode.Avis); @@ -613,8 +661,8 @@ public class HeifSequenceParserTests BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), (uint)Heif4CharCode.Avif); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), (uint)Heif4CharCode.Mif1); movie.CopyTo(data, fileTypeLength); - sample.CopyTo(data.AsSpan((int)chunkOffset)); - sample.CopyTo(data.AsSpan((int)chunkOffset + sample.Length)); + firstSample.CopyTo(data.AsSpan((int)chunkOffset)); + secondSample.CopyTo(data.AsSpan((int)chunkOffset + firstSample.Length)); return data; } @@ -822,6 +870,7 @@ public class HeifSequenceParserTests int height, byte[] av1Configuration, int? sampleSize, + int? secondSampleSize, bool allSamplesSync, bool alpha = false) { @@ -848,7 +897,7 @@ public class HeifSequenceParserTests WriteUInt32(writer, 0); WriteUInt32(writer, 2); WriteUInt32(writer, (uint)(sampleSize ?? 10)); - WriteUInt32(writer, (uint)(sampleSize ?? 12)); + WriteUInt32(writer, (uint)(secondSampleSize ?? sampleSize ?? 12)); EndBox(writer, sampleSizes); long chunkOffsets = BeginBox(writer, Heif4CharCode.Stco);