From 63c4d302ad32319af59806502444abd03fe4b021 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Aug 2026 14:50:47 +1000 Subject: [PATCH] Accept final unsized AV1 OBUs --- HEIF_IMPLEMENTATION_PLAN.md | 11 ++++--- .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 9 ++---- .../Formats/Heif/Av1/ObuFrameHeaderTests.cs | 30 ++++++++++++++++++- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index f843b3e83..bbda34a71 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat ## Delivery dashboard -Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `07cc0291e`, including the completed AV1 transform, OBU-framing, intra-block-copy, and 12-profile reconstruction checkpoints. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead. +Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `91d79f771`, including the completed AV1 transform, OBU-framing, intra-block-copy, and 12-profile reconstruction checkpoints. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead. Status meanings: @@ -40,7 +40,7 @@ Status meanings: Current development stage: **Stage 3 — complete AV1 still-image decoding.** The transform checkpoint is closed: forward transforms use one libaom-shaped SIMD-first operator architecture across `Vector512`, `Vector256`, and `Vector128`, with scalar fallback; inverse production traversal uses the verified `Vector256` and `Vector128` tiers with scalar fallback; and implementation-mechanic type and file suffixes have been removed. Neither AV1 nor HEVC production encoding is implemented. -Immediate checkpoint: **inventory and remove every remaining valid AV1 still-image unsupported branch, one independently verified syntax tool at a time.** The base AV1 profile matrix is now exact across 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 reconstruction and presentation under every available dispatch tier. That matrix is the regression gate for the remaining compression-tool fixtures; it does not by itself prove every normative still-image tool. +Immediate checkpoint: **complete layered AV1 image-item decoding through the existing image-only container surface.** This includes `a1op`, `lsel`, and `a1lx` properties, operating-point selection, dependency-preserving layer consumption, and final or explicitly selected spatial-layer output for color, alpha, and grid items. It requires stateful AV1 reference/CDF reconstruction; it must not be represented as animation or expanded into a general ISO BMFF/video model. | Order | Delivery stage | State | Delivered state | Gate that remains open | | --- | --- | --- | --- | --- | @@ -57,7 +57,10 @@ Immediate checkpoint: **inventory and remove every remaining valid AV1 still-ima - [x] Finish the libaom-shaped AV1 forward-transform architecture, measured production dispatch, inverse-tier correction, suffix cleanup, `FeatureTestRunner` matrix, and focused Release verification recorded below. - [x] Close the base AV1 profile matrix with exact native-plane and presented-image comparisons for 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 fixtures under normal, AVX2, 128-bit, and scalar dispatch. -- [ ] **Current:** inventory and remove every remaining valid AV1 still-image unsupported branch, adding exact independent compression-tool fixtures to the profile-matrix regression gate. +- [x] Accept the AV1-ISOBMFF final low-overhead OBU form that omits its payload-size field and uses the bounded image-item remainder; focused Release coverage reconstructs a valid combined frame in that form. +- [ ] **Current:** implement layered AV1 image-item properties and stateful dependency reconstruction, then verify default final-layer output against the two pinned libavif progressive fixtures. +- [ ] Correct the audited 12-bit inverse ADST4, Identity4, and Identity16 SIMD arithmetic by widening only the libaom-widened multiply/accumulate operations, with exact conformant-range vectors and `FeatureTestRunner` coverage. +- [ ] Continue inventorying and removing every remaining valid AV1 still-image unsupported branch, adding exact independent compression-tool fixtures to the profile-matrix regression gate. - [ ] Complete the remaining HEVC still-image profile and Range Extensions matrix with exact independent native-plane and presentation evidence. - [ ] Close shared decoded presentation, ICC, alpha, grid, transform, metadata, and animated AV1/HEVC decode gates. - [ ] Implement and independently verify real AV1/AVIF still encoding. @@ -497,7 +500,7 @@ Exit gate: Implement and verify in dependency order: - [ ] OBU framing, sequence headers, frame headers, tile groups, byte alignment, and trailing bits. - - [x] Isolate every declared OBU payload with a zero-copy bounded span reader, advance ignored metadata and reserved units without parsing their bytes as headers, validate padding and trailing bytes, enforce primary/redundant/combined frame-header order, and reject lengths that cross the containing image-item boundary. Focused malformed-input tests and independent 8/10/12-bit AVIF reconstruction pass in Release. + - [x] Isolate every declared OBU payload with a zero-copy bounded span reader, advance ignored metadata and reserved units without parsing their bytes as headers, validate padding and trailing bytes, enforce primary/redundant/combined frame-header order, accept the final unsized low-overhead OBU by consuming the bounded image-item remainder, and reject lengths that cross the containing boundary. Focused malformed-input tests and independent 8/10/12-bit AVIF reconstruction pass in Release. - [ ] One coherent decoder lifecycle that retains parsed frame and tile state and disposes all buffers deterministically. - [ ] Tile partitioning, mode information, segmentation, delta quantization, transform-size selection, coefficient token decode, inverse quantization, and inverse transforms. - [x] Match libaom's depth-first traversal and frame-edge behavior for all ten AV1 partition types. Independent 8/10/12-bit streams collectively select every terminal partition shape and contain nested block geometry that requires recursive `Split` traversal; their complete native planes remain byte-exact under normal hardware dispatch and the scalar fallback. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index bfc5b2ef6..457a4df51 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -70,13 +70,6 @@ internal class ObuReader int obuStartBitPosition = reader.BitPosition; ObuHeader header = ReadObuHeaderSize(ref reader, out _); - if (!isAnnexB && !header.HasSize) - { - // AV1 section 5 requires every low-overhead OBU to carry its own payload size. Only Annex B may - // derive the payload length from the outer obu_length field. - throw new InvalidImageContentException("A low-overhead AV1 OBU is missing its payload-size field."); - } - int headerAndLengthSize = (reader.BitPosition - obuStartBitPosition) >> 3; int boundedObuSize = isAnnexB ? annexObuSize : dataSize; if (headerAndLengthSize > boundedObuSize) @@ -84,6 +77,8 @@ internal class ObuReader throw new InvalidImageContentException("The AV1 OBU header exceeds its declared boundary."); } + // AV1-ISOBMFF permits the final low-overhead OBU to omit its size field. In that form the remaining + // sample bytes are the payload, which also makes this OBU final because no following boundary exists. int payloadSize = header.HasSize ? header.PayloadSize : boundedObuSize - headerAndLengthSize; if ((uint)payloadSize > (uint)(boundedObuSize - headerAndLengthSize)) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index ca3643ca3..b2bb1004e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -148,6 +148,35 @@ public class ObuFrameHeaderTests Assert.Null(obuReader.FrameHeader); } + /// + /// Verifies that a final low-overhead frame OBU may use the enclosing image-item boundary instead of an OBU size field. + /// + [Fact] + public void ReadFinalLowOverheadFrameWithoutSizeField() + { + const int itemDataOffset = 0x010E; + const int itemDataLength = 0x001D; + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.Orange4x4); + byte[] fileContent = File.ReadAllBytes(filePath); + byte[] sizedBitStream = fileContent.AsSpan(itemDataOffset, itemDataLength).ToArray(); + int frameOffset = DefaultTemporalDelimiterBitStream.Length; + Av1BitStreamReader sequenceSizeReader = new(sizedBitStream.AsSpan(frameOffset + 1)); + ulong sequencePayloadLength = sequenceSizeReader.ReadLittleEndianBytes128(out int sequenceSizeLength); + frameOffset += 1 + sequenceSizeLength + (int)sequencePayloadLength; + + Av1BitStreamReader sizeReader = new(sizedBitStream.AsSpan(frameOffset + 1)); + ulong framePayloadLength = sizeReader.ReadLittleEndianBytes128(out int encodedSizeLength); + byte[] bitStream = new byte[sizedBitStream.Length - encodedSizeLength]; + + // Preserve the independently encoded frame payload while changing only the final OBU's legal boundary form. + sizedBitStream.AsSpan(0, frameOffset).CopyTo(bitStream); + bitStream[frameOffset] = (byte)(sizedBitStream[frameOffset] & ~0x02); + sizedBitStream.AsSpan(frameOffset + 1 + encodedSizeLength).CopyTo(bitStream.AsSpan(frameOffset + 1)); + + Assert.Equal(framePayloadLength, (ulong)(bitStream.Length - frameOffset - 1)); + ReadObuStream(bitStream); + } + [Fact] public void ReadSequenceHeader() { @@ -253,7 +282,6 @@ public class ObuFrameHeaderTests [InlineData(new byte[] { 0x7A, 0x02, 0x11 })] [InlineData(new byte[] { 0x7A, 0x01, 0x00 })] [InlineData(new byte[] { 0x12, 0x01, 0x01 })] - [InlineData(new byte[] { 0x10 })] [InlineData(new byte[] { 0x7A, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 })] public void ReadInvalidObuBoundaryThrows(byte[] bitStream) => Assert.Throws(() => ReadObuStream(bitStream));