From ab590b7c547c0c2a5d23cfeb35a80b26fd435590 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 17:19:52 +1000 Subject: [PATCH] Document HEIF track header parsing --- .../Formats/Heif/HeifSequenceParser.cs | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs index 4bafb3bbb7..1424b3d8c3 100644 --- a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -344,22 +344,43 @@ internal sealed class HeifSequenceParser /// The fixed track identity fields. private static TrackIdentity ParseTrackHeader(Stream stream, long boxLength, Span scratch) { - ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "track header"); + 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; + const uint trackInMovieFlag = 1 << 1; + + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, fullBoxHeaderLength, "track header"); byte version = prefix[0]; - int requiredLength = version switch + + // 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 => 84, - 1 => 96, + 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); - int trackIdOffset = version == 0 ? 12 : 20; - int durationOffset = version == 0 ? 20 : 28; - int matrixOffset = version == 0 ? 40 : 52; - int widthOffset = version == 0 ? 76 : 88; 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."); @@ -369,25 +390,41 @@ internal sealed class HeifSequenceParser ? 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; } - int width = checked((int)(BinaryPrimitives.ReadUInt32BigEndian(prefix[widthOffset..]) >> 16)); - int height = checked((int)(BinaryPrimitives.ReadUInt32BigEndian(prefix[(widthOffset + 4)..]) >> 16)); + // 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, 36)); + 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."); } - return new TrackIdentity(id, (flags & 3) == 3, width, height, duration); + // Bits 0 and 1 are track_enabled and track_in_movie respectively. A primary image-sequence candidate must + // participate in movie playback as well as being enabled; preview-only and disabled tracks remain available + // for explicit references but are not selected as the primary color track. + uint enabledInMovieFlags = trackEnabledFlag | trackInMovieFlag; + bool isEnabledInMovie = (flags & enabledInMovieFlags) == enabledInMovieFlags; + + return new TrackIdentity(id, isEnabledInMovie, width, height, duration); } ///