mirror of https://github.com/SixLabors/ImageSharp
4 changed files with 1000 additions and 117 deletions
@ -0,0 +1,888 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers.Binary; |
||||
|
using System.Text; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
using SixLabors.ImageSharp.IO; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
||||
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif; |
||||
|
|
||||
|
internal sealed partial class HeifEncoderCore |
||||
|
{ |
||||
|
private const uint DefaultSequenceTimescale = 1000; |
||||
|
private const uint FallbackSequenceTimescale = 1000000; |
||||
|
private const uint UnityFixed16Point16 = 1U << 16; |
||||
|
private const uint UnityFixed2Point30 = 1U << 30; |
||||
|
private const ushort UnityFixed8Point8 = 1 << 8; |
||||
|
private const ushort PackedUndeterminedLanguage = 0x55C4; |
||||
|
|
||||
|
private Av1EncodingSettings ResolveAv1Encoding<TPixel>(Image<TPixel> image) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); |
||||
|
HeifBitDepth bitDepth = this.encoder.BitDepth ?? metadata.BitDepth; |
||||
|
Av1BitDepth av1BitDepth = bitDepth switch |
||||
|
{ |
||||
|
HeifBitDepth.Bit8 => Av1BitDepth.EightBit, |
||||
|
HeifBitDepth.Bit10 => Av1BitDepth.TenBit, |
||||
|
HeifBitDepth.Bit12 => Av1BitDepth.TwelveBit, |
||||
|
_ => throw new NotSupportedException($"HEIF bit depth '{bitDepth}' is not supported.") |
||||
|
}; |
||||
|
|
||||
|
HeifChromaSubsampling defaultChromaSubsampling = this.encoder.Lossless |
||||
|
? HeifChromaSubsampling.Yuv444 |
||||
|
: HeifChromaSubsampling.Yuv420; |
||||
|
|
||||
|
HeifChromaSubsampling chromaSubsampling = this.encoder.ChromaSubsampling ?? |
||||
|
(metadata.IsMonochrome ? HeifChromaSubsampling.Monochrome : defaultChromaSubsampling); |
||||
|
|
||||
|
(bool isMonochrome, bool subsamplingX, bool subsamplingY) = chromaSubsampling switch |
||||
|
{ |
||||
|
HeifChromaSubsampling.Monochrome => (true, true, true), |
||||
|
HeifChromaSubsampling.Yuv420 => (false, true, true), |
||||
|
HeifChromaSubsampling.Yuv422 => (false, true, false), |
||||
|
HeifChromaSubsampling.Yuv444 => (false, false, false), |
||||
|
_ => throw new NotSupportedException($"HEIF chroma sampling '{chromaSubsampling}' is not supported.") |
||||
|
}; |
||||
|
|
||||
|
CicpProfile? sourceColorProfile = image.Metadata.CicpProfile; |
||||
|
CicpProfile colorProfile; |
||||
|
if (sourceColorProfile is null) |
||||
|
{ |
||||
|
colorProfile = new CicpProfile(2, 2, 6, false); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
bool identityMatrix = sourceColorProfile.MatrixCoefficients == CicpMatrixCoefficients.Identity; |
||||
|
bool legalIdentityMatrix = !isMonochrome |
||||
|
&& chromaSubsampling == HeifChromaSubsampling.Yuv444 |
||||
|
&& sourceColorProfile.ColorPrimaries == CicpColorPrimaries.ItuRBt709_6 |
||||
|
&& sourceColorProfile.TransferCharacteristics == CicpTransferCharacteristics.Iec61966_2_1; |
||||
|
|
||||
|
if (sourceColorProfile.MatrixCoefficients == CicpMatrixCoefficients.Unspecified |
||||
|
|| (identityMatrix && !legalIdentityMatrix)) |
||||
|
{ |
||||
|
// The converter uses BT.601 for unspecified or incompatible identity signaling, so record that actual matrix.
|
||||
|
colorProfile = new CicpProfile( |
||||
|
(byte)sourceColorProfile.ColorPrimaries, |
||||
|
(byte)sourceColorProfile.TransferCharacteristics, |
||||
|
(byte)CicpMatrixCoefficients.ItuRBt601_7_525, |
||||
|
sourceColorProfile.FullRange); |
||||
|
} |
||||
|
else if (identityMatrix && !sourceColorProfile.FullRange) |
||||
|
{ |
||||
|
colorProfile = new CicpProfile( |
||||
|
(byte)sourceColorProfile.ColorPrimaries, |
||||
|
(byte)sourceColorProfile.TransferCharacteristics, |
||||
|
(byte)sourceColorProfile.MatrixCoefficients, |
||||
|
true); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
colorProfile = sourceColorProfile; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ObuColorConfig colorConfig = new() |
||||
|
{ |
||||
|
IsColorDescriptionPresent = true, |
||||
|
IsMonochrome = isMonochrome, |
||||
|
ColorPrimaries = (ObuColorPrimaries)colorProfile.ColorPrimaries, |
||||
|
TransferCharacteristics = (ObuTransferCharacteristics)colorProfile.TransferCharacteristics, |
||||
|
MatrixCoefficients = (ObuMatrixCoefficients)colorProfile.MatrixCoefficients, |
||||
|
ColorRange = colorProfile.FullRange, |
||||
|
SubSamplingX = subsamplingX, |
||||
|
SubSamplingY = subsamplingY, |
||||
|
ChromaSamplePosition = ObuChromoSamplePosition.Unknown, |
||||
|
BitDepth = av1BitDepth |
||||
|
}; |
||||
|
|
||||
|
ObuColorConfig alphaConfig = new() |
||||
|
{ |
||||
|
IsMonochrome = true, |
||||
|
ColorRange = true, |
||||
|
SubSamplingX = true, |
||||
|
SubSamplingY = true, |
||||
|
BitDepth = av1BitDepth |
||||
|
}; |
||||
|
|
||||
|
int quality = this.encoder.Quality ?? 75; |
||||
|
int colorQIndex = this.encoder.Lossless ? 0 : GetAv1QuantizerIndex(quality); |
||||
|
int alphaQuality = this.encoder.AlphaQuality ?? quality; |
||||
|
int alphaQIndex = this.encoder.Lossless ? 0 : GetAv1QuantizerIndex(alphaQuality); |
||||
|
bool hasAlpha = TPixel.GetPixelTypeInfo().AlphaRepresentation != PixelAlphaRepresentation.None; |
||||
|
return new Av1EncodingSettings( |
||||
|
bitDepth, |
||||
|
chromaSubsampling, |
||||
|
colorProfile, |
||||
|
colorConfig, |
||||
|
alphaConfig, |
||||
|
colorQIndex, |
||||
|
alphaQIndex, |
||||
|
hasAlpha); |
||||
|
} |
||||
|
|
||||
|
private HeifSequenceEncoding CompressAv1Sequence<TPixel>( |
||||
|
Image<TPixel> image, |
||||
|
ChunkedMemoryStream stream, |
||||
|
Av1EncodingSettings settings, |
||||
|
CancellationToken cancellationToken) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
if (image.Width > ushort.MaxValue || image.Height > ushort.MaxValue) |
||||
|
{ |
||||
|
throw new NotSupportedException("AV1 image-sequence dimensions cannot exceed 65535 pixels."); |
||||
|
} |
||||
|
|
||||
|
int frameCount = image.Frames.Count; |
||||
|
uint timescale = GetSequenceTimescale(image); |
||||
|
int sampleCount = checked(frameCount * (settings.HasAlpha ? 2 : 1)); |
||||
|
|
||||
|
// The container needs only offset, length, and duration after each frame is streamed. Color and alpha
|
||||
|
// share one compact table, with each track occupying one contiguous slice for its complete operation lifetime.
|
||||
|
HeifSequenceSampleInfo[] samples = new HeifSequenceSampleInfo[sampleCount]; |
||||
|
Span<HeifSequenceSampleInfo> colorSamples = samples.AsSpan(0, frameCount); |
||||
|
ImageFrame<TPixel> rootFrame = image.Frames.RootFrame; |
||||
|
uint duration = GetSequenceSampleDuration(rootFrame.Metadata.GetHeifMetadata().FrameDelay, timescale); |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
long colorOffset = stream.Length; |
||||
|
ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode( |
||||
|
this.configuration, |
||||
|
rootFrame, |
||||
|
stream, |
||||
|
settings.ColorConfig, |
||||
|
settings.ColorQIndex, |
||||
|
this.encoder.Effort); |
||||
|
|
||||
|
colorSamples[0] = new HeifSequenceSampleInfo( |
||||
|
colorOffset, |
||||
|
checked((int)(stream.Length - colorOffset)), |
||||
|
duration); |
||||
|
|
||||
|
for (int frameIndex = 1; frameIndex < frameCount; frameIndex++) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
ImageFrame<TPixel> frame = image.Frames[frameIndex]; |
||||
|
duration = GetSequenceSampleDuration(frame.Metadata.GetHeifMetadata().FrameDelay, timescale); |
||||
|
colorOffset = stream.Length; |
||||
|
_ = Av1FrameEncoder.Encode( |
||||
|
this.configuration, |
||||
|
frame, |
||||
|
stream, |
||||
|
settings.ColorConfig, |
||||
|
settings.ColorQIndex, |
||||
|
this.encoder.Effort); |
||||
|
|
||||
|
colorSamples[frameIndex] = new HeifSequenceSampleInfo( |
||||
|
colorOffset, |
||||
|
checked((int)(stream.Length - colorOffset)), |
||||
|
duration); |
||||
|
} |
||||
|
|
||||
|
HeifSequenceTrackEncoding colorTrack = new( |
||||
|
new Av1CodecConfiguration(colorHeader), |
||||
|
samples, |
||||
|
0, |
||||
|
frameCount, |
||||
|
false); |
||||
|
|
||||
|
HeifSequenceTrackEncoding? alphaTrack = null; |
||||
|
if (settings.HasAlpha) |
||||
|
{ |
||||
|
Span<HeifSequenceSampleInfo> alphaSamples = samples.AsSpan(frameCount, frameCount); |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
long alphaOffset = stream.Length; |
||||
|
ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha( |
||||
|
this.configuration, |
||||
|
rootFrame, |
||||
|
stream, |
||||
|
settings.AlphaConfig, |
||||
|
settings.AlphaQIndex, |
||||
|
this.encoder.Effort); |
||||
|
|
||||
|
alphaSamples[0] = new HeifSequenceSampleInfo( |
||||
|
alphaOffset, |
||||
|
checked((int)(stream.Length - alphaOffset)), |
||||
|
colorSamples[0].Duration); |
||||
|
|
||||
|
for (int frameIndex = 1; frameIndex < frameCount; frameIndex++) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
alphaOffset = stream.Length; |
||||
|
_ = Av1FrameEncoder.EncodeAlpha( |
||||
|
this.configuration, |
||||
|
image.Frames[frameIndex], |
||||
|
stream, |
||||
|
settings.AlphaConfig, |
||||
|
settings.AlphaQIndex, |
||||
|
this.encoder.Effort); |
||||
|
|
||||
|
alphaSamples[frameIndex] = new HeifSequenceSampleInfo( |
||||
|
alphaOffset, |
||||
|
checked((int)(stream.Length - alphaOffset)), |
||||
|
colorSamples[frameIndex].Duration); |
||||
|
} |
||||
|
|
||||
|
alphaTrack = new HeifSequenceTrackEncoding( |
||||
|
new Av1CodecConfiguration(alphaHeader), |
||||
|
samples, |
||||
|
frameCount, |
||||
|
frameCount, |
||||
|
true); |
||||
|
} |
||||
|
|
||||
|
return new HeifSequenceEncoding( |
||||
|
image.Width, |
||||
|
image.Height, |
||||
|
image.Metadata.GetHeifMetadata().RepeatCount, |
||||
|
timescale, |
||||
|
colorTrack, |
||||
|
alphaTrack, |
||||
|
settings.ColorProfile, |
||||
|
this.encoder.SkipMetadata ? null : image.Metadata.IccProfile); |
||||
|
} |
||||
|
|
||||
|
private int WriteSequenceFileTypeBox(Stream stream) |
||||
|
{ |
||||
|
Span<byte> buffer = stackalloc byte[32]; |
||||
|
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ftyp); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avis); |
||||
|
bytesWritten += sizeof(uint); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0); |
||||
|
bytesWritten += sizeof(uint); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avif); |
||||
|
bytesWritten += sizeof(uint); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Msf1); |
||||
|
bytesWritten += sizeof(uint); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Iso8); |
||||
|
bytesWritten += sizeof(uint); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avio); |
||||
|
bytesWritten += sizeof(uint); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); |
||||
|
stream.Write(buffer[..bytesWritten]); |
||||
|
return bytesWritten; |
||||
|
} |
||||
|
|
||||
|
private void WriteSequenceMovieBox(HeifSequenceEncoding sequence, int fileTypeLength, Stream stream) |
||||
|
{ |
||||
|
// Chunk offsets point past the completed movie box, so retain only this bounded metadata box and patch
|
||||
|
// its two offsets once its size is known. The encoded frame payload remains in allocator-backed chunks.
|
||||
|
using AutoExpandingMemory<byte> memory = new(this.configuration, 0x1000); |
||||
|
int offset = 0; |
||||
|
int movieStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Moov); |
||||
|
ulong mediaDuration = GetSequenceMediaDuration(sequence.ColorTrack.Samples); |
||||
|
ulong trackDuration = sequence.RepeatCount == 0 |
||||
|
? ulong.MaxValue |
||||
|
: checked(mediaDuration * sequence.RepeatCount); |
||||
|
bool hasAlpha = sequence.AlphaTrack.HasValue; |
||||
|
|
||||
|
WriteSequenceMovieHeader( |
||||
|
memory, |
||||
|
ref offset, |
||||
|
sequence.Timescale, |
||||
|
trackDuration, |
||||
|
hasAlpha ? 3U : 2U); |
||||
|
int colorChunkOffsetPosition = WriteSequenceTrack( |
||||
|
memory, |
||||
|
ref offset, |
||||
|
sequence, |
||||
|
sequence.ColorTrack, |
||||
|
1, |
||||
|
mediaDuration, |
||||
|
trackDuration); |
||||
|
|
||||
|
int alphaChunkOffsetPosition = -1; |
||||
|
long alphaPayloadOffset = 0; |
||||
|
if (hasAlpha) |
||||
|
{ |
||||
|
HeifSequenceTrackEncoding alphaTrack = sequence.AlphaTrack.GetValueOrDefault(); |
||||
|
alphaPayloadOffset = alphaTrack.Samples[0].Offset; |
||||
|
alphaChunkOffsetPosition = WriteSequenceTrack( |
||||
|
memory, |
||||
|
ref offset, |
||||
|
sequence, |
||||
|
alphaTrack, |
||||
|
2, |
||||
|
mediaDuration, |
||||
|
trackDuration); |
||||
|
} |
||||
|
|
||||
|
EndSequenceBox(memory, movieStart, offset); |
||||
|
ulong mediaDataOffset = checked((ulong)fileTypeLength + (uint)offset + 8U); |
||||
|
Span<byte> movie = memory.GetSpan(offset); |
||||
|
BinaryPrimitives.WriteUInt64BigEndian( |
||||
|
movie[colorChunkOffsetPosition..], |
||||
|
checked(mediaDataOffset + (ulong)sequence.ColorTrack.Samples[0].Offset)); |
||||
|
|
||||
|
if (alphaChunkOffsetPosition >= 0) |
||||
|
{ |
||||
|
BinaryPrimitives.WriteUInt64BigEndian( |
||||
|
movie[alphaChunkOffsetPosition..], |
||||
|
checked(mediaDataOffset + (ulong)alphaPayloadOffset)); |
||||
|
} |
||||
|
|
||||
|
stream.Write(movie); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceMovieHeader( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
uint timescale, |
||||
|
ulong duration, |
||||
|
uint nextTrackId) |
||||
|
{ |
||||
|
int movieHeaderStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Mvhd); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 1, 0); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, timescale); |
||||
|
WriteSequenceUInt64(memory, ref offset, duration); |
||||
|
WriteSequenceUInt32(memory, ref offset, UnityFixed16Point16); |
||||
|
WriteSequenceUInt16(memory, ref offset, UnityFixed8Point8); |
||||
|
WriteSequenceUInt16(memory, ref offset, 0); |
||||
|
WriteSequenceZeros(memory, ref offset, 2 * sizeof(uint)); |
||||
|
WriteSequenceIdentityMatrix(memory, ref offset); |
||||
|
WriteSequenceZeros(memory, ref offset, 6 * sizeof(uint)); |
||||
|
WriteSequenceUInt32(memory, ref offset, nextTrackId); |
||||
|
EndSequenceBox(memory, movieHeaderStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static int WriteSequenceTrack( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
HeifSequenceEncoding sequence, |
||||
|
HeifSequenceTrackEncoding track, |
||||
|
uint trackId, |
||||
|
ulong mediaDuration, |
||||
|
ulong trackDuration) |
||||
|
{ |
||||
|
int trackStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Trak); |
||||
|
WriteSequenceTrackHeader( |
||||
|
memory, |
||||
|
ref offset, |
||||
|
sequence.Width, |
||||
|
sequence.Height, |
||||
|
trackId, |
||||
|
trackDuration); |
||||
|
|
||||
|
if (track.IsAlpha) |
||||
|
{ |
||||
|
WriteSequenceTrackReference(memory, ref offset, Heif4CharCode.Auxl, 1); |
||||
|
} |
||||
|
|
||||
|
if (sequence.RepeatCount != 1) |
||||
|
{ |
||||
|
WriteSequenceEditList(memory, ref offset, mediaDuration); |
||||
|
} |
||||
|
|
||||
|
int mediaStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Mdia); |
||||
|
WriteSequenceMediaHeader(memory, ref offset, sequence.Timescale, mediaDuration); |
||||
|
WriteSequenceHandler(memory, ref offset, track.IsAlpha ? Heif4CharCode.Auxv : Heif4CharCode.Pict); |
||||
|
int mediaInformationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Minf); |
||||
|
WriteSequenceDataInformation(memory, ref offset); |
||||
|
int chunkOffsetPosition = WriteSequenceSampleTable(memory, ref offset, sequence, track); |
||||
|
EndSequenceBox(memory, mediaInformationStart, offset); |
||||
|
EndSequenceBox(memory, mediaStart, offset); |
||||
|
EndSequenceBox(memory, trackStart, offset); |
||||
|
return chunkOffsetPosition; |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceTrackHeader( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
int width, |
||||
|
int height, |
||||
|
uint trackId, |
||||
|
ulong duration) |
||||
|
{ |
||||
|
int trackHeaderStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Tkhd); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 1, 1); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, trackId); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt64(memory, ref offset, duration); |
||||
|
WriteSequenceZeros(memory, ref offset, (2 * sizeof(uint)) + (4 * sizeof(ushort))); |
||||
|
WriteSequenceIdentityMatrix(memory, ref offset); |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)width << 16)); |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)height << 16)); |
||||
|
EndSequenceBox(memory, trackHeaderStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceTrackReference( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
Heif4CharCode referenceType, |
||||
|
uint referencedTrackId) |
||||
|
{ |
||||
|
int referencesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Tref); |
||||
|
int referenceStart = BeginSequenceBox(memory, ref offset, referenceType); |
||||
|
WriteSequenceUInt32(memory, ref offset, referencedTrackId); |
||||
|
EndSequenceBox(memory, referenceStart, offset); |
||||
|
EndSequenceBox(memory, referencesStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceEditList( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
ulong mediaDuration) |
||||
|
{ |
||||
|
int editStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Edts); |
||||
|
int editListStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Elst); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 1, 1); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
WriteSequenceUInt64(memory, ref offset, mediaDuration); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt16(memory, ref offset, 1); |
||||
|
WriteSequenceUInt16(memory, ref offset, 0); |
||||
|
EndSequenceBox(memory, editListStart, offset); |
||||
|
EndSequenceBox(memory, editStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceMediaHeader( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
uint timescale, |
||||
|
ulong mediaDuration) |
||||
|
{ |
||||
|
int mediaHeaderStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Mdhd); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 1, 0); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, timescale); |
||||
|
WriteSequenceUInt64(memory, ref offset, mediaDuration); |
||||
|
WriteSequenceUInt16(memory, ref offset, PackedUndeterminedLanguage); |
||||
|
WriteSequenceUInt16(memory, ref offset, 0); |
||||
|
EndSequenceBox(memory, mediaHeaderStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceHandler( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
Heif4CharCode handlerType) |
||||
|
{ |
||||
|
int handlerStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Hdlr); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, (uint)handlerType); |
||||
|
WriteSequenceZeros(memory, ref offset, 12); |
||||
|
memory.GetSpan(offset++, 1)[0] = 0; |
||||
|
EndSequenceBox(memory, handlerStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceDataInformation(AutoExpandingMemory<byte> memory, ref int offset) |
||||
|
{ |
||||
|
int dataInformationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dinf); |
||||
|
int dataReferenceStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dref); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
int locationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Url); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 1); |
||||
|
EndSequenceBox(memory, locationStart, offset); |
||||
|
EndSequenceBox(memory, dataReferenceStart, offset); |
||||
|
EndSequenceBox(memory, dataInformationStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static int WriteSequenceSampleTable( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
HeifSequenceEncoding sequence, |
||||
|
HeifSequenceTrackEncoding track) |
||||
|
{ |
||||
|
int sampleTableStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stbl); |
||||
|
WriteSequenceSampleDescription(memory, ref offset, sequence, track); |
||||
|
WriteSequenceSampleTiming(memory, ref offset, track.Samples); |
||||
|
|
||||
|
// Payloads are emitted contiguously per track, so one chunk maps directly to every sample in that track.
|
||||
|
int sampleToChunkStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stsc); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length)); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
EndSequenceBox(memory, sampleToChunkStart, offset); |
||||
|
|
||||
|
int sampleSizesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stsz); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length)); |
||||
|
foreach (HeifSequenceSampleInfo sample in track.Samples) |
||||
|
{ |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)sample.Length)); |
||||
|
} |
||||
|
|
||||
|
EndSequenceBox(memory, sampleSizesStart, offset); |
||||
|
|
||||
|
int chunkOffsetsStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Co64); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
int chunkOffsetPosition = offset; |
||||
|
WriteSequenceUInt64(memory, ref offset, 0); |
||||
|
EndSequenceBox(memory, chunkOffsetsStart, offset); |
||||
|
|
||||
|
// The current bounded sequence encoder emits independent all-intra pictures; every sample is seekable.
|
||||
|
int syncSamplesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stss); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length)); |
||||
|
for (uint sampleIndex = 1; sampleIndex <= track.Samples.Length; sampleIndex++) |
||||
|
{ |
||||
|
WriteSequenceUInt32(memory, ref offset, sampleIndex); |
||||
|
} |
||||
|
|
||||
|
EndSequenceBox(memory, syncSamplesStart, offset); |
||||
|
EndSequenceBox(memory, sampleTableStart, offset); |
||||
|
return chunkOffsetPosition; |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceSampleDescription( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
HeifSequenceEncoding sequence, |
||||
|
HeifSequenceTrackEncoding track) |
||||
|
{ |
||||
|
int descriptionStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stsd); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 1); |
||||
|
int sampleEntryStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Av01); |
||||
|
WriteSequenceZeros(memory, ref offset, 6); |
||||
|
WriteSequenceUInt16(memory, ref offset, 1); |
||||
|
WriteSequenceZeros(memory, ref offset, (2 * sizeof(ushort)) + (3 * sizeof(uint))); |
||||
|
WriteSequenceUInt16(memory, ref offset, checked((ushort)sequence.Width)); |
||||
|
WriteSequenceUInt16(memory, ref offset, checked((ushort)sequence.Height)); |
||||
|
WriteSequenceUInt32(memory, ref offset, 72U << 16); |
||||
|
WriteSequenceUInt32(memory, ref offset, 72U << 16); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt16(memory, ref offset, 1); |
||||
|
WriteSequenceZeros(memory, ref offset, 32); |
||||
|
WriteSequenceUInt16(memory, ref offset, 24); |
||||
|
WriteSequenceUInt16(memory, ref offset, ushort.MaxValue); |
||||
|
|
||||
|
int configurationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Av1C); |
||||
|
track.Configuration.WriteFixedHeader(memory.GetSpan(offset, Av1CodecConfiguration.FixedHeaderSize)); |
||||
|
offset += Av1CodecConfiguration.FixedHeaderSize; |
||||
|
EndSequenceBox(memory, configurationStart, offset); |
||||
|
|
||||
|
if (track.IsAlpha) |
||||
|
{ |
||||
|
int auxiliaryTypeStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Auxi); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(HeifConstants.AlphaAuxiliaryType); |
||||
|
Span<byte> auxiliaryType = memory.GetSpan(offset, auxiliaryTypeLength + 1); |
||||
|
offset += Encoding.UTF8.GetBytes(HeifConstants.AlphaAuxiliaryType, auxiliaryType); |
||||
|
memory.GetSpan(offset++, 1)[0] = 0; |
||||
|
EndSequenceBox(memory, auxiliaryTypeStart, offset); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (sequence.IccProfile is not null) |
||||
|
{ |
||||
|
offset += WriteIccColorInformationPropertyBox(memory, offset, sequence.IccProfile); |
||||
|
} |
||||
|
|
||||
|
offset += WriteColorInformationPropertyBox(memory, offset, sequence.ColorProfile); |
||||
|
} |
||||
|
|
||||
|
int codingConstraintsStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Ccst); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
|
||||
|
// Every emitted sequence sample is independently decodable, while intra prediction remains available inside
|
||||
|
// each picture. No inter-picture reference slot is therefore advertised.
|
||||
|
WriteSequenceUInt32(memory, ref offset, 0xC0000000); |
||||
|
EndSequenceBox(memory, codingConstraintsStart, offset); |
||||
|
EndSequenceBox(memory, sampleEntryStart, offset); |
||||
|
EndSequenceBox(memory, descriptionStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceSampleTiming( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
ReadOnlySpan<HeifSequenceSampleInfo> samples) |
||||
|
{ |
||||
|
// The time-to-sample table stores runs, not one entry per frame. Preserve exact resolved durations while
|
||||
|
// combining only adjacent frames whose delays are equal.
|
||||
|
int runCount = 1; |
||||
|
for (int sampleIndex = 1; sampleIndex < samples.Length; sampleIndex++) |
||||
|
{ |
||||
|
runCount += samples[sampleIndex].Duration == samples[sampleIndex - 1].Duration ? 0 : 1; |
||||
|
} |
||||
|
|
||||
|
int timingStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stts); |
||||
|
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, checked((uint)runCount)); |
||||
|
uint runDuration = samples[0].Duration; |
||||
|
uint runLength = 1; |
||||
|
for (int sampleIndex = 1; sampleIndex <= samples.Length; sampleIndex++) |
||||
|
{ |
||||
|
if (sampleIndex < samples.Length && samples[sampleIndex].Duration == runDuration) |
||||
|
{ |
||||
|
runLength++; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
WriteSequenceUInt32(memory, ref offset, runLength); |
||||
|
WriteSequenceUInt32(memory, ref offset, runDuration); |
||||
|
if (sampleIndex < samples.Length) |
||||
|
{ |
||||
|
runDuration = samples[sampleIndex].Duration; |
||||
|
runLength = 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
EndSequenceBox(memory, timingStart, offset); |
||||
|
} |
||||
|
|
||||
|
private static uint GetSequenceSampleDuration(Rational delay, uint timescale) |
||||
|
{ |
||||
|
if (delay.Numerator == 0) |
||||
|
{ |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
ulong scaledDuration = ((ulong)delay.Numerator * timescale) + (delay.Denominator / 2U); |
||||
|
return checked((uint)Math.Max(1UL, scaledDuration / delay.Denominator)); |
||||
|
} |
||||
|
|
||||
|
private static uint GetSequenceTimescale<TPixel>(Image<TPixel> image) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
uint timescale = DefaultSequenceTimescale; |
||||
|
foreach (ImageFrame<TPixel> frame in image.Frames) |
||||
|
{ |
||||
|
Rational delay = frame.Metadata.GetHeifMetadata().FrameDelay; |
||||
|
if (delay.Numerator == 0) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
uint commonDivisor = GetGreatestCommonDivisor(timescale, delay.Denominator); |
||||
|
ulong commonTimescale = ((ulong)timescale / commonDivisor) * delay.Denominator; |
||||
|
if (commonTimescale > uint.MaxValue) |
||||
|
{ |
||||
|
// A media timescale is a 32-bit field. Microsecond fallback retains bounded timing precision when
|
||||
|
// the exact least common multiple of caller-provided rational delays cannot be represented.
|
||||
|
return FallbackSequenceTimescale; |
||||
|
} |
||||
|
|
||||
|
timescale = (uint)commonTimescale; |
||||
|
} |
||||
|
|
||||
|
return timescale; |
||||
|
} |
||||
|
|
||||
|
private static uint GetGreatestCommonDivisor(uint left, uint right) |
||||
|
{ |
||||
|
while (right != 0) |
||||
|
{ |
||||
|
uint remainder = left % right; |
||||
|
left = right; |
||||
|
right = remainder; |
||||
|
} |
||||
|
|
||||
|
return left; |
||||
|
} |
||||
|
|
||||
|
private static ulong GetSequenceMediaDuration(ReadOnlySpan<HeifSequenceSampleInfo> samples) |
||||
|
{ |
||||
|
ulong duration = 0; |
||||
|
foreach (HeifSequenceSampleInfo sample in samples) |
||||
|
{ |
||||
|
duration = checked(duration + sample.Duration); |
||||
|
} |
||||
|
|
||||
|
return duration; |
||||
|
} |
||||
|
|
||||
|
private static int BeginSequenceBox( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
Heif4CharCode type) |
||||
|
{ |
||||
|
// Reserve the size field now and patch it at the matching EndSequenceBox call after nested boxes expand.
|
||||
|
int start = offset; |
||||
|
offset += WriteBoxHeader(memory.GetSpan(offset, 8), type); |
||||
|
return start; |
||||
|
} |
||||
|
|
||||
|
private static void EndSequenceBox(AutoExpandingMemory<byte> memory, int start, int offset) |
||||
|
=> BinaryPrimitives.WriteUInt32BigEndian( |
||||
|
memory.GetSpan(start, sizeof(uint)), |
||||
|
checked((uint)(offset - start))); |
||||
|
|
||||
|
private static void WriteSequenceFullBoxHeader( |
||||
|
AutoExpandingMemory<byte> memory, |
||||
|
ref int offset, |
||||
|
byte version, |
||||
|
uint flags) |
||||
|
{ |
||||
|
Span<byte> destination = memory.GetSpan(offset, sizeof(uint)); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(destination, flags); |
||||
|
destination[0] = version; |
||||
|
offset += sizeof(uint); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceIdentityMatrix(AutoExpandingMemory<byte> memory, ref int offset) |
||||
|
{ |
||||
|
WriteSequenceUInt32(memory, ref offset, UnityFixed16Point16); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, UnityFixed16Point16); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, 0); |
||||
|
WriteSequenceUInt32(memory, ref offset, UnityFixed2Point30); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceZeros(AutoExpandingMemory<byte> memory, ref int offset, int length) |
||||
|
{ |
||||
|
memory.GetSpan(offset, length).Clear(); |
||||
|
offset += length; |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceUInt16(AutoExpandingMemory<byte> memory, ref int offset, ushort value) |
||||
|
{ |
||||
|
BinaryPrimitives.WriteUInt16BigEndian(memory.GetSpan(offset, sizeof(ushort)), value); |
||||
|
offset += sizeof(ushort); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceUInt32(AutoExpandingMemory<byte> memory, ref int offset, uint value) |
||||
|
{ |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(memory.GetSpan(offset, sizeof(uint)), value); |
||||
|
offset += sizeof(uint); |
||||
|
} |
||||
|
|
||||
|
private static void WriteSequenceUInt64(AutoExpandingMemory<byte> memory, ref int offset, ulong value) |
||||
|
{ |
||||
|
BinaryPrimitives.WriteUInt64BigEndian(memory.GetSpan(offset, sizeof(ulong)), value); |
||||
|
offset += sizeof(ulong); |
||||
|
} |
||||
|
|
||||
|
private readonly struct Av1EncodingSettings |
||||
|
{ |
||||
|
public Av1EncodingSettings( |
||||
|
HeifBitDepth bitDepth, |
||||
|
HeifChromaSubsampling chromaSubsampling, |
||||
|
CicpProfile colorProfile, |
||||
|
ObuColorConfig colorConfig, |
||||
|
ObuColorConfig alphaConfig, |
||||
|
int colorQIndex, |
||||
|
int alphaQIndex, |
||||
|
bool hasAlpha) |
||||
|
{ |
||||
|
this.BitDepth = bitDepth; |
||||
|
this.ChromaSubsampling = chromaSubsampling; |
||||
|
this.ColorProfile = colorProfile; |
||||
|
this.ColorConfig = colorConfig; |
||||
|
this.AlphaConfig = alphaConfig; |
||||
|
this.ColorQIndex = colorQIndex; |
||||
|
this.AlphaQIndex = alphaQIndex; |
||||
|
this.HasAlpha = hasAlpha; |
||||
|
} |
||||
|
|
||||
|
public HeifBitDepth BitDepth { get; } |
||||
|
|
||||
|
public HeifChromaSubsampling ChromaSubsampling { get; } |
||||
|
|
||||
|
public CicpProfile ColorProfile { get; } |
||||
|
|
||||
|
public ObuColorConfig ColorConfig { get; } |
||||
|
|
||||
|
public ObuColorConfig AlphaConfig { get; } |
||||
|
|
||||
|
public int ColorQIndex { get; } |
||||
|
|
||||
|
public int AlphaQIndex { get; } |
||||
|
|
||||
|
public bool HasAlpha { get; } |
||||
|
} |
||||
|
|
||||
|
private readonly struct HeifSequenceSampleInfo |
||||
|
{ |
||||
|
public HeifSequenceSampleInfo(long offset, int length, uint duration) |
||||
|
{ |
||||
|
this.Offset = offset; |
||||
|
this.Length = length; |
||||
|
this.Duration = duration; |
||||
|
} |
||||
|
|
||||
|
public long Offset { get; } |
||||
|
|
||||
|
public int Length { get; } |
||||
|
|
||||
|
public uint Duration { get; } |
||||
|
} |
||||
|
|
||||
|
private readonly struct HeifSequenceEncoding |
||||
|
{ |
||||
|
public HeifSequenceEncoding( |
||||
|
int width, |
||||
|
int height, |
||||
|
ushort repeatCount, |
||||
|
uint timescale, |
||||
|
HeifSequenceTrackEncoding colorTrack, |
||||
|
HeifSequenceTrackEncoding? alphaTrack, |
||||
|
CicpProfile colorProfile, |
||||
|
IccProfile? iccProfile) |
||||
|
{ |
||||
|
this.Width = width; |
||||
|
this.Height = height; |
||||
|
this.RepeatCount = repeatCount; |
||||
|
this.Timescale = timescale; |
||||
|
this.ColorTrack = colorTrack; |
||||
|
this.AlphaTrack = alphaTrack; |
||||
|
this.ColorProfile = colorProfile; |
||||
|
this.IccProfile = iccProfile; |
||||
|
} |
||||
|
|
||||
|
public int Width { get; } |
||||
|
|
||||
|
public int Height { get; } |
||||
|
|
||||
|
public ushort RepeatCount { get; } |
||||
|
|
||||
|
public uint Timescale { get; } |
||||
|
|
||||
|
public HeifSequenceTrackEncoding ColorTrack { get; } |
||||
|
|
||||
|
public HeifSequenceTrackEncoding? AlphaTrack { get; } |
||||
|
|
||||
|
public CicpProfile ColorProfile { get; } |
||||
|
|
||||
|
public IccProfile? IccProfile { get; } |
||||
|
} |
||||
|
|
||||
|
private readonly struct HeifSequenceTrackEncoding |
||||
|
{ |
||||
|
private readonly HeifSequenceSampleInfo[] samples; |
||||
|
private readonly int sampleOffset; |
||||
|
private readonly int sampleCount; |
||||
|
|
||||
|
public HeifSequenceTrackEncoding( |
||||
|
Av1CodecConfiguration configuration, |
||||
|
HeifSequenceSampleInfo[] samples, |
||||
|
int sampleOffset, |
||||
|
int sampleCount, |
||||
|
bool isAlpha) |
||||
|
{ |
||||
|
this.Configuration = configuration; |
||||
|
this.samples = samples; |
||||
|
this.sampleOffset = sampleOffset; |
||||
|
this.sampleCount = sampleCount; |
||||
|
this.IsAlpha = isAlpha; |
||||
|
} |
||||
|
|
||||
|
public Av1CodecConfiguration Configuration { get; } |
||||
|
|
||||
|
public ReadOnlySpan<HeifSequenceSampleInfo> Samples |
||||
|
=> this.samples.AsSpan(this.sampleOffset, this.sampleCount); |
||||
|
|
||||
|
public bool IsAlpha { get; } |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue