Browse Source

Refine AV1 encoder memory ownership and storage

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
74fedce281
  1. 81
      src/ImageSharp/Common/InlineArray.cs
  2. 2
      src/ImageSharp/Common/InlineArray.tt
  3. 52
      src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs
  4. 15
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs
  5. 15
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs
  6. 20
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  7. 40
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs
  8. 15
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs
  9. 14
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs
  10. 27
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs
  11. 41
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs
  12. 14
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs
  13. 18
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs
  14. 21
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs
  15. 67
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  16. 51
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs
  17. 55
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs
  18. 14
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs
  19. 21
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs
  20. 10
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs
  21. 26
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs
  22. 13
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  23. 24
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs
  24. 18
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs
  25. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs
  26. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs
  27. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs
  28. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs
  29. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs
  30. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs
  31. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs
  32. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs
  33. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs
  34. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs
  35. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs
  36. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs
  37. 6
      src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs
  38. 1
      src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs
  39. 4
      src/ImageSharp/Formats/Heif/HeifEncoder.cs
  40. 445
      src/ImageSharp/Formats/Heif/HeifEncoderCore.Sequence.cs
  41. 357
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  42. 30
      src/ImageSharp/Formats/Heif/HeifItem.cs
  43. 78
      src/ImageSharp/Memory/AutoExpandingMemory.cs
  44. 55
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs
  45. 27
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  46. 90
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  47. 8
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs
  48. 217
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
  49. 10
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs
  50. 14
      tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs
  51. 48
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  52. 87
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
  53. 2
      tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs
  54. 53
      tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs

81
src/ImageSharp/Common/InlineArray.cs

@ -8,6 +8,24 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
/// <summary>
/// Represents a safe, fixed sized buffer of 2 elements.
/// </summary>
[InlineArray(2)]
internal struct InlineArray2<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 3 elements.
/// </summary>
[InlineArray(3)]
internal struct InlineArray3<T>
{
private T t;
}
/// <summary> /// <summary>
/// Represents a safe, fixed sized buffer of 4 elements. /// Represents a safe, fixed sized buffer of 4 elements.
/// </summary> /// </summary>
@ -17,6 +35,24 @@ internal struct InlineArray4<T>
private T t; private T t;
} }
/// <summary>
/// Represents a safe, fixed sized buffer of 6 elements.
/// </summary>
[InlineArray(6)]
internal struct InlineArray6<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 7 elements.
/// </summary>
[InlineArray(7)]
internal struct InlineArray7<T>
{
private T t;
}
/// <summary> /// <summary>
/// Represents a safe, fixed sized buffer of 8 elements. /// Represents a safe, fixed sized buffer of 8 elements.
/// </summary> /// </summary>
@ -26,6 +62,24 @@ internal struct InlineArray8<T>
private T t; private T t;
} }
/// <summary>
/// Represents a safe, fixed sized buffer of 10 elements.
/// </summary>
[InlineArray(10)]
internal struct InlineArray10<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 12 elements.
/// </summary>
[InlineArray(12)]
internal struct InlineArray12<T>
{
private T t;
}
/// <summary> /// <summary>
/// Represents a safe, fixed sized buffer of 14 elements. /// Represents a safe, fixed sized buffer of 14 elements.
/// </summary> /// </summary>
@ -62,6 +116,24 @@ internal struct InlineArray19<T>
private T t; private T t;
} }
/// <summary>
/// Represents a safe, fixed sized buffer of 24 elements.
/// </summary>
[InlineArray(24)]
internal struct InlineArray24<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 25 elements.
/// </summary>
[InlineArray(25)]
internal struct InlineArray25<T>
{
private T t;
}
/// <summary> /// <summary>
/// Represents a safe, fixed sized buffer of 26 elements. /// Represents a safe, fixed sized buffer of 26 elements.
/// </summary> /// </summary>
@ -80,6 +152,15 @@ internal struct InlineArray36<T>
private T t; private T t;
} }
/// <summary>
/// Represents a safe, fixed sized buffer of 65 elements.
/// </summary>
[InlineArray(65)]
internal struct InlineArray65<T>
{
private T t;
}
/// <summary> /// <summary>
/// Represents a safe, fixed sized buffer of 256 elements. /// Represents a safe, fixed sized buffer of 256 elements.
/// </summary> /// </summary>

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#> <#GenerateInlineArrays();#>
<#+ <#+
private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256]; private static int[] Lengths = [2, 3, 4, 6, 7, 8, 10, 12, 14, 16, 18, 19, 24, 25, 26, 36, 65, 256];
void GenerateInlineArrays() void GenerateInlineArrays()
{ {

52
src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs

@ -1,12 +1,10 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1; namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary> /// <summary>
/// Writes AV1 fixed-width and variable-length syntax to reusable expanding memory. /// Writes AV1 fixed-width and variable-length syntax to a caller-provided buffer.
/// </summary> /// </summary>
internal ref struct Av1BitStreamWriter internal ref struct Av1BitStreamWriter
{ {
@ -16,19 +14,9 @@ internal ref struct Av1BitStreamWriter
private const int WordSize = 8; private const int WordSize = 8;
/// <summary> /// <summary>
/// The expanding output allocation. /// The writable output buffer.
/// </summary>
private readonly AutoExpandingMemory<byte> memory;
/// <summary>
/// The current writable view over <see cref="memory"/>.
/// </summary> /// </summary>
private Span<byte> span; private readonly Span<byte> span;
/// <summary>
/// The final byte index that can be written without expanding <see cref="memory"/>.
/// </summary>
private int capacityTrigger;
/// <summary> /// <summary>
/// The partially assembled output byte. /// The partially assembled output byte.
@ -38,12 +26,10 @@ internal ref struct Av1BitStreamWriter
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1BitStreamWriter"/> struct. /// Initializes a new instance of the <see cref="Av1BitStreamWriter"/> struct.
/// </summary> /// </summary>
/// <param name="memory">The reusable expanding output allocation.</param> /// <param name="span">The preallocated output buffer.</param>
public Av1BitStreamWriter(AutoExpandingMemory<byte> memory) public Av1BitStreamWriter(Span<byte> span)
{ {
this.memory = memory; this.span = span;
this.span = memory.GetEntireSpan();
this.capacityTrigger = memory.Capacity - 1;
} }
/// <summary> /// <summary>
@ -54,7 +40,7 @@ internal ref struct Av1BitStreamWriter
/// <summary> /// <summary>
/// Gets the current output capacity in bytes. /// Gets the current output capacity in bytes.
/// </summary> /// </summary>
public readonly int Capacity => this.memory.Capacity; public readonly int Capacity => this.span.Length;
/// <summary> /// <summary>
/// Encodes an unsigned 32-bit value using little-endian base-128 bytes. /// Encodes an unsigned 32-bit value using little-endian base-128 bytes.
@ -155,14 +141,6 @@ internal ref struct Av1BitStreamWriter
public void WriteLittleEndianBytes128(uint value) public void WriteLittleEndianBytes128(uint value)
{ {
int wordPosition = this.BitPosition >> 3; int wordPosition = this.BitPosition >> 3;
const int maximumEncodedLength = 5;
if (this.span.Length - wordPosition < maximumEncodedLength)
{
this.memory.GetSpan(wordPosition + maximumEncodedLength);
this.span = this.memory.GetEntireSpan();
this.capacityTrigger = this.span.Length - 1;
}
int bytesWritten = GetLittleEndianBytes128(value, this.span[wordPosition..]); int bytesWritten = GetLittleEndianBytes128(value, this.span[wordPosition..]);
this.BitPosition += bytesWritten << 3; this.BitPosition += bytesWritten << 3;
} }
@ -237,30 +215,16 @@ internal ref struct Av1BitStreamWriter
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Tile Data only allowed on byte alignment"); DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Tile Data only allowed on byte alignment");
int wordPosition = this.BitPosition >> 3; int wordPosition = this.BitPosition >> 3;
if (this.span.Length < wordPosition + tileData.Length)
{
this.memory.GetSpan(wordPosition + tileData.Length);
this.span = this.memory.GetEntireSpan();
}
tileData.CopyTo(this.span[wordPosition..]); tileData.CopyTo(this.span[wordPosition..]);
this.BitPosition += tileData.Length << 3; this.BitPosition += tileData.Length << 3;
} }
/// <summary> /// <summary>
/// Stores the current output byte, expanding the allocation when necessary. /// Stores the current output byte.
/// </summary> /// </summary>
private void WriteBuffer() private void WriteBuffer()
{ {
int wordPosition = Av1Math.DivideBy8Floor(this.BitPosition); int wordPosition = Av1Math.DivideBy8Floor(this.BitPosition);
if (wordPosition > this.capacityTrigger)
{
// Expand the memory allocation.
this.memory.GetSpan(wordPosition + 1);
this.span = this.memory.GetEntireSpan();
this.capacityTrigger = this.span.Length - 1;
}
this.span[wordPosition] = this.buffer; this.span[wordPosition] = this.buffer;
this.buffer = 0; this.buffer = 0;
} }

15
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs

@ -36,7 +36,7 @@ internal sealed class Av1Distribution
/// <summary> /// <summary>
/// The inverse cumulative thresholds followed by the required zero sentinel. /// The inverse cumulative thresholds followed by the required zero sentinel.
/// </summary> /// </summary>
private readonly uint[] probabilities; private InlineArray16<uint> probabilities;
/// <summary> /// <summary>
/// The symbol-count contribution to the adaptive update rate. /// The symbol-count contribution to the adaptive update rate.
@ -273,13 +273,13 @@ internal sealed class Av1Distribution
/// <param name="speed">The symbol-count contribution to the update rate.</param> /// <param name="speed">The symbol-count contribution to the update rate.</param>
private Av1Distribution(ReadOnlySpan<uint> props, int speed) private Av1Distribution(ReadOnlySpan<uint> props, int speed)
{ {
this.probabilities = new uint[props.Length]; Span<uint> probabilities = this.probabilities;
// AV1 range coding consumes inverse cumulative thresholds. The defaults are written in the more readable // AV1 range coding consumes inverse cumulative thresholds. The defaults are written in the more readable
// forward form, so convert every real threshold while leaving the final zero sentinel untouched. // forward form, so convert every real threshold while leaving the final zero sentinel untouched.
for (int i = 0; i < props.Length - 1; i++) for (int i = 0; i < props.Length - 1; i++)
{ {
this.probabilities[i] = ProbabilityTop - props[i]; probabilities[i] = ProbabilityTop - props[i];
} }
this.NumberOfSymbols = props.Length; this.NumberOfSymbols = props.Length;
@ -292,8 +292,9 @@ internal sealed class Av1Distribution
/// <param name="source">The distribution state to copy.</param> /// <param name="source">The distribution state to copy.</param>
private Av1Distribution(Av1Distribution source) private Av1Distribution(Av1Distribution source)
{ {
this.probabilities = new uint[source.probabilities.Length]; ReadOnlySpan<uint> sourceProbabilities = source.probabilities;
source.probabilities.CopyTo(this.probabilities, 0); Span<uint> probabilities = this.probabilities;
sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities);
// The adaptation rate depends on both the alphabet size and prior update count, so copying only the // The adaptation rate depends on both the alphabet size and prior update count, so copying only the
// thresholds would make the cloned frame context diverge after its next symbol. // thresholds would make the cloned frame context diverge after its next symbol.
@ -328,7 +329,9 @@ internal sealed class Av1Distribution
{ {
// Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a // Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a
// working tile never allocates or replaces the distribution objects referenced by the symbol decoder. // working tile never allocates or replaces the distribution objects referenced by the symbol decoder.
source.probabilities.AsSpan().CopyTo(this.probabilities); ReadOnlySpan<uint> sourceProbabilities = source.probabilities;
Span<uint> probabilities = this.probabilities;
sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities);
this.updateCount = source.updateCount; this.updateCount = source.updateCount;
} }

15
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary> /// <summary>
@ -139,17 +137,4 @@ internal sealed class Av1FrameEntropyContexts
} }
} }
} }
/// <summary>
/// Provides inline storage for every entropy snapshot graph that one decoder session can allocate concurrently.
/// </summary>
/// <typeparam name="T">The stored reference type.</typeparam>
[InlineArray(MaximumSnapshotCount)]
private struct InlineArray10<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

20
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary> /// <summary>
/// Encodes AV1 tile syntax elements and transform coefficients with tile-local adaptive distributions. /// Encodes AV1 tile syntax elements and transform coefficients with tile-local adaptive distributions.
/// </summary> /// </summary>
internal class Av1SymbolEncoder : IDisposable internal sealed class Av1SymbolEncoder : IDisposable
{ {
/// <summary> /// <summary>
/// The largest coefficient-context plane required after AV1 removes the uncoded half of 64-point transforms. /// The largest coefficient-context plane required after AV1 removes the uncoded half of 64-point transforms.
@ -186,10 +186,10 @@ internal class Av1SymbolEncoder : IDisposable
/// Initializes a new instance of the <see cref="Av1SymbolEncoder"/> class for one AV1 tile. /// Initializes a new instance of the <see cref="Av1SymbolEncoder"/> class for one AV1 tile.
/// </summary> /// </summary>
/// <param name="configuration">The configuration providing output and temporary memory.</param> /// <param name="configuration">The configuration providing output and temporary memory.</param>
/// <param name="initialSize">The initial output buffer size in bytes.</param> /// <param name="bufferLength">The complete fixed output allocation length in bytes.</param>
/// <param name="qIndex">The frame base quantizer index.</param> /// <param name="qIndex">The frame base quantizer index.</param>
/// <param name="updateCdf">A value indicating whether encoded symbols adapt their tile distributions.</param> /// <param name="updateCdf">A value indicating whether encoded symbols adapt their tile distributions.</param>
public Av1SymbolEncoder(Configuration configuration, int initialSize, int qIndex, bool updateCdf = true) public Av1SymbolEncoder(Configuration configuration, int bufferLength, int qIndex, bool updateCdf)
{ {
this.configuration = configuration; this.configuration = configuration;
@ -219,7 +219,7 @@ internal class Av1SymbolEncoder : IDisposable
this.coefficientsBaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex); this.coefficientsBaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.dcSign = Av1DefaultDistributions.GetDcSign(qIndex); this.dcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.endOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex); this.endOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
this.writer = new(configuration, initialSize, updateCdf); this.writer = new(configuration, bufferLength, updateCdf);
this.baseQIndex = qIndex; this.baseQIndex = qIndex;
} }
@ -1310,7 +1310,7 @@ internal class Av1SymbolEncoder : IDisposable
} }
/// <summary> /// <summary>
/// Finalizes the range-coded tile payload and transfers ownership of its memory. /// Finalizes the range-coded tile payload and returns an owned exact-length copy.
/// </summary> /// </summary>
/// <returns>The memory owner containing the encoded tile bytes.</returns> /// <returns>The memory owner containing the encoded tile bytes.</returns>
public IMemoryOwner<byte> Exit() public IMemoryOwner<byte> Exit()
@ -1320,18 +1320,18 @@ internal class Av1SymbolEncoder : IDisposable
} }
/// <summary> /// <summary>
/// Finalizes the range-coded tile payload and transfers its current allocation without copying. /// Finalizes the range-coded tile payload and exposes its encoded prefix without copying.
/// </summary> /// </summary>
/// <param name="length">The number of encoded bytes at the beginning of the returned allocation.</param> /// <param name="length">The number of encoded bytes in the returned memory.</param>
/// <returns>The complete allocation containing the encoded tile prefix.</returns> /// <returns>The encoded prefix, valid until this encoder is disposed.</returns>
public IMemoryOwner<byte> Exit(out int length) public ReadOnlyMemory<byte> Exit(out int length)
{ {
ref Av1SymbolWriter w = ref this.writer; ref Av1SymbolWriter w = ref this.writer;
return w.Exit(out length); return w.Exit(out length);
} }
/// <summary> /// <summary>
/// Releases output memory that has not been transferred by <see cref="Exit()"/>. /// Releases the range-coder output buffer and coefficient scratch memory.
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {

40
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary> /// <summary>
/// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence. /// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence.
/// </summary> /// </summary>
internal class Av1SymbolWriter : IDisposable internal sealed class Av1SymbolWriter : IDisposable
{ {
/// <summary> /// <summary>
/// The lower endpoint of the current coding interval. /// The lower endpoint of the current coding interval.
@ -36,9 +36,14 @@ internal class Av1SymbolWriter : IDisposable
private readonly Configuration configuration; private readonly Configuration configuration;
/// <summary> /// <summary>
/// The output bytes accumulated during renormalization. /// The owner of the fixed output buffer supplied for this tile.
/// </summary> /// </summary>
private readonly AutoExpandingMemory<byte> memory; private readonly IMemoryOwner<byte> bufferOwner;
/// <summary>
/// The requested output range, excluding any excess capacity returned by a pooling allocator.
/// </summary>
private readonly Memory<byte> buffer;
/// <summary> /// <summary>
/// Indicates whether encoded symbols adapt their distributions. /// Indicates whether encoded symbols adapt their distributions.
@ -51,22 +56,23 @@ internal class Av1SymbolWriter : IDisposable
private int position; private int position;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolWriter"/> class with an estimated output size. /// Initializes a new instance of the <see cref="Av1SymbolWriter"/> class with a bounded output size.
/// </summary> /// </summary>
/// <param name="configuration">The configuration that supplies output allocation.</param> /// <param name="configuration">The configuration that supplies output allocation.</param>
/// <param name="initialSize">The estimated encoded size in bytes.</param> /// <param name="bufferLength">The complete fixed output allocation length in bytes.</param>
/// <param name="updateCdf">A value indicating whether encoded symbols adapt their distributions.</param> /// <param name="updateCdf">A value indicating whether encoded symbols adapt their distributions.</param>
public Av1SymbolWriter(Configuration configuration, int initialSize, bool updateCdf = true) public Av1SymbolWriter(Configuration configuration, int bufferLength, bool updateCdf)
{ {
this.configuration = configuration; this.configuration = configuration;
this.memory = new AutoExpandingMemory<byte>(configuration, initialSize); this.bufferOwner = configuration.MemoryAllocator.Allocate<byte>(bufferLength);
this.buffer = this.bufferOwner.Memory[..bufferLength];
this.updateCdf = updateCdf; this.updateCdf = updateCdf;
} }
/// <summary> /// <summary>
/// Releases the expandable pre-carry buffer. /// Releases the tile output buffer.
/// </summary> /// </summary>
public void Dispose() => this.memory.Dispose(); public void Dispose() => this.bufferOwner.Dispose();
/// <summary> /// <summary>
/// Writes one binary symbol and adapts its distribution when CDF updates are enabled. /// Writes one binary symbol and adapts its distribution when CDF updates are enabled.
@ -132,20 +138,20 @@ internal class Av1SymbolWriter : IDisposable
{ {
int length = this.FinalizeRange(); int length = this.FinalizeRange();
IMemoryOwner<byte> output = this.configuration.MemoryAllocator.Allocate<byte>(length); IMemoryOwner<byte> output = this.configuration.MemoryAllocator.Allocate<byte>(length);
this.memory.GetSpan(length).CopyTo(output.GetSpan()[..length]); this.buffer.Span[..length].CopyTo(output.Memory.Span);
return output; return output;
} }
/// <summary> /// <summary>
/// Finalizes the range-coded sequence and transfers its current allocation without copying. /// Finalizes the range-coded sequence and exposes its encoded prefix without copying.
/// </summary> /// </summary>
/// <param name="length">The number of encoded bytes at the beginning of the returned allocation.</param> /// <param name="length">The number of encoded bytes in the returned memory.</param>
/// <returns>The complete allocation containing the encoded byte prefix.</returns> /// <returns>The encoded prefix, valid until this writer is disposed.</returns>
public IMemoryOwner<byte> Exit(out int length) public ReadOnlyMemory<byte> Exit(out int length)
{ {
length = this.FinalizeRange(); length = this.FinalizeRange();
return this.memory.Detach(); return this.buffer[..length];
} }
/// <summary> /// <summary>
@ -164,7 +170,7 @@ internal class Av1SymbolWriter : IDisposable
ulong e = ((l + m) & ~m) | (m + 1); ulong e = ((l + m) & ~m) | (m + 1);
s += c; s += c;
int pendingByteCount = Math.Max((s + 7) >> 3, 0); int pendingByteCount = Math.Max((s + 7) >> 3, 0);
Span<byte> buffer = this.memory.GetSpan(pos + pendingByteCount); Span<byte> buffer = this.buffer.Span[..(pos + pendingByteCount)];
if (s > 0) if (s > 0)
{ {
ulong n = (1UL << (c + 16)) - 1; ulong n = (1UL << (c + 16)) - 1;
@ -289,7 +295,7 @@ internal class Av1SymbolWriter : IDisposable
// bytes together while preserving one carry bit. // bytes together while preserving one carry bit.
if (s >= 40) if (s >= 40)
{ {
Span<byte> buffer = this.memory.GetSpan(this.position + sizeof(ulong)); Span<byte> buffer = this.buffer.Span[..(this.position + sizeof(ulong))];
int readyByteCount = (s >> 3) + 1; int readyByteCount = (s >> 3) + 1;
c += 24 - (readyByteCount << 3); c += 24 - (readyByteCount << 3);
ulong output = low >> c; ulong output = low >> c;

15
src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs

@ -2,8 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
@ -484,17 +482,4 @@ internal struct Av1GlobalMotionParameters
=> value < 0 => value < 0
? -(((-value) + ((1L << bitCount) >> 1)) >> bitCount) ? -(((-value) + ((1L << bitCount) >> 1)) >> bitCount)
: (value + ((1L << bitCount) >> 1)) >> bitCount; : (value + ((1L << bitCount) >> 1)) >> bitCount;
/// <summary>
/// Provides inline storage for the six parameters in an AV1 affine matrix.
/// </summary>
/// <typeparam name="T">The stored parameter type.</typeparam>
[InlineArray(6)]
private struct InlineArray6<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

14
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -281,17 +280,4 @@ internal sealed class Av1MotionVariationCandidates
/// <returns><see langword="true"/> for inter prediction or intra-block copy; otherwise, <see langword="false"/>.</returns> /// <returns><see langword="true"/> for inter prediction or intra-block copy; otherwise, <see langword="false"/>.</returns>
private static bool IsOverlappable(Av1BlockModeInfo candidate) private static bool IsOverlappable(Av1BlockModeInfo candidate)
=> candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; => candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra;
/// <summary>
/// Provides fixed storage for AV1's eight local warped-motion projection samples.
/// </summary>
/// <typeparam name="T">The source or reference point type stored in the inline buffer.</typeparam>
[InlineArray(ProjectionSampleCapacity)]
private struct InlineArray8<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

27
src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -1365,30 +1364,4 @@ internal sealed class Av1ReferenceMotionVectors
Av1PredictionMode.NewNearestMotionVector or Av1PredictionMode.NewNearestMotionVector or
Av1PredictionMode.NearNewMotionVector or Av1PredictionMode.NearNewMotionVector or
Av1PredictionMode.NewNearMotionVector; Av1PredictionMode.NewNearMotionVector;
/// <summary>
/// Provides fixed storage for AV1's eight reference-motion-vector candidates.
/// </summary>
/// <typeparam name="T">The motion-vector or weight type stored in the inline buffer.</typeparam>
[InlineArray(CandidateCapacity)]
private struct InlineArray8<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides fixed storage for the nearest and near motion-vector references.
/// </summary>
/// <typeparam name="T">The motion-vector type stored in the inline buffer.</typeparam>
[InlineArray(2)]
private struct InlineArray2<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

41
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
@ -250,43 +248,4 @@ internal sealed class ObuFilmGrainParameters
this.OverlapFlag = source.OverlapFlag; this.OverlapFlag = source.OverlapFlag;
this.ClipToRestrictedRange = source.ClipToRestrictedRange; this.ClipToRestrictedRange = source.ClipToRestrictedRange;
} }
/// <summary>
/// Provides inline storage for the maximum luma autoregressive coefficient count.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(24)]
private struct InlineArray24<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides inline storage for the ten scaling points permitted on either chroma plane.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(10)]
private struct InlineArray10<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides inline storage for the maximum autoregressive coefficient count of either chroma plane.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(25)]
private struct InlineArray25<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

14
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -341,17 +340,4 @@ internal sealed class ObuFrameHeader
} }
} }
} }
/// <summary>
/// Provides inline storage for the seven canonical AV1 inter reference types.
/// </summary>
/// <typeparam name="T">The stored parameter type.</typeparam>
[InlineArray(Av1Constants.ReferencesPerFrame)]
private struct InlineArray7<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

18
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
@ -10,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// </summary> /// </summary>
internal sealed class ObuTileGroupHeader internal sealed class ObuTileGroupHeader
{ {
private InlineTileColumnBoundaryArray tileColumnStartModeInfo; private InlineArray65<int> tileColumnStartModeInfo;
private InlineTileRowBoundaryArray tileRowStartModeInfo; private InlineArray65<int> tileRowStartModeInfo;
/// <summary> /// <summary>
/// Gets or sets the maximum tile width, in superblocks. /// Gets or sets the maximum tile width, in superblocks.
@ -92,16 +90,4 @@ internal sealed class ObuTileGroupHeader
/// Gets or sets the number of bytes used to signal each tile size. /// Gets or sets the number of bytes used to signal each tile size.
/// </summary> /// </summary>
public int TileSizeBytes { get; set; } public int TileSizeBytes { get; set; }
[InlineArray(Av1Constants.MaxTileColumnCount + 1)]
private struct InlineTileColumnBoundaryArray
{
private int element;
}
[InlineArray(Av1Constants.MaxTileRowCount + 1)]
private struct InlineTileRowBoundaryArray
{
private int element;
}
} }

21
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -11,8 +12,12 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Writes the AV1 open bitstream units required for a single still-image frame. /// Writes the AV1 open bitstream units required for a single still-image frame.
/// </summary> /// </summary>
internal class ObuWriter internal sealed class ObuWriter
{ {
// Sequence and uncompressed-frame syntax have fixed field and array limits. A 512-byte owner covers their
// maximum supported representation without retaining any entropy-coded tile bytes in the header scratch.
private const int MaximumHeaderLength = 512;
/// <summary> /// <summary>
/// Writes a temporal delimiter and the supplied sequence and frame OBUs. /// Writes a temporal delimiter and the supplied sequence and frame OBUs.
/// </summary> /// </summary>
@ -27,11 +32,11 @@ internal class ObuWriter
Justification = "Preserves the existing writer instance contract.")] Justification = "Preserves the existing writer instance contract.")]
public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter) public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter)
{ {
// The reusable scratch only contains headers. Entropy-coded tiles remain in their owning // The reusable scratch only contains headers. Entropy-coded tiles remain in their owning buffers and are
// buffers and are streamed directly so the complete compressed frame is never duplicated. // streamed directly so the complete compressed frame is never duplicated.
int initialBufferSize = 2000; using IMemoryOwner<byte> headerOwner = configuration.MemoryAllocator.Allocate<byte>(MaximumHeaderLength);
using AutoExpandingMemory<byte> buffer = new(configuration, initialBufferSize); Span<byte> headerBuffer = headerOwner.Memory.Span[..MaximumHeaderLength];
Av1BitStreamWriter writer = new(buffer); Av1BitStreamWriter writer = new(headerBuffer);
WriteObuHeaderAndSize(stream, ObuType.TemporalDelimiter, []); WriteObuHeaderAndSize(stream, ObuType.TemporalDelimiter, []);
if (sequenceHeader != null) if (sequenceHeader != null)
@ -39,7 +44,7 @@ internal class ObuWriter
WriteSequenceHeader(ref writer, sequenceHeader); WriteSequenceHeader(ref writer, sequenceHeader);
int bytesWritten = (writer.BitPosition + 7) >> 3; int bytesWritten = (writer.BitPosition + 7) >> 3;
writer.Flush(); writer.Flush();
WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, buffer.GetSpan(bytesWritten)); WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, headerBuffer[..bytesWritten]);
} }
if (frameHeader != null && sequenceHeader != null) if (frameHeader != null && sequenceHeader != null)
@ -67,7 +72,7 @@ internal class ObuWriter
} }
WriteObuHeaderAndSize(stream, ObuType.Frame, framePayloadSize); WriteObuHeaderAndSize(stream, ObuType.Frame, framePayloadSize);
stream.Write(buffer.GetSpan(frameHeaderBytes)); stream.Write(headerBuffer[..frameHeaderBytes]);
if (tileInfo != null) if (tileInfo != null)
{ {

67
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -17,6 +18,29 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// </summary> /// </summary>
internal static class Av1FrameEncoder internal static class Av1FrameEncoder
{ {
/// <summary>
/// The base-two exponent used to align each frame dimension for output sizing. Rounding to 32 samples accounts
/// for partial edge storage before the raw-plane size and all-intra expansion factor are calculated.
/// </summary>
private const int OutputAlignmentLog2 = 5;
/// <summary>
/// The lower bound, in bytes, for the bounded compressed-frame buffer. The raw-size ratio is too small for tiny
/// images to provide useful coder headroom, so the reference allocation retains an 8 KiB floor.
/// </summary>
private const int MinimumCompressedFrameBufferLength = 8 * 1024;
/// <summary>
/// The numerator of the all-intra output-capacity ratio. Together with the denominator, this reserves 2.5 times
/// the aligned uncompressed plane size because incompressible input can produce more output than its raw size.
/// </summary>
private const int AllIntraBufferScaleNumerator = 5;
/// <summary>
/// The denominator of the all-intra output-capacity ratio, completing the reference encoder's 5:2 sizing rule.
/// </summary>
private const int AllIntraBufferScaleDenominator = 2;
/// <summary> /// <summary>
/// Encodes one reduced-still-picture AV1 frame into a low-overhead OBU stream. /// Encodes one reduced-still-picture AV1 frame into a low-overhead OBU stream.
/// </summary> /// </summary>
@ -141,8 +165,8 @@ internal static class Av1FrameEncoder
// Libaom reserves 2.5 times the 32-sample-aligned native input for an all-intra output packet. // Libaom reserves 2.5 times the 32-sample-aligned native input for an all-intra output packet.
// Counting the active planes directly retains that headroom without charging monochrome for unused chroma. // Counting the active planes directly retains that headroom without charging monochrome for unused chroma.
int alignedWidth = Av1Math.AlignPowerOf2(width, 5); int alignedWidth = Av1Math.AlignPowerOf2(width, OutputAlignmentLog2);
int alignedHeight = Av1Math.AlignPowerOf2(height, 5); int alignedHeight = Av1Math.AlignPowerOf2(height, OutputAlignmentLog2);
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;
long sampleCount = (long)alignedWidth * alignedHeight; long sampleCount = (long)alignedWidth * alignedHeight;
@ -152,14 +176,17 @@ internal static class Av1FrameEncoder
} }
int sampleSize = colorConfig.BitDepth == Av1BitDepth.EightBit ? 1 : 2; int sampleSize = colorConfig.BitDepth == Av1BitDepth.EightBit ? 1 : 2;
int initialTileSize = checked((int)Math.Max(8192L, (sampleCount * sampleSize * 5) / 2)); long scaledInputLength = (sampleCount * sampleSize * AllIntraBufferScaleNumerator)
/ AllIntraBufferScaleDenominator;
int tileBufferLength = checked((int)Math.Max(MinimumCompressedFrameBufferLength, scaledInputLength));
if (colorConfig.BitDepth == Av1BitDepth.EightBit) if (colorConfig.BitDepth == Av1BitDepth.EightBit)
{ {
EncodeByte(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, effort, encodeAlpha); EncodeByte(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, tileBufferLength, effort, encodeAlpha);
} }
else else
{ {
EncodeHighBitDepth(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, effort, encodeAlpha); EncodeHighBitDepth(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, tileBufferLength, effort, encodeAlpha);
} }
return sequenceHeader; return sequenceHeader;
@ -204,7 +231,7 @@ internal static class Av1FrameEncoder
ObuSequenceHeader sequenceHeader, ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1ColorFormat colorFormat, Av1ColorFormat colorFormat,
int initialTileSize, int tileBufferLength,
int effort, int effort,
bool encodeAlpha) bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -227,7 +254,7 @@ internal static class Av1FrameEncoder
chromaPositionX: 1, chromaPositionX: 1,
chromaPositionY: 1); chromaPositionY: 1);
Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, initialTileSize, effort, encodeAlpha); Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, tileBufferLength, effort, encodeAlpha);
} }
private static void EncodeHighBitDepth<TPixel>( private static void EncodeHighBitDepth<TPixel>(
@ -237,7 +264,7 @@ internal static class Av1FrameEncoder
ObuSequenceHeader sequenceHeader, ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1ColorFormat colorFormat, Av1ColorFormat colorFormat,
int initialTileSize, int tileBufferLength,
int effort, int effort,
bool encodeAlpha) bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -261,7 +288,7 @@ internal static class Av1FrameEncoder
chromaPositionX: 1, chromaPositionX: 1,
chromaPositionY: 1); chromaPositionY: 1);
Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, initialTileSize, effort, encodeAlpha); Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, tileBufferLength, effort, encodeAlpha);
} }
private static void Encode<TPixel>( private static void Encode<TPixel>(
@ -272,7 +299,7 @@ internal static class Av1FrameEncoder
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1EncoderFrameBuffer<byte> source, Av1EncoderFrameBuffer<byte> source,
Av1EncoderFrameBuffer<byte> reconstruction, Av1EncoderFrameBuffer<byte> reconstruction,
int initialTileSize, int tileBufferLength,
int effort, int effort,
bool encodeAlpha) bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -316,15 +343,20 @@ internal static class Av1FrameEncoder
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(configuration); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(configuration);
using Av1EncoderBlockWorkspace blockWorkspace = new(configuration); using Av1EncoderBlockWorkspace blockWorkspace = new(configuration);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = new(
configuration, configuration,
tileBufferLength,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialTileSize,
effort); effort);
ObuWriter writer = new(); ObuWriter writer = new();
@ -339,7 +371,7 @@ internal static class Av1FrameEncoder
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1EncoderFrameBuffer<ushort> source, Av1EncoderFrameBuffer<ushort> source,
Av1EncoderFrameBuffer<ushort> reconstruction, Av1EncoderFrameBuffer<ushort> reconstruction,
int initialTileSize, int tileBufferLength,
int effort, int effort,
bool encodeAlpha) bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -383,15 +415,20 @@ internal static class Av1FrameEncoder
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(configuration); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(configuration);
using Av1EncoderBlockWorkspace blockWorkspace = new(configuration); using Av1EncoderBlockWorkspace blockWorkspace = new(configuration);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = new(
configuration, configuration,
tileBufferLength,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialTileSize,
effort); effort);
ObuWriter writer = new(); ObuWriter writer = new();

51
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -10,38 +9,36 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary> /// <summary>
/// Encodes and owns one range-coded all-intra tile payload. /// Encodes one range-coded all-intra tile payload.
/// </summary> /// </summary>
internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable internal sealed partial class Av1IntraTileWriter : IAv1TileWriter
{ {
private IMemoryOwner<byte>? tileData; private readonly ReadOnlyMemory<byte> tileData;
private readonly int tileDataLength; private readonly int tileDataLength;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1IntraTileWriter"/> class for eight-bit samples. /// Initializes a new instance of the <see cref="Av1IntraTileWriter"/> class for eight-bit samples.
/// </summary> /// </summary>
/// <param name="configuration">The configuration providing tile output memory.</param> /// <param name="writer">The operation-owned symbol encoder that retains the tile output memory.</param>
/// <param name="source">The coded source frame.</param> /// <param name="source">The coded source frame.</param>
/// <param name="reconstruction">The reconstructed frame updated during encoding.</param> /// <param name="reconstruction">The reconstructed frame updated during encoding.</param>
/// <param name="picture">The frame coding and mode-information state.</param> /// <param name="picture">The frame coding and mode-information state.</param>
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param> /// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
/// <param name="superblockWorkspace">The reusable partition and final-block decision workspace.</param> /// <param name="superblockWorkspace">The reusable partition and final-block decision workspace.</param>
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param> /// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
/// <param name="initialSize">The estimated encoded tile size in bytes.</param>
/// <param name="effort">The mode-search effort in the inclusive range zero through ten.</param> /// <param name="effort">The mode-search effort in the inclusive range zero through ten.</param>
public Av1IntraTileWriter( public Av1IntraTileWriter(
Configuration configuration, Av1SymbolEncoder writer,
Av1EncoderFrame<byte> source, Av1EncoderFrame<byte> source,
Av1EncoderFrame<byte> reconstruction, Av1EncoderFrame<byte> reconstruction,
Av1PictureControlSet picture, Av1PictureControlSet picture,
Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace, Av1EncoderBlockWorkspace blockWorkspace,
int initialSize,
int effort) int effort)
{ {
this.tileData = Encode<byte, Av1IntraSuperblockEncoder.ByteOperator>( this.tileData = Encode<byte, Av1IntraSuperblockEncoder.ByteOperator>(
configuration, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
@ -49,35 +46,32 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
effort, effort,
initialSize,
out this.tileDataLength); out this.tileDataLength);
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1IntraTileWriter"/> class for high-bit-depth samples. /// Initializes a new instance of the <see cref="Av1IntraTileWriter"/> class for high-bit-depth samples.
/// </summary> /// </summary>
/// <param name="configuration">The configuration providing tile output memory.</param> /// <param name="writer">The operation-owned symbol encoder that retains the tile output memory.</param>
/// <param name="source">The coded source frame.</param> /// <param name="source">The coded source frame.</param>
/// <param name="reconstruction">The reconstructed frame updated during encoding.</param> /// <param name="reconstruction">The reconstructed frame updated during encoding.</param>
/// <param name="picture">The frame coding and mode-information state.</param> /// <param name="picture">The frame coding and mode-information state.</param>
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param> /// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
/// <param name="superblockWorkspace">The reusable partition and final-block decision workspace.</param> /// <param name="superblockWorkspace">The reusable partition and final-block decision workspace.</param>
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param> /// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
/// <param name="initialSize">The estimated encoded tile size in bytes.</param>
/// <param name="effort">The mode-search effort in the inclusive range zero through ten.</param> /// <param name="effort">The mode-search effort in the inclusive range zero through ten.</param>
public Av1IntraTileWriter( public Av1IntraTileWriter(
Configuration configuration, Av1SymbolEncoder writer,
Av1EncoderFrame<ushort> source, Av1EncoderFrame<ushort> source,
Av1EncoderFrame<ushort> reconstruction, Av1EncoderFrame<ushort> reconstruction,
Av1PictureControlSet picture, Av1PictureControlSet picture,
Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace, Av1EncoderBlockWorkspace blockWorkspace,
int initialSize,
int effort) int effort)
{ {
this.tileData = Encode<ushort, Av1IntraSuperblockEncoder.UInt16Operator>( this.tileData = Encode<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(
configuration, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
@ -85,28 +79,14 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
effort, effort,
initialSize,
out this.tileDataLength); out this.tileDataLength);
} }
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlySpan<byte> GetTileData(int tileNum) public ReadOnlySpan<byte> GetTileData(int tileNum) => this.tileData.Span[..this.tileDataLength];
{
ObjectDisposedException.ThrowIf(this.tileData is null, this);
return this.tileData.Memory.Span[..this.tileDataLength];
}
/// <summary>
/// Returns the detached range-coded tile allocation to the configured allocator.
/// </summary>
public void Dispose()
{
this.tileData?.Dispose();
this.tileData = null;
}
private static IMemoryOwner<byte> Encode<TSample, TOperator>( private static ReadOnlyMemory<byte> Encode<TSample, TOperator>(
Configuration configuration, Av1SymbolEncoder writer,
Av1EncoderFrame<TSample> source, Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> reconstruction, Av1EncoderFrame<TSample> reconstruction,
Av1PictureControlSet picture, Av1PictureControlSet picture,
@ -114,7 +94,6 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace, Av1EncoderBlockWorkspace blockWorkspace,
int effort, int effort,
int initialSize,
out int tileDataLength) out int tileDataLength)
where TSample : unmanaged where TSample : unmanaged
where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample> where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample>
@ -136,12 +115,6 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
MacroBlockModeInfo = picture.GetMacroBlockModeInfo(firstModeInfoPosition) MacroBlockModeInfo = picture.GetMacroBlockModeInfo(firstModeInfoPosition)
}; };
using Av1SymbolEncoder writer = new(
configuration,
initialSize,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize; int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize;
int superblockShift = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; int superblockShift = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2;
if (frameHeader.AllowIntraBlockCopy) if (frameHeader.AllowIntraBlockCopy)

55
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs

@ -10,34 +10,20 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
/// </summary> /// </summary>
internal static class Av1QuantizationLookup internal static class Av1QuantizationLookup
{ {
// Coefficient scaling and quantization with AV1 TX are tailored to private const int LinearQuantizerScale = 4;
// the AV1 TX transforms. Regardless of the bit-depth of the input, private const int LastLinearQuantizer = 61;
// the transform stages scale the coefficient values up by a factor of private const int PenultimateQuantizer = 62;
// 8 (3 bits) over the scale of the pixel values. Thus, for 8-bit private const int PenultimateQuantizerIndex = 249;
// input, the coefficients have effectively 11 bits of scale depth
// (8+3), 10-bit input pixels result in 13-bit coefficient depth
// (10+3) and 12-bit pixels yield 15-bit (12+3) coefficient depth.
// All quantizers are built using this invariant of x8, 3-bit scaling,
// thus the Q3 suffix.
// A partial exception to this rule is large transforms; to avoid // AV1 transforms normally retain three fractional coefficient bits. The quantizer tables use the same Q3
// overflow, TX blocks with > 256 pels (>16x16) are scaled only // scale, leaving coded coefficients in Q0 and reconstructed coefficients in Q3.
// 4-times unity (2 bits) over the pixel depth, and TX blocks with
// over 1024 pixels (>32x32) are scaled up only 2x unity (1 bit).
// This descaling is found via av1_tx_get_scale(). Thus, 16x32, 32x16
// and 32x32 transforms actually return Q2 coefficients, and 32x64,
// 64x32 and 64x64 transforms return Q1 coefficients. However, the
// quantizers are de-scaled down on-the-fly by the same amount
// (av1_tx_get_scale()) during quantization, and as such the
// dequantized/decoded coefficients, even for large TX blocks, are always
// effectively Q3. Meanwhile, quantized/coded coefficients are Q0
// because Qn quantizers are applied to Qn tx coefficients.
// Note that encoder decision making (which uses the quantizer to // Transforms larger than 16x16 reduce coefficient scaling by one bit, and transforms larger than 32x32 reduce
// generate several bespoke lamdas for RDO and other heuristics) // it by two bits to preserve numeric range. Quantization applies the same reduction to its step, so every
// expects quantizers to be larger for higher-bitdepth input. In // reconstructed transform still reaches the inverse transform in Q3.
// addition, the minimum allowable quantizer is 4; smaller values will
// underflow to 0 in the actual quantization routines. // Encoder rate decisions intentionally retain bit-depth-specific quantizer values. The minimum table value is
// four because a smaller step would round to zero during fixed-point quantization.
/// <summary> /// <summary>
/// The Q3 AC dequantization values for 8-bit samples, indexed by quantizer index. /// The Q3 AC dequantization values for 8-bit samples, indexed by quantizer index.
@ -162,6 +148,23 @@ internal static class Av1QuantizationLookup
12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387, 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387,
]; ];
/// <summary>
/// Converts a quantizer on libaom's external zero-through-63 scale to an AV1 quantizer index.
/// </summary>
/// <param name="quantizer">The external quantizer.</param>
/// <returns>The corresponding AV1 quantizer index.</returns>
public static int GetQIndex(int quantizer)
{
// Four qindex steps separate the regular entries. The final two entries use 249 and 255 so the external
// scale reaches AV1's complete qindex range without changing the spacing of its first 62 entries.
if (quantizer <= LastLinearQuantizer)
{
return quantizer * LinearQuantizerScale;
}
return quantizer == PenultimateQuantizer ? PenultimateQuantizerIndex : Av1Constants.MaxQ;
}
/// <summary> /// <summary>
/// Gets the DC dequantization value after applying a plane delta to the frame quantizer index. /// Gets the DC dequantization value after applying a plane delta to the frame quantizer index.
/// </summary> /// </summary>

14
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -463,17 +462,4 @@ internal struct Av1BlockModeInfo
this.chromaPaletteColorIndexBounds = bounds; this.chromaPaletteColorIndexBounds = bounds;
} }
} }
/// <summary>
/// Provides fixed storage for the two values associated with AV1's primary and secondary inter references.
/// </summary>
/// <typeparam name="T">The stored reference label, motion vector, or interpolation-filter type.</typeparam>
[InlineArray(2)]
private struct InlineArray2<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

21
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -52,24 +51,4 @@ internal struct Av1EncoderPaletteInfo
Span<ushort> destination = this.paletteColors; Span<ushort> destination = this.paletteColors;
colors.CopyTo(destination[offset..]); colors.CopyTo(destination[offset..]);
} }
/// <summary>
/// Provides fixed storage for the luma and shared chroma palette sizes.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(2)]
private struct InlineArray2<T>
{
private T element;
}
/// <summary>
/// Provides fixed storage for all three eight-color palette planes.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(3 * Av1Constants.PaletteMaxSize)]
private struct InlineArray24<T>
{
private T element;
}
} }

10
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -33,13 +32,4 @@ internal struct Av1EncoderPredictionUnit
/// Gets or sets the packed chroma-from-luma alpha signs for the U and V planes. /// Gets or sets the packed chroma-from-luma alpha signs for the U and V planes.
/// </summary> /// </summary>
public sbyte ChromaFromLumaSigns { get; set; } public sbyte ChromaFromLumaSigns { get; set; }
/// <summary>
/// Stores the two signed angle deltas embedded by libaom in block mode information.
/// </summary>
[InlineArray(Av1Constants.PlaneTypeCount)]
private struct InlineArray2<T>
{
private T element;
}
} }

26
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary> /// <summary>
@ -13,17 +11,17 @@ internal struct Av1LoopRestorationUnit
/// <summary> /// <summary>
/// The three transmitted symmetric vertical Wiener coefficients. /// The three transmitted symmetric vertical Wiener coefficients.
/// </summary> /// </summary>
public WienerCoefficientBuffer WienerVertical; public InlineArray3<int> WienerVertical;
/// <summary> /// <summary>
/// The three transmitted symmetric horizontal Wiener coefficients. /// The three transmitted symmetric horizontal Wiener coefficients.
/// </summary> /// </summary>
public WienerCoefficientBuffer WienerHorizontal; public InlineArray3<int> WienerHorizontal;
/// <summary> /// <summary>
/// The two self-guided projection coefficients. /// The two self-guided projection coefficients.
/// </summary> /// </summary>
public SgrProjectionCoefficientBuffer SgrProjectionCoefficients; public InlineArray2<int> SgrProjectionCoefficients;
/// <summary> /// <summary>
/// Gets or sets the restoration filter selected for the unit. /// Gets or sets the restoration filter selected for the unit.
@ -34,22 +32,4 @@ internal struct Av1LoopRestorationUnit
/// Gets or sets the self-guided filter parameter-set index. /// Gets or sets the self-guided filter parameter-set index.
/// </summary> /// </summary>
public int SgrParameterSet { get; set; } public int SgrParameterSet { get; set; }
/// <summary>
/// Stores the transmitted coefficients inline with the restoration unit.
/// </summary>
[InlineArray(Av1Constants.WienerCoefficientCount)]
public struct WienerCoefficientBuffer
{
private int element0;
}
/// <summary>
/// Stores the projection coefficients inline with the restoration unit.
/// </summary>
[InlineArray(2)]
public struct SgrProjectionCoefficientBuffer
{
private int element0;
}
} }

13
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -3449,17 +3449,4 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary> /// </summary>
public Buffer2D<byte> Chroma { get; } = chroma; public Buffer2D<byte> Chroma { get; } = chroma;
} }
/// <summary>
/// Provides inline storage for the two self-guided restoration coefficients of each of the three AV1 planes.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(6)]
private struct InlineArray6<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
} }

24
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <summary> /// <summary>
@ -28,17 +26,17 @@ internal ref struct Av1Transform2dFlipConfiguration
/// <summary> /// <summary>
/// The fixed-point shifts applied between successive stages of the configured transform pipeline. /// The fixed-point shifts applied between successive stages of the configured transform pipeline.
/// </summary> /// </summary>
private ShiftBuffer shift; private InlineArray3<int> shift;
/// <summary> /// <summary>
/// The signed-bit ranges produced by the column transform stages. /// The signed-bit ranges produced by the column transform stages.
/// </summary> /// </summary>
private Av1TransformStageRange stageRangeColumn; private InlineArray12<byte> stageRangeColumn;
/// <summary> /// <summary>
/// The signed-bit ranges produced by the row transform stages. /// The signed-bit ranges produced by the row transform stages.
/// </summary> /// </summary>
private Av1TransformStageRange stageRangeRow; private InlineArray12<byte> stageRangeRow;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1Transform2dFlipConfiguration"/> struct. /// Initializes a new instance of the <see cref="Av1Transform2dFlipConfiguration"/> struct.
@ -337,12 +335,12 @@ internal ref struct Av1Transform2dFlipConfiguration
/// <summary> /// <summary>
/// Gets the allowed signed-bit range after each column-transform stage. /// Gets the allowed signed-bit range after each column-transform stage.
/// </summary> /// </summary>
public readonly Av1TransformStageRange StageRangeColumn => this.stageRangeColumn; public readonly InlineArray12<byte> StageRangeColumn => this.stageRangeColumn;
/// <summary> /// <summary>
/// Gets the allowed signed-bit range after each row-transform stage. /// Gets the allowed signed-bit range after each row-transform stage.
/// </summary> /// </summary>
public readonly Av1TransformStageRange StageRangeRow => this.stageRangeRow; public readonly InlineArray12<byte> StageRangeRow => this.stageRangeRow;
/// <summary> /// <summary>
/// Creates the configuration used to transform spatial residuals into coefficients. /// Creates the configuration used to transform spatial residuals into coefficients.
@ -499,16 +497,4 @@ internal ref struct Av1Transform2dFlipConfiguration
this.stageRangeRow[i] = rowRange; this.stageRangeRow[i] = rowRange;
} }
} }
/// <summary>
/// Stores the three fixed-point shifts without allocating an array for each transform block.
/// </summary>
[InlineArray(3)]
private struct ShiftBuffer
{
/// <summary>
/// The first fixed-point shift.
/// </summary>
private int element0;
}
} }

18
src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs

@ -1,18 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <summary>
/// Stores the signed-bit ranges assigned to every stage of one AV1 transform axis.
/// </summary>
[InlineArray(Av1Transform2dFlipConfiguration.MaxStageNumber)]
internal struct Av1TransformStageRange
{
/// <summary>
/// The signed-bit range assigned to the first transform stage.
/// </summary>
private byte element0;
}

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The sixteen-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The sixteen-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -205,7 +205,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -393,7 +393,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the sine constants.</param> /// <param name="cosBit">The fixed-point precision of the sine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> sinpi = Av1SinusConstants.SinusPi(cosBit); ReadOnlySpan<int> sinpi = Av1SinusConstants.SinusPi(cosBit);
@ -79,7 +79,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount; bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount;
@ -115,7 +115,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount; bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The eight-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The eight-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -112,7 +112,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -207,7 +207,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The sixteen-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The sixteen-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -174,7 +174,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -331,7 +331,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The 32-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The 32-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -358,7 +358,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -699,7 +699,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The four-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The four-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output[0] = input[0]; output[0] = input[0];
@ -53,7 +53,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output.V0 = input.V0; output.V0 = input.V0;
@ -89,7 +89,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output.V0 = input.V0; output.V0 = input.V0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The 64-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The 64-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -773,7 +773,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -1529,7 +1529,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The eight-element stage buffer owned by the containing two-dimensional transform.</param> /// <param name="step">The eight-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -93,7 +93,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;
@ -169,7 +169,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit); ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0; int stage = 0;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param> /// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param> /// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param> /// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
_ = step; _ = step;
_ = cosBit; _ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
// The doubled scale exceeds Int32 only for the 20-bit twelve-bit row range. Widen that exact product and // The doubled scale exceeds Int32 only for the 20-bit twelve-bit row range. Widen that exact product and
// rounding sequence, matching the reference decoder without changing the established lower-range SIMD path. // rounding sequence, matching the reference decoder without changing the established lower-range SIMD path.
@ -66,7 +66,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount)
{ {

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param> /// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param> /// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param> /// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
_ = step; _ = step;
_ = cosBit; _ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0); Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0);
_ = step; _ = step;
@ -57,7 +57,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0); Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0);
_ = step; _ = step;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param> /// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param> /// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param> /// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
_ = step; _ = step;
_ = cosBit; _ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
// Only a twelve-bit row transform has the 20-bit input range that can overflow this fixed-point product. // Only a twelve-bit row transform has the 20-bit input range that can overflow this fixed-point product.
// Match the reference decoder's high-bit-depth kernel there while retaining the compact Int32 path for narrower ranges. // Match the reference decoder's high-bit-depth kernel there while retaining the compact Int32 path for narrower ranges.
@ -66,7 +66,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount)
{ {

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs

@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param> /// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param> /// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param> /// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange) public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange)
{ {
_ = step; _ = step;
_ = cosBit; _ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0); Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0);
_ = step; _ = step;
@ -57,7 +57,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
{ {
Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0); Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0);
_ = step; _ = step;

6
src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs

@ -27,7 +27,7 @@ internal static partial class Av1Inverse2dTransformer
/// <param name="step">The fixed stage storage for the transform axis.</param> /// <param name="step">The fixed stage storage for the transform axis.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param> /// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param> /// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static abstract void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange); public static abstract void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, InlineArray12<byte> stageRange);
/// <summary> /// <summary>
/// Transforms four independent axes in parallel. /// Transforms four independent axes in parallel.
@ -42,7 +42,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output, ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step, ref Av1TransformVector<Vector128<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange); InlineArray12<byte> stageRange);
/// <summary> /// <summary>
/// Transforms eight independent axes in parallel. /// Transforms eight independent axes in parallel.
@ -57,6 +57,6 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output, ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step, ref Av1TransformVector<Vector256<int>> step,
int cosBit, int cosBit,
Av1TransformStageRange stageRange); InlineArray12<byte> stageRange);
} }
} }

1
src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs

@ -11,6 +11,7 @@ public sealed class HeifConfigurationModule : IImageFormatConfigurationModule
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(Configuration configuration) public void Configure(Configuration configuration)
{ {
configuration.ImageFormatsManager.SetEncoder(HeifFormat.Instance, new HeifEncoder());
configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance); configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector()); configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector());
} }

4
src/ImageSharp/Formats/Heif/HeifEncoder.cs

@ -25,9 +25,9 @@ public sealed class HeifEncoder : AnimatedImageEncoder
/// <summary> /// <summary>
/// Gets the compression method used for the primary image item. /// Gets the compression method used for the primary image item.
/// The default is <see cref="HeifCompressionMethod.LegacyJpeg"/>. /// The default is <see cref="HeifCompressionMethod.Av1"/>.
/// </summary> /// </summary>
public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.LegacyJpeg; public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.Av1;
/// <summary> /// <summary>
/// Gets the lossy compression quality, or <see langword="null"/> to use the compression method's default quality. /// Gets the lossy compression quality, or <see langword="null"/> to use the compression method's default quality.

445
src/ImageSharp/Formats/Heif/HeifEncoderCore.Sequence.cs

@ -1,13 +1,13 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -22,6 +22,11 @@ internal sealed partial class HeifEncoderCore
private const uint UnityFixed2Point30 = 1U << 30; private const uint UnityFixed2Point30 = 1U << 30;
private const ushort UnityFixed8Point8 = 1 << 8; private const ushort UnityFixed8Point8 = 1 << 8;
private const ushort PackedUndeterminedLanguage = 0x55C4; private const ushort PackedUndeterminedLanguage = 0x55C4;
private const uint AllReferencePicturesIntraMask = 1U << 31;
private const uint IntraPicturePredictionUsedMask = 1U << 30;
private const uint DefaultVisualSampleResolution = 72U << 16;
private const int VisualSampleCompressorNameLength = 32;
private const ushort VisualSampleDepth = 24;
private Av1EncodingSettings ResolveAv1Encoding<TPixel>(Image<TPixel> image) private Av1EncodingSettings ResolveAv1Encoding<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -56,7 +61,11 @@ internal sealed partial class HeifEncoderCore
CicpProfile colorProfile; CicpProfile colorProfile;
if (sourceColorProfile is null) if (sourceColorProfile is null)
{ {
colorProfile = new CicpProfile(2, 2, 6, false); colorProfile = new CicpProfile(
(byte)CicpColorPrimaries.Unspecified,
(byte)CicpTransferCharacteristics.Unspecified,
(byte)CicpMatrixCoefficients.ItuRBt601_7_525,
false);
} }
else else
{ {
@ -133,6 +142,7 @@ internal sealed partial class HeifEncoderCore
Image<TPixel> image, Image<TPixel> image,
ChunkedMemoryStream stream, ChunkedMemoryStream stream,
Av1EncodingSettings settings, Av1EncodingSettings settings,
Memory<HeifSequenceSampleInfo> samples,
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -141,14 +151,25 @@ internal sealed partial class HeifEncoderCore
throw new NotSupportedException("AV1 image-sequence dimensions cannot exceed 65535 pixels."); throw new NotSupportedException("AV1 image-sequence dimensions cannot exceed 65535 pixels.");
} }
byte[]? exifData = null;
uint tiffHeaderOffset = 0;
byte[]? xmpData = null;
if (!this.encoder.SkipMetadata)
{
exifData = GetExifData(image.Metadata, out tiffHeaderOffset);
byte[]? sourceXmpData = image.Metadata.XmpProfile?.Data;
if (sourceXmpData is not null && sourceXmpData.Length > 0)
{
xmpData = sourceXmpData;
}
}
int frameCount = image.Frames.Count; int frameCount = image.Frames.Count;
uint timescale = GetSequenceTimescale(image); 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 // 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. // share one allocator-owned table, with each track occupying one contiguous slice until moov is written.
HeifSequenceSampleInfo[] samples = new HeifSequenceSampleInfo[sampleCount]; Span<HeifSequenceSampleInfo> colorSamples = samples.Span[..frameCount];
Span<HeifSequenceSampleInfo> colorSamples = samples.AsSpan(0, frameCount);
ImageFrame<TPixel> rootFrame = image.Frames.RootFrame; ImageFrame<TPixel> rootFrame = image.Frames.RootFrame;
uint duration = GetSequenceSampleDuration(rootFrame.Metadata.GetHeifMetadata().FrameDelay, timescale); uint duration = GetSequenceSampleDuration(rootFrame.Metadata.GetHeifMetadata().FrameDelay, timescale);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -188,15 +209,14 @@ internal sealed partial class HeifEncoderCore
HeifSequenceTrackEncoding colorTrack = new( HeifSequenceTrackEncoding colorTrack = new(
new Av1CodecConfiguration(colorHeader), new Av1CodecConfiguration(colorHeader),
samples, samples[..frameCount],
0,
frameCount,
false); false);
HeifSequenceTrackEncoding? alphaTrack = null; HeifSequenceTrackEncoding? alphaTrack = null;
if (settings.HasAlpha) if (settings.HasAlpha)
{ {
Span<HeifSequenceSampleInfo> alphaSamples = samples.AsSpan(frameCount, frameCount); Memory<HeifSequenceSampleInfo> alphaSampleMemory = samples.Slice(frameCount, frameCount);
Span<HeifSequenceSampleInfo> alphaSamples = alphaSampleMemory.Span;
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
long alphaOffset = stream.Length; long alphaOffset = stream.Length;
ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha( ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha(
@ -232,12 +252,17 @@ internal sealed partial class HeifEncoderCore
alphaTrack = new HeifSequenceTrackEncoding( alphaTrack = new HeifSequenceTrackEncoding(
new Av1CodecConfiguration(alphaHeader), new Av1CodecConfiguration(alphaHeader),
samples, alphaSampleMemory,
frameCount,
frameCount,
true); true);
} }
ReadOnlyMemory<byte> iccProfileData = ReadOnlyMemory<byte>.Empty;
IccProfile? iccProfile = image.Metadata.IccProfile;
if (!this.encoder.SkipMetadata && iccProfile is not null)
{
iccProfileData = iccProfile.GetDataForWriting();
}
return new HeifSequenceEncoding( return new HeifSequenceEncoding(
image.Width, image.Width,
image.Height, image.Height,
@ -246,7 +271,10 @@ internal sealed partial class HeifEncoderCore
colorTrack, colorTrack,
alphaTrack, alphaTrack,
settings.ColorProfile, settings.ColorProfile,
this.encoder.SkipMetadata ? null : image.Metadata.IccProfile); iccProfileData,
exifData,
tiffHeaderOffset,
xmpData);
} }
private int WriteSequenceFileTypeBox(Stream stream) private int WriteSequenceFileTypeBox(Stream stream)
@ -272,9 +300,9 @@ internal sealed partial class HeifEncoderCore
private void WriteSequenceMovieBox(HeifSequenceEncoding sequence, int fileTypeLength, Stream stream) 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 int movieLength = GetSequenceMovieBoxLength(sequence);
// its two offsets once its size is known. The encoded frame payload remains in allocator-backed chunks. using IMemoryOwner<byte> movieOwner = this.configuration.MemoryAllocator.Allocate<byte>(movieLength);
using AutoExpandingMemory<byte> memory = new(this.configuration, 0x1000); Span<byte> memory = movieOwner.Memory.Span[..movieLength];
int offset = 0; int offset = 0;
int movieStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Moov); int movieStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Moov);
ulong mediaDuration = GetSequenceMediaDuration(sequence.ColorTrack.Samples); ulong mediaDuration = GetSequenceMediaDuration(sequence.ColorTrack.Samples);
@ -316,23 +344,126 @@ internal sealed partial class HeifEncoderCore
EndSequenceBox(memory, movieStart, offset); EndSequenceBox(memory, movieStart, offset);
ulong mediaDataOffset = checked((ulong)fileTypeLength + (uint)offset + 8U); ulong mediaDataOffset = checked((ulong)fileTypeLength + (uint)offset + 8U);
Span<byte> movie = memory.GetSpan(offset);
BinaryPrimitives.WriteUInt64BigEndian( BinaryPrimitives.WriteUInt64BigEndian(
movie[colorChunkOffsetPosition..], memory[colorChunkOffsetPosition..],
checked(mediaDataOffset + (ulong)sequence.ColorTrack.Samples[0].Offset)); checked(mediaDataOffset + (ulong)sequence.ColorTrack.Samples[0].Offset));
if (alphaChunkOffsetPosition >= 0) if (alphaChunkOffsetPosition >= 0)
{ {
BinaryPrimitives.WriteUInt64BigEndian( BinaryPrimitives.WriteUInt64BigEndian(
movie[alphaChunkOffsetPosition..], memory[alphaChunkOffsetPosition..],
checked(mediaDataOffset + (ulong)alphaPayloadOffset)); checked(mediaDataOffset + (ulong)alphaPayloadOffset));
} }
stream.Write(movie); stream.Write(memory);
}
private static int GetSequenceMovieBoxLength(HeifSequenceEncoding sequence)
{
const int movieHeaderBoxLength = 120;
const int trackHeaderBoxLength = 104;
const int trackReferenceBoxLength = 20;
const int editListBoxLength = 44;
const int mediaBoxFixedLength = 129;
const int colorInformationBoxLength = 19;
const int codecConfigurationBoxLength = 12;
const int codingConstraintsBoxLength = 16;
const int visualSampleEntryLength = 86;
const int sampleDescriptionBoxLength = 16;
const int sampleTableBoxHeaderLength = 8;
const int timeToSampleBoxFixedLength = 16;
const int sampleToChunkBoxLength = 28;
const int sampleSizeBoxFixedLength = 20;
const int chunkOffsetBoxLength = 24;
const int syncSampleBoxFixedLength = 16;
const int timingRunLength = 8;
const int sampleSizeAndSyncEntryLength = 8;
const int sampleTableFixedLength =
sampleTableBoxHeaderLength
+ sampleDescriptionBoxLength
+ visualSampleEntryLength
+ codecConfigurationBoxLength
+ codingConstraintsBoxLength
+ timeToSampleBoxFixedLength
+ sampleToChunkBoxLength
+ sampleSizeBoxFixedLength
+ chunkOffsetBoxLength
+ syncSampleBoxFixedLength;
const int metadataFixedLength = 83;
const int metadataLocationLength = 16;
const int exifInformationLength = 25;
const int xmpInformationLength = 44;
const int exifOffsetLength = sizeof(uint);
int repeatBoxLength = sequence.RepeatCount == 1 ? 0 : editListBoxLength;
int colorRunCount = GetSequenceTimingRunCount(sequence.ColorTrack.Samples);
long colorSampleTableLength =
(long)sampleTableFixedLength
+ (colorRunCount * timingRunLength)
+ (sequence.ColorTrack.Samples.Length * sampleSizeAndSyncEntryLength)
+ colorInformationBoxLength;
if (!sequence.IccProfileData.IsEmpty)
{
colorSampleTableLength = colorSampleTableLength
+ IccColorInformationPropertyBoxFixedLength
+ sequence.IccProfileData.Length;
}
byte[]? exifData = sequence.ExifData;
byte[]? xmpData = sequence.XmpData;
long metadataLength = 0;
if (exifData is not null || xmpData is not null)
{
int metadataItemCount = (exifData is not null ? 1 : 0) + (xmpData is not null ? 1 : 0);
metadataLength = (long)metadataFixedLength
+ (metadataItemCount * metadataLocationLength)
+ (exifData is not null ? (long)exifInformationLength + exifOffsetLength + exifData.Length : 0)
+ (xmpData is not null ? (long)xmpInformationLength + xmpData.Length : 0);
}
long colorTrackLength =
BasicBoxHeaderLength
+ trackHeaderBoxLength
+ repeatBoxLength
+ metadataLength
+ mediaBoxFixedLength
+ colorSampleTableLength;
long alphaTrackLength = 0;
if (sequence.AlphaTrack.HasValue)
{
HeifSequenceTrackEncoding alphaTrack = sequence.AlphaTrack.GetValueOrDefault();
int alphaRunCount = GetSequenceTimingRunCount(alphaTrack.Samples);
int auxiliaryTypeBoxLength =
FullBoxHeaderLength
+ Encoding.UTF8.GetByteCount(HeifConstants.AlphaAuxiliaryType)
+ 1;
long alphaSampleTableLength =
(long)sampleTableFixedLength
+ (alphaRunCount * timingRunLength)
+ (alphaTrack.Samples.Length * sampleSizeAndSyncEntryLength)
+ auxiliaryTypeBoxLength;
alphaTrackLength =
BasicBoxHeaderLength
+ trackHeaderBoxLength
+ trackReferenceBoxLength
+ repeatBoxLength
+ mediaBoxFixedLength
+ alphaSampleTableLength;
}
// The movie box contains one header and one or two tracks. Every nested variable-length field above is
// resolved before this exact allocation, so container writing cannot re-rent or copy its buffer.
long movieLength = BasicBoxHeaderLength + movieHeaderBoxLength + colorTrackLength + alphaTrackLength;
return checked((int)movieLength);
} }
private static void WriteSequenceMovieHeader( private static void WriteSequenceMovieHeader(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
uint timescale, uint timescale,
ulong duration, ulong duration,
@ -355,7 +486,7 @@ internal sealed partial class HeifEncoderCore
} }
private static int WriteSequenceTrack( private static int WriteSequenceTrack(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
HeifSequenceEncoding sequence, HeifSequenceEncoding sequence,
HeifSequenceTrackEncoding track, HeifSequenceTrackEncoding track,
@ -382,6 +513,11 @@ internal sealed partial class HeifEncoderCore
WriteSequenceEditList(memory, ref offset, mediaDuration); WriteSequenceEditList(memory, ref offset, mediaDuration);
} }
if (!track.IsAlpha && (sequence.ExifData is not null || sequence.XmpData is not null))
{
WriteSequenceTrackMetadata(memory, ref offset, sequence);
}
int mediaStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Mdia); int mediaStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Mdia);
WriteSequenceMediaHeader(memory, ref offset, sequence.Timescale, mediaDuration); WriteSequenceMediaHeader(memory, ref offset, sequence.Timescale, mediaDuration);
WriteSequenceHandler(memory, ref offset, track.IsAlpha ? Heif4CharCode.Auxv : Heif4CharCode.Pict); WriteSequenceHandler(memory, ref offset, track.IsAlpha ? Heif4CharCode.Auxv : Heif4CharCode.Pict);
@ -395,7 +531,7 @@ internal sealed partial class HeifEncoderCore
} }
private static void WriteSequenceTrackHeader( private static void WriteSequenceTrackHeader(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
int width, int width,
int height, int height,
@ -411,13 +547,13 @@ internal sealed partial class HeifEncoderCore
WriteSequenceUInt64(memory, ref offset, duration); WriteSequenceUInt64(memory, ref offset, duration);
WriteSequenceZeros(memory, ref offset, (2 * sizeof(uint)) + (4 * sizeof(ushort))); WriteSequenceZeros(memory, ref offset, (2 * sizeof(uint)) + (4 * sizeof(ushort)));
WriteSequenceIdentityMatrix(memory, ref offset); WriteSequenceIdentityMatrix(memory, ref offset);
WriteSequenceUInt32(memory, ref offset, checked((uint)width << 16)); WriteSequenceUInt32(memory, ref offset, (uint)width << 16);
WriteSequenceUInt32(memory, ref offset, checked((uint)height << 16)); WriteSequenceUInt32(memory, ref offset, (uint)height << 16);
EndSequenceBox(memory, trackHeaderStart, offset); EndSequenceBox(memory, trackHeaderStart, offset);
} }
private static void WriteSequenceTrackReference( private static void WriteSequenceTrackReference(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
Heif4CharCode referenceType, Heif4CharCode referenceType,
uint referencedTrackId) uint referencedTrackId)
@ -430,7 +566,7 @@ internal sealed partial class HeifEncoderCore
} }
private static void WriteSequenceEditList( private static void WriteSequenceEditList(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
ulong mediaDuration) ulong mediaDuration)
{ {
@ -446,8 +582,119 @@ internal sealed partial class HeifEncoderCore
EndSequenceBox(memory, editStart, offset); EndSequenceBox(memory, editStart, offset);
} }
private static void WriteSequenceTrackMetadata(
Span<byte> memory,
ref int offset,
HeifSequenceEncoding sequence)
{
const byte fourByteOffsetAndLengthSizes = 0x44;
byte[]? exifData = sequence.ExifData;
byte[]? xmpData = sequence.XmpData;
ushort itemCount = (ushort)((exifData is not null ? 1 : 0) + (xmpData is not null ? 1 : 0));
int metadataStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Meta);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceHandler(memory, ref offset, Heif4CharCode.Pict);
// Construction method one makes each extent relative to the local idat payload, keeping metadata independent
// of the final file and movie-box offsets.
int locationsStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Iloc);
WriteSequenceFullBoxHeader(memory, ref offset, 1, 0);
memory[offset++] = fourByteOffsetAndLengthSizes;
memory[offset++] = 0;
WriteSequenceUInt16(memory, ref offset, itemCount);
ushort itemId = 1;
uint itemDataOffset = 0;
if (exifData is not null)
{
uint exifLength = (uint)exifData.Length + sizeof(uint);
WriteSequenceTrackMetadataLocation(memory, ref offset, itemId++, itemDataOffset, exifLength);
itemDataOffset += exifLength;
}
if (xmpData is not null)
{
WriteSequenceTrackMetadataLocation(
memory,
ref offset,
itemId,
itemDataOffset,
(uint)xmpData.Length);
}
EndSequenceBox(memory, locationsStart, offset);
int informationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Iinf);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt16(memory, ref offset, itemCount);
itemId = 1;
if (exifData is not null)
{
WriteSequenceTrackMetadataItem(memory, ref offset, itemId++, Heif4CharCode.Exif);
}
if (xmpData is not null)
{
WriteSequenceTrackMetadataItem(memory, ref offset, itemId, Heif4CharCode.Mime);
}
EndSequenceBox(memory, informationStart, offset);
int itemDataStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Idat);
if (exifData is not null)
{
WriteSequenceUInt32(memory, ref offset, sequence.ExifTiffHeaderOffset);
WriteSequenceBytes(memory, ref offset, exifData);
}
if (xmpData is not null)
{
WriteSequenceBytes(memory, ref offset, xmpData);
}
EndSequenceBox(memory, itemDataStart, offset);
EndSequenceBox(memory, metadataStart, offset);
}
private static void WriteSequenceTrackMetadataLocation(
Span<byte> memory,
ref int offset,
ushort itemId,
uint itemDataOffset,
uint itemLength)
{
WriteSequenceUInt16(memory, ref offset, itemId);
WriteSequenceUInt16(memory, ref offset, 1);
WriteSequenceUInt16(memory, ref offset, 0);
WriteSequenceUInt16(memory, ref offset, 1);
WriteSequenceUInt32(memory, ref offset, itemDataOffset);
WriteSequenceUInt32(memory, ref offset, itemLength);
}
private static void WriteSequenceTrackMetadataItem(
Span<byte> memory,
ref int offset,
ushort itemId,
Heif4CharCode itemType)
{
int itemStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Infe);
WriteSequenceFullBoxHeader(memory, ref offset, 2, 0);
WriteSequenceUInt16(memory, ref offset, itemId);
WriteSequenceUInt16(memory, ref offset, 0);
WriteSequenceUInt32(memory, ref offset, (uint)itemType);
ReadOnlySpan<byte> itemName = itemType == Heif4CharCode.Exif ? "Exif"u8 : "XMP"u8;
WriteSequenceBytes(memory, ref offset, itemName);
memory[offset++] = 0;
if (itemType == Heif4CharCode.Mime)
{
WriteSequenceBytes(memory, ref offset, "application/rdf+xml"u8);
memory[offset++] = 0;
}
EndSequenceBox(memory, itemStart, offset);
}
private static void WriteSequenceMediaHeader( private static void WriteSequenceMediaHeader(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
uint timescale, uint timescale,
ulong mediaDuration) ulong mediaDuration)
@ -464,7 +711,7 @@ internal sealed partial class HeifEncoderCore
} }
private static void WriteSequenceHandler( private static void WriteSequenceHandler(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
Heif4CharCode handlerType) Heif4CharCode handlerType)
{ {
@ -473,11 +720,11 @@ internal sealed partial class HeifEncoderCore
WriteSequenceUInt32(memory, ref offset, 0); WriteSequenceUInt32(memory, ref offset, 0);
WriteSequenceUInt32(memory, ref offset, (uint)handlerType); WriteSequenceUInt32(memory, ref offset, (uint)handlerType);
WriteSequenceZeros(memory, ref offset, 12); WriteSequenceZeros(memory, ref offset, 12);
memory.GetSpan(offset++, 1)[0] = 0; memory[offset++] = 0;
EndSequenceBox(memory, handlerStart, offset); EndSequenceBox(memory, handlerStart, offset);
} }
private static void WriteSequenceDataInformation(AutoExpandingMemory<byte> memory, ref int offset) private static void WriteSequenceDataInformation(Span<byte> memory, ref int offset)
{ {
int dataInformationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dinf); int dataInformationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dinf);
int dataReferenceStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dref); int dataReferenceStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dref);
@ -491,7 +738,7 @@ internal sealed partial class HeifEncoderCore
} }
private static int WriteSequenceSampleTable( private static int WriteSequenceSampleTable(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
HeifSequenceEncoding sequence, HeifSequenceEncoding sequence,
HeifSequenceTrackEncoding track) HeifSequenceTrackEncoding track)
@ -505,17 +752,17 @@ internal sealed partial class HeifEncoderCore
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, 1); WriteSequenceUInt32(memory, ref offset, 1);
WriteSequenceUInt32(memory, ref offset, 1); WriteSequenceUInt32(memory, ref offset, 1);
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length)); WriteSequenceUInt32(memory, ref offset, (uint)track.Samples.Length);
WriteSequenceUInt32(memory, ref offset, 1); WriteSequenceUInt32(memory, ref offset, 1);
EndSequenceBox(memory, sampleToChunkStart, offset); EndSequenceBox(memory, sampleToChunkStart, offset);
int sampleSizesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stsz); int sampleSizesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stsz);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, 0); WriteSequenceUInt32(memory, ref offset, 0);
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length)); WriteSequenceUInt32(memory, ref offset, (uint)track.Samples.Length);
foreach (HeifSequenceSampleInfo sample in track.Samples) foreach (HeifSequenceSampleInfo sample in track.Samples)
{ {
WriteSequenceUInt32(memory, ref offset, checked((uint)sample.Length)); WriteSequenceUInt32(memory, ref offset, (uint)sample.Length);
} }
EndSequenceBox(memory, sampleSizesStart, offset); EndSequenceBox(memory, sampleSizesStart, offset);
@ -530,7 +777,7 @@ internal sealed partial class HeifEncoderCore
// The current bounded sequence encoder emits independent all-intra pictures; every sample is seekable. // The current bounded sequence encoder emits independent all-intra pictures; every sample is seekable.
int syncSamplesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stss); int syncSamplesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stss);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length)); WriteSequenceUInt32(memory, ref offset, (uint)track.Samples.Length);
for (uint sampleIndex = 1; sampleIndex <= track.Samples.Length; sampleIndex++) for (uint sampleIndex = 1; sampleIndex <= track.Samples.Length; sampleIndex++)
{ {
WriteSequenceUInt32(memory, ref offset, sampleIndex); WriteSequenceUInt32(memory, ref offset, sampleIndex);
@ -542,7 +789,7 @@ internal sealed partial class HeifEncoderCore
} }
private static void WriteSequenceSampleDescription( private static void WriteSequenceSampleDescription(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
HeifSequenceEncoding sequence, HeifSequenceEncoding sequence,
HeifSequenceTrackEncoding track) HeifSequenceTrackEncoding track)
@ -554,18 +801,18 @@ internal sealed partial class HeifEncoderCore
WriteSequenceZeros(memory, ref offset, 6); WriteSequenceZeros(memory, ref offset, 6);
WriteSequenceUInt16(memory, ref offset, 1); WriteSequenceUInt16(memory, ref offset, 1);
WriteSequenceZeros(memory, ref offset, (2 * sizeof(ushort)) + (3 * sizeof(uint))); WriteSequenceZeros(memory, ref offset, (2 * sizeof(ushort)) + (3 * sizeof(uint)));
WriteSequenceUInt16(memory, ref offset, checked((ushort)sequence.Width)); WriteSequenceUInt16(memory, ref offset, (ushort)sequence.Width);
WriteSequenceUInt16(memory, ref offset, checked((ushort)sequence.Height)); WriteSequenceUInt16(memory, ref offset, (ushort)sequence.Height);
WriteSequenceUInt32(memory, ref offset, 72U << 16); WriteSequenceUInt32(memory, ref offset, DefaultVisualSampleResolution);
WriteSequenceUInt32(memory, ref offset, 72U << 16); WriteSequenceUInt32(memory, ref offset, DefaultVisualSampleResolution);
WriteSequenceUInt32(memory, ref offset, 0); WriteSequenceUInt32(memory, ref offset, 0);
WriteSequenceUInt16(memory, ref offset, 1); WriteSequenceUInt16(memory, ref offset, 1);
WriteSequenceZeros(memory, ref offset, 32); WriteSequenceZeros(memory, ref offset, VisualSampleCompressorNameLength);
WriteSequenceUInt16(memory, ref offset, 24); WriteSequenceUInt16(memory, ref offset, VisualSampleDepth);
WriteSequenceUInt16(memory, ref offset, ushort.MaxValue); WriteSequenceUInt16(memory, ref offset, ushort.MaxValue);
int configurationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Av1C); int configurationStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Av1C);
track.Configuration.WriteFixedHeader(memory.GetSpan(offset, Av1CodecConfiguration.FixedHeaderSize)); track.Configuration.WriteFixedHeader(memory.Slice(offset, Av1CodecConfiguration.FixedHeaderSize));
offset += Av1CodecConfiguration.FixedHeaderSize; offset += Av1CodecConfiguration.FixedHeaderSize;
EndSequenceBox(memory, configurationStart, offset); EndSequenceBox(memory, configurationStart, offset);
@ -574,16 +821,16 @@ internal sealed partial class HeifEncoderCore
int auxiliaryTypeStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Auxi); int auxiliaryTypeStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Auxi);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(HeifConstants.AlphaAuxiliaryType); int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(HeifConstants.AlphaAuxiliaryType);
Span<byte> auxiliaryType = memory.GetSpan(offset, auxiliaryTypeLength + 1); Span<byte> auxiliaryType = memory.Slice(offset, auxiliaryTypeLength + 1);
offset += Encoding.UTF8.GetBytes(HeifConstants.AlphaAuxiliaryType, auxiliaryType); offset += Encoding.UTF8.GetBytes(HeifConstants.AlphaAuxiliaryType, auxiliaryType);
memory.GetSpan(offset++, 1)[0] = 0; memory[offset++] = 0;
EndSequenceBox(memory, auxiliaryTypeStart, offset); EndSequenceBox(memory, auxiliaryTypeStart, offset);
} }
else else
{ {
if (sequence.IccProfile is not null) if (!sequence.IccProfileData.IsEmpty)
{ {
offset += WriteIccColorInformationPropertyBox(memory, offset, sequence.IccProfile); offset += WriteIccColorInformationPropertyBox(memory, offset, sequence.IccProfileData);
} }
offset += WriteColorInformationPropertyBox(memory, offset, sequence.ColorProfile); offset += WriteColorInformationPropertyBox(memory, offset, sequence.ColorProfile);
@ -594,28 +841,24 @@ internal sealed partial class HeifEncoderCore
// Every emitted sequence sample is independently decodable, while intra prediction remains available inside // Every emitted sequence sample is independently decodable, while intra prediction remains available inside
// each picture. No inter-picture reference slot is therefore advertised. // each picture. No inter-picture reference slot is therefore advertised.
WriteSequenceUInt32(memory, ref offset, 0xC0000000); WriteSequenceUInt32(memory, ref offset, AllReferencePicturesIntraMask | IntraPicturePredictionUsedMask);
EndSequenceBox(memory, codingConstraintsStart, offset); EndSequenceBox(memory, codingConstraintsStart, offset);
EndSequenceBox(memory, sampleEntryStart, offset); EndSequenceBox(memory, sampleEntryStart, offset);
EndSequenceBox(memory, descriptionStart, offset); EndSequenceBox(memory, descriptionStart, offset);
} }
private static void WriteSequenceSampleTiming( private static void WriteSequenceSampleTiming(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
ReadOnlySpan<HeifSequenceSampleInfo> samples) ReadOnlySpan<HeifSequenceSampleInfo> samples)
{ {
// The time-to-sample table stores runs, not one entry per frame. Preserve exact resolved durations while // 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. // combining only adjacent frames whose delays are equal.
int runCount = 1; int runCount = GetSequenceTimingRunCount(samples);
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); int timingStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stts);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0); WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, checked((uint)runCount)); WriteSequenceUInt32(memory, ref offset, (uint)runCount);
uint runDuration = samples[0].Duration; uint runDuration = samples[0].Duration;
uint runLength = 1; uint runLength = 1;
for (int sampleIndex = 1; sampleIndex <= samples.Length; sampleIndex++) for (int sampleIndex = 1; sampleIndex <= samples.Length; sampleIndex++)
@ -638,9 +881,22 @@ internal sealed partial class HeifEncoderCore
EndSequenceBox(memory, timingStart, offset); EndSequenceBox(memory, timingStart, offset);
} }
private static int GetSequenceTimingRunCount(ReadOnlySpan<HeifSequenceSampleInfo> samples)
{
int runCount = 1;
for (int sampleIndex = 1; sampleIndex < samples.Length; sampleIndex++)
{
runCount += samples[sampleIndex].Duration == samples[sampleIndex - 1].Duration ? 0 : 1;
}
return runCount;
}
private static uint GetSequenceSampleDuration(Rational delay, uint timescale) private static uint GetSequenceSampleDuration(Rational delay, uint timescale)
{ {
if (delay.Numerator == 0) // HEIF metadata uses either a zero numerator or a zero denominator for an unspecified duration.
// BMFF samples still require a finite positive duration, so encode the smallest representable value.
if (delay.Numerator == 0 || delay.Denominator == 0)
{ {
return 1; return 1;
} }
@ -656,7 +912,7 @@ internal sealed partial class HeifEncoderCore
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
Rational delay = frame.Metadata.GetHeifMetadata().FrameDelay; Rational delay = frame.Metadata.GetHeifMetadata().FrameDelay;
if (delay.Numerator == 0) if (delay.Numerator == 0 || delay.Denominator == 0)
{ {
continue; continue;
} }
@ -700,34 +956,34 @@ internal sealed partial class HeifEncoderCore
} }
private static int BeginSequenceBox( private static int BeginSequenceBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
Heif4CharCode type) Heif4CharCode type)
{ {
// Reserve the size field now and patch it at the matching EndSequenceBox call after nested boxes expand. // Reserve the size field now and patch it at the matching EndSequenceBox call after nested boxes expand.
int start = offset; int start = offset;
offset += WriteBoxHeader(memory.GetSpan(offset, 8), type); offset += WriteBoxHeader(memory[offset..], type);
return start; return start;
} }
private static void EndSequenceBox(AutoExpandingMemory<byte> memory, int start, int offset) private static void EndSequenceBox(Span<byte> memory, int start, int offset)
=> BinaryPrimitives.WriteUInt32BigEndian( => BinaryPrimitives.WriteUInt32BigEndian(
memory.GetSpan(start, sizeof(uint)), memory.Slice(start, sizeof(uint)),
checked((uint)(offset - start))); (uint)(offset - start));
private static void WriteSequenceFullBoxHeader( private static void WriteSequenceFullBoxHeader(
AutoExpandingMemory<byte> memory, Span<byte> memory,
ref int offset, ref int offset,
byte version, byte version,
uint flags) uint flags)
{ {
Span<byte> destination = memory.GetSpan(offset, sizeof(uint)); Span<byte> destination = memory.Slice(offset, sizeof(uint));
BinaryPrimitives.WriteUInt32BigEndian(destination, flags); BinaryPrimitives.WriteUInt32BigEndian(destination, flags);
destination[0] = version; destination[0] = version;
offset += sizeof(uint); offset += sizeof(uint);
} }
private static void WriteSequenceIdentityMatrix(AutoExpandingMemory<byte> memory, ref int offset) private static void WriteSequenceIdentityMatrix(Span<byte> memory, ref int offset)
{ {
WriteSequenceUInt32(memory, ref offset, UnityFixed16Point16); WriteSequenceUInt32(memory, ref offset, UnityFixed16Point16);
WriteSequenceUInt32(memory, ref offset, 0); WriteSequenceUInt32(memory, ref offset, 0);
@ -740,27 +996,36 @@ internal sealed partial class HeifEncoderCore
WriteSequenceUInt32(memory, ref offset, UnityFixed2Point30); WriteSequenceUInt32(memory, ref offset, UnityFixed2Point30);
} }
private static void WriteSequenceZeros(AutoExpandingMemory<byte> memory, ref int offset, int length) private static void WriteSequenceZeros(Span<byte> memory, ref int offset, int length)
{ {
memory.GetSpan(offset, length).Clear(); memory.Slice(offset, length).Clear();
offset += length; offset += length;
} }
private static void WriteSequenceUInt16(AutoExpandingMemory<byte> memory, ref int offset, ushort value) private static void WriteSequenceBytes(
Span<byte> memory,
ref int offset,
ReadOnlySpan<byte> source)
{ {
BinaryPrimitives.WriteUInt16BigEndian(memory.GetSpan(offset, sizeof(ushort)), value); source.CopyTo(memory[offset..]);
offset += source.Length;
}
private static void WriteSequenceUInt16(Span<byte> memory, ref int offset, ushort value)
{
BinaryPrimitives.WriteUInt16BigEndian(memory[offset..], value);
offset += sizeof(ushort); offset += sizeof(ushort);
} }
private static void WriteSequenceUInt32(AutoExpandingMemory<byte> memory, ref int offset, uint value) private static void WriteSequenceUInt32(Span<byte> memory, ref int offset, uint value)
{ {
BinaryPrimitives.WriteUInt32BigEndian(memory.GetSpan(offset, sizeof(uint)), value); BinaryPrimitives.WriteUInt32BigEndian(memory[offset..], value);
offset += sizeof(uint); offset += sizeof(uint);
} }
private static void WriteSequenceUInt64(AutoExpandingMemory<byte> memory, ref int offset, ulong value) private static void WriteSequenceUInt64(Span<byte> memory, ref int offset, ulong value)
{ {
BinaryPrimitives.WriteUInt64BigEndian(memory.GetSpan(offset, sizeof(ulong)), value); BinaryPrimitives.WriteUInt64BigEndian(memory[offset..], value);
offset += sizeof(ulong); offset += sizeof(ulong);
} }
@ -829,7 +1094,10 @@ internal sealed partial class HeifEncoderCore
HeifSequenceTrackEncoding colorTrack, HeifSequenceTrackEncoding colorTrack,
HeifSequenceTrackEncoding? alphaTrack, HeifSequenceTrackEncoding? alphaTrack,
CicpProfile colorProfile, CicpProfile colorProfile,
IccProfile? iccProfile) ReadOnlyMemory<byte> iccProfileData,
byte[]? exifData,
uint exifTiffHeaderOffset,
byte[]? xmpData)
{ {
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
@ -838,7 +1106,10 @@ internal sealed partial class HeifEncoderCore
this.ColorTrack = colorTrack; this.ColorTrack = colorTrack;
this.AlphaTrack = alphaTrack; this.AlphaTrack = alphaTrack;
this.ColorProfile = colorProfile; this.ColorProfile = colorProfile;
this.IccProfile = iccProfile; this.IccProfileData = iccProfileData;
this.ExifData = exifData;
this.ExifTiffHeaderOffset = exifTiffHeaderOffset;
this.XmpData = xmpData;
} }
public int Width { get; } public int Width { get; }
@ -855,33 +1126,33 @@ internal sealed partial class HeifEncoderCore
public CicpProfile ColorProfile { get; } public CicpProfile ColorProfile { get; }
public IccProfile? IccProfile { get; } public ReadOnlyMemory<byte> IccProfileData { get; }
public byte[]? ExifData { get; }
public uint ExifTiffHeaderOffset { get; }
public byte[]? XmpData { get; }
} }
private readonly struct HeifSequenceTrackEncoding private readonly struct HeifSequenceTrackEncoding
{ {
private readonly HeifSequenceSampleInfo[] samples; private readonly ReadOnlyMemory<HeifSequenceSampleInfo> samples;
private readonly int sampleOffset;
private readonly int sampleCount;
public HeifSequenceTrackEncoding( public HeifSequenceTrackEncoding(
Av1CodecConfiguration configuration, Av1CodecConfiguration configuration,
HeifSequenceSampleInfo[] samples, ReadOnlyMemory<HeifSequenceSampleInfo> samples,
int sampleOffset,
int sampleCount,
bool isAlpha) bool isAlpha)
{ {
this.Configuration = configuration; this.Configuration = configuration;
this.samples = samples; this.samples = samples;
this.sampleOffset = sampleOffset;
this.sampleCount = sampleCount;
this.IsAlpha = isAlpha; this.IsAlpha = isAlpha;
} }
public Av1CodecConfiguration Configuration { get; } public Av1CodecConfiguration Configuration { get; }
public ReadOnlySpan<HeifSequenceSampleInfo> Samples public ReadOnlySpan<HeifSequenceSampleInfo> Samples
=> this.samples.AsSpan(this.sampleOffset, this.sampleCount); => this.samples.Span;
public bool IsAlpha { get; } public bool IsAlpha { get; }
} }

357
src/ImageSharp/Formats/Heif/HeifEncoderCore.cs

@ -1,14 +1,17 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -20,6 +23,30 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// </summary> /// </summary>
internal sealed partial class HeifEncoderCore internal sealed partial class HeifEncoderCore
{ {
// ISO BMFF box lengths include their size and type fields. Full boxes also include version and flags.
private const int BasicBoxHeaderLength = 8;
private const int FullBoxHeaderLength = 12;
private const int HandlerBoxLength = 33;
private const int PrimaryItemBoxLength = 14;
private const int ItemInformationBoxFixedLength = 14;
private const int ItemInformationEntryFixedLength = 21;
private const int ItemReferenceBoxFixedLength = 12;
private const int ItemReferenceEntryFixedLength = 12;
private const int ItemPropertiesBoxFixedLength = 32;
private const int PropertyAssociationEntryFixedLength = 3;
private const int ItemLocationBoxFixedLength = 16;
private const int ItemLocationEntryFixedLength = 8;
private const int ItemExtentLength = 12;
private const int SpatialExtentPropertyBoxLength = 20;
private const int PixelInformationPropertyBoxFixedLength = 13;
private const int Av1CodecConfigurationPropertyBoxLength = BasicBoxHeaderLength + Av1CodecConfiguration.FixedHeaderSize;
private const int AuxiliaryTypePropertyBoxFixedLength = 13;
private const int IccColorInformationPropertyBoxFixedLength = 12;
private const int CicpColorInformationPropertyBoxLength = 19;
private const int MaximumCompactPropertyIndex = 0x7F;
private const ushort EssentialPropertyFlag = 0x8000;
private const byte CompactEssentialPropertyFlag = 0x80;
/// <summary> /// <summary>
/// The global configuration. /// The global configuration.
/// </summary> /// </summary>
@ -58,10 +85,16 @@ internal sealed partial class HeifEncoderCore
if (this.encoder.CompressionMethod == HeifCompressionMethod.Av1 && image.Frames.Count > 1) if (this.encoder.CompressionMethod == HeifCompressionMethod.Av1 && image.Frames.Count > 1)
{ {
Av1EncodingSettings settings = this.ResolveAv1Encoding(image); Av1EncodingSettings settings = this.ResolveAv1Encoding(image);
int sampleCount = image.Frames.Count * (settings.HasAlpha ? 2 : 1);
using IMemoryOwner<HeifSequenceSampleInfo> samplesOwner =
this.configuration.MemoryAllocator.Allocate<HeifSequenceSampleInfo>(sampleCount);
Memory<HeifSequenceSampleInfo> samples = samplesOwner.Memory[..sampleCount];
HeifSequenceEncoding sequence = this.CompressAv1Sequence( HeifSequenceEncoding sequence = this.CompressAv1Sequence(
image, image,
compressedPixels, compressedPixels,
settings, settings,
samples,
cancellationToken); cancellationToken);
int fileTypeLength = this.WriteSequenceFileTypeBox(stream); int fileTypeLength = this.WriteSequenceFileTypeBox(stream);
@ -199,8 +232,10 @@ internal sealed partial class HeifEncoderCore
/// <param name="stream">The destination stream positioned after the file-type box.</param> /// <param name="stream">The destination stream positioned after the file-type box.</param>
private void WriteMetadataBox(List<HeifItem> items, List<HeifItemLink> links, long metadataBoxOffset, Stream stream) private void WriteMetadataBox(List<HeifItem> items, List<HeifItemLink> links, long metadataBoxOffset, Stream stream)
{ {
using AutoExpandingMemory<byte> memory = new(this.configuration, 0x1000); int metadataLength = GetMetadataBoxLength(items, links);
Span<byte> buffer = memory.GetSpan(12); using IMemoryOwner<byte> metadataOwner = this.configuration.MemoryAllocator.Allocate<byte>(metadataLength);
Span<byte> memory = metadataOwner.Memory.Span[..metadataLength];
Span<byte> buffer = memory[..FullBoxHeaderLength];
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Meta, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Meta, 0, 0);
bytesWritten += WriteHandlerBox(memory, bytesWritten); bytesWritten += WriteHandlerBox(memory, bytesWritten);
bytesWritten += WritePrimaryItemBox(memory, bytesWritten); bytesWritten += WritePrimaryItemBox(memory, bytesWritten);
@ -208,7 +243,7 @@ internal sealed partial class HeifEncoderCore
if (links.Count > 0) if (links.Count > 0)
{ {
// iref is optional and has no meaning without at least one typed item relationship. // iref is optional and has no meaning without at least one typed item relationship.
bytesWritten += WriteItemReferenceBox(memory, bytesWritten, items, links); bytesWritten += WriteItemReferenceBox(memory, bytesWritten, links);
} }
bytesWritten += WriteItemPropertiesBox(memory, bytesWritten, items); bytesWritten += WriteItemPropertiesBox(memory, bytesWritten, items);
@ -219,23 +254,130 @@ internal sealed partial class HeifEncoderCore
bytesWritten += WriteItemLocationBox(memory, bytesWritten, items, 0); bytesWritten += WriteItemLocationBox(memory, bytesWritten, items, 0);
// The mdat payload immediately follows the completed meta box and its own eight-byte header. // The mdat payload immediately follows the completed meta box and its own eight-byte header.
long mediaDataOffset = checked(metadataBoxOffset + bytesWritten + 8); long mediaDataOffset = checked(metadataBoxOffset + bytesWritten + BasicBoxHeaderLength);
WriteItemLocationBox(memory, itemLocationOffset, items, mediaDataOffset); WriteItemLocationBox(memory, itemLocationOffset, items, mediaDataOffset);
buffer = memory.GetSpan(bytesWritten); buffer = memory[..bytesWritten];
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
stream.Write(buffer); stream.Write(buffer);
} }
private static int GetMetadataBoxLength(List<HeifItem> items, List<HeifItemLink> links)
{
// All variable-length strings, profiles, relationships, properties, and extents are resolved before
// allocating the metadata box, so writing it never needs to re-rent or copy a backing buffer.
return checked(
FullBoxHeaderLength
+ HandlerBoxLength
+ PrimaryItemBoxLength
+ GetItemInformationBoxLength(items)
+ (links.Count == 0 ? 0 : GetItemReferenceBoxLength(links))
+ GetItemPropertiesBoxLength(items)
+ GetItemLocationBoxLength(items));
}
private static int GetItemInformationBoxLength(List<HeifItem> items)
{
long length = ItemInformationBoxFixedLength;
foreach (HeifItem item in items)
{
length += ItemInformationEntryFixedLength + Encoding.UTF8.GetByteCount(item.Name ?? string.Empty);
if (item.Type == Heif4CharCode.Mime)
{
length += 1 + Encoding.UTF8.GetByteCount(item.ContentType ?? string.Empty);
if (item.ContentEncoding is not null)
{
length += 1 + Encoding.UTF8.GetByteCount(item.ContentEncoding);
}
}
}
return checked((int)length);
}
private static int GetItemReferenceBoxLength(List<HeifItemLink> links)
{
long length = ItemReferenceBoxFixedLength;
foreach (HeifItemLink link in links)
{
length += ItemReferenceEntryFixedLength + ((long)link.DestinationIds.Count * sizeof(ushort));
}
return checked((int)length);
}
/// <summary>
/// Gets the exact number of bytes required for the item-properties box.
/// </summary>
/// <param name="items">The items whose properties and associations are counted.</param>
/// <returns>The complete item-properties-box length.</returns>
public static int GetItemPropertiesBoxLength(List<HeifItem> items)
{
long propertyCount = 0;
long associationItemCount = 0;
long propertyBytes = 0;
foreach (HeifItem item in items)
{
int itemPropertyCount = GetPropertyCount(item);
propertyCount += itemPropertyCount;
associationItemCount += itemPropertyCount == 0 ? 0 : 1;
propertyBytes += item.Extent == default ? 0 : SpatialExtentPropertyBoxLength;
if (item.ChannelBitDepths is not null)
{
propertyBytes += PixelInformationPropertyBoxFixedLength + item.ChannelBitDepths.Length;
}
else if (item.UniformChannelBitDepth is not null)
{
propertyBytes += PixelInformationPropertyBoxFixedLength + item.ChannelCount;
}
propertyBytes += item.Av1CodecConfiguration is null
? 0
: Av1CodecConfigurationPropertyBoxLength;
propertyBytes += item.AuxiliaryType is null
? 0
: AuxiliaryTypePropertyBoxFixedLength + Encoding.UTF8.GetByteCount(item.AuxiliaryType);
propertyBytes += item.IccProfile is null
? 0
: IccColorInformationPropertyBoxFixedLength + item.GetIccProfileDataForWriting().Length;
propertyBytes += item.CicpProfile is null ? 0 : CicpColorInformationPropertyBoxLength;
}
int associationSize = propertyCount > MaximumCompactPropertyIndex ? sizeof(ushort) : sizeof(byte);
long length = ItemPropertiesBoxFixedLength
+ propertyBytes
+ (associationItemCount * PropertyAssociationEntryFixedLength)
+ (propertyCount * associationSize);
return checked((int)length);
}
private static int GetItemLocationBoxLength(List<HeifItem> items)
{
long extentCount = 0;
foreach (HeifItem item in items)
{
extentCount += item.DataLocations.Count;
}
long length =
ItemLocationBoxFixedLength
+ ((long)items.Count * ItemLocationEntryFixedLength)
+ (extentCount * ItemExtentLength);
return checked((int)length);
}
/// <summary> /// <summary>
/// Writes the picture metadata handler box. /// Writes the picture metadata handler box.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the metadata box.</param> /// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <returns>The complete handler-box length.</returns> /// <returns>The complete handler-box length.</returns>
private static int WriteHandlerBox(AutoExpandingMemory<byte> memory, int memoryOffset) private static int WriteHandlerBox(Span<byte> memory, int memoryOffset)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 33); Span<byte> buffer = memory.Slice(memoryOffset, HandlerBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Hdlr, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Hdlr, 0, 0);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0);
bytesWritten += 4; bytesWritten += 4;
@ -253,12 +395,12 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes the identifier of the primary presentation item. /// Writes the identifier of the primary presentation item.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the metadata box.</param> /// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <returns>The complete primary-item-box length.</returns> /// <returns>The complete primary-item-box length.</returns>
private static int WritePrimaryItemBox(AutoExpandingMemory<byte> memory, int memoryOffset) private static int WritePrimaryItemBox(Span<byte> memory, int memoryOffset)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 14); Span<byte> buffer = memory.Slice(memoryOffset, PrimaryItemBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pitm, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pitm, 0, 0);
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1); BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1);
bytesWritten += 2; bytesWritten += 2;
@ -270,27 +412,13 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes the item-information box and one version-two entry for each item. /// Writes the item-information box and one version-two entry for each item.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the metadata box.</param> /// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <param name="items">The items to declare.</param> /// <param name="items">The items to declare.</param>
/// <returns>The complete item-information-box length.</returns> /// <returns>The complete item-information-box length.</returns>
private static int WriteItemInfoBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items) private static int WriteItemInfoBox(Span<byte> memory, int memoryOffset, List<HeifItem> items)
{ {
int capacity = 14; Span<byte> buffer = memory.Slice(memoryOffset, GetItemInformationBoxLength(items));
foreach (HeifItem item in items)
{
capacity += 21 + Encoding.UTF8.GetByteCount(item.Name ?? string.Empty);
if (item.Type == Heif4CharCode.Mime)
{
capacity += 1 + Encoding.UTF8.GetByteCount(item.ContentType ?? string.Empty);
if (item.ContentEncoding is not null)
{
capacity += 1 + Encoding.UTF8.GetByteCount(item.ContentEncoding);
}
}
}
Span<byte> buffer = memory.GetSpan(memoryOffset, capacity);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iinf, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iinf, 0, 0);
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count); BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count);
bytesWritten += 2; bytesWritten += 2;
@ -327,14 +455,13 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes typed item-reference child boxes using 16-bit item identifiers. /// Writes typed item-reference child boxes using 16-bit item identifiers.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the metadata box.</param> /// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <param name="items">The declared items used to size the destination.</param>
/// <param name="links">The relationships to write.</param> /// <param name="links">The relationships to write.</param>
/// <returns>The complete item-reference-box length.</returns> /// <returns>The complete item-reference-box length.</returns>
private static int WriteItemReferenceBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items, List<HeifItemLink> links) private static int WriteItemReferenceBox(Span<byte> memory, int memoryOffset, List<HeifItemLink> links)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 12 + (links.Count * (12 + (items.Count * 2)))); Span<byte> buffer = memory.Slice(memoryOffset, GetItemReferenceBoxLength(links));
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iref, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iref, 0, 0);
foreach (HeifItemLink link in links) foreach (HeifItemLink link in links)
{ {
@ -360,13 +487,13 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes spatial-extent properties and their one-based item associations. /// Writes spatial-extent properties and their one-based item associations.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the metadata box.</param> /// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <param name="items">The items whose dimensions are written and associated.</param> /// <param name="items">The items whose dimensions are written and associated.</param>
/// <returns>The complete item-properties-box length.</returns> /// <returns>The complete item-properties-box length.</returns>
public static int WriteItemPropertiesBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items) public static int WriteItemPropertiesBox(Span<byte> memory, int memoryOffset, List<HeifItem> items)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 20); Span<byte> buffer = memory.Slice(memoryOffset, GetItemPropertiesBoxLength(items));
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp);
// ipco order defines the one-based property indices written later in ipma. // ipco order defines the one-based property indices written later in ipma.
@ -412,7 +539,7 @@ internal sealed partial class HeifEncoderCore
IccProfile? iccProfile = item.IccProfile; IccProfile? iccProfile = item.IccProfile;
if (iccProfile is not null) if (iccProfile is not null)
{ {
bytesWritten += WriteIccColorInformationPropertyBox(memory, memoryOffset + bytesWritten, iccProfile); bytesWritten += WriteIccColorInformationPropertyBox(memory, memoryOffset + bytesWritten, item.GetIccProfileDataForWriting());
} }
CicpProfile? cicpProfile = item.CicpProfile; CicpProfile? cicpProfile = item.CicpProfile;
@ -422,11 +549,9 @@ internal sealed partial class HeifEncoderCore
} }
} }
buffer = memory.GetSpan(memoryOffset, bytesWritten);
BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset)); BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset));
int propertyCount = 0; int propertyCount = 0;
int associationItemCount = 0; int associationItemCount = 0;
int associationBoxCapacity = 16;
foreach (HeifItem item in items) foreach (HeifItem item in items)
{ {
int itemPropertyCount = GetPropertyCount(item); int itemPropertyCount = GetPropertyCount(item);
@ -437,16 +562,9 @@ internal sealed partial class HeifEncoderCore
propertyCount += itemPropertyCount; propertyCount += itemPropertyCount;
associationItemCount++; associationItemCount++;
associationBoxCapacity += 3 + itemPropertyCount;
}
bool largePropertyIndex = propertyCount > 0x7F;
if (largePropertyIndex)
{
associationBoxCapacity += propertyCount;
} }
buffer = memory.GetSpan(memoryOffset, bytesWritten + associationBoxCapacity); bool largePropertyIndex = propertyCount > MaximumCompactPropertyIndex;
// ipma uses a 15-bit index only when the property table cannot fit in the compact seven-bit form. // ipma uses a 15-bit index only when the property table cannot fit in the compact seven-bit form.
int ipmaLengthOffset = bytesWritten; int ipmaLengthOffset = bytesWritten;
@ -537,29 +655,29 @@ internal sealed partial class HeifEncoderCore
{ {
if (largePropertyIndex) if (largePropertyIndex)
{ {
ushort association = essential ? (ushort)(propertyIndex | 0x8000) : propertyIndex; ushort association = essential ? (ushort)(propertyIndex | EssentialPropertyFlag) : propertyIndex;
BinaryPrimitives.WriteUInt16BigEndian(buffer[offset..], association); BinaryPrimitives.WriteUInt16BigEndian(buffer[offset..], association);
offset += 2; offset += 2;
} }
else else
{ {
buffer[offset++] = essential ? (byte)(propertyIndex | 0x80) : (byte)propertyIndex; buffer[offset++] = essential ? (byte)(propertyIndex | CompactEssentialPropertyFlag) : (byte)propertyIndex;
} }
} }
/// <summary> /// <summary>
/// Writes the encoded precision of each image channel. /// Writes the encoded precision of each image channel.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="channelBitDepths">The encoded precision of each channel.</param> /// <param name="channelBitDepths">The encoded precision of each channel.</param>
/// <returns>The complete pixel-information-box length.</returns> /// <returns>The complete pixel-information-box length.</returns>
private static int WritePixelInformationPropertyBox( private static int WritePixelInformationPropertyBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
int memoryOffset, int memoryOffset,
ReadOnlySpan<byte> channelBitDepths) ReadOnlySpan<byte> channelBitDepths)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 13 + channelBitDepths.Length); Span<byte> buffer = memory.Slice(memoryOffset, PixelInformationPropertyBoxFixedLength + channelBitDepths.Length);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pixi, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pixi, 0, 0);
buffer[bytesWritten++] = (byte)channelBitDepths.Length; buffer[bytesWritten++] = (byte)channelBitDepths.Length;
channelBitDepths.CopyTo(buffer[bytesWritten..]); channelBitDepths.CopyTo(buffer[bytesWritten..]);
@ -572,18 +690,18 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes one common encoded precision for every image channel. /// Writes one common encoded precision for every image channel.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="channelCount">The number of encoded image channels.</param> /// <param name="channelCount">The number of encoded image channels.</param>
/// <param name="channelBitDepth">The common encoded precision.</param> /// <param name="channelBitDepth">The common encoded precision.</param>
/// <returns>The complete pixel-information-box length.</returns> /// <returns>The complete pixel-information-box length.</returns>
private static int WritePixelInformationPropertyBox( private static int WritePixelInformationPropertyBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
int memoryOffset, int memoryOffset,
int channelCount, int channelCount,
byte channelBitDepth) byte channelBitDepth)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 13 + channelCount); Span<byte> buffer = memory.Slice(memoryOffset, PixelInformationPropertyBoxFixedLength + channelCount);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pixi, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pixi, 0, 0);
buffer[bytesWritten++] = (byte)channelCount; buffer[bytesWritten++] = (byte)channelCount;
buffer.Slice(bytesWritten, channelCount).Fill(channelBitDepth); buffer.Slice(bytesWritten, channelCount).Fill(channelBitDepth);
@ -596,16 +714,16 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes an AV1 codec-configuration property. /// Writes an AV1 codec-configuration property.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="configuration">The fixed image configuration.</param> /// <param name="configuration">The fixed image configuration.</param>
/// <returns>The complete AV1 codec-configuration-box length.</returns> /// <returns>The complete AV1 codec-configuration-box length.</returns>
private static int WriteAv1CodecConfigurationPropertyBox( private static int WriteAv1CodecConfigurationPropertyBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
int memoryOffset, int memoryOffset,
Av1CodecConfiguration configuration) Av1CodecConfiguration configuration)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 8 + Av1CodecConfiguration.FixedHeaderSize); Span<byte> buffer = memory.Slice(memoryOffset, Av1CodecConfigurationPropertyBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Av1C); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Av1C);
configuration.WriteFixedHeader(buffer.Slice(bytesWritten, Av1CodecConfiguration.FixedHeaderSize)); configuration.WriteFixedHeader(buffer.Slice(bytesWritten, Av1CodecConfiguration.FixedHeaderSize));
bytesWritten += Av1CodecConfiguration.FixedHeaderSize; bytesWritten += Av1CodecConfiguration.FixedHeaderSize;
@ -617,17 +735,17 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes the registered type of an auxiliary image item. /// Writes the registered type of an auxiliary image item.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="auxiliaryType">The null-terminated registered auxiliary type.</param> /// <param name="auxiliaryType">The null-terminated registered auxiliary type.</param>
/// <returns>The complete auxiliary-type-box length.</returns> /// <returns>The complete auxiliary-type-box length.</returns>
private static int WriteAuxiliaryTypePropertyBox( private static int WriteAuxiliaryTypePropertyBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
int memoryOffset, int memoryOffset,
string auxiliaryType) string auxiliaryType)
{ {
int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(auxiliaryType); int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(auxiliaryType);
Span<byte> buffer = memory.GetSpan(memoryOffset, 13 + auxiliaryTypeLength); Span<byte> buffer = memory.Slice(memoryOffset, AuxiliaryTypePropertyBoxFixedLength + auxiliaryTypeLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.AuxC, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.AuxC, 0, 0);
bytesWritten += Encoding.UTF8.GetBytes(auxiliaryType, buffer[bytesWritten..]); bytesWritten += Encoding.UTF8.GetBytes(auxiliaryType, buffer[bytesWritten..]);
buffer[bytesWritten++] = 0; buffer[bytesWritten++] = 0;
@ -639,17 +757,16 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes an unrestricted ICC color profile for a color image item. /// Writes an unrestricted ICC color profile for a color image item.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="profile">The ICC profile to write.</param> /// <param name="profileData">The serialized ICC profile to write.</param>
/// <returns>The complete color-information-box length.</returns> /// <returns>The complete color-information-box length.</returns>
private static int WriteIccColorInformationPropertyBox( private static int WriteIccColorInformationPropertyBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
int memoryOffset, int memoryOffset,
IccProfile profile) ReadOnlyMemory<byte> profileData)
{ {
ReadOnlyMemory<byte> profileData = profile.GetDataForWriting(); Span<byte> buffer = memory.Slice(memoryOffset, IccColorInformationPropertyBoxFixedLength + profileData.Length);
Span<byte> buffer = memory.GetSpan(memoryOffset, 12 + profileData.Length);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Prof); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Prof);
bytesWritten += 4; bytesWritten += 4;
@ -663,16 +780,16 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes an H.273 color description for a color image item. /// Writes an H.273 color description for a color image item.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="profile">The color description to write.</param> /// <param name="profile">The color description to write.</param>
/// <returns>The complete color-information-box length.</returns> /// <returns>The complete color-information-box length.</returns>
private static int WriteColorInformationPropertyBox( private static int WriteColorInformationPropertyBox(
AutoExpandingMemory<byte> memory, Span<byte> memory,
int memoryOffset, int memoryOffset,
CicpProfile profile) CicpProfile profile)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 19); Span<byte> buffer = memory.Slice(memoryOffset, CicpColorInformationPropertyBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Nclx); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Nclx);
bytesWritten += 4; bytesWritten += 4;
@ -691,13 +808,13 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes an item's display width and height as an image-spatial-extents property. /// Writes an item's display width and height as an image-spatial-extents property.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param> /// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="item">The item whose extent is written.</param> /// <param name="item">The item whose extent is written.</param>
/// <returns>The complete image-spatial-extents-box length.</returns> /// <returns>The complete image-spatial-extents-box length.</returns>
private static int WriteSpatialExtentPropertyBox(AutoExpandingMemory<byte> memory, int memoryOffset, HeifItem item) private static int WriteSpatialExtentPropertyBox(Span<byte> memory, int memoryOffset, HeifItem item)
{ {
Span<byte> buffer = memory.GetSpan(memoryOffset, 20); Span<byte> buffer = memory.Slice(memoryOffset, SpatialExtentPropertyBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ispe, 0, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ispe, 0, 0);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Width); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Width);
bytesWritten += 4; bytesWritten += 4;
@ -711,15 +828,14 @@ internal sealed partial class HeifEncoderCore
/// <summary> /// <summary>
/// Writes version-one file-relative locations for every ordered item extent. /// Writes version-one file-relative locations for every ordered item extent.
/// </summary> /// </summary>
/// <param name="memory">The expanding metadata buffer.</param> /// <param name="memory">The preallocated metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the metadata box.</param> /// <param name="memoryOffset">The destination offset within the metadata box.</param>
/// <param name="items">The items and relative payload extents to locate.</param> /// <param name="items">The items and relative payload extents to locate.</param>
/// <param name="mediaDataOffset">The absolute stream offset of the media-data payload.</param> /// <param name="mediaDataOffset">The absolute stream offset of the media-data payload.</param>
/// <returns>The complete item-location-box length.</returns> /// <returns>The complete item-location-box length.</returns>
private static int WriteItemLocationBox(AutoExpandingMemory<byte> memory, int memoryOffset, List<HeifItem> items, long mediaDataOffset) private static int WriteItemLocationBox(Span<byte> memory, int memoryOffset, List<HeifItem> items, long mediaDataOffset)
{ {
int extentCount = items.Sum(item => item.DataLocations.Count); Span<byte> buffer = memory.Slice(memoryOffset, GetItemLocationBoxLength(items));
Span<byte> buffer = memory.GetSpan(memoryOffset, 16 + (items.Count * 8) + (extentCount * 12));
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iloc, 1, 0); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iloc, 1, 0);
// The high and low nibbles select eight-byte offsets and four-byte lengths. Base offsets and extent indices // The high and low nibbles select eight-byte offsets and four-byte lengths. Base offsets and extent indices
@ -782,7 +898,7 @@ internal sealed partial class HeifEncoderCore
// External quantizer zero maps to the codec's lossless qindex. Keep quality 100 lossy as its public contract requires. // External quantizer zero maps to the codec's lossless qindex. Keep quality 100 lossy as its public contract requires.
quantizer = Math.Max(quantizer, 1); quantizer = Math.Max(quantizer, 1);
return quantizer < 62 ? quantizer * 4 : quantizer == 62 ? 249 : 255; return Av1QuantizationLookup.GetQIndex(quantizer);
} }
/// <summary> /// <summary>
@ -802,6 +918,19 @@ internal sealed partial class HeifEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte[]? exifData = null;
uint tiffHeaderOffset = 0;
byte[]? xmpData = null;
if (!this.encoder.SkipMetadata)
{
exifData = GetExifData(image.Metadata, out tiffHeaderOffset);
byte[]? sourceXmpData = image.Metadata.XmpProfile?.Data;
if (sourceXmpData is not null && sourceXmpData.Length > 0)
{
xmpData = sourceXmpData;
}
}
Av1EncodingSettings settings = this.ResolveAv1Encoding(image); Av1EncodingSettings settings = this.ResolveAv1Encoding(image);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode( ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode(
@ -863,40 +992,11 @@ internal sealed partial class HeifEncoderCore
return; return;
} }
byte[]? exifData = image.Metadata.ExifProfile?.ToByteArray(); if (exifData is not null)
if (exifData is not null && exifData.Length > 0)
{ {
int tiffHeaderOffset = -1;
// The HEIF Exif prefix identifies the first TIFF byte-order marker, which can follow an optional Exif
// identifier in profiles supplied directly by callers.
for (int i = 0; i <= exifData.Length - 4; i++)
{
bool isBigEndianTiff = exifData[i] == (byte)'M'
&& exifData[i + 1] == (byte)'M'
&& exifData[i + 2] == 0
&& exifData[i + 3] == 42;
bool isLittleEndianTiff = exifData[i] == (byte)'I'
&& exifData[i + 1] == (byte)'I'
&& exifData[i + 2] == 42
&& exifData[i + 3] == 0;
if (isBigEndianTiff || isLittleEndianTiff)
{
tiffHeaderOffset = i;
break;
}
}
if (tiffHeaderOffset < 0)
{
throw new ImageFormatException("The Exif profile does not contain a TIFF header.");
}
long exifOffset = stream.Length; long exifOffset = stream.Length;
Span<byte> offsetBuffer = stackalloc byte[4]; Span<byte> offsetBuffer = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(offsetBuffer, (uint)tiffHeaderOffset); BinaryPrimitives.WriteUInt32BigEndian(offsetBuffer, tiffHeaderOffset);
stream.Write(offsetBuffer); stream.Write(offsetBuffer);
stream.Write(exifData); stream.Write(exifData);
@ -918,8 +1018,7 @@ internal sealed partial class HeifEncoderCore
links.Add(exifLink); links.Add(exifLink);
} }
byte[]? xmpData = image.Metadata.XmpProfile?.Data; if (xmpData is not null)
if (xmpData is not null && xmpData.Length > 0)
{ {
long xmpOffset = stream.Length; long xmpOffset = stream.Length;
stream.Write(xmpData); stream.Write(xmpData);
@ -943,6 +1042,44 @@ internal sealed partial class HeifEncoderCore
} }
} }
/// <summary>
/// Materializes the caller's Exif profile once and locates the TIFF header addressed by HEIF's four-byte prefix.
/// </summary>
/// <param name="metadata">The source image metadata.</param>
/// <param name="tiffHeaderOffset">The byte offset of the TIFF header within the returned profile.</param>
/// <returns>The serialized profile, or <see langword="null"/> when the source has no Exif payload.</returns>
private static byte[]? GetExifData(ImageMetadata metadata, out uint tiffHeaderOffset)
{
byte[]? exifData = metadata.ExifProfile?.ToByteArray();
if (exifData is null || exifData.Length == 0)
{
tiffHeaderOffset = 0;
return null;
}
// A directly supplied profile can retain the optional Exif identifier before its TIFF byte-order marker.
for (int i = 0; i <= exifData.Length - 4; i++)
{
bool isBigEndianTiff = exifData[i] == (byte)'M'
&& exifData[i + 1] == (byte)'M'
&& exifData[i + 2] == 0
&& exifData[i + 3] == 42;
bool isLittleEndianTiff = exifData[i] == (byte)'I'
&& exifData[i + 1] == (byte)'I'
&& exifData[i + 2] == 42
&& exifData[i + 3] == 0;
if (isBigEndianTiff || isLittleEndianTiff)
{
tiffHeaderOffset = (uint)i;
return exifData;
}
}
throw new ImageFormatException("The Exif profile does not contain a TIFF header.");
}
/// <summary> /// <summary>
/// Encodes the source pixels as the current legacy JPEG item payload. /// Encodes the source pixels as the current legacy JPEG item payload.
/// </summary> /// </summary>

30
src/ImageSharp/Formats/Heif/HeifItem.cs

@ -14,6 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// <param name="id">The item identifier used by locations, properties, and references.</param> /// <param name="id">The item identifier used by locations, properties, and references.</param>
internal sealed class HeifItem(Heif4CharCode type, uint id) internal sealed class HeifItem(Heif4CharCode type, uint id)
{ {
private IccProfile? iccProfile;
private ReadOnlyMemory<byte> serializedIccProfile;
/// <summary> /// <summary>
/// Gets the ID of this Item. /// Gets the ID of this Item.
/// </summary> /// </summary>
@ -58,7 +62,15 @@ internal sealed class HeifItem(Heif4CharCode type, uint id)
/// Gets or sets the ICC profile associated with this color image item, or <see langword="null"/> when the item /// Gets or sets the ICC profile associated with this color image item, or <see langword="null"/> when the item
/// has no restricted or unrestricted ICC color-information property. /// has no restricted or unrestricted ICC color-information property.
/// </summary> /// </summary>
public IccProfile? IccProfile { get; set; } public IccProfile? IccProfile
{
get => this.iccProfile;
set
{
this.iccProfile = value;
this.serializedIccProfile = default;
}
}
/// <summary> /// <summary>
/// Gets or sets the CICP color description associated with this color image item, or <see langword="null"/> /// Gets or sets the CICP color description associated with this color image item, or <see langword="null"/>
@ -187,6 +199,22 @@ internal sealed class HeifItem(Heif4CharCode type, uint id)
/// </summary> /// </summary>
public List<HeifLocation> DataLocations { get; } = []; public List<HeifLocation> DataLocations { get; } = [];
/// <summary>
/// Gets the serialized ICC payload used while sizing and writing an encoded item.
/// </summary>
/// <returns>The serialized profile data, or an empty memory when no profile is assigned.</returns>
public ReadOnlyMemory<byte> GetIccProfileDataForWriting()
{
if (this.serializedIccProfile.IsEmpty && this.iccProfile is not null)
{
// Exact-size container writing queries the payload length before copying it. Retaining the serialized
// view on this transient item prevents an entry-built profile from being serialized for both passes.
this.serializedIccProfile = this.iccProfile.GetDataForWriting();
}
return this.serializedIccProfile;
}
/// <summary> /// <summary>
/// Set the image extent. /// Set the image extent.
/// </summary> /// </summary>

78
src/ImageSharp/Memory/AutoExpandingMemory.cs

@ -1,78 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp.Memory;
/// <summary>
/// Memory class that will expand dynamically when full.
/// </summary>
internal sealed class AutoExpandingMemory<T> : IDisposable
where T : unmanaged
{
private const int IncreaseFactor = 5;
private readonly Configuration configuration;
private IMemoryOwner<T> allocation;
private bool isDetached;
public AutoExpandingMemory(Configuration configuration, int initialSize)
{
Guard.MustBeGreaterThan(initialSize, 0, nameof(initialSize));
this.configuration = configuration;
this.allocation = this.configuration.MemoryAllocator.Allocate<T>(initialSize);
}
public int Capacity => this.allocation.Memory.Length;
public Span<T> GetSpan(int requestedSize)
{
Guard.MustBeGreaterThanOrEqualTo(requestedSize, 0, nameof(requestedSize));
this.EnsureCapacity(requestedSize);
return this.allocation.Memory.Span[..requestedSize];
}
public Span<T> GetSpan(int offset, int requestedSize)
{
Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset));
Guard.MustBeGreaterThanOrEqualTo(requestedSize, 0, nameof(requestedSize));
this.EnsureCapacity(offset + requestedSize);
return this.allocation.Memory.Span.Slice(offset, requestedSize);
}
public Span<T> GetEntireSpan()
=> this.GetSpan(this.Capacity);
/// <summary>
/// Transfers the current allocation to the caller without copying its contents.
/// </summary>
/// <returns>The allocation previously owned by this instance.</returns>
public IMemoryOwner<T> Detach()
{
this.isDetached = true;
return this.allocation;
}
public void Dispose()
{
if (!this.isDetached)
{
this.allocation.Dispose();
}
}
private void EnsureCapacity(int requestedSize)
{
if (requestedSize > this.allocation.Memory.Length)
{
int newSize = requestedSize + (requestedSize / IncreaseFactor);
IMemoryOwner<T> newAllocation = this.configuration.MemoryAllocator.Allocate<T>(newSize);
this.allocation.Memory.CopyTo(newAllocation.Memory);
this.allocation.Dispose();
this.allocation = newAllocation;
}
}
}

55
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs

@ -3,7 +3,6 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
@ -74,8 +73,8 @@ public class Av1BitStreamTests
[InlineData(new bool[] { false, true, false, true })] [InlineData(new bool[] { false, true, false, true })]
public void WriteAsBoolean(bool[] booleans) public void WriteAsBoolean(bool[] booleans)
{ {
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 8); byte[] buffer = new byte[Numerics.DivideCeil((uint)booleans.Length, 8)];
Av1BitStreamWriter writer = new(stream); Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < booleans.Length; i++) for (int i = 0; i < booleans.Length; i++)
{ {
writer.WriteBoolean(booleans[i]); writer.WriteBoolean(booleans[i]);
@ -84,7 +83,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
bool[] actual = new bool[booleans.Length]; bool[] actual = new bool[booleans.Length];
for (int i = 0; i < booleans.Length; i++) for (int i = 0; i < booleans.Length; i++)
{ {
@ -101,13 +100,13 @@ public class Av1BitStreamTests
[InlineData(4050, 16)] [InlineData(4050, 16)]
public void WriteAsLiteral(uint value, int bitCount) public void WriteAsLiteral(uint value, int bitCount)
{ {
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 8); byte[] buffer = new byte[Numerics.DivideCeil((uint)bitCount, 8)];
Av1BitStreamWriter writer = new(stream); Av1BitStreamWriter writer = new(buffer);
writer.WriteLiteral(value, bitCount); writer.WriteLiteral(value, bitCount);
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
uint actual = reader.ReadLiteral(bitCount); uint actual = reader.ReadLiteral(bitCount);
Assert.Equal(value, actual); Assert.Equal(value, actual);
} }
@ -123,8 +122,9 @@ public class Av1BitStreamTests
public void ReadLiteralRainbowArray(int bitCount) public void ReadLiteralRainbowArray(int bitCount)
{ {
uint[] values = Enumerable.Range(0, (1 << bitCount) - 1).Select(i => (uint)i).ToArray(); uint[] values = Enumerable.Range(0, (1 << bitCount) - 1).Select(i => (uint)i).ToArray();
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 280); int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
Av1BitStreamWriter writer = new(stream); byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
writer.WriteLiteral(values[i], bitCount); writer.WriteLiteral(values[i], bitCount);
@ -133,7 +133,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
uint[] actuals = new uint[values.Length]; uint[] actuals = new uint[values.Length];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
@ -152,8 +152,9 @@ public class Av1BitStreamTests
public void ReadWriteAsLiteralArray(int bitCount, uint val1, uint val2, uint val3, uint val4) public void ReadWriteAsLiteralArray(int bitCount, uint val1, uint val2, uint val3, uint val4)
{ {
uint[] values = [val1, val2, val3, val4]; uint[] values = [val1, val2, val3, val4];
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 80); int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
Av1BitStreamWriter writer = new(stream); byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
writer.WriteLiteral(values[i], bitCount); writer.WriteLiteral(values[i], bitCount);
@ -162,7 +163,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
uint actual = reader.ReadLiteral(bitCount); uint actual = reader.ReadLiteral(bitCount);
@ -182,8 +183,8 @@ public class Av1BitStreamTests
public void ReadWriteAsNonSymmetricArray(uint numberOfSymbols, uint val1, uint val2, uint val3, uint val4) public void ReadWriteAsNonSymmetricArray(uint numberOfSymbols, uint val1, uint val2, uint val3, uint val4)
{ {
uint[] values = [val1, val2, val3, val4]; uint[] values = [val1, val2, val3, val4];
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 80); byte[] buffer = new byte[values.Length * sizeof(uint)];
Av1BitStreamWriter writer = new(stream); Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
writer.WriteNonSymmetric(values[i], numberOfSymbols); writer.WriteNonSymmetric(values[i], numberOfSymbols);
@ -192,7 +193,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
uint[] actuals = new uint[4]; uint[] actuals = new uint[4];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
@ -213,8 +214,9 @@ public class Av1BitStreamTests
{ {
int maxValue = (1 << (bitCount - 1)) - 1; int maxValue = (1 << (bitCount - 1)) - 1;
int[] values = Enumerable.Range(-maxValue, maxValue).ToArray(); int[] values = Enumerable.Range(-maxValue, maxValue).ToArray();
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 280); int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
Av1BitStreamWriter writer = new(stream); byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
writer.WriteSignedFromUnsigned(values[i], bitCount); writer.WriteSignedFromUnsigned(values[i], bitCount);
@ -223,7 +225,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
int[] actuals = new int[values.Length]; int[] actuals = new int[values.Length];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
@ -294,8 +296,9 @@ public class Av1BitStreamTests
public void ReadWriteSignedArray(int bitCount, int val1, int val2, int val3, int val4) public void ReadWriteSignedArray(int bitCount, int val1, int val2, int val3, int val4)
{ {
int[] values = [val1, val2, val3, val4]; int[] values = [val1, val2, val3, val4];
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 80); int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
Av1BitStreamWriter writer = new(stream); byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
writer.WriteSignedFromUnsigned(values[i], bitCount); writer.WriteSignedFromUnsigned(values[i], bitCount);
@ -304,7 +307,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan()); Av1BitStreamReader reader = new(buffer);
int[] actuals = new int[4]; int[] actuals = new int[4];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
@ -342,9 +345,9 @@ public class Av1BitStreamTests
public void ReadWriteLittleEndianBytes128Array(uint val0, uint val1, uint val2, uint val3, uint val4) public void ReadWriteLittleEndianBytes128Array(uint val0, uint val1, uint val2, uint val3, uint val4)
{ {
uint[] values = [val0, val1, val2, val3, val4]; uint[] values = [val0, val1, val2, val3, val4];
int bufferSize = 80; const int MaximumEncodedUInt32Length = 5;
using AutoExpandingMemory<byte> stream = new(Configuration.Default, bufferSize); byte[] buffer = new byte[values.Length * MaximumEncodedUInt32Length];
Av1BitStreamWriter writer = new(stream); Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
writer.WriteLittleEndianBytes128(values[i]); writer.WriteLittleEndianBytes128(values[i]);
@ -353,7 +356,7 @@ public class Av1BitStreamTests
writer.Flush(); writer.Flush();
// Read the written value back. // Read the written value back.
Av1BitStreamReader reader = new(stream.GetSpan(bufferSize)); Av1BitStreamReader reader = new(buffer);
uint[] actuals = new uint[5]; uint[] actuals = new uint[5];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {

27
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -19,6 +19,9 @@ public class Av1CoefficientsEntropyTests
{ {
private const int BaseQIndex = 23; private const int BaseQIndex = 23;
// These tests encode at most one 8x8 transform with generated magnitudes no greater than 64.
private const int CoefficientSyntaxBufferLength = 256;
[Fact] [Fact]
public void NeighborArrayWritesEveryCoveredFourByFourEdgeUnit() public void NeighborArrayWritesEveryCoveredFourByFourEdgeUnit()
{ {
@ -405,7 +408,7 @@ public class Av1CoefficientsEntropyTests
IsLeftAvailable = true IsLeftAvailable = true
}; };
using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex); using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex, updateCdf: true);
Av1TileWriter.WritePaletteModeInfo( Av1TileWriter.WritePaletteModeInfo(
picture.Sequence, picture.Sequence,
picture, picture,
@ -544,7 +547,7 @@ public class Av1CoefficientsEntropyTests
transformBlocks.Fill(new Av1EncoderTransformBlockState { TransformType = Av1TransformType.Identity }); transformBlocks.Fill(new Av1EncoderTransformBlockState { TransformType = Av1TransformType.Identity });
Av1EncoderBlockStruct block = default; Av1EncoderBlockStruct block = default;
using Av1SymbolEncoder writer = new(Configuration.Default, 4096, BaseQIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 4096, BaseQIndex, updateCdf: true);
Av1TileWriter.EncodeTransformCoefficientsY( Av1TileWriter.EncodeTransformCoefficientsY(
picture, picture,
context, context,
@ -681,7 +684,7 @@ public class Av1CoefficientsEntropyTests
transforms.Left[leftIndex] = 16; transforms.Left[leftIndex] = 16;
picture.TransformFunctionContexts = [transforms]; picture.TransformFunctionContexts = [transforms];
using Av1SymbolEncoder writer = new(Configuration.Default, 64, BaseQIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
Av1TileWriter.WriteTransformSize( Av1TileWriter.WriteTransformSize(
picture, picture,
writer, writer,
@ -828,7 +831,7 @@ public class Av1CoefficientsEntropyTests
picture.Parent.FrameHeader.CdefParameters.BitCount = 2; picture.Parent.FrameHeader.CdefParameters.BitCount = 2;
picture.ModeInfoAllocation.Span[16].CdefStrength = 3; picture.ModeInfoAllocation.Span[16].CdefStrength = 3;
picture.ModeInfoAllocation.Span[20].CdefStrength = 1; picture.ModeInfoAllocation.Span[20].CdefStrength = 1;
using Av1SymbolEncoder writer = new(Configuration.Default, 16, BaseQIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
Av1TileWriter.WriteCdef( Av1TileWriter.WriteCdef(
picture.Sequence, picture.Sequence,
@ -936,7 +939,7 @@ public class Av1CoefficientsEntropyTests
width: 128, width: 128,
height: 64); height: 64);
using Av1SymbolEncoder writer = new(Configuration.Default, 512, BaseQIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 512, BaseQIndex, updateCdf: true);
Av1TileWriter.WriteSuperblock( Av1TileWriter.WriteSuperblock(
picture, picture,
@ -1010,8 +1013,8 @@ public class Av1CoefficientsEntropyTests
Av1PartitionType.Split Av1PartitionType.Split
]; ];
using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex); using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex); using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
foreach (Av1PartitionType decision in decisions) foreach (Av1PartitionType decision in decisions)
{ {
Av1TileWriter.EncodePartition( Av1TileWriter.EncodePartition(
@ -1080,8 +1083,8 @@ public class Av1CoefficientsEntropyTests
Av1ChromaPredictionMode.SmoothHorizontal Av1ChromaPredictionMode.SmoothHorizontal
]; ];
using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex); using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex); using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
foreach (Av1ChromaPredictionMode decision in decisions) foreach (Av1ChromaPredictionMode decision in decisions)
{ {
Av1TileWriter.EncodeIntraChromaMode( Av1TileWriter.EncodeIntraChromaMode(
@ -1123,7 +1126,7 @@ public class Av1CoefficientsEntropyTests
int[] leftContexts = new int[1]; int[] leftContexts = new int[1];
Av1TransformBlockContext transformBlockContext = default; Av1TransformBlockContext transformBlockContext = default;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, CoefficientSyntaxBufferLength, BaseQIndex, updateCdf: true);
Span<int> coefficientsBuffer = [1, 2, 3, 4, 5]; Span<int> coefficientsBuffer = [1, 2, 3, 4, 5];
Span<int> expected = new int[16]; Span<int> expected = new int[16];
Span<int> actuals = new int[16]; Span<int> actuals = new int[16];
@ -1193,7 +1196,7 @@ public class Av1CoefficientsEntropyTests
int[] leftContexts = new int[1]; int[] leftContexts = new int[1];
Av1TransformBlockContext transformBlockContext = default; Av1TransformBlockContext transformBlockContext = default;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, CoefficientSyntaxBufferLength, BaseQIndex, updateCdf: true);
Span<int> coefficientsBuffer = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; Span<int> coefficientsBuffer = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan;
for (int scanIndex = endOfBlock; scanIndex < scan.Length; scanIndex++) for (int scanIndex = endOfBlock; scanIndex < scan.Length; scanIndex++)
@ -1304,7 +1307,7 @@ public class Av1CoefficientsEntropyTests
int[] leftContexts = new int[transformSize.Get4x4HighCount()]; int[] leftContexts = new int[transformSize.Get4x4HighCount()];
Av1TransformBlockContext transformBlockContext = default; Av1TransformBlockContext transformBlockContext = default;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, CoefficientSyntaxBufferLength, BaseQIndex, updateCdf: true);
int coefficientCount = blockSize.GetHeight() * blockSize.GetWidth(); int coefficientCount = blockSize.GetHeight() * blockSize.GetWidth();
ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan;
Span<int> coefficientsBuffer = new int[coefficientCount]; Span<int> coefficientsBuffer = new int[coefficientCount];

90
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
@ -20,6 +21,9 @@ public class Av1EntropyTests
{ {
private const int BaseQIndex = 23; private const int BaseQIndex = 23;
// Short syntax round trips encode only their small in-method symbol vectors.
private const int ShortSyntaxBufferLength = 64;
[Fact] [Fact]
public void ProbabilityCostTableMatchesDefinition() public void ProbabilityCostTableMatchesDefinition()
{ {
@ -158,7 +162,7 @@ public class Av1EntropyTests
const int BlockSkipContext = 2; const int BlockSkipContext = 2;
const int TransformSkipContext = 0; const int TransformSkipContext = 0;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
using Av1SymbolEncoder encoder = new(Configuration.Default, 256, QIndex); using Av1SymbolEncoder encoder = new(Configuration.Default, 256, QIndex, updateCdf: true);
int emptyTransformRate = encoder.GetTransformBlockSkipCost( int emptyTransformRate = encoder.GetTransformBlockSkipCost(
true, true,
TransformSize, TransformSize,
@ -293,7 +297,7 @@ public class Av1EntropyTests
[Fact] [Fact]
public void SymbolEncoderCostTracksWrittenLumaMode() public void SymbolEncoderCostTracksWrittenLumaMode()
{ {
using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex); using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
Av1Distribution expected = Av1DefaultDistributions.KeyFrameYMode[0][0]; Av1Distribution expected = Av1DefaultDistributions.KeyFrameYMode[0][0];
Assert.Equal( Assert.Equal(
@ -563,8 +567,8 @@ public class Av1EntropyTests
coefficients[scan[0]] = -25; coefficients[scan[0]] = -25;
coefficients[scan[2]] = 3; coefficients[scan[2]] = 3;
coefficients[scan[3]] = 1; coefficients[scan[3]] = 1;
using Av1SymbolEncoder actualEncoder = new(Configuration.Default, 64, BaseQIndex); using Av1SymbolEncoder actualEncoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
using Av1SymbolEncoder expectedEncoder = new(Configuration.Default, 64, BaseQIndex); using Av1SymbolEncoder expectedEncoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
int initialCost = actualEncoder.GetCoefficientCost( int initialCost = actualEncoder.GetCoefficientCost(
transformSize, transformSize,
@ -743,7 +747,7 @@ public class Av1EntropyTests
[Fact] [Fact]
public void SymbolWriterMatchesCurrentLibaomCarryRegression() public void SymbolWriterMatchesCurrentLibaomCarryRegression()
{ {
using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: false); using Av1SymbolWriter writer = new(Configuration.Default, ShortSyntaxBufferLength, updateCdf: false);
writer.WriteBoolean(false, 16_384); writer.WriteBoolean(false, 16_384);
writer.WriteBoolean(false, 16_384); writer.WriteBoolean(false, 16_384);
writer.WriteBoolean(true, 512); writer.WriteBoolean(true, 512);
@ -755,22 +759,22 @@ public class Av1EntropyTests
} }
[Fact] [Fact]
public void SymbolWriterUsesOneByteOfScratchPerEstimatedOutputByte() public void SymbolWriterRentsFixedOutputBuffer()
{ {
const int initialSize = 257; const int bufferLength = 257;
TestMemoryAllocator allocator = new(); TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging(); allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone(); Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator; configuration.MemoryAllocator = allocator;
TestMemoryAllocator.AllocationRequest allocation; TestMemoryAllocator.AllocationRequest allocation;
using (Av1SymbolWriter writer = new(configuration, initialSize, updateCdf: false)) using (Av1SymbolWriter writer = new(configuration, bufferLength, updateCdf: false))
{ {
writer.WriteLiteral(false); writer.WriteLiteral(false);
allocation = Assert.Single(allocator.AllocationLog); allocation = Assert.Single(allocator.AllocationLog);
Assert.Equal(typeof(byte), allocation.ElementType); Assert.Equal(typeof(byte), allocation.ElementType);
Assert.Equal(initialSize, allocation.Length); Assert.Equal(bufferLength, allocation.Length);
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
@ -778,38 +782,31 @@ public class Av1EntropyTests
} }
[Fact] [Fact]
public void SymbolWriterTransfersExistingOutputAllocationWithoutCopy() public void SymbolWriterExposesExistingOutputAllocationWithoutCopy()
{ {
const int initialSize = 257; const int bufferLength = 257;
TestMemoryAllocator allocator = new(); TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging(); allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone(); Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator; configuration.MemoryAllocator = allocator;
TestMemoryAllocator.AllocationRequest allocation; TestMemoryAllocator.AllocationRequest allocation;
IMemoryOwner<byte> encoded;
int length;
using (Av1SymbolWriter writer = new(configuration, initialSize, updateCdf: false)) using (Av1SymbolWriter writer = new(configuration, bufferLength, updateCdf: false))
{ {
writer.WriteBoolean(false, 16_384); writer.WriteBoolean(false, 16_384);
writer.WriteBoolean(false, 16_384); writer.WriteBoolean(false, 16_384);
writer.WriteBoolean(true, 512); writer.WriteBoolean(true, 512);
writer.WriteBoolean(false, 8_192); writer.WriteBoolean(false, 8_192);
allocation = Assert.Single(allocator.AllocationLog); allocation = Assert.Single(allocator.AllocationLog);
encoded = writer.Exit(out length); ReadOnlyMemory<byte> encoded = writer.Exit(out int length);
Assert.Equal(2, length);
Assert.Equal(length, encoded.Length);
Assert.Equal(63, encoded.Span[0]);
Assert.Single(allocator.AllocationLog); Assert.Single(allocator.AllocationLog);
Assert.Empty(allocator.ReturnLog); Assert.Empty(allocator.ReturnLog);
} }
Assert.Empty(allocator.ReturnLog);
using (encoded)
{
Assert.Equal(2, length);
Assert.Equal(initialSize, encoded.Memory.Length);
Assert.Equal(63, encoded.Memory.Span[0]);
}
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.AllocationId, returned.AllocationId);
} }
@ -823,7 +820,7 @@ public class Av1EntropyTests
configuration.MemoryAllocator = allocator; configuration.MemoryAllocator = allocator;
Span<int> coefficients = stackalloc int[16]; Span<int> coefficients = stackalloc int[16];
using (Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex)) using (Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex, updateCdf: true))
{ {
TestMemoryAllocator.AllocationRequest outputScratch = Assert.Single(allocator.AllocationLog); TestMemoryAllocator.AllocationRequest outputScratch = Assert.Single(allocator.AllocationLog);
Assert.Equal(typeof(byte), outputScratch.ElementType); Assert.Equal(typeof(byte), outputScratch.ElementType);
@ -976,7 +973,7 @@ public class Av1EntropyTests
uint[] values = new uint[writeCount]; uint[] values = new uint[writeCount];
Array.Fill(values, value); Array.Fill(values, value);
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolWriter writer = new(configuration, (writeCount * bitCount) >> 3); using Av1SymbolWriter writer = new(configuration, ShortSyntaxBufferLength, updateCdf: true);
// Act // Act
for (int i = 0; i < writeCount; i++) for (int i = 0; i < writeCount; i++)
@ -1070,7 +1067,7 @@ public class Av1EntropyTests
public void RoundTripUniformPaletteIndices() public void RoundTripUniformPaletteIndices()
{ {
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex, updateCdf: true);
for (int valueCount = 2; valueCount <= Av1Constants.PaletteMaxSize; valueCount++) for (int valueCount = 2; valueCount <= Av1Constants.PaletteMaxSize; valueCount++)
{ {
@ -1095,7 +1092,7 @@ public class Av1EntropyTests
public void RoundTripPaletteSymbols() public void RoundTripPaletteSymbols()
{ {
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 256, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, 256, BaseQIndex, updateCdf: true);
for (int blockSizeContext = 0; blockSizeContext < 7; blockSizeContext++) for (int blockSizeContext = 0; blockSizeContext < 7; blockSizeContext++)
{ {
@ -1213,7 +1210,7 @@ public class Av1EntropyTests
ushort[] uColors = [17, 51, 100]; ushort[] uColors = [17, 51, 100];
ushort[] deltaVColors = [1, 2, 1]; ushort[] deltaVColors = [1, 2, 1];
ushort[] rawVColors = [0, (ushort)(1 << (bitDepth - 1)), 0]; ushort[] rawVColors = [0, (ushort)(1 << (bitDepth - 1)), 0];
using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex); using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex, updateCdf: true);
encoder.WritePaletteYColors(colorCache, yColors, bitDepth); encoder.WritePaletteYColors(colorCache, yColors, bitDepth);
encoder.WritePaletteUvColors(colorCache, uColors, deltaVColors, bitDepth); encoder.WritePaletteUvColors(colorCache, uColors, deltaVColors, bitDepth);
encoder.WritePaletteUvColors(colorCache, uColors, rawVColors, bitDepth); encoder.WritePaletteUvColors(colorCache, uColors, rawVColors, bitDepth);
@ -1371,7 +1368,7 @@ public class Av1EntropyTests
using Buffer2D<byte> decoded = configuration.MemoryAllocator.Allocate2D<byte>(Width, Height); using Buffer2D<byte> decoded = configuration.MemoryAllocator.Allocate2D<byte>(Width, Height);
Buffer2DRegion<byte> sourceRegion = new(source); Buffer2DRegion<byte> sourceRegion = new(source);
Buffer2DRegion<byte> decodedRegion = new(decoded); Buffer2DRegion<byte> decodedRegion = new(decoded);
using Av1SymbolEncoder encoder = new(configuration, 512, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, 512, BaseQIndex, updateCdf: true);
for (int paletteSize = 2; paletteSize <= Av1Constants.PaletteMaxSize; paletteSize++) for (int paletteSize = 2; paletteSize <= Av1Constants.PaletteMaxSize; paletteSize++)
{ {
for (int plane = 0; plane < 2; plane++) for (int plane = 0; plane < 2; plane++)
@ -1481,7 +1478,7 @@ public class Av1EntropyTests
{ {
// Assign // Assign
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1PartitionType[] values = [ Av1PartitionType[] values = [
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None,
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None, Av1PartitionType.None]; Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None, Av1PartitionType.None];
@ -1512,7 +1509,7 @@ public class Av1EntropyTests
// Assign // Assign
Av1BlockSize blockSize = (Av1BlockSize)size; Av1BlockSize blockSize = (Av1BlockSize)size;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1PartitionType[] values = [ Av1PartitionType[] values = [
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal,
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal, Av1PartitionType.Horizontal]; Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal, Av1PartitionType.Horizontal];
@ -1543,7 +1540,7 @@ public class Av1EntropyTests
// Assign // Assign
Av1BlockSize blockSize = (Av1BlockSize)size; Av1BlockSize blockSize = (Av1BlockSize)size;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1PartitionType[] values = [ Av1PartitionType[] values = [
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical,
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical, Av1PartitionType.Vertical]; Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical, Av1PartitionType.Vertical];
@ -1575,7 +1572,7 @@ public class Av1EntropyTests
{ {
// Assign // Assign
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
bool[] values = [true, true, false, false, false, false, false, false, true]; bool[] values = [true, true, false, false, false, false, false, false, true];
bool[] actuals = new bool[values.Length]; bool[] actuals = new bool[values.Length];
@ -1604,7 +1601,7 @@ public class Av1EntropyTests
// Assign // Assign
Av1TransformSize transformSizeContext = (Av1TransformSize)transformContext; Av1TransformSize transformSizeContext = (Av1TransformSize)transformContext;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
bool[] values = [true, true, false, false, false, false, false, false, true]; bool[] values = [true, true, false, false, false, false, false, false, true];
bool[] actuals = new bool[values.Length]; bool[] actuals = new bool[values.Length];
@ -1637,7 +1634,7 @@ public class Av1EntropyTests
Av1FilterIntraMode filterIntraMode = (Av1FilterIntraMode)intraMode; Av1FilterIntraMode filterIntraMode = (Av1FilterIntraMode)intraMode;
Av1PredictionMode intraDirection = (Av1PredictionMode)intraDir; Av1PredictionMode intraDirection = (Av1PredictionMode)intraDir;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
// TODO: Include AdstFlipAdst, which is currently mapped to Identity. // TODO: Include AdstFlipAdst, which is currently mapped to Identity.
Av1TransformType[] values = [ Av1TransformType[] values = [
@ -1680,8 +1677,8 @@ public class Av1EntropyTests
Av1DefaultDistributions.InterExtendedTransform[extendedSet][(int)squareTransformSize]; Av1DefaultDistributions.InterExtendedTransform[extendedSet][(int)squareTransformSize];
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder costEncoder = new(configuration, 100 / 8, BaseQIndex, updateCdf: false); using Av1SymbolEncoder costEncoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: false);
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
int transformTypeCount = Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType); int transformTypeCount = Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType);
for (int symbol = 0; symbol < transformTypeCount; symbol++) for (int symbol = 0; symbol < transformTypeCount; symbol++)
@ -1737,7 +1734,7 @@ public class Av1EntropyTests
Av1PlaneType planeType = (Av1PlaneType)plane; Av1PlaneType planeType = (Av1PlaneType)plane;
Av1TransformClass transformClass = (Av1TransformClass)txClass; Av1TransformClass transformClass = (Av1TransformClass)txClass;
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
int[] values = [1, 2, 3, 4, 5]; int[] values = [1, 2, 3, 4, 5];
int[] actuals = new int[values.Length]; int[] actuals = new int[values.Length];
@ -1765,11 +1762,14 @@ public class Av1EntropyTests
{ {
// Assign // Assign
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex);
int[] values = Enumerable.Range(0, 16384).ToArray(); int[] values = Enumerable.Range(0, 16384).ToArray();
int[] actuals = new int[values.Length]; int[] actuals = new int[values.Length];
// Reserve the longest code for every value so this broad corpus cannot exhaust the fixed entropy output.
int maximumCodeBitCount = (BitOperations.Log2((uint)values.Length) * 2) + 1;
int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * maximumCodeBitCount), 8);
using Av1SymbolEncoder encoder = new(configuration, bufferLength, BaseQIndex, updateCdf: true);
// Act // Act
foreach (int value in values) foreach (int value in values)
{ {
@ -1797,7 +1797,7 @@ public class Av1EntropyTests
// Assign // Assign
int[] values = [3, 6, 7, 0, 2, 0, 2, 1, 1]; int[] values = [3, 6, 7, 0, 2, 0, 2, 1, 1];
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
int[] actuals = new int[values.Length]; int[] actuals = new int[values.Length];
// Act // Act
@ -1824,7 +1824,7 @@ public class Av1EntropyTests
// Assign // Assign
int[] values = [3, 6, -7, -8, -2, 0, 2, 1, -1]; int[] values = [3, 6, -7, -8, -2, 0, 2, 1, -1];
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
int[] actuals = new int[values.Length]; int[] actuals = new int[values.Length];
// Act // Act
@ -1855,7 +1855,7 @@ public class Av1EntropyTests
Av1FilterIntraMode.DC, Av1FilterIntraMode.Vertical, Av1FilterIntraMode.DC, Av1FilterIntraMode.Paeth, Av1FilterIntraMode.DC, Av1FilterIntraMode.Vertical, Av1FilterIntraMode.DC, Av1FilterIntraMode.Paeth,
Av1FilterIntraMode.AllFilterIntraModes, Av1FilterIntraMode.Directional157, Av1FilterIntraMode.DC, Av1FilterIntraMode.Directional157]; Av1FilterIntraMode.AllFilterIntraModes, Av1FilterIntraMode.Directional157, Av1FilterIntraMode.DC, Av1FilterIntraMode.Directional157];
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1FilterIntraMode[] actuals = new Av1FilterIntraMode[values.Length]; Av1FilterIntraMode[] actuals = new Av1FilterIntraMode[values.Length];
// Act // Act
@ -1882,7 +1882,7 @@ public class Av1EntropyTests
// Assign // Assign
bool[] values = [true, true, false, true, false, false, false]; bool[] values = [true, true, false, true, false, false, false];
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
bool[] actuals = new bool[values.Length]; bool[] actuals = new bool[values.Length];
Assert.Equal(51, encoder.GetUseIntraBlockCopyCost(false)); Assert.Equal(51, encoder.GetUseIntraBlockCopyCost(false));
@ -1934,7 +1934,7 @@ public class Av1EntropyTests
int[] expectedCosts = [1440, 1661, 5231, 5807, 16955, 31656]; int[] expectedCosts = [1440, 1661, 5231, 5807, 16955, 31656];
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex); using Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex, updateCdf: true);
// These current-libaom costs cover every joint, both signs, class zero, and large-class offset bits. // These current-libaom costs cover every joint, both signs, class zero, and large-class offset bits.
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)

8
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs

@ -190,7 +190,7 @@ public class Av1IntraBlockCopyTests
picture.Parent.Common.ModeInfoRowCount, picture.Parent.Common.ModeInfoRowCount,
picture.Parent.Common.ModeInfoColumnCount); picture.Parent.Common.ModeInfoColumnCount);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, 0); using Av1SymbolEncoder writer = new(Configuration.Default, 64, 0, updateCdf: true);
Av1TileWriter.WriteIntraBlockCopyInfo( Av1TileWriter.WriteIntraBlockCopyInfo(
picture, picture,
writer, writer,
@ -302,7 +302,7 @@ public class Av1IntraBlockCopyTests
Av1PictureControlSet picture = pictureBuffer.Picture; Av1PictureControlSet picture = pictureBuffer.Picture;
picture.IntraBlockCopySearch.Initialize<byte, Av1IntraSuperblockEncoder.ByteOperator>(sourceLuma); picture.IntraBlockCopySearch.Initialize<byte, Av1IntraSuperblockEncoder.ByteOperator>(sourceLuma);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
Span<Av1MotionVector> candidates = stackalloc Av1MotionVector[2]; Span<Av1MotionVector> candidates = stackalloc Av1MotionVector[2];
Av1MotionVector reference = new(0, -2560); Av1MotionVector reference = new(0, -2560);
int candidateCount = picture.IntraBlockCopySearch.FindCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>( int candidateCount = picture.IntraBlockCopySearch.FindCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>(
@ -397,7 +397,7 @@ public class Av1IntraBlockCopyTests
codedReconstructionLuma, codedReconstructionLuma,
new Point(15, 120))); new Point(15, 120)));
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
Span<Av1MotionVector> candidates = stackalloc Av1MotionVector[2]; Span<Av1MotionVector> candidates = stackalloc Av1MotionVector[2];
int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch
.FindPixelCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>( .FindPixelCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>(
@ -481,7 +481,7 @@ public class Av1IntraBlockCopyTests
Buffer2DRegion<byte> codedSourceLuma = source.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion<byte> codedSourceLuma = source.Frame.CodedView.GetPlane(Av1Plane.Y);
Buffer2DRegion<byte> codedReconstructionLuma = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion<byte> codedReconstructionLuma = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
Span<Av1MotionVector> candidates = stackalloc Av1MotionVector[2]; Span<Av1MotionVector> candidates = stackalloc Av1MotionVector[2];
int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch
.FindPixelCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>( .FindPixelCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>(

217
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

@ -211,7 +211,7 @@ public class Av1IntraSuperblockEncoderTests
SuperblockOrigin = default SuperblockOrigin = default
}; };
using Av1SymbolEncoder writer = new(Configuration.Default, 512, 73); using Av1SymbolEncoder writer = new(Configuration.Default, 512, 73, updateCdf: true);
Av1TileWriter.WriteSuperblock( Av1TileWriter.WriteSuperblock(
picture, picture,
entropyContext, entropyContext,
@ -255,15 +255,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace tileSuperblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace tileSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace tileBlockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace tileBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder tileSymbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, tilePicture.Picture,
512);
Av1IntraTileWriter tileWriter = new(
tileSymbolEncoder,
source.Frame, source.Frame,
tileReconstruction.Frame, tileReconstruction.Frame,
tilePicture.Picture, tilePicture.Picture,
tileCoefficients, tileCoefficients,
tileSuperblockWorkspace, tileSuperblockWorkspace,
tileBlockWorkspace, tileBlockWorkspace,
initialSize: 512,
effort: 5); effort: 5);
// The production tile traversal must be byte-identical to the explicit analyze-then-write composition above. // The production tile traversal must be byte-identical to the explicit analyze-then-write composition above.
@ -364,7 +367,7 @@ public class Av1IntraSuperblockEncoderTests
SuperblockOrigin = default SuperblockOrigin = default
}; };
using Av1SymbolEncoder writer = new(Configuration.Default, 256, 37); using Av1SymbolEncoder writer = new(Configuration.Default, 256, 37, updateCdf: true);
Av1TileWriter.WriteSuperblock( Av1TileWriter.WriteSuperblock(
picture, picture,
entropyContext, entropyContext,
@ -406,15 +409,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace liveSuperblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace liveSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace liveBlockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace liveBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter liveTileWriter = new( using Av1SymbolEncoder liveSymbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, livePicture.Picture,
256);
Av1IntraTileWriter liveTileWriter = new(
liveSymbolEncoder,
source.Frame, source.Frame,
liveReconstruction.Frame, liveReconstruction.Frame,
livePicture.Picture, livePicture.Picture,
liveCoefficients, liveCoefficients,
liveSuperblockWorkspace, liveSuperblockWorkspace,
liveBlockWorkspace, liveBlockWorkspace,
initialSize: 256,
effort: 5); effort: 5);
Assert.True(precomputedTile.GetSpan().SequenceEqual(liveTileWriter.GetTileData(0))); Assert.True(precomputedTile.GetSpan().SequenceEqual(liveTileWriter.GetTileData(0)));
@ -531,15 +537,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace tileSuperblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace tileSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace tileBlockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace tileBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder tileSymbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, tilePicture.Picture,
256);
Av1IntraTileWriter tileWriter = new(
tileSymbolEncoder,
source.Frame, source.Frame,
tileReconstruction.Frame, tileReconstruction.Frame,
tilePicture.Picture, tilePicture.Picture,
tileCoefficients, tileCoefficients,
tileSuperblockWorkspace, tileSuperblockWorkspace,
tileBlockWorkspace, tileBlockWorkspace,
initialSize: 256,
effort: 5); effort: 5);
Assert.NotEqual(0, tileWriter.GetTileData(0).Length); Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
@ -603,7 +612,7 @@ public class Av1IntraSuperblockEncoderTests
int[] costs = new int[2]; int[] costs = new int[2];
BlockCostRecorder blockEncoder = new(costs, QIndex); BlockCostRecorder blockEncoder = new(costs, QIndex);
using Av1SymbolEncoder writer = new(Configuration.Default, 256, QIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 256, QIndex, updateCdf: true);
Av1TileWriter.WriteSuperblock( Av1TileWriter.WriteSuperblock(
picture.Picture, picture.Picture,
entropyContext, entropyContext,
@ -696,7 +705,7 @@ public class Av1IntraSuperblockEncoderTests
}; };
PaletteBlockEncoder blockEncoder = new(workspace, QIndex, mapVariant); PaletteBlockEncoder blockEncoder = new(workspace, QIndex, mapVariant);
using Av1SymbolEncoder writer = new(Configuration.Default, 128, QIndex); using Av1SymbolEncoder writer = new(Configuration.Default, 128, QIndex, updateCdf: true);
Av1TileWriter.WriteSuperblock( Av1TileWriter.WriteSuperblock(
picture, picture,
entropyContext, entropyContext,
@ -739,16 +748,15 @@ public class Av1IntraSuperblockEncoderTests
(byte)224, (byte)224,
32, 32,
224, 224,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 256,
effort: 5)); effort: 5));
AssertProductionTileSelectsExactLumaPalette( AssertProductionTileSelectsExactLumaPalette(
@ -761,16 +769,15 @@ public class Av1IntraSuperblockEncoderTests
(ushort)3584, (ushort)3584,
512, 512,
3584, 3584,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 256,
effort: 5)); effort: 5));
AssertProductionTileSelectsExactLumaPalette( AssertProductionTileSelectsExactLumaPalette(
@ -783,16 +790,15 @@ public class Av1IntraSuperblockEncoderTests
(byte)208, (byte)208,
48, 48,
208, 208,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 256,
effort: 5)); effort: 5));
} }
@ -809,16 +815,15 @@ public class Av1IntraSuperblockEncoderTests
(byte)192, (byte)192,
64, 64,
192, 192,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 256,
effort: 6)); effort: 6));
AssertProductionTileSelectsExactLumaPalette( AssertProductionTileSelectsExactLumaPalette(
@ -831,16 +836,15 @@ public class Av1IntraSuperblockEncoderTests
(ushort)3072, (ushort)3072,
1024, 1024,
3072, 3072,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 256,
effort: 6)); effort: 6));
} }
@ -938,15 +942,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, picture.Picture,
256);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 256,
effort: 5); effort: 5);
ref Av1MacroBlockModeInfo mode = ref picture.Picture.GetMacroBlockModeInfo(default); ref Av1MacroBlockModeInfo mode = ref picture.Picture.GetMacroBlockModeInfo(default);
@ -1151,15 +1158,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, picture.Picture,
512);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 5); effort: 5);
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2)); ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
@ -1265,15 +1275,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, picture.Picture,
512);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 5); effort: 5);
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2)); ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
@ -1306,16 +1319,15 @@ public class Av1IntraSuperblockEncoderTests
=> VerifyProductionTileSelectsChromaFromReconstructedLuma<byte>( => VerifyProductionTileSelectsChromaFromReconstructedLuma<byte>(
colorFormatValue, colorFormatValue,
8, 8,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new( new(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 5)); effort: 5));
[Theory] [Theory]
@ -1331,16 +1343,15 @@ public class Av1IntraSuperblockEncoderTests
=> VerifyProductionTileSelectsChromaFromReconstructedLuma<ushort>( => VerifyProductionTileSelectsChromaFromReconstructedLuma<ushort>(
colorFormatValue, colorFormatValue,
bitDepth, bitDepth,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new( new(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 5)); effort: 5));
private static void VerifyProductionTileSelectsChromaFromReconstructedLuma<TSample>( private static void VerifyProductionTileSelectsChromaFromReconstructedLuma<TSample>(
@ -1352,6 +1363,7 @@ public class Av1IntraSuperblockEncoderTests
const int Width = 16; const int Width = 16;
const int Height = 16; const int Height = 16;
const int QIndex = 1; const int QIndex = 1;
const int TileBufferLength = 512;
const int AlphaU = 16; const int AlphaU = 16;
const int AlphaV = -16; const int AlphaV = -16;
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue; Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
@ -1426,7 +1438,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace pilotBlockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace pilotBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter pilotWriter = createWriter( using Av1SymbolEncoder pilotSymbolEncoder = CreateTileSymbolEncoder(
pilotPicture.Picture,
TileBufferLength);
Av1IntraTileWriter pilotWriter = createWriter(
pilotSymbolEncoder,
pilotSource.Frame, pilotSource.Frame,
pilotReconstruction.Frame, pilotReconstruction.Frame,
pilotPicture.Picture, pilotPicture.Picture,
@ -1528,7 +1545,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = createWriter( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
TileBufferLength);
Av1IntraTileWriter tileWriter = createWriter(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
@ -1584,16 +1606,15 @@ public class Av1IntraSuperblockEncoderTests
filterIntraModeValue, filterIntraModeValue,
8, 8,
false, false,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new( new(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 5), effort: 5),
static (mode, destination, stride, above, left, width, height, _, scratch) => static (mode, destination, stride, above, left, width, height, _, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode) Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1617,16 +1638,15 @@ public class Av1IntraSuperblockEncoderTests
filterIntraModeValue, filterIntraModeValue,
bitDepth, bitDepth,
false, false,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new( new(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 5), effort: 5),
static (mode, destination, stride, above, left, width, height, sampleBitDepth, scratch) => static (mode, destination, stride, above, left, width, height, sampleBitDepth, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode) Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1646,16 +1666,15 @@ public class Av1IntraSuperblockEncoderTests
(int)Av1FilterIntraMode.DC, (int)Av1FilterIntraMode.DC,
8, 8,
true, true,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new( new(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 6), effort: 6),
static (mode, destination, stride, above, left, width, height, _, scratch) => static (mode, destination, stride, above, left, width, height, _, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode) Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1669,16 +1688,15 @@ public class Av1IntraSuperblockEncoderTests
(int)Av1FilterIntraMode.DC, (int)Av1FilterIntraMode.DC,
bitDepth, bitDepth,
true, true,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new( new(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 512,
effort: 6), effort: 6),
static (mode, destination, stride, above, left, width, height, sampleBitDepth, scratch) => static (mode, destination, stride, above, left, width, height, sampleBitDepth, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode) Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1703,6 +1721,7 @@ public class Av1IntraSuperblockEncoderTests
const int Width = 16; const int Width = 16;
const int Height = 16; const int Height = 16;
const int QIndex = 37; const int QIndex = 37;
const int TileBufferLength = 512;
const int TargetX = 8; const int TargetX = 8;
const int TargetY = 8; const int TargetY = 8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
@ -1769,7 +1788,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace pilotBlockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace pilotBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter pilotWriter = createWriter( using Av1SymbolEncoder pilotSymbolEncoder = CreateTileSymbolEncoder(
pilotPicture.Picture,
TileBufferLength);
Av1IntraTileWriter pilotWriter = createWriter(
pilotSymbolEncoder,
pilotSource.Frame, pilotSource.Frame,
pilotReconstruction.Frame, pilotReconstruction.Frame,
pilotPicture.Picture, pilotPicture.Picture,
@ -1926,7 +1950,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = createWriter( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
TileBufferLength);
Av1IntraTileWriter tileWriter = createWriter(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
@ -2125,15 +2154,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, picture.Picture,
2048);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 2048,
effort: 5); effort: 5);
ref Av1MacroBlockModeInfo topRightBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(0, 2)); ref Av1MacroBlockModeInfo topRightBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(0, 2));
@ -2150,32 +2182,30 @@ public class Av1IntraSuperblockEncoderTests
Av1BitDepth.EightBit, Av1BitDepth.EightBit,
8, 8,
static value => (byte)value, static value => (byte)value,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 4096,
effort: 5)); effort: 5));
VerifyProductionTileSelectsIntraBlockCopy( VerifyProductionTileSelectsIntraBlockCopy(
Av1BitDepth.TwelveBit, Av1BitDepth.TwelveBit,
12, 12,
static value => (ushort)(value << 4), static value => (ushort)(value << 4),
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) => static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter( new Av1IntraTileWriter(
Configuration.Default, writer,
source, source,
reconstruction, reconstruction,
picture, picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 4096,
effort: 5)); effort: 5));
} }
@ -2189,6 +2219,7 @@ public class Av1IntraSuperblockEncoderTests
const int Width = 328; const int Width = 328;
const int Height = 8; const int Height = 8;
const int QIndex = 1; const int QIndex = 1;
const int TileBufferLength = 4096;
const int ReferenceColumn = 0; const int ReferenceColumn = 0;
const int TargetColumn = 320; const int TargetColumn = 320;
ObuColorConfig colorConfig = new() ObuColorConfig colorConfig = new()
@ -2268,7 +2299,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = createTileWriter( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
TileBufferLength);
Av1IntraTileWriter tileWriter = createTileWriter(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
@ -2396,15 +2432,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, picture.Picture,
4096);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 4096,
effort: 5); effort: 5);
Point targetModeInfoPosition = new(TargetColumn >> Av1Constants.ModeInfoSizeLog2, 0); Point targetModeInfoPosition = new(TargetColumn >> Av1Constants.ModeInfoSizeLog2, 0);
@ -2507,15 +2546,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
Configuration.Default, picture.Picture,
4096);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
coefficients, coefficients,
superblockWorkspace, superblockWorkspace,
blockWorkspace, blockWorkspace,
initialSize: 4096,
effort: 5); effort: 5);
Assert.Equal(4, coefficients.SuperblockCount); Assert.Equal(4, coefficients.SuperblockCount);
@ -2672,6 +2714,7 @@ public class Av1IntraSuperblockEncoderTests
where TSample : unmanaged, IBinaryInteger<TSample> where TSample : unmanaged, IBinaryInteger<TSample>
{ {
const int QIndex = 37; const int QIndex = 37;
const int TileBufferLength = 256;
ObuColorConfig colorConfig = new() ObuColorConfig colorConfig = new()
{ {
IsMonochrome = true, IsMonochrome = true,
@ -2766,7 +2809,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = createTileWriter( using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
TileBufferLength);
Av1IntraTileWriter tileWriter = createTileWriter(
symbolEncoder,
source.Frame, source.Frame,
reconstruction.Frame, reconstruction.Frame,
picture.Picture, picture.Picture,
@ -2980,7 +3028,24 @@ public class Av1IntraSuperblockEncoderTests
} }
} }
/// <summary>
/// Creates the operation owner for a production tile's entropy state and bounded output memory.
/// </summary>
/// <param name="picture">The picture supplying quantization and CDF-update settings.</param>
/// <param name="bufferLength">The bounded output allocation length in bytes.</param>
/// <returns>The symbol encoder that must remain alive while the tile output is consumed.</returns>
private static Av1SymbolEncoder CreateTileSymbolEncoder(Av1PictureControlSet picture, int bufferLength)
{
ObuFrameHeader frameHeader = picture.Parent.FrameHeader;
return new Av1SymbolEncoder(
Configuration.Default,
bufferLength,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
}
private delegate Av1IntraTileWriter TileWriterFactory<TSample>( private delegate Av1IntraTileWriter TileWriterFactory<TSample>(
Av1SymbolEncoder writer,
Av1EncoderFrame<TSample> source, Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> reconstruction, Av1EncoderFrame<TSample> reconstruction,
Av1PictureControlSet picture, Av1PictureControlSet picture,

10
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs

@ -88,7 +88,7 @@ public class Av1InverseTransformTests
private static void AssertTwelveBitWideIntermediateParity() private static void AssertTwelveBitWideIntermediateParity()
{ {
const int cosBit = 12; const int cosBit = 12;
Av1TransformStageRange stageRange = default; InlineArray12<byte> stageRange = default;
for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++) for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++)
{ {
stageRange[index] = 20; stageRange[index] = 20;
@ -182,7 +182,7 @@ public class Av1InverseTransformTests
Vector128<int> expected128, Vector128<int> expected128,
Vector256<int> input256, Vector256<int> input256,
Vector256<int> expected256, Vector256<int> expected256,
Av1TransformStageRange stageRange) InlineArray12<byte> stageRange)
where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator
{ {
const int cosBit = 12; const int cosBit = 12;
@ -252,8 +252,8 @@ public class Av1InverseTransformTests
Av1TransformSize.Size16x16, Av1TransformSize.Size16x16,
bitDepth); bitDepth);
Av1TransformStageRange configuredRowRange = config.StageRangeRow; InlineArray12<byte> configuredRowRange = config.StageRangeRow;
Av1TransformStageRange configuredColumnRange = config.StageRangeColumn; InlineArray12<byte> configuredColumnRange = config.StageRangeColumn;
for (int index = 0; index < config.StageNumberRow; index++) for (int index = 0; index < config.StageNumberRow; index++)
{ {
@ -600,7 +600,7 @@ public class Av1InverseTransformTests
where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator
{ {
const int cosBit = 12; const int cosBit = 12;
Av1TransformStageRange stageRange = default; InlineArray12<byte> stageRange = default;
for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++) for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++)
{ {

14
tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

@ -586,8 +586,8 @@ public class ObuFrameHeaderTests
public void ReadFrameHeaderRejectsIntraOnlyAllSlotsRefresh() public void ReadFrameHeaderRejectsIntraOnlyAllSlotsRefresh()
{ {
byte[] sequenceHeader = CreateNonReducedSequenceHeaderObu(default); byte[] sequenceHeader = CreateNonReducedSequenceHeaderObu(default);
using AutoExpandingMemory<byte> frameMemory = new(Configuration.Default, 8); byte[] framePayload = new byte[2];
Av1BitStreamWriter frameWriter = new(frameMemory); Av1BitStreamWriter frameWriter = new(framePayload);
frameWriter.WriteBoolean(false); frameWriter.WriteBoolean(false);
frameWriter.WriteLiteral((uint)ObuFrameType.IntraOnlyFrame, 2); frameWriter.WriteLiteral((uint)ObuFrameType.IntraOnlyFrame, 2);
@ -605,7 +605,7 @@ public class ObuFrameHeaderTests
int frameObuOffset = sequenceHeader.Length; int frameObuOffset = sequenceHeader.Length;
bitStream[frameObuOffset] = (byte)(((byte)ObuType.FrameHeader << 3) | 0x02); bitStream[frameObuOffset] = (byte)(((byte)ObuType.FrameHeader << 3) | 0x02);
bitStream[frameObuOffset + 1] = (byte)framePayloadLength; bitStream[frameObuOffset + 1] = (byte)framePayloadLength;
frameMemory.GetSpan(framePayloadLength).CopyTo(bitStream.AsSpan(frameObuOffset + 2)); framePayload.AsSpan(0, framePayloadLength).CopyTo(bitStream.AsSpan(frameObuOffset + 2));
Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream)); Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream));
} }
@ -796,6 +796,8 @@ public class ObuFrameHeaderTests
/// <returns>The complete explicitly sized sequence-header OBU.</returns> /// <returns>The complete explicitly sized sequence-header OBU.</returns>
private static byte[] CreateNonReducedSequenceHeaderObu(InvalidSequenceHeaderCase invalidCase) private static byte[] CreateNonReducedSequenceHeaderObu(InvalidSequenceHeaderCase invalidCase)
{ {
const int SequenceHeaderBufferLength = 32;
bool hasTimingInfo = invalidCase is bool hasTimingInfo = invalidCase is
InvalidSequenceHeaderCase.ZeroDisplayTick or InvalidSequenceHeaderCase.ZeroDisplayTick or
InvalidSequenceHeaderCase.ZeroTimeScale or InvalidSequenceHeaderCase.ZeroTimeScale or
@ -807,8 +809,8 @@ public class ObuFrameHeaderTests
InvalidSequenceHeaderCase.MainProfileSrgbIdentity or InvalidSequenceHeaderCase.MainProfileSrgbIdentity or
InvalidSequenceHeaderCase.SubsampledIdentityMatrix; InvalidSequenceHeaderCase.SubsampledIdentityMatrix;
using AutoExpandingMemory<byte> payloadMemory = new(Configuration.Default, 32); byte[] payloadBuffer = new byte[SequenceHeaderBufferLength];
Av1BitStreamWriter writer = new(payloadMemory); Av1BitStreamWriter writer = new(payloadBuffer);
writer.WriteLiteral((uint)ObuSequenceProfile.Main, 3); writer.WriteLiteral((uint)ObuSequenceProfile.Main, 3);
writer.WriteBoolean(false); writer.WriteBoolean(false);
writer.WriteBoolean(false); writer.WriteBoolean(false);
@ -908,7 +910,7 @@ public class ObuFrameHeaderTests
byte[] obu = new byte[payloadLength + 2]; byte[] obu = new byte[payloadLength + 2];
obu[0] = (byte)(((byte)ObuType.SequenceHeader << 3) | 0x02); obu[0] = (byte)(((byte)ObuType.SequenceHeader << 3) | 0x02);
obu[1] = (byte)payloadLength; obu[1] = (byte)payloadLength;
payloadMemory.GetSpan(payloadLength).CopyTo(obu.AsSpan(2)); payloadBuffer.AsSpan(0, payloadLength).CopyTo(obu.AsSpan(2));
return obu; return obu;
} }

48
tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

@ -329,7 +329,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void DecodeIgnoresUnknownTopLevelBox() public void DecodeIgnoresUnknownTopLevelBox()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
data = InsertBytes(data, data.Length, CreateUnknownBox()); data = InsertBytes(data, data.Length, CreateUnknownBox());
using Image<Rgba32> image = Image.Load<Rgba32>(data); using Image<Rgba32> image = Image.Load<Rgba32>(data);
@ -412,7 +412,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void DecodePropagatesConfigurationToLegacyJpegItems() public void DecodePropagatesConfigurationToLegacyJpegItems()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
Configuration configuration = Configuration.CreateDefaultInstance(); Configuration configuration = Configuration.CreateDefaultInstance();
DecoderOptions options = new() { Configuration = configuration }; DecoderOptions options = new() { Configuration = configuration };
@ -555,7 +555,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyIgnoresUnknownMetadataBox() public void IdentifyIgnoresUnknownMetadataBox()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox()); data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox());
@ -701,7 +701,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Jpeg)] [InlineData(Heif4CharCode.Jpeg)]
public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand) public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand)
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new(); HeifImageFormatDetector detector = new();
@ -715,7 +715,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Avis)] [InlineData(Heif4CharCode.Avis)]
public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand) public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand)
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new(); HeifImageFormatDetector detector = new();
@ -745,7 +745,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Jpgs)] [InlineData(Heif4CharCode.Jpgs)]
public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand) public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand)
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new(); HeifImageFormatDetector detector = new();
@ -755,7 +755,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsUnsupportedBrands() public void IdentifyRejectsUnsupportedBrands()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType);
@ -767,7 +767,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsExtendedSizeTopLevelBox() public void IdentifyAcceptsExtendedSizeTopLevelBox()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[16]; byte[] box = new byte[16];
BinaryPrimitives.WriteUInt32BigEndian(box, 1); BinaryPrimitives.WriteUInt32BigEndian(box, 1);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
@ -782,7 +782,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsUuidTopLevelBox() public void IdentifyAcceptsUuidTopLevelBox()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[24]; byte[] box = new byte[24];
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
@ -796,7 +796,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsSizeZeroTopLevelBox() public void IdentifyAcceptsSizeZeroTopLevelBox()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
BinaryPrimitives.WriteUInt32BigEndian(box, 0); BinaryPrimitives.WriteUInt32BigEndian(box, 0);
data = InsertBytes(data, data.Length, box); data = InsertBytes(data, data.Length, box);
@ -809,7 +809,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsExtendedSizeItemInfoEntry() public void IdentifyAcceptsExtendedSizeItemInfoEntry()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -829,7 +829,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsSizeZeroMetadataChild() public void IdentifyRejectsSizeZeroMetadataChild()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
@ -843,7 +843,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsMetadataChildBeyondParent() public void IdentifyRejectsMetadataChildBeyondParent()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
@ -857,7 +857,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsItemInfoEntryBeyondParent() public void IdentifyRejectsItemInfoEntryBeyondParent()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -871,7 +871,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsBoxSmallerThanHeader() public void IdentifyRejectsBoxSmallerThanHeader()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
BinaryPrimitives.WriteUInt32BigEndian(box, 4); BinaryPrimitives.WriteUInt32BigEndian(box, 4);
data = InsertBytes(data, data.Length, box); data = InsertBytes(data, data.Length, box);
@ -882,7 +882,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsTruncatedExtendedSizeHeader() public void IdentifyRejectsTruncatedExtendedSizeHeader()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[12]; byte[] box = new byte[12];
BinaryPrimitives.WriteUInt32BigEndian(box, 1); BinaryPrimitives.WriteUInt32BigEndian(box, 1);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
@ -894,7 +894,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsTruncatedUuidHeader() public void IdentifyRejectsTruncatedUuidHeader()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[16]; byte[] box = new byte[16];
BinaryPrimitives.WriteUInt32BigEndian(box, 24); BinaryPrimitives.WriteUInt32BigEndian(box, 24);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
@ -906,7 +906,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsItemPropertiesBeforeItemInfo() public void IdentifyAcceptsItemPropertiesBeforeItemInfo()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -922,7 +922,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsItemLocationBeforeItemInfo() public void IdentifyAcceptsItemLocationBeforeItemInfo()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -938,7 +938,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsDuplicateUniqueMetadataBox() public void IdentifyRejectsDuplicateUniqueMetadataBox()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12); int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12);
@ -949,11 +949,11 @@ public class HeifDecoderTests
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data)); Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
} }
private static byte[] CreateEncodedContainer() private static byte[] CreateLegacyJpegContainer()
{ {
using Image<Rgba32> image = new(2, 3); using Image<Rgba32> image = new(2, 3);
using MemoryStream stream = new(); using MemoryStream stream = new();
image.Save(stream, new HeifEncoder()); image.Save(stream, new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
return stream.ToArray(); return stream.ToArray();
} }
@ -962,7 +962,7 @@ public class HeifDecoderTests
private static byte[] CreateContainerWithProperty(ReadOnlySpan<byte> property, bool essential) private static byte[] CreateContainerWithProperty(ReadOnlySpan<byte> property, bool essential)
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
@ -992,7 +992,7 @@ public class HeifDecoderTests
private static byte[] CreateContainerWithMalformedJpegMetadata() private static byte[] CreateContainerWithMalformedJpegMetadata()
{ {
byte[] data = CreateEncodedContainer(); byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12);

87
tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles;
@ -39,7 +40,7 @@ public class HeifEncoderTests
{ {
HeifEncoder encoder = new(); HeifEncoder encoder = new();
Assert.Equal(HeifCompressionMethod.LegacyJpeg, encoder.CompressionMethod); Assert.Equal(HeifCompressionMethod.Av1, encoder.CompressionMethod);
Assert.Null(encoder.Quality); Assert.Null(encoder.Quality);
Assert.Null(encoder.AlphaQuality); Assert.Null(encoder.AlphaQuality);
Assert.Equal(5, encoder.Effort); Assert.Equal(5, encoder.Effort);
@ -91,7 +92,11 @@ public class HeifEncoderTests
using Image<Rgba32> image = new(1, 1); using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30); image[0, 0] = new Rgba32(10, 20, 30);
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() { Quality = 0 }; HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Quality = 0
};
image.Save(stream, encoder); image.Save(stream, encoder);
@ -109,7 +114,9 @@ public class HeifEncoderTests
using MemoryStream storage = new(); using MemoryStream storage = new();
using NonSeekableStream destination = new(storage); using NonSeekableStream destination = new(storage);
image.Save(destination, new HeifEncoder()); image.Save(
destination,
new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
Assert.NotEqual(0, storage.Length); Assert.NotEqual(0, storage.Length);
storage.Position = 0; storage.Position = 0;
@ -126,7 +133,9 @@ public class HeifEncoderTests
stream.Write([1, 2, 3, 4]); stream.Write([1, 2, 3, 4]);
long fileStart = stream.Position; long fileStart = stream.Position;
image.Save(stream, new HeifEncoder()); image.Save(
stream,
new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
stream.Position = fileStart; stream.Position = fileStart;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream); using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
@ -141,7 +150,11 @@ public class HeifEncoderTests
using Image<Rgb24> image = new(8, 8); using Image<Rgb24> image = new(8, 8);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray); image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() { SkipMetadata = skipMetadata }; HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
SkipMetadata = skipMetadata
};
image.Save(stream, encoder); image.Save(stream, encoder);
@ -162,7 +175,11 @@ public class HeifEncoderTests
{ {
using Image<Rgba32> image = new(1, 1); using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() { Lossless = true }; HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Lossless = true
};
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder)); Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
Assert.Equal(0, stream.Length); Assert.Equal(0, stream.Length);
@ -175,7 +192,11 @@ public class HeifEncoderTests
{ {
using Image<Rgba32> image = new(1, 1); using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() { BitDepth = bitDepth }; HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
BitDepth = bitDepth
};
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder)); Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
Assert.Equal(0, stream.Length); Assert.Equal(0, stream.Length);
@ -349,6 +370,12 @@ public class HeifEncoderTests
} }
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true); image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
ExifProfile exifProfile = new();
exifProfile.SetValue(ExifTag.Software, "ImageSharp AV1 sequence");
image.Metadata.ExifProfile = exifProfile;
byte[] xmpData = Encoding.UTF8.GetBytes("<xmp>ImageSharp AV1 sequence</xmp>");
image.Metadata.XmpProfile = new XmpProfile(xmpData);
image.Metadata.GetHeifMetadata().RepeatCount = repeatCount; image.Metadata.GetHeifMetadata().RepeatCount = repeatCount;
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
@ -363,10 +390,19 @@ public class HeifEncoderTests
Assert.Equal((uint)Heif4CharCode.Avis, BinaryPrimitives.ReadUInt32BigEndian(file.AsSpan(8))); Assert.Equal((uint)Heif4CharCode.Avis, BinaryPrimitives.ReadUInt32BigEndian(file.AsSpan(8)));
stream.Position = 0; stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream); DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
using Image<Rgba32> decoded = Image.Load<Rgba32>(preserveOptions, stream);
Assert.Equal(frameCount, decoded.Frames.Count); Assert.Equal(frameCount, decoded.Frames.Count);
Assert.Equal(repeatCount, decoded.Metadata.GetHeifMetadata().RepeatCount); Assert.Equal(repeatCount, decoded.Metadata.GetHeifMetadata().RepeatCount);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded)); Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
Assert.Equal(
IccTestDataProfiles.ProfileRandomArray,
Assert.IsType<IccProfile>(decoded.Metadata.IccProfile).ToByteArray());
ExifProfile decodedExif = Assert.IsType<ExifProfile>(decoded.Metadata.ExifProfile);
Assert.True(decodedExif.TryGetValue(ExifTag.Software, out IExifValue<string> software));
Assert.Equal("ImageSharp AV1 sequence", software.Value);
Assert.Equal(xmpData, Assert.IsType<XmpProfile>(decoded.Metadata.XmpProfile).ToByteArray());
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{ {
Assert.Equal( Assert.Equal(
@ -382,6 +418,10 @@ public class HeifEncoderTests
image.Frames.AddFrame(image.Frames.RootFrame); image.Frames.AddFrame(image.Frames.RootFrame);
image.Frames.RootFrame.Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 10); image.Frames.RootFrame.Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 10);
image.Frames[1].Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 20); image.Frames[1].Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 20);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
image.Metadata.ExifProfile = new ExifProfile();
image.Metadata.ExifProfile.SetValue(ExifTag.Software, "suppressed");
image.Metadata.XmpProfile = new XmpProfile(Encoding.UTF8.GetBytes("<xmp>suppressed</xmp>"));
using MemoryStream storage = new(); using MemoryStream storage = new();
storage.Write([1, 2, 3, 4]); storage.Write([1, 2, 3, 4]);
long fileStart = storage.Position; long fileStart = storage.Position;
@ -389,7 +429,8 @@ public class HeifEncoderTests
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1, CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0 Effort = 0,
SkipMetadata = true
}; };
image.Save(destination, encoder); image.Save(destination, encoder);
@ -397,6 +438,9 @@ public class HeifEncoderTests
using Image<Rgb24> decoded = Image.Load<Rgb24>(storage); using Image<Rgb24> decoded = Image.Load<Rgb24>(storage);
Assert.Equal(image.Size, decoded.Size); Assert.Equal(image.Size, decoded.Size);
Assert.Equal(image.Frames.Count, decoded.Frames.Count); Assert.Equal(image.Frames.Count, decoded.Frames.Count);
Assert.Null(decoded.Metadata.IccProfile);
Assert.Null(decoded.Metadata.ExifProfile);
Assert.Null(decoded.Metadata.XmpProfile);
} }
[Theory] [Theory]
@ -867,9 +911,10 @@ public class HeifEncoderTests
alphaItem.SetExtent(new Size(64, 48)); alphaItem.SetExtent(new Size(64, 48));
List<HeifItem> items = [colorItem, alphaItem]; List<HeifItem> items = [colorItem, alphaItem];
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16); int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
ReadOnlySpan<byte> propertyBox = memory.GetSpan(length); Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
Assert.Equal(length, BinaryPrimitives.ReadInt32BigEndian(propertyBox)); Assert.Equal(length, BinaryPrimitives.ReadInt32BigEndian(propertyBox));
Assert.Equal(Heif4CharCode.Iprp, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[4..])); Assert.Equal(Heif4CharCode.Iprp, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[4..]));
@ -970,9 +1015,10 @@ public class HeifEncoderTests
new HeifItem(Heif4CharCode.Mime, 3) new HeifItem(Heif4CharCode.Mime, 3)
]; ];
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16); int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
ReadOnlySpan<byte> propertyBox = memory.GetSpan(length); Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
const int IpcoOffset = 8; const int IpcoOffset = 8;
int ipcoEnd = IpcoOffset + BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]); int ipcoEnd = IpcoOffset + BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]);
int propertyOffset = IpcoOffset + 8; int propertyOffset = IpcoOffset + 8;
@ -1034,9 +1080,10 @@ public class HeifEncoderTests
items.Add(item); items.Add(item);
} }
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16); int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
ReadOnlySpan<byte> propertyBox = memory.GetSpan(length); Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
const int IpcoOffset = 8; const int IpcoOffset = 8;
int ipcoSize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]); int ipcoSize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]);
int ipmaOffset = IpcoOffset + ipcoSize; int ipmaOffset = IpcoOffset + ipcoSize;
@ -1061,7 +1108,7 @@ public class HeifEncoderTests
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = HeifCompressionMethod.Av1; metadata.CompressionMethod = HeifCompressionMethod.Av1;
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.LegacyJpeg };
image.Save(stream, encoder); image.Save(stream, encoder);
@ -1076,7 +1123,7 @@ public class HeifEncoderTests
{ {
using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance)); using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance));
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new(); HeifEncoder encoder = new() { CompressionMethod = compressionMethod };
image.Save(stream, encoder); image.Save(stream, encoder);
stream.Position = 0; stream.Position = 0;

2
tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

@ -38,7 +38,7 @@ public class ImageFormatManagerTests
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<BmpEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<BmpEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<JpegEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<JpegEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<GifEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<GifEncoder>().Count());
Assert.Empty(this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<HeifEncoder>()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<HeifEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TgaEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TgaEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TiffEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TiffEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<WebpEncoder>().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<WebpEncoder>().Count());

53
tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs

@ -1,53 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Memory;
public class AutoExpandingMemoryTests
{
private readonly Configuration configurtion = Configuration.Default;
[Theory]
[InlineData(1000, 2000)]
[InlineData(1000, 1000)]
[InlineData(200, 1000)]
[InlineData(200, 200)]
[InlineData(200, 100)]
public void ExpandToRequestedCapacity(int initialCapacity, int requestedCapacity)
{
AutoExpandingMemory<byte> memory = new(this.configurtion, initialCapacity);
Span<byte> span = memory.GetSpan(requestedCapacity);
Assert.Equal(requestedCapacity, span.Length);
}
[Theory]
[InlineData(1000, 2000)]
[InlineData(1000, 1000)]
[InlineData(200, 1000)]
[InlineData(200, 200)]
[InlineData(200, 100)]
public void KeepDataWhileExpanding(int initialCapacity, int requestedCapacity)
{
AutoExpandingMemory<byte> memory = new(this.configurtion, initialCapacity);
Span<byte> firstSpan = memory.GetSpan(initialCapacity);
firstSpan[1] = 1;
firstSpan[2] = 2;
firstSpan[3] = 3;
Span<byte> expandedSpan = memory.GetSpan(requestedCapacity);
Assert.Equal(3, firstSpan[3]);
Assert.Equal(firstSpan[3], expandedSpan[3]);
}
[Theory]
[InlineData(1, -1)]
[InlineData(-2, 1)]
[InlineData(-2, 0)]
public void Guards(int initialCapacity, int requestedCapacity) =>
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
AutoExpandingMemory<byte> memory = new(this.configurtion, initialCapacity);
_ = memory.GetSpan(requestedCapacity);
});
}
Loading…
Cancel
Save