Browse Source

Align HEIF decoder option handling

pull/2633/head
James Jackson-South 1 week ago
parent
commit
a8567c02e0
  1. 2
      HEIF_IMPLEMENTATION_PLAN.md
  2. 234
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  3. 15
      src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
  4. 24
      src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs
  5. 1
      src/ImageSharp/Formats/Heif/HeifDecoder.cs
  6. 900
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  7. 30
      src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
  8. 8
      src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs
  9. 19
      src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs
  10. 54
      src/ImageSharp/Formats/ImageDecoderCore.cs
  11. 279
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  12. 65
      tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs

2
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.

234
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -48,7 +48,8 @@ internal sealed class Av1CodecConfiguration
/// item-property payload.
/// </summary>
/// <param name="boxBuffer">The configuration payload beginning with the marker and version fields.</param>
public Av1CodecConfiguration(Span<byte> boxBuffer)
/// <param name="options">The general options governing metadata validation.</param>
public Av1CodecConfiguration(Span<byte> 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
/// <param name="itemMasteringDisplayColorVolume">
/// The mastering-display property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and item OBUs.
/// </param>
@ -186,83 +189,161 @@ internal sealed class Av1CodecConfiguration
ReadOnlySpan<byte> 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);
/// <summary>
/// Validates one AV1 track sample against its sync-sample declaration, sample-entry metadata, and configuration record.
/// </summary>
/// <param name="sampleData">The complete AV1 sample payload.</param>
/// <param name="isSyncSample">Indicates that the sample is declared as a random-access point.</param>
/// <param name="sampleContentLightLevel">
/// The content light-level property associated with the sample entry, or <see langword="null"/> when absent.
/// </param>
/// <param name="sampleMasteringDisplayColorVolume">
/// The mastering-display property associated with the sample entry, or <see langword="null"/> when absent.
/// </param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and sample OBUs.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the combined configuration and sample OBUs.
/// </param>
public void ValidateSampleData(
ReadOnlySpan<byte> 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);
/// <summary>
/// Validates one bounded AV1 payload while applying the item or track sequence-header requirement.
/// </summary>
/// <param name="data">The complete bounded AV1 payload.</param>
/// <param name="sequenceHeaderRequired">Indicates that exactly one sequence header is required.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="containerContentLightLevel">The content light-level property associated with the payload.</param>
/// <param name="containerMasteringDisplayColorVolume">The mastering-display property associated with the payload.</param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">Receives validated OBU content light-level metadata.</param>
/// <param name="masteringDisplayColorVolume">Receives validated OBU mastering-display metadata.</param>
private void ValidateData(
ReadOnlySpan<byte> 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<byte> configSequenceHeader = this.configObus.AsSpan(
this.configSequenceHeaderOffset,
this.configSequenceHeaderLength);
ReadOnlySpan<byte> itemSequenceHeader = itemData.Slice(
itemSequenceHeaderOffset,
itemSequenceHeaderLength);
ReadOnlySpan<byte> 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;
}
/// <summary>
@ -315,6 +396,7 @@ internal sealed class Av1CodecConfiguration
/// Indicates that a sequence-header OBU, when present, must be the first OBU in the sequence.
/// </param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="sequenceHeaderOffset">Receives the first sequence-header payload offset, or <c>-1</c>.</param>
/// <param name="sequenceHeaderLength">Receives the first sequence-header payload length.</param>
/// <param name="sequenceHeaderExtension">Receives the first sequence-header extension byte, or <c>-1</c>.</param>
@ -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.
}
}

15
src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs

@ -27,19 +27,22 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
/// <summary>
/// Decodes the encoded AV1 payload of an image item.
/// </summary>
/// <param name="configuration">The configuration that supplies memory allocation and codec services.</param>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
/// <param name="data">The encoded AV1 payload.</param>
/// <param name="colorProfile">
/// The container color description that supplies unspecified color information in the AV1 sequence header.
/// </param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> DecodeItemData(
Configuration configuration,
DecoderOptions options,
HeifItem item,
Span<byte> 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<TPixel> : IHeifItemDecoder<TPixel>
{
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<TPixel> : IHeifItemDecoder<TPixel>
data,
item.ContentLightLevel,
item.MasteringDisplayColorVolume,
options,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
using Av1Decoder decoder = new(configuration);
using Av1Decoder decoder = new(options.Configuration);
Image<TPixel> image = decoder.Decode<TPixel>(data, colorProfile, codecConfiguration);
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = this.CompressionMethod;

24
src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs

@ -18,11 +18,6 @@ namespace SixLabors.ImageSharp.Formats.Heif;
internal class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// The configuration used to decode each compressed grid tile.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The item definitions available to the grid.
/// </summary>
@ -46,7 +41,6 @@ internal class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
/// <summary>
/// Initializes a new instance of the <see cref="GridHeifItemDecoder{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration used to decode compressed grid tiles.</param>
/// <param name="items">The item definitions in the containing HEIF file.</param>
/// <param name="itemLinks">The item-reference relationships in the containing HEIF file.</param>
/// <param name="buffers">The assembled encoded payload for each image item.</param>
@ -54,13 +48,11 @@ internal class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
/// Optional row-major tile identifiers that replace the grid item's own derived-image references.
/// </param>
public GridHeifItemDecoder(
Configuration configuration,
IList<HeifItem> items,
IList<HeifItemLink> itemLinks,
IDictionary<uint, IMemoryOwner<byte>> buffers,
IReadOnlyList<uint>? tileItemIds = null)
{
this.configuration = configuration;
this.items = items;
this.itemLinks = itemLinks;
this.buffers = buffers;
@ -80,16 +72,18 @@ internal class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
/// <summary>
/// Decodes the tiles referenced by a grid derived-image item.
/// </summary>
/// <param name="configuration">The configuration associated with the containing HEIF decode.</param>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="gridItem">The grid derived-image item.</param>
/// <param name="data">The grid descriptor payload.</param>
/// <param name="colorProfile">The container color description inherited by tiles that do not declare one.</param>
/// <param name="cancellationToken">The token used to cancel between tile payloads.</param>
/// <returns>The image reconstructed from the referenced grid tiles.</returns>
public Image<TPixel> DecodeItemData(
Configuration configuration,
DecoderOptions options,
HeifItem gridItem,
Span<byte> data,
CicpProfile? colorProfile)
CicpProfile? colorProfile,
CancellationToken cancellationToken)
{
if (data.Length < 8)
{
@ -157,6 +151,7 @@ internal class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
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<TPixel> : IHeifItemDecoder<TPixel>
this.CompressionMethod = decoder.CompressionMethod;
Image<TPixel> tile = decoder.DecodeItemData(
this.configuration,
options,
item,
itemMemory.GetSpan(),
item.CicpProfile ?? colorProfile);
item.CicpProfile ?? colorProfile,
cancellationToken);
try
{
@ -227,7 +223,7 @@ internal class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
throw new InvalidImageContentException("The HEIF image grid edge tiles do not overlap the output canvas.");
}
Image<TPixel> result = new(configuration, (int)outputWidth, (int)outputHeight, firstTile.Metadata.DeepClone());
Image<TPixel> result = new(options.Configuration, (int)outputWidth, (int)outputHeight, firstTile.Metadata.DeepClone());
ImageFrame<TPixel> destination = result.Frames.RootFrame;
for (int tileIndex = 0; tileIndex < gridTiles.Count; tileIndex++)
{

1
src/ImageSharp/Formats/Heif/HeifDecoder.cs

@ -39,6 +39,7 @@ public sealed class HeifDecoder : ImageDecoder
HeifDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}

900
src/ImageSharp/Formats/Heif/HeifDecoderCore.cs

File diff suppressed because it is too large

30
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<byte> 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;
}
/// <summary>
/// Determines whether a recoverable ancillary-segment error should be ignored by the configured decoder policy.
/// </summary>
/// <param name="exception">The exception raised while parsing the ancillary segment.</param>
/// <returns><see langword="true"/> when decoding may continue without the segment.</returns>
private bool ShouldIgnoreAncillarySegmentError(Exception exception)
=> this.options.SegmentIntegrityHandling is not SegmentIntegrityHandling.Strict && ImageDecoderCore.IsRecoverableSegmentError(exception);
/// <summary>
/// Determines whether a recoverable image-data-segment error should be ignored by the configured decoder policy.
/// </summary>
/// <param name="exception">The exception raised while parsing the image-data segment.</param>
/// <returns><see langword="true"/> when decoding may continue without the segment.</returns>
private bool ShouldIgnoreImageDataSegmentError(Exception exception)
=> this.options.SegmentIntegrityHandling is SegmentIntegrityHandling.IgnoreImageData && ImageDecoderCore.IsRecoverableSegmentError(exception);
/// <summary>
/// Records one unique child box while retaining only its stream range.
/// </summary>

8
src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs

@ -26,16 +26,18 @@ internal interface IHeifItemDecoder<TPixel>
/// <summary>
/// Decodes the compressed payload of an image item.
/// </summary>
/// <param name="configuration">The configuration that supplies memory allocation and codec services.</param>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
/// <param name="data">The encoded image payload.</param>
/// <param name="colorProfile">
/// The container color description that overrides matching color information in the encoded image payload.
/// </param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> DecodeItemData(
Configuration configuration,
DecoderOptions options,
HeifItem item,
Span<byte> data,
CicpProfile? colorProfile);
CicpProfile? colorProfile,
CancellationToken cancellationToken);
}

19
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<TPixel> : IHeifItemDecoder<TPixel>
/// <summary>
/// Decodes the encoded JPEG payload of an image item.
/// </summary>
/// <param name="configuration">The configuration associated with the containing HEIF decode.</param>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
/// <param name="data">The encoded JPEG payload.</param>
/// <param name="colorProfile">The container color description associated with the image item.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> DecodeItemData(
Configuration configuration,
public unsafe Image<TPixel> DecodeItemData(
DecoderOptions options,
HeifItem item,
Span<byte> 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<TPixel> image = Image.Load<TPixel>(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<TPixel>(options.Configuration, stream, cancellationToken);
}
}
}

54
src/ImageSharp/Formats/ImageDecoderCore.cs

@ -39,7 +39,7 @@ internal abstract class ImageDecoderCore
/// <param name="action">The action.</param>
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
/// <param name="action">The action.</param>
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;
/// <summary>
/// Determines whether the configured policy permits recoverable ancillary-segment errors to be ignored.
/// </summary>
/// <param name="options">The general decoder options.</param>
/// <returns><see langword="true"/> when recoverable ancillary-segment errors may be ignored.</returns>
public static bool ShouldIgnoreAncillarySegmentErrors(DecoderOptions options)
=> options.SegmentIntegrityHandling is not SegmentIntegrityHandling.Strict;
/// <summary>
/// Determines whether the configured policy permits recoverable image-data-segment errors to be ignored.
/// </summary>
/// <param name="options">The general decoder options.</param>
/// <returns><see langword="true"/> when recoverable image-data-segment errors may be ignored.</returns>
public static bool ShouldIgnoreImageDataSegmentErrors(DecoderOptions options)
=> options.SegmentIntegrityHandling is SegmentIntegrityHandling.IgnoreImageData;
/// <summary>
/// Determines whether an ancillary-segment exception may be ignored by the configured decoder policy.
/// </summary>
/// <param name="options">The general decoder options.</param>
/// <param name="exception">The exception raised while processing an ancillary segment.</param>
/// <returns><see langword="true"/> when decoding may continue without the ancillary segment.</returns>
public static bool ShouldIgnoreAncillarySegmentError(DecoderOptions options, Exception exception)
=> ShouldIgnoreAncillarySegmentErrors(options) && IsRecoverableSegmentError(exception);
/// <summary>
/// Determines whether an image-data-segment exception may be ignored by the configured decoder policy.
/// </summary>
/// <param name="options">The general decoder options.</param>
/// <param name="exception">The exception raised while processing an image-data segment.</param>
/// <returns><see langword="true"/> when decoding may continue without the image-data segment.</returns>
public static bool ShouldIgnoreImageDataSegmentError(DecoderOptions options, Exception exception)
=> ShouldIgnoreImageDataSegmentErrors(options) && IsRecoverableSegmentError(exception);
/// <summary>
/// Throws unless the decoder is running in a non-strict segment integrity mode.
/// Use this only from within <see cref="ExecuteAncillarySegmentAction"/> when local control flow
/// must continue after the error.
/// Use this when ancillary parsing must continue locally after the error rather than returning through
/// <see cref="ExecuteAncillarySegmentAction"/>.
/// </summary>
/// <param name="message">The exception message.</param>
protected void ThrowOrIgnoreNonStrictSegmentError(string message)
@ -103,6 +137,18 @@ internal abstract class ImageDecoderCore
}
}
/// <summary>
/// Throws unless the decoder permits recoverable image-data segment errors to be ignored.
/// </summary>
/// <param name="message">The exception message.</param>
protected void ThrowOrIgnoreImageDataSegmentError(string message)
{
if (!ShouldIgnoreImageDataSegmentErrors(this.Options))
{
throw new InvalidImageContentException(message);
}
}
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>

279
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<byte> 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<Rgba32> source = new(64, 48);
for (int y = 0; y < source.Height; y++)
{
Span<Rgba32> 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<Rgba32> expected = Image.Load<Rgba32>(data);
expected.Mutate(context => context.Resize(new ResizeOptions { Size = targetSize, Mode = ResizeMode.Max, Sampler = options.Sampler }));
using Image<Rgba32> image = Image.Load<Rgba32>(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<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
[Theory]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
[InlineData(SegmentIntegrityHandling.IgnoreImageData)]
public void DecodePropagatesRecoverableMetadataValidationToLegacyJpegItems(SegmentIntegrityHandling handling)
{
byte[] data = CreateContainerWithMalformedJpegMetadata();
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
using Image<Rgba32> image = Image.Load<Rgba32>(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<Rgba32> image = Image.Load<Rgba32>(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<Rgba32> image = Image.Load<Rgba32>(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<InvalidImageContentException>(() => 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<InvalidImageContentException>(() => 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<InvalidImageContentException>(() => 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<InvalidImageContentException>(() => 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<byte> 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<byte> 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<byte> 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;
}

65
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<InvalidImageContentException>(() => Image.Load<Rgba32>(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<Rgba32> image = Image.Load<Rgba32>(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<byte> sample, ReadOnlySpan<byte> configuration)
=> CreateDecodableAv1SequenceContainer(sample, sample, configuration, true);
private static byte[] CreateDecodableAv1SequenceContainer(
ReadOnlySpan<byte> firstSample,
ReadOnlySpan<byte> secondSample,
ReadOnlySpan<byte> 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);

Loading…
Cancel
Save