Browse Source

Document HEIF track header parsing

pull/2633/head
James Jackson-South 2 months ago
parent
commit
ab590b7c54
  1. 61
      src/ImageSharp/Formats/Heif/HeifSequenceParser.cs

61
src/ImageSharp/Formats/Heif/HeifSequenceParser.cs

@ -344,22 +344,43 @@ internal sealed class HeifSequenceParser
/// <returns>The fixed track identity fields.</returns> /// <returns>The fixed track identity fields.</returns>
private static TrackIdentity ParseTrackHeader(Stream stream, long boxLength, Span<byte> scratch) private static TrackIdentity ParseTrackHeader(Stream stream, long boxLength, Span<byte> scratch)
{ {
ReadOnlySpan<byte> 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<byte> prefix = ReadPrefix(stream, boxLength, scratch, fullBoxHeaderLength, "track header");
byte version = prefix[0]; 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, 0 => sizeof(uint),
1 => 96, 1 => sizeof(ulong),
_ => throw new InvalidImageContentException($"The track header has unsupported version {version}.") _ => 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"); prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "track header");
uint flags = ReadFlags(prefix); 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..]); 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) if (id == 0)
{ {
throw new InvalidImageContentException("A HEIF image-sequence track has identifier zero."); throw new InvalidImageContentException("A HEIF image-sequence track has identifier zero.");
@ -369,25 +390,41 @@ internal sealed class HeifSequenceParser
? BinaryPrimitives.ReadUInt32BigEndian(prefix[durationOffset..]) ? BinaryPrimitives.ReadUInt32BigEndian(prefix[durationOffset..])
: BinaryPrimitives.ReadUInt64BigEndian(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) if (version == 0 && duration == uint.MaxValue)
{ {
duration = ulong.MaxValue; duration = ulong.MaxValue;
} }
int width = checked((int)(BinaryPrimitives.ReadUInt32BigEndian(prefix[widthOffset..]) >> 16)); // Section 8.3.2 stores width and height as unsigned 16.16 fixed-point values. ImageSharp dimensions are
int height = checked((int)(BinaryPrimitives.ReadUInt32BigEndian(prefix[(widthOffset + 4)..]) >> 16)); // 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) if (width == 0 || height == 0)
{ {
throw new InvalidImageContentException("A HEIF image-sequence track has zero dimensions."); 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) if (!matrix.IsIdentity)
{ {
throw new NotSupportedException("The HEIF image-sequence track requires an unsupported movie presentation matrix."); 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);
} }
/// <summary> /// <summary>

Loading…
Cancel
Save