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