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.
///