From 243524c2c0b52a49d8d161fab806ab092cabe47c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 31 Aug 2026 07:39:03 +1000 Subject: [PATCH] Complete AV1 bounded OBU parsing checkpoint --- HEIF_IMPLEMENTATION_PLAN.md | 40 +- .../Formats/Heif/Av1/Av1CodecConfiguration.cs | 16 +- .../OpenBitstreamUnit/ObuOperatingPoint.cs | 2 +- .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 138 +++++-- .../Heif/Av1/OpenBitstreamUnit/ObuWriter.cs | 11 +- .../Av1/Av1ReconstructionConformanceTests.cs | 2 +- .../Formats/Heif/Av1/ObuFrameHeaderTests.cs | 357 +++++++++++++++++- .../Heif/Av1/ObuFrameLifecycleTests.cs | 24 ++ ...32_libavif-progressive-draw-points-8b.png} | 0 9 files changed, 537 insertions(+), 53 deletions(-) rename tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/{DecodeProgressiveSingleReferenceMatchesPinnedReferences_Rgba32_libavif-progressive-draw-points-8b.png => DecodeProgressiveSingleReferenceMatchesCurrentLibaomReferences_Rgba32_libavif-progressive-draw-points-8b.png} (100%) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 6f7989b14..0e16688bb 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -127,7 +127,7 @@ Verified single-reference checkpoint evidence on 2026-08-31: - The current-main `aomdec` was rebuilt directly from `D:\GitHub\AOMediaCodec\aom` and identified itself as `3.15.0-13-g441c439b99`. - Decoding the 72-byte progressive payload with `--all-layers`, one thread, and row multithreading disabled produced 2,178 YUV444 color samples. All samples in both layers match the first three planes of the stored YUV444-alpha reference exactly. -- `DecodeProgressiveSingleReferenceMatchesPinnedReferences` executes the production decoder through FeatureTestRunner and compares the complete presented `Rgba32` image with `CompareToReferenceOutput(ImageComparer.Exact, provider)`. The redundant manual alpha loop was removed. +- `DecodeProgressiveSingleReferenceMatchesCurrentLibaomReferences` executes the production decoder through FeatureTestRunner and compares the complete presented `Rgba32` image with `CompareToReferenceOutput(ImageComparer.Exact, provider)`. The redundant manual alpha loop was removed. - `DecodeProgressiveSingleReferenceWithConstrainedAllocator` executes the same production reconstruction with a 1,024-byte allocator group capacity and verifies that every allocation is returned exactly once. - `MotionFieldsFollowReferenceAliasesAndPresentationOwnership`, `MotionFieldAllocationFailureUnwindsTileReaderOwnership`, `DecodeProgressiveSingleReferenceTracksMotionFieldResultOwnership`, and the reference-store replacement, reset, and transfer tests cover initialization, aliases, presentation ownership, decoder-result ownership, failure unwinding, repeated disposal, and exactly-once final returns. - The focused Release set passed 17 of 17 tests on net10.0 and 17 of 17 tests on net11.0, with zero failures and zero skips. This includes both GLOBALMV syntax cases, spatial extension, plane invariants, production reconstruction, FeatureTestRunner dispatch, and ownership. @@ -148,7 +148,7 @@ $env:COMPlus_DbgEnableMiniDump = '0' $env:DOTNET_EnableCrashReport = '0' $env:COMPlus_EnableCrashReport = '0' -$heifCheckpointFilter = 'FullyQualifiedName~Av1InterFrameModeInfoTests.ReadInterFrameModeInfoReadsInterpolationFilters|FullyQualifiedName~Av1InterFrameModeInfoTests.ReadInterFrameModeInfoOmitsInterpolationFiltersForIdentityGlobalMotion|FullyQualifiedName~Av1ReferenceMotionVectorsTests.BuildReversesOppositeDirectionExtensionCandidate|FullyQualifiedName~Av1FrameBufferTests|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldsFollowReferenceAliasesAndPresentationOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldAllocationFailureUnwindsTileReaderOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.PartialReplacementPreservesSharedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.FinalReplacementReleasesDisplacedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.ResetReleasesUniqueOwnersAndClearsSlots|FullyQualifiedName~Av1ReferenceFrameStoreTests.TakeOutputTransfersPlanesAndReleasesOtherReferences|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleReferenceMatchesPinnedReferences|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleReferenceWithConstrainedAllocator|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleReferenceTracksMotionFieldResultOwnership' +$heifCheckpointFilter = 'FullyQualifiedName~Av1InterFrameModeInfoTests.ReadInterFrameModeInfoReadsInterpolationFilters|FullyQualifiedName~Av1InterFrameModeInfoTests.ReadInterFrameModeInfoOmitsInterpolationFiltersForIdentityGlobalMotion|FullyQualifiedName~Av1ReferenceMotionVectorsTests.BuildReversesOppositeDirectionExtensionCandidate|FullyQualifiedName~Av1FrameBufferTests|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldsFollowReferenceAliasesAndPresentationOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldAllocationFailureUnwindsTileReaderOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.PartialReplacementPreservesSharedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.FinalReplacementReleasesDisplacedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.ResetReleasesUniqueOwnersAndClearsSlots|FullyQualifiedName~Av1ReferenceFrameStoreTests.TakeOutputTransfersPlanesAndReleasesOtherReferences|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleReferenceMatchesCurrentLibaomReferences|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleReferenceWithConstrainedAllocator|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleReferenceTracksMotionFieldResultOwnership' dotnet build src\ImageSharp\ImageSharp.csproj -c Release -f net10.0 --no-restore --disable-build-servers -m:1 --no-incremental --nologo --verbosity:minimal dotnet build src\ImageSharp\ImageSharp.csproj -c Release -f net11.0 --no-restore --disable-build-servers -m:1 --no-incremental --nologo --verbosity:minimal @@ -541,6 +541,8 @@ Verified inter-deblocking checkpoint evidence on 2026-08-31: or skips. - [x] Scoped analyzer verification passes for all four changed C# files. Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `fcb502e4960cc7b8efb06b6f060e2c73a913a2bf` + with author and committer `James Jackson-South `. For every item: @@ -556,7 +558,7 @@ For every item: Previously verified algorithm checkpoints remain valuable evidence, but the final decoder gate requires a fresh current-tree run after the inter and cleanup corrections. -- [~] Bounded OBU framing, sequence headers, frame headers, tile groups, alignment, and trailing-bit parsing have historical checkpoint evidence against an obsolete pinned tree. Re-audit the current libaom `main` implementation before restoring verified status. Current item. +- [x] Bounded OBU framing, sequence headers, frame headers, tile groups, alignment, and trailing-bit parsing have been re-audited and verified against current libaom `main`. - [~] Partition traversal, mode information, segmentation, delta quantization, transform-size selection, coefficient decoding, inverse quantization, and inverse transforms have historical checkpoint evidence against an obsolete pinned tree. Re-audit the current libaom `main` implementation before restoring verified status. - [x] Intra prediction covers directional, DC, smooth, Paeth, chroma-from-luma, filter-intra, and palette families with the established operator architecture. - [x] Intra-block copy has exact native reconstruction and feature-isolated SIMD evidence. @@ -571,6 +573,38 @@ Previously verified algorithm checkpoints remain valuable evidence, but the fina - [ ] Complete the public AVIF format/API review so registered capabilities match implemented behavior. - [ ] Remove or reject every valid in-scope AV1 syntax branch that remains silently ignored or unsupported. +Verified bounded-OBU checkpoint evidence on 2026-08-31: + +- [x] Audited `av1/decoder/obu.c`, `av1/decoder/decodeframe.c`, `av1/common/obu_util.c`, + `av1/common/tile_common.c`, `aom/src/aom_integer.c`, and `aom_dsp/bitreader_buffer.c` in the + clean official libaom `main` checkout. Both `HEAD` and `origin/main` resolved to the observed + revision `441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin. +- [x] The bounded container scanner and production OBU reader now agree with current libaom on ignored + reserved header fields and the shared unsigned 32-bit LEB128 limit. +- [x] Sequence-header validation now rejects undefined level indices, initial display delays above ten, + frame identifiers above sixteen bits, zero timing units, the UVLC overflow sentinel, and invalid + identity-matrix profile or subsampling combinations at the owning syntax boundary. +- [x] Frame and tile parsing now rejects `show_existing_frame` in a combined `OBU_FRAME`, the all-slots + intra-only refresh mask, inner tile columns below current libaom's super-resolution-aware minimum, + overflowing or out-of-bounds tile sizes, and empty final tile payloads. +- [x] The still-image writer now emits the required zero tile-bound-presence bit for a multi-tile combined + `OBU_FRAME`, matching current libaom's single-tile-group encoder path. +- [x] `ObuFrameHeaderTests` and `ObuFrameLifecycleTests` cover the corrected syntax through the real + bounded parser. The focused parser set passes 50 of 50 cases on net10.0. +- [x] The final focused production set passes 55 of 55 cases on net10.0 and 55 of 55 on net11.0, with zero + failures or skips. It includes exact final-layer and selected-layer native planes, exact established + reference-image presentation, constrained allocator ownership, malformed-following-OBU recovery, and + FeatureTestRunner normal, AVX-512-disabled, AVX-disabled, and scalar execution. +- [x] A fresh direct foreground current-main `aomdec` run decoded both progressive layers with one thread + and row multithreading disabled. All 2,178 Y, U, and V samples match the retained YUV444-alpha reference; + the alpha plane is excluded from the AV1 native-plane comparison. +- [x] The current-libaom production reference test and its established PNG were renamed together. The PNG + bytes remain unchanged at SHA-256 + `0758C17DC36E38AEE9F4389A335C2BF332AB91E4C79D7B0B22994FDDD0FD1605`, both paths resolve to + `diff=lfs`, and `.gitattributes` was not edited. +- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors. Roslynk reports + zero compiler errors, and scoped production and test analyzer verification reports no changes. + Decoder exit gate: - [ ] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs index 3d7798ec2..087ce232a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -422,11 +422,13 @@ internal sealed class Av1CodecConfiguration while (offset < data.Length) { byte header = data[offset++]; - if ((header & 0x81) != 0) + if ((header & 0x80) != 0) { - throw new InvalidImageContentException($"The {sourceName} contains an OBU with a set forbidden or reserved header bit."); + throw new InvalidImageContentException($"The {sourceName} contains an OBU with a set forbidden header bit."); } + // Current libaom deliberately ignores obu_reserved_1bit. The bit does not alter the OBU boundary or + // decoded syntax, so the bounded container scan must not reject data that the production parser accepts. ObuType type = (ObuType)((header >> 3) & 0x0F); bool hasExtension = (header & 0x04) != 0; bool hasSizeField = (header & 0x02) != 0; @@ -438,11 +440,8 @@ internal sealed class Av1CodecConfiguration throw new InvalidImageContentException($"The {sourceName} contains a truncated OBU extension header."); } + // extension_header_reserved_3bits is also consumed but ignored by current libaom. extension = data[offset++]; - if ((extension & 0x07) != 0) - { - throw new InvalidImageContentException($"The {sourceName} contains an OBU extension with nonzero reserved bits."); - } } if (requireSizeFields && !hasSizeField) @@ -664,6 +663,11 @@ internal sealed class Av1CodecConfiguration value |= (ulong)(current & 0x7F) << (byteIndex * 7); if ((current & 0x80) == 0) { + if (value > uint.MaxValue) + { + throw new InvalidImageContentException($"The {sourceName} contains a {valueName} larger than the AV1 32-bit limit."); + } + return value; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs index a6b63f96a..a36aa68ca 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs @@ -34,7 +34,7 @@ internal class ObuOperatingPoint public bool IsInitialDisplayDelayPresent { get; set; } /// - /// Gets or sets the initial display delay minus one, in decoded frames. + /// Gets or sets the initial display delay, in decoded frames. /// public uint InitialDisplayDelay { get; set; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index d5d7412b6..8363fe99e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -322,11 +322,10 @@ internal class ObuReader if (combinedFrameHeader.ShowExistingFrame) { - // A combined OBU carries no tile-group syntax when it only presents a retained frame. - this.decoder ??= creator(); - frameDecodingFinished = true; - decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition); - break; + // Current libaom permits show_existing_frame only in a standalone frame-header OBU. A + // combined frame OBU is required to continue with a tile group and therefore cannot use + // the header-only retained-frame presentation form. + throw new InvalidImageContentException("A combined AV1 frame OBU cannot display an existing frame."); } goto TILE_GROUP; @@ -476,20 +475,20 @@ internal class ObuReader header.Type = (ObuType)reader.ReadLiteral(4); header.HasExtension = reader.ReadBoolean(); header.HasSize = reader.ReadBoolean(); - if (reader.ReadBoolean()) - { - throw new ImageFormatException("Reserved bit in header should be unset."); - } + + // Current libaom consumes obu_reserved_1bit without rejecting its value. Reserved fields do not change the + // decoded syntax, so accepting either value preserves forward-compatible framing while the forbidden bit + // remains a hard error above. + _ = reader.ReadBoolean(); if (header.HasExtension) { header.Size++; header.TemporalId = (int)reader.ReadLiteral(3); header.SpatialId = (int)reader.ReadLiteral(2); - if (reader.ReadLiteral(3) != 0u) - { - throw new ImageFormatException("Reserved bits in header extension should be unset."); - } + + // Current libaom likewise consumes extension_header_reserved_3bits without interpreting their value. + _ = reader.ReadLiteral(3); } else { @@ -575,7 +574,6 @@ internal class ObuReader /// Computes the mode-information dimensions and stride for the current frame. /// /// The sequence header defining the maximum frame geometry and superblock size. - /// SVT: compute_image_size private void ComputeImageSize(ObuSequenceHeader sequenceHeader) { ObuFrameHeader frameHeader = this.FrameHeader!; @@ -652,6 +650,11 @@ internal class ObuReader Idc = reader.ReadLiteral(12), SequenceLevelIndex = (int)reader.ReadLiteral(5) }; + if (!IsValidSequenceLevel(sequenceHeader.OperatingPoint[i].SequenceLevelIndex)) + { + throw new InvalidImageContentException("The AV1 sequence header contains an undefined sequence-level index."); + } + if (sequenceHeader.OperatingPoint[i].SequenceLevelIndex > 7) { sequenceHeader.OperatingPoint[i].SequenceTier = (int)reader.ReadLiteral(1); @@ -682,15 +685,17 @@ internal class ObuReader if (sequenceHeader.OperatingPoint[i].IsInitialDisplayDelayPresent) { sequenceHeader.OperatingPoint[i].InitialDisplayDelay = reader.ReadLiteral(4) + 1; + if (sequenceHeader.OperatingPoint[i].InitialDisplayDelay > 10) + { + throw new InvalidImageContentException("The AV1 initial display delay exceeds ten decoded frames."); + } } } } } - // Video related flags removed - - // SVT-TODO: int operatingPoint = this.ChooseOperatingPoint(); - // sequenceHeader.OperatingPointIndex = (int)operatingPointIndices[operatingPoint]; + // The operating-point selector is supplied by the bounded item or sequence decoder. Every operating point is + // still parsed above because its timing syntax precedes the shared coded-image dimensions. sequenceHeader.FrameWidthBits = (int)reader.ReadLiteral(4) + 1; sequenceHeader.FrameHeightBits = (int)reader.ReadLiteral(4) + 1; sequenceHeader.MaxFrameWidth = (int)reader.ReadLiteral(sequenceHeader.FrameWidthBits) + 1; @@ -709,9 +714,12 @@ internal class ObuReader sequenceHeader.DeltaFrameIdLength = (int)reader.ReadLiteral(4) + 2; sequenceHeader.AdditionalFrameIdLength = reader.ReadLiteral(3) + 1; sequenceHeader.FrameIdLength = sequenceHeader.DeltaFrameIdLength + (int)sequenceHeader.AdditionalFrameIdLength; + if (sequenceHeader.FrameIdLength > 16) + { + throw new InvalidImageContentException("The AV1 frame identifier length exceeds sixteen bits."); + } } - // Video related flags removed sequenceHeader.Use128x128Superblock = reader.ReadBoolean(); sequenceHeader.EnableFilterIntra = reader.ReadBoolean(); sequenceHeader.EnableIntraEdgeFilter = reader.ReadBoolean(); @@ -783,7 +791,6 @@ internal class ObuReader } } - // Video related flags removed sequenceHeader.EnableSuperResolution = reader.ReadBoolean(); sequenceHeader.EnableCdef = reader.ReadBoolean(); sequenceHeader.EnableRestoration = reader.ReadBoolean(); @@ -836,6 +843,13 @@ internal class ObuReader colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Srgb && colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity) { + if (sequenceHeader.SequenceProfile != ObuSequenceProfile.High + && !(sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional + && colorConfig.BitDepth == Av1BitDepth.TwelveBit)) + { + throw new InvalidImageContentException("The AV1 sRGB identity-matrix color configuration is incompatible with its sequence profile."); + } + // AV1 defines this RGB identity-matrix combination as full-range 4:4:4 and omits // the range and subsampling syntax that other color combinations carry. colorConfig.ColorRange = true; @@ -874,6 +888,12 @@ internal class ObuReader break; } + if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity + && (colorConfig.SubSamplingX || colorConfig.SubSamplingY)) + { + throw new InvalidImageContentException("The AV1 identity matrix requires 4:4:4 color sampling."); + } + if (colorConfig.SubSamplingX && colorConfig.SubSamplingY) { colorConfig.ChromaSamplePosition = (ObuChromoSamplePosition)reader.ReadLiteral(2); @@ -916,16 +936,29 @@ internal class ObuReader /// The sequence header that receives the timing information. private static void ReadTimingInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { + uint numUnitsInDisplayTick = reader.ReadLiteral(32); + uint timeScale = reader.ReadLiteral(32); + if (numUnitsInDisplayTick == 0 || timeScale == 0) + { + throw new InvalidImageContentException("The AV1 timing tick and time scale must both be nonzero."); + } + sequenceHeader.TimingInfo = new ObuTimingInfo { - NumUnitsInDisplayTick = reader.ReadLiteral(32), - TimeScale = reader.ReadLiteral(32), + NumUnitsInDisplayTick = numUnitsInDisplayTick, + TimeScale = timeScale, EqualPictureInterval = reader.ReadBoolean() }; if (sequenceHeader.TimingInfo.EqualPictureInterval) { - sequenceHeader.TimingInfo.NumTicksPerPicture = reader.ReadUnsignedVariableLength() + 1; + uint numTicksPerPictureMinusOne = reader.ReadUnsignedVariableLength(); + if (numTicksPerPictureMinusOne == uint.MaxValue) + { + throw new InvalidImageContentException("The AV1 ticks-per-picture value exceeds its permitted range."); + } + + sequenceHeader.TimingInfo.NumTicksPerPicture = numTicksPerPictureMinusOne + 1; } } @@ -1290,6 +1323,24 @@ internal class ObuReader throw new ImageFormatException("Tile width or height too big."); } + if (tileInfo.TileColumnCount > 1) + { + int minimumInnerTileWidth = 64 << (frameHeader.FrameSize.FrameWidth != frameHeader.FrameSize.SuperResolutionUpscaledWidth ? 1 : 0); + for (int column = 0; column < tileInfo.TileColumnCount - 1; column++) + { + int tileWidth = (tileInfo.TileColumnStartModeInfo[column + 1] - tileInfo.TileColumnStartModeInfo[column]) + << Av1Constants.ModeInfoSizeLog2; + + // Current libaom excludes the rightmost column from this conformance check because it receives the + // remainder of the coded width. Every inner column must be at least 64 pixels, doubled when the frame + // is super-resolution scaled. + if (tileWidth < minimumInnerTileWidth) + { + throw new InvalidImageContentException("The AV1 frame contains an inner tile column narrower than the permitted minimum."); + } + } + } + if (tileInfo.TileColumnCountLog2 > 0 || tileInfo.TileRowCountLog2 > 0) { tileInfo.ContextUpdateTileId = reader.ReadLiteral(tileInfo.TileRowCountLog2 + tileInfo.TileColumnCountLog2); @@ -1318,12 +1369,8 @@ internal class ObuReader ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuFrameHeader frameHeader = this.FrameHeader!; Av1ReferenceFrame? primaryReference = null; - int idLength = sequenceHeader.FrameIdLength; bool frameSizeOverrideFlag = false; - if (sequenceHeader.IsFrameIdNumbersPresent) - { - DebugGuard.MustBeLessThanOrEqualTo(idLength, 16, nameof(idLength)); - } + int idLength = sequenceHeader.FrameIdLength; if (sequenceHeader.IsReducedStillPictureHeader) { @@ -1563,7 +1610,10 @@ internal class ObuReader if (frameHeader.FrameType == ObuFrameType.IntraOnlyFrame) { - DebugGuard.IsTrue(frameHeader.RefreshFrameFlags != 0xFFU, nameof(frameHeader.RefreshFrameFlags)); + if (frameHeader.RefreshFrameFlags == byte.MaxValue) + { + throw new InvalidImageContentException("An AV1 intra-only frame cannot refresh every reference-map slot."); + } } if (!frameHeader.IsIntra || (frameHeader.RefreshFrameFlags != 0xFFU)) @@ -1890,11 +1940,35 @@ internal class ObuReader for (int tileNum = tileGroupStart; tileNum <= tileGroupEnd; tileNum++) { bool isLastTile = tileNum == tileGroupEnd; - int tileDataSize = header.PayloadSize; + int tileDataSize; if (!isLastTile) { - tileDataSize = (int)reader.ReadLittleEndian(tileInfo.TileSizeBytes) + 1; - header.PayloadSize -= tileDataSize + tileInfo.TileSizeBytes; + if (header.PayloadSize <= tileInfo.TileSizeBytes) + { + throw new InvalidImageContentException("The AV1 tile group ends before its declared tile-size field and payload."); + } + + uint tileDataSizeMinusOne = reader.ReadLittleEndian(tileInfo.TileSizeBytes); + header.PayloadSize -= tileInfo.TileSizeBytes; + + // Compare in the encoded unsigned domain before adding one. A four-byte 0xFFFFFFFF field would + // otherwise wrap to a zero-length signed tile and shift the following tile boundary. + if (tileDataSizeMinusOne >= (uint)header.PayloadSize) + { + throw new InvalidImageContentException("The AV1 tile size exceeds the remaining tile-group payload."); + } + + tileDataSize = (int)tileDataSizeMinusOne + 1; + header.PayloadSize -= tileDataSize; + } + else + { + tileDataSize = header.PayloadSize; + header.PayloadSize = 0; + if (tileDataSize <= 0) + { + throw new InvalidImageContentException("The AV1 tile group contains an empty tile payload."); + } } Span tileData = reader.GetSymbolReader(tileDataSize); diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs index 376b7f108..4744a5988 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs @@ -523,14 +523,9 @@ internal class ObuWriter int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; if (tileCount > 1) { - // This writer places every tile in one group, so the optional range spans the - // complete frame whenever the range syntax is present. - writer.WriteBoolean(true); - uint tileGroupStart = 0U; - uint tileGroupEnd = (uint)tileCount - 1U; - int tileBits = tileInfo.TileColumnCountLog2 + tileInfo.TileRowCountLog2; - writer.WriteLiteral(tileGroupStart, tileBits); - writer.WriteLiteral(tileGroupEnd, tileBits); + // A combined OBU_FRAME has implicit complete-frame tile bounds. Current libaom still + // writes the presence bit for a multi-tile frame, but requires that bit to remain zero. + writer.WriteBoolean(false); } AlignToByteBoundary(ref writer); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs index 14de4b743..cb542a71c 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -686,7 +686,7 @@ public class Av1ReconstructionConformanceTests /// The AVIF input and matching reference-output naming context. [Theory] [WithFile(TestImages.Heif.Av1Progressive8BitAvif, PixelTypes.Rgba32)] - public void DecodeProgressiveSingleReferenceMatchesPinnedReferences( + public void DecodeProgressiveSingleReferenceMatchesCurrentLibaomReferences( TestImageProvider provider) => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateProgressiveSingleReferenceFixtureWithDefaultConfiguration, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index e937a1c78..3717fade6 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -1,14 +1,67 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; [Trait("Format", "Avif")] public class ObuFrameHeaderTests { + /// + /// Identifies one current-libaom sequence-header conformance condition used by the malformed-input theory. + /// + public enum InvalidSequenceHeaderCase + { + /// + /// Encodes the otherwise valid baseline. + /// + None, + + /// + /// Encodes an unassigned sequence-level index. + /// + UndefinedSequenceLevel, + + /// + /// Encodes an initial display delay greater than ten frames. + /// + InitialDisplayDelayAboveTen, + + /// + /// Encodes a frame identifier wider than sixteen bits. + /// + FrameIdentifierLengthAboveSixteen, + + /// + /// Encodes a zero display-tick unit. + /// + ZeroDisplayTick, + + /// + /// Encodes a zero time scale. + /// + ZeroTimeScale, + + /// + /// Encodes the unsigned-variable-length overflow sentinel. + /// + OverflowingTicksPerPicture, + + /// + /// Encodes the sRGB identity-matrix tuple with the main profile. + /// + MainProfileSrgbIdentity, + + /// + /// Encodes an identity matrix with subsampled components. + /// + SubsampledIdentityMatrix + } + private static readonly byte[] DefaultSequenceHeaderBitStream = [0x0a, 0x06, 0b001_1_1_000, 0b00_1000_01, 0b11_110101, 0b001_11101, 0b111_1_1_1_0_1, 0b1_0_0_1_1_1_10]; @@ -204,6 +257,88 @@ public class ObuFrameHeaderTests Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); } + /// + /// Verifies current-libaom sequence-header conformance failures through the complete bounded OBU parser. + /// + /// The single invalid syntax condition encoded into an otherwise valid sequence header. + [Theory] + [InlineData(InvalidSequenceHeaderCase.UndefinedSequenceLevel)] + [InlineData(InvalidSequenceHeaderCase.InitialDisplayDelayAboveTen)] + [InlineData(InvalidSequenceHeaderCase.FrameIdentifierLengthAboveSixteen)] + [InlineData(InvalidSequenceHeaderCase.ZeroDisplayTick)] + [InlineData(InvalidSequenceHeaderCase.ZeroTimeScale)] + [InlineData(InvalidSequenceHeaderCase.OverflowingTicksPerPicture)] + [InlineData(InvalidSequenceHeaderCase.MainProfileSrgbIdentity)] + [InlineData(InvalidSequenceHeaderCase.SubsampledIdentityMatrix)] + public void ReadSequenceHeaderRejectsCurrentLibaomConformanceFailure(InvalidSequenceHeaderCase invalidCase) + { + byte[] bitStream = CreateNonReducedSequenceHeaderObu(invalidCase); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that reserved OBU header fields are ignored consistently by container validation and syntax parsing. + /// + /// Whether the sequence header also carries nonzero reserved extension bits. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadSequenceHeaderIgnoresReservedObuHeaderBits(bool hasExtension) + { + byte[] bitStream; + if (hasExtension) + { + bitStream = new byte[DefaultSequenceHeaderBitStream.Length + 1]; + bitStream[0] = (byte)(DefaultSequenceHeaderBitStream[0] | 0x05); + bitStream[1] = 0x07; + DefaultSequenceHeaderBitStream.AsSpan(1).CopyTo(bitStream.AsSpan(2)); + } + else + { + bitStream = [.. DefaultSequenceHeaderBitStream]; + bitStream[0] |= 0x01; + } + + Av1CodecConfiguration configuration = new([0x81, 0x00, 0x00, 0x00], new DecoderOptions()); + configuration.ValidateItemData( + bitStream, + null, + null, + new DecoderOptions(), + out _, + out _); + + ReadObuStream(bitStream); + } + + /// + /// Verifies that metadata-type LEB128 values obey current libaom's shared unsigned 32-bit limit. + /// + [Fact] + public void ValidateItemDataRejectsMetadataTypeAboveCurrentLibaomLimit() + { + byte[] bitStream = + [ + .. DefaultSequenceHeaderBitStream, + + // Metadata type 2^32 followed by valid byte-aligned trailing bits. + 0x2A, 0x06, 0x80, 0x80, 0x80, 0x80, 0x10, 0x80 + ]; + + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Av1CodecConfiguration configuration = new([0x81, 0x00, 0x00, 0x00], options); + + Assert.Throws( + () => configuration.ValidateItemData( + bitStream, + null, + null, + options, + out _, + out _)); + } + /// /// Verifies that an item cannot select an operating-point index absent from its sequence header. /// @@ -364,6 +499,74 @@ public class ObuFrameHeaderTests Assert.Equal(bitStream.Length * 8, reader.BitPosition); } + /// + /// Verifies that a four-byte tile size cannot wrap into an empty first tile. + /// + [Fact] + public void ReadTileGroupRejectsFourByteTileSizeOverflow() + { + byte[] bitStream = CreateTwoTileFrame(GetDefaultSequenceHeader(), GetKeyFrameHeader(), 4); + Span encodedTileSize = bitStream.AsSpan(bitStream.Length - 6, 4); + encodedTileSize.Fill(byte.MaxValue); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies current libaom's doubled minimum inner-tile width for a super-resolution-scaled frame. + /// + [Fact] + public void ReadTileInfoRejectsNarrowSuperResolutionInnerTile() + { + ObuSequenceHeader sequenceHeader = GetDefaultSequenceHeader(); + sequenceHeader.Use128x128Superblock = false; + sequenceHeader.EnableSuperResolution = true; + sequenceHeader.FrameWidthBits = 8; + sequenceHeader.MaxFrameWidth = 192; + + ObuFrameHeader frameHeader = GetKeyFrameHeader(); + frameHeader.FrameSize.FrameWidth = 96; + frameHeader.FrameSize.SuperResolutionUpscaledWidth = 192; + frameHeader.FrameSize.RenderWidth = 192; + frameHeader.FrameSize.SuperResolutionDenominator = 16; + frameHeader.ModeInfoColumnCount = 24; + + byte[] bitStream = CreateTwoTileFrame(sequenceHeader, frameHeader, 1); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that an intra-only frame cannot signal the all-slots refresh mask reserved for key and switch frames. + /// + [Fact] + public void ReadFrameHeaderRejectsIntraOnlyAllSlotsRefresh() + { + byte[] sequenceHeader = CreateNonReducedSequenceHeaderObu(default); + using AutoExpandingMemory frameMemory = new(Configuration.Default, 8); + Av1BitStreamWriter frameWriter = new(frameMemory); + + frameWriter.WriteBoolean(false); + frameWriter.WriteLiteral((uint)ObuFrameType.IntraOnlyFrame, 2); + frameWriter.WriteBoolean(true); + frameWriter.WriteBoolean(true); + frameWriter.WriteBoolean(false); + frameWriter.WriteBoolean(false); + frameWriter.WriteLiteral(byte.MaxValue, 8); + + int framePayloadLength = (frameWriter.BitPosition + 7) >> 3; + frameWriter.Flush(); + + byte[] bitStream = new byte[sequenceHeader.Length + 2 + framePayloadLength]; + sequenceHeader.CopyTo(bitStream, 0); + int frameObuOffset = sequenceHeader.Length; + bitStream[frameObuOffset] = (byte)(((byte)ObuType.FrameHeader << 3) | 0x02); + bitStream[frameObuOffset + 1] = (byte)framePayloadLength; + frameMemory.GetSpan(framePayloadLength).CopyTo(bitStream.AsSpan(frameObuOffset + 2)); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + /// /// Verifies that invalid OBU boundaries, size fields, and trailing bytes are rejected. /// @@ -430,8 +633,8 @@ public class ObuFrameHeaderTests ObuSequenceHeader sequenceInput = GetDefaultSequenceHeader(); ObuFrameHeader frameInput = GetKeyFrameHeader(); Av1TileDecoderStub tileStub = new(); - byte[] empty = []; - tileStub.ReadTile(empty, 0); + byte[] tileData = [0x80]; + tileStub.ReadTile(tileData, 0); ObuWriter obuWriter = new(); // Act @@ -457,6 +660,156 @@ public class ObuFrameHeaderTests Assert.Equal(bitStream.Length * 8, reader.BitPosition); } + /// + /// Encodes one non-reduced sequence header with a single selected conformance failure. + /// + /// The syntax condition to make invalid, or . + /// The complete explicitly sized sequence-header OBU. + private static byte[] CreateNonReducedSequenceHeaderObu(InvalidSequenceHeaderCase invalidCase) + { + bool hasTimingInfo = invalidCase is + InvalidSequenceHeaderCase.ZeroDisplayTick or + InvalidSequenceHeaderCase.ZeroTimeScale or + InvalidSequenceHeaderCase.OverflowingTicksPerPicture; + + bool hasInitialDisplayDelay = invalidCase == InvalidSequenceHeaderCase.InitialDisplayDelayAboveTen; + bool hasFrameIdentifiers = invalidCase == InvalidSequenceHeaderCase.FrameIdentifierLengthAboveSixteen; + bool hasColorDescription = invalidCase is + InvalidSequenceHeaderCase.MainProfileSrgbIdentity or + InvalidSequenceHeaderCase.SubsampledIdentityMatrix; + + using AutoExpandingMemory payloadMemory = new(Configuration.Default, 32); + Av1BitStreamWriter writer = new(payloadMemory); + writer.WriteLiteral((uint)ObuSequenceProfile.Main, 3); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(hasTimingInfo); + if (hasTimingInfo) + { + writer.WriteLiteral(invalidCase == InvalidSequenceHeaderCase.ZeroDisplayTick ? 0U : 1U, 32); + writer.WriteLiteral(invalidCase == InvalidSequenceHeaderCase.ZeroTimeScale ? 0U : 1U, 32); + + bool overflowingTicksPerPicture = invalidCase == InvalidSequenceHeaderCase.OverflowingTicksPerPicture; + writer.WriteBoolean(overflowingTicksPerPicture); + if (overflowingTicksPerPicture) + { + // Thirty-two leading zeros are the UVLC sentinel which current libaom rejects as UINT32_MAX. + writer.WriteLiteral(0U, 32); + } + + writer.WriteBoolean(false); + } + + writer.WriteBoolean(hasInitialDisplayDelay); + writer.WriteLiteral(0U, 5); + writer.WriteLiteral(0U, 12); + + uint sequenceLevel = invalidCase == InvalidSequenceHeaderCase.UndefinedSequenceLevel ? 24U : 0U; + writer.WriteLiteral(sequenceLevel, 5); + if (sequenceLevel > 7) + { + writer.WriteBoolean(false); + } + + if (hasInitialDisplayDelay) + { + writer.WriteBoolean(true); + writer.WriteLiteral(10U, 4); + } + + writer.WriteLiteral(7U, 4); + writer.WriteLiteral(7U, 4); + writer.WriteLiteral(63U, 8); + writer.WriteLiteral(63U, 8); + writer.WriteBoolean(hasFrameIdentifiers); + if (hasFrameIdentifiers) + { + writer.WriteLiteral(15U, 4); + writer.WriteLiteral(0U, 3); + } + + // Disable superblock and intra-edge tools. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Disable the inter compound, warped, dual-filter, and order-hint tools. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Select fixed disabled screen-content and integer-motion-vector behavior. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Disable super resolution, CDEF, and restoration in the otherwise valid baseline. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Encode an 8-bit, non-monochrome color configuration. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(hasColorDescription); + if (hasColorDescription) + { + bool isSrgbIdentity = invalidCase == InvalidSequenceHeaderCase.MainProfileSrgbIdentity; + writer.WriteLiteral((uint)(isSrgbIdentity ? ObuColorPrimaries.Bt709 : ObuColorPrimaries.Unspecified), 8); + writer.WriteLiteral((uint)(isSrgbIdentity ? ObuTransferCharacteristics.Srgb : ObuTransferCharacteristics.Unspecified), 8); + writer.WriteLiteral((uint)ObuMatrixCoefficients.Identity, 8); + } + + if (invalidCase != InvalidSequenceHeaderCase.MainProfileSrgbIdentity) + { + writer.WriteBoolean(false); + writer.WriteLiteral((uint)ObuChromoSamplePosition.Unknown, 2); + } + + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + int trailingBitCount = 8 - (writer.BitPosition & 0x07); + writer.WriteLiteral(1U << (trailingBitCount - 1), trailingBitCount); + + int payloadLength = (writer.BitPosition + 7) >> 3; + writer.Flush(); + + byte[] obu = new byte[payloadLength + 2]; + obu[0] = (byte)(((byte)ObuType.SequenceHeader << 3) | 0x02); + obu[1] = (byte)payloadLength; + payloadMemory.GetSpan(payloadLength).CopyTo(obu.AsSpan(2)); + return obu; + } + + /// + /// Encodes one valid reduced frame with two one-byte tile payloads. + /// + /// The sequence syntax to encode. + /// The frame syntax to encode. + /// The number of bytes used for the first tile's size field. + /// The complete temporal delimiter, sequence header, and combined frame OBU stream. + private static byte[] CreateTwoTileFrame( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + int tileSizeBytes) + { + frameHeader.TilesInfo.HasUniformTileSpacing = true; + frameHeader.TilesInfo.TileColumnCount = 2; + frameHeader.TilesInfo.TileRowCount = 1; + frameHeader.TilesInfo.TileSizeBytes = tileSizeBytes; + + Av1TileDecoderStub tileStub = new(); + tileStub.ReadTile([0x80], 0); + tileStub.ReadTile([0x80], 1); + + using MemoryStream stream = new(); + ObuWriter writer = new(); + writer.WriteAll(Configuration.Default, stream, sequenceHeader, frameHeader, tileStub); + return stream.ToArray(); + } + private static ObuSequenceHeader GetDefaultSequenceHeader() // Offset Bits Syntax element Value diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs index e53139a4b..bdb1d4630 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs @@ -166,6 +166,30 @@ public class ObuFrameLifecycleTests Assert.Equal(1, factory.Readers[2].CompletionCount); } + /// + /// Verifies that a combined frame OBU cannot use the header-only retained-frame presentation form. + /// + [Fact] + public void ReadAllRejectsShowExistingFrameInCombinedFrameObu() + { + byte[] bitStream = + [ + .. ProgressiveTwoFrameObuStream[..FirstProgressiveLayerLength], + + // A one-byte combined-frame payload selecting retained slot zero. Current libaom rejects this form + // because show_existing_frame is permitted only in a standalone frame-header OBU. + 0x32, 0x01, 0x80 + ]; + + using Av1ReferenceFrameStore referenceFrames = new(); + ObuReader obuReader = new(ProgressiveOperatingPointIndex, referenceFrames); + LifecycleTileReaderFactory factory = new(obuReader, referenceFrames, NoFailingReaderIndex); + + Assert.Throws(() => ReadObuStream(bitStream, obuReader, factory.Create)); + AssertParserSessionReset(obuReader, referenceFrames); + Assert.Equal(1, Assert.Single(factory.Readers).CompletionCount); + } + /// /// Extends the sequence-header OBU by one nonzero byte while retaining all following encoded frame bytes. /// diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleReferenceMatchesPinnedReferences_Rgba32_libavif-progressive-draw-points-8b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleReferenceMatchesCurrentLibaomReferences_Rgba32_libavif-progressive-draw-points-8b.png similarity index 100% rename from tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleReferenceMatchesPinnedReferences_Rgba32_libavif-progressive-draw-points-8b.png rename to tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleReferenceMatchesCurrentLibaomReferences_Rgba32_libavif-progressive-draw-points-8b.png