From 07cc0291e3f35edc227405f924147b2a6ac59787 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Aug 2026 11:51:49 +1000 Subject: [PATCH] Validate AV1 still-picture framing --- .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 21 +++++++- .../Formats/Heif/Av1/ObuFrameHeaderTests.cs | 53 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index 3b28ca1519..bfc5b2ef6e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -385,6 +385,13 @@ internal class ObuReader sequenceHeader.IsStillPicture = reader.ReadBoolean(); sequenceHeader.IsReducedStillPictureHeader = reader.ReadBoolean(); + if (!sequenceHeader.IsStillPicture && sequenceHeader.IsReducedStillPictureHeader) + { + // The reduced header omits state required by a multi-frame sequence, so AV1 permits it only when the + // sequence is explicitly declared to contain a single still picture. + throw new InvalidImageContentException("An AV1 reduced still-picture header requires the still-picture flag."); + } + if (sequenceHeader.IsReducedStillPictureHeader) { sequenceHeader.TimingInfo = null; @@ -1005,6 +1012,11 @@ internal class ObuReader frameHeader.ShowExistingFrame = reader.ReadBoolean(); if (frameHeader.ShowExistingFrame) { + if (sequenceHeader.IsStillPicture) + { + throw new InvalidImageContentException("An AV1 still picture cannot display a previously decoded frame."); + } + frameHeader.FrameToShowMapIdx = reader.ReadLiteral(3); if (sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) @@ -1018,12 +1030,17 @@ internal class ObuReader frameHeader.DisplayFrameId = reader.ReadLiteral(idLength); } - // TODO: This is incomplete here, not sure how we can display an already decoded frame here or if this is really relevent for still pictures. - throw new NotImplementedException("ShowExistingFrame is not yet implemented"); + // Showing an existing frame requires sequence reference storage. The image-item decoder deliberately + // owns one independently coded picture and therefore rejects this sequence-only operation at its boundary. + throw new InvalidImageContentException("An AV1 image item cannot display a previously decoded frame."); } frameHeader.FrameType = (ObuFrameType)reader.ReadLiteral(2); frameHeader.ShowFrame = reader.ReadBoolean(); + if (sequenceHeader.IsStillPicture && (frameHeader.FrameType != ObuFrameType.KeyFrame || !frameHeader.ShowFrame)) + { + throw new InvalidImageContentException("An AV1 still picture must be encoded as a shown key frame."); + } if (frameHeader.ShowFrame && sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index 963e3c63d9..ca3643ca35 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -167,6 +167,59 @@ public class ObuFrameHeaderTests Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); } + /// + /// Verifies that the reduced sequence syntax cannot be used without declaring a still picture. + /// + [Fact] + public void ReadReducedHeaderWithoutStillPictureThrowsInvalidImageContent() + { + const int sequenceHeaderPayloadOffset = 2; + byte[] bitStream = [.. DefaultSequenceHeaderBitStream]; + + // The first payload byte stores the three profile bits followed by still_picture and + // reduced_still_picture_header. Clear only still_picture so the remaining syntax stays reduced. + bitStream[sequenceHeaderPayloadOffset] &= 0b1110_1111; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that a declared still-picture sequence rejects frame prefixes which do not describe a shown key frame. + /// + /// The high nibble containing show-existing, frame-type, and show-frame syntax. + [Theory] + [InlineData(0b1000_0000)] // show_existing_frame = 1 + [InlineData(0b0101_0000)] // frame_type = INTRA_ONLY_FRAME, show_frame = 1 + [InlineData(0b0000_0000)] // frame_type = KEY_FRAME, show_frame = 0 + public void ReadInvalidStillPictureFramePrefixThrowsInvalidImageContent(int invalidFramePrefix) + { + ObuSequenceHeader sequenceHeader = GetDefaultSequenceHeader(); + sequenceHeader.IsReducedStillPictureHeader = false; + ObuFrameHeader frameHeader = GetKeyFrameHeader(); + Av1TileDecoderStub tileStub = new(); + byte[] emptyTile = []; + tileStub.ReadTile(emptyTile, 0); + + using MemoryStream stream = new(); + ObuWriter writer = new(); + writer.WriteAll(Configuration.Default, stream, sequenceHeader, frameHeader, tileStub); + byte[] bitStream = stream.ToArray(); + + int sequenceObuOffset = DefaultTemporalDelimiterBitStream.Length; + Av1BitStreamReader sequenceSizeReader = new(bitStream.AsSpan(sequenceObuOffset + 1)); + ulong sequencePayloadLength = sequenceSizeReader.ReadLittleEndianBytes128(out int sequenceSizeLength); + int frameObuOffset = sequenceObuOffset + 1 + sequenceSizeLength + (int)sequencePayloadLength; + Av1BitStreamReader frameSizeReader = new(bitStream.AsSpan(frameObuOffset + 1)); + _ = frameSizeReader.ReadLittleEndianBytes128(out int frameSizeLength); + int framePayloadOffset = frameObuOffset + 1 + frameSizeLength; + + // Preserve syntax after the frame prefix so each malformed stream differs from a valid writer result only in + // show_existing_frame or the still-picture frame-type/show-frame conformance condition under test. + bitStream[framePayloadOffset] = (byte)((bitStream[framePayloadOffset] & 0x0F) | invalidFramePrefix); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + /// /// Verifies that ignored OBU payloads are bounded and skipped before parsing the following sequence header. ///