Browse Source

Validate AV1 still-picture framing

pull/2633/head
James Jackson-South 2 months ago
parent
commit
07cc0291e3
  1. 21
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs
  2. 53
      tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

21
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)
{

53
tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

@ -167,6 +167,59 @@ public class ObuFrameHeaderTests
Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader));
}
/// <summary>
/// Verifies that the reduced sequence syntax cannot be used without declaring a still picture.
/// </summary>
[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<InvalidImageContentException>(() => ReadObuStream(bitStream));
}
/// <summary>
/// Verifies that a declared still-picture sequence rejects frame prefixes which do not describe a shown key frame.
/// </summary>
/// <param name="invalidFramePrefix">The high nibble containing show-existing, frame-type, and show-frame syntax.</param>
[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<InvalidImageContentException>(() => ReadObuStream(bitStream));
}
/// <summary>
/// Verifies that ignored OBU payloads are bounded and skipped before parsing the following sequence header.
/// </summary>

Loading…
Cancel
Save