diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 1408db5c6a..a6e1fc2bf5 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/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 diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs index 48af6a5942..8226b58fdd 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs +++ b/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 => 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 => Encode(configuration, image, stream, colorConfig, qIndex, effort, true); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs index e65e0007d5..8351842042 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs +++ b/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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs index bc8147e1f9..139c9cf312 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs +++ b/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( 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( configuration, diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs index 1565dbcdc2..8fc0fa93d0 100644 --- a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs @@ -323,6 +323,11 @@ public enum Heif4CharCode : uint /// Mif1 = 0x6D696631U, + /// + /// Multi-Image Application Format brand. + /// + Miaf = 0x6D696166U, + /// /// AVIF brand. /// diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs index 5fc2d18d61..7577d258b8 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs +++ b/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 items = new(); + List 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 items = new(); - List 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 /// The source image. /// The encoded primary-item payload length. /// The destination item collection. - private static void GenerateItems(Image image, long pixelDataLength, List items) + private static void GenerateLegacyJpegItem(Image image, long pixelDataLength, List items) where TPixel : unmanaged, IPixel { HeifItem primaryItem = new(Heif4CharCode.Jpeg, 1u); @@ -144,15 +146,29 @@ internal sealed class HeifEncoderCore /// The number of bytes written. private int WriteFileTypeBox(Stream stream) { - Span buffer = stackalloc byte[16]; + Span 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; } + /// + /// Writes one common encoded precision for every image channel. + /// + /// The expanding 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, + int memoryOffset, + int channelCount, + byte channelBitDepth) + { + Span 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; + } + /// /// Writes an AV1 codec-configuration property. /// @@ -622,6 +674,193 @@ internal sealed class HeifEncoderCore data.WriteTo(stream); } + /// + /// Maps the public lossy quality scale through libaom's external quantizer scale to its internal quantizer index. + /// + /// The lossy quality in the inclusive range zero through one hundred. + /// The AV1 quantizer index. + 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; + } + + /// + /// Encodes the source root frame into AV1 color and optional auxiliary-alpha item payloads. + /// + /// The source pixel format. + /// The source image. + /// The shared destination for consecutive item payloads. + /// The destination item declarations. + /// The destination item relationships. + /// The token used to cancel payload encoding. + private void CompressAv1Pixels( + Image image, + ChunkedMemoryStream stream, + List items, + List links, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + 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); + } + /// /// 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 eebb831b8a..5531fd9645 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -161,6 +161,12 @@ internal sealed class HeifItem(Heif4CharCode type, uint id) /// public byte[]? ChannelBitDepths { get; set; } + /// + /// Gets or sets the common encoded precision of every image channel, or when channel + /// precision is absent or represented individually by . + /// + public byte? UniformChannelBitDepth { get; set; } + /// /// Gets or sets the number of bits in a single pixel. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs index 0b168f635d..dcbe393c7e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs +++ b/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); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs index cefefc60d4..e9377db1b1 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs +++ b/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( 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( @@ -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; diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs index 43eb593d3b..5bbcdb31db 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs +++ b/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 image = new(1, 1); using MemoryStream stream = new(); + HeifEncoder encoder = new() + { + CompressionMethod = HeifCompressionMethod.Av1, + Lossless = true + }; + + Assert.Throws(() => image.Save(stream, encoder)); + Assert.Equal(0, stream.Length); + } + + [Fact] + public void Av1RejectsImageSequenceBeforeWritingOutput() + { + using Image image = new(1, 1); + image.Frames.AddFrame(image.Frames.RootFrame); + using MemoryStream stream = new(); HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.Av1 }; Assert.Throws(() => 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 image = new(width, height); + for (int row = 0; row < height; row++) + { + Span 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 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 payload = GetItemPayload(file, 1); + using Av1Decoder payloadDecoder = new(Configuration.Default); + using Image payloadImage = payloadDecoder.Decode(payload); + ObuFrameHeader frameHeader = Assert.IsType(payloadDecoder.FrameHeader); + Assert.Equal(64, frameHeader.QuantizationParameters.BaseQIndex); + Assert.Equal(image.Size, payloadImage.Size); + + stream.Position = 0; + using Image decoded = Image.Load(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(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 image = new(width, height); + for (int row = 0; row < height; row++) + { + Span 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 colorPayload = GetItemPayload(file, 1); + Span alphaPayload = GetItemPayload(file, 2); + using Av1Decoder colorDecoder = new(Configuration.Default); + using Image colorImage = colorDecoder.Decode(colorPayload); + ObuFrameHeader colorFrameHeader = Assert.IsType(colorDecoder.FrameHeader); + Assert.Equal(64, colorFrameHeader.QuantizationParameters.BaseQIndex); + + using Av1Decoder alphaDecoder = new(Configuration.Default); + using Image alphaImage = alphaDecoder.Decode(alphaPayload); + ObuSequenceHeader alphaSequenceHeader = Assert.IsType(alphaDecoder.SequenceHeader); + ObuFrameHeader alphaFrameHeader = Assert.IsType(alphaDecoder.FrameHeader); + Assert.True(alphaSequenceHeader.ColorConfig.IsMonochrome); + Assert.Equal(4, alphaFrameHeader.QuantizationParameters.BaseQIndex); + + stream.Position = 0; + using Image decoded = Image.Load(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 image = new(8, 8); + for (int row = 0; row < image.Height; row++) + { + Span 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 payload = GetItemPayload(file, 1); + using Av1Decoder payloadDecoder = new(Configuration.Default); + using Image payloadImage = payloadDecoder.Decode(payload); + ObuSequenceHeader sequenceHeader = Assert.IsType(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 decoded = Image.Load(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 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 payload = GetItemPayload(file, 1); + using Av1Decoder payloadDecoder = new(Configuration.Default); + using Image payloadImage = payloadDecoder.Decode(payload); + ObuSequenceHeader sequenceHeader = Assert.IsType(payloadDecoder.SequenceHeader); + Assert.Equal(Av1BitDepth.TenBit, sequenceHeader.ColorConfig.BitDepth); + } + + [Fact] + public void Av1SanitizesIncompatibleIdentityMatrixWithoutMutatingSourceMetadata() + { + using Image 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 decoded = Image.Load(stream); + CicpProfile decodedProfile = Assert.IsType(decoded.Metadata.CicpProfile); + Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, decodedProfile.MatrixCoefficients); + Assert.False(decodedProfile.FullRange); + } + + [Fact] + public void Av1WritesNonSeekableStream() + { + using Image 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 decoded = Image.Load(storage); + Assert.Equal(image.Size, decoded.Size); + } + + [Fact] + public void Av1WritesAtCurrentStreamPosition() + { + using Image 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 decoded = Image.Load(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 GetItemPayload(Span 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}."); + } }