Browse Source

Enable bounded AV1 still-image encoding

pull/2633/head
James Jackson-South 1 month ago
parent
commit
33759e54b1
  1. 18
      HEIF_IMPLEMENTATION_PLAN.md
  2. 4
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  3. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  4. 4
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs
  5. 5
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  6. 261
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  7. 6
      src/ImageSharp/Formats/Heif/HeifItem.cs
  8. 18
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs
  9. 54
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
  10. 365
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

18
HEIF_IMPLEMENTATION_PLAN.md

@ -27,14 +27,14 @@ Reference checkout evidence on 2026-08-31:
## Current source reconciliation
Reconciled with the worktree on 2026-08-31.
Reconciled with the worktree on 2026-09-03.
- [~] The bounded container reader, still-image path, sequence parser, AV1 decoder, color pipeline, presentation pipeline, and broad AV1 test suite exist locally.
- [x] The inter-frame decoder has verified checkpoints through inter deblocking decisions and
reference/mode deltas.
- [~] Loop filtering, CDEF, super-resolution, restoration, film grain, layered presentation, alpha composition, and color conversion exist locally. Shared-source cleanup changed the current tree, so final production-path verification is open.
- [~] AV1 writer primitives, forward transforms, symbol encoding, and tile-writing source exist locally, but they are not connected to the public encoder.
- [ ] The public AV1 encoder is not implemented. HeifEncoderCore.Encode throws NotSupportedException when AV1 is selected.
- [~] AV1 writer primitives, forward transforms, symbol encoding, and tile-writing source are connected to the public encoder for bounded still-image AVIF color and optional auxiliary alpha output.
- [~] The public AV1 encoder supports explicit single-image requests. Lossless output, bounded sequences, grids, metadata preservation, orientation handling, and default format registration remain open.
- [x] Patented codec production code, registrations, tests, benchmarks, fixtures, reference outputs, and notices were manually deleted and committed by `78a74d448`.
- [x] Remaining task-created HM, HEVC, libheif, GPAC, Nokia, FFmpeg, Pillow HEIF, libavif-build, and libjpeg-build directories were traced to their creation commands in the recovered Codex session history and deleted on 2026-08-31. The user-provided repositories and all libaom-only source, build, and reference data were left untouched.
- [x] The PNG metadata-suppression fix and three HEIF/AV1 diagnostic-save call-site corrections passed the exact 34 net11.0 ARM CI cases and were committed with the single-reference checkpoint as `54bb6cbe59bd113058854a3ee31448cf61f462ca`. They are infrastructure evidence, not decoder or encoder completion evidence.
@ -872,12 +872,14 @@ Encoder verification contract:
- [~] The encoder-side AV1 codec configuration is now derived directly from the encoded sequence header and writes the fixed four-byte `av1C` record with empty `configOBUs`. The image payload retains the required sequence header, so the property introduces no sequence-header allocation, retention, or copy. Four production-header cases cover main, high, and professional profiles; 8-, 10-, and 12-bit precision; monochrome, 4:2:0, 4:2:2, and 4:4:4 sampling; exact fixed bytes; decoder reparsing; and header/property equivalence through direct net11 Release VSTest. Property-container emission and public AVIF activation remain open.
- [~] AV1 image properties now write `ispe`, `pixi`, `av1C`, `colr`, and `auxC` in current AVIF item order. Only `av1C` is essential; color and alpha items retain independent property sets and the registered alpha auxiliary type. The property container reacquires its span after nested expansion before patching `ipco`, removing the prior stale-buffer write, and selects compact or 15-bit `ipma` indices from the property count rather than the unrelated item count. A forced-growth color-plus-alpha case validates every property payload and association byte; a separate 43-item, 129-property case proves indices 127 through 129 and the extended essential bit. Both pass direct foreground net11 Release VSTest. Complete AVIF assembly remains open.
- [ ] Write the correct AVIF file type, item information, locations, references, properties, AV1 configuration, dimensions, color, alpha, metadata, and media data.
- [ ] Support single images, alpha auxiliary images, grids, multiple extents, and bounded image sequences in the final public scope.
- [~] Explicit public AV1 encoding now writes a still-image AVIF with `avif` major brand, compatible `avif`, `mif1`, and `miaf` brands, one primary color item, an optional alpha auxiliary item, `auxl` from alpha to color, independent item properties, absolute version-one `iloc` extents, and a shared `mdat`. Quality uses current libaom's quantizer-to-qindex mapping with public quality 100 deliberately clamped from lossless qindex 0 to qindex 4. Effort controls the implemented search stages, and the resolved value is required explicitly by every internal frame, tile, and mode-decision operation rather than repeated as optional defaults. Encoder options take precedence over source metadata for 8-, 10-, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 output. Alpha derives from the source pixel type without scanning pixels, and incompatible identity-matrix metadata is normalized without mutating the source image.
- [~] The production path writes color and alpha payloads sequentially through allocator-backed chunked storage, supports non-seekable and prefixed destinations, and does not materialize a complete file or payload copy. Uniform encoder-side `pixi` depth is written directly without allocating per-item channel-depth arrays; decoder-side non-uniform channel depths remain supported. The Release test project builds with zero errors, all 39 encoder cases pass, the complete non-HEVC HEIF namespace passes 9,277 of 9,277, and current official libaom accepts all 47 generated payloads.
- [~] Write the correct AVIF file type, item information, locations, references, properties, AV1 configuration, dimensions, color, alpha, metadata, and media data.
- [~] Support single images, alpha auxiliary images, grids, multiple extents, and bounded image sequences in the final public scope.
- [ ] Preserve ICC, Exif, and XMP according to encoder options.
- [ ] Write CICP, range, chroma position, bit depth, and subsampling values that match the encoded planes.
- [~] Write CICP, range, chroma position, bit depth, and subsampling values that match the encoded planes.
- [ ] Apply orientation and clean-aperture behavior consistently with ImageSharp encoder conventions.
- [ ] Stream output through allocator-backed chunked storage without file-sized copies or ToArray materialization.
- [~] Stream output through allocator-backed chunked storage without file-sized copies or ToArray materialization.
Encoder exit gate:
@ -887,7 +889,7 @@ Encoder exit gate:
- [ ] 8, 10, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 outputs pass.
- [ ] Alpha, grids, metadata, color profiles, transforms, and bounded sequences pass.
- [ ] ImageSharp decode of its own output is supplemental coverage only, never the sole oracle.
- [ ] Public encoding no longer throws for a supported AV1 request.
- [x] Public encoding no longer throws for a supported AV1 request.
- [ ] Focused Release and FeatureTestRunner verification passes with exact recorded evidence.
## Architecture rules

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

@ -34,7 +34,7 @@ internal static class Av1FrameEncoder
Stream stream,
ObuColorConfig colorConfig,
int qIndex,
int effort = 5)
int effort)
where TPixel : unmanaged, IPixel<TPixel>
=> Encode(configuration, image, stream, colorConfig, qIndex, effort, false);
@ -55,7 +55,7 @@ internal static class Av1FrameEncoder
Stream stream,
ObuColorConfig colorConfig,
int qIndex,
int effort = 5)
int effort)
where TPixel : unmanaged, IPixel<TPixel>
=> Encode(configuration, image, stream, colorConfig, qIndex, effort, true);

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -138,7 +138,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1Superblock superblock,
Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderBlockWorkspace blockWorkspace,
int effort = 5)
int effort)
{
this.source = source.CodedView;
this.reconstruction = reconstruction.CodedView;

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

@ -38,7 +38,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace,
int initialSize,
int effort = 5)
int effort)
{
this.tileData = Encode<byte, Av1IntraSuperblockEncoder.ByteOperator>(
configuration,
@ -74,7 +74,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace,
int initialSize,
int effort = 5)
int effort)
{
this.tileData = Encode<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(
configuration,

5
src/ImageSharp/Formats/Heif/Heif4CharCode.cs

@ -323,6 +323,11 @@ public enum Heif4CharCode : uint
/// </summary>
Mif1 = 0x6D696631U,
/// <summary>
/// Multi-Image Application Format brand.
/// </summary>
Miaf = 0x6D696166U,
/// <summary>
/// AVIF brand.
/// </summary>

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

@ -4,6 +4,8 @@
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.Jpeg;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@ -51,22 +53,22 @@ internal sealed class HeifEncoderCore
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
List<HeifItem> items = new();
List<HeifItemLink> links = new();
using ChunkedMemoryStream compressedPixels = new(this.configuration.MemoryAllocator);
switch (this.encoder.CompressionMethod)
{
case HeifCompressionMethod.LegacyJpeg:
this.CompressPixels(image, compressedPixels, cancellationToken);
GenerateLegacyJpegItem(image, compressedPixels.Length, items);
break;
case HeifCompressionMethod.Av1:
throw new NotSupportedException("AV1 encoding is not implemented.");
this.CompressAv1Pixels(image, compressedPixels, items, links, cancellationToken);
break;
default:
throw new NotSupportedException($"HEIF compression method '{this.encoder.CompressionMethod}' is not supported.");
}
List<HeifItem> items = new();
List<HeifItemLink> links = new();
GenerateItems(image, compressedPixels.Length, items);
// Write out the generated header and pixels.
long metadataBoxOffset = this.WriteFileTypeBox(stream);
this.WriteMetadataBox(items, links, metadataBoxOffset, stream);
@ -81,7 +83,7 @@ internal sealed class HeifEncoderCore
/// <param name="image">The source image.</param>
/// <param name="pixelDataLength">The encoded primary-item payload length.</param>
/// <param name="items">The destination item collection.</param>
private static void GenerateItems<TPixel>(Image<TPixel> image, long pixelDataLength, List<HeifItem> items)
private static void GenerateLegacyJpegItem<TPixel>(Image<TPixel> image, long pixelDataLength, List<HeifItem> items)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifItem primaryItem = new(Heif4CharCode.Jpeg, 1u);
@ -144,15 +146,29 @@ internal sealed class HeifEncoderCore
/// <returns>The number of bytes written.</returns>
private int WriteFileTypeBox(Stream stream)
{
Span<byte> buffer = stackalloc byte[16];
Span<byte> buffer = stackalloc byte[28];
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ftyp);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Mif1);
Heif4CharCode majorBrand = this.encoder.CompressionMethod == HeifCompressionMethod.Av1
? Heif4CharCode.Avif
: Heif4CharCode.Mif1;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)majorBrand);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0);
bytesWritten += 4;
if (majorBrand == Heif4CharCode.Avif)
{
// A still AVIF is also a MIAF image collection, so advertise both structural brands with the codec brand.
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avif);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Mif1);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Miaf);
bytesWritten += 4;
}
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
stream.Write(buffer);
stream.Write(buffer[..bytesWritten]);
return bytesWritten;
}
@ -323,6 +339,18 @@ internal sealed class HeifEncoderCore
{
bytesWritten += WritePixelInformationPropertyBox(memory, memoryOffset + bytesWritten, channelBitDepths);
}
else
{
byte? uniformChannelBitDepth = item.UniformChannelBitDepth;
if (uniformChannelBitDepth is not null)
{
bytesWritten += WritePixelInformationPropertyBox(
memory,
memoryOffset + bytesWritten,
item.ChannelCount,
uniformChannelBitDepth.Value);
}
}
Av1CodecConfiguration? codecConfiguration = item.Av1CodecConfiguration;
if (codecConfiguration is not null)
@ -377,7 +405,7 @@ internal sealed class HeifEncoderCore
buffer[bytesWritten++] = (byte)itemPropertyCount;
WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, false);
if (item.ChannelBitDepths is not null)
if (item.ChannelBitDepths is not null || item.UniformChannelBitDepth is not null)
{
WritePropertyAssociation(buffer, ref bytesWritten, propertyIndex++, largePropertyIndex, false);
}
@ -413,7 +441,7 @@ internal sealed class HeifEncoderCore
private static int GetPropertyCount(HeifItem item)
{
int count = 1;
count += item.ChannelBitDepths is not null ? 1 : 0;
count += item.ChannelBitDepths is not null || item.UniformChannelBitDepth is not null ? 1 : 0;
count += item.Av1CodecConfiguration is not null ? 1 : 0;
count += item.AuxiliaryType is not null ? 1 : 0;
count += item.CicpProfile is not null ? 1 : 0;
@ -469,6 +497,30 @@ internal sealed class HeifEncoderCore
return bytesWritten;
}
/// <summary>
/// Writes one common encoded precision for every image channel.
/// </summary>
/// <param name="memory">The expanding metadata buffer.</param>
/// <param name="memoryOffset">The destination offset within the property container.</param>
/// <param name="channelCount">The number of encoded image channels.</param>
/// <param name="channelBitDepth">The common encoded precision.</param>
/// <returns>The complete pixel-information-box length.</returns>
private static int WritePixelInformationPropertyBox(
AutoExpandingMemory<byte> memory,
int memoryOffset,
int channelCount,
byte channelBitDepth)
{
Span<byte> buffer = memory.GetSpan(memoryOffset, 13 + channelCount);
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pixi, 0, 0);
buffer[bytesWritten++] = (byte)channelCount;
buffer.Slice(bytesWritten, channelCount).Fill(channelBitDepth);
bytesWritten += channelCount;
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
return bytesWritten;
}
/// <summary>
/// Writes an AV1 codec-configuration property.
/// </summary>
@ -622,6 +674,193 @@ internal sealed class HeifEncoderCore
data.WriteTo(stream);
}
/// <summary>
/// Maps the public lossy quality scale through libaom's external quantizer scale to its internal quantizer index.
/// </summary>
/// <param name="quality">The lossy quality in the inclusive range zero through one hundred.</param>
/// <returns>The AV1 quantizer index.</returns>
public static int GetAv1QuantizerIndex(int quality)
{
int scaledQuality = (100 - quality) * 63;
int quantizer = (scaledQuality + 50) / 100;
// 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;
}
/// <summary>
/// Encodes the source root frame into AV1 color and optional auxiliary-alpha item payloads.
/// </summary>
/// <typeparam name="TPixel">The source pixel format.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="stream">The shared destination for consecutive item payloads.</param>
/// <param name="items">The destination item declarations.</param>
/// <param name="links">The destination item relationships.</param>
/// <param name="cancellationToken">The token used to cancel payload encoding.</param>
private void CompressAv1Pixels<TPixel>(
Image<TPixel> image,
ChunkedMemoryStream stream,
List<HeifItem> items,
List<HeifItemLink> links,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.encoder.Lossless)
{
throw new NotSupportedException("Lossless AV1 encoding is not implemented.");
}
if (image.Frames.Count != 1)
{
throw new NotSupportedException("AV1 image-sequence encoding is not implemented.");
}
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
HeifBitDepth bitDepth = this.encoder.BitDepth ?? metadata.BitDepth;
Av1BitDepth av1BitDepth = bitDepth switch
{
HeifBitDepth.Bit8 => Av1BitDepth.EightBit,
HeifBitDepth.Bit10 => Av1BitDepth.TenBit,
HeifBitDepth.Bit12 => Av1BitDepth.TwelveBit,
_ => throw new NotSupportedException($"HEIF bit depth '{bitDepth}' is not supported.")
};
HeifChromaSubsampling chromaSubsampling = this.encoder.ChromaSubsampling ??
(metadata.IsMonochrome ? HeifChromaSubsampling.Monochrome : HeifChromaSubsampling.Yuv420);
(bool isMonochrome, bool subsamplingX, bool subsamplingY) = chromaSubsampling switch
{
HeifChromaSubsampling.Monochrome => (true, true, true),
HeifChromaSubsampling.Yuv420 => (false, true, true),
HeifChromaSubsampling.Yuv422 => (false, true, false),
HeifChromaSubsampling.Yuv444 => (false, false, false),
_ => throw new NotSupportedException($"HEIF chroma sampling '{chromaSubsampling}' is not supported.")
};
CicpProfile? sourceColorProfile = image.Metadata.CicpProfile;
CicpProfile colorProfile;
if (sourceColorProfile is null)
{
colorProfile = new CicpProfile(2, 2, 6, false);
}
else
{
bool identityMatrix = sourceColorProfile.MatrixCoefficients == CicpMatrixCoefficients.Identity;
bool legalIdentityMatrix = !isMonochrome
&& chromaSubsampling == HeifChromaSubsampling.Yuv444
&& sourceColorProfile.ColorPrimaries == CicpColorPrimaries.ItuRBt709_6
&& sourceColorProfile.TransferCharacteristics == CicpTransferCharacteristics.Iec61966_2_1;
if (sourceColorProfile.MatrixCoefficients == CicpMatrixCoefficients.Unspecified
|| (identityMatrix && !legalIdentityMatrix))
{
// The converter uses BT.601 for unspecified or incompatible identity signaling, so record that actual matrix.
colorProfile = new CicpProfile(
(byte)sourceColorProfile.ColorPrimaries,
(byte)sourceColorProfile.TransferCharacteristics,
(byte)CicpMatrixCoefficients.ItuRBt601_7_525,
sourceColorProfile.FullRange);
}
else if (identityMatrix && !sourceColorProfile.FullRange)
{
colorProfile = new CicpProfile(
(byte)sourceColorProfile.ColorPrimaries,
(byte)sourceColorProfile.TransferCharacteristics,
(byte)sourceColorProfile.MatrixCoefficients,
true);
}
else
{
colorProfile = sourceColorProfile;
}
}
ObuColorConfig colorConfig = new()
{
IsColorDescriptionPresent = true,
IsMonochrome = isMonochrome,
ColorPrimaries = (ObuColorPrimaries)colorProfile.ColorPrimaries,
TransferCharacteristics = (ObuTransferCharacteristics)colorProfile.TransferCharacteristics,
MatrixCoefficients = (ObuMatrixCoefficients)colorProfile.MatrixCoefficients,
ColorRange = colorProfile.FullRange,
SubSamplingX = subsamplingX,
SubSamplingY = subsamplingY,
ChromaSamplePosition = ObuChromoSamplePosition.Unknown,
BitDepth = av1BitDepth
};
int quality = this.encoder.Quality ?? 75;
int qIndex = GetAv1QuantizerIndex(quality);
cancellationToken.ThrowIfCancellationRequested();
ObuSequenceHeader colorHeader = Av1FrameEncoder.Encode(
this.configuration,
image.Frames.RootFrame,
stream,
colorConfig,
qIndex,
this.encoder.Effort);
long colorLength = stream.Length;
byte channelBitDepth = (byte)bitDepth;
HeifItem colorItem = new(Heif4CharCode.Av01, 1)
{
ChannelCount = isMonochrome ? 1 : 3,
UniformChannelBitDepth = channelBitDepth,
BitsPerPixel = channelBitDepth * (isMonochrome ? 1 : 3),
Av1CodecConfiguration = new Av1CodecConfiguration(colorHeader),
CicpProfile = colorProfile
};
colorItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, 0L, colorLength));
colorItem.SetExtent(image.Size);
items.Add(colorItem);
bool hasAlpha = TPixel.GetPixelTypeInfo().AlphaRepresentation != PixelAlphaRepresentation.None;
if (!hasAlpha)
{
return;
}
ObuColorConfig alphaConfig = new()
{
IsMonochrome = true,
ColorRange = true,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = av1BitDepth
};
int alphaQuality = this.encoder.AlphaQuality ?? quality;
int alphaQIndex = GetAv1QuantizerIndex(alphaQuality);
cancellationToken.ThrowIfCancellationRequested();
long alphaOffset = stream.Length;
ObuSequenceHeader alphaHeader = Av1FrameEncoder.EncodeAlpha(
this.configuration,
image.Frames.RootFrame,
stream,
alphaConfig,
alphaQIndex,
this.encoder.Effort);
long alphaLength = stream.Length - alphaOffset;
HeifItem alphaItem = new(Heif4CharCode.Av01, 2)
{
ChannelCount = 1,
UniformChannelBitDepth = channelBitDepth,
BitsPerPixel = channelBitDepth,
Av1CodecConfiguration = new Av1CodecConfiguration(alphaHeader),
AuxiliaryType = HeifConstants.AlphaAuxiliaryType
};
alphaItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, alphaOffset, alphaLength));
alphaItem.SetExtent(image.Size);
items.Add(alphaItem);
HeifItemLink alphaLink = new(Heif4CharCode.Auxl, alphaItem.Id);
alphaLink.DestinationIds.Add(colorItem.Id);
links.Add(alphaLink);
}
/// <summary>
/// Encodes the source pixels as the current legacy JPEG item payload.
/// </summary>

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

@ -161,6 +161,12 @@ internal sealed class HeifItem(Heif4CharCode type, uint id)
/// </summary>
public byte[]? ChannelBitDepths { get; set; }
/// <summary>
/// Gets or sets the common encoded precision of every image channel, or <see langword="null"/> when channel
/// precision is absent or represented individually by <see cref="ChannelBitDepths"/>.
/// </summary>
public byte? UniformChannelBitDepth { get; set; }
/// <summary>
/// Gets or sets the number of bits in a single pixel.
/// </summary>

18
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -87,7 +87,8 @@ public class Av1EncoderFrameTests
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37);
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
string outputDirectory = Path.Combine(
@ -177,7 +178,8 @@ public class Av1EncoderFrameTests
source.Frames.RootFrame,
stream,
CreateColorConfig(bitDepth),
qIndex: 37);
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
@ -273,7 +275,8 @@ public class Av1EncoderFrameTests
source.Frames.RootFrame,
stream,
CreateColorConfig(bitDepth, colorFormat),
qIndex: 37);
qIndex: 37,
effort: 5);
Av1CodecConfiguration configuration = new(sequenceHeader);
byte[] fixedHeader = new byte[Av1CodecConfiguration.FixedHeaderSize];
@ -459,7 +462,8 @@ public class Av1EncoderFrameTests
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37);
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
Av1BitStreamReader reader = new(payload);
@ -594,7 +598,8 @@ public class Av1EncoderFrameTests
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37);
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
@ -797,7 +802,8 @@ public class Av1EncoderFrameTests
source.Frames.RootFrame,
destination,
CreateColorConfig(Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv444),
qIndex: 37);
qIndex: 37,
effort: 5);
Assert.False(destination.CanSeek);
Assert.NotEqual(0, storage.Length);

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

@ -261,7 +261,8 @@ public class Av1IntraSuperblockEncoderTests
tileCoefficients,
tileSuperblockWorkspace,
tileBlockWorkspace,
initialSize: 512);
initialSize: 512,
effort: 5);
// The production tile traversal must be byte-identical to the explicit analyze-then-write composition above.
Assert.True(encoded.GetSpan().SequenceEqual(tileWriter.GetTileData(0)));
@ -409,7 +410,8 @@ public class Av1IntraSuperblockEncoderTests
liveCoefficients,
liveSuperblockWorkspace,
liveBlockWorkspace,
initialSize: 256);
initialSize: 256,
effort: 5);
Assert.True(precomputedTile.GetSpan().SequenceEqual(liveTileWriter.GetTileData(0)));
}
@ -532,7 +534,8 @@ public class Av1IntraSuperblockEncoderTests
tileCoefficients,
tileSuperblockWorkspace,
tileBlockWorkspace,
initialSize: 256);
initialSize: 256,
effort: 5);
Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
ushort reconstructedSample = tileReconstruction.Frame.CodedView
@ -737,7 +740,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256));
initialSize: 256,
effort: 5));
AssertProductionTileSelectsExactLumaPalette(
Av1BitDepth.TwelveBit,
@ -757,7 +761,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256));
initialSize: 256,
effort: 5));
AssertProductionTileSelectsExactLumaPalette(
Av1BitDepth.EightBit,
@ -777,7 +782,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256));
initialSize: 256,
effort: 5));
}
[Fact]
@ -881,7 +887,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 256);
initialSize: 256,
effort: 5);
ref Av1MacroBlockModeInfo mode = ref picture.Picture.GetMacroBlockModeInfo(default);
Assert.Equal(Av1ChromaPredictionMode.DC, mode.Block.UvMode);
@ -1092,7 +1099,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512);
initialSize: 512,
effort: 5);
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
Assert.Equal(expectedMode, targetBlock.Block.Mode);
@ -1204,7 +1212,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512);
initialSize: 512,
effort: 5);
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
Assert.Equal(expectedMode, targetBlock.Block.UvMode);
@ -1245,7 +1254,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512));
initialSize: 512,
effort: 5));
[Theory]
[InlineData((int)Av1ColorFormat.Yuv420, 10)]
@ -1269,7 +1279,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512));
initialSize: 512,
effort: 5));
private static void VerifyProductionTileSelectsChromaFromReconstructedLuma<TSample>(
int colorFormatValue,
@ -1518,7 +1529,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512),
initialSize: 512,
effort: 5),
static (mode, destination, above, left, _, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
.Predict(destination, 8, above, left, 8, 8, scratch));
@ -1549,7 +1561,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512),
initialSize: 512,
effort: 5),
static (mode, destination, above, left, sampleBitDepth, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
.Predict(
@ -1855,7 +1868,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 2048);
initialSize: 2048,
effort: 5);
ref Av1MacroBlockModeInfo topRightBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(0, 2));
ref Av1MacroBlockModeInfo bottomLeftBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(16, 0));
@ -1880,7 +1894,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096));
initialSize: 4096,
effort: 5));
VerifyProductionTileSelectsIntraBlockCopy(
Av1BitDepth.TwelveBit,
@ -1895,7 +1910,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096));
initialSize: 4096,
effort: 5));
}
private static void VerifyProductionTileSelectsIntraBlockCopy<TSample>(
@ -2121,7 +2137,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096);
initialSize: 4096,
effort: 5);
Point targetModeInfoPosition = new(TargetColumn >> Av1Constants.ModeInfoSizeLog2, 0);
ref Av1MacroBlockModeInfo targetMode = ref picture.Picture.GetMacroBlockModeInfo(targetModeInfoPosition);
@ -2230,7 +2247,8 @@ public class Av1IntraSuperblockEncoderTests
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 4096);
initialSize: 4096,
effort: 5);
Assert.Equal(4, coefficients.SuperblockCount);
bool usesNonDctTransform = false;

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

@ -18,6 +18,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif;
[ValidateDisposedMemoryAllocations]
public class HeifEncoderTests
{
private const int Av1EightBit = (int)Av1BitDepth.EightBit;
private const int Av1TenBit = (int)Av1BitDepth.TenBit;
private const int Av1TwelveBit = (int)Av1BitDepth.TwelveBit;
private const int Yuv400 = (int)Av1ColorFormat.Yuv400;
private const int Yuv420 = (int)Av1ColorFormat.Yuv420;
private const int Yuv422 = (int)Av1ColorFormat.Yuv422;
private const int Yuv444 = (int)Av1ColorFormat.Yuv444;
[Fact]
public void OptionsHaveExpectedDefaults()
{
@ -142,16 +150,321 @@ public class HeifEncoderTests
}
[Fact]
public void Av1RejectsEncodingBeforeWritingOutput()
public void Av1RejectsLosslessEncodingBeforeWritingOutput()
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true
};
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
Assert.Equal(0, stream.Length);
}
[Fact]
public void Av1RejectsImageSequenceBeforeWritingOutput()
{
using Image<Rgb24> image = new(1, 1);
image.Frames.AddFrame(image.Frames.RootFrame);
using MemoryStream stream = new();
HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1 };
Assert.Throws<NotSupportedException>(() => image.Save(stream, encoder));
Assert.Equal(0, stream.Length);
}
[Theory]
[InlineData(0, 255)]
[InlineData(1, 249)]
[InlineData(2, 249)]
[InlineData(50, 128)]
[InlineData(60, 100)]
[InlineData(75, 64)]
[InlineData(99, 4)]
[InlineData(100, 4)]
public void Av1QualityMapsThroughLibaomQuantizers(int quality, int expectedQIndex)
=> Assert.Equal(expectedQIndex, HeifEncoderCore.GetAv1QuantizerIndex(quality));
[Fact]
public void Av1WritesStillImageWithRequiredBrandsAndProductionPayload()
{
const int width = 16;
const int height = 16;
using Image<Rgb24> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgb24> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgb24(
(byte)(column * 11),
(byte)(row * 13),
(byte)((column + row) * 7));
}
}
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Quality = 75,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
ReadOnlySpan<byte> fileType = file.AsSpan(0, 28);
Assert.Equal(28, BinaryPrimitives.ReadInt32BigEndian(fileType));
Assert.Equal(Heif4CharCode.Ftyp, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[4..]));
Assert.Equal(Heif4CharCode.Avif, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[8..]));
Assert.Equal(0, BinaryPrimitives.ReadInt32BigEndian(fileType[12..]));
Assert.Equal(Heif4CharCode.Avif, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[16..]));
Assert.Equal(Heif4CharCode.Mif1, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[20..]));
Assert.Equal(Heif4CharCode.Miaf, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[24..]));
Span<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb24> payloadImage = payloadDecoder.Decode<Rgb24>(payload);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(payloadDecoder.FrameHeader);
Assert.Equal(64, frameHeader.QuantizationParameters.BaseQIndex);
Assert.Equal(image.Size, payloadImage.Size);
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream);
HeifMetadata metadata = decoded.Metadata.GetHeifMetadata();
Assert.Equal(image.Size, decoded.Size);
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.False(metadata.IsMonochrome);
Assert.False(metadata.HasAlpha);
CicpProfile colorProfile = Assert.IsType<CicpProfile>(decoded.Metadata.CicpProfile);
Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, colorProfile.MatrixCoefficients);
Assert.False(colorProfile.FullRange);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-16x16-8b-420.avif"), file);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-16x16-8b-420.obu"), payload.ToArray());
}
[Fact]
public void Av1WritesAuxiliaryAlphaFromSourcePixelType()
{
const int width = 16;
const int height = 8;
using Image<Rgba32> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = column < 8
? new Rgba32(40, 80, 120, 0)
: new Rgba32(40, 80, 120, 255);
}
}
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Quality = 75,
AlphaQuality = 100,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> colorPayload = GetItemPayload(file, 1);
Span<byte> alphaPayload = GetItemPayload(file, 2);
using Av1Decoder colorDecoder = new(Configuration.Default);
using Image<Rgba32> colorImage = colorDecoder.Decode<Rgba32>(colorPayload);
ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader);
Assert.Equal(64, colorFrameHeader.QuantizationParameters.BaseQIndex);
using Av1Decoder alphaDecoder = new(Configuration.Default);
using Image<L8> alphaImage = alphaDecoder.Decode<L8>(alphaPayload);
ObuSequenceHeader alphaSequenceHeader = Assert.IsType<ObuSequenceHeader>(alphaDecoder.SequenceHeader);
ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader);
Assert.True(alphaSequenceHeader.ColorConfig.IsMonochrome);
Assert.Equal(4, alphaFrameHeader.QuantizationParameters.BaseQIndex);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
HeifMetadata metadata = decoded.Metadata.GetHeifMetadata();
Assert.True(metadata.HasAlpha);
Assert.InRange(decoded[0, 0].A, (byte)0, (byte)8);
Assert.InRange(decoded[width - 1, 0].A, (byte)247, byte.MaxValue);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-alpha-color.obu"), colorPayload.ToArray());
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-alpha-auxiliary.obu"), alphaPayload.ToArray());
}
[Theory]
[InlineData(HeifBitDepth.Bit8, HeifChromaSubsampling.Monochrome, Av1EightBit, Yuv400)]
[InlineData(HeifBitDepth.Bit10, HeifChromaSubsampling.Yuv420, Av1TenBit, Yuv420)]
[InlineData(HeifBitDepth.Bit10, HeifChromaSubsampling.Yuv422, Av1TenBit, Yuv422)]
[InlineData(HeifBitDepth.Bit12, HeifChromaSubsampling.Yuv444, Av1TwelveBit, Yuv444)]
public void Av1ExplicitPrecisionAndSamplingReachPayload(
HeifBitDepth bitDepth,
HeifChromaSubsampling chromaSubsampling,
int expectedAv1BitDepthValue,
int expectedColorFormatValue)
{
Av1BitDepth expectedAv1BitDepth = (Av1BitDepth)expectedAv1BitDepthValue;
Av1ColorFormat expectedColorFormat = (Av1ColorFormat)expectedColorFormatValue;
using Image<Rgb24> image = new(8, 8);
for (int row = 0; row < image.Height; row++)
{
Span<Rgb24> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < image.Width; column++)
{
pixels[column] = new Rgb24(
(byte)(column * 29),
(byte)(row * 29),
(byte)((column + row) * 13));
}
}
image.Metadata.GetHeifMetadata().BitDepth = HeifBitDepth.Bit10;
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
BitDepth = bitDepth,
ChromaSubsampling = chromaSubsampling,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb48> payloadImage = payloadDecoder.Decode<Rgb48>(payload);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader);
Assert.Equal(expectedAv1BitDepth, sequenceHeader.ColorConfig.BitDepth);
Assert.Equal(expectedColorFormat, sequenceHeader.ColorConfig.GetColorFormat());
Assert.Equal(image.Size, payloadImage.Size);
stream.Position = 0;
using Image<Rgb48> decoded = Image.Load<Rgb48>(stream);
HeifMetadata metadata = decoded.Metadata.GetHeifMetadata();
Assert.Equal(bitDepth, metadata.BitDepth);
Assert.Equal(chromaSubsampling == HeifChromaSubsampling.Monochrome, metadata.IsMonochrome);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-public-8x8-{(int)bitDepth}b-{chromaSubsampling}.obu"),
payload.ToArray());
}
[Fact]
public void Av1UsesMetadataBitDepthWhenOptionIsNull()
{
using Image<Rgb24> image = new(8, 8);
image.Metadata.GetHeifMetadata().BitDepth = HeifBitDepth.Bit10;
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
ChromaSubsampling = HeifChromaSubsampling.Yuv444,
Effort = 0
};
image.Save(stream, encoder);
byte[] file = stream.ToArray();
Span<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb48> payloadImage = payloadDecoder.Decode<Rgb48>(payload);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader);
Assert.Equal(Av1BitDepth.TenBit, sequenceHeader.ColorConfig.BitDepth);
}
[Fact]
public void Av1SanitizesIncompatibleIdentityMatrixWithoutMutatingSourceMetadata()
{
using Image<Rgb24> image = new(8, 8);
CicpProfile sourceProfile = new(1, 13, 0, false);
image.Metadata.CicpProfile = sourceProfile;
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
ChromaSubsampling = HeifChromaSubsampling.Yuv420,
Effort = 0
};
image.Save(stream, encoder);
Assert.Same(sourceProfile, image.Metadata.CicpProfile);
Assert.Equal(CicpMatrixCoefficients.Identity, sourceProfile.MatrixCoefficients);
Assert.False(sourceProfile.FullRange);
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream);
CicpProfile decodedProfile = Assert.IsType<CicpProfile>(decoded.Metadata.CicpProfile);
Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, decodedProfile.MatrixCoefficients);
Assert.False(decodedProfile.FullRange);
}
[Fact]
public void Av1WritesNonSeekableStream()
{
using Image<Rgb24> image = new(8, 8);
using MemoryStream storage = new();
using NonSeekableStream destination = new(storage);
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0
};
image.Save(destination, encoder);
Assert.NotEqual(0, storage.Length);
storage.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(storage);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void Av1WritesAtCurrentStreamPosition()
{
using Image<Rgb24> image = new(8, 8);
using MemoryStream stream = new();
stream.Write([1, 2, 3, 4]);
long fileStart = stream.Position;
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0
};
image.Save(stream, encoder);
stream.Position = fileStart;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void Av1ItemPropertiesWriteRequiredTypesAndEssentialConfiguration()
{
@ -369,4 +682,54 @@ public class HeifEncoderTests
ImageComparer.Exact.CompareImages(image, encodedImage);
Assert.Equal(compressionMethod, heifMetadata.CompressionMethod);
}
private static Span<byte> GetItemPayload(Span<byte> file, ushort itemId)
{
int offset = 0;
while (offset < file.Length)
{
int boxSize = BinaryPrimitives.ReadInt32BigEndian(file[offset..]);
Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(offset + 4)..]);
if (boxType == Heif4CharCode.Meta)
{
int childOffset = offset + 12;
int boxEnd = offset + boxSize;
while (childOffset < boxEnd)
{
int childSize = BinaryPrimitives.ReadInt32BigEndian(file[childOffset..]);
Heif4CharCode childType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(file[(childOffset + 4)..]);
if (childType == Heif4CharCode.Iloc)
{
int locationOffset = childOffset + 14;
int itemCount = BinaryPrimitives.ReadUInt16BigEndian(file[locationOffset..]);
locationOffset += 2;
for (int itemIndex = 0; itemIndex < itemCount; itemIndex++)
{
ushort currentItemId = BinaryPrimitives.ReadUInt16BigEndian(file[locationOffset..]);
locationOffset += 6;
int extentCount = BinaryPrimitives.ReadUInt16BigEndian(file[locationOffset..]);
locationOffset += 2;
for (int extentIndex = 0; extentIndex < extentCount; extentIndex++)
{
int itemOffset = checked((int)BinaryPrimitives.ReadUInt64BigEndian(file[locationOffset..]));
locationOffset += 8;
int itemLength = BinaryPrimitives.ReadInt32BigEndian(file[locationOffset..]);
locationOffset += 4;
if (currentItemId == itemId)
{
return file.Slice(itemOffset, itemLength);
}
}
}
}
childOffset += childSize;
}
}
offset += boxSize;
}
throw new InvalidImageContentException($"The encoded file has no payload for item {itemId}.");
}
}

Loading…
Cancel
Save