From 74fedce2810559efe5310e17176b306dc8694df1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 4 Sep 2026 13:52:36 +1000 Subject: [PATCH] Refine AV1 encoder memory ownership and storage --- src/ImageSharp/Common/InlineArray.cs | 81 ++++ src/ImageSharp/Common/InlineArray.tt | 2 +- .../Formats/Heif/Av1/Av1BitStreamWriter.cs | 52 +- .../Heif/Av1/Entropy/Av1Distribution.cs | 15 +- .../Av1/Entropy/Av1FrameEntropyContexts.cs | 15 - .../Heif/Av1/Entropy/Av1SymbolEncoder.cs | 20 +- .../Heif/Av1/Entropy/Av1SymbolWriter.cs | 40 +- .../Av1/Motion/Av1GlobalMotionParameters.cs | 15 - .../Motion/Av1MotionVariationCandidates.cs | 14 - .../Av1/Motion/Av1ReferenceMotionVectors.cs | 27 -- .../ObuFilmGrainParameters.cs | 41 -- .../Av1/OpenBitstreamUnit/ObuFrameHeader.cs | 14 - .../OpenBitstreamUnit/ObuTileGroupHeader.cs | 18 +- .../Heif/Av1/OpenBitstreamUnit/ObuWriter.cs | 21 +- .../Heif/Av1/Pipeline/Av1FrameEncoder.cs | 67 ++- .../Heif/Av1/Pipeline/Av1IntraTileWriter.cs | 51 +- .../Quantizers/Av1QuantizationLookup.cs | 55 ++- .../Heif/Av1/Tiling/Av1BlockModeInfo.cs | 14 - .../Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs | 21 - .../Av1/Tiling/Av1EncoderPredictionUnit.cs | 10 - .../Heif/Av1/Tiling/Av1LoopRestorationUnit.cs | 26 +- .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 13 - .../Av1Transform2dFlipConfiguration.cs | 24 +- .../Av1/Transform/Av1TransformStageRange.cs | 18 - .../Av1Inverse2dTransformer.Adst16Operator.cs | 6 +- .../Av1Inverse2dTransformer.Adst4Operator.cs | 6 +- .../Av1Inverse2dTransformer.Adst8Operator.cs | 6 +- .../Av1Inverse2dTransformer.Dct16Operator.cs | 6 +- .../Av1Inverse2dTransformer.Dct32Operator.cs | 6 +- .../Av1Inverse2dTransformer.Dct4Operator.cs | 6 +- .../Av1Inverse2dTransformer.Dct64Operator.cs | 6 +- .../Av1Inverse2dTransformer.Dct8Operator.cs | 6 +- ...Inverse2dTransformer.Identity16Operator.cs | 6 +- ...Inverse2dTransformer.Identity32Operator.cs | 6 +- ...1Inverse2dTransformer.Identity4Operator.cs | 6 +- ...1Inverse2dTransformer.Identity8Operator.cs | 6 +- .../Av1Inverse2dTransformer.Operator.cs | 6 +- .../Formats/Heif/HeifConfigurationModule.cs | 1 + src/ImageSharp/Formats/Heif/HeifEncoder.cs | 4 +- .../Formats/Heif/HeifEncoderCore.Sequence.cs | 445 ++++++++++++++---- .../Formats/Heif/HeifEncoderCore.cs | 357 +++++++++----- src/ImageSharp/Formats/Heif/HeifItem.cs | 30 +- src/ImageSharp/Memory/AutoExpandingMemory.cs | 78 --- .../Formats/Heif/Av1/Av1BitStreamTests.cs | 55 ++- .../Heif/Av1/Av1CoefficientsEntropyTests.cs | 27 +- .../Formats/Heif/Av1/Av1EntropyTests.cs | 90 ++-- .../Heif/Av1/Av1IntraBlockCopyTests.cs | 8 +- .../Av1/Av1IntraSuperblockEncoderTests.cs | 217 ++++++--- .../Heif/Av1/Av1InverseTransformTests.cs | 10 +- .../Formats/Heif/Av1/ObuFrameHeaderTests.cs | 14 +- .../Formats/Heif/HeifDecoderTests.cs | 48 +- .../Formats/Heif/HeifEncoderTests.cs | 87 +++- .../Formats/ImageFormatManagerTests.cs | 2 +- .../Memory/AutoExpandingMemoryTests.cs | 53 --- 54 files changed, 1263 insertions(+), 1015 deletions(-) delete mode 100644 src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs delete mode 100644 src/ImageSharp/Memory/AutoExpandingMemory.cs delete mode 100644 tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index 700551a8f3..12c12bd365 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -8,6 +8,24 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp; +/// +/// Represents a safe, fixed sized buffer of 2 elements. +/// +[InlineArray(2)] +internal struct InlineArray2 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 3 elements. +/// +[InlineArray(3)] +internal struct InlineArray3 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 4 elements. /// @@ -17,6 +35,24 @@ internal struct InlineArray4 private T t; } +/// +/// Represents a safe, fixed sized buffer of 6 elements. +/// +[InlineArray(6)] +internal struct InlineArray6 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 7 elements. +/// +[InlineArray(7)] +internal struct InlineArray7 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 8 elements. /// @@ -26,6 +62,24 @@ internal struct InlineArray8 private T t; } +/// +/// Represents a safe, fixed sized buffer of 10 elements. +/// +[InlineArray(10)] +internal struct InlineArray10 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 12 elements. +/// +[InlineArray(12)] +internal struct InlineArray12 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 14 elements. /// @@ -62,6 +116,24 @@ internal struct InlineArray19 private T t; } +/// +/// Represents a safe, fixed sized buffer of 24 elements. +/// +[InlineArray(24)] +internal struct InlineArray24 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 25 elements. +/// +[InlineArray(25)] +internal struct InlineArray25 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 26 elements. /// @@ -80,6 +152,15 @@ internal struct InlineArray36 private T t; } +/// +/// Represents a safe, fixed sized buffer of 65 elements. +/// +[InlineArray(65)] +internal struct InlineArray65 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 256 elements. /// diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index d689b0469a..3869e42bb6 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/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() { diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs index a7e193b11c..51ffdb432d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs +++ b/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; /// -/// 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. /// internal ref struct Av1BitStreamWriter { @@ -16,19 +14,9 @@ internal ref struct Av1BitStreamWriter private const int WordSize = 8; /// - /// The expanding output allocation. - /// - private readonly AutoExpandingMemory memory; - - /// - /// The current writable view over . + /// The writable output buffer. /// - private Span span; - - /// - /// The final byte index that can be written without expanding . - /// - private int capacityTrigger; + private readonly Span span; /// /// The partially assembled output byte. @@ -38,12 +26,10 @@ internal ref struct Av1BitStreamWriter /// /// Initializes a new instance of the struct. /// - /// The reusable expanding output allocation. - public Av1BitStreamWriter(AutoExpandingMemory memory) + /// The preallocated output buffer. + public Av1BitStreamWriter(Span span) { - this.memory = memory; - this.span = memory.GetEntireSpan(); - this.capacityTrigger = memory.Capacity - 1; + this.span = span; } /// @@ -54,7 +40,7 @@ internal ref struct Av1BitStreamWriter /// /// Gets the current output capacity in bytes. /// - public readonly int Capacity => this.memory.Capacity; + public readonly int Capacity => this.span.Length; /// /// 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; } /// - /// Stores the current output byte, expanding the allocation when necessary. + /// Stores the current output byte. /// 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; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs index c56744fb05..d962e5a605 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs @@ -36,7 +36,7 @@ internal sealed class Av1Distribution /// /// The inverse cumulative thresholds followed by the required zero sentinel. /// - private readonly uint[] probabilities; + private InlineArray16 probabilities; /// /// The symbol-count contribution to the adaptive update rate. @@ -273,13 +273,13 @@ internal sealed class Av1Distribution /// The symbol-count contribution to the update rate. private Av1Distribution(ReadOnlySpan props, int speed) { - this.probabilities = new uint[props.Length]; + Span 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 /// The distribution state to copy. private Av1Distribution(Av1Distribution source) { - this.probabilities = new uint[source.probabilities.Length]; - source.probabilities.CopyTo(this.probabilities, 0); + ReadOnlySpan sourceProbabilities = source.probabilities; + Span 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 sourceProbabilities = source.probabilities; + Span probabilities = this.probabilities; + sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities); this.updateCount = source.updateCount; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs index c180fad59f..4394aa06ea 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs +++ b/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; /// @@ -139,17 +137,4 @@ internal sealed class Av1FrameEntropyContexts } } } - - /// - /// Provides inline storage for every entropy snapshot graph that one decoder session can allocate concurrently. - /// - /// The stored reference type. - [InlineArray(MaximumSnapshotCount)] - private struct InlineArray10 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 9a02cb479e..87a3170933 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; /// /// Encodes AV1 tile syntax elements and transform coefficients with tile-local adaptive distributions. /// -internal class Av1SymbolEncoder : IDisposable +internal sealed class Av1SymbolEncoder : IDisposable { /// /// 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 class for one AV1 tile. /// /// The configuration providing output and temporary memory. - /// The initial output buffer size in bytes. + /// The complete fixed output allocation length in bytes. /// The frame base quantizer index. /// A value indicating whether encoded symbols adapt their tile distributions. - 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 } /// - /// 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. /// /// The memory owner containing the encoded tile bytes. public IMemoryOwner Exit() @@ -1320,18 +1320,18 @@ internal class Av1SymbolEncoder : IDisposable } /// - /// 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. /// - /// The number of encoded bytes at the beginning of the returned allocation. - /// The complete allocation containing the encoded tile prefix. - public IMemoryOwner Exit(out int length) + /// The number of encoded bytes in the returned memory. + /// The encoded prefix, valid until this encoder is disposed. + public ReadOnlyMemory Exit(out int length) { ref Av1SymbolWriter w = ref this.writer; return w.Exit(out length); } /// - /// Releases output memory that has not been transferred by . + /// Releases the range-coder output buffer and coefficient scratch memory. /// public void Dispose() { diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs index 522df8653e..fda8597fa2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; /// /// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence. /// -internal class Av1SymbolWriter : IDisposable +internal sealed class Av1SymbolWriter : IDisposable { /// /// The lower endpoint of the current coding interval. @@ -36,9 +36,14 @@ internal class Av1SymbolWriter : IDisposable private readonly Configuration configuration; /// - /// The output bytes accumulated during renormalization. + /// The owner of the fixed output buffer supplied for this tile. /// - private readonly AutoExpandingMemory memory; + private readonly IMemoryOwner bufferOwner; + + /// + /// The requested output range, excluding any excess capacity returned by a pooling allocator. + /// + private readonly Memory buffer; /// /// Indicates whether encoded symbols adapt their distributions. @@ -51,22 +56,23 @@ internal class Av1SymbolWriter : IDisposable private int position; /// - /// Initializes a new instance of the class with an estimated output size. + /// Initializes a new instance of the class with a bounded output size. /// /// The configuration that supplies output allocation. - /// The estimated encoded size in bytes. + /// The complete fixed output allocation length in bytes. /// A value indicating whether encoded symbols adapt their distributions. - public Av1SymbolWriter(Configuration configuration, int initialSize, bool updateCdf = true) + public Av1SymbolWriter(Configuration configuration, int bufferLength, bool updateCdf) { this.configuration = configuration; - this.memory = new AutoExpandingMemory(configuration, initialSize); + this.bufferOwner = configuration.MemoryAllocator.Allocate(bufferLength); + this.buffer = this.bufferOwner.Memory[..bufferLength]; this.updateCdf = updateCdf; } /// - /// Releases the expandable pre-carry buffer. + /// Releases the tile output buffer. /// - public void Dispose() => this.memory.Dispose(); + public void Dispose() => this.bufferOwner.Dispose(); /// /// 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 output = this.configuration.MemoryAllocator.Allocate(length); - this.memory.GetSpan(length).CopyTo(output.GetSpan()[..length]); + this.buffer.Span[..length].CopyTo(output.Memory.Span); return output; } /// - /// Finalizes the range-coded sequence and transfers its current allocation without copying. + /// Finalizes the range-coded sequence and exposes its encoded prefix without copying. /// - /// The number of encoded bytes at the beginning of the returned allocation. - /// The complete allocation containing the encoded byte prefix. - public IMemoryOwner Exit(out int length) + /// The number of encoded bytes in the returned memory. + /// The encoded prefix, valid until this writer is disposed. + public ReadOnlyMemory Exit(out int length) { length = this.FinalizeRange(); - return this.memory.Detach(); + return this.buffer[..length]; } /// @@ -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 buffer = this.memory.GetSpan(pos + pendingByteCount); + Span 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 buffer = this.memory.GetSpan(this.position + sizeof(ulong)); + Span buffer = this.buffer.Span[..(this.position + sizeof(ulong))]; int readyByteCount = (s >> 3) + 1; c += 24 - (readyByteCount << 3); ulong output = low >> c; diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs index ee1930f7a6..532b6f2e67 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs +++ b/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; - - /// - /// Provides inline storage for the six parameters in an AV1 affine matrix. - /// - /// The stored parameter type. - [InlineArray(6)] - private struct InlineArray6 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs index 3987ecd902..aa89625234 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs +++ b/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 /// for inter prediction or intra-block copy; otherwise, . private static bool IsOverlappable(Av1BlockModeInfo candidate) => candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; - - /// - /// Provides fixed storage for AV1's eight local warped-motion projection samples. - /// - /// The source or reference point type stored in the inline buffer. - [InlineArray(ProjectionSampleCapacity)] - private struct InlineArray8 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs index 04dfb5060c..20209cc36b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs +++ b/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; - - /// - /// Provides fixed storage for AV1's eight reference-motion-vector candidates. - /// - /// The motion-vector or weight type stored in the inline buffer. - [InlineArray(CandidateCapacity)] - private struct InlineArray8 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } - - /// - /// Provides fixed storage for the nearest and near motion-vector references. - /// - /// The motion-vector type stored in the inline buffer. - [InlineArray(2)] - private struct InlineArray2 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs index 8b9a5d7069..8838c28ddf 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs +++ b/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; /// @@ -250,43 +248,4 @@ internal sealed class ObuFilmGrainParameters this.OverlapFlag = source.OverlapFlag; this.ClipToRestrictedRange = source.ClipToRestrictedRange; } - - /// - /// Provides inline storage for the maximum luma autoregressive coefficient count. - /// - /// The stored value type. - [InlineArray(24)] - private struct InlineArray24 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } - - /// - /// Provides inline storage for the ten scaling points permitted on either chroma plane. - /// - /// The stored value type. - [InlineArray(10)] - private struct InlineArray10 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } - - /// - /// Provides inline storage for the maximum autoregressive coefficient count of either chroma plane. - /// - /// The stored value type. - [InlineArray(25)] - private struct InlineArray25 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs index 99e62da161..ca496c9903 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs +++ b/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 } } } - - /// - /// Provides inline storage for the seven canonical AV1 inter reference types. - /// - /// The stored parameter type. - [InlineArray(Av1Constants.ReferencesPerFrame)] - private struct InlineArray7 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs index b508b1038e..99238b13fb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs +++ b/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; /// @@ -10,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// internal sealed class ObuTileGroupHeader { - private InlineTileColumnBoundaryArray tileColumnStartModeInfo; - private InlineTileRowBoundaryArray tileRowStartModeInfo; + private InlineArray65 tileColumnStartModeInfo; + private InlineArray65 tileRowStartModeInfo; /// /// 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. /// 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; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs index 5d73612cdb..5b3e3ba5af 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs +++ b/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; /// /// Writes the AV1 open bitstream units required for a single still-image frame. /// -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; + /// /// Writes a temporal delimiter and the supplied sequence and frame OBUs. /// @@ -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 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 headerOwner = configuration.MemoryAllocator.Allocate(MaximumHeaderLength); + Span 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) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs index 01e7b5fcb5..c8586d9d8e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs +++ b/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; /// internal static class Av1FrameEncoder { + /// + /// 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. + /// + private const int OutputAlignmentLog2 = 5; + + /// + /// 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. + /// + private const int MinimumCompressedFrameBufferLength = 8 * 1024; + + /// + /// 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. + /// + private const int AllIntraBufferScaleNumerator = 5; + + /// + /// The denominator of the all-intra output-capacity ratio, completing the reference encoder's 5:2 sizing rule. + /// + private const int AllIntraBufferScaleDenominator = 2; + /// /// Encodes one reduced-still-picture AV1 frame into a low-overhead OBU stream. /// @@ -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 @@ -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( @@ -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 @@ -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( @@ -272,7 +299,7 @@ internal static class Av1FrameEncoder ObuFrameHeader frameHeader, Av1EncoderFrameBuffer source, Av1EncoderFrameBuffer reconstruction, - int initialTileSize, + int tileBufferLength, int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel @@ -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 source, Av1EncoderFrameBuffer reconstruction, - int initialTileSize, + int tileBufferLength, int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel @@ -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(); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs index 139c9cf312..5fe0803227 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs +++ b/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; /// -/// Encodes and owns one range-coded all-intra tile payload. +/// Encodes one range-coded all-intra tile payload. /// -internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable +internal sealed partial class Av1IntraTileWriter : IAv1TileWriter { - private IMemoryOwner? tileData; + private readonly ReadOnlyMemory tileData; private readonly int tileDataLength; /// /// Initializes a new instance of the class for eight-bit samples. /// - /// The configuration providing tile output memory. + /// The operation-owned symbol encoder that retains the tile output memory. /// The coded source frame. /// The reconstructed frame updated during encoding. /// The frame coding and mode-information state. /// The frame-owned quantized coefficient and transform state. /// The reusable partition and final-block decision workspace. /// The reusable block arithmetic workspace. - /// The estimated encoded tile size in bytes. /// The mode-search effort in the inclusive range zero through ten. public Av1IntraTileWriter( - Configuration configuration, + Av1SymbolEncoder writer, Av1EncoderFrame source, Av1EncoderFrame reconstruction, Av1PictureControlSet picture, Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderBlockWorkspace blockWorkspace, - int initialSize, int effort) { this.tileData = Encode( - configuration, + writer, source, reconstruction, picture, @@ -49,35 +46,32 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable superblockWorkspace, blockWorkspace, effort, - initialSize, out this.tileDataLength); } /// /// Initializes a new instance of the class for high-bit-depth samples. /// - /// The configuration providing tile output memory. + /// The operation-owned symbol encoder that retains the tile output memory. /// The coded source frame. /// The reconstructed frame updated during encoding. /// The frame coding and mode-information state. /// The frame-owned quantized coefficient and transform state. /// The reusable partition and final-block decision workspace. /// The reusable block arithmetic workspace. - /// The estimated encoded tile size in bytes. /// The mode-search effort in the inclusive range zero through ten. public Av1IntraTileWriter( - Configuration configuration, + Av1SymbolEncoder writer, Av1EncoderFrame source, Av1EncoderFrame reconstruction, Av1PictureControlSet picture, Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderBlockWorkspace blockWorkspace, - int initialSize, int effort) { this.tileData = Encode( - configuration, + writer, source, reconstruction, picture, @@ -85,28 +79,14 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable superblockWorkspace, blockWorkspace, effort, - initialSize, out this.tileDataLength); } /// - public ReadOnlySpan GetTileData(int tileNum) - { - ObjectDisposedException.ThrowIf(this.tileData is null, this); - return this.tileData.Memory.Span[..this.tileDataLength]; - } - - /// - /// Returns the detached range-coded tile allocation to the configured allocator. - /// - public void Dispose() - { - this.tileData?.Dispose(); - this.tileData = null; - } + public ReadOnlySpan GetTileData(int tileNum) => this.tileData.Span[..this.tileDataLength]; - private static IMemoryOwner Encode( - Configuration configuration, + private static ReadOnlyMemory Encode( + Av1SymbolEncoder writer, Av1EncoderFrame source, Av1EncoderFrame 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 @@ -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) diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs index 0e2d30f297..ace7efab0f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs @@ -10,34 +10,20 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; /// 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. /// /// 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, ]; + /// + /// Converts a quantizer on libaom's external zero-through-63 scale to an AV1 quantizer index. + /// + /// The external quantizer. + /// The corresponding AV1 quantizer index. + 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; + } + /// /// Gets the DC dequantization value after applying a plane delta to the frame quantizer index. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs index f12492b3bd..ba9453aa29 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs +++ b/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; } } - - /// - /// Provides fixed storage for the two values associated with AV1's primary and secondary inter references. - /// - /// The stored reference label, motion vector, or interpolation-filter type. - [InlineArray(2)] - private struct InlineArray2 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs index 12ec0aa79d..db3dfca31a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs +++ b/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 destination = this.paletteColors; colors.CopyTo(destination[offset..]); } - - /// - /// Provides fixed storage for the luma and shared chroma palette sizes. - /// - /// The stored value type. - [InlineArray(2)] - private struct InlineArray2 - { - private T element; - } - - /// - /// Provides fixed storage for all three eight-color palette planes. - /// - /// The stored value type. - [InlineArray(3 * Av1Constants.PaletteMaxSize)] - private struct InlineArray24 - { - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs index 47865e43f1..eaf17068e1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs +++ b/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. /// public sbyte ChromaFromLumaSigns { get; set; } - - /// - /// Stores the two signed angle deltas embedded by libaom in block mode information. - /// - [InlineArray(Av1Constants.PlaneTypeCount)] - private struct InlineArray2 - { - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs index c0765184f9..023d9dfd1e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs +++ b/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; /// @@ -13,17 +11,17 @@ internal struct Av1LoopRestorationUnit /// /// The three transmitted symmetric vertical Wiener coefficients. /// - public WienerCoefficientBuffer WienerVertical; + public InlineArray3 WienerVertical; /// /// The three transmitted symmetric horizontal Wiener coefficients. /// - public WienerCoefficientBuffer WienerHorizontal; + public InlineArray3 WienerHorizontal; /// /// The two self-guided projection coefficients. /// - public SgrProjectionCoefficientBuffer SgrProjectionCoefficients; + public InlineArray2 SgrProjectionCoefficients; /// /// 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. /// public int SgrParameterSet { get; set; } - - /// - /// Stores the transmitted coefficients inline with the restoration unit. - /// - [InlineArray(Av1Constants.WienerCoefficientCount)] - public struct WienerCoefficientBuffer - { - private int element0; - } - - /// - /// Stores the projection coefficients inline with the restoration unit. - /// - [InlineArray(2)] - public struct SgrProjectionCoefficientBuffer - { - private int element0; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 339f6adcb0..8cece2ddb7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -3449,17 +3449,4 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// public Buffer2D Chroma { get; } = chroma; } - - /// - /// Provides inline storage for the two self-guided restoration coefficients of each of the three AV1 planes. - /// - /// The stored value type. - [InlineArray(6)] - private struct InlineArray6 - { - /// - /// The first element in the compiler-expanded inline buffer. - /// - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs index 3246bca19e..905136ea1d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs +++ b/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; /// @@ -28,17 +26,17 @@ internal ref struct Av1Transform2dFlipConfiguration /// /// The fixed-point shifts applied between successive stages of the configured transform pipeline. /// - private ShiftBuffer shift; + private InlineArray3 shift; /// /// The signed-bit ranges produced by the column transform stages. /// - private Av1TransformStageRange stageRangeColumn; + private InlineArray12 stageRangeColumn; /// /// The signed-bit ranges produced by the row transform stages. /// - private Av1TransformStageRange stageRangeRow; + private InlineArray12 stageRangeRow; /// /// Initializes a new instance of the struct. @@ -337,12 +335,12 @@ internal ref struct Av1Transform2dFlipConfiguration /// /// Gets the allowed signed-bit range after each column-transform stage. /// - public readonly Av1TransformStageRange StageRangeColumn => this.stageRangeColumn; + public readonly InlineArray12 StageRangeColumn => this.stageRangeColumn; /// /// Gets the allowed signed-bit range after each row-transform stage. /// - public readonly Av1TransformStageRange StageRangeRow => this.stageRangeRow; + public readonly InlineArray12 StageRangeRow => this.stageRangeRow; /// /// Creates the configuration used to transform spatial residuals into coefficients. @@ -499,16 +497,4 @@ internal ref struct Av1Transform2dFlipConfiguration this.stageRangeRow[i] = rowRange; } } - - /// - /// Stores the three fixed-point shifts without allocating an array for each transform block. - /// - [InlineArray(3)] - private struct ShiftBuffer - { - /// - /// The first fixed-point shift. - /// - private int element0; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs deleted file mode 100644 index d4beaa42d4..0000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs +++ /dev/null @@ -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; - -/// -/// Stores the signed-bit ranges assigned to every stage of one AV1 transform axis. -/// -[InlineArray(Av1Transform2dFlipConfiguration.MaxStageNumber)] -internal struct Av1TransformStageRange -{ - /// - /// The signed-bit range assigned to the first transform stage. - /// - private byte element0; -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs index eaaeb6590d..4c0a80f97a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The sixteen-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -205,7 +205,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -393,7 +393,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs index ed04c36b20..ba1badf334 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the sine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); @@ -79,7 +79,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount; @@ -115,7 +115,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs index e9646e8e22..c741ec7dce 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The eight-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -112,7 +112,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -207,7 +207,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs index b23dcf616f..7db0baec48 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The sixteen-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -174,7 +174,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -331,7 +331,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs index 4f284da314..9808f02bf6 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The 32-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -358,7 +358,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -699,7 +699,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs index 149d646819..e9abc192fe 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The four-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 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> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 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> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. output.V0 = input.V0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs index f3659699e0..020940df70 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The 64-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -773,7 +773,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -1529,7 +1529,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs index ada40dc907..409b973301 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// The eight-element stage buffer owned by the containing two-dimensional transform. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -93,7 +93,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; @@ -169,7 +169,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); int stage = 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs index e9c435d9c8..3d2a7d8d0b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// Unused stage storage supplied by the common transform-kernel contract. /// Unused cosine precision supplied by the common transform-kernel contract. /// The signed-bit range assigned to the transform output. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { _ = step; _ = cosBit; @@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 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> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs index bbed35d812..933fb8060b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// Unused stage storage supplied by the common transform-kernel contract. /// Unused cosine precision supplied by the common transform-kernel contract. /// The signed-bit range assigned to the transform output. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { _ = step; _ = cosBit; @@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0); _ = step; @@ -57,7 +57,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0); _ = step; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs index f952466283..5789e0e842 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// Unused stage storage supplied by the common transform-kernel contract. /// Unused cosine precision supplied by the common transform-kernel contract. /// The signed-bit range assigned to the transform output. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { _ = step; _ = cosBit; @@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 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> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs index ca9f21f762..c4d6f36f7c 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs @@ -24,7 +24,7 @@ internal static partial class Av1Inverse2dTransformer /// Unused stage storage supplied by the common transform-kernel contract. /// Unused cosine precision supplied by the common transform-kernel contract. /// The signed-bit range assigned to the transform output. - public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange) { _ = step; _ = cosBit; @@ -43,7 +43,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0); _ = step; @@ -57,7 +57,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange) + InlineArray12 stageRange) { Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0); _ = step; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs index 579524b3c9..d88e575d3d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs @@ -27,7 +27,7 @@ internal static partial class Av1Inverse2dTransformer /// The fixed stage storage for the transform axis. /// The fixed-point precision of the cosine constants. /// The signed-bit range assigned to each transform stage. - public static abstract void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange); + public static abstract void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, InlineArray12 stageRange); /// /// Transforms four independent axes in parallel. @@ -42,7 +42,7 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange); + InlineArray12 stageRange); /// /// Transforms eight independent axes in parallel. @@ -57,6 +57,6 @@ internal static partial class Av1Inverse2dTransformer ref Av1TransformVector> output, ref Av1TransformVector> step, int cosBit, - Av1TransformStageRange stageRange); + InlineArray12 stageRange); } } diff --git a/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs b/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs index f0ceb30e8d..c4077cb29b 100644 --- a/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs +++ b/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs @@ -11,6 +11,7 @@ public sealed class HeifConfigurationModule : IImageFormatConfigurationModule /// public void Configure(Configuration configuration) { + configuration.ImageFormatsManager.SetEncoder(HeifFormat.Instance, new HeifEncoder()); configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance); configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector()); } diff --git a/src/ImageSharp/Formats/Heif/HeifEncoder.cs b/src/ImageSharp/Formats/Heif/HeifEncoder.cs index 8afae17803..db26aec078 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoder.cs +++ b/src/ImageSharp/Formats/Heif/HeifEncoder.cs @@ -25,9 +25,9 @@ public sealed class HeifEncoder : AnimatedImageEncoder /// /// Gets the compression method used for the primary image item. - /// The default is . + /// The default is . /// - public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.LegacyJpeg; + public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.Av1; /// /// Gets the lossy compression quality, or to use the compression method's default quality. diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.Sequence.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.Sequence.cs index c6917403c2..1bf61d1904 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.Sequence.cs +++ b/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(Image image) where TPixel : unmanaged, IPixel @@ -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 image, ChunkedMemoryStream stream, Av1EncodingSettings settings, + Memory samples, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -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 colorSamples = samples.AsSpan(0, frameCount); + // share one allocator-owned table, with each track occupying one contiguous slice until moov is written. + Span colorSamples = samples.Span[..frameCount]; ImageFrame 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 alphaSamples = samples.AsSpan(frameCount, frameCount); + Memory alphaSampleMemory = samples.Slice(frameCount, frameCount); + Span 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 iccProfileData = ReadOnlyMemory.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 memory = new(this.configuration, 0x1000); + int movieLength = GetSequenceMovieBoxLength(sequence); + using IMemoryOwner movieOwner = this.configuration.MemoryAllocator.Allocate(movieLength); + Span 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 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 memory, + Span memory, ref int offset, uint timescale, ulong duration, @@ -355,7 +486,7 @@ internal sealed partial class HeifEncoderCore } private static int WriteSequenceTrack( - AutoExpandingMemory memory, + Span 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 memory, + Span 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 memory, + Span memory, ref int offset, Heif4CharCode referenceType, uint referencedTrackId) @@ -430,7 +566,7 @@ internal sealed partial class HeifEncoderCore } private static void WriteSequenceEditList( - AutoExpandingMemory memory, + Span memory, ref int offset, ulong mediaDuration) { @@ -446,8 +582,119 @@ internal sealed partial class HeifEncoderCore EndSequenceBox(memory, editStart, offset); } + private static void WriteSequenceTrackMetadata( + Span 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 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 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 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 memory, + Span memory, ref int offset, uint timescale, ulong mediaDuration) @@ -464,7 +711,7 @@ internal sealed partial class HeifEncoderCore } private static void WriteSequenceHandler( - AutoExpandingMemory memory, + Span 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 memory, ref int offset) + private static void WriteSequenceDataInformation(Span 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 memory, + Span 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 memory, + Span 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 auxiliaryType = memory.GetSpan(offset, auxiliaryTypeLength + 1); + Span 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 memory, + Span memory, ref int offset, ReadOnlySpan 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 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 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 memory, + Span 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 memory, int start, int offset) + private static void EndSequenceBox(Span 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 memory, + Span memory, ref int offset, byte version, uint flags) { - Span destination = memory.GetSpan(offset, sizeof(uint)); + Span destination = memory.Slice(offset, sizeof(uint)); BinaryPrimitives.WriteUInt32BigEndian(destination, flags); destination[0] = version; offset += sizeof(uint); } - private static void WriteSequenceIdentityMatrix(AutoExpandingMemory memory, ref int offset) + private static void WriteSequenceIdentityMatrix(Span 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 memory, ref int offset, int length) + private static void WriteSequenceZeros(Span memory, ref int offset, int length) { - memory.GetSpan(offset, length).Clear(); + memory.Slice(offset, length).Clear(); offset += length; } - private static void WriteSequenceUInt16(AutoExpandingMemory memory, ref int offset, ushort value) + private static void WriteSequenceBytes( + Span memory, + ref int offset, + ReadOnlySpan source) { - BinaryPrimitives.WriteUInt16BigEndian(memory.GetSpan(offset, sizeof(ushort)), value); + source.CopyTo(memory[offset..]); + offset += source.Length; + } + + private static void WriteSequenceUInt16(Span memory, ref int offset, ushort value) + { + BinaryPrimitives.WriteUInt16BigEndian(memory[offset..], value); offset += sizeof(ushort); } - private static void WriteSequenceUInt32(AutoExpandingMemory memory, ref int offset, uint value) + private static void WriteSequenceUInt32(Span 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 memory, ref int offset, ulong value) + private static void WriteSequenceUInt64(Span 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 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 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 samples; public HeifSequenceTrackEncoding( Av1CodecConfiguration configuration, - HeifSequenceSampleInfo[] samples, - int sampleOffset, - int sampleCount, + ReadOnlyMemory samples, bool isAlpha) { this.Configuration = configuration; this.samples = samples; - this.sampleOffset = sampleOffset; - this.sampleCount = sampleCount; this.IsAlpha = isAlpha; } public Av1CodecConfiguration Configuration { get; } public ReadOnlySpan Samples - => this.samples.AsSpan(this.sampleOffset, this.sampleCount); + => this.samples.Span; public bool IsAlpha { get; } } diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs index 6b666fd308..bc41d6ed34 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs +++ b/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; /// 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; + /// /// The global configuration. /// @@ -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 samplesOwner = + this.configuration.MemoryAllocator.Allocate(sampleCount); + + Memory 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 /// The destination stream positioned after the file-type box. private void WriteMetadataBox(List items, List links, long metadataBoxOffset, Stream stream) { - using AutoExpandingMemory memory = new(this.configuration, 0x1000); - Span buffer = memory.GetSpan(12); + int metadataLength = GetMetadataBoxLength(items, links); + using IMemoryOwner metadataOwner = this.configuration.MemoryAllocator.Allocate(metadataLength); + Span memory = metadataOwner.Memory.Span[..metadataLength]; + Span 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 items, List 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 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 links) + { + long length = ItemReferenceBoxFixedLength; + foreach (HeifItemLink link in links) + { + length += ItemReferenceEntryFixedLength + ((long)link.DestinationIds.Count * sizeof(ushort)); + } + + return checked((int)length); + } + + /// + /// Gets the exact number of bytes required for the item-properties box. + /// + /// The items whose properties and associations are counted. + /// The complete item-properties-box length. + public static int GetItemPropertiesBoxLength(List 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 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); + } + /// /// Writes the picture metadata handler box. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the metadata box. /// The complete handler-box length. - private static int WriteHandlerBox(AutoExpandingMemory memory, int memoryOffset) + private static int WriteHandlerBox(Span memory, int memoryOffset) { - Span buffer = memory.GetSpan(memoryOffset, 33); + Span 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 /// /// Writes the identifier of the primary presentation item. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the metadata box. /// The complete primary-item-box length. - private static int WritePrimaryItemBox(AutoExpandingMemory memory, int memoryOffset) + private static int WritePrimaryItemBox(Span memory, int memoryOffset) { - Span buffer = memory.GetSpan(memoryOffset, 14); + Span 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 /// /// Writes the item-information box and one version-two entry for each item. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the metadata box. /// The items to declare. /// The complete item-information-box length. - private static int WriteItemInfoBox(AutoExpandingMemory memory, int memoryOffset, List items) + private static int WriteItemInfoBox(Span memory, int memoryOffset, List 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 buffer = memory.GetSpan(memoryOffset, capacity); + Span 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 /// /// Writes typed item-reference child boxes using 16-bit item identifiers. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the metadata box. - /// The declared items used to size the destination. /// The relationships to write. /// The complete item-reference-box length. - private static int WriteItemReferenceBox(AutoExpandingMemory memory, int memoryOffset, List items, List links) + private static int WriteItemReferenceBox(Span memory, int memoryOffset, List links) { - Span buffer = memory.GetSpan(memoryOffset, 12 + (links.Count * (12 + (items.Count * 2)))); + Span 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 /// /// Writes spatial-extent properties and their one-based item associations. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the metadata box. /// The items whose dimensions are written and associated. /// The complete item-properties-box length. - public static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items) + public static int WriteItemPropertiesBox(Span memory, int memoryOffset, List items) { - Span buffer = memory.GetSpan(memoryOffset, 20); + Span 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; } } /// /// Writes the encoded precision of each image channel. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. /// The encoded precision of each channel. /// The complete pixel-information-box length. private static int WritePixelInformationPropertyBox( - AutoExpandingMemory memory, + Span memory, int memoryOffset, ReadOnlySpan channelBitDepths) { - Span buffer = memory.GetSpan(memoryOffset, 13 + channelBitDepths.Length); + Span 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 /// /// Writes one common encoded precision for every image channel. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. /// The number of encoded image channels. /// The common encoded precision. /// The complete pixel-information-box length. private static int WritePixelInformationPropertyBox( - AutoExpandingMemory memory, + Span memory, int memoryOffset, int channelCount, byte channelBitDepth) { - Span buffer = memory.GetSpan(memoryOffset, 13 + channelCount); + Span 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 /// /// Writes an AV1 codec-configuration property. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. /// The fixed image configuration. /// The complete AV1 codec-configuration-box length. private static int WriteAv1CodecConfigurationPropertyBox( - AutoExpandingMemory memory, + Span memory, int memoryOffset, Av1CodecConfiguration configuration) { - Span buffer = memory.GetSpan(memoryOffset, 8 + Av1CodecConfiguration.FixedHeaderSize); + Span 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 /// /// Writes the registered type of an auxiliary image item. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. /// The null-terminated registered auxiliary type. /// The complete auxiliary-type-box length. private static int WriteAuxiliaryTypePropertyBox( - AutoExpandingMemory memory, + Span memory, int memoryOffset, string auxiliaryType) { int auxiliaryTypeLength = Encoding.UTF8.GetByteCount(auxiliaryType); - Span buffer = memory.GetSpan(memoryOffset, 13 + auxiliaryTypeLength); + Span 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 /// /// Writes an unrestricted ICC color profile for a color image item. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. - /// The ICC profile to write. + /// The serialized ICC profile to write. /// The complete color-information-box length. private static int WriteIccColorInformationPropertyBox( - AutoExpandingMemory memory, + Span memory, int memoryOffset, - IccProfile profile) + ReadOnlyMemory profileData) { - ReadOnlyMemory profileData = profile.GetDataForWriting(); - Span buffer = memory.GetSpan(memoryOffset, 12 + profileData.Length); + Span 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 /// /// Writes an H.273 color description for a color image item. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. /// The color description to write. /// The complete color-information-box length. private static int WriteColorInformationPropertyBox( - AutoExpandingMemory memory, + Span memory, int memoryOffset, CicpProfile profile) { - Span buffer = memory.GetSpan(memoryOffset, 19); + Span 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 /// /// Writes an item's display width and height as an image-spatial-extents property. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the property container. /// The item whose extent is written. /// The complete image-spatial-extents-box length. - private static int WriteSpatialExtentPropertyBox(AutoExpandingMemory memory, int memoryOffset, HeifItem item) + private static int WriteSpatialExtentPropertyBox(Span memory, int memoryOffset, HeifItem item) { - Span buffer = memory.GetSpan(memoryOffset, 20); + Span 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 /// /// Writes version-one file-relative locations for every ordered item extent. /// - /// The expanding metadata buffer. + /// The preallocated metadata buffer. /// The destination offset within the metadata box. /// The items and relative payload extents to locate. /// The absolute stream offset of the media-data payload. /// The complete item-location-box length. - private static int WriteItemLocationBox(AutoExpandingMemory memory, int memoryOffset, List items, long mediaDataOffset) + private static int WriteItemLocationBox(Span memory, int memoryOffset, List items, long mediaDataOffset) { - int extentCount = items.Sum(item => item.DataLocations.Count); - Span buffer = memory.GetSpan(memoryOffset, 16 + (items.Count * 8) + (extentCount * 12)); + Span 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); } /// @@ -802,6 +918,19 @@ internal sealed partial class HeifEncoderCore CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + 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 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 } } + /// + /// Materializes the caller's Exif profile once and locates the TIFF header addressed by HEIF's four-byte prefix. + /// + /// The source image metadata. + /// The byte offset of the TIFF header within the returned profile. + /// The serialized profile, or when the source has no Exif payload. + 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."); + } + /// /// Encodes the source pixels as the current legacy JPEG item payload. /// diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs index 5531fd9645..2c8d26de19 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -14,6 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// The item identifier used by locations, properties, and references. internal sealed class HeifItem(Heif4CharCode type, uint id) { + private IccProfile? iccProfile; + + private ReadOnlyMemory serializedIccProfile; + /// /// Gets the ID of this Item. /// @@ -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 when the item /// has no restricted or unrestricted ICC color-information property. /// - public IccProfile? IccProfile { get; set; } + public IccProfile? IccProfile + { + get => this.iccProfile; + set + { + this.iccProfile = value; + this.serializedIccProfile = default; + } + } /// /// Gets or sets the CICP color description associated with this color image item, or @@ -187,6 +199,22 @@ internal sealed class HeifItem(Heif4CharCode type, uint id) /// public List DataLocations { get; } = []; + /// + /// Gets the serialized ICC payload used while sizing and writing an encoded item. + /// + /// The serialized profile data, or an empty memory when no profile is assigned. + public ReadOnlyMemory 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; + } + /// /// Set the image extent. /// diff --git a/src/ImageSharp/Memory/AutoExpandingMemory.cs b/src/ImageSharp/Memory/AutoExpandingMemory.cs deleted file mode 100644 index 6787ca4951..0000000000 --- a/src/ImageSharp/Memory/AutoExpandingMemory.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Memory; - -/// -/// Memory class that will expand dynamically when full. -/// -internal sealed class AutoExpandingMemory : IDisposable - where T : unmanaged -{ - private const int IncreaseFactor = 5; - private readonly Configuration configuration; - private IMemoryOwner 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(initialSize); - } - - public int Capacity => this.allocation.Memory.Length; - - public Span GetSpan(int requestedSize) - { - Guard.MustBeGreaterThanOrEqualTo(requestedSize, 0, nameof(requestedSize)); - this.EnsureCapacity(requestedSize); - - return this.allocation.Memory.Span[..requestedSize]; - } - - public Span 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 GetEntireSpan() - => this.GetSpan(this.Capacity); - - /// - /// Transfers the current allocation to the caller without copying its contents. - /// - /// The allocation previously owned by this instance. - public IMemoryOwner 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 newAllocation = this.configuration.MemoryAllocator.Allocate(newSize); - this.allocation.Memory.CopyTo(newAllocation.Memory); - this.allocation.Dispose(); - this.allocation = newAllocation; - } - } -} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs index be05e0b085..cc8aa52584 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs +++ b/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 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 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 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 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 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 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 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 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++) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs index ba6c062c81..9802a563a1 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs +++ b/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 coefficientsBuffer = [1, 2, 3, 4, 5]; Span expected = new int[16]; Span 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 coefficientsBuffer = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; ReadOnlySpan 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 scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; Span coefficientsBuffer = new int[coefficientCount]; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index 72a74e68d7..31b73786f1 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/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 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 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 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 decoded = configuration.MemoryAllocator.Allocate2D(Width, Height); Buffer2DRegion sourceRegion = new(source); Buffer2DRegion 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++) diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs index d5fb5477c4..0fcaba5cdb 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs +++ b/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(sourceLuma); - using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex); + using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true); Span candidates = stackalloc Av1MotionVector[2]; Av1MotionVector reference = new(0, -2560); int candidateCount = picture.IntraBlockCopySearch.FindCandidates( @@ -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 candidates = stackalloc Av1MotionVector[2]; int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch .FindPixelCandidates( @@ -481,7 +481,7 @@ public class Av1IntraBlockCopyTests Buffer2DRegion codedSourceLuma = source.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion 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 candidates = stackalloc Av1MotionVector[2]; int candidateCount = pictureBuffer.Picture.IntraBlockCopySearch .FindPixelCandidates( diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs index 4c84ade06f..4acb8d8510 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs +++ b/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( 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( 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( @@ -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 { 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 } } + /// + /// Creates the operation owner for a production tile's entropy state and bounded output memory. + /// + /// The picture supplying quantization and CDF-update settings. + /// The bounded output allocation length in bytes. + /// The symbol encoder that must remain alive while the tile output is consumed. + 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( + Av1SymbolEncoder writer, Av1EncoderFrame source, Av1EncoderFrame reconstruction, Av1PictureControlSet picture, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs index bde9ef4cda..9230be5b44 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs +++ b/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 stageRange = default; for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++) { stageRange[index] = 20; @@ -182,7 +182,7 @@ public class Av1InverseTransformTests Vector128 expected128, Vector256 input256, Vector256 expected256, - Av1TransformStageRange stageRange) + InlineArray12 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 configuredRowRange = config.StageRangeRow; + InlineArray12 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 stageRange = default; for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index a0251b8b23..360c9291f7 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -586,8 +586,8 @@ public class ObuFrameHeaderTests public void ReadFrameHeaderRejectsIntraOnlyAllSlotsRefresh() { byte[] sequenceHeader = CreateNonReducedSequenceHeaderObu(default); - using AutoExpandingMemory 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(() => ReadObuStream(bitStream)); } @@ -796,6 +796,8 @@ public class ObuFrameHeaderTests /// The complete explicitly sized sequence-header OBU. 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 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; } diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 189a2cb5e0..e43090a06e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/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 image = Image.Load(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(() => Image.Identify(data)); } - private static byte[] CreateEncodedContainer() + private static byte[] CreateLegacyJpegContainer() { using Image 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 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); diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs index 03b9ac8cf3..224699905d 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs +++ b/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 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 decoded = Image.Load(stream); @@ -141,7 +150,11 @@ public class HeifEncoderTests using Image 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 image = new(1, 1); using MemoryStream stream = new(); - HeifEncoder encoder = new() { Lossless = true }; + HeifEncoder encoder = new() + { + CompressionMethod = HeifCompressionMethod.LegacyJpeg, + Lossless = true + }; Assert.Throws(() => image.Save(stream, encoder)); Assert.Equal(0, stream.Length); @@ -175,7 +192,11 @@ public class HeifEncoderTests { using Image image = new(1, 1); using MemoryStream stream = new(); - HeifEncoder encoder = new() { BitDepth = bitDepth }; + HeifEncoder encoder = new() + { + CompressionMethod = HeifCompressionMethod.LegacyJpeg, + BitDepth = bitDepth + }; Assert.Throws(() => 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("ImageSharp AV1 sequence"); + 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 decoded = Image.Load(stream); + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + using Image decoded = Image.Load(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(decoded.Metadata.IccProfile).ToByteArray()); + + ExifProfile decodedExif = Assert.IsType(decoded.Metadata.ExifProfile); + Assert.True(decodedExif.TryGetValue(ExifTag.Software, out IExifValue software)); + Assert.Equal("ImageSharp AV1 sequence", software.Value); + Assert.Equal(xmpData, Assert.IsType(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("suppressed")); 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 decoded = Image.Load(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 items = [colorItem, alphaItem]; - using AutoExpandingMemory memory = new(Configuration.Default, 16); - int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); - ReadOnlySpan propertyBox = memory.GetSpan(length); + int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items); + using IMemoryOwner owner = Configuration.Default.MemoryAllocator.Allocate(expectedLength); + Span 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 memory = new(Configuration.Default, 16); - int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); - ReadOnlySpan propertyBox = memory.GetSpan(length); + int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items); + using IMemoryOwner owner = Configuration.Default.MemoryAllocator.Allocate(expectedLength); + Span 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 memory = new(Configuration.Default, 16); - int length = HeifEncoderCore.WriteItemPropertiesBox(memory, 0, items); - ReadOnlySpan propertyBox = memory.GetSpan(length); + int expectedLength = HeifEncoderCore.GetItemPropertiesBoxLength(items); + using IMemoryOwner owner = Configuration.Default.MemoryAllocator.Allocate(expectedLength); + Span 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 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; diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 3fd90b36a3..146898b46f 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -38,7 +38,7 @@ public class ImageFormatManagerTests Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); - Assert.Empty(this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType()); + Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); diff --git a/tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs b/tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs deleted file mode 100644 index 5b13889c37..0000000000 --- a/tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs +++ /dev/null @@ -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 memory = new(this.configurtion, initialCapacity); - Span 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 memory = new(this.configurtion, initialCapacity); - Span firstSpan = memory.GetSpan(initialCapacity); - firstSpan[1] = 1; - firstSpan[2] = 2; - firstSpan[3] = 3; - Span 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(() => - { - AutoExpandingMemory memory = new(this.configurtion, initialCapacity); - _ = memory.GetSpan(requestedCapacity); - }); -}