diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs index 3cac9f2d4a..67ae414159 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs @@ -27,7 +27,12 @@ internal ref struct Av1BitStreamWriter(Stream stream) public void Flush() { - this.stream.WriteByte(this.buffer); + if (Av1Math.Modulus8(this.bitOffset) != 0) + { + // Flush a partial byte also. + this.stream.WriteByte(this.buffer); + } + this.bitOffset = 0; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index aafe086447..7721f6d39c 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -21,7 +21,7 @@ internal class Av1Decoder : IAv1TileDecoder public void Decode(Span buffer) { Av1BitStreamReader reader = new(buffer); - this.obuReader.Read(ref reader, buffer.Length, this, false); + this.obuReader.ReadAll(ref reader, buffer.Length, this, false); this.frameBuffer = this.tileDecoder?.FrameBuffer; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index 38507b28aa..36c9b0c15a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -45,7 +45,7 @@ internal class ObuReader /// /// Decode all OBU's in a frame. /// - public void Read(ref Av1BitStreamReader reader, int dataSize, IAv1TileDecoder decoder, bool isAnnexB = false) + public void ReadAll(ref Av1BitStreamReader reader, int dataSize, IAv1TileDecoder decoder, bool isAnnexB = false) { bool seenFrameHeader = false; bool frameDecodingFinished = false; @@ -258,7 +258,7 @@ internal class ObuReader /// /// 5.5.1. General sequence header OBU syntax. /// - private static void ReadSequenceHeader(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) + internal static void ReadSequenceHeader(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { sequenceHeader.SequenceProfile = (ObuSequenceProfile)reader.ReadLiteral(3); if (sequenceHeader.SequenceProfile > Av1Constants.MaxSequenceProfile) @@ -1212,7 +1212,7 @@ internal class ObuReader /// /// 5.9.1. General frame header OBU syntax. /// - private void ReadFrameHeader(ref Av1BitStreamReader reader, ObuHeader header, bool trailingBit) + internal void ReadFrameHeader(ref Av1BitStreamReader reader, ObuHeader header, bool trailingBit) { int planeCount = this.SequenceHeader!.ColorConfig.PlaneCount; int startBitPosition = reader.BitPosition; diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs index 85f744dc15..ea38b28975 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs @@ -10,25 +10,37 @@ internal class ObuWriter /// /// Encode a single frame into OBU's. /// - public static void Write(Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameInfo) + public static void WriteAll(Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameInfo) { MemoryStream bufferStream = new(100); Av1BitStreamWriter writer = new(bufferStream); WriteObuHeaderAndSize(stream, ObuType.TemporalDelimiter, [], 0); - WriteSequenceHeader(ref writer, sequenceHeader); - writer.Flush(); - WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, bufferStream.GetBuffer(), (int)bufferStream.Position); + if (sequenceHeader != null) + { + WriteSequenceHeader(ref writer, sequenceHeader); + int bytesWritten = (writer.BitPosition + 7) >> 3; + writer.Flush(); + WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, bufferStream.GetBuffer(), bytesWritten); + } - bufferStream.Position = 0; - WriteFrameHeader(ref writer, sequenceHeader, frameInfo, true); - writer.Flush(); - WriteObuHeaderAndSize(stream, ObuType.FrameHeader, bufferStream.GetBuffer(), (int)bufferStream.Position); + if (frameInfo != null && sequenceHeader != null) + { + bufferStream.Position = 0; + WriteFrameHeader(ref writer, sequenceHeader, frameInfo, true); + int bytesWritten = (writer.BitPosition + 7) >> 3; + writer.Flush(); + WriteObuHeaderAndSize(stream, ObuType.FrameHeader, bufferStream.GetBuffer(), bytesWritten); + } - bufferStream.Position = 0; - WriteTileGroup(ref writer, frameInfo.TilesInfo); - writer.Flush(); - WriteObuHeaderAndSize(stream, ObuType.TileGroup, bufferStream.GetBuffer(), (int)bufferStream.Position); + if (frameInfo?.TilesInfo != null) + { + bufferStream.Position = 0; + WriteTileGroup(ref writer, frameInfo.TilesInfo); + int bytesWritten = (writer.BitPosition + 7) >> 3; + writer.Flush(); + WriteObuHeaderAndSize(stream, ObuType.TileGroup, bufferStream.GetBuffer(), bytesWritten); + } } private static void WriteObuHeader(ref Av1BitStreamWriter writer, ObuType type) @@ -58,7 +70,10 @@ internal class ObuWriter private static void WriteTrailingBits(ref Av1BitStreamWriter writer) { int bitsBeforeAlignment = 8 - (writer.BitPosition & 0x7); - writer.WriteLiteral(0, bitsBeforeAlignment); + if (bitsBeforeAlignment != 8) + { + writer.WriteLiteral(1U << (bitsBeforeAlignment - 1), bitsBeforeAlignment); + } } private static void AlignToByteBoundary(ref Av1BitStreamWriter writer) @@ -101,11 +116,12 @@ internal class ObuWriter writer.WriteBoolean(sequenceHeader.EnableRestoration); WriteColorConfig(ref writer, sequenceHeader); writer.WriteBoolean(sequenceHeader.AreFilmGrainingParametersPresent); + WriteTrailingBits(ref writer); } private static void WriteColorConfig(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader) { - ObuColorConfig colorConfig = new(); + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; WriteBitDepth(ref writer, colorConfig, sequenceHeader); if (sequenceHeader.SequenceProfile != ObuSequenceProfile.High) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs index 93de8f2271..dbc02ea6fb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs @@ -40,7 +40,7 @@ internal enum Av1TransformType : byte /// /// Number of Transform types. /// - TransformTypes, + AllTransformTypes, /// /// Invalid value. diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScanOrderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScanOrderTests.cs new file mode 100644 index 0000000000..8bceeb136b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScanOrderTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1ScanOrderTests +{ + [Theory] + [MemberData(nameof(GetCombinations))] + internal void AllIndicesScannedExactlyOnce(int s, int t) + { + // Assign + HashSet visitedScans = []; + Av1TransformSize transformSize = (Av1TransformSize)s; + Av1TransformType transformType = (Av1TransformType)t; + + // Act + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + + // Assert + foreach (short scan in scanOrder.Scan) + { + Assert.False(visitedScans.Contains(scan), $"Scan {scan} already visited before."); + visitedScans.Add(scan); + } + } + + public static TheoryData GetCombinations() + { + TheoryData combinations = []; + for (int s = 0; s < (int)Av1TransformSize.AllSizes; s++) + { + for (int t = 0; t < (int)Av1TransformType.AllTransformTypes; t++) + { + combinations.Add(s, t); + } + } + + return combinations; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index 2d91f6f7f2..d17c49a179 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -1,9 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Collections; -using System.Reflection; -using System.Text; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; @@ -13,7 +10,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; public class ObuFrameHeaderTests { private static readonly byte[] DefaultSequenceHeaderBitStream = - [0x0a, 0x0b, 0x00, 0x00, 0x00, 0x04, 0x3e, 0xa7, 0xbd, 0xf7, 0xf9, 0x80, 0x40]; + [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]; + + // Bits Syntax element Value + // 1 obu_forbidden_bit 0 + // 4 obu_type 2 (OBU_TEMPORAL_DELIMITER) + // 1 obu_extension_flag 0 + // 1 obu_has_size_field 1 + // 1 obu_reserved_1bit 0 + // 8 obu_size 0 + private static readonly byte[] DefaultTemporalDelimiterBitStream = [0x12, 0x00]; [Theory] // [InlineData(TestImages.Heif.IrvineAvif, 0x0102, 0x000D)] @@ -30,7 +36,7 @@ public class ObuFrameHeaderTests ObuReader obuReader = new(); // Act - obuReader.Read(ref reader, blockSize, decoder); + obuReader.ReadAll(ref reader, blockSize, decoder); // Assert Assert.NotNull(obuReader.SequenceHeader); @@ -54,7 +60,7 @@ public class ObuFrameHeaderTests ObuReader obuReader = new(); // Act 1 - obuReader.Read(ref reader, blockSize, tileDecoder); + obuReader.ReadAll(ref reader, blockSize, tileDecoder); // Assign 2 MemoryStream encoded = new(); @@ -81,13 +87,13 @@ public class ObuFrameHeaderTests ObuReader obuReader1 = new(); // Act 1 - obuReader1.Read(ref reader, blockSize, tileDecoder); + obuReader1.ReadAll(ref reader, blockSize, tileDecoder); // Assign 2 MemoryStream encoded = new(); // Act 2 - ObuWriter.Write(encoded, obuReader1.SequenceHeader, obuReader1.FrameHeader); + ObuWriter.WriteAll(encoded, obuReader1.SequenceHeader, obuReader1.FrameHeader); // Assign 2 Span encodedBuffer = encoded.ToArray(); @@ -96,7 +102,7 @@ public class ObuFrameHeaderTests ObuReader obuReader2 = new(); // Act 2 - obuReader2.Read(ref reader2, encodedBuffer.Length, tileDecoder2); + obuReader2.ReadAll(ref reader2, encodedBuffer.Length, tileDecoder2); // Assert Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(obuReader1.SequenceHeader.ColorConfig), ObuPrettyPrint.PrettyPrintProperties(obuReader2.SequenceHeader.ColorConfig)); @@ -106,16 +112,15 @@ public class ObuFrameHeaderTests } [Fact] - public void DefaultTemporalDelimiter() + public void ReadTemporalDelimiter() { // Arrange - byte[] bitStream = [0x12, 0x00]; - Av1BitStreamReader reader = new(bitStream); + Av1BitStreamReader reader = new(DefaultTemporalDelimiterBitStream); ObuReader obuReader = new(); IAv1TileDecoder tileDecoder = new Av1TileDecoderStub(); // Act - obuReader.Read(ref reader, bitStream.Length, tileDecoder); + obuReader.ReadAll(ref reader, DefaultTemporalDelimiterBitStream.Length, tileDecoder); // Assert Assert.Null(obuReader.SequenceHeader); @@ -123,27 +128,16 @@ public class ObuFrameHeaderTests } [Fact] - public void DefaultTemporalDelimiterWithExtension() + public void ReadHeaderWithoutSizeField() { - // Bits Syntax element Value - // 1 obu_forbidden_bit 0 - // 4 obu_type 2 (OBU_TEMPORAL_DELIMITER) - // 1 obu_extension_flag 1 - // 1 obu_has_size_field 1 - // 1 obu_reserved_1bit 0 - // 3 temporal_id 6 - // 2 spatial_id 2 - // 3 extension_header_reserved_3bits 0 - // 8 obu_size 0 - // Arrange - byte[] bitStream = [0x16, 0xd0, 0x00]; + byte[] bitStream = [0x10]; Av1BitStreamReader reader = new(bitStream); ObuReader obuReader = new(); IAv1TileDecoder tileDecoder = new Av1TileDecoderStub(); // Act - obuReader.Read(ref reader, bitStream.Length, tileDecoder); + obuReader.ReadAll(ref reader, bitStream.Length, tileDecoder); // Assert Assert.Null(obuReader.SequenceHeader); @@ -151,109 +145,119 @@ public class ObuFrameHeaderTests } [Fact] - public void DefaultHeaderWithoutSizeField() + public void ReadSequenceHeader() { // Arrange - byte[] bitStream = [0x10]; + byte[] bitStream = DefaultSequenceHeaderBitStream; Av1BitStreamReader reader = new(bitStream); ObuReader obuReader = new(); IAv1TileDecoder tileDecoder = new Av1TileDecoderStub(); + ObuSequenceHeader expected = GetDefaultSequenceHeader(); // Act - obuReader.Read(ref reader, bitStream.Length, tileDecoder); + obuReader.ReadAll(ref reader, bitStream.Length, tileDecoder); // Assert - Assert.Null(obuReader.SequenceHeader); + Assert.NotNull(obuReader.SequenceHeader); Assert.Null(obuReader.FrameHeader); + Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); } [Fact] - public void DefaultSequenceHeader() + public void WriteTemporalDelimiter() { - // Offset Bits Syntax element Value - // 0 3 seq_profile 0 - // 3 1 still_picture 0 - // 4 1 reduced_still_picture_header 0 - // 5 1 timing_info_present_flag 0 - // 6 1 initial_display_delay_present_flag 0 - // 7 5 operating_points_cnt_minus_1 0 - // 12 12 operating_point_idc[ 0 ] 0 - // 24 5 seq_level_idx[ 0 ] 0 - // 29 4 frame_width_bits_minus_1 8 - // 33 4 frame_height_bits_minus_1 7 - // 37 9 max_frame_width_minus_1 425 - // 46 8 max_frame_height_minus_1 239 - // 54 1 frame_id_numbers_present_flag 0 - // 55 1 use_128x128_superblock 1 - // 56 1 enable_filter_intra 1 - // 57 1 enable_intra_edge_filter 1 - // 58 1 enable_interintra_compound 1 - // 59 1 enable_masked_compound 1 - // 60 1 enable_warped_motion 0 - // 61 1 enable_dual_filter 1 - // 62 1 enable_order_hint 1 - // 63 1 enable_jnt_comp 1 - // 64 1 enable_ref_frame_mvs 1 - // 65 1 seq_choose_screen_content_tools 1 - // 66 1 seq_choose_integer_mv 1 - // 67 3 order_hint_bits_minus_1 6 - // 70 1 enable_superres 0 - // 71 1 enable_cdef 1 - // 72 1 enable_restoration 1 - // ... + // Arrange + using MemoryStream stream = new(2); + // Act + ObuWriter.WriteAll(stream, null, null); + byte[] actual = stream.GetBuffer(); + + // Assert + Assert.Equal(DefaultTemporalDelimiterBitStream, actual); + } + + [Fact] + public void WriteSequenceHeader() + { // Arrange - byte[] bitStream = DefaultSequenceHeaderBitStream; - Av1BitStreamReader reader = new(bitStream); - ObuReader obuReader = new(); - IAv1TileDecoder tileDecoder = new Av1TileDecoderStub(); - ObuSequenceHeader expected = new() - { - SequenceProfile = 0, - IsStillPicture = false, - IsReducedStillPictureHeader = false, - TimingInfoPresentFlag = false, - InitialDisplayDelayPresentFlag = false, - FrameWidthBits = 8 + 1, - FrameHeightBits = 7 + 1, - MaxFrameWidth = 425 + 1, - MaxFrameHeight = 239 + 1, - IsFrameIdNumbersPresent = false, - Use128x128Superblock = true, - EnableFilterIntra = true, - EnableIntraEdgeFilter = true, - EnableInterIntraCompound = true, - EnableMaskedCompound = true, - EnableWarpedMotion = false, - EnableDualFilter = true, - EnableOrderHint = true, - OperatingPoint = [new()], - - // EnableJountCompound = true, - // EnableReferenceFrameMotionVectors = true, - ForceScreenContentTools = 2, - ForceIntegerMotionVector = 2, - EnableSuperResolution = false, - EnableCdef = true, - EnableRestoration = true, - ColorConfig = new() - { - IsMonochrome = false, - ColorPrimaries = ObuColorPrimaries.Unspecified, - TransferCharacteristics = ObuTransferCharacteristics.Unspecified, - MatrixCoefficients = ObuMatrixCoefficients.Unspecified, - SubSamplingX = true, - SubSamplingY = true, - BitDepth = 8, - } - }; + using MemoryStream stream = new(10); + ObuSequenceHeader input = GetDefaultSequenceHeader(); // Act - obuReader.Read(ref reader, bitStream.Length, tileDecoder); + ObuWriter.WriteAll(stream, input, null); + byte[] buffer = stream.GetBuffer(); // Assert - Assert.NotNull(obuReader.SequenceHeader); - Assert.Null(obuReader.FrameHeader); - Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); + // Skip over Temporal Delimiter header. + byte[] actual = buffer.AsSpan()[DefaultTemporalDelimiterBitStream.Length..].ToArray(); + Assert.Equal(DefaultSequenceHeaderBitStream, actual); } + + private static ObuSequenceHeader GetDefaultSequenceHeader() + + // Offset Bits Syntax element Value + // 0 3 seq_profile 1 + // 3 1 still_picture 1 + // 4 1 reduced_still_picture_header 1 + // 5 5 seq_level_idx[ 0 ] 0 + // 10 4 frame_width_bits_minus_1 8 + // 14 4 frame_height_bits_minus_1 7 + // 18 9 max_frame_width_minus_1 425 + // 27 8 max_frame_height_minus_1 239 + // 35 1 use_128x128_superblock 1 + // 36 1 enable_filter_intra 1 + // 37 1 enable_intra_edge_filter 1 + // 38 1 enable_superres 0 + // 39 1 enable_cdef 1 + // 40 1 enable_restoration 1 + // 41 1 ColorConfig.BitDepth.HasHighBit 0 + // 42 1 ColorConfig.IsDescriptionPresent 0 + // 43 1 ColorConfig.ColorRange 1 + // 44 1 ColorConfig.HasSeparateUVDelta 1 + // 45 1 film_grain_present 1 + // 47 2 Trailing bits 2 + => new() + { + SequenceProfile = ObuSequenceProfile.High, + IsStillPicture = true, + IsReducedStillPictureHeader = true, + TimingInfoPresentFlag = false, + InitialDisplayDelayPresentFlag = false, + FrameWidthBits = 8 + 1, + FrameHeightBits = 7 + 1, + MaxFrameWidth = 425 + 1, + MaxFrameHeight = 239 + 1, + IsFrameIdNumbersPresent = false, + Use128x128Superblock = true, + EnableFilterIntra = true, + EnableIntraEdgeFilter = true, + EnableInterIntraCompound = false, + EnableMaskedCompound = false, + EnableWarpedMotion = false, + EnableDualFilter = false, + EnableOrderHint = false, + OperatingPoint = [new()], + + // EnableJountCompound = true, + // EnableReferenceFrameMotionVectors = true, + ForceScreenContentTools = 2, + ForceIntegerMotionVector = 2, + EnableSuperResolution = false, + EnableCdef = true, + EnableRestoration = true, + ColorConfig = new() + { + IsMonochrome = false, + ColorPrimaries = ObuColorPrimaries.Unspecified, + TransferCharacteristics = ObuTransferCharacteristics.Unspecified, + MatrixCoefficients = ObuMatrixCoefficients.Unspecified, + SubSamplingX = false, + SubSamplingY = false, + BitDepth = 8, + HasSeparateUvDelta = true, + ColorRange = true, + }, + AreFilmGrainingParametersPresent = true, + }; }