mirror of https://github.com/SixLabors/ImageSharp
11 changed files with 2859 additions and 217 deletions
@ -0,0 +1,223 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Buffers.Binary; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Reads bounded ISO BMFF box headers and payloads used by the HEIF image container.
|
|||
/// </summary>
|
|||
internal readonly struct HeifBoxReader |
|||
{ |
|||
/// <summary>
|
|||
/// The allocator used for payloads that must be materialized while parsing.
|
|||
/// </summary>
|
|||
private readonly MemoryAllocator allocator; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifBoxReader"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="allocator">The allocator used for bounded payload buffers.</param>
|
|||
public HeifBoxReader(MemoryAllocator allocator) => this.allocator = allocator; |
|||
|
|||
/// <summary>
|
|||
/// Reads an ISO BMFF box header using caller-owned scratch and resolves its validated payload length.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream positioned at the box size field.</param>
|
|||
/// <param name="parentEndPosition">The absolute end position of the containing box or file.</param>
|
|||
/// <param name="scratch">Caller-owned scratch containing at least eight bytes.</param>
|
|||
/// <param name="boxType">Receives the box four-character code.</param>
|
|||
/// <param name="topLevel">Indicates whether a size-zero box may extend to the end of the file.</param>
|
|||
/// <returns>The number of payload bytes following the complete variable-length header.</returns>
|
|||
public static long ReadHeader(Stream stream, long parentEndPosition, Span<byte> scratch, out Heif4CharCode boxType, bool topLevel = false) |
|||
{ |
|||
if (parentEndPosition - stream.Position < 8) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough data to read the box header."); |
|||
} |
|||
|
|||
Span<byte> buffer = scratch[..8]; |
|||
ReadExactly(stream, buffer, "Not enough data to read the box header."); |
|||
|
|||
ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); |
|||
int headerSize = 8; |
|||
boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); |
|||
|
|||
if (boxSize == 1) |
|||
{ |
|||
if (parentEndPosition - stream.Position < 8) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough data to read the extended box size."); |
|||
} |
|||
|
|||
ReadExactly(stream, buffer, "Not enough data to read the extended box size."); |
|||
boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer); |
|||
headerSize += 8; |
|||
} |
|||
|
|||
if (boxType == Heif4CharCode.Uuid) |
|||
{ |
|||
if (parentEndPosition - stream.Position < 16) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough data to read the UUID box user type."); |
|||
} |
|||
|
|||
// The UUID user type belongs to the variable box header even though the bounded image parser does not
|
|||
// interpret it. Advance here so every caller receives the actual payload start and length.
|
|||
Skip(stream, 16); |
|||
headerSize += 16; |
|||
} |
|||
|
|||
if (boxSize == 0) |
|||
{ |
|||
if (!topLevel) |
|||
{ |
|||
throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); |
|||
} |
|||
|
|||
return parentEndPosition - stream.Position; |
|||
} |
|||
|
|||
if (boxSize < (ulong)headerSize) |
|||
{ |
|||
throw new InvalidImageContentException("Box size is smaller than its header."); |
|||
} |
|||
|
|||
ulong contentLength = boxSize - (ulong)headerSize; |
|||
if (contentLength > (ulong)(parentEndPosition - stream.Position)) |
|||
{ |
|||
throw new InvalidImageContentException("Box size extends beyond its parent boundary."); |
|||
} |
|||
|
|||
return (long)contentLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses an ISO BMFF child-box header from a bounded parent payload.
|
|||
/// </summary>
|
|||
/// <param name="buffer">The remaining bytes in the parent payload, beginning at the child size field.</param>
|
|||
/// <param name="length">Receives the validated child payload length.</param>
|
|||
/// <param name="boxType">Receives the child box four-character code.</param>
|
|||
/// <returns>The number of bytes occupied by the complete child header.</returns>
|
|||
public static int ParseHeader(ReadOnlySpan<byte> buffer, out long length, out Heif4CharCode boxType) |
|||
{ |
|||
if (buffer.Length < 8) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough data to read the box header."); |
|||
} |
|||
|
|||
ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); |
|||
int bytesRead = 8; |
|||
boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); |
|||
if (boxSize == 1) |
|||
{ |
|||
if (buffer.Length < 16) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough data to read the extended box size."); |
|||
} |
|||
|
|||
boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]); |
|||
bytesRead += 8; |
|||
} |
|||
|
|||
if (boxType == Heif4CharCode.Uuid) |
|||
{ |
|||
if (buffer.Length - bytesRead < 16) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough data to read the UUID box user type."); |
|||
} |
|||
|
|||
bytesRead += 16; |
|||
} |
|||
|
|||
if (boxSize == 0) |
|||
{ |
|||
throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); |
|||
} |
|||
|
|||
if (boxSize < (ulong)bytesRead) |
|||
{ |
|||
throw new InvalidImageContentException("Box size is smaller than its header."); |
|||
} |
|||
|
|||
ulong contentLength = boxSize - (ulong)bytesRead; |
|||
if (contentLength > (ulong)(buffer.Length - bytesRead)) |
|||
{ |
|||
throw new InvalidImageContentException("Box size extends beyond its parent boundary."); |
|||
} |
|||
|
|||
length = (long)contentLength; |
|||
return bytesRead; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a complete bounded box payload into allocator-owned memory.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream positioned at the payload start.</param>
|
|||
/// <param name="length">The validated payload length.</param>
|
|||
/// <returns>An owner containing exactly the requested payload bytes.</returns>
|
|||
public IMemoryOwner<byte> ReadPayload(Stream stream, long length) |
|||
{ |
|||
if ((ulong)length > int.MaxValue) |
|||
{ |
|||
throw new InvalidImageContentException("Box content is too large to buffer."); |
|||
} |
|||
|
|||
int bufferLength = (int)length; |
|||
IMemoryOwner<byte> memory = this.allocator.Allocate<byte>(bufferLength); |
|||
try |
|||
{ |
|||
ReadExactly(stream, memory.GetSpan(), "Stream length is not sufficient for box content."); |
|||
return memory; |
|||
} |
|||
catch |
|||
{ |
|||
memory.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Advances over a validated box payload without narrowing its 64-bit length.
|
|||
/// </summary>
|
|||
/// <param name="stream">The seekable container stream.</param>
|
|||
/// <param name="length">The validated payload length.</param>
|
|||
public static void Skip(Stream stream, long length) => stream.Seek(length, SeekOrigin.Current); |
|||
|
|||
/// <summary>
|
|||
/// Validates a child payload length against the bytes remaining in its parent.
|
|||
/// </summary>
|
|||
/// <param name="length">The declared child payload length.</param>
|
|||
/// <param name="parentLength">The number of bytes remaining in the parent.</param>
|
|||
public static void EnsureInsideParent(long length, long parentLength) |
|||
{ |
|||
if (length < 0 || parentLength < 0 || length > parentLength) |
|||
{ |
|||
throw new InvalidImageContentException("Box size extends beyond its parent boundary."); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads exactly the requested number of bytes or rejects the truncated payload.
|
|||
/// </summary>
|
|||
/// <param name="stream">The source stream.</param>
|
|||
/// <param name="destination">The complete destination span.</param>
|
|||
/// <param name="message">The malformed-image message used when the stream ends early.</param>
|
|||
public static void ReadExactly(Stream stream, Span<byte> destination, string message) |
|||
{ |
|||
int offset = 0; |
|||
while (offset < destination.Length) |
|||
{ |
|||
int read = stream.Read(destination[offset..]); |
|||
if (read == 0) |
|||
{ |
|||
throw new InvalidImageContentException(message); |
|||
} |
|||
|
|||
offset += read; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Contains the selected color and optional alpha tracks of one HEIF image sequence.
|
|||
/// </summary>
|
|||
internal sealed class HeifSequence |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifSequence"/> class.
|
|||
/// </summary>
|
|||
/// <param name="colorTrack">The selected master image-sequence track.</param>
|
|||
/// <param name="alphaTrack">The linked alpha image-sequence track, when present.</param>
|
|||
/// <param name="movieTimescale">The movie time scale in units per second.</param>
|
|||
public HeifSequence(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack, uint movieTimescale) |
|||
{ |
|||
this.ColorTrack = colorTrack; |
|||
this.AlphaTrack = alphaTrack; |
|||
this.MovieTimescale = movieTimescale; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the selected master image-sequence track.
|
|||
/// </summary>
|
|||
public HeifSequenceTrack ColorTrack { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the linked alpha image-sequence track, when present.
|
|||
/// </summary>
|
|||
public HeifSequenceTrack? AlphaTrack { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the movie time scale in units per second.
|
|||
/// </summary>
|
|||
public uint MovieTimescale { get; } |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,30 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Describes one retained coded sample in a HEIF image sequence.
|
|||
/// </summary>
|
|||
internal struct HeifSequenceSample |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the absolute file offset of the coded sample.
|
|||
/// </summary>
|
|||
public long Offset { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the coded sample length in bytes.
|
|||
/// </summary>
|
|||
public int Length { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the sample duration in media-time-scale units.
|
|||
/// </summary>
|
|||
public uint Duration { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether decoding can begin at this sample.
|
|||
/// </summary>
|
|||
public bool IsSync { get; set; } |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Owns the bounded image behavior retained from one HEIF image-sequence track.
|
|||
/// </summary>
|
|||
internal sealed class HeifSequenceTrack |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifSequenceTrack"/> class.
|
|||
/// </summary>
|
|||
public HeifSequenceTrack() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the file-defined track identifier.
|
|||
/// </summary>
|
|||
public uint Id { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the displayed track width in pixels.
|
|||
/// </summary>
|
|||
public int Width { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the displayed track height in pixels.
|
|||
/// </summary>
|
|||
public int Height { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the coded sample width in pixels before track presentation transforms.
|
|||
/// </summary>
|
|||
public int CodedWidth { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the coded sample height in pixels before track presentation transforms.
|
|||
/// </summary>
|
|||
public int CodedHeight { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the track transformation matrix.
|
|||
/// </summary>
|
|||
public HeifTrackMatrix Matrix { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the media time scale in units per second.
|
|||
/// </summary>
|
|||
public uint MediaTimescale { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the declared media duration in media-time-scale units.
|
|||
/// </summary>
|
|||
public ulong MediaDuration { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the total number of samples declared by the sample table.
|
|||
/// </summary>
|
|||
public uint TotalSampleCount { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the coded sample-entry type.
|
|||
/// </summary>
|
|||
public Heif4CharCode CodecType { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the parsed AV1 configuration when <see cref="CodecType"/> is <see cref="Heif4CharCode.Av01"/>.
|
|||
/// </summary>
|
|||
public Av1CodecConfiguration? Av1CodecConfiguration { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the parsed HEVC configuration when <see cref="CodecType"/> is <see cref="Heif4CharCode.Hvc1"/>.
|
|||
/// </summary>
|
|||
public HevcCodecConfiguration? HevcCodecConfiguration { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the retained sample descriptors in decode order.
|
|||
/// </summary>
|
|||
public HeifSequenceSample[] Samples { get; set; } = []; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the identifier of the master track served by this auxiliary track, or zero for a master track.
|
|||
/// </summary>
|
|||
public uint AuxiliaryForTrackId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the track is an alpha auxiliary image sequence.
|
|||
/// </summary>
|
|||
public bool IsAlpha { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the color track is premultiplied by this auxiliary alpha track.
|
|||
/// </summary>
|
|||
public bool IsPremultiplied { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether every reference picture is intra coded.
|
|||
/// </summary>
|
|||
public bool AllReferencePicturesIntra { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether coded pictures use intra-picture prediction.
|
|||
/// </summary>
|
|||
public bool IntraPicturePredictionUsed { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the maximum number of reference pictures permitted for one coded picture.
|
|||
/// </summary>
|
|||
public byte MaximumReferencesPerPicture { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of times the sequence is played. Zero indicates indefinite repetition.
|
|||
/// </summary>
|
|||
public ushort RepeatCount { get; set; } = 1; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the track handler type.
|
|||
/// </summary>
|
|||
public Heif4CharCode HandlerType { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the track duration in movie-time-scale units.
|
|||
/// </summary>
|
|||
public ulong TrackDuration { get; set; } |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Contains the fixed-point transformation matrix of a HEIF image-sequence track.
|
|||
/// </summary>
|
|||
internal readonly struct HeifTrackMatrix |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifTrackMatrix"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="a">The horizontal scale and rotation coefficient in 16.16 fixed-point form.</param>
|
|||
/// <param name="b">The horizontal skew and rotation coefficient in 16.16 fixed-point form.</param>
|
|||
/// <param name="u">The first perspective coefficient in 2.30 fixed-point form.</param>
|
|||
/// <param name="c">The vertical skew and rotation coefficient in 16.16 fixed-point form.</param>
|
|||
/// <param name="d">The vertical scale and rotation coefficient in 16.16 fixed-point form.</param>
|
|||
/// <param name="v">The second perspective coefficient in 2.30 fixed-point form.</param>
|
|||
/// <param name="x">The horizontal translation in 16.16 fixed-point form.</param>
|
|||
/// <param name="y">The vertical translation in 16.16 fixed-point form.</param>
|
|||
/// <param name="w">The homogeneous scale coefficient in 2.30 fixed-point form.</param>
|
|||
public HeifTrackMatrix(int a, int b, int u, int c, int d, int v, int x, int y, int w) |
|||
{ |
|||
this.A = a; |
|||
this.B = b; |
|||
this.U = u; |
|||
this.C = c; |
|||
this.D = d; |
|||
this.V = v; |
|||
this.X = x; |
|||
this.Y = y; |
|||
this.W = w; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal scale and rotation coefficient in 16.16 fixed-point form.
|
|||
/// </summary>
|
|||
public int A { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal skew and rotation coefficient in 16.16 fixed-point form.
|
|||
/// </summary>
|
|||
public int B { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the first perspective coefficient in 2.30 fixed-point form.
|
|||
/// </summary>
|
|||
public int U { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical skew and rotation coefficient in 16.16 fixed-point form.
|
|||
/// </summary>
|
|||
public int C { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical scale and rotation coefficient in 16.16 fixed-point form.
|
|||
/// </summary>
|
|||
public int D { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the second perspective coefficient in 2.30 fixed-point form.
|
|||
/// </summary>
|
|||
public int V { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal translation in 16.16 fixed-point form.
|
|||
/// </summary>
|
|||
public int X { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical translation in 16.16 fixed-point form.
|
|||
/// </summary>
|
|||
public int Y { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the homogeneous scale coefficient in 2.30 fixed-point form.
|
|||
/// </summary>
|
|||
public int W { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the matrix contains unsupported perspective projection.
|
|||
/// </summary>
|
|||
public bool HasPerspective => this.U != 0 || this.V != 0 || this.W != 0x40000000; |
|||
} |
|||
@ -0,0 +1,297 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers.Binary; |
|||
using System.Text; |
|||
using SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif; |
|||
|
|||
[Trait("Format", "Heif")] |
|||
[ValidateDisposedMemoryAllocations] |
|||
public class HeifSequenceParserTests |
|||
{ |
|||
[Fact] |
|||
public void ParseResolvesLibavifShapedSampleTable() |
|||
{ |
|||
byte[] data = CreateSequenceFile(1024); |
|||
using MemoryStream stream = new(data, false); |
|||
HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 2); |
|||
stream.Position = 8; |
|||
|
|||
HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); |
|||
|
|||
Assert.Equal(1000U, sequence.MovieTimescale); |
|||
Assert.Null(sequence.AlphaTrack); |
|||
Assert.Equal(1U, sequence.ColorTrack.Id); |
|||
Assert.Equal(320, sequence.ColorTrack.Width); |
|||
Assert.Equal(240, sequence.ColorTrack.Height); |
|||
Assert.Equal(Heif4CharCode.Av01, sequence.ColorTrack.CodecType); |
|||
Assert.NotNull(sequence.ColorTrack.Av1CodecConfiguration); |
|||
Assert.Equal(2U, sequence.ColorTrack.TotalSampleCount); |
|||
Assert.Equal(3, sequence.ColorTrack.RepeatCount); |
|||
Assert.False(sequence.ColorTrack.AllReferencePicturesIntra); |
|||
Assert.True(sequence.ColorTrack.IntraPicturePredictionUsed); |
|||
Assert.Equal(15, sequence.ColorTrack.MaximumReferencesPerPicture); |
|||
Assert.Collection( |
|||
sequence.ColorTrack.Samples, |
|||
sample => |
|||
{ |
|||
Assert.Equal(1024, sample.Offset); |
|||
Assert.Equal(10, sample.Length); |
|||
Assert.Equal(100U, sample.Duration); |
|||
Assert.True(sample.IsSync); |
|||
}, |
|||
sample => |
|||
{ |
|||
Assert.Equal(1034, sample.Offset); |
|||
Assert.Equal(12, sample.Length); |
|||
Assert.Equal(100U, sample.Duration); |
|||
Assert.False(sample.IsSync); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ParseRetainsOnlyConfiguredFrameCount() |
|||
{ |
|||
byte[] data = CreateSequenceFile(1024); |
|||
using MemoryStream stream = new(data, false); |
|||
HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 1); |
|||
stream.Position = 8; |
|||
|
|||
HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); |
|||
|
|||
Assert.Equal(2U, sequence.ColorTrack.TotalSampleCount); |
|||
HeifSequenceSample sample = Assert.Single(sequence.ColorTrack.Samples); |
|||
Assert.Equal(1024, sample.Offset); |
|||
Assert.Equal(10, sample.Length); |
|||
Assert.Equal(100U, sample.Duration); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ParseRejectsRetainedSampleBeyondFile() |
|||
{ |
|||
byte[] data = CreateSequenceFile(2040); |
|||
using MemoryStream stream = new(data, false); |
|||
HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 2); |
|||
stream.Position = 8; |
|||
|
|||
Assert.Throws<InvalidImageContentException>(() => parser.Parse(stream, GetMoviePayloadLength(data))); |
|||
} |
|||
|
|||
private static byte[] CreateSequenceFile(uint chunkOffset) |
|||
{ |
|||
using MemoryStream stream = new(); |
|||
using BinaryWriter writer = new(stream, Encoding.UTF8, true); |
|||
long movie = BeginBox(writer, Heif4CharCode.Moov); |
|||
|
|||
long movieHeader = BeginBox(writer, Heif4CharCode.Mvhd); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 1000); |
|||
WriteUInt32(writer, 600); |
|||
WriteZeros(writer, 80); |
|||
EndBox(writer, movieHeader); |
|||
|
|||
long track = BeginBox(writer, Heif4CharCode.Trak); |
|||
WriteTrackHeader(writer); |
|||
WriteEditList(writer); |
|||
|
|||
long media = BeginBox(writer, Heif4CharCode.Mdia); |
|||
WriteMediaHeader(writer); |
|||
WriteHandler(writer, Heif4CharCode.Pict); |
|||
|
|||
long mediaInformation = BeginBox(writer, Heif4CharCode.Minf); |
|||
WriteDataInformation(writer); |
|||
WriteSampleTable(writer, chunkOffset); |
|||
EndBox(writer, mediaInformation); |
|||
EndBox(writer, media); |
|||
EndBox(writer, track); |
|||
EndBox(writer, movie); |
|||
|
|||
byte[] movieBytes = stream.ToArray(); |
|||
byte[] file = new byte[2048]; |
|||
|
|||
movieBytes.CopyTo(file, 0); |
|||
return file; |
|||
} |
|||
|
|||
private static void WriteTrackHeader(BinaryWriter writer) |
|||
{ |
|||
long trackHeader = BeginBox(writer, Heif4CharCode.Tkhd); |
|||
WriteFullBoxHeader(writer, 0, 3); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 600); |
|||
WriteZeros(writer, 16); |
|||
WriteUInt32(writer, 0x00010000); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0x00010000); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0x40000000); |
|||
WriteUInt32(writer, 320U << 16); |
|||
WriteUInt32(writer, 240U << 16); |
|||
EndBox(writer, trackHeader); |
|||
} |
|||
|
|||
private static void WriteEditList(BinaryWriter writer) |
|||
{ |
|||
long edit = BeginBox(writer, Heif4CharCode.Edts); |
|||
long editList = BeginBox(writer, Heif4CharCode.Elst); |
|||
WriteFullBoxHeader(writer, 0, 1); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, 200); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt16(writer, 1); |
|||
WriteUInt16(writer, 0); |
|||
EndBox(writer, editList); |
|||
EndBox(writer, edit); |
|||
} |
|||
|
|||
private static void WriteMediaHeader(BinaryWriter writer) |
|||
{ |
|||
long mediaHeader = BeginBox(writer, Heif4CharCode.Mdhd); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 1000); |
|||
WriteUInt32(writer, 200); |
|||
WriteUInt16(writer, 21956); |
|||
WriteUInt16(writer, 0); |
|||
EndBox(writer, mediaHeader); |
|||
} |
|||
|
|||
private static void WriteHandler(BinaryWriter writer, Heif4CharCode handlerType) |
|||
{ |
|||
long handler = BeginBox(writer, Heif4CharCode.Hdlr); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, (uint)handlerType); |
|||
WriteZeros(writer, 12); |
|||
writer.Write((byte)0); |
|||
EndBox(writer, handler); |
|||
} |
|||
|
|||
private static void WriteDataInformation(BinaryWriter writer) |
|||
{ |
|||
long dataInformation = BeginBox(writer, Heif4CharCode.Dinf); |
|||
long dataReference = BeginBox(writer, Heif4CharCode.Dref); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 1); |
|||
long location = BeginBox(writer, Heif4CharCode.Url); |
|||
WriteFullBoxHeader(writer, 0, 1); |
|||
EndBox(writer, location); |
|||
EndBox(writer, dataReference); |
|||
EndBox(writer, dataInformation); |
|||
} |
|||
|
|||
private static void WriteSampleTable(BinaryWriter writer, uint chunkOffset) |
|||
{ |
|||
long sampleTable = BeginBox(writer, Heif4CharCode.Stbl); |
|||
WriteSampleDescription(writer); |
|||
|
|||
long timing = BeginBox(writer, Heif4CharCode.Stts); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, 2); |
|||
WriteUInt32(writer, 100); |
|||
EndBox(writer, timing); |
|||
|
|||
long sampleToChunk = BeginBox(writer, Heif4CharCode.Stsc); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, 2); |
|||
WriteUInt32(writer, 1); |
|||
EndBox(writer, sampleToChunk); |
|||
|
|||
long sampleSizes = BeginBox(writer, Heif4CharCode.Stsz); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, 2); |
|||
WriteUInt32(writer, 10); |
|||
WriteUInt32(writer, 12); |
|||
EndBox(writer, sampleSizes); |
|||
|
|||
long chunkOffsets = BeginBox(writer, Heif4CharCode.Stco); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, chunkOffset); |
|||
EndBox(writer, chunkOffsets); |
|||
|
|||
long syncSamples = BeginBox(writer, Heif4CharCode.Stss); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 1); |
|||
WriteUInt32(writer, 1); |
|||
EndBox(writer, syncSamples); |
|||
EndBox(writer, sampleTable); |
|||
} |
|||
|
|||
private static void WriteSampleDescription(BinaryWriter writer) |
|||
{ |
|||
long description = BeginBox(writer, Heif4CharCode.Stsd); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 1); |
|||
long sampleEntry = BeginBox(writer, Heif4CharCode.Av01); |
|||
WriteZeros(writer, 6); |
|||
WriteUInt16(writer, 1); |
|||
WriteZeros(writer, 16); |
|||
WriteUInt16(writer, 320); |
|||
WriteUInt16(writer, 240); |
|||
WriteUInt32(writer, 0x00480000); |
|||
WriteUInt32(writer, 0x00480000); |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt16(writer, 1); |
|||
WriteZeros(writer, 32); |
|||
WriteUInt16(writer, 0x18); |
|||
WriteUInt16(writer, ushort.MaxValue); |
|||
|
|||
long configuration = BeginBox(writer, Heif4CharCode.Av1C); |
|||
writer.Write(new byte[] { 0x81, 0, 0, 0 }); |
|||
EndBox(writer, configuration); |
|||
|
|||
long codingConstraints = BeginBox(writer, Heif4CharCode.Ccst); |
|||
WriteFullBoxHeader(writer, 0, 0); |
|||
WriteUInt32(writer, 0x7C000000); |
|||
EndBox(writer, codingConstraints); |
|||
EndBox(writer, sampleEntry); |
|||
EndBox(writer, description); |
|||
} |
|||
|
|||
private static long BeginBox(BinaryWriter writer, Heif4CharCode type) |
|||
{ |
|||
long start = writer.BaseStream.Position; |
|||
WriteUInt32(writer, 0); |
|||
WriteUInt32(writer, (uint)type); |
|||
return start; |
|||
} |
|||
|
|||
private static void EndBox(BinaryWriter writer, long start) |
|||
{ |
|||
long end = writer.BaseStream.Position; |
|||
writer.BaseStream.Position = start; |
|||
WriteUInt32(writer, checked((uint)(end - start))); |
|||
writer.BaseStream.Position = end; |
|||
} |
|||
|
|||
private static void WriteFullBoxHeader(BinaryWriter writer, byte version, uint flags) |
|||
=> WriteUInt32(writer, ((uint)version << 24) | flags); |
|||
|
|||
private static void WriteUInt16(BinaryWriter writer, ushort value) |
|||
=> writer.Write(BinaryPrimitives.ReverseEndianness(value)); |
|||
|
|||
private static void WriteUInt32(BinaryWriter writer, uint value) |
|||
=> writer.Write(BinaryPrimitives.ReverseEndianness(value)); |
|||
|
|||
private static void WriteZeros(BinaryWriter writer, int count) => writer.Write(new byte[count]); |
|||
|
|||
private static int GetMoviePayloadLength(byte[] data) |
|||
=> checked((int)BinaryPrimitives.ReadUInt32BigEndian(data) - 8); |
|||
} |
|||
Loading…
Reference in new issue