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;
/// <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>
/// Represents a safe, fixed sized buffer of 4 elements.
/// </summary>
@ -17,6 +35,24 @@ internal struct InlineArray4<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>
/// Represents a safe, fixed sized buffer of 8 elements.
/// </summary>
@ -26,6 +62,24 @@ internal struct InlineArray8<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>
/// Represents a safe, fixed sized buffer of 14 elements.
/// </summary>
@ -62,6 +116,24 @@ internal struct InlineArray19<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>
/// Represents a safe, fixed sized buffer of 26 elements.
/// </summary>
@ -80,6 +152,15 @@ internal struct InlineArray36<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>
/// Represents a safe, fixed sized buffer of 256 elements.
/// </summary>

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#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()
{

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

@ -1,12 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <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>
internal ref struct Av1BitStreamWriter
{
@ -16,19 +14,9 @@ internal ref struct Av1BitStreamWriter
private const int WordSize = 8;
/// <summary>
/// The expanding output allocation.
/// </summary>
private readonly AutoExpandingMemory<byte> memory;
/// <summary>
/// The current writable view over <see cref="memory"/>.
/// The writable output buffer.
/// </summary>
private Span<byte> span;
/// <summary>
/// The final byte index that can be written without expanding <see cref="memory"/>.
/// </summary>
private int capacityTrigger;
private readonly Span<byte> span;
/// <summary>
/// The partially assembled output byte.
@ -38,12 +26,10 @@ internal ref struct Av1BitStreamWriter
/// <summary>
/// Initializes a new instance of the <see cref="Av1BitStreamWriter"/> struct.
/// </summary>
/// <param name="memory">The reusable expanding output allocation.</param>
public Av1BitStreamWriter(AutoExpandingMemory<byte> memory)
/// <param name="span">The preallocated output buffer.</param>
public Av1BitStreamWriter(Span<byte> span)
{
this.memory = memory;
this.span = memory.GetEntireSpan();
this.capacityTrigger = memory.Capacity - 1;
this.span = span;
}
/// <summary>
@ -54,7 +40,7 @@ internal ref struct Av1BitStreamWriter
/// <summary>
/// Gets the current output capacity in bytes.
/// </summary>
public readonly int Capacity => this.memory.Capacity;
public readonly int Capacity => this.span.Length;
/// <summary>
/// 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)
{
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..]);
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");
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..]);
this.BitPosition += tileData.Length << 3;
}
/// <summary>
/// Stores the current output byte, expanding the allocation when necessary.
/// Stores the current output byte.
/// </summary>
private void WriteBuffer()
{
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.buffer = 0;
}

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

@ -36,7 +36,7 @@ internal sealed class Av1Distribution
/// <summary>
/// The inverse cumulative thresholds followed by the required zero sentinel.
/// </summary>
private readonly uint[] probabilities;
private InlineArray16<uint> probabilities;
/// <summary>
/// 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>
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
// forward form, so convert every real threshold while leaving the final zero sentinel untouched.
for (int i = 0; i < props.Length - 1; i++)
{
this.probabilities[i] = ProbabilityTop - props[i];
probabilities[i] = ProbabilityTop - props[i];
}
this.NumberOfSymbols = props.Length;
@ -292,8 +292,9 @@ internal sealed class Av1Distribution
/// <param name="source">The distribution state to copy.</param>
private Av1Distribution(Av1Distribution source)
{
this.probabilities = new uint[source.probabilities.Length];
source.probabilities.CopyTo(this.probabilities, 0);
ReadOnlySpan<uint> sourceProbabilities = source.probabilities;
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
// 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
// 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;
}

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

@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <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>
/// Encodes AV1 tile syntax elements and transform coefficients with tile-local adaptive distributions.
/// </summary>
internal class Av1SymbolEncoder : IDisposable
internal sealed class Av1SymbolEncoder : IDisposable
{
/// <summary>
/// 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.
/// </summary>
/// <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="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;
@ -219,7 +219,7 @@ internal class Av1SymbolEncoder : IDisposable
this.coefficientsBaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.dcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.endOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
this.writer = new(configuration, initialSize, updateCdf);
this.writer = new(configuration, bufferLength, updateCdf);
this.baseQIndex = qIndex;
}
@ -1310,7 +1310,7 @@ internal class Av1SymbolEncoder : IDisposable
}
/// <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>
/// <returns>The memory owner containing the encoded tile bytes.</returns>
public IMemoryOwner<byte> Exit()
@ -1320,18 +1320,18 @@ internal class Av1SymbolEncoder : IDisposable
}
/// <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>
/// <param name="length">The number of encoded bytes at the beginning of the returned allocation.</param>
/// <returns>The complete allocation containing the encoded tile prefix.</returns>
public IMemoryOwner<byte> Exit(out int length)
/// <param name="length">The number of encoded bytes in the returned memory.</param>
/// <returns>The encoded prefix, valid until this encoder is disposed.</returns>
public ReadOnlyMemory<byte> Exit(out int length)
{
ref Av1SymbolWriter w = ref this.writer;
return w.Exit(out length);
}
/// <summary>
/// Releases output memory that has not been transferred by <see cref="Exit()"/>.
/// Releases the range-coder output buffer and coefficient scratch memory.
/// </summary>
public void Dispose()
{

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

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence.
/// </summary>
internal class Av1SymbolWriter : IDisposable
internal sealed class Av1SymbolWriter : IDisposable
{
/// <summary>
/// The lower endpoint of the current coding interval.
@ -36,9 +36,14 @@ internal class Av1SymbolWriter : IDisposable
private readonly Configuration configuration;
/// <summary>
/// The output bytes accumulated during renormalization.
/// The owner of the fixed output buffer supplied for this tile.
/// </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>
/// Indicates whether encoded symbols adapt their distributions.
@ -51,22 +56,23 @@ internal class Av1SymbolWriter : IDisposable
private int position;
/// <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>
/// <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>
public Av1SymbolWriter(Configuration configuration, int initialSize, bool updateCdf = true)
public Av1SymbolWriter(Configuration configuration, int bufferLength, bool updateCdf)
{
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;
}
/// <summary>
/// Releases the expandable pre-carry buffer.
/// Releases the tile output buffer.
/// </summary>
public void Dispose() => this.memory.Dispose();
public void Dispose() => this.bufferOwner.Dispose();
/// <summary>
/// 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();
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;
}
/// <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>
/// <param name="length">The number of encoded bytes at the beginning of the returned allocation.</param>
/// <returns>The complete allocation containing the encoded byte prefix.</returns>
public IMemoryOwner<byte> Exit(out int length)
/// <param name="length">The number of encoded bytes in the returned memory.</param>
/// <returns>The encoded prefix, valid until this writer is disposed.</returns>
public ReadOnlyMemory<byte> Exit(out int length)
{
length = this.FinalizeRange();
return this.memory.Detach();
return this.buffer[..length];
}
/// <summary>
@ -164,7 +170,7 @@ internal class Av1SymbolWriter : IDisposable
ulong e = ((l + m) & ~m) | (m + 1);
s += c;
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)
{
ulong n = (1UL << (c + 16)) - 1;
@ -289,7 +295,7 @@ internal class Av1SymbolWriter : IDisposable
// bytes together while preserving one carry bit.
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;
c += 24 - (readyByteCount << 3);
ulong output = low >> c;

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

@ -2,8 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
@ -484,17 +482,4 @@ internal struct Av1GlobalMotionParameters
=> value < 0
? -(((-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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
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>
private static bool IsOverlappable(Av1BlockModeInfo candidate)
=> 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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -1365,30 +1364,4 @@ internal sealed class Av1ReferenceMotionVectors
Av1PredictionMode.NewNearestMotionVector or
Av1PredictionMode.NearNewMotionVector or
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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
@ -250,43 +248,4 @@ internal sealed class ObuFilmGrainParameters
this.OverlapFlag = source.OverlapFlag;
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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
@ -10,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// </summary>
internal sealed class ObuTileGroupHeader
{
private InlineTileColumnBoundaryArray tileColumnStartModeInfo;
private InlineTileRowBoundaryArray tileRowStartModeInfo;
private InlineArray65<int> tileColumnStartModeInfo;
private InlineArray65<int> tileRowStartModeInfo;
/// <summary>
/// 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.
/// </summary>
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.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -11,8 +12,12 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Writes the AV1 open bitstream units required for a single still-image frame.
/// </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>
/// Writes a temporal delimiter and the supplied sequence and frame OBUs.
/// </summary>
@ -27,11 +32,11 @@ internal class ObuWriter
Justification = "Preserves the existing writer instance contract.")]
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
// buffers and are streamed directly so the complete compressed frame is never duplicated.
int initialBufferSize = 2000;
using AutoExpandingMemory<byte> buffer = new(configuration, initialBufferSize);
Av1BitStreamWriter writer = new(buffer);
// The reusable scratch only contains headers. Entropy-coded tiles remain in their owning buffers and are
// streamed directly so the complete compressed frame is never duplicated.
using IMemoryOwner<byte> headerOwner = configuration.MemoryAllocator.Allocate<byte>(MaximumHeaderLength);
Span<byte> headerBuffer = headerOwner.Memory.Span[..MaximumHeaderLength];
Av1BitStreamWriter writer = new(headerBuffer);
WriteObuHeaderAndSize(stream, ObuType.TemporalDelimiter, []);
if (sequenceHeader != null)
@ -39,7 +44,7 @@ internal class ObuWriter
WriteSequenceHeader(ref writer, sequenceHeader);
int bytesWritten = (writer.BitPosition + 7) >> 3;
writer.Flush();
WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, buffer.GetSpan(bytesWritten));
WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, headerBuffer[..bytesWritten]);
}
if (frameHeader != null && sequenceHeader != null)
@ -67,7 +72,7 @@ internal class ObuWriter
}
WriteObuHeaderAndSize(stream, ObuType.Frame, framePayloadSize);
stream.Write(buffer.GetSpan(frameHeaderBytes));
stream.Write(headerBuffer[..frameHeaderBytes]);
if (tileInfo != null)
{

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
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.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -17,6 +18,29 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// </summary>
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>
/// Encodes one reduced-still-picture AV1 frame into a low-overhead OBU stream.
/// </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.
// Counting the active planes directly retains that headroom without charging monochrome for unused chroma.
int alignedWidth = Av1Math.AlignPowerOf2(width, 5);
int alignedHeight = Av1Math.AlignPowerOf2(height, 5);
int alignedWidth = Av1Math.AlignPowerOf2(width, OutputAlignmentLog2);
int alignedHeight = Av1Math.AlignPowerOf2(height, OutputAlignmentLog2);
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;
long sampleCount = (long)alignedWidth * alignedHeight;
@ -152,14 +176,17 @@ internal static class Av1FrameEncoder
}
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)
{
EncodeByte(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, effort, encodeAlpha);
EncodeByte(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, tileBufferLength, effort, encodeAlpha);
}
else
{
EncodeHighBitDepth(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, effort, encodeAlpha);
EncodeHighBitDepth(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, tileBufferLength, effort, encodeAlpha);
}
return sequenceHeader;
@ -204,7 +231,7 @@ internal static class Av1FrameEncoder
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ColorFormat colorFormat,
int initialTileSize,
int tileBufferLength,
int effort,
bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel>
@ -227,7 +254,7 @@ internal static class Av1FrameEncoder
chromaPositionX: 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>(
@ -237,7 +264,7 @@ internal static class Av1FrameEncoder
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ColorFormat colorFormat,
int initialTileSize,
int tileBufferLength,
int effort,
bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel>
@ -261,7 +288,7 @@ internal static class Av1FrameEncoder
chromaPositionX: 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>(
@ -272,7 +299,7 @@ internal static class Av1FrameEncoder
ObuFrameHeader frameHeader,
Av1EncoderFrameBuffer<byte> source,
Av1EncoderFrameBuffer<byte> reconstruction,
int initialTileSize,
int tileBufferLength,
int effort,
bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel>
@ -316,15 +343,20 @@ internal static class Av1FrameEncoder
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(configuration);
using Av1EncoderBlockWorkspace blockWorkspace = new(configuration);
using Av1IntraTileWriter tileWriter = new(
using Av1SymbolEncoder symbolEncoder = new(
configuration,
tileBufferLength,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialTileSize,
effort);
ObuWriter writer = new();
@ -339,7 +371,7 @@ internal static class Av1FrameEncoder
ObuFrameHeader frameHeader,
Av1EncoderFrameBuffer<ushort> source,
Av1EncoderFrameBuffer<ushort> reconstruction,
int initialTileSize,
int tileBufferLength,
int effort,
bool encodeAlpha)
where TPixel : unmanaged, IPixel<TPixel>
@ -383,15 +415,20 @@ internal static class Av1FrameEncoder
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(configuration);
using Av1EncoderBlockWorkspace blockWorkspace = new(configuration);
using Av1IntraTileWriter tileWriter = new(
using Av1SymbolEncoder symbolEncoder = new(
configuration,
tileBufferLength,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialTileSize,
effort);
ObuWriter writer = new();

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
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;
/// <summary>
/// Encodes and owns one range-coded all-intra tile payload.
/// Encodes one range-coded all-intra tile payload.
/// </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;
/// <summary>
/// Initializes a new instance of the <see cref="Av1IntraTileWriter"/> class for eight-bit samples.
/// </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="reconstruction">The reconstructed frame updated during encoding.</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="superblockWorkspace">The reusable partition and final-block decision 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>
public Av1IntraTileWriter(
Configuration configuration,
Av1SymbolEncoder writer,
Av1EncoderFrame<byte> source,
Av1EncoderFrame<byte> reconstruction,
Av1PictureControlSet picture,
Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace,
int initialSize,
int effort)
{
this.tileData = Encode<byte, Av1IntraSuperblockEncoder.ByteOperator>(
configuration,
writer,
source,
reconstruction,
picture,
@ -49,35 +46,32 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
superblockWorkspace,
blockWorkspace,
effort,
initialSize,
out this.tileDataLength);
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1IntraTileWriter"/> class for high-bit-depth samples.
/// </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="reconstruction">The reconstructed frame updated during encoding.</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="superblockWorkspace">The reusable partition and final-block decision 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>
public Av1IntraTileWriter(
Configuration configuration,
Av1SymbolEncoder writer,
Av1EncoderFrame<ushort> source,
Av1EncoderFrame<ushort> reconstruction,
Av1PictureControlSet picture,
Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace,
int initialSize,
int effort)
{
this.tileData = Encode<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(
configuration,
writer,
source,
reconstruction,
picture,
@ -85,28 +79,14 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
superblockWorkspace,
blockWorkspace,
effort,
initialSize,
out this.tileDataLength);
}
/// <inheritdoc/>
public ReadOnlySpan<byte> GetTileData(int tileNum)
{
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;
}
public ReadOnlySpan<byte> GetTileData(int tileNum) => this.tileData.Span[..this.tileDataLength];
private static IMemoryOwner<byte> Encode<TSample, TOperator>(
Configuration configuration,
private static ReadOnlyMemory<byte> Encode<TSample, TOperator>(
Av1SymbolEncoder writer,
Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> reconstruction,
Av1PictureControlSet picture,
@ -114,7 +94,6 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace,
int effort,
int initialSize,
out int tileDataLength)
where TSample : unmanaged
where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample>
@ -136,12 +115,6 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
MacroBlockModeInfo = picture.GetMacroBlockModeInfo(firstModeInfoPosition)
};
using Av1SymbolEncoder writer = new(
configuration,
initialSize,
frameHeader.QuantizationParameters.BaseQIndex,
updateCdf: !frameHeader.DisableCdfUpdate);
int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize;
int superblockShift = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2;
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>
internal static class Av1QuantizationLookup
{
// Coefficient scaling and quantization with AV1 TX are tailored to
// the AV1 TX transforms. Regardless of the bit-depth of the input,
// the transform stages scale the coefficient values up by a factor of
// 8 (3 bits) over the scale of the pixel values. Thus, for 8-bit
// 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.
private const int LinearQuantizerScale = 4;
private const int LastLinearQuantizer = 61;
private const int PenultimateQuantizer = 62;
private const int PenultimateQuantizerIndex = 249;
// A partial exception to this rule is large transforms; to avoid
// overflow, TX blocks with > 256 pels (>16x16) are scaled only
// 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.
// AV1 transforms normally retain three fractional coefficient bits. The quantizer tables use the same Q3
// scale, leaving coded coefficients in Q0 and reconstructed coefficients in Q3.
// Note that encoder decision making (which uses the quantizer to
// generate several bespoke lamdas for RDO and other heuristics)
// expects quantizers to be larger for higher-bitdepth input. In
// addition, the minimum allowable quantizer is 4; smaller values will
// underflow to 0 in the actual quantization routines.
// Transforms larger than 16x16 reduce coefficient scaling by one bit, and transforms larger than 32x32 reduce
// it by two bits to preserve numeric range. Quantization applies the same reduction to its step, so every
// reconstructed transform still reaches the inverse transform in Q3.
// 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>
/// 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,
];
/// <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>
/// Gets the DC dequantization value after applying a plane delta to the frame quantizer index.
/// </summary>

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

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -463,17 +462,4 @@ internal struct Av1BlockModeInfo
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.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -52,24 +51,4 @@ internal struct Av1EncoderPaletteInfo
Span<ushort> destination = this.paletteColors;
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.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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.
/// </summary>
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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
@ -13,17 +11,17 @@ internal struct Av1LoopRestorationUnit
/// <summary>
/// The three transmitted symmetric vertical Wiener coefficients.
/// </summary>
public WienerCoefficientBuffer WienerVertical;
public InlineArray3<int> WienerVertical;
/// <summary>
/// The three transmitted symmetric horizontal Wiener coefficients.
/// </summary>
public WienerCoefficientBuffer WienerHorizontal;
public InlineArray3<int> WienerHorizontal;
/// <summary>
/// The two self-guided projection coefficients.
/// </summary>
public SgrProjectionCoefficientBuffer SgrProjectionCoefficients;
public InlineArray2<int> SgrProjectionCoefficients;
/// <summary>
/// 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.
/// </summary>
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>
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.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <summary>
@ -28,17 +26,17 @@ internal ref struct Av1Transform2dFlipConfiguration
/// <summary>
/// The fixed-point shifts applied between successive stages of the configured transform pipeline.
/// </summary>
private ShiftBuffer shift;
private InlineArray3<int> shift;
/// <summary>
/// The signed-bit ranges produced by the column transform stages.
/// </summary>
private Av1TransformStageRange stageRangeColumn;
private InlineArray12<byte> stageRangeColumn;
/// <summary>
/// The signed-bit ranges produced by the row transform stages.
/// </summary>
private Av1TransformStageRange stageRangeRow;
private InlineArray12<byte> stageRangeRow;
/// <summary>
/// Initializes a new instance of the <see cref="Av1Transform2dFlipConfiguration"/> struct.
@ -337,12 +335,12 @@ internal ref struct Av1Transform2dFlipConfiguration
/// <summary>
/// Gets the allowed signed-bit range after each column-transform stage.
/// </summary>
public readonly Av1TransformStageRange StageRangeColumn => this.stageRangeColumn;
public readonly InlineArray12<byte> StageRangeColumn => this.stageRangeColumn;
/// <summary>
/// Gets the allowed signed-bit range after each row-transform stage.
/// </summary>
public readonly Av1TransformStageRange StageRangeRow => this.stageRangeRow;
public readonly InlineArray12<byte> StageRangeRow => this.stageRangeRow;
/// <summary>
/// Creates the configuration used to transform spatial residuals into coefficients.
@ -499,16 +497,4 @@ internal ref struct Av1Transform2dFlipConfiguration
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="cosBit">The fixed-point precision of the cosine constants.</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);
int stage = 0;
@ -205,7 +205,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
@ -393,7 +393,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
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="cosBit">The fixed-point precision of the sine constants.</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);
@ -79,7 +79,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount;
@ -115,7 +115,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
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="cosBit">The fixed-point precision of the cosine constants.</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);
int stage = 0;
@ -112,7 +112,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
@ -207,7 +207,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
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="cosBit">The fixed-point precision of the cosine constants.</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);
int stage = 0;
@ -174,7 +174,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
@ -331,7 +331,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
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="cosBit">The fixed-point precision of the cosine constants.</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);
int stage = 0;
@ -358,7 +358,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
@ -699,7 +699,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
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="cosBit">The fixed-point precision of the cosine constants.</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.
output[0] = input[0];
@ -53,7 +53,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output.V0 = input.V0;
@ -89,7 +89,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
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="cosBit">The fixed-point precision of the cosine constants.</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);
int stage = 0;
@ -773,7 +773,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
@ -1529,7 +1529,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
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="cosBit">The fixed-point precision of the cosine constants.</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);
int stage = 0;
@ -93,7 +93,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
@ -169,7 +169,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
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="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>
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;
_ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
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
// 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>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
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="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>
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;
_ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0);
_ = step;
@ -57,7 +57,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0);
_ = 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="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>
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;
_ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
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.
// 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>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
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="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>
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;
_ = cosBit;
@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0);
_ = step;
@ -57,7 +57,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0);
_ = 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="cosBit">The fixed-point precision of the cosine constants.</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>
/// Transforms four independent axes in parallel.
@ -42,7 +42,7 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange);
InlineArray12<byte> stageRange);
/// <summary>
/// Transforms eight independent axes in parallel.
@ -57,6 +57,6 @@ internal static partial class Av1Inverse2dTransformer
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange);
InlineArray12<byte> stageRange);
}
}

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

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

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

@ -25,9 +25,9 @@ public sealed class HeifEncoder : AnimatedImageEncoder
/// <summary>
/// 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>
public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.LegacyJpeg;
public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.Av1;
/// <summary>
/// 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.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
@ -22,6 +22,11 @@ internal sealed partial class HeifEncoderCore
private const uint UnityFixed2Point30 = 1U << 30;
private const ushort UnityFixed8Point8 = 1 << 8;
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)
where TPixel : unmanaged, IPixel<TPixel>
@ -56,7 +61,11 @@ internal sealed partial class HeifEncoderCore
CicpProfile colorProfile;
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
{
@ -133,6 +142,7 @@ internal sealed partial class HeifEncoderCore
Image<TPixel> image,
ChunkedMemoryStream stream,
Av1EncodingSettings settings,
Memory<HeifSequenceSampleInfo> samples,
CancellationToken cancellationToken)
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.");
}
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;
uint timescale = GetSequenceTimescale(image);
int sampleCount = checked(frameCount * (settings.HasAlpha ? 2 : 1));
// The container needs only offset, length, and duration after each frame is streamed. Color and alpha
// share one compact table, with each track occupying one contiguous slice for its complete operation lifetime.
HeifSequenceSampleInfo[] samples = new HeifSequenceSampleInfo[sampleCount];
Span<HeifSequenceSampleInfo> colorSamples = samples.AsSpan(0, frameCount);
// share one allocator-owned table, with each track occupying one contiguous slice until moov is written.
Span<HeifSequenceSampleInfo> colorSamples = samples.Span[..frameCount];
ImageFrame<TPixel> rootFrame = image.Frames.RootFrame;
uint duration = GetSequenceSampleDuration(rootFrame.Metadata.GetHeifMetadata().FrameDelay, timescale);
cancellationToken.ThrowIfCancellationRequested();
@ -188,15 +209,14 @@ internal sealed partial class HeifEncoderCore
HeifSequenceTrackEncoding colorTrack = new(
new Av1CodecConfiguration(colorHeader),
samples,
0,
frameCount,
samples[..frameCount],
false);
HeifSequenceTrackEncoding? alphaTrack = null;
if (settings.HasAlpha)
{
Span<HeifSequenceSampleInfo> alphaSamples = samples.AsSpan(frameCount, frameCount);
Memory<HeifSequenceSampleInfo> alphaSampleMemory = samples.Slice(frameCount, frameCount);
Span<HeifSequenceSampleInfo> alphaSamples = alphaSampleMemory.Span;
cancellationToken.ThrowIfCancellationRequested();
long alphaOffset = stream.Length;
ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha(
@ -232,12 +252,17 @@ internal sealed partial class HeifEncoderCore
alphaTrack = new HeifSequenceTrackEncoding(
new Av1CodecConfiguration(alphaHeader),
samples,
frameCount,
frameCount,
alphaSampleMemory,
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(
image.Width,
image.Height,
@ -246,7 +271,10 @@ internal sealed partial class HeifEncoderCore
colorTrack,
alphaTrack,
settings.ColorProfile,
this.encoder.SkipMetadata ? null : image.Metadata.IccProfile);
iccProfileData,
exifData,
tiffHeaderOffset,
xmpData);
}
private int WriteSequenceFileTypeBox(Stream stream)
@ -272,9 +300,9 @@ internal sealed partial class HeifEncoderCore
private void WriteSequenceMovieBox(HeifSequenceEncoding sequence, int fileTypeLength, Stream stream)
{
// Chunk offsets point past the completed movie box, so retain only this bounded metadata box and patch
// its two offsets once its size is known. The encoded frame payload remains in allocator-backed chunks.
using AutoExpandingMemory<byte> memory = new(this.configuration, 0x1000);
int movieLength = GetSequenceMovieBoxLength(sequence);
using IMemoryOwner<byte> movieOwner = this.configuration.MemoryAllocator.Allocate<byte>(movieLength);
Span<byte> memory = movieOwner.Memory.Span[..movieLength];
int offset = 0;
int movieStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Moov);
ulong mediaDuration = GetSequenceMediaDuration(sequence.ColorTrack.Samples);
@ -316,23 +344,126 @@ internal sealed partial class HeifEncoderCore
EndSequenceBox(memory, movieStart, offset);
ulong mediaDataOffset = checked((ulong)fileTypeLength + (uint)offset + 8U);
Span<byte> movie = memory.GetSpan(offset);
BinaryPrimitives.WriteUInt64BigEndian(
movie[colorChunkOffsetPosition..],
memory[colorChunkOffsetPosition..],
checked(mediaDataOffset + (ulong)sequence.ColorTrack.Samples[0].Offset));
if (alphaChunkOffsetPosition >= 0)
{
BinaryPrimitives.WriteUInt64BigEndian(
movie[alphaChunkOffsetPosition..],
memory[alphaChunkOffsetPosition..],
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(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
uint timescale,
ulong duration,
@ -355,7 +486,7 @@ internal sealed partial class HeifEncoderCore
}
private static int WriteSequenceTrack(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
HeifSequenceEncoding sequence,
HeifSequenceTrackEncoding track,
@ -382,6 +513,11 @@ internal sealed partial class HeifEncoderCore
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);
WriteSequenceMediaHeader(memory, ref offset, sequence.Timescale, mediaDuration);
WriteSequenceHandler(memory, ref offset, track.IsAlpha ? Heif4CharCode.Auxv : Heif4CharCode.Pict);
@ -395,7 +531,7 @@ internal sealed partial class HeifEncoderCore
}
private static void WriteSequenceTrackHeader(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
int width,
int height,
@ -411,13 +547,13 @@ internal sealed partial class HeifEncoderCore
WriteSequenceUInt64(memory, ref offset, duration);
WriteSequenceZeros(memory, ref offset, (2 * sizeof(uint)) + (4 * sizeof(ushort)));
WriteSequenceIdentityMatrix(memory, ref offset);
WriteSequenceUInt32(memory, ref offset, checked((uint)width << 16));
WriteSequenceUInt32(memory, ref offset, checked((uint)height << 16));
WriteSequenceUInt32(memory, ref offset, (uint)width << 16);
WriteSequenceUInt32(memory, ref offset, (uint)height << 16);
EndSequenceBox(memory, trackHeaderStart, offset);
}
private static void WriteSequenceTrackReference(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
Heif4CharCode referenceType,
uint referencedTrackId)
@ -430,7 +566,7 @@ internal sealed partial class HeifEncoderCore
}
private static void WriteSequenceEditList(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
ulong mediaDuration)
{
@ -446,8 +582,119 @@ internal sealed partial class HeifEncoderCore
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(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
uint timescale,
ulong mediaDuration)
@ -464,7 +711,7 @@ internal sealed partial class HeifEncoderCore
}
private static void WriteSequenceHandler(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
Heif4CharCode handlerType)
{
@ -473,11 +720,11 @@ internal sealed partial class HeifEncoderCore
WriteSequenceUInt32(memory, ref offset, 0);
WriteSequenceUInt32(memory, ref offset, (uint)handlerType);
WriteSequenceZeros(memory, ref offset, 12);
memory.GetSpan(offset++, 1)[0] = 0;
memory[offset++] = 0;
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 dataReferenceStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Dref);
@ -491,7 +738,7 @@ internal sealed partial class HeifEncoderCore
}
private static int WriteSequenceSampleTable(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
HeifSequenceEncoding sequence,
HeifSequenceTrackEncoding track)
@ -505,17 +752,17 @@ internal sealed partial class HeifEncoderCore
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, 1);
WriteSequenceUInt32(memory, ref offset, 1);
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length));
WriteSequenceUInt32(memory, ref offset, (uint)track.Samples.Length);
WriteSequenceUInt32(memory, ref offset, 1);
EndSequenceBox(memory, sampleToChunkStart, offset);
int sampleSizesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stsz);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, 0);
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length));
WriteSequenceUInt32(memory, ref offset, (uint)track.Samples.Length);
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);
@ -530,7 +777,7 @@ internal sealed partial class HeifEncoderCore
// The current bounded sequence encoder emits independent all-intra pictures; every sample is seekable.
int syncSamplesStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stss);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
WriteSequenceUInt32(memory, ref offset, checked((uint)track.Samples.Length));
WriteSequenceUInt32(memory, ref offset, (uint)track.Samples.Length);
for (uint sampleIndex = 1; sampleIndex <= track.Samples.Length; sampleIndex++)
{
WriteSequenceUInt32(memory, ref offset, sampleIndex);
@ -542,7 +789,7 @@ internal sealed partial class HeifEncoderCore
}
private static void WriteSequenceSampleDescription(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
HeifSequenceEncoding sequence,
HeifSequenceTrackEncoding track)
@ -554,18 +801,18 @@ internal sealed partial class HeifEncoderCore
WriteSequenceZeros(memory, ref offset, 6);
WriteSequenceUInt16(memory, ref offset, 1);
WriteSequenceZeros(memory, ref offset, (2 * sizeof(ushort)) + (3 * sizeof(uint)));
WriteSequenceUInt16(memory, ref offset, checked((ushort)sequence.Width));
WriteSequenceUInt16(memory, ref offset, checked((ushort)sequence.Height));
WriteSequenceUInt32(memory, ref offset, 72U << 16);
WriteSequenceUInt32(memory, ref offset, 72U << 16);
WriteSequenceUInt16(memory, ref offset, (ushort)sequence.Width);
WriteSequenceUInt16(memory, ref offset, (ushort)sequence.Height);
WriteSequenceUInt32(memory, ref offset, DefaultVisualSampleResolution);
WriteSequenceUInt32(memory, ref offset, DefaultVisualSampleResolution);
WriteSequenceUInt32(memory, ref offset, 0);
WriteSequenceUInt16(memory, ref offset, 1);
WriteSequenceZeros(memory, ref offset, 32);
WriteSequenceUInt16(memory, ref offset, 24);
WriteSequenceZeros(memory, ref offset, VisualSampleCompressorNameLength);
WriteSequenceUInt16(memory, ref offset, VisualSampleDepth);
WriteSequenceUInt16(memory, ref offset, ushort.MaxValue);
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;
EndSequenceBox(memory, configurationStart, offset);
@ -574,16 +821,16 @@ internal sealed partial class HeifEncoderCore
int auxiliaryTypeStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Auxi);
WriteSequenceFullBoxHeader(memory, ref offset, 0, 0);
int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(HeifConstants.AlphaAuxiliaryType);
Span<byte> auxiliaryType = memory.GetSpan(offset, auxiliaryTypeLength + 1);
Span<byte> auxiliaryType = memory.Slice(offset, auxiliaryTypeLength + 1);
offset += Encoding.UTF8.GetBytes(HeifConstants.AlphaAuxiliaryType, auxiliaryType);
memory.GetSpan(offset++, 1)[0] = 0;
memory[offset++] = 0;
EndSequenceBox(memory, auxiliaryTypeStart, offset);
}
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);
@ -594,28 +841,24 @@ internal sealed partial class HeifEncoderCore
// Every emitted sequence sample is independently decodable, while intra prediction remains available inside
// each picture. No inter-picture reference slot is therefore advertised.
WriteSequenceUInt32(memory, ref offset, 0xC0000000);
WriteSequenceUInt32(memory, ref offset, AllReferencePicturesIntraMask | IntraPicturePredictionUsedMask);
EndSequenceBox(memory, codingConstraintsStart, offset);
EndSequenceBox(memory, sampleEntryStart, offset);
EndSequenceBox(memory, descriptionStart, offset);
}
private static void WriteSequenceSampleTiming(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
ReadOnlySpan<HeifSequenceSampleInfo> samples)
{
// The time-to-sample table stores runs, not one entry per frame. Preserve exact resolved durations while
// combining only adjacent frames whose delays are equal.
int runCount = 1;
for (int sampleIndex = 1; sampleIndex < samples.Length; sampleIndex++)
{
runCount += samples[sampleIndex].Duration == samples[sampleIndex - 1].Duration ? 0 : 1;
}
int runCount = GetSequenceTimingRunCount(samples);
int timingStart = BeginSequenceBox(memory, ref offset, Heif4CharCode.Stts);
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 runLength = 1;
for (int sampleIndex = 1; sampleIndex <= samples.Length; sampleIndex++)
@ -638,9 +881,22 @@ internal sealed partial class HeifEncoderCore
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)
{
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;
}
@ -656,7 +912,7 @@ internal sealed partial class HeifEncoderCore
foreach (ImageFrame<TPixel> frame in image.Frames)
{
Rational delay = frame.Metadata.GetHeifMetadata().FrameDelay;
if (delay.Numerator == 0)
if (delay.Numerator == 0 || delay.Denominator == 0)
{
continue;
}
@ -700,34 +956,34 @@ internal sealed partial class HeifEncoderCore
}
private static int BeginSequenceBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
Heif4CharCode type)
{
// Reserve the size field now and patch it at the matching EndSequenceBox call after nested boxes expand.
int start = offset;
offset += WriteBoxHeader(memory.GetSpan(offset, 8), type);
offset += WriteBoxHeader(memory[offset..], type);
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(
memory.GetSpan(start, sizeof(uint)),
checked((uint)(offset - start)));
memory.Slice(start, sizeof(uint)),
(uint)(offset - start));
private static void WriteSequenceFullBoxHeader(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
ref int offset,
byte version,
uint flags)
{
Span<byte> destination = memory.GetSpan(offset, sizeof(uint));
Span<byte> destination = memory.Slice(offset, sizeof(uint));
BinaryPrimitives.WriteUInt32BigEndian(destination, flags);
destination[0] = version;
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, 0);
@ -740,27 +996,36 @@ internal sealed partial class HeifEncoderCore
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;
}
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);
}
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);
}
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);
}
@ -829,7 +1094,10 @@ internal sealed partial class HeifEncoderCore
HeifSequenceTrackEncoding colorTrack,
HeifSequenceTrackEncoding? alphaTrack,
CicpProfile colorProfile,
IccProfile? iccProfile)
ReadOnlyMemory<byte> iccProfileData,
byte[]? exifData,
uint exifTiffHeaderOffset,
byte[]? xmpData)
{
this.Width = width;
this.Height = height;
@ -838,7 +1106,10 @@ internal sealed partial class HeifEncoderCore
this.ColorTrack = colorTrack;
this.AlphaTrack = alphaTrack;
this.ColorProfile = colorProfile;
this.IccProfile = iccProfile;
this.IccProfileData = iccProfileData;
this.ExifData = exifData;
this.ExifTiffHeaderOffset = exifTiffHeaderOffset;
this.XmpData = xmpData;
}
public int Width { get; }
@ -855,33 +1126,33 @@ internal sealed partial class HeifEncoderCore
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 HeifSequenceSampleInfo[] samples;
private readonly int sampleOffset;
private readonly int sampleCount;
private readonly ReadOnlyMemory<HeifSequenceSampleInfo> samples;
public HeifSequenceTrackEncoding(
Av1CodecConfiguration configuration,
HeifSequenceSampleInfo[] samples,
int sampleOffset,
int sampleCount,
ReadOnlyMemory<HeifSequenceSampleInfo> samples,
bool isAlpha)
{
this.Configuration = configuration;
this.samples = samples;
this.sampleOffset = sampleOffset;
this.sampleCount = sampleCount;
this.IsAlpha = isAlpha;
}
public Av1CodecConfiguration Configuration { get; }
public ReadOnlySpan<HeifSequenceSampleInfo> Samples
=> this.samples.AsSpan(this.sampleOffset, this.sampleCount);
=> this.samples.Span;
public bool IsAlpha { get; }
}

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

@ -1,14 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
@ -20,6 +23,30 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// </summary>
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>
/// The global configuration.
/// </summary>
@ -58,10 +85,16 @@ internal sealed partial class HeifEncoderCore
if (this.encoder.CompressionMethod == HeifCompressionMethod.Av1 && image.Frames.Count > 1)
{
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(
image,
compressedPixels,
settings,
samples,
cancellationToken);
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>
private void WriteMetadataBox(List<HeifItem> items, List<HeifItemLink> links, long metadataBoxOffset, Stream stream)
{
using AutoExpandingMemory<byte> memory = new(this.configuration, 0x1000);
Span<byte> buffer = memory.GetSpan(12);
int metadataLength = GetMetadataBoxLength(items, links);
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);
bytesWritten += WriteHandlerBox(memory, bytesWritten);
bytesWritten += WritePrimaryItemBox(memory, bytesWritten);
@ -208,7 +243,7 @@ internal sealed partial class HeifEncoderCore
if (links.Count > 0)
{
// 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);
@ -219,23 +254,130 @@ internal sealed partial class HeifEncoderCore
bytesWritten += WriteItemLocationBox(memory, bytesWritten, items, 0);
// 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);
buffer = memory.GetSpan(bytesWritten);
buffer = memory[..bytesWritten];
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
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>
/// Writes the picture metadata handler box.
/// </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>
/// <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);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0);
bytesWritten += 4;
@ -253,12 +395,12 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes the identifier of the primary presentation item.
/// </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>
/// <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);
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1);
bytesWritten += 2;
@ -270,27 +412,13 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes the item-information box and one version-two entry for each item.
/// </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="items">The items to declare.</param>
/// <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;
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);
Span<byte> buffer = memory.Slice(memoryOffset, GetItemInformationBoxLength(items));
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iinf, 0, 0);
BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count);
bytesWritten += 2;
@ -327,14 +455,13 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes typed item-reference child boxes using 16-bit item identifiers.
/// </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="items">The declared items used to size the destination.</param>
/// <param name="links">The relationships to write.</param>
/// <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);
foreach (HeifItemLink link in links)
{
@ -360,13 +487,13 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes spatial-extent properties and their one-based item associations.
/// </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="items">The items whose dimensions are written and associated.</param>
/// <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);
// 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;
if (iccProfile is not null)
{
bytesWritten += WriteIccColorInformationPropertyBox(memory, memoryOffset + bytesWritten, iccProfile);
bytesWritten += WriteIccColorInformationPropertyBox(memory, memoryOffset + bytesWritten, item.GetIccProfileDataForWriting());
}
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));
int propertyCount = 0;
int associationItemCount = 0;
int associationBoxCapacity = 16;
foreach (HeifItem item in items)
{
int itemPropertyCount = GetPropertyCount(item);
@ -437,16 +562,9 @@ internal sealed partial class HeifEncoderCore
propertyCount += itemPropertyCount;
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.
int ipmaLengthOffset = bytesWritten;
@ -537,29 +655,29 @@ internal sealed partial class HeifEncoderCore
{
if (largePropertyIndex)
{
ushort association = essential ? (ushort)(propertyIndex | 0x8000) : propertyIndex;
ushort association = essential ? (ushort)(propertyIndex | EssentialPropertyFlag) : propertyIndex;
BinaryPrimitives.WriteUInt16BigEndian(buffer[offset..], association);
offset += 2;
}
else
{
buffer[offset++] = essential ? (byte)(propertyIndex | 0x80) : (byte)propertyIndex;
buffer[offset++] = essential ? (byte)(propertyIndex | CompactEssentialPropertyFlag) : (byte)propertyIndex;
}
}
/// <summary>
/// Writes the encoded precision of each image channel.
/// </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="channelBitDepths">The encoded precision of each channel.</param>
/// <returns>The complete pixel-information-box length.</returns>
private static int WritePixelInformationPropertyBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
int memoryOffset,
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);
buffer[bytesWritten++] = (byte)channelBitDepths.Length;
channelBitDepths.CopyTo(buffer[bytesWritten..]);
@ -572,18 +690,18 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes one common encoded precision for every image channel.
/// </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="channelCount">The number of encoded image channels.</param>
/// <param name="channelBitDepth">The common encoded precision.</param>
/// <returns>The complete pixel-information-box length.</returns>
private static int WritePixelInformationPropertyBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
int memoryOffset,
int channelCount,
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);
buffer[bytesWritten++] = (byte)channelCount;
buffer.Slice(bytesWritten, channelCount).Fill(channelBitDepth);
@ -596,16 +714,16 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes an AV1 codec-configuration property.
/// </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="configuration">The fixed image configuration.</param>
/// <returns>The complete AV1 codec-configuration-box length.</returns>
private static int WriteAv1CodecConfigurationPropertyBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
int memoryOffset,
Av1CodecConfiguration configuration)
{
Span<byte> buffer = memory.GetSpan(memoryOffset, 8 + Av1CodecConfiguration.FixedHeaderSize);
Span<byte> buffer = memory.Slice(memoryOffset, Av1CodecConfigurationPropertyBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Av1C);
configuration.WriteFixedHeader(buffer.Slice(bytesWritten, Av1CodecConfiguration.FixedHeaderSize));
bytesWritten += Av1CodecConfiguration.FixedHeaderSize;
@ -617,17 +735,17 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes the registered type of an auxiliary image item.
/// </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="auxiliaryType">The null-terminated registered auxiliary type.</param>
/// <returns>The complete auxiliary-type-box length.</returns>
private static int WriteAuxiliaryTypePropertyBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
int memoryOffset,
string 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);
bytesWritten += Encoding.UTF8.GetBytes(auxiliaryType, buffer[bytesWritten..]);
buffer[bytesWritten++] = 0;
@ -639,17 +757,16 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes an unrestricted ICC color profile for a color image item.
/// </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="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>
private static int WriteIccColorInformationPropertyBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
int memoryOffset,
IccProfile profile)
ReadOnlyMemory<byte> profileData)
{
ReadOnlyMemory<byte> profileData = profile.GetDataForWriting();
Span<byte> buffer = memory.GetSpan(memoryOffset, 12 + profileData.Length);
Span<byte> buffer = memory.Slice(memoryOffset, IccColorInformationPropertyBoxFixedLength + profileData.Length);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Prof);
bytesWritten += 4;
@ -663,16 +780,16 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes an H.273 color description for a color image item.
/// </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="profile">The color description to write.</param>
/// <returns>The complete color-information-box length.</returns>
private static int WriteColorInformationPropertyBox(
AutoExpandingMemory<byte> memory,
Span<byte> memory,
int memoryOffset,
CicpProfile profile)
{
Span<byte> buffer = memory.GetSpan(memoryOffset, 19);
Span<byte> buffer = memory.Slice(memoryOffset, CicpColorInformationPropertyBoxLength);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Colr);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Nclx);
bytesWritten += 4;
@ -691,13 +808,13 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes an item's display width and height as an image-spatial-extents property.
/// </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="item">The item whose extent is written.</param>
/// <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);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Width);
bytesWritten += 4;
@ -711,15 +828,14 @@ internal sealed partial class HeifEncoderCore
/// <summary>
/// Writes version-one file-relative locations for every ordered item extent.
/// </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="items">The items and relative payload extents to locate.</param>
/// <param name="mediaDataOffset">The absolute stream offset of the media-data payload.</param>
/// <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.GetSpan(memoryOffset, 16 + (items.Count * 8) + (extentCount * 12));
Span<byte> buffer = memory.Slice(memoryOffset, GetItemLocationBoxLength(items));
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
@ -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.
quantizer = Math.Max(quantizer, 1);
return quantizer < 62 ? quantizer * 4 : quantizer == 62 ? 249 : 255;
return Av1QuantizationLookup.GetQIndex(quantizer);
}
/// <summary>
@ -802,6 +918,19 @@ internal sealed partial class HeifEncoderCore
CancellationToken cancellationToken)
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);
cancellationToken.ThrowIfCancellationRequested();
ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode(
@ -863,40 +992,11 @@ internal sealed partial class HeifEncoderCore
return;
}
byte[]? exifData = image.Metadata.ExifProfile?.ToByteArray();
if (exifData is not null && exifData.Length > 0)
if (exifData is not null)
{
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;
Span<byte> offsetBuffer = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(offsetBuffer, (uint)tiffHeaderOffset);
BinaryPrimitives.WriteUInt32BigEndian(offsetBuffer, tiffHeaderOffset);
stream.Write(offsetBuffer);
stream.Write(exifData);
@ -918,8 +1018,7 @@ internal sealed partial class HeifEncoderCore
links.Add(exifLink);
}
byte[]? xmpData = image.Metadata.XmpProfile?.Data;
if (xmpData is not null && xmpData.Length > 0)
if (xmpData is not null)
{
long xmpOffset = stream.Length;
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>
/// Encodes the source pixels as the current legacy JPEG item payload.
/// </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>
internal sealed class HeifItem(Heif4CharCode type, uint id)
{
private IccProfile? iccProfile;
private ReadOnlyMemory<byte> serializedIccProfile;
/// <summary>
/// Gets the ID of this Item.
/// </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
/// has no restricted or unrestricted ICC color-information property.
/// </summary>
public IccProfile? IccProfile { get; set; }
public IccProfile? IccProfile
{
get => this.iccProfile;
set
{
this.iccProfile = value;
this.serializedIccProfile = default;
}
}
/// <summary>
/// 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>
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>
/// Set the image extent.
/// </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 SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
@ -74,8 +73,8 @@ public class Av1BitStreamTests
[InlineData(new bool[] { false, true, false, true })]
public void WriteAsBoolean(bool[] booleans)
{
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 8);
Av1BitStreamWriter writer = new(stream);
byte[] buffer = new byte[Numerics.DivideCeil((uint)booleans.Length, 8)];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < booleans.Length; i++)
{
writer.WriteBoolean(booleans[i]);
@ -84,7 +83,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
bool[] actual = new bool[booleans.Length];
for (int i = 0; i < booleans.Length; i++)
{
@ -101,13 +100,13 @@ public class Av1BitStreamTests
[InlineData(4050, 16)]
public void WriteAsLiteral(uint value, int bitCount)
{
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 8);
Av1BitStreamWriter writer = new(stream);
byte[] buffer = new byte[Numerics.DivideCeil((uint)bitCount, 8)];
Av1BitStreamWriter writer = new(buffer);
writer.WriteLiteral(value, bitCount);
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
uint actual = reader.ReadLiteral(bitCount);
Assert.Equal(value, actual);
}
@ -123,8 +122,9 @@ public class Av1BitStreamTests
public void ReadLiteralRainbowArray(int bitCount)
{
uint[] values = Enumerable.Range(0, (1 << bitCount) - 1).Select(i => (uint)i).ToArray();
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 280);
Av1BitStreamWriter writer = new(stream);
int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++)
{
writer.WriteLiteral(values[i], bitCount);
@ -133,7 +133,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
uint[] actuals = new uint[values.Length];
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)
{
uint[] values = [val1, val2, val3, val4];
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 80);
Av1BitStreamWriter writer = new(stream);
int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++)
{
writer.WriteLiteral(values[i], bitCount);
@ -162,7 +163,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
for (int i = 0; i < values.Length; i++)
{
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)
{
uint[] values = [val1, val2, val3, val4];
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 80);
Av1BitStreamWriter writer = new(stream);
byte[] buffer = new byte[values.Length * sizeof(uint)];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++)
{
writer.WriteNonSymmetric(values[i], numberOfSymbols);
@ -192,7 +193,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
uint[] actuals = new uint[4];
for (int i = 0; i < values.Length; i++)
{
@ -213,8 +214,9 @@ public class Av1BitStreamTests
{
int maxValue = (1 << (bitCount - 1)) - 1;
int[] values = Enumerable.Range(-maxValue, maxValue).ToArray();
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 280);
Av1BitStreamWriter writer = new(stream);
int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++)
{
writer.WriteSignedFromUnsigned(values[i], bitCount);
@ -223,7 +225,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
int[] actuals = new int[values.Length];
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)
{
int[] values = [val1, val2, val3, val4];
using AutoExpandingMemory<byte> stream = new(Configuration.Default, 80);
Av1BitStreamWriter writer = new(stream);
int bufferLength = (int)Numerics.DivideCeil((uint)(values.Length * bitCount), 8);
byte[] buffer = new byte[bufferLength];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++)
{
writer.WriteSignedFromUnsigned(values[i], bitCount);
@ -304,7 +307,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetEntireSpan());
Av1BitStreamReader reader = new(buffer);
int[] actuals = new int[4];
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)
{
uint[] values = [val0, val1, val2, val3, val4];
int bufferSize = 80;
using AutoExpandingMemory<byte> stream = new(Configuration.Default, bufferSize);
Av1BitStreamWriter writer = new(stream);
const int MaximumEncodedUInt32Length = 5;
byte[] buffer = new byte[values.Length * MaximumEncodedUInt32Length];
Av1BitStreamWriter writer = new(buffer);
for (int i = 0; i < values.Length; i++)
{
writer.WriteLittleEndianBytes128(values[i]);
@ -353,7 +356,7 @@ public class Av1BitStreamTests
writer.Flush();
// Read the written value back.
Av1BitStreamReader reader = new(stream.GetSpan(bufferSize));
Av1BitStreamReader reader = new(buffer);
uint[] actuals = new uint[5];
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;
// These tests encode at most one 8x8 transform with generated magnitudes no greater than 64.
private const int CoefficientSyntaxBufferLength = 256;
[Fact]
public void NeighborArrayWritesEveryCoveredFourByFourEdgeUnit()
{
@ -405,7 +408,7 @@ public class Av1CoefficientsEntropyTests
IsLeftAvailable = true
};
using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex);
using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex, updateCdf: true);
Av1TileWriter.WritePaletteModeInfo(
picture.Sequence,
picture,
@ -544,7 +547,7 @@ public class Av1CoefficientsEntropyTests
transformBlocks.Fill(new Av1EncoderTransformBlockState { TransformType = Av1TransformType.Identity });
Av1EncoderBlockStruct block = default;
using Av1SymbolEncoder writer = new(Configuration.Default, 4096, BaseQIndex);
using Av1SymbolEncoder writer = new(Configuration.Default, 4096, BaseQIndex, updateCdf: true);
Av1TileWriter.EncodeTransformCoefficientsY(
picture,
context,
@ -681,7 +684,7 @@ public class Av1CoefficientsEntropyTests
transforms.Left[leftIndex] = 16;
picture.TransformFunctionContexts = [transforms];
using Av1SymbolEncoder writer = new(Configuration.Default, 64, BaseQIndex);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
Av1TileWriter.WriteTransformSize(
picture,
writer,
@ -828,7 +831,7 @@ public class Av1CoefficientsEntropyTests
picture.Parent.FrameHeader.CdefParameters.BitCount = 2;
picture.ModeInfoAllocation.Span[16].CdefStrength = 3;
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(
picture.Sequence,
@ -936,7 +939,7 @@ public class Av1CoefficientsEntropyTests
width: 128,
height: 64);
using Av1SymbolEncoder writer = new(Configuration.Default, 512, BaseQIndex);
using Av1SymbolEncoder writer = new(Configuration.Default, 512, BaseQIndex, updateCdf: true);
Av1TileWriter.WriteSuperblock(
picture,
@ -1010,8 +1013,8 @@ public class Av1CoefficientsEntropyTests
Av1PartitionType.Split
];
using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex);
using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex);
using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
foreach (Av1PartitionType decision in decisions)
{
Av1TileWriter.EncodePartition(
@ -1080,8 +1083,8 @@ public class Av1CoefficientsEntropyTests
Av1ChromaPredictionMode.SmoothHorizontal
];
using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex);
using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex);
using Av1SymbolEncoder actualWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
using Av1SymbolEncoder expectedWriter = new(Configuration.Default, 16, BaseQIndex, updateCdf: true);
foreach (Av1ChromaPredictionMode decision in decisions)
{
Av1TileWriter.EncodeIntraChromaMode(
@ -1123,7 +1126,7 @@ public class Av1CoefficientsEntropyTests
int[] leftContexts = new int[1];
Av1TransformBlockContext transformBlockContext = 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> expected = new int[16];
Span<int> actuals = new int[16];
@ -1193,7 +1196,7 @@ public class Av1CoefficientsEntropyTests
int[] leftContexts = new int[1];
Av1TransformBlockContext transformBlockContext = 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];
ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan;
for (int scanIndex = endOfBlock; scanIndex < scan.Length; scanIndex++)
@ -1304,7 +1307,7 @@ public class Av1CoefficientsEntropyTests
int[] leftContexts = new int[transformSize.Get4x4HighCount()];
Av1TransformBlockContext transformBlockContext = 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();
ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan;
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.
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
@ -20,6 +21,9 @@ public class Av1EntropyTests
{
private const int BaseQIndex = 23;
// Short syntax round trips encode only their small in-method symbol vectors.
private const int ShortSyntaxBufferLength = 64;
[Fact]
public void ProbabilityCostTableMatchesDefinition()
{
@ -158,7 +162,7 @@ public class Av1EntropyTests
const int BlockSkipContext = 2;
const int TransformSkipContext = 0;
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(
true,
TransformSize,
@ -293,7 +297,7 @@ public class Av1EntropyTests
[Fact]
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];
Assert.Equal(
@ -563,8 +567,8 @@ public class Av1EntropyTests
coefficients[scan[0]] = -25;
coefficients[scan[2]] = 3;
coefficients[scan[3]] = 1;
using Av1SymbolEncoder actualEncoder = new(Configuration.Default, 64, BaseQIndex);
using Av1SymbolEncoder expectedEncoder = new(Configuration.Default, 64, BaseQIndex);
using Av1SymbolEncoder actualEncoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
using Av1SymbolEncoder expectedEncoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: true);
int initialCost = actualEncoder.GetCoefficientCost(
transformSize,
@ -743,7 +747,7 @@ public class Av1EntropyTests
[Fact]
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(true, 512);
@ -755,22 +759,22 @@ public class Av1EntropyTests
}
[Fact]
public void SymbolWriterUsesOneByteOfScratchPerEstimatedOutputByte()
public void SymbolWriterRentsFixedOutputBuffer()
{
const int initialSize = 257;
const int bufferLength = 257;
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
TestMemoryAllocator.AllocationRequest allocation;
using (Av1SymbolWriter writer = new(configuration, initialSize, updateCdf: false))
using (Av1SymbolWriter writer = new(configuration, bufferLength, updateCdf: false))
{
writer.WriteLiteral(false);
allocation = Assert.Single(allocator.AllocationLog);
Assert.Equal(typeof(byte), allocation.ElementType);
Assert.Equal(initialSize, allocation.Length);
Assert.Equal(bufferLength, allocation.Length);
}
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
@ -778,38 +782,31 @@ public class Av1EntropyTests
}
[Fact]
public void SymbolWriterTransfersExistingOutputAllocationWithoutCopy()
public void SymbolWriterExposesExistingOutputAllocationWithoutCopy()
{
const int initialSize = 257;
const int bufferLength = 257;
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
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(true, 512);
writer.WriteBoolean(false, 8_192);
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.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);
Assert.Equal(allocation.AllocationId, returned.AllocationId);
}
@ -823,7 +820,7 @@ public class Av1EntropyTests
configuration.MemoryAllocator = allocator;
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);
Assert.Equal(typeof(byte), outputScratch.ElementType);
@ -976,7 +973,7 @@ public class Av1EntropyTests
uint[] values = new uint[writeCount];
Array.Fill(values, value);
Configuration configuration = Configuration.Default;
using Av1SymbolWriter writer = new(configuration, (writeCount * bitCount) >> 3);
using Av1SymbolWriter writer = new(configuration, ShortSyntaxBufferLength, updateCdf: true);
// Act
for (int i = 0; i < writeCount; i++)
@ -1070,7 +1067,7 @@ public class Av1EntropyTests
public void RoundTripUniformPaletteIndices()
{
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++)
{
@ -1095,7 +1092,7 @@ public class Av1EntropyTests
public void RoundTripPaletteSymbols()
{
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++)
{
@ -1213,7 +1210,7 @@ public class Av1EntropyTests
ushort[] uColors = [17, 51, 100];
ushort[] deltaVColors = [1, 2, 1];
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.WritePaletteUvColors(colorCache, uColors, deltaVColors, bitDepth);
encoder.WritePaletteUvColors(colorCache, uColors, rawVColors, bitDepth);
@ -1371,7 +1368,7 @@ public class Av1EntropyTests
using Buffer2D<byte> decoded = configuration.MemoryAllocator.Allocate2D<byte>(Width, Height);
Buffer2DRegion<byte> sourceRegion = new(source);
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 plane = 0; plane < 2; plane++)
@ -1481,7 +1478,7 @@ public class Av1EntropyTests
{
// Assign
Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex);
using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1PartitionType[] values = [
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None,
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None, Av1PartitionType.None];
@ -1512,7 +1509,7 @@ public class Av1EntropyTests
// Assign
Av1BlockSize blockSize = (Av1BlockSize)size;
Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex);
using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1PartitionType[] values = [
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal,
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal, Av1PartitionType.Horizontal];
@ -1543,7 +1540,7 @@ public class Av1EntropyTests
// Assign
Av1BlockSize blockSize = (Av1BlockSize)size;
Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex);
using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
Av1PartitionType[] values = [
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical,
Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical, Av1PartitionType.Vertical];
@ -1575,7 +1572,7 @@ public class Av1EntropyTests
{
// Assign
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[] actuals = new bool[values.Length];
@ -1604,7 +1601,7 @@ public class Av1EntropyTests
// Assign
Av1TransformSize transformSizeContext = (Av1TransformSize)transformContext;
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[] actuals = new bool[values.Length];
@ -1637,7 +1634,7 @@ public class Av1EntropyTests
Av1FilterIntraMode filterIntraMode = (Av1FilterIntraMode)intraMode;
Av1PredictionMode intraDirection = (Av1PredictionMode)intraDir;
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.
Av1TransformType[] values = [
@ -1680,8 +1677,8 @@ public class Av1EntropyTests
Av1DefaultDistributions.InterExtendedTransform[extendedSet][(int)squareTransformSize];
Configuration configuration = Configuration.Default;
using Av1SymbolEncoder costEncoder = new(configuration, 100 / 8, BaseQIndex, updateCdf: false);
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex);
using Av1SymbolEncoder costEncoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: false);
using Av1SymbolEncoder encoder = new(configuration, ShortSyntaxBufferLength, BaseQIndex, updateCdf: true);
int transformTypeCount = Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType);
for (int symbol = 0; symbol < transformTypeCount; symbol++)
@ -1737,7 +1734,7 @@ public class Av1EntropyTests
Av1PlaneType planeType = (Av1PlaneType)plane;
Av1TransformClass transformClass = (Av1TransformClass)txClass;
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[] actuals = new int[values.Length];
@ -1765,11 +1762,14 @@ public class Av1EntropyTests
{
// Assign
Configuration configuration = Configuration.Default;
using Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex);
int[] values = Enumerable.Range(0, 16384).ToArray();
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
foreach (int value in values)
{
@ -1797,7 +1797,7 @@ public class Av1EntropyTests
// Assign
int[] values = [3, 6, 7, 0, 2, 0, 2, 1, 1];
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];
// Act
@ -1824,7 +1824,7 @@ public class Av1EntropyTests
// Assign
int[] values = [3, 6, -7, -8, -2, 0, 2, 1, -1];
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];
// Act
@ -1855,7 +1855,7 @@ public class Av1EntropyTests
Av1FilterIntraMode.DC, Av1FilterIntraMode.Vertical, Av1FilterIntraMode.DC, Av1FilterIntraMode.Paeth,
Av1FilterIntraMode.AllFilterIntraModes, Av1FilterIntraMode.Directional157, Av1FilterIntraMode.DC, Av1FilterIntraMode.Directional157];
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];
// Act
@ -1882,7 +1882,7 @@ public class Av1EntropyTests
// Assign
bool[] values = [true, true, false, true, false, false, false];
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];
Assert.Equal(51, encoder.GetUseIntraBlockCopyCost(false));
@ -1934,7 +1934,7 @@ public class Av1EntropyTests
int[] expectedCosts = [1440, 1661, 5231, 5807, 16955, 31656];
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.
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.ModeInfoColumnCount);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, 0);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, 0, updateCdf: true);
Av1TileWriter.WriteIntraBlockCopyInfo(
picture,
writer,
@ -302,7 +302,7 @@ public class Av1IntraBlockCopyTests
Av1PictureControlSet picture = pictureBuffer.Picture;
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];
Av1MotionVector reference = new(0, -2560);
int candidateCount = picture.IntraBlockCopySearch.FindCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>(
@ -397,7 +397,7 @@ public class Av1IntraBlockCopyTests
codedReconstructionLuma,
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];
int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch
.FindPixelCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>(
@ -481,7 +481,7 @@ public class Av1IntraBlockCopyTests
Buffer2DRegion<byte> codedSourceLuma = source.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];
int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch
.FindPixelCandidates<byte, Av1IntraSuperblockEncoder.ByteOperator>(

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

@ -211,7 +211,7 @@ public class Av1IntraSuperblockEncoderTests
SuperblockOrigin = default
};
using Av1SymbolEncoder writer = new(Configuration.Default, 512, 73);
using Av1SymbolEncoder writer = new(Configuration.Default, 512, 73, updateCdf: true);
Av1TileWriter.WriteSuperblock(
picture,
entropyContext,
@ -255,15 +255,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace tileSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace tileBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder tileSymbolEncoder = CreateTileSymbolEncoder(
tilePicture.Picture,
512);
Av1IntraTileWriter tileWriter = new(
tileSymbolEncoder,
source.Frame,
tileReconstruction.Frame,
tilePicture.Picture,
tileCoefficients,
tileSuperblockWorkspace,
tileBlockWorkspace,
initialSize: 512,
effort: 5);
// 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
};
using Av1SymbolEncoder writer = new(Configuration.Default, 256, 37);
using Av1SymbolEncoder writer = new(Configuration.Default, 256, 37, updateCdf: true);
Av1TileWriter.WriteSuperblock(
picture,
entropyContext,
@ -406,15 +409,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace liveSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace liveBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter liveTileWriter = new(
Configuration.Default,
using Av1SymbolEncoder liveSymbolEncoder = CreateTileSymbolEncoder(
livePicture.Picture,
256);
Av1IntraTileWriter liveTileWriter = new(
liveSymbolEncoder,
source.Frame,
liveReconstruction.Frame,
livePicture.Picture,
liveCoefficients,
liveSuperblockWorkspace,
liveBlockWorkspace,
initialSize: 256,
effort: 5);
Assert.True(precomputedTile.GetSpan().SequenceEqual(liveTileWriter.GetTileData(0)));
@ -531,15 +537,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace tileSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace tileBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder tileSymbolEncoder = CreateTileSymbolEncoder(
tilePicture.Picture,
256);
Av1IntraTileWriter tileWriter = new(
tileSymbolEncoder,
source.Frame,
tileReconstruction.Frame,
tilePicture.Picture,
tileCoefficients,
tileSuperblockWorkspace,
tileBlockWorkspace,
initialSize: 256,
effort: 5);
Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
@ -603,7 +612,7 @@ public class Av1IntraSuperblockEncoderTests
int[] costs = new int[2];
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(
picture.Picture,
entropyContext,
@ -696,7 +705,7 @@ public class Av1IntraSuperblockEncoderTests
};
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(
picture,
entropyContext,
@ -739,16 +748,15 @@ public class Av1IntraSuperblockEncoderTests
(byte)224,
32,
224,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256,
effort: 5));
AssertProductionTileSelectsExactLumaPalette(
@ -761,16 +769,15 @@ public class Av1IntraSuperblockEncoderTests
(ushort)3584,
512,
3584,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256,
effort: 5));
AssertProductionTileSelectsExactLumaPalette(
@ -783,16 +790,15 @@ public class Av1IntraSuperblockEncoderTests
(byte)208,
48,
208,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256,
effort: 5));
}
@ -809,16 +815,15 @@ public class Av1IntraSuperblockEncoderTests
(byte)192,
64,
192,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256,
effort: 6));
AssertProductionTileSelectsExactLumaPalette(
@ -831,16 +836,15 @@ public class Av1IntraSuperblockEncoderTests
(ushort)3072,
1024,
3072,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256,
effort: 6));
}
@ -938,15 +942,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
256);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256,
effort: 5);
ref Av1MacroBlockModeInfo mode = ref picture.Picture.GetMacroBlockModeInfo(default);
@ -1151,15 +1158,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
512);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 5);
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 Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
512);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 5);
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
@ -1306,16 +1319,15 @@ public class Av1IntraSuperblockEncoderTests
=> VerifyProductionTileSelectsChromaFromReconstructedLuma<byte>(
colorFormatValue,
8,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 5));
[Theory]
@ -1331,16 +1343,15 @@ public class Av1IntraSuperblockEncoderTests
=> VerifyProductionTileSelectsChromaFromReconstructedLuma<ushort>(
colorFormatValue,
bitDepth,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 5));
private static void VerifyProductionTileSelectsChromaFromReconstructedLuma<TSample>(
@ -1352,6 +1363,7 @@ public class Av1IntraSuperblockEncoderTests
const int Width = 16;
const int Height = 16;
const int QIndex = 1;
const int TileBufferLength = 512;
const int AlphaU = 16;
const int AlphaV = -16;
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
@ -1426,7 +1438,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = 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,
pilotReconstruction.Frame,
pilotPicture.Picture,
@ -1528,7 +1545,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = 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,
reconstruction.Frame,
picture.Picture,
@ -1584,16 +1606,15 @@ public class Av1IntraSuperblockEncoderTests
filterIntraModeValue,
8,
false,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 5),
static (mode, destination, stride, above, left, width, height, _, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1617,16 +1638,15 @@ public class Av1IntraSuperblockEncoderTests
filterIntraModeValue,
bitDepth,
false,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 5),
static (mode, destination, stride, above, left, width, height, sampleBitDepth, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1646,16 +1666,15 @@ public class Av1IntraSuperblockEncoderTests
(int)Av1FilterIntraMode.DC,
8,
true,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 6),
static (mode, destination, stride, above, left, width, height, _, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1669,16 +1688,15 @@ public class Av1IntraSuperblockEncoderTests
(int)Av1FilterIntraMode.DC,
bitDepth,
true,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512,
effort: 6),
static (mode, destination, stride, above, left, width, height, sampleBitDepth, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
@ -1703,6 +1721,7 @@ public class Av1IntraSuperblockEncoderTests
const int Width = 16;
const int Height = 16;
const int QIndex = 37;
const int TileBufferLength = 512;
const int TargetX = 8;
const int TargetY = 8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
@ -1769,7 +1788,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = 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,
pilotReconstruction.Frame,
pilotPicture.Picture,
@ -1926,7 +1950,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = 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,
reconstruction.Frame,
picture.Picture,
@ -2125,15 +2154,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
2048);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 2048,
effort: 5);
ref Av1MacroBlockModeInfo topRightBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(0, 2));
@ -2150,32 +2182,30 @@ public class Av1IntraSuperblockEncoderTests
Av1BitDepth.EightBit,
8,
static value => (byte)value,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096,
effort: 5));
VerifyProductionTileSelectsIntraBlockCopy(
Av1BitDepth.TwelveBit,
12,
static value => (ushort)(value << 4),
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
static (writer, source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new Av1IntraTileWriter(
Configuration.Default,
writer,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096,
effort: 5));
}
@ -2189,6 +2219,7 @@ public class Av1IntraSuperblockEncoderTests
const int Width = 328;
const int Height = 8;
const int QIndex = 1;
const int TileBufferLength = 4096;
const int ReferenceColumn = 0;
const int TargetColumn = 320;
ObuColorConfig colorConfig = new()
@ -2268,7 +2299,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = 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,
reconstruction.Frame,
picture.Picture,
@ -2396,15 +2432,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
4096);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096,
effort: 5);
Point targetModeInfoPosition = new(TargetColumn >> Av1Constants.ModeInfoSizeLog2, 0);
@ -2507,15 +2546,18 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = new(
Configuration.Default,
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(
picture.Picture,
4096);
Av1IntraTileWriter tileWriter = new(
symbolEncoder,
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096,
effort: 5);
Assert.Equal(4, coefficients.SuperblockCount);
@ -2672,6 +2714,7 @@ public class Av1IntraSuperblockEncoderTests
where TSample : unmanaged, IBinaryInteger<TSample>
{
const int QIndex = 37;
const int TileBufferLength = 256;
ObuColorConfig colorConfig = new()
{
IsMonochrome = true,
@ -2766,7 +2809,12 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderSuperblockWorkspace superblockWorkspace = 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,
reconstruction.Frame,
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>(
Av1SymbolEncoder writer,
Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> reconstruction,
Av1PictureControlSet picture,

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

@ -88,7 +88,7 @@ public class Av1InverseTransformTests
private static void AssertTwelveBitWideIntermediateParity()
{
const int cosBit = 12;
Av1TransformStageRange stageRange = default;
InlineArray12<byte> stageRange = default;
for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++)
{
stageRange[index] = 20;
@ -182,7 +182,7 @@ public class Av1InverseTransformTests
Vector128<int> expected128,
Vector256<int> input256,
Vector256<int> expected256,
Av1TransformStageRange stageRange)
InlineArray12<byte> stageRange)
where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator
{
const int cosBit = 12;
@ -252,8 +252,8 @@ public class Av1InverseTransformTests
Av1TransformSize.Size16x16,
bitDepth);
Av1TransformStageRange configuredRowRange = config.StageRangeRow;
Av1TransformStageRange configuredColumnRange = config.StageRangeColumn;
InlineArray12<byte> configuredRowRange = config.StageRangeRow;
InlineArray12<byte> configuredColumnRange = config.StageRangeColumn;
for (int index = 0; index < config.StageNumberRow; index++)
{
@ -600,7 +600,7 @@ public class Av1InverseTransformTests
where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator
{
const int cosBit = 12;
Av1TransformStageRange stageRange = default;
InlineArray12<byte> stageRange = default;
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()
{
byte[] sequenceHeader = CreateNonReducedSequenceHeaderObu(default);
using AutoExpandingMemory<byte> frameMemory = new(Configuration.Default, 8);
Av1BitStreamWriter frameWriter = new(frameMemory);
byte[] framePayload = new byte[2];
Av1BitStreamWriter frameWriter = new(framePayload);
frameWriter.WriteBoolean(false);
frameWriter.WriteLiteral((uint)ObuFrameType.IntraOnlyFrame, 2);
@ -605,7 +605,7 @@ public class ObuFrameHeaderTests
int frameObuOffset = sequenceHeader.Length;
bitStream[frameObuOffset] = (byte)(((byte)ObuType.FrameHeader << 3) | 0x02);
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));
}
@ -796,6 +796,8 @@ public class ObuFrameHeaderTests
/// <returns>The complete explicitly sized sequence-header OBU.</returns>
private static byte[] CreateNonReducedSequenceHeaderObu(InvalidSequenceHeaderCase invalidCase)
{
const int SequenceHeaderBufferLength = 32;
bool hasTimingInfo = invalidCase is
InvalidSequenceHeaderCase.ZeroDisplayTick or
InvalidSequenceHeaderCase.ZeroTimeScale or
@ -807,8 +809,8 @@ public class ObuFrameHeaderTests
InvalidSequenceHeaderCase.MainProfileSrgbIdentity or
InvalidSequenceHeaderCase.SubsampledIdentityMatrix;
using AutoExpandingMemory<byte> payloadMemory = new(Configuration.Default, 32);
Av1BitStreamWriter writer = new(payloadMemory);
byte[] payloadBuffer = new byte[SequenceHeaderBufferLength];
Av1BitStreamWriter writer = new(payloadBuffer);
writer.WriteLiteral((uint)ObuSequenceProfile.Main, 3);
writer.WriteBoolean(false);
writer.WriteBoolean(false);
@ -908,7 +910,7 @@ public class ObuFrameHeaderTests
byte[] obu = new byte[payloadLength + 2];
obu[0] = (byte)(((byte)ObuType.SequenceHeader << 3) | 0x02);
obu[1] = (byte)payloadLength;
payloadMemory.GetSpan(payloadLength).CopyTo(obu.AsSpan(2));
payloadBuffer.AsSpan(0, payloadLength).CopyTo(obu.AsSpan(2));
return obu;
}

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

@ -329,7 +329,7 @@ public class HeifDecoderTests
[Fact]
public void DecodeIgnoresUnknownTopLevelBox()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
data = InsertBytes(data, data.Length, CreateUnknownBox());
using Image<Rgba32> image = Image.Load<Rgba32>(data);
@ -412,7 +412,7 @@ public class HeifDecoderTests
[Fact]
public void DecodePropagatesConfigurationToLegacyJpegItems()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
Configuration configuration = Configuration.CreateDefaultInstance();
DecoderOptions options = new() { Configuration = configuration };
@ -555,7 +555,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyIgnoresUnknownMetadataBox()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox());
@ -701,7 +701,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Jpeg)]
public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand)
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new();
@ -715,7 +715,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Avis)]
public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand)
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new();
@ -745,7 +745,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Jpgs)]
public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand)
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new();
@ -755,7 +755,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsUnsupportedBrands()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType);
@ -767,7 +767,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyAcceptsExtendedSizeTopLevelBox()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[16];
BinaryPrimitives.WriteUInt32BigEndian(box, 1);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
@ -782,7 +782,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyAcceptsUuidTopLevelBox()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[24];
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
@ -796,7 +796,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyAcceptsSizeZeroTopLevelBox()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
byte[] box = CreateUnknownBox();
BinaryPrimitives.WriteUInt32BigEndian(box, 0);
data = InsertBytes(data, data.Length, box);
@ -809,7 +809,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyAcceptsExtendedSizeItemInfoEntry()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -829,7 +829,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsSizeZeroMetadataChild()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
byte[] box = CreateUnknownBox();
@ -843,7 +843,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsMetadataChildBeyondParent()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
byte[] box = CreateUnknownBox();
@ -857,7 +857,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsItemInfoEntryBeyondParent()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -871,7 +871,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsBoxSmallerThanHeader()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
byte[] box = CreateUnknownBox();
BinaryPrimitives.WriteUInt32BigEndian(box, 4);
data = InsertBytes(data, data.Length, box);
@ -882,7 +882,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsTruncatedExtendedSizeHeader()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[12];
BinaryPrimitives.WriteUInt32BigEndian(box, 1);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
@ -894,7 +894,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsTruncatedUuidHeader()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
byte[] box = new byte[16];
BinaryPrimitives.WriteUInt32BigEndian(box, 24);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
@ -906,7 +906,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyAcceptsItemPropertiesBeforeItemInfo()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -922,7 +922,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyAcceptsItemLocationBeforeItemInfo()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -938,7 +938,7 @@ public class HeifDecoderTests
[Fact]
public void IdentifyRejectsDuplicateUniqueMetadataBox()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12);
@ -949,11 +949,11 @@ public class HeifDecoderTests
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
}
private static byte[] CreateEncodedContainer()
private static byte[] CreateLegacyJpegContainer()
{
using Image<Rgba32> image = new(2, 3);
using MemoryStream stream = new();
image.Save(stream, new HeifEncoder());
image.Save(stream, new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
return stream.ToArray();
}
@ -962,7 +962,7 @@ public class HeifDecoderTests
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 metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
@ -992,7 +992,7 @@ public class HeifDecoderTests
private static byte[] CreateContainerWithMalformedJpegMetadata()
{
byte[] data = CreateEncodedContainer();
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
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.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.ColorProfiles;
@ -39,7 +40,7 @@ public class HeifEncoderTests
{
HeifEncoder encoder = new();
Assert.Equal(HeifCompressionMethod.LegacyJpeg, encoder.CompressionMethod);
Assert.Equal(HeifCompressionMethod.Av1, encoder.CompressionMethod);
Assert.Null(encoder.Quality);
Assert.Null(encoder.AlphaQuality);
Assert.Equal(5, encoder.Effort);
@ -91,7 +92,11 @@ public class HeifEncoderTests
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30);
using MemoryStream stream = new();
HeifEncoder encoder = new() { Quality = 0 };
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Quality = 0
};
image.Save(stream, encoder);
@ -109,7 +114,9 @@ public class HeifEncoderTests
using MemoryStream storage = new();
using NonSeekableStream destination = new(storage);
image.Save(destination, new HeifEncoder());
image.Save(
destination,
new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
Assert.NotEqual(0, storage.Length);
storage.Position = 0;
@ -126,7 +133,9 @@ public class HeifEncoderTests
stream.Write([1, 2, 3, 4]);
long fileStart = stream.Position;
image.Save(stream, new HeifEncoder());
image.Save(
stream,
new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
stream.Position = fileStart;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
@ -141,7 +150,11 @@ public class HeifEncoderTests
using Image<Rgb24> image = new(8, 8);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
using MemoryStream stream = new();
HeifEncoder encoder = new() { SkipMetadata = skipMetadata };
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
SkipMetadata = skipMetadata
};
image.Save(stream, encoder);
@ -162,7 +175,11 @@ public class HeifEncoderTests
{
using Image<Rgba32> image = new(1, 1);
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.Equal(0, stream.Length);
@ -175,7 +192,11 @@ public class HeifEncoderTests
{
using Image<Rgba32> image = new(1, 1);
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.Equal(0, stream.Length);
@ -349,6 +370,12 @@ public class HeifEncoderTests
}
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;
using MemoryStream stream = new();
HeifEncoder encoder = new()
@ -363,10 +390,19 @@ public class HeifEncoderTests
Assert.Equal((uint)Heif4CharCode.Avis, BinaryPrimitives.ReadUInt32BigEndian(file.AsSpan(8)));
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(repeatCount, decoded.Metadata.GetHeifMetadata().RepeatCount);
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++)
{
Assert.Equal(
@ -382,6 +418,10 @@ public class HeifEncoderTests
image.Frames.AddFrame(image.Frames.RootFrame);
image.Frames.RootFrame.Metadata.GetHeifMetadata().FrameDelay = new Rational(1, 10);
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();
storage.Write([1, 2, 3, 4]);
long fileStart = storage.Position;
@ -389,7 +429,8 @@ public class HeifEncoderTests
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0
Effort = 0,
SkipMetadata = true
};
image.Save(destination, encoder);
@ -397,6 +438,9 @@ public class HeifEncoderTests
using Image<Rgb24> decoded = Image.Load<Rgb24>(storage);
Assert.Equal(image.Size, decoded.Size);
Assert.Equal(image.Frames.Count, decoded.Frames.Count);
Assert.Null(decoded.Metadata.IccProfile);
Assert.Null(decoded.Metadata.ExifProfile);
Assert.Null(decoded.Metadata.XmpProfile);
}
[Theory]
@ -867,9 +911,10 @@ public class HeifEncoderTests
alphaItem.SetExtent(new Size(64, 48));
List<HeifItem> items = [colorItem, alphaItem];
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items);
ReadOnlySpan<byte> propertyBox = memory.GetSpan(length);
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
Assert.Equal(length, BinaryPrimitives.ReadInt32BigEndian(propertyBox));
Assert.Equal(Heif4CharCode.Iprp, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(propertyBox[4..]));
@ -970,9 +1015,10 @@ public class HeifEncoderTests
new HeifItem(Heif4CharCode.Mime, 3)
];
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items);
ReadOnlySpan<byte> propertyBox = memory.GetSpan(length);
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
const int IpcoOffset = 8;
int ipcoEnd = IpcoOffset + BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]);
int propertyOffset = IpcoOffset + 8;
@ -1034,9 +1080,10 @@ public class HeifEncoderTests
items.Add(item);
}
using AutoExpandingMemory<byte> memory = new(Configuration.Default, 16);
int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items);
ReadOnlySpan<byte> propertyBox = memory.GetSpan(length);
int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items);
using IMemoryOwner<byte> owner = Configuration.Default.MemoryAllocator.Allocate<byte>(expectedLength);
Span<byte> propertyBox = owner.Memory.Span[..expectedLength];
int length = HeifEncoderCore.WriteItemPropertiesBox(propertyBox, 0, items);
const int IpcoOffset = 8;
int ipcoSize = BinaryPrimitives.ReadInt32BigEndian(propertyBox[IpcoOffset..]);
int ipmaOffset = IpcoOffset + ipcoSize;
@ -1061,7 +1108,7 @@ public class HeifEncoderTests
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = HeifCompressionMethod.Av1;
using MemoryStream stream = new();
HeifEncoder encoder = new();
HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.LegacyJpeg };
image.Save(stream, encoder);
@ -1076,7 +1123,7 @@ public class HeifEncoderTests
{
using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance));
using MemoryStream stream = new();
HeifEncoder encoder = new();
HeifEncoder encoder = new() { CompressionMethod = compressionMethod };
image.Save(stream, encoder);
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<JpegEncoder>().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<TiffEncoder>().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