mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2441 lines
109 KiB
2441 lines
109 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1;
|
|
using SixLabors.ImageSharp.Memory;
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
|
|
|
|
namespace SixLabors.ImageSharp.Formats.Heif;
|
|
|
|
/// <summary>
|
|
/// Parses the bounded track and sample syntax required to identify a HEIF image sequence.
|
|
/// </summary>
|
|
internal sealed class HeifSequenceParser
|
|
{
|
|
/// <summary>
|
|
/// The reusable scratch size used by sequential table reads.
|
|
/// </summary>
|
|
private const int ScratchLength = 4096;
|
|
|
|
/// <summary>
|
|
/// The configured allocator used for parser scratch and bounded table state.
|
|
/// </summary>
|
|
private readonly MemoryAllocator allocator;
|
|
|
|
/// <summary>
|
|
/// The shared reader for variable-length HEIF box headers.
|
|
/// </summary>
|
|
private readonly HeifBoxReader boxReader;
|
|
|
|
/// <summary>
|
|
/// The bounded parser for Exif and XMP items embedded in selected image tracks.
|
|
/// </summary>
|
|
private readonly HeifTrackMetadataParser metadataParser;
|
|
|
|
/// <summary>
|
|
/// The maximum number of sample descriptors retained for decoding or identification.
|
|
/// </summary>
|
|
private readonly int maxFrames;
|
|
|
|
/// <summary>
|
|
/// The general decoder options controlling frame limits, metadata loading, and recoverable segment errors.
|
|
/// </summary>
|
|
private readonly DecoderOptions options;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="HeifSequenceParser"/> class.
|
|
/// </summary>
|
|
/// <param name="options">The general decoder options.</param>
|
|
public HeifSequenceParser(DecoderOptions options)
|
|
{
|
|
this.options = options;
|
|
this.allocator = options.Configuration.MemoryAllocator;
|
|
this.boxReader = new HeifBoxReader(this.allocator);
|
|
this.metadataParser = new HeifTrackMetadataParser(this.allocator);
|
|
this.maxFrames = (int)options.MaxFrames;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses one movie box and selects its master image-sequence track and linked alpha track.
|
|
/// </summary>
|
|
/// <param name="stream">The seekable HEIF stream positioned at the movie payload.</param>
|
|
/// <param name="boxLength">The validated movie payload length.</param>
|
|
/// <returns>The bounded image-sequence model required by the HEIF decoder.</returns>
|
|
public HeifSequence Parse(Stream stream, long boxLength)
|
|
{
|
|
long movieStart = stream.Position;
|
|
long movieEnd = checked(movieStart + boxLength);
|
|
HeifBoxReader.EnsureInsideParent(boxLength, stream.Length - movieStart);
|
|
|
|
using IMemoryOwner<byte> scratchOwner = this.allocator.Allocate<byte>(ScratchLength);
|
|
Span<byte> scratch = scratchOwner.GetSpan();
|
|
BoxReference movieHeader = default;
|
|
uint colorTrackId = 0;
|
|
|
|
// The first pass reads only fixed track identity fields. This prevents files containing unrelated media tracks
|
|
// from forcing codec configurations and sample tables into the image decoder's retained model.
|
|
while (stream.Position < movieEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, movieEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Mvhd)
|
|
{
|
|
SetUnique(ref movieHeader, childStart, childLength, "movie", childType);
|
|
}
|
|
else if (childType == Heif4CharCode.Trak)
|
|
{
|
|
TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch);
|
|
if (colorTrackId == 0
|
|
&& identity.IsEnabled
|
|
&& identity.HandlerType == Heif4CharCode.Pict
|
|
&& identity.AuxiliaryForTrackId == 0)
|
|
{
|
|
colorTrackId = identity.Id;
|
|
}
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!movieHeader.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF image sequence has no movie header.");
|
|
}
|
|
|
|
if (colorTrackId == 0)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF image sequence has no enabled picture track.");
|
|
}
|
|
|
|
stream.Position = movieHeader.Offset;
|
|
uint movieTimescale = ParseMovieHeader(stream, movieHeader.Length, scratch);
|
|
HeifSequenceTrack? colorTrack = null;
|
|
HeifSequenceTrack? alphaTrack = null;
|
|
|
|
// Track references can precede the master track. Re-scan the bounded movie now that the selected master ID is
|
|
// known, and fully parse only that track and the one alpha auxiliary linked to it.
|
|
stream.Position = movieStart;
|
|
while (stream.Position < movieEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, movieEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Trak)
|
|
{
|
|
TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch);
|
|
bool isColor = identity.Id == colorTrackId;
|
|
bool isLinkedAuxiliary = identity.AuxiliaryForTrackId == colorTrackId
|
|
&& identity.HandlerType is Heif4CharCode.Auxv or Heif4CharCode.Pict;
|
|
|
|
if (isColor || (isLinkedAuxiliary && alphaTrack is null))
|
|
{
|
|
stream.Position = childStart;
|
|
HeifSequenceTrack track = this.ParseTrack(stream, childLength, identity, scratch);
|
|
if (isColor)
|
|
{
|
|
colorTrack = track;
|
|
}
|
|
else if (track.IsAlpha)
|
|
{
|
|
alphaTrack = track;
|
|
}
|
|
}
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (colorTrack is null)
|
|
{
|
|
throw new InvalidImageContentException("The selected HEIF picture track could not be parsed.");
|
|
}
|
|
|
|
try
|
|
{
|
|
ValidateAlphaTrack(colorTrack, alphaTrack);
|
|
}
|
|
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.
|
|
alphaTrack = null;
|
|
colorTrack.IsPremultiplied = false;
|
|
}
|
|
|
|
return new HeifSequence(colorTrack, alphaTrack, movieTimescale);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the fixed identity fields used to select an image track without materializing its sample table.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the track payload.</param>
|
|
/// <param name="boxLength">The validated track payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The track identity and image relationship fields.</returns>
|
|
private static TrackIdentity ScanTrackIdentity(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
long trackEnd = checked(stream.Position + boxLength);
|
|
BoxReference trackHeader = default;
|
|
BoxReference trackReferences = default;
|
|
BoxReference media = default;
|
|
|
|
while (stream.Position < trackEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, trackEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
switch (childType)
|
|
{
|
|
case Heif4CharCode.Tkhd:
|
|
SetUnique(ref trackHeader, childStart, childLength, "track", childType);
|
|
break;
|
|
case Heif4CharCode.Tref:
|
|
SetUnique(ref trackReferences, childStart, childLength, "track", childType);
|
|
break;
|
|
case Heif4CharCode.Mdia:
|
|
SetUnique(ref media, childStart, childLength, "track", childType);
|
|
break;
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!media.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF track is missing its media box.");
|
|
}
|
|
|
|
stream.Position = media.Offset;
|
|
Heif4CharCode handlerType = ScanMediaHandler(stream, media.Length, scratch);
|
|
if (handlerType is not Heif4CharCode.Pict and not Heif4CharCode.Auxv)
|
|
{
|
|
// Non-image tracks are outside this parser's retained ISOBMFF surface. Do not impose image dimensions,
|
|
// matrices, or reference semantics on audio and other unrelated media carried by a valid image file.
|
|
TrackIdentity unrelatedIdentity = default;
|
|
unrelatedIdentity.HandlerType = handlerType;
|
|
return unrelatedIdentity;
|
|
}
|
|
|
|
if (!trackHeader.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence track is missing its track header.");
|
|
}
|
|
|
|
stream.Position = trackHeader.Offset;
|
|
TrackIdentity identity = ParseTrackHeader(stream, trackHeader.Length, scratch);
|
|
if (trackReferences.IsPresent)
|
|
{
|
|
stream.Position = trackReferences.Offset;
|
|
ParseTrackReferences(stream, trackReferences.Length, ref identity, scratch);
|
|
}
|
|
|
|
identity.HandlerType = handlerType;
|
|
return identity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the retained behavior of one selected image-sequence track.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the track payload.</param>
|
|
/// <param name="boxLength">The validated track payload length.</param>
|
|
/// <param name="identity">The fixed identity fields from the selection pass.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The selected track with its validated sample descriptors.</returns>
|
|
private HeifSequenceTrack ParseTrack(Stream stream, long boxLength, TrackIdentity identity, Span<byte> scratch)
|
|
{
|
|
long trackEnd = checked(stream.Position + boxLength);
|
|
BoxReference edit = default;
|
|
BoxReference metadata = default;
|
|
BoxReference media = default;
|
|
|
|
while (stream.Position < trackEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, trackEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Edts)
|
|
{
|
|
SetUnique(ref edit, childStart, childLength, "track", childType);
|
|
}
|
|
else if (childType == Heif4CharCode.Meta)
|
|
{
|
|
SetUnique(ref metadata, childStart, childLength, "track", childType);
|
|
}
|
|
else if (childType == Heif4CharCode.Mdia)
|
|
{
|
|
SetUnique(ref media, childStart, childLength, "track", childType);
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!media.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A selected HEIF image-sequence track has no media box.");
|
|
}
|
|
|
|
HeifSequenceTrack track = new()
|
|
{
|
|
Id = identity.Id,
|
|
Width = identity.Width,
|
|
Height = identity.Height,
|
|
HandlerType = identity.HandlerType,
|
|
TrackDuration = identity.TrackDuration,
|
|
AuxiliaryForTrackId = identity.AuxiliaryForTrackId,
|
|
PremultipliedByTrackId = identity.PremultipliedByTrackId
|
|
};
|
|
|
|
if (edit.IsPresent)
|
|
{
|
|
stream.Position = edit.Offset;
|
|
ParseEdit(stream, edit.Length, track, scratch);
|
|
}
|
|
|
|
if (metadata.IsPresent && !this.options.SkipMetadata)
|
|
{
|
|
try
|
|
{
|
|
stream.Position = metadata.Offset;
|
|
track.Metadata = this.metadataParser.Parse(stream, metadata.Length, scratch);
|
|
}
|
|
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex))
|
|
{
|
|
// The validated parent range lets decoding continue safely without this optional metadata box.
|
|
}
|
|
}
|
|
|
|
stream.Position = media.Offset;
|
|
this.ParseMedia(stream, media.Length, track, scratch);
|
|
if (track.TotalSampleCount == 0 || track.Samples.Length == 0)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF image-sequence track contains no retained image samples.");
|
|
}
|
|
|
|
return track;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the movie time scale and validates that no general movie-canvas transformation is required.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the movie-header payload.</param>
|
|
/// <param name="boxLength">The validated movie-header payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The nonzero movie time scale.</returns>
|
|
private static uint ParseMovieHeader(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 4, "movie header");
|
|
byte version = prefix[0];
|
|
int requiredLength = version switch
|
|
{
|
|
0 => 100,
|
|
1 => 112,
|
|
_ => throw new InvalidImageContentException($"The movie header has unsupported version {version}.")
|
|
};
|
|
|
|
prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "movie header");
|
|
EnsureZeroFlags(prefix, "movie header");
|
|
uint timescale = BinaryPrimitives.ReadUInt32BigEndian(prefix[(version == 0 ? 12 : 20)..]);
|
|
if (timescale == 0)
|
|
{
|
|
throw new InvalidImageContentException("The movie header has a zero time scale.");
|
|
}
|
|
|
|
int matrixOffset = version == 0 ? 36 : 48;
|
|
HeifTrackMatrix matrix = HeifTrackMatrix.Parse(prefix.Slice(matrixOffset, 36));
|
|
if (!matrix.IsIdentity)
|
|
{
|
|
throw new NotSupportedException("The HEIF image sequence requires an unsupported movie presentation matrix.");
|
|
}
|
|
|
|
return timescale;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses track identity, dimensions, duration, and transformation matrix.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the track-header payload.</param>
|
|
/// <param name="boxLength">The validated track-header payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The fixed track identity fields.</returns>
|
|
private static TrackIdentity ParseTrackHeader(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
const int fullBoxHeaderLength = sizeof(uint);
|
|
const int trackIdAndReservedLength = 2 * sizeof(uint);
|
|
const int postDurationFieldsLength = (2 * sizeof(uint)) + (4 * sizeof(ushort));
|
|
const int matrixLength = 9 * sizeof(int);
|
|
const int dimensionsLength = 2 * sizeof(uint);
|
|
const int fixedPointFractionalBits = 16;
|
|
const uint trackEnabledFlag = 1 << 0;
|
|
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, fullBoxHeaderLength, "track header");
|
|
byte version = prefix[0];
|
|
|
|
// ISO/IEC 14496-12, Section 8.3.2 defines 'tkhd' as a FullBox followed by creation and modification
|
|
// times, the track identifier, a reserved field, and the duration. Version 1 widens each time and duration
|
|
// field from 32 to 64 bits, which shifts every field that follows them by twelve bytes.
|
|
int versionedFieldLength = version switch
|
|
{
|
|
0 => sizeof(uint),
|
|
1 => sizeof(ulong),
|
|
_ => throw new InvalidImageContentException($"The track header has unsupported version {version}.")
|
|
};
|
|
|
|
int trackIdOffset = fullBoxHeaderLength + (2 * versionedFieldLength);
|
|
int durationOffset = trackIdOffset + trackIdAndReservedLength;
|
|
|
|
// The fields between duration and matrix are two reserved 32-bit values followed by the 16-bit layer,
|
|
// alternate_group, volume, and reserved values specified by Section 8.3.2. The matrix then contains nine
|
|
// 32-bit fixed-point coefficients, followed by the two 32-bit track dimensions.
|
|
int matrixOffset = durationOffset + versionedFieldLength + postDurationFieldsLength;
|
|
int widthOffset = matrixOffset + matrixLength;
|
|
int requiredLength = widthOffset + dimensionsLength;
|
|
|
|
prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "track header");
|
|
uint flags = ReadFlags(prefix);
|
|
uint id = BinaryPrimitives.ReadUInt32BigEndian(prefix[trackIdOffset..]);
|
|
|
|
// Section 8.3.2 reserves track_ID zero, so accepting it would make track references ambiguous.
|
|
if (id == 0)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence track has identifier zero.");
|
|
}
|
|
|
|
ulong duration = version == 0
|
|
? BinaryPrimitives.ReadUInt32BigEndian(prefix[durationOffset..])
|
|
: BinaryPrimitives.ReadUInt64BigEndian(prefix[durationOffset..]);
|
|
|
|
// The specification uses the all-ones value for an indefinite duration. Normalize the 32-bit version to
|
|
// the 64-bit sentinel used by the sequence model so both TrackHeaderBox versions follow the same path.
|
|
if (version == 0 && duration == uint.MaxValue)
|
|
{
|
|
duration = ulong.MaxValue;
|
|
}
|
|
|
|
// Section 8.3.2 stores width and height as unsigned 16.16 fixed-point values. ImageSharp dimensions are
|
|
// integral pixels, matching libavif, so discard the fractional half before validating the display size.
|
|
uint fixedWidth = BinaryPrimitives.ReadUInt32BigEndian(prefix[widthOffset..]);
|
|
uint fixedHeight = BinaryPrimitives.ReadUInt32BigEndian(prefix[(widthOffset + sizeof(uint))..]);
|
|
int width = checked((int)(fixedWidth >> fixedPointFractionalBits));
|
|
int height = checked((int)(fixedHeight >> fixedPointFractionalBits));
|
|
|
|
if (width == 0 || height == 0)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence track has zero dimensions.");
|
|
}
|
|
|
|
HeifTrackMatrix matrix = HeifTrackMatrix.Parse(prefix.Slice(matrixOffset, matrixLength));
|
|
|
|
// The Section 8.3.2 matrix transforms the track into the movie presentation coordinate system. This image
|
|
// decoder currently emits the stored raster directly, so a non-unity matrix would produce incorrect pixels.
|
|
if (!matrix.IsIdentity)
|
|
{
|
|
throw new NotSupportedException("The HEIF image-sequence track requires an unsupported movie presentation matrix.");
|
|
}
|
|
|
|
// ISO/IEC 14496-12 Section 8.3.2 assigns bit zero to track_enabled. Image sequences, including files written
|
|
// by libavif, do not require track_in_movie to be set, so only the enabled bit participates in selection.
|
|
bool isEnabled = (flags & trackEnabledFlag) != 0;
|
|
|
|
return new TrackIdentity(id, isEnabled, width, height, duration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the image-specific track references used for alpha and premultiplication linkage.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the track-reference payload.</param>
|
|
/// <param name="boxLength">The validated track-reference payload length.</param>
|
|
/// <param name="identity">The track identity receiving image relationships.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseTrackReferences(Stream stream, long boxLength, ref TrackIdentity identity, Span<byte> scratch)
|
|
{
|
|
long referenceEnd = checked(stream.Position + boxLength);
|
|
while (stream.Position < referenceEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, referenceEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType is Heif4CharCode.Auxl or Heif4CharCode.Prem)
|
|
{
|
|
if (childLength < 4 || (childLength & 3) != 0)
|
|
{
|
|
throw new InvalidImageContentException($"The '{childType}' track reference has an invalid identifier list.");
|
|
}
|
|
|
|
ReadOnlySpan<byte> data = ReadPrefix(stream, childLength, scratch, 4, $"{childType} track reference");
|
|
uint referencedTrackId = BinaryPrimitives.ReadUInt32BigEndian(data);
|
|
if (referencedTrackId == 0)
|
|
{
|
|
throw new InvalidImageContentException($"The '{childType}' track reference contains identifier zero.");
|
|
}
|
|
|
|
if (childType == Heif4CharCode.Auxl)
|
|
{
|
|
if (identity.AuxiliaryForTrackId != 0)
|
|
{
|
|
throw new InvalidImageContentException("The track contains duplicate alpha-auxiliary references.");
|
|
}
|
|
|
|
identity.AuxiliaryForTrackId = referencedTrackId;
|
|
}
|
|
else
|
|
{
|
|
if (identity.PremultipliedByTrackId != 0)
|
|
{
|
|
throw new InvalidImageContentException("The track contains duplicate premultiplication references.");
|
|
}
|
|
|
|
identity.PremultipliedByTrackId = referencedTrackId;
|
|
}
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds and parses the handler type from one media box.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the media payload.</param>
|
|
/// <param name="boxLength">The validated media payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The declared media handler type.</returns>
|
|
private static Heif4CharCode ScanMediaHandler(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
long mediaEnd = checked(stream.Position + boxLength);
|
|
BoxReference handler = default;
|
|
while (stream.Position < mediaEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, mediaEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Hdlr)
|
|
{
|
|
SetUnique(ref handler, childStart, childLength, "media", childType);
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!handler.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence media box has no handler.");
|
|
}
|
|
|
|
stream.Position = handler.Offset;
|
|
return ParseHandler(stream, handler.Length, scratch);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the media header and sample table of a selected image track.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the media payload.</param>
|
|
/// <param name="boxLength">The validated media payload length.</param>
|
|
/// <param name="track">The selected track receiving media state.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseMedia(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
long mediaEnd = checked(stream.Position + boxLength);
|
|
BoxReference mediaHeader = default;
|
|
BoxReference handler = default;
|
|
BoxReference mediaInformation = default;
|
|
|
|
while (stream.Position < mediaEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, mediaEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
switch (childType)
|
|
{
|
|
case Heif4CharCode.Mdhd:
|
|
SetUnique(ref mediaHeader, childStart, childLength, "media", childType);
|
|
break;
|
|
case Heif4CharCode.Hdlr:
|
|
SetUnique(ref handler, childStart, childLength, "media", childType);
|
|
break;
|
|
case Heif4CharCode.Minf:
|
|
SetUnique(ref mediaInformation, childStart, childLength, "media", childType);
|
|
break;
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!mediaHeader.IsPresent || !handler.IsPresent || !mediaInformation.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A selected HEIF image-sequence media box is incomplete.");
|
|
}
|
|
|
|
stream.Position = mediaHeader.Offset;
|
|
ParseMediaHeader(stream, mediaHeader.Length, track, scratch);
|
|
stream.Position = handler.Offset;
|
|
Heif4CharCode handlerType = ParseHandler(stream, handler.Length, scratch);
|
|
if (handlerType != track.HandlerType)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF image-sequence track handler changed between parser passes.");
|
|
}
|
|
|
|
stream.Position = mediaInformation.Offset;
|
|
this.ParseMediaInformation(stream, mediaInformation.Length, track, scratch);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the media time scale and duration of a selected track.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the media-header payload.</param>
|
|
/// <param name="boxLength">The validated media-header payload length.</param>
|
|
/// <param name="track">The selected track receiving timing state.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseMediaHeader(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 4, "media header");
|
|
byte version = prefix[0];
|
|
int requiredLength = version switch
|
|
{
|
|
0 => 24,
|
|
1 => 36,
|
|
_ => throw new InvalidImageContentException($"The media header has unsupported version {version}.")
|
|
};
|
|
|
|
prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "media header");
|
|
EnsureZeroFlags(prefix, "media header");
|
|
int timescaleOffset = version == 0 ? 12 : 20;
|
|
track.MediaTimescale = BinaryPrimitives.ReadUInt32BigEndian(prefix[timescaleOffset..]);
|
|
track.MediaDuration = version == 0
|
|
? BinaryPrimitives.ReadUInt32BigEndian(prefix[(timescaleOffset + 4)..])
|
|
: BinaryPrimitives.ReadUInt64BigEndian(prefix[(timescaleOffset + 4)..]);
|
|
|
|
if (track.MediaTimescale == 0)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF image-sequence media header has a zero time scale.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a media handler and returns its four-character handler type.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the handler payload.</param>
|
|
/// <param name="boxLength">The validated handler payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The declared handler type.</returns>
|
|
private static Heif4CharCode ParseHandler(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 24, "handler");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "handler");
|
|
if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 0)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF image-sequence handler has a nonzero predefined field.");
|
|
}
|
|
|
|
return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Locates the self-contained data reference and sample table inside a selected track.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the media-information payload.</param>
|
|
/// <param name="boxLength">The validated media-information payload length.</param>
|
|
/// <param name="track">The selected track receiving its sample table.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseMediaInformation(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
long informationEnd = checked(stream.Position + boxLength);
|
|
BoxReference dataInformation = default;
|
|
BoxReference sampleTable = default;
|
|
while (stream.Position < informationEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, informationEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Dinf)
|
|
{
|
|
SetUnique(ref dataInformation, childStart, childLength, "media information", childType);
|
|
}
|
|
else if (childType == Heif4CharCode.Stbl)
|
|
{
|
|
SetUnique(ref sampleTable, childStart, childLength, "media information", childType);
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!dataInformation.IsPresent || !sampleTable.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A selected HEIF image-sequence track has no data reference or sample table.");
|
|
}
|
|
|
|
stream.Position = dataInformation.Offset;
|
|
ParseDataInformation(stream, dataInformation.Length, scratch);
|
|
stream.Position = sampleTable.Offset;
|
|
this.ParseSampleTable(stream, sampleTable.Length, track, scratch);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requires a selected image track to address sample bytes in the current HEIF file.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the data-information payload.</param>
|
|
/// <param name="boxLength">The validated data-information payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseDataInformation(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
long informationEnd = checked(stream.Position + boxLength);
|
|
BoxReference dataReference = default;
|
|
while (stream.Position < informationEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, informationEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Dref)
|
|
{
|
|
SetUnique(ref dataReference, childStart, childLength, "data information", childType);
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!dataReference.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A selected HEIF image-sequence track has no data-reference box.");
|
|
}
|
|
|
|
stream.Position = dataReference.Offset;
|
|
long referenceEnd = checked(stream.Position + dataReference.Length);
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, dataReference.Length, scratch, 8, "data reference");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "data reference");
|
|
if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence track must contain exactly one data reference.");
|
|
}
|
|
|
|
long locationLength = HeifBoxReader.ReadHeader(stream, referenceEnd, scratch, out Heif4CharCode locationType);
|
|
if (locationType != Heif4CharCode.Url || locationLength != 4)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence track uses an external data reference.");
|
|
}
|
|
|
|
prefix = ReadPrefix(stream, locationLength, scratch, 4, "data location");
|
|
EnsureVersionAndFlags(prefix, 0, 1, "data location");
|
|
if (stream.Position != referenceEnd)
|
|
{
|
|
throw new InvalidImageContentException("The data-reference box contains undeclared entries.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indexes and resolves the bounded sample-table boxes required by an image sequence.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-table payload.</param>
|
|
/// <param name="boxLength">The validated sample-table payload length.</param>
|
|
/// <param name="track">The selected track receiving sample descriptors.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseSampleTable(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
long tableEnd = checked(stream.Position + boxLength);
|
|
BoxReference sampleDescription = default;
|
|
BoxReference sampleTiming = default;
|
|
BoxReference sampleToChunk = default;
|
|
BoxReference sampleSizes = default;
|
|
BoxReference chunkOffsets = default;
|
|
BoxReference syncSamples = default;
|
|
BoxReference compositionOffsets = default;
|
|
BoxReference compositionToDecode = default;
|
|
BoxReference sampleGroupDescriptions = default;
|
|
BoxReference sampleToGroup = default;
|
|
|
|
while (stream.Position < tableEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, tableEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
switch (childType)
|
|
{
|
|
case Heif4CharCode.Stsd:
|
|
SetUnique(ref sampleDescription, childStart, childLength, "sample table", childType);
|
|
break;
|
|
case Heif4CharCode.Stts:
|
|
SetUnique(ref sampleTiming, childStart, childLength, "sample table", childType);
|
|
break;
|
|
case Heif4CharCode.Stsc:
|
|
SetUnique(ref sampleToChunk, childStart, childLength, "sample table", childType);
|
|
break;
|
|
case Heif4CharCode.Stsz:
|
|
case Heif4CharCode.Stz2:
|
|
SetUnique(ref sampleSizes, childStart, childLength, "sample table", childType);
|
|
sampleSizes.Type = childType;
|
|
break;
|
|
case Heif4CharCode.Stco:
|
|
case Heif4CharCode.Co64:
|
|
SetUnique(ref chunkOffsets, childStart, childLength, "sample table", childType);
|
|
chunkOffsets.Type = childType;
|
|
break;
|
|
case Heif4CharCode.Stss:
|
|
SetUnique(ref syncSamples, childStart, childLength, "sample table", childType);
|
|
break;
|
|
case Heif4CharCode.Ctts:
|
|
SetUnique(ref compositionOffsets, childStart, childLength, "sample table", childType);
|
|
break;
|
|
case Heif4CharCode.Cslg:
|
|
SetUnique(ref compositionToDecode, childStart, childLength, "sample table", childType);
|
|
break;
|
|
case Heif4CharCode.Sgpd:
|
|
if (ReadSampleGroupType(stream, childLength, scratch) == Heif4CharCode.Refs)
|
|
{
|
|
SetUnique(ref sampleGroupDescriptions, childStart, childLength, "sample table", childType);
|
|
}
|
|
|
|
break;
|
|
case Heif4CharCode.Sbgp:
|
|
if (ReadSampleGroupType(stream, childLength, scratch) == Heif4CharCode.Refs)
|
|
{
|
|
SetUnique(ref sampleToGroup, childStart, childLength, "sample table", childType);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!sampleDescription.IsPresent
|
|
|| !sampleTiming.IsPresent
|
|
|| !sampleToChunk.IsPresent
|
|
|| !sampleSizes.IsPresent
|
|
|| !chunkOffsets.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("A selected HEIF image-sequence sample table is incomplete.");
|
|
}
|
|
|
|
stream.Position = sampleDescription.Offset;
|
|
this.ParseSampleDescription(stream, sampleDescription.Length, track, scratch);
|
|
stream.Position = sampleSizes.Offset;
|
|
this.ParseSampleSizes(stream, sampleSizes.Length, sampleSizes.Type, track, scratch);
|
|
stream.Position = sampleTiming.Offset;
|
|
ulong decodedDuration = ParseSampleTiming(stream, sampleTiming.Length, track, scratch);
|
|
|
|
if (track.MediaDuration != 0 && track.MediaDuration != ulong.MaxValue && track.MediaDuration != decodedDuration)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample durations do not match the media-header duration.");
|
|
}
|
|
|
|
stream.Position = chunkOffsets.Offset;
|
|
uint chunkCount = ReadChunkCount(stream, chunkOffsets.Length, chunkOffsets.Type, scratch);
|
|
stream.Position = sampleToChunk.Offset;
|
|
using IMemoryOwner<SampleToChunkEntry> entries = this.ParseSampleToChunk(
|
|
stream,
|
|
sampleToChunk.Length,
|
|
chunkCount,
|
|
track,
|
|
scratch,
|
|
out int entryCount);
|
|
|
|
stream.Position = chunkOffsets.Offset;
|
|
ResolveSampleLocations(stream, chunkOffsets.Length, chunkOffsets.Type, chunkCount, entries.GetSpan()[..entryCount], track, scratch);
|
|
if (syncSamples.IsPresent)
|
|
{
|
|
stream.Position = syncSamples.Offset;
|
|
ParseSyncSamples(stream, syncSamples.Length, track, scratch);
|
|
}
|
|
else
|
|
{
|
|
Span<HeifSequenceSample> samples = track.Samples;
|
|
for (int i = 0; i < samples.Length; i++)
|
|
{
|
|
samples[i].IsSync = true;
|
|
}
|
|
}
|
|
|
|
if (sampleGroupDescriptions.IsPresent != sampleToGroup.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group is missing its description or sample map.");
|
|
}
|
|
|
|
if (sampleGroupDescriptions.IsPresent)
|
|
{
|
|
this.ParseDirectReferences(stream, sampleGroupDescriptions, sampleToGroup, track, scratch);
|
|
}
|
|
|
|
if (compositionOffsets.IsPresent || compositionToDecode.IsPresent)
|
|
{
|
|
// AV1-ISOBMFF defines AV1 sample composition time as decode time and explicitly prohibits both boxes.
|
|
throw new InvalidImageContentException("An AV1 image-sequence track contains prohibited composition timing boxes.");
|
|
}
|
|
|
|
SetCompositionTimes(track);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the single visual sample entry and its codec and coding-constraint children.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-description payload.</param>
|
|
/// <param name="boxLength">The validated sample-description payload length.</param>
|
|
/// <param name="track">The selected track receiving its codec configuration.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseSampleDescription(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
long descriptionEnd = checked(stream.Position + boxLength);
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample description");
|
|
byte version = prefix[0];
|
|
if (version is not 0 and not 1 || ReadFlags(prefix) != 0)
|
|
{
|
|
throw new InvalidImageContentException("The sample-description box has an unsupported version or flags.");
|
|
}
|
|
|
|
if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1)
|
|
{
|
|
throw new InvalidImageContentException("A HEIF image-sequence track must contain exactly one sample description.");
|
|
}
|
|
|
|
long entryLength = HeifBoxReader.ReadHeader(stream, descriptionEnd, scratch, out Heif4CharCode entryType);
|
|
if (entryType != Heif4CharCode.Av01 || entryLength < 78)
|
|
{
|
|
throw new InvalidImageContentException($"The image-sequence sample entry '{entryType}' is unsupported or truncated.");
|
|
}
|
|
|
|
long entryEnd = checked(stream.Position + entryLength);
|
|
prefix = ReadPrefix(stream, entryLength, scratch, 78, "visual sample entry");
|
|
if (BinaryPrimitives.ReadUInt16BigEndian(prefix[6..]) != 1)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry uses a nonlocal data reference.");
|
|
}
|
|
|
|
int codedWidth = BinaryPrimitives.ReadUInt16BigEndian(prefix[24..]);
|
|
int codedHeight = BinaryPrimitives.ReadUInt16BigEndian(prefix[26..]);
|
|
if (codedWidth == 0 || codedHeight == 0)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has zero coded dimensions.");
|
|
}
|
|
|
|
track.CodecType = entryType;
|
|
track.CodedWidth = codedWidth;
|
|
track.CodedHeight = codedHeight;
|
|
bool configurationSeen = false;
|
|
bool codingConstraintsSeen = false;
|
|
bool auxiliaryTypeSeen = false;
|
|
while (stream.Position < entryEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, entryEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
switch (childType)
|
|
{
|
|
case Heif4CharCode.Av1C when entryType == Heif4CharCode.Av01:
|
|
if (configurationSeen)
|
|
{
|
|
throw new InvalidImageContentException("The AV1 image-sequence sample entry has duplicate codec configurations.");
|
|
}
|
|
|
|
using (IMemoryOwner<byte> configuration = this.boxReader.ReadPayload(stream, childLength))
|
|
{
|
|
track.Av1CodecConfiguration = new Av1CodecConfiguration(configuration.GetSpan(), this.options);
|
|
}
|
|
|
|
configurationSeen = true;
|
|
break;
|
|
case Heif4CharCode.Ccst:
|
|
if (codingConstraintsSeen)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate coding constraints.");
|
|
}
|
|
|
|
ParseCodingConstraints(stream, childLength, track, scratch);
|
|
codingConstraintsSeen = true;
|
|
break;
|
|
case Heif4CharCode.Auxi:
|
|
if (auxiliaryTypeSeen)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate auxiliary types.");
|
|
}
|
|
|
|
track.IsAlpha = this.ParseAuxiliaryType(stream, childLength);
|
|
auxiliaryTypeSeen = true;
|
|
break;
|
|
case Heif4CharCode.Colr:
|
|
this.ParseTrackColorInformation(stream, childLength, track, scratch);
|
|
break;
|
|
case Heif4CharCode.Pasp:
|
|
case Heif4CharCode.Clli:
|
|
case Heif4CharCode.Mdcv:
|
|
case Heif4CharCode.Cclv:
|
|
case Heif4CharCode.Amve:
|
|
case Heif4CharCode.Reve:
|
|
case Heif4CharCode.Ndwt:
|
|
if (!this.options.SkipMetadata)
|
|
{
|
|
try
|
|
{
|
|
ParseTrackImageProperty(stream, childLength, childType, track, scratch);
|
|
}
|
|
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex))
|
|
{
|
|
// The complete child range remains known, so optional metadata can be discarded safely.
|
|
}
|
|
}
|
|
|
|
break;
|
|
case Heif4CharCode.Clap:
|
|
case Heif4CharCode.Irot:
|
|
case Heif4CharCode.Imir:
|
|
try
|
|
{
|
|
ParseTrackImageProperty(stream, childLength, childType, track, scratch);
|
|
}
|
|
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex))
|
|
{
|
|
// IgnoreImageData permits a recoverable presentation property to be omitted.
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (stream.Position != descriptionEnd || !configurationSeen || !codingConstraintsSeen)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample description is incomplete or has trailing entries.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses one presentation, color, or HDR property carried by a visual sample entry.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the property payload.</param>
|
|
/// <param name="boxLength">The validated property payload length.</param>
|
|
/// <param name="boxType">The registered image property type.</param>
|
|
/// <param name="track">The selected image track receiving the property.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseTrackImageProperty(
|
|
Stream stream,
|
|
long boxLength,
|
|
Heif4CharCode boxType,
|
|
HeifSequenceTrack track,
|
|
Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> data = ReadPropertyPayload(stream, boxLength, scratch, boxType);
|
|
switch (boxType)
|
|
{
|
|
case Heif4CharCode.Pasp:
|
|
if (track.PixelAspectRatio is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate pixel-aspect-ratio properties.");
|
|
}
|
|
|
|
track.PixelAspectRatio = HeifPropertyParser.ParsePixelAspectRatio(data);
|
|
break;
|
|
case Heif4CharCode.Clli:
|
|
if (track.ContentLightLevel is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate content-light-level properties.");
|
|
}
|
|
|
|
track.ContentLightLevel = HeifPropertyParser.ParseContentLightLevel(data);
|
|
break;
|
|
case Heif4CharCode.Mdcv:
|
|
if (track.MasteringDisplayColorVolume is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate mastering-display properties.");
|
|
}
|
|
|
|
track.MasteringDisplayColorVolume = HeifPropertyParser.ParseMasteringDisplayColorVolume(data);
|
|
break;
|
|
case Heif4CharCode.Cclv:
|
|
if (track.ContentColorVolume is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate content-color-volume properties.");
|
|
}
|
|
|
|
track.ContentColorVolume = HeifPropertyParser.ParseContentColorVolume(data);
|
|
break;
|
|
case Heif4CharCode.Amve:
|
|
if (track.AmbientViewingEnvironment is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate ambient-viewing properties.");
|
|
}
|
|
|
|
track.AmbientViewingEnvironment = HeifPropertyParser.ParseAmbientViewingEnvironment(data);
|
|
break;
|
|
case Heif4CharCode.Reve:
|
|
if (track.ReferenceViewingEnvironment is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate reference-viewing properties.");
|
|
}
|
|
|
|
track.ReferenceViewingEnvironment = HeifPropertyParser.ParseReferenceViewingEnvironment(data);
|
|
break;
|
|
case Heif4CharCode.Ndwt:
|
|
if (track.NominalDiffuseWhite is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate nominal-diffuse-white properties.");
|
|
}
|
|
|
|
track.NominalDiffuseWhite = HeifPropertyParser.ParseNominalDiffuseWhite(data);
|
|
break;
|
|
case Heif4CharCode.Clap:
|
|
if (track.CleanAperture is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate clean-aperture properties.");
|
|
}
|
|
|
|
HeifCleanAperture cleanAperture = HeifPropertyParser.ParseCleanAperture(data);
|
|
_ = cleanAperture.ToRectangle(new Size(track.CodedWidth, track.CodedHeight));
|
|
track.CleanAperture = cleanAperture;
|
|
break;
|
|
case Heif4CharCode.Irot:
|
|
if (track.RotationAngle is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate rotation properties.");
|
|
}
|
|
|
|
track.RotationAngle = HeifPropertyParser.ParseRotation(data);
|
|
break;
|
|
case Heif4CharCode.Imir:
|
|
if (track.MirrorAxis is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate mirror properties.");
|
|
}
|
|
|
|
track.MirrorAxis = HeifPropertyParser.ParseMirrorAxis(data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses one ICC or CICP color-information property from a visual sample entry.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the color-information payload.</param>
|
|
/// <param name="boxLength">The validated color-information payload length.</param>
|
|
/// <param name="track">The selected image track receiving the color description.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseTrackColorInformation(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 4, "color information");
|
|
Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix);
|
|
if (profileType == Heif4CharCode.Nclx)
|
|
{
|
|
try
|
|
{
|
|
if (track.CicpProfile is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate CICP color properties.");
|
|
}
|
|
|
|
if (boxLength != 11)
|
|
{
|
|
throw new InvalidImageContentException("The CICP color-information property has an invalid length.");
|
|
}
|
|
|
|
prefix = ReadPrefixFromStart(stream, boxLength, scratch, 11, "color information");
|
|
track.CicpProfile = HeifPropertyParser.ParseCicpProfile(prefix[4..]);
|
|
}
|
|
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex))
|
|
{
|
|
// IgnoreImageData permits the decoder to fall back to the coded sequence's color description.
|
|
}
|
|
}
|
|
else if ((profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) && !this.options.SkipMetadata)
|
|
{
|
|
try
|
|
{
|
|
if (track.IccProfile is not null)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample entry has duplicate ICC color properties.");
|
|
}
|
|
|
|
if (boxLength <= 4 || boxLength > int.MaxValue)
|
|
{
|
|
throw new InvalidImageContentException("The ICC color-information property is empty or too large.");
|
|
}
|
|
|
|
stream.Position -= 4;
|
|
using IMemoryOwner<byte> payload = this.boxReader.ReadPayload(stream, boxLength);
|
|
track.IccProfile = HeifPropertyParser.ParseIccProfile(payload.GetSpan()[4..]);
|
|
}
|
|
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex))
|
|
{
|
|
// A malformed optional ICC profile does not invalidate the coded image outside strict mode.
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a bounded fixed-size image property through the parser's reusable scratch buffer.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the property payload.</param>
|
|
/// <param name="boxLength">The validated property payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <param name="boxType">The property type used in malformed-image diagnostics.</param>
|
|
/// <returns>The complete property payload within <paramref name="scratch"/>.</returns>
|
|
private static ReadOnlySpan<byte> ReadPropertyPayload(Stream stream, long boxLength, Span<byte> scratch, Heif4CharCode boxType)
|
|
{
|
|
if (boxLength > scratch.Length)
|
|
{
|
|
throw new InvalidImageContentException($"The '{boxType}' image-sequence property exceeds its registered bounded size.");
|
|
}
|
|
|
|
return ReadPrefix(stream, boxLength, scratch, (int)boxLength, $"{boxType} image-sequence property");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses coding constraints that bound inter-picture references for an image sequence.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the coding-constraints payload.</param>
|
|
/// <param name="boxLength">The validated coding-constraints payload length.</param>
|
|
/// <param name="track">The selected track receiving coding constraints.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseCodingConstraints(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> data = ReadPrefix(stream, boxLength, scratch, 8, "coding constraints");
|
|
if (boxLength != 8)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence coding-constraints box has an invalid length.");
|
|
}
|
|
|
|
EnsureVersionAndFlags(data, 0, 0, "coding constraints");
|
|
uint constraints = BinaryPrimitives.ReadUInt32BigEndian(data[4..]);
|
|
if ((constraints & 0x03FFFFFF) != 0)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence coding constraints contain nonzero reserved bits.");
|
|
}
|
|
|
|
track.AllReferencePicturesIntra = (constraints & 0x80000000) != 0;
|
|
track.IntraPicturePredictionUsed = (constraints & 0x40000000) != 0;
|
|
track.MaximumReferencesPerPicture = (byte)((constraints >> 26) & 15);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether an auxiliary-track type identifies an alpha image sequence.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the auxiliary-type payload.</param>
|
|
/// <param name="boxLength">The validated auxiliary-type payload length.</param>
|
|
/// <returns><see langword="true"/> when the payload contains the registered HEIF alpha URN.</returns>
|
|
private bool ParseAuxiliaryType(Stream stream, long boxLength)
|
|
{
|
|
if (boxLength < 5 || boxLength > int.MaxValue)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence auxiliary type is truncated or too large.");
|
|
}
|
|
|
|
using IMemoryOwner<byte> payload = this.boxReader.ReadPayload(stream, boxLength);
|
|
ReadOnlySpan<byte> data = payload.GetSpan();
|
|
EnsureVersionAndFlags(data, 0, 0, "auxiliary type");
|
|
ReadOnlySpan<byte> type = data[4..];
|
|
if (type[^1] != 0)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence auxiliary type is not null terminated.");
|
|
}
|
|
|
|
type = type[..^1];
|
|
return type.SequenceEqual("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"u8);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses either full-width or compact sample sizes into the retained descriptor array.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-size payload.</param>
|
|
/// <param name="boxLength">The validated sample-size payload length.</param>
|
|
/// <param name="boxType">The full-width or compact sample-size box type.</param>
|
|
/// <param name="track">The selected track receiving retained sample lengths.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseSampleSizes(Stream stream, long boxLength, Heif4CharCode boxType, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
if (boxType == Heif4CharCode.Stsz)
|
|
{
|
|
this.ParseFullSampleSizes(stream, boxLength, track, scratch);
|
|
}
|
|
else
|
|
{
|
|
this.ParseCompactSampleSizes(stream, boxLength, track, scratch);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a full-width sample-size table without retaining entries beyond <see cref="maxFrames"/>.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-size payload.</param>
|
|
/// <param name="boxLength">The validated sample-size payload length.</param>
|
|
/// <param name="track">The selected track receiving retained sample lengths.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseFullSampleSizes(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 12, "sample sizes");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "sample sizes");
|
|
uint constantSize = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
|
|
uint sampleCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]);
|
|
if (sampleCount == 0)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample-size table is empty.");
|
|
}
|
|
|
|
long entryBytes = constantSize == 0 ? checked((long)sampleCount * 4) : 0;
|
|
if (boxLength != 12 + entryBytes)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence sample-size table length does not match its entry count.");
|
|
}
|
|
|
|
int retainedCount = (int)Math.Min(sampleCount, (uint)this.maxFrames);
|
|
track.TotalSampleCount = sampleCount;
|
|
track.Samples = new HeifSequenceSample[retainedCount];
|
|
if (constantSize != 0)
|
|
{
|
|
int size = ValidateSampleSize(constantSize);
|
|
Span<HeifSequenceSample> samples = track.Samples;
|
|
for (int i = 0; i < samples.Length; i++)
|
|
{
|
|
samples[i].Length = size;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sample sizes");
|
|
for (uint i = 0; i < sampleCount; i++)
|
|
{
|
|
int size = ValidateSampleSize(reader.ReadUInt32());
|
|
if (i < retainedCount)
|
|
{
|
|
track.Samples[(int)i].Length = size;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a compact sample-size table without expanding entries beyond <see cref="maxFrames"/>.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the compact sample-size payload.</param>
|
|
/// <param name="boxLength">The validated compact sample-size payload length.</param>
|
|
/// <param name="track">The selected track receiving retained sample lengths.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseCompactSampleSizes(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 12, "compact sample sizes");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "compact sample sizes");
|
|
if (prefix[4] != 0 || prefix[5] != 0 || prefix[6] != 0 || prefix[7] is not 4 and not 8 and not 16)
|
|
{
|
|
throw new InvalidImageContentException("The compact sample-size table has invalid reserved fields or field width.");
|
|
}
|
|
|
|
int fieldSize = prefix[7];
|
|
uint sampleCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]);
|
|
if (sampleCount == 0)
|
|
{
|
|
throw new InvalidImageContentException("The compact sample-size table is empty.");
|
|
}
|
|
|
|
long entryBytes = checked((((long)sampleCount * fieldSize) + 7) / 8);
|
|
if (boxLength != 12 + entryBytes)
|
|
{
|
|
throw new InvalidImageContentException("The compact sample-size table length does not match its entry count.");
|
|
}
|
|
|
|
int retainedCount = (int)Math.Min(sampleCount, (uint)this.maxFrames);
|
|
track.TotalSampleCount = sampleCount;
|
|
track.Samples = new HeifSequenceSample[retainedCount];
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "compact sample sizes");
|
|
for (uint i = 0; i < sampleCount; i++)
|
|
{
|
|
uint size;
|
|
if (fieldSize == 4)
|
|
{
|
|
byte packed = reader.ReadByte();
|
|
size = (uint)(packed >> 4);
|
|
if (i < retainedCount)
|
|
{
|
|
track.Samples[(int)i].Length = ValidateSampleSize(size);
|
|
}
|
|
|
|
i++;
|
|
if (i >= sampleCount)
|
|
{
|
|
if ((packed & 15) != 0)
|
|
{
|
|
throw new InvalidImageContentException("The compact sample-size table has nonzero padding bits.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
size = (uint)(packed & 15);
|
|
}
|
|
else
|
|
{
|
|
size = fieldSize == 8 ? reader.ReadByte() : reader.ReadUInt16();
|
|
}
|
|
|
|
int validatedSize = ValidateSampleSize(size);
|
|
if (i < retainedCount)
|
|
{
|
|
track.Samples[(int)i].Length = validatedSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands retained sample durations and validates the complete timing run table.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the time-to-sample payload.</param>
|
|
/// <param name="boxLength">The validated time-to-sample payload length.</param>
|
|
/// <param name="track">The selected track receiving retained durations.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The total decoded duration in media-time-scale units.</returns>
|
|
private static ulong ParseSampleTiming(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample timing");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "sample timing");
|
|
uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
|
|
long entryBytes = checked((long)entryCount * 8);
|
|
if (entryCount == 0 || boxLength != 8 + entryBytes)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence timing table is empty or has an invalid length.");
|
|
}
|
|
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sample timing");
|
|
ulong describedSamples = 0;
|
|
ulong decodedDuration = 0;
|
|
int retainedOffset = 0;
|
|
for (uint entry = 0; entry < entryCount; entry++)
|
|
{
|
|
uint sampleCount = reader.ReadUInt32();
|
|
uint sampleDelta = reader.ReadUInt32();
|
|
if (sampleCount == 0 || sampleDelta == 0)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence timing table contains a zero run or duration.");
|
|
}
|
|
|
|
describedSamples = checked(describedSamples + sampleCount);
|
|
decodedDuration = checked(decodedDuration + ((ulong)sampleCount * sampleDelta));
|
|
int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), track.Samples.Length - retainedOffset);
|
|
Span<HeifSequenceSample> samples = track.Samples;
|
|
for (int i = 0; i < retainedRun; i++)
|
|
{
|
|
samples[retainedOffset + i].Duration = sampleDelta;
|
|
}
|
|
|
|
retainedOffset += retainedRun;
|
|
}
|
|
|
|
if (describedSamples != track.TotalSampleCount)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence timing table does not describe every sample.");
|
|
}
|
|
|
|
return decodedDuration;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads and validates the declared chunk count without retaining chunk offsets.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at a chunk-offset payload.</param>
|
|
/// <param name="boxLength">The validated chunk-offset payload length.</param>
|
|
/// <param name="boxType">The 32-bit or 64-bit chunk-offset box type.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The nonzero number of chunks.</returns>
|
|
private static uint ReadChunkCount(Stream stream, long boxLength, Heif4CharCode boxType, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "chunk offsets");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "chunk offsets");
|
|
uint chunkCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
|
|
int entrySize = boxType == Heif4CharCode.Co64 ? 8 : 4;
|
|
if (chunkCount == 0 || boxLength != 8 + checked((long)chunkCount * entrySize))
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence chunk-offset table is empty or has an invalid length.");
|
|
}
|
|
|
|
return chunkCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses sample-to-chunk runs into allocator-owned state bounded by the retained frame count.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-to-chunk payload.</param>
|
|
/// <param name="boxLength">The validated sample-to-chunk payload length.</param>
|
|
/// <param name="chunkCount">The validated number of chunks.</param>
|
|
/// <param name="track">The selected track whose complete sample count is validated.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <param name="retainedEntryCount">Receives the number of retained mapping entries.</param>
|
|
/// <returns>Allocator-owned sample-to-chunk runs that cover all retained samples.</returns>
|
|
private IMemoryOwner<SampleToChunkEntry> ParseSampleToChunk(
|
|
Stream stream,
|
|
long boxLength,
|
|
uint chunkCount,
|
|
HeifSequenceTrack track,
|
|
Span<byte> scratch,
|
|
out int retainedEntryCount)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-to-chunk");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "sample-to-chunk");
|
|
uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
|
|
long entryBytes = checked((long)entryCount * 12);
|
|
if (entryCount == 0 || entryCount > chunkCount || boxLength != 8 + entryBytes)
|
|
{
|
|
throw new InvalidImageContentException("The sample-to-chunk table is empty or has an invalid length or entry count.");
|
|
}
|
|
|
|
int retainedCapacity = (int)Math.Min(entryCount, (uint)track.Samples.Length);
|
|
IMemoryOwner<SampleToChunkEntry> owner = this.allocator.Allocate<SampleToChunkEntry>(retainedCapacity);
|
|
Span<SampleToChunkEntry> retainedEntries = owner.GetSpan();
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sample-to-chunk");
|
|
uint previousFirstChunk = 0;
|
|
uint previousSamplesPerChunk = 0;
|
|
ulong describedSamples = 0;
|
|
retainedEntryCount = 0;
|
|
|
|
try
|
|
{
|
|
for (uint i = 0; i < entryCount; i++)
|
|
{
|
|
uint firstChunk = reader.ReadUInt32();
|
|
uint samplesPerChunk = reader.ReadUInt32();
|
|
uint sampleDescriptionIndex = reader.ReadUInt32();
|
|
if ((i == 0 && firstChunk != 1) || firstChunk <= previousFirstChunk || firstChunk > chunkCount
|
|
|| samplesPerChunk == 0 || sampleDescriptionIndex != 1)
|
|
{
|
|
throw new InvalidImageContentException("The sample-to-chunk table contains an invalid run.");
|
|
}
|
|
|
|
if (i != 0)
|
|
{
|
|
describedSamples = checked(describedSamples + ((ulong)(firstChunk - previousFirstChunk) * previousSamplesPerChunk));
|
|
}
|
|
|
|
if (retainedEntryCount < retainedEntries.Length)
|
|
{
|
|
retainedEntries[retainedEntryCount++] = new SampleToChunkEntry(firstChunk, samplesPerChunk);
|
|
}
|
|
|
|
previousFirstChunk = firstChunk;
|
|
previousSamplesPerChunk = samplesPerChunk;
|
|
}
|
|
|
|
describedSamples = checked(describedSamples + ((ulong)(chunkCount + 1U - previousFirstChunk) * previousSamplesPerChunk));
|
|
if (describedSamples != track.TotalSampleCount)
|
|
{
|
|
throw new InvalidImageContentException("The sample-to-chunk table does not map every declared sample.");
|
|
}
|
|
|
|
return owner;
|
|
}
|
|
catch
|
|
{
|
|
owner.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves retained samples directly from sequential chunk offsets and compact mapping runs.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the chunk-offset payload.</param>
|
|
/// <param name="boxLength">The validated chunk-offset payload length.</param>
|
|
/// <param name="boxType">The 32-bit or 64-bit chunk-offset box type.</param>
|
|
/// <param name="chunkCount">The validated number of chunks.</param>
|
|
/// <param name="entries">The retained sample-to-chunk runs.</param>
|
|
/// <param name="track">The selected track receiving absolute sample locations.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ResolveSampleLocations(
|
|
Stream stream,
|
|
long boxLength,
|
|
Heif4CharCode boxType,
|
|
uint chunkCount,
|
|
ReadOnlySpan<SampleToChunkEntry> entries,
|
|
HeifSequenceTrack track,
|
|
Span<byte> scratch)
|
|
{
|
|
_ = ReadChunkCount(stream, boxLength, boxType, scratch);
|
|
int entrySize = boxType == Heif4CharCode.Co64 ? 8 : 4;
|
|
HeifBoxPayloadReader reader = new(stream, checked((long)chunkCount * entrySize), scratch, "chunk offsets");
|
|
int retainedSample = 0;
|
|
int runIndex = 0;
|
|
for (uint chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++)
|
|
{
|
|
ulong chunkOffset = entrySize == 8 ? reader.ReadUInt64() : reader.ReadUInt32();
|
|
if (chunkOffset > (ulong)stream.Length)
|
|
{
|
|
throw new InvalidImageContentException("An image-sequence chunk offset extends beyond the file.");
|
|
}
|
|
|
|
uint chunkNumber = chunkIndex + 1;
|
|
if (runIndex + 1 < entries.Length && entries[runIndex + 1].FirstChunk <= chunkNumber)
|
|
{
|
|
runIndex++;
|
|
}
|
|
|
|
uint samplesPerChunk = entries[runIndex].SamplesPerChunk;
|
|
ulong sampleOffset = chunkOffset;
|
|
for (uint sampleInChunk = 0; sampleInChunk < samplesPerChunk && retainedSample < track.Samples.Length; sampleInChunk++)
|
|
{
|
|
ref HeifSequenceSample sample = ref track.Samples[retainedSample++];
|
|
ulong sampleEnd = checked(sampleOffset + (uint)sample.Length);
|
|
if (sampleEnd > (ulong)stream.Length || sampleOffset > long.MaxValue)
|
|
{
|
|
throw new InvalidImageContentException("An image-sequence sample extends beyond the file.");
|
|
}
|
|
|
|
sample.Offset = (long)sampleOffset;
|
|
sampleOffset = sampleEnd;
|
|
}
|
|
}
|
|
|
|
if (retainedSample != track.Samples.Length)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence chunk table does not locate every retained sample.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies explicit one-based sync-sample declarations to retained sample descriptors.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sync-sample payload.</param>
|
|
/// <param name="boxLength">The validated sync-sample payload length.</param>
|
|
/// <param name="track">The selected track receiving random-access markers.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseSyncSamples(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sync samples");
|
|
EnsureVersionAndFlags(prefix, 0, 0, "sync samples");
|
|
uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
|
|
long entryBytes = checked((long)entryCount * 4);
|
|
if (entryCount == 0 || boxLength != 8 + entryBytes)
|
|
{
|
|
throw new InvalidImageContentException("The sync-sample table is empty or has an invalid length.");
|
|
}
|
|
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sync samples");
|
|
uint previousSample = 0;
|
|
for (uint i = 0; i < entryCount; i++)
|
|
{
|
|
uint sampleNumber = reader.ReadUInt32();
|
|
if (sampleNumber <= previousSample || sampleNumber > track.TotalSampleCount)
|
|
{
|
|
throw new InvalidImageContentException("The sync-sample table contains an invalid sample number.");
|
|
}
|
|
|
|
if (sampleNumber <= track.Samples.Length)
|
|
{
|
|
track.Samples[(int)sampleNumber - 1].IsSync = true;
|
|
}
|
|
|
|
previousSample = sampleNumber;
|
|
}
|
|
|
|
if (!track.Samples[0].IsSync)
|
|
{
|
|
throw new InvalidImageContentException("The first retained image-sequence sample is not a random-access sample.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the image-specific direct-reference sample group into compact zero-based sample indices.
|
|
/// </summary>
|
|
/// <param name="stream">The seekable source stream.</param>
|
|
/// <param name="descriptions">The validated direct-reference group-description payload.</param>
|
|
/// <param name="sampleMap">The validated direct-reference sample-map payload.</param>
|
|
/// <param name="track">The selected image track receiving its dependency graph.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private void ParseDirectReferences(
|
|
Stream stream,
|
|
BoxReference descriptions,
|
|
BoxReference sampleMap,
|
|
HeifSequenceTrack track,
|
|
Span<byte> scratch)
|
|
{
|
|
using IMemoryOwner<SampleGroupAssignment> assignmentOwner = this.allocator.Allocate<SampleGroupAssignment>(track.Samples.Length);
|
|
Span<SampleGroupAssignment> assignments = assignmentOwner.GetSpan()[..track.Samples.Length];
|
|
stream.Position = sampleMap.Offset;
|
|
uint greatestGroupIndex = ParseSampleToGroup(stream, sampleMap.Length, track, assignments, scratch);
|
|
|
|
// Sorting the retained value-type assignments lets each group description be applied in one sequential pass.
|
|
// This avoids a dictionary and prevents attacker-controlled group counts from causing quadratic lookup work.
|
|
assignments.Sort();
|
|
stream.Position = descriptions.Offset;
|
|
int directReferenceCount = ParseDirectReferenceDescriptions(
|
|
stream,
|
|
descriptions.Length,
|
|
greatestGroupIndex,
|
|
track,
|
|
assignments,
|
|
[],
|
|
false,
|
|
scratch);
|
|
|
|
using IMemoryOwner<SampleIdIndexEntry> sampleIdOwner = this.allocator.Allocate<SampleIdIndexEntry>(track.Samples.Length);
|
|
Span<SampleIdIndexEntry> sampleIds = sampleIdOwner.GetSpan()[..track.Samples.Length];
|
|
int sampleIdCount = 0;
|
|
Span<HeifSequenceSample> samples = track.Samples;
|
|
for (int i = 0; i < samples.Length; i++)
|
|
{
|
|
uint sampleId = samples[i].SampleId;
|
|
if (sampleId != 0)
|
|
{
|
|
sampleIds[sampleIdCount++] = new SampleIdIndexEntry(sampleId, i);
|
|
}
|
|
}
|
|
|
|
sampleIds = sampleIds[..sampleIdCount];
|
|
sampleIds.Sort();
|
|
for (int i = 1; i < sampleIds.Length; i++)
|
|
{
|
|
if (sampleIds[i - 1].Id == sampleIds[i].Id)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group contains a duplicate positive sample identifier.");
|
|
}
|
|
}
|
|
|
|
if (directReferenceCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
track.DirectReferenceSampleIndices = new int[directReferenceCount];
|
|
stream.Position = descriptions.Offset;
|
|
_ = ParseDirectReferenceDescriptions(
|
|
stream,
|
|
descriptions.Length,
|
|
greatestGroupIndex,
|
|
track,
|
|
assignments,
|
|
sampleIds,
|
|
true,
|
|
scratch);
|
|
|
|
if (track.AllReferencePicturesIntra)
|
|
{
|
|
ReadOnlySpan<int> referenceIndices = track.DirectReferenceSampleIndices;
|
|
for (int i = 0; i < referenceIndices.Length; i++)
|
|
{
|
|
if (samples[referenceIndices[i]].DirectReferenceCount != 0)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group contradicts its all-reference-pictures-intra constraint.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands the run-length sample-to-group map only for samples retained by the decoder.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-to-group payload.</param>
|
|
/// <param name="boxLength">The validated sample-to-group payload length.</param>
|
|
/// <param name="track">The selected track whose complete sample count is validated.</param>
|
|
/// <param name="assignments">The exact retained assignment span.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The greatest group-description index used by any declared sample.</returns>
|
|
private static uint ParseSampleToGroup(
|
|
Stream stream,
|
|
long boxLength,
|
|
HeifSequenceTrack track,
|
|
Span<SampleGroupAssignment> assignments,
|
|
Span<byte> scratch)
|
|
{
|
|
long payloadStart = stream.Position;
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-to-group");
|
|
byte version = prefix[0];
|
|
if (version is not 0 and not 1 || ReadFlags(prefix) != 0
|
|
|| (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != Heif4CharCode.Refs)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample-to-group box has an unsupported version, flags, or grouping type.");
|
|
}
|
|
|
|
int headerLength = version == 0 ? 12 : 16;
|
|
stream.Position = payloadStart;
|
|
prefix = ReadPrefix(stream, boxLength, scratch, headerLength, "sample-to-group");
|
|
if (version == 1 && BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]) != 0)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group has a nonzero grouping-type parameter.");
|
|
}
|
|
|
|
int entryCountOffset = version == 0 ? 8 : 12;
|
|
uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[entryCountOffset..]);
|
|
long entryBytes = checked((long)entryCount * 8);
|
|
if (entryCount == 0 || boxLength != headerLength + entryBytes)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample map is empty or has an invalid length.");
|
|
}
|
|
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "direct-reference sample map");
|
|
ulong describedSamples = 0;
|
|
int retainedOffset = 0;
|
|
uint greatestGroupIndex = 0;
|
|
for (uint i = 0; i < entryCount; i++)
|
|
{
|
|
uint sampleCount = reader.ReadUInt32();
|
|
uint groupDescriptionIndex = reader.ReadUInt32();
|
|
if (sampleCount == 0)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample map contains a zero-length run.");
|
|
}
|
|
|
|
describedSamples = checked(describedSamples + sampleCount);
|
|
greatestGroupIndex = Math.Max(greatestGroupIndex, groupDescriptionIndex);
|
|
int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), assignments.Length - retainedOffset);
|
|
for (int j = 0; j < retainedRun; j++)
|
|
{
|
|
assignments[retainedOffset + j] = new SampleGroupAssignment(groupDescriptionIndex, retainedOffset + j);
|
|
}
|
|
|
|
retainedOffset += retainedRun;
|
|
}
|
|
|
|
if (describedSamples != track.TotalSampleCount || retainedOffset != assignments.Length)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample map does not describe every sample.");
|
|
}
|
|
|
|
return greatestGroupIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses direct-reference descriptions, first sizing and then resolving the retained dependency graph.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the sample-group-description payload.</param>
|
|
/// <param name="boxLength">The validated sample-group-description payload length.</param>
|
|
/// <param name="greatestGroupIndex">The greatest description index used by the complete sample map.</param>
|
|
/// <param name="track">The selected track receiving sample identifiers and dependency indices.</param>
|
|
/// <param name="assignments">The retained sample assignments sorted by group-description index.</param>
|
|
/// <param name="sampleIds">The sorted positive sample identifiers, or an empty span during the sizing pass.</param>
|
|
/// <param name="resolveReferences">Whether this pass resolves reference identifiers into compact sample indices.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The exact number of retained direct-reference indices.</returns>
|
|
private static int ParseDirectReferenceDescriptions(
|
|
Stream stream,
|
|
long boxLength,
|
|
uint greatestGroupIndex,
|
|
HeifSequenceTrack track,
|
|
ReadOnlySpan<SampleGroupAssignment> assignments,
|
|
ReadOnlySpan<SampleIdIndexEntry> sampleIds,
|
|
bool resolveReferences,
|
|
Span<byte> scratch)
|
|
{
|
|
long payloadStart = stream.Position;
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-group descriptions");
|
|
byte version = prefix[0];
|
|
if (version is not 1 and not 2 || ReadFlags(prefix) != 0
|
|
|| (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != Heif4CharCode.Refs)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample-group-description box has an unsupported version, flags, or grouping type.");
|
|
}
|
|
|
|
int headerLength = version == 1 ? 16 : 20;
|
|
stream.Position = payloadStart;
|
|
prefix = ReadPrefix(stream, boxLength, scratch, headerLength, "sample-group descriptions");
|
|
uint defaultLength = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]);
|
|
uint defaultGroupIndex = version == 2 ? BinaryPrimitives.ReadUInt32BigEndian(prefix[12..]) : 0;
|
|
int entryCountOffset = version == 1 ? 12 : 16;
|
|
uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[entryCountOffset..]);
|
|
if (greatestGroupIndex > entryCount || defaultGroupIndex > entryCount)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample map uses an undefined group-description index.");
|
|
}
|
|
|
|
long entryBytes = boxLength - headerLength;
|
|
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "direct-reference descriptions");
|
|
long consumedBytes = 0;
|
|
int assignmentOffset = 0;
|
|
int directReferenceCount = 0;
|
|
while (assignmentOffset < assignments.Length && assignments[assignmentOffset].GroupDescriptionIndex == 0)
|
|
{
|
|
assignmentOffset++;
|
|
}
|
|
|
|
for (uint entry = 0; entry < entryCount; entry++)
|
|
{
|
|
uint descriptionIndex = entry + 1;
|
|
uint descriptionLength = defaultLength;
|
|
if (descriptionLength == 0)
|
|
{
|
|
if (entryBytes - consumedBytes < 4)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample-group description is truncated.");
|
|
}
|
|
|
|
descriptionLength = reader.ReadUInt32();
|
|
consumedBytes += 4;
|
|
}
|
|
|
|
if (descriptionLength < 5 || descriptionLength > entryBytes - consumedBytes)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample-group description has an invalid length.");
|
|
}
|
|
|
|
uint sampleId = reader.ReadUInt32();
|
|
byte referenceCount = reader.ReadByte();
|
|
uint requiredLength = 5U + ((uint)referenceCount * 4U);
|
|
if (descriptionLength != requiredLength)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample-group entry has an invalid length.");
|
|
}
|
|
|
|
int firstAssignment = assignmentOffset;
|
|
while (assignmentOffset < assignments.Length && assignments[assignmentOffset].GroupDescriptionIndex == descriptionIndex)
|
|
{
|
|
int sampleIndex = assignments[assignmentOffset].SampleIndex;
|
|
ref HeifSequenceSample sample = ref track.Samples[sampleIndex];
|
|
if (!resolveReferences)
|
|
{
|
|
if (referenceCount > track.MaximumReferencesPerPicture || directReferenceCount > int.MaxValue - referenceCount)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group exceeds its coding constraints or supported size.");
|
|
}
|
|
|
|
sample.SampleId = sampleId;
|
|
sample.DirectReferenceOffset = directReferenceCount;
|
|
sample.DirectReferenceCount = referenceCount;
|
|
directReferenceCount += referenceCount;
|
|
}
|
|
else if (sample.SampleId != sampleId || sample.DirectReferenceCount != referenceCount)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group changed between parser passes.");
|
|
}
|
|
|
|
assignmentOffset++;
|
|
}
|
|
|
|
for (int reference = 0; reference < referenceCount; reference++)
|
|
{
|
|
uint referenceSampleId = reader.ReadUInt32();
|
|
if (referenceSampleId == 0)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample group contains identifier zero in a reference list.");
|
|
}
|
|
|
|
if (resolveReferences)
|
|
{
|
|
int low = 0;
|
|
int high = sampleIds.Length - 1;
|
|
while (low <= high)
|
|
{
|
|
int middle = low + ((high - low) >> 1);
|
|
uint candidate = sampleIds[middle].Id;
|
|
if (candidate < referenceSampleId)
|
|
{
|
|
low = middle + 1;
|
|
}
|
|
else if (candidate > referenceSampleId)
|
|
{
|
|
high = middle - 1;
|
|
}
|
|
else
|
|
{
|
|
low = middle;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (low >= sampleIds.Length || sampleIds[low].Id != referenceSampleId)
|
|
{
|
|
throw new InvalidImageContentException("A direct-reference sample identifier does not name a retained sample.");
|
|
}
|
|
|
|
int referencedSampleIndex = sampleIds[low].SampleIndex;
|
|
for (int assignment = firstAssignment; assignment < assignmentOffset; assignment++)
|
|
{
|
|
int sampleIndex = assignments[assignment].SampleIndex;
|
|
HeifSequenceSample sample = track.Samples[sampleIndex];
|
|
if (sample.IsSync || referencedSampleIndex >= sampleIndex)
|
|
{
|
|
throw new InvalidImageContentException("A direct-reference sample is not earlier in decode order or is attached to a sync sample.");
|
|
}
|
|
|
|
track.DirectReferenceSampleIndices[sample.DirectReferenceOffset + reference] = referencedSampleIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
consumedBytes += descriptionLength;
|
|
}
|
|
|
|
if (consumedBytes != entryBytes || assignmentOffset != assignments.Length)
|
|
{
|
|
throw new InvalidImageContentException("The direct-reference sample-group descriptions do not cover the retained sample map.");
|
|
}
|
|
|
|
return directReferenceCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the grouping type shared by a sample map or sample-group-description box.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the full-box payload.</param>
|
|
/// <param name="boxLength">The validated payload length.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
/// <returns>The declared grouping type.</returns>
|
|
private static Heif4CharCode ReadSampleGroupType(Stream stream, long boxLength, Span<byte> scratch)
|
|
{
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample group");
|
|
return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes retained sample composition times while preserving decode-order storage.
|
|
/// </summary>
|
|
/// <param name="track">The selected track whose durations and offsets have been validated.</param>
|
|
private static void SetCompositionTimes(HeifSequenceTrack track)
|
|
{
|
|
long decodeTime = 0;
|
|
Span<HeifSequenceSample> samples = track.Samples;
|
|
for (int i = 0; i < samples.Length; i++)
|
|
{
|
|
ref HeifSequenceSample sample = ref samples[i];
|
|
sample.CompositionTime = sample.IsHidden ? long.MinValue : checked(decodeTime + sample.CompositionOffset);
|
|
decodeTime = checked(decodeTime + sample.Duration);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the single normal-rate edit list used to signal image-sequence repetition.
|
|
/// </summary>
|
|
/// <param name="stream">The stream positioned at the edit-container payload.</param>
|
|
/// <param name="boxLength">The validated edit-container payload length.</param>
|
|
/// <param name="track">The selected track receiving repetition behavior.</param>
|
|
/// <param name="scratch">The parser-owned reusable scratch span.</param>
|
|
private static void ParseEdit(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
|
|
{
|
|
long editEnd = checked(stream.Position + boxLength);
|
|
BoxReference editList = default;
|
|
while (stream.Position < editEnd)
|
|
{
|
|
long childLength = HeifBoxReader.ReadHeader(stream, editEnd, scratch, out Heif4CharCode childType);
|
|
long childStart = stream.Position;
|
|
if (childType == Heif4CharCode.Elst)
|
|
{
|
|
SetUnique(ref editList, childStart, childLength, "edit", childType);
|
|
}
|
|
|
|
stream.Position = checked(childStart + childLength);
|
|
}
|
|
|
|
if (!editList.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence edit container has no edit list.");
|
|
}
|
|
|
|
stream.Position = editList.Offset;
|
|
ReadOnlySpan<byte> prefix = ReadPrefix(stream, editList.Length, scratch, 8, "edit list");
|
|
byte version = prefix[0];
|
|
uint flags = ReadFlags(prefix);
|
|
int entryLength = version switch
|
|
{
|
|
0 => 12,
|
|
1 => 20,
|
|
_ => throw new InvalidImageContentException($"The edit list has unsupported version {version}.")
|
|
};
|
|
|
|
if ((flags & ~1U) != 0 || BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1 || editList.Length != 8 + entryLength)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence edit list has unsupported flags, entries, or length.");
|
|
}
|
|
|
|
prefix = ReadPrefix(stream, entryLength, scratch, entryLength, "edit-list entry");
|
|
ulong segmentDuration;
|
|
long mediaTime;
|
|
int rateOffset;
|
|
if (version == 0)
|
|
{
|
|
segmentDuration = BinaryPrimitives.ReadUInt32BigEndian(prefix);
|
|
mediaTime = BinaryPrimitives.ReadInt32BigEndian(prefix[4..]);
|
|
rateOffset = 8;
|
|
}
|
|
else
|
|
{
|
|
segmentDuration = BinaryPrimitives.ReadUInt64BigEndian(prefix);
|
|
mediaTime = BinaryPrimitives.ReadInt64BigEndian(prefix[8..]);
|
|
rateOffset = 16;
|
|
}
|
|
|
|
if (segmentDuration == 0 || mediaTime != 0 || BinaryPrimitives.ReadInt16BigEndian(prefix[rateOffset..]) != 1
|
|
|| BinaryPrimitives.ReadInt16BigEndian(prefix[(rateOffset + 2)..]) != 0)
|
|
{
|
|
throw new InvalidImageContentException("The image-sequence edit list requires unsupported splicing or playback rate behavior.");
|
|
}
|
|
|
|
if ((flags & 1) == 0)
|
|
{
|
|
track.RepeatCount = 1;
|
|
}
|
|
else if (track.TrackDuration == ulong.MaxValue)
|
|
{
|
|
track.RepeatCount = 0;
|
|
}
|
|
else
|
|
{
|
|
if (track.TrackDuration == 0)
|
|
{
|
|
throw new InvalidImageContentException("A repeating image-sequence track has zero duration.");
|
|
}
|
|
|
|
ulong plays = (track.TrackDuration / segmentDuration) + (track.TrackDuration % segmentDuration == 0 ? 0UL : 1UL);
|
|
|
|
// The public metadata uses zero for indefinite repetition. Preserve extremely large finite edit counts as
|
|
// indefinite rather than wrapping the observable ushort play count.
|
|
track.RepeatCount = plays is 0 or > ushort.MaxValue ? (ushort)0 : (ushort)plays;
|
|
}
|
|
|
|
track.HasEditList = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a linked alpha track can be matched frame-for-frame with its master color track.
|
|
/// </summary>
|
|
/// <param name="colorTrack">The selected master color track.</param>
|
|
/// <param name="alphaTrack">The optional linked alpha track.</param>
|
|
private static void ValidateAlphaTrack(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack)
|
|
{
|
|
if (colorTrack.PremultipliedByTrackId != 0)
|
|
{
|
|
if (alphaTrack is null || colorTrack.PremultipliedByTrackId != alphaTrack.Id)
|
|
{
|
|
throw new InvalidImageContentException("The color image-sequence track references an unrelated premultiplication track.");
|
|
}
|
|
|
|
colorTrack.IsPremultiplied = true;
|
|
}
|
|
|
|
if (alphaTrack is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (alphaTrack.TotalSampleCount != colorTrack.TotalSampleCount || alphaTrack.Samples.Length != colorTrack.Samples.Length)
|
|
{
|
|
throw new InvalidImageContentException("The alpha and color image-sequence tracks contain different sample counts.");
|
|
}
|
|
|
|
for (int i = 0; i < colorTrack.Samples.Length; i++)
|
|
{
|
|
HeifSequenceSample colorSample = colorTrack.Samples[i];
|
|
HeifSequenceSample alphaSample = alphaTrack.Samples[i];
|
|
ulong colorDuration = (ulong)colorSample.Duration * alphaTrack.MediaTimescale;
|
|
ulong alphaDuration = (ulong)alphaSample.Duration * colorTrack.MediaTimescale;
|
|
if (colorDuration != alphaDuration)
|
|
{
|
|
throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation durations.");
|
|
}
|
|
|
|
if (colorSample.IsHidden != alphaSample.IsHidden)
|
|
{
|
|
throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation visibility.");
|
|
}
|
|
|
|
if (!colorSample.IsHidden)
|
|
{
|
|
// Cross-multiplication retains exact signed presentation times without floating-point rounding or overflow.
|
|
Int128 colorCompositionTime = (Int128)colorSample.CompositionTime * alphaTrack.MediaTimescale;
|
|
Int128 alphaCompositionTime = (Int128)alphaSample.CompositionTime * colorTrack.MediaTimescale;
|
|
if (colorCompositionTime != alphaCompositionTime)
|
|
{
|
|
throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation times.");
|
|
}
|
|
}
|
|
}
|
|
|
|
bool alphaHasPresentationProperties = alphaTrack.CleanAperture is not null || alphaTrack.RotationAngle is not null || alphaTrack.MirrorAxis is not null;
|
|
if (alphaHasPresentationProperties &&
|
|
(!Nullable.Equals(colorTrack.CleanAperture, alphaTrack.CleanAperture) ||
|
|
colorTrack.RotationAngle != alphaTrack.RotationAngle ||
|
|
colorTrack.MirrorAxis != alphaTrack.MirrorAxis))
|
|
{
|
|
// libavif accepts legacy alpha tracks with no transform properties, but requires exact equality when any
|
|
// alpha transform is declared because composition occurs before the shared color-track presentation step.
|
|
throw new NotSupportedException("The alpha and color image-sequence tracks use different presentation transforms.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates and narrows a sample size to the decoder's contiguous-buffer length type.
|
|
/// </summary>
|
|
/// <param name="size">The file-defined unsigned sample size.</param>
|
|
/// <returns>The positive sample size as an <see cref="int"/>.</returns>
|
|
private static int ValidateSampleSize(uint size)
|
|
{
|
|
if (size == 0 || size > int.MaxValue)
|
|
{
|
|
throw new InvalidImageContentException("An image-sequence sample has an unsupported size.");
|
|
}
|
|
|
|
return (int)size;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records one unique child box while retaining only its stream range.
|
|
/// </summary>
|
|
/// <param name="reference">The child reference owned by the bounded parent parser.</param>
|
|
/// <param name="offset">The absolute payload offset.</param>
|
|
/// <param name="length">The validated payload length.</param>
|
|
/// <param name="parentName">The parent name used in malformed-image diagnostics.</param>
|
|
/// <param name="boxType">The unique child box type.</param>
|
|
private static void SetUnique(ref BoxReference reference, long offset, long length, string parentName, Heif4CharCode boxType)
|
|
{
|
|
if (reference.IsPresent)
|
|
{
|
|
throw new InvalidImageContentException($"The {parentName} box contains duplicate '{boxType}' boxes.");
|
|
}
|
|
|
|
reference = new BoxReference(offset, length, boxType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a fixed prefix from the stream's current position.
|
|
/// </summary>
|
|
/// <param name="stream">The source stream.</param>
|
|
/// <param name="boxLength">The validated enclosing payload length.</param>
|
|
/// <param name="scratch">The reusable destination scratch span.</param>
|
|
/// <param name="length">The required prefix length.</param>
|
|
/// <param name="name">The payload name used in malformed-image diagnostics.</param>
|
|
/// <returns>The requested prefix within <paramref name="scratch"/>.</returns>
|
|
private static ReadOnlySpan<byte> ReadPrefix(Stream stream, long boxLength, Span<byte> scratch, int length, string name)
|
|
{
|
|
if (boxLength < length)
|
|
{
|
|
throw new InvalidImageContentException($"The {name} payload is truncated.");
|
|
}
|
|
|
|
Span<byte> destination = scratch[..length];
|
|
HeifBoxReader.ReadExactly(stream, destination, $"The {name} payload is truncated.");
|
|
return destination;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rewinds to the start of a partially read payload and reads a larger fixed prefix.
|
|
/// </summary>
|
|
/// <param name="stream">The source stream positioned after a four-byte prefix.</param>
|
|
/// <param name="boxLength">The validated enclosing payload length.</param>
|
|
/// <param name="scratch">The reusable destination scratch span.</param>
|
|
/// <param name="length">The required prefix length.</param>
|
|
/// <param name="name">The payload name used in malformed-image diagnostics.</param>
|
|
/// <returns>The requested prefix within <paramref name="scratch"/>.</returns>
|
|
private static ReadOnlySpan<byte> ReadPrefixFromStart(Stream stream, long boxLength, Span<byte> scratch, int length, string name)
|
|
{
|
|
stream.Position -= 4;
|
|
return ReadPrefix(stream, boxLength, scratch, length, name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the lower 24-bit flags field from a full-box prefix.
|
|
/// </summary>
|
|
/// <param name="data">The prefix beginning with version and flags.</param>
|
|
/// <returns>The unsigned flags value.</returns>
|
|
private static uint ReadFlags(ReadOnlySpan<byte> data) => BinaryPrimitives.ReadUInt32BigEndian(data) & 0x00FFFFFF;
|
|
|
|
/// <summary>
|
|
/// Requires a full-box prefix to use one supported version and flags value.
|
|
/// </summary>
|
|
/// <param name="data">The prefix beginning with version and flags.</param>
|
|
/// <param name="version">The required version.</param>
|
|
/// <param name="flags">The required flags.</param>
|
|
/// <param name="name">The box name used in malformed-image diagnostics.</param>
|
|
private static void EnsureVersionAndFlags(ReadOnlySpan<byte> data, byte version, uint flags, string name)
|
|
{
|
|
if (data[0] != version || ReadFlags(data) != flags)
|
|
{
|
|
throw new InvalidImageContentException($"The {name} box has unsupported version or flags.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requires a full-box prefix to contain zero flags.
|
|
/// </summary>
|
|
/// <param name="data">The prefix beginning with version and flags.</param>
|
|
/// <param name="name">The box name used in malformed-image diagnostics.</param>
|
|
private static void EnsureZeroFlags(ReadOnlySpan<byte> data, string name)
|
|
{
|
|
if (ReadFlags(data) != 0)
|
|
{
|
|
throw new InvalidImageContentException($"The {name} box has unsupported flags.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies a selected track without retaining its codec or sample-table payloads.
|
|
/// </summary>
|
|
private struct TrackIdentity
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="TrackIdentity"/> struct.
|
|
/// </summary>
|
|
/// <param name="id">The file-defined track identifier.</param>
|
|
/// <param name="isEnabled">Whether the track is enabled.</param>
|
|
/// <param name="width">The displayed track width.</param>
|
|
/// <param name="height">The displayed track height.</param>
|
|
/// <param name="trackDuration">The track duration in movie-time-scale units.</param>
|
|
public TrackIdentity(uint id, bool isEnabled, int width, int height, ulong trackDuration)
|
|
{
|
|
this.Id = id;
|
|
this.IsEnabled = isEnabled;
|
|
this.Width = width;
|
|
this.Height = height;
|
|
this.TrackDuration = trackDuration;
|
|
this.HandlerType = default;
|
|
this.AuxiliaryForTrackId = 0;
|
|
this.PremultipliedByTrackId = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the file-defined track identifier.
|
|
/// </summary>
|
|
public uint Id { get; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the track is enabled.
|
|
/// </summary>
|
|
public bool IsEnabled { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the displayed track width.
|
|
/// </summary>
|
|
public int Width { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the displayed track height.
|
|
/// </summary>
|
|
public int Height { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the track duration in movie-time-scale units.
|
|
/// </summary>
|
|
public ulong TrackDuration { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the media handler type.
|
|
/// </summary>
|
|
public Heif4CharCode HandlerType { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the master track served by this auxiliary track.
|
|
/// </summary>
|
|
public uint AuxiliaryForTrackId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the track identifier used by premultiplication signaling.
|
|
/// </summary>
|
|
public uint PremultipliedByTrackId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retains one unique child payload range without creating a general box object model.
|
|
/// </summary>
|
|
private struct BoxReference
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="BoxReference"/> struct.
|
|
/// </summary>
|
|
/// <param name="offset">The absolute payload offset.</param>
|
|
/// <param name="length">The validated payload length.</param>
|
|
/// <param name="type">The child box type.</param>
|
|
public BoxReference(long offset, long length, Heif4CharCode type)
|
|
{
|
|
this.Offset = offset;
|
|
this.Length = length;
|
|
this.Type = type;
|
|
this.IsPresent = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the absolute payload offset.
|
|
/// </summary>
|
|
public long Offset { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the validated payload length.
|
|
/// </summary>
|
|
public long Length { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the child box type when one logical slot accepts multiple concrete box types.
|
|
/// </summary>
|
|
public Heif4CharCode Type { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the child was present.
|
|
/// </summary>
|
|
public bool IsPresent { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a one-based chunk run to its number of samples per chunk.
|
|
/// </summary>
|
|
private readonly struct SampleToChunkEntry
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SampleToChunkEntry"/> struct.
|
|
/// </summary>
|
|
/// <param name="firstChunk">The one-based first chunk in the run.</param>
|
|
/// <param name="samplesPerChunk">The number of samples stored in each run chunk.</param>
|
|
public SampleToChunkEntry(uint firstChunk, uint samplesPerChunk)
|
|
{
|
|
this.FirstChunk = firstChunk;
|
|
this.SamplesPerChunk = samplesPerChunk;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the one-based first chunk in the run.
|
|
/// </summary>
|
|
public uint FirstChunk { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the number of samples stored in each run chunk.
|
|
/// </summary>
|
|
public uint SamplesPerChunk { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Associates one retained sample with its one-based direct-reference group description.
|
|
/// </summary>
|
|
private readonly struct SampleGroupAssignment : IComparable<SampleGroupAssignment>
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SampleGroupAssignment"/> struct.
|
|
/// </summary>
|
|
/// <param name="groupDescriptionIndex">The one-based group-description index, or zero for no group.</param>
|
|
/// <param name="sampleIndex">The zero-based retained sample index.</param>
|
|
public SampleGroupAssignment(uint groupDescriptionIndex, int sampleIndex)
|
|
{
|
|
this.GroupDescriptionIndex = groupDescriptionIndex;
|
|
this.SampleIndex = sampleIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the one-based group-description index, or zero for no group.
|
|
/// </summary>
|
|
public uint GroupDescriptionIndex { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the zero-based retained sample index.
|
|
/// </summary>
|
|
public int SampleIndex { get; }
|
|
|
|
/// <summary>
|
|
/// Compares this assignment with another assignment in group-description and sample order.
|
|
/// </summary>
|
|
/// <param name="other">The other assignment.</param>
|
|
/// <returns>A value indicating the relative sort order.</returns>
|
|
public int CompareTo(SampleGroupAssignment other)
|
|
{
|
|
int result = this.GroupDescriptionIndex.CompareTo(other.GroupDescriptionIndex);
|
|
return result != 0 ? result : this.SampleIndex.CompareTo(other.SampleIndex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps one positive direct-reference identifier to its retained decode-order sample index.
|
|
/// </summary>
|
|
private readonly struct SampleIdIndexEntry : IComparable<SampleIdIndexEntry>
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SampleIdIndexEntry"/> struct.
|
|
/// </summary>
|
|
/// <param name="id">The positive file-defined sample identifier.</param>
|
|
/// <param name="sampleIndex">The zero-based retained sample index.</param>
|
|
public SampleIdIndexEntry(uint id, int sampleIndex)
|
|
{
|
|
this.Id = id;
|
|
this.SampleIndex = sampleIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the positive file-defined sample identifier.
|
|
/// </summary>
|
|
public uint Id { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the zero-based retained sample index.
|
|
/// </summary>
|
|
public int SampleIndex { get; }
|
|
|
|
/// <summary>
|
|
/// Compares this entry with another entry by sample identifier.
|
|
/// </summary>
|
|
/// <param name="other">The other identifier entry.</param>
|
|
/// <returns>A value indicating the relative sort order.</returns>
|
|
public int CompareTo(SampleIdIndexEntry other) => this.Id.CompareTo(other.Id);
|
|
}
|
|
}
|
|
|