diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 6dc04842d..50748dea9 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -44,13 +44,15 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Keep optimized color arithmetic, fixed-point parameter storage, SIMD-width traversal, sample loading/storage, chroma filtering, and RGB packing primitives in the shared HEIF color namespace. AV1 and HEVC namespaces adapt their native planes, crop windows, and signaled chroma positions without owning parallel color arithmetic. - [x] Implement sequential pooled HEVC conversion in both directions for monochrome, 4:2:0, 4:2:2, and 4:4:4; independent luma/chroma precision; 8/10/12-bit full/limited ranges; and all six progressive 4:2:0 chroma sample locations. Eight-bit paths use JPEG's optimized RGB plane contracts, while high-bit-depth paths retain 16-bit packed pixels. - [x] Implement allocation-free SIMD-first HEVC intra prediction for all 35 modes, 4/8/16/32 blocks, and 8/10/12-bit samples, including three-tap and strong-bilinear reference filtering, negative-angle reference extension, luma boundary filters, and SIMD horizontal transposition. Verify the normal and forced-scalar paths against one scalar oracle. + - [x] Match HM's channel-specific smoothing rules: combined 4:4:4 chroma uses the normal three-tap filter when selected, while strong bilinear smoothing remains restricted to luma syntax. Exact official RExt reconstruction passes at 8, 10, and 12 bits. - [x] Implement allocation-free reconstructed-plane reference collection and normative substitution for complete, empty, and partially available borders, including asymmetric 4:2:2 availability units and the HEVC mode/size/component filter-selection rules. - [x] Implement allocation-free SIMD-first HEVC inverse quantization for flat and custom scaling lists, 4/8/16/32 transforms, transform-skip scaling rules, extended precision, signed normalization shifts, and transform-range clipping. Store compact and transform-sized scaling matrices in one contiguous parameter-set allocation so the hot path can widen consecutive weights without coordinate division. - [x] Derive each reference-unit availability flag from coding-tree, slice, tile, constrained-intra, and picture-boundary state, then connect prediction to transform-unit traversal and the bounded item decoder lifecycle. - [x] Register the bounded `hvc1` still-image decoder, reconstruct the selected PPS/SPS picture, crop the conformance window directly in the pooled row converter, and pass the native planes through the shared SIMD-first H.273 pipeline. - [x] Compare native HEVC planes byte-for-byte with HM 18 output for four item payloads extracted from independently produced HEIC files: two 8-bit 4:2:0 grid tiles, a cropped 8-bit 4:2:0 grid tile, and an 8-bit 4:4:4 wavefront tile. - [x] Compare complete HEIC presentation output exactly with libheif 1.23.1 and libde265 1.1.1 for single images, multi-tile grids, an auxiliary alpha image, ICC-bearing images, 4:2:0, and 4:4:4 output. Keep the exact HM native-plane tests as the independent codec-reconstruction oracle. - - [ ] Add permanent HEVC 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 fixtures covering every exposed profile and Range Extensions tool, with exact native-plane references from HM or another independent decoder. + - [x] Add permanent official HEVC 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 fixtures with exact native-plane references and published per-plane digests. All 10 first pictures from the Sony GENERAL RExt conformance streams match every Y, Cb, and Cr sample with normal SIMD dispatch and with all hardware intrinsics disabled. + - [ ] Extend the permanent HEVC matrix to every exposed profile and each Range Extensions tool not individually isolated by the Sony GENERAL streams, with exact native-plane references from HM or another independent decoder. - [x] Implement the shared SIMD-first YUV/CICP paths in both directions for AV1 and HEVC bit depths, chroma formats, ranges, matrix coefficients, transfer functions, color primaries, and chroma positions, with scalar fallback only after the available vector widths are exhausted. - [ ] Complete the independently generated AV1 and HEVC conformance matrix for every supported color-signaling combination before closing the Phase 5 color exit gate. - [x] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation. diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs index 64210e9d4..6ebf0900c 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs @@ -89,6 +89,10 @@ internal sealed partial class HevcPictureDecoder ReadOnlySpan selectedLeft = left[..referenceLength]; if (filterReferences) { + // Normal three-tap smoothing extends to combined 4:4:4 chroma, but strong bilinear smoothing is a luma + // operation. Separate color planes use luma syntax and therefore retain the luma behavior. + bool useStrongSmoothing = useLumaSyntax && this.sequenceParameterSet.StrongIntraSmoothingEnabled; + HevcIntraPredictor.FilterReferenceSamples( selectedTop, selectedLeft, @@ -96,7 +100,7 @@ internal sealed partial class HevcPictureDecoder filteredLeft, log2Size, this.Picture.GetBitDepth(plane), - this.sequenceParameterSet.StrongIntraSmoothingEnabled); + useStrongSmoothing); selectedTop = filteredTop[..referenceLength]; selectedLeft = filteredLeft[..referenceLength]; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcIntraPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcIntraPredictorTests.cs index 8378e77f0..6a30dc970 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcIntraPredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcIntraPredictorTests.cs @@ -23,7 +23,7 @@ public class HevcIntraPredictorTests int[] modes = [0, 1, 2, 9, 18, 30, 34]; ushort[][] expected = [ - [83, 97, 110, 123, 87, 97, 108, 118, 90, 98, 105, 113, 93, 98, 103, 108], + [86, 101, 117, 132, 90, 103, 115, 128, 94, 104, 114, 123, 98, 105, 112, 119], [84, 93, 97, 101, 88, 92, 92, 92, 90, 92, 92, 92, 91, 92, 92, 92], [76, 82, 88, 94, 82, 88, 94, 100, 88, 94, 100, 106, 94, 100, 106, 112], [70, 71, 71, 72, 76, 77, 77, 78, 82, 83, 83, 84, 88, 89, 89, 90], @@ -191,8 +191,8 @@ public class HevcIntraPredictorTests { for (int x = 0; x < size; x++) { - int horizontal = ((size - x - 1) * left[y + 1]) + ((x + 1) * top[size]); - int vertical = ((size - y - 1) * top[x + 1]) + ((y + 1) * left[size]); + int horizontal = ((size - x - 1) * left[y + 1]) + ((x + 1) * top[size + 1]); + int vertical = ((size - y - 1) * top[x + 1]) + ((y + 1) * left[size + 1]); destination[(y * destinationStride) + x] = (ushort)((horizontal + vertical + size) >> shift); } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcPictureDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcPictureDecoderTests.cs index 27a81568a..edb28fafd 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcPictureDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcPictureDecoderTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; +using System.Security.Cryptography; using SixLabors.ImageSharp.Formats.Heif.Hevc; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; @@ -11,6 +13,71 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; [Trait("Format", "Heif")] public class HevcPictureDecoderTests { + /// + /// Verifies the first independently coded picture from official ITU RExt conformance streams against its + /// published decoded-picture hashes. + /// + /// The official Annex B conformance stream. + /// The signaled component precision. + /// The signaled HEVC chroma-format identifier. + /// The normative luma-plane MD5 digest. + /// The normative blue-difference-plane MD5 digest, when present. + /// The normative red-difference-plane MD5 digest, when present. + [Theory] + [InlineData(TestImages.Heif.General8BitMonochrome, 8, 0, "e5223be3da805fb96440dbf2bd170db0", null, null)] + [InlineData(TestImages.Heif.General8Bit420, 8, 1, "7d66d87736d627193acef745b3b7d014", "cecabae4dd685151d8de966ad01f01a8", "d069d15c457867a0a4fb9c0201eb0585")] + [InlineData(TestImages.Heif.General8Bit444, 8, 3, "2be0bad2e95b42a9f53138cd874db1c9", "f7d50f66757b468f438df47b7e6b4f36", "72d935e42f5e76aa04e0326c64f026dc")] + [InlineData(TestImages.Heif.General10Bit420, 10, 1, "9262fdf6a69587b8f1eed23c9026cb24", "cd81cc4b427565dc8c17761f2bd07c09", "0409bf573e03e2a6dd00b760b997a824")] + [InlineData(TestImages.Heif.General10Bit422, 10, 2, "4c0a0a1bf001ebf1dc440ccd9e0ae3ea", "bd35abc3f86ead4bd59e19403248ee5e", "8ce96a8885e10cda55e67eba25d9ec03")] + [InlineData(TestImages.Heif.General10Bit444, 10, 3, "d6293dfd466b7ed570beb56dee7823e3", "a82bf54ac3b2e996f40db77beff69b03", "d4f38dae50bbaa4087c7c1cf020d30a2")] + [InlineData(TestImages.Heif.General12BitMonochrome, 12, 0, "549ff2b94ede8d83bfdc64a34440817d", null, null)] + [InlineData(TestImages.Heif.General12Bit420, 12, 1, "346f709b5dfe5dd41f2ba1c70d072eb6", "6eee29326b96bb032a4a0ed822e4ba17", "59ef3982a4e0e9597d498a0d035a645a")] + [InlineData(TestImages.Heif.General12Bit422, 12, 2, "be9c8562410e42b2db985444bf8a448e", "c5d616f1ccf8b2e9f56e1bb1d3e23134", "676a9e1ad75cff3193fce58bdb2721cb")] + [InlineData(TestImages.Heif.General12Bit444, 12, 3, "057c9c3dd78c63b2689a159e21da1071", "4d0529c8e5755bb49d0ba0ec9a8e7e89", "9f5e9d559b0cf62440c2e05f141aa5d0")] + public void DecodeOfficialRangeExtensionsPictureMatchesPublishedDigest( + string path, + int bitDepth, + byte chromaFormat, + string lumaDigest, + string chromaBlueDigest, + string chromaRedDigest) + { + byte[] annexB = TestFile.Create(path).Bytes; + byte[] expectedYuv = TestFile.Create($"{path[..^4]}_frame0.yuv").Bytes; + ConvertAnnexBStillPicture(annexB, bitDepth, chromaFormat, out byte[] configurationData, out byte[] itemData); + HevcCodecConfiguration configuration = new(configurationData); + HevcImageItemBitstream bitstream = new(itemData, configuration); + using HevcPictureDecoder decoder = new(Configuration.Default, bitstream.SliceSegments[0].PictureParameterSet); + + decoder.Decode(bitstream); + + Assert.Equal(bitDepth, decoder.Picture.BitDepthLuma); + Assert.Equal(chromaFormat, decoder.Picture.ChromaFormat); + int expectedLength = decoder.Picture.GetWidth(HevcPlane.Y) * decoder.Picture.GetHeight(HevcPlane.Y) * (bitDepth > 8 ? 2 : 1); + if (chromaFormat != 0) + { + int chromaLength = decoder.Picture.GetWidth(HevcPlane.Cb) * decoder.Picture.GetHeight(HevcPlane.Cb) * (bitDepth > 8 ? 2 : 1); + expectedLength += chromaLength * 2; + } + + Assert.Equal(expectedLength, expectedYuv.Length); + int referenceOffset = 0; + AssertCodedPlaneEqual(decoder.Picture, HevcPlane.Y, expectedYuv, ref referenceOffset); + if (chromaFormat != 0) + { + AssertCodedPlaneEqual(decoder.Picture, HevcPlane.Cb, expectedYuv, ref referenceOffset); + AssertCodedPlaneEqual(decoder.Picture, HevcPlane.Cr, expectedYuv, ref referenceOffset); + } + + Assert.Equal(expectedYuv.Length, referenceOffset); + Assert.Equal(lumaDigest, GetPlaneDigest(decoder.Picture, HevcPlane.Y)); + if (chromaFormat != 0) + { + Assert.Equal(chromaBlueDigest, GetPlaneDigest(decoder.Picture, HevcPlane.Cb)); + Assert.Equal(chromaRedDigest, GetPlaneDigest(decoder.Picture, HevcPlane.Cr)); + } + } + /// /// Verifies all reconstructed samples from a real HEIC grid tile against the HM reference decoder. /// @@ -76,10 +143,49 @@ public class HevcPictureDecoderTests int sourceY = sequenceParameterSet.ConformanceWindowTopOffset >> subsamplingY; int width = GetDisplaySize(sequenceParameterSet.DisplayWidth, subsamplingX); int height = GetDisplaySize(sequenceParameterSet.DisplayHeight, subsamplingY); + AssertPlaneEqual(picture, plane, sourceX, sourceY, width, height, expected, ref offset); + } + + /// + /// Compares one complete coded component plane with its planar reference samples. + /// + /// The decoded picture containing the component plane. + /// The component plane to compare. + /// The complete planar YUV reference. + /// The current reference offset, advanced past the compared plane. + private static void AssertCodedPlaneEqual(HevcPictureBuffer picture, HevcPlane plane, ReadOnlySpan expected, ref int offset) + => AssertPlaneEqual(picture, plane, 0, 0, picture.GetWidth(plane), picture.GetHeight(plane), expected, ref offset); + + /// + /// Compares one rectangular component region with its planar reference samples. + /// + /// The decoded picture containing the component plane. + /// The component plane to compare. + /// The source-region X coordinate in component samples. + /// The source-region Y coordinate in component samples. + /// The compared width in component samples. + /// The compared height in component samples. + /// The complete planar YUV reference. + /// The current reference offset, advanced past the compared plane. + private static void AssertPlaneEqual( + HevcPictureBuffer picture, + HevcPlane plane, + int sourceX, + int sourceY, + int width, + int height, + ReadOnlySpan expected, + ref int offset) + { + bool usesHighBitDepthSamples = picture.GetBitDepth(plane) > 8; int mismatchCount = 0; int maximumDifference = 0; int firstMismatchX = 0; int firstMismatchY = 0; + int minimumMismatchX = width; + int minimumMismatchY = height; + int maximumMismatchX = 0; + int maximumMismatchY = 0; ushort firstActual = 0; ushort firstExpected = 0; for (int y = 0; y < height; y++) @@ -87,7 +193,17 @@ public class HevcPictureDecoderTests Span actualRow = picture.GetRowSpan(plane, sourceY + y).Slice(sourceX, width); for (int x = 0; x < width; x++) { - ushort expectedSample = expected[offset++]; + ushort expectedSample; + if (usesHighBitDepthSamples) + { + expectedSample = BinaryPrimitives.ReadUInt16LittleEndian(expected[offset..]); + offset += 2; + } + else + { + expectedSample = expected[offset++]; + } + int difference = Math.Abs(actualRow[x] - expectedSample); if (difference == 0) { @@ -104,13 +220,18 @@ public class HevcPictureDecoderTests mismatchCount++; maximumDifference = Math.Max(maximumDifference, difference); + minimumMismatchX = Math.Min(minimumMismatchX, x); + minimumMismatchY = Math.Min(minimumMismatchY, y); + maximumMismatchX = Math.Max(maximumMismatchX, x); + maximumMismatchY = Math.Max(maximumMismatchY, y); } } Assert.True( mismatchCount == 0, $"{plane} contained {mismatchCount} differing samples. The maximum difference was {maximumDifference}; " + - $"the first mismatch at ({firstMismatchX}, {firstMismatchY}) was {firstActual}, expected {firstExpected}."); + $"the mismatches span ({minimumMismatchX}, {minimumMismatchY}) through ({maximumMismatchX}, {maximumMismatchY}), " + + $"and the first mismatch at ({firstMismatchX}, {firstMismatchY}) was {firstActual}, expected {firstExpected}."); } /// @@ -120,4 +241,280 @@ public class HevcPictureDecoderTests /// The component subsampling shift. /// The displayed component extent. private static int GetDisplaySize(int lumaSize, int subsampling) => (lumaSize + (1 << subsampling) - 1) >> subsampling; + + /// + /// Adapts the first independently coded Annex B picture to the bounded hvc1 item contract used by the + /// production decoder. + /// + /// The complete official conformance stream. + /// The stream's published component precision. + /// The stream's published chroma-format identifier. + /// The generated item-local HEVC decoder configuration. + /// The generated length-delimited payload containing only the first picture. + private static void ConvertAnnexBStillPicture( + ReadOnlySpan annexB, + int bitDepth, + byte chromaFormat, + out byte[] configurationData, + out byte[] itemData) + { + const int VideoParameterSetNalUnitType = 32; + const int SequenceParameterSetNalUnitType = 33; + const int PictureParameterSetNalUnitType = 34; + const int HighestVideoCodingLayerNalUnitType = 31; + const int NalUnitHeaderLength = 2; + const int SpsConfigurationPrefixLength = 13; + const int ProfileTierLevelLength = 12; + const int ConfigurationHeaderLength = 23; + const int ParameterSetArrayHeaderLength = 5; + const int ParameterSetCount = 3; + const int ItemNalUnitLengthFieldLength = 4; + + (int Offset, int Length) videoParameterSet = default; + (int Offset, int Length) sequenceParameterSet = default; + (int Offset, int Length) pictureParameterSet = default; + List<(int Offset, int Length)> pictureNalUnits = []; + int offset = 0; + bool foundPicture = false; + while (TryReadAnnexBNalUnit(annexB, ref offset, out int nalOffset, out int nalLength)) + { + // HEVC stores nal_unit_type in the six bits following forbidden_zero_bit. + int nalUnitType = (annexB[nalOffset] >> 1) & 0x3F; + if (!foundPicture) + { + switch (nalUnitType) + { + case VideoParameterSetNalUnitType: + videoParameterSet = (nalOffset, nalLength); + break; + case SequenceParameterSetNalUnitType: + sequenceParameterSet = (nalOffset, nalLength); + break; + case PictureParameterSetNalUnitType: + pictureParameterSet = (nalOffset, nalLength); + break; + } + } + + if (nalUnitType > HighestVideoCodingLayerNalUnitType) + { + continue; + } + + // The first RBSP bit after the two-byte NAL header is first_slice_segment_in_pic_flag. No emulation byte + // can precede that first bit, so it can terminate the extracted picture without parsing later sequences. + bool firstSliceSegment = (annexB[nalOffset + NalUnitHeaderLength] & 0x80) != 0; + if (foundPicture && firstSliceSegment) + { + break; + } + + foundPicture = true; + pictureNalUnits.Add((nalOffset, nalLength)); + } + + Assert.True(videoParameterSet.Length > 0, "The conformance stream does not contain a VPS before its first picture."); + Assert.True(sequenceParameterSet.Length > 0, "The conformance stream does not contain an SPS before its first picture."); + Assert.True(pictureParameterSet.Length > 0, "The conformance stream does not contain a PPS before its first picture."); + Assert.NotEmpty(pictureNalUnits); + + ReadOnlySpan sps = annexB.Slice(sequenceParameterSet.Offset, sequenceParameterSet.Length); + Span spsRbspPrefix = stackalloc byte[SpsConfigurationPrefixLength]; + CopyRbspPrefix(sps[NalUnitHeaderLength..], spsRbspPrefix); + + int configurationLength = ConfigurationHeaderLength + + (ParameterSetCount * ParameterSetArrayHeaderLength) + + videoParameterSet.Length + + sequenceParameterSet.Length + + pictureParameterSet.Length; + + configurationData = new byte[configurationLength]; + + // ISO/IEC 14496-15 defines a fixed 23-byte HEVCDecoderConfigurationRecord header. Copying + // profile_tier_level directly from the published SPS avoids synthesizing codec capability claims. + configurationData[0] = 1; // configurationVersion + spsRbspPrefix.Slice(1, ProfileTierLevelLength).CopyTo(configurationData.AsSpan(1, ProfileTierLevelLength)); + configurationData[13] = 0xF0; // reserved and min_spatial_segmentation_idc = 0 + configurationData[15] = 0xFC; // reserved and parallelismType = 0 + configurationData[16] = (byte)(0xFC | chromaFormat); // reserved and chromaFormat + configurationData[17] = (byte)(0xF8 | (bitDepth - 8)); // reserved and bitDepthLumaMinus8 + configurationData[18] = (byte)(0xF8 | (chromaFormat == 0 ? 0 : bitDepth - 8)); // reserved and bitDepthChromaMinus8 + + int maxSubLayers = ((spsRbspPrefix[0] >> 1) & 7) + 1; + int temporalIdNesting = spsRbspPrefix[0] & 1; + configurationData[21] = (byte)((maxSubLayers << 3) | (temporalIdNesting << 2) | 3); // lengthSizeMinusOne = 3 + configurationData[22] = ParameterSetCount; + int configurationOffset = ConfigurationHeaderLength; + WriteParameterSetArray(configurationData, ref configurationOffset, VideoParameterSetNalUnitType, annexB.Slice(videoParameterSet.Offset, videoParameterSet.Length)); + WriteParameterSetArray(configurationData, ref configurationOffset, SequenceParameterSetNalUnitType, sps); + WriteParameterSetArray(configurationData, ref configurationOffset, PictureParameterSetNalUnitType, annexB.Slice(pictureParameterSet.Offset, pictureParameterSet.Length)); + Assert.Equal(configurationData.Length, configurationOffset); + + int itemLength = 0; + foreach ((int _, int nalLength) in pictureNalUnits) + { + itemLength += ItemNalUnitLengthFieldLength + nalLength; + } + + itemData = new byte[itemLength]; + int itemOffset = 0; + foreach ((int nalOffset, int nalLength) in pictureNalUnits) + { + BinaryPrimitives.WriteUInt32BigEndian(itemData.AsSpan(itemOffset), (uint)nalLength); + itemOffset += ItemNalUnitLengthFieldLength; + annexB.Slice(nalOffset, nalLength).CopyTo(itemData.AsSpan(itemOffset)); + itemOffset += nalLength; + } + } + + /// + /// Reads the next NAL-unit payload from an Annex B byte stream. + /// + /// The complete Annex B byte stream. + /// The current search offset, advanced to the next start code. + /// The returned NAL-unit payload offset. + /// The returned NAL-unit payload length. + /// when another complete NAL unit was found. + private static bool TryReadAnnexBNalUnit(ReadOnlySpan source, ref int offset, out int nalOffset, out int nalLength) + { + int startCodeOffset = FindAnnexBStartCode(source, offset, out int startCodeLength); + if (startCodeOffset < 0) + { + nalOffset = 0; + nalLength = 0; + return false; + } + + nalOffset = startCodeOffset + startCodeLength; + int nextStartCodeOffset = FindAnnexBStartCode(source, nalOffset, out _); + int nalEnd = nextStartCodeOffset < 0 ? source.Length : nextStartCodeOffset; + + // Annex B permits trailing_zero_8bits between a NAL unit and the next start-code prefix. They are byte-stream + // framing and must not enter the length-delimited item payload. + while (nalEnd > nalOffset && source[nalEnd - 1] == 0) + { + nalEnd--; + } + + offset = nextStartCodeOffset < 0 ? source.Length : nextStartCodeOffset; + nalLength = nalEnd - nalOffset; + return nalLength >= 2; + } + + /// + /// Locates the next three- or four-byte Annex B start code. + /// + /// The complete Annex B byte stream. + /// The first byte to inspect. + /// The returned start-code length. + /// The start-code offset, or negative one when no code remains. + private static int FindAnnexBStartCode(ReadOnlySpan source, int offset, out int length) + { + for (int index = offset; index <= source.Length - 3; index++) + { + if (source[index] != 0 || source[index + 1] != 0) + { + continue; + } + + if (source[index + 2] == 1) + { + length = 3; + return index; + } + + if (index <= source.Length - 4 && source[index + 2] == 0 && source[index + 3] == 1) + { + length = 4; + return index; + } + } + + length = 0; + return -1; + } + + /// + /// Copies the fixed SPS prefix through general_level_idc while removing emulation-prevention bytes. + /// + /// The SPS bytes following the NAL-unit header. + /// The fixed 13-byte SPS prefix destination. + private static void CopyRbspPrefix(ReadOnlySpan escapedRbsp, Span destination) + { + const int EscapeZeroCount = 2; + const byte EmulationPreventionByte = 3; + + int sourceOffset = 0; + int destinationOffset = 0; + int consecutiveZeroes = 0; + while (destinationOffset < destination.Length) + { + byte value = escapedRbsp[sourceOffset++]; + if (consecutiveZeroes == EscapeZeroCount && value == EmulationPreventionByte) + { + consecutiveZeroes = 0; + continue; + } + + destination[destinationOffset++] = value; + consecutiveZeroes = value == 0 ? consecutiveZeroes + 1 : 0; + } + } + + /// + /// Writes one complete parameter-set array to an HEVC decoder-configuration record. + /// + /// The complete configuration destination. + /// The current destination offset, advanced past the array. + /// The parameter-set NAL-unit type. + /// The complete NAL unit without Annex B framing. + private static void WriteParameterSetArray(Span configuration, ref int offset, byte nalUnitType, ReadOnlySpan nalUnit) + { + // Each complete array contains exactly one parameter set from the source stream. ISO/IEC 14496-15 stores + // array_completeness in the high bit and the six-bit HEVC NAL-unit type in the low bits. + configuration[offset++] = (byte)(0x80 | nalUnitType); + BinaryPrimitives.WriteUInt16BigEndian(configuration[offset..], 1); + offset += 2; + BinaryPrimitives.WriteUInt16BigEndian(configuration[offset..], (ushort)nalUnit.Length); + offset += 2; + nalUnit.CopyTo(configuration[offset..]); + offset += nalUnit.Length; + } + + /// + /// Calculates the HEVC decoded-picture MD5 digest for one reconstructed component plane. + /// + /// The reconstructed picture. + /// The component plane to hash. + /// The lowercase hexadecimal decoded-picture digest. + private static string GetPlaneDigest(HevcPictureBuffer picture, HevcPlane plane) + { + int width = picture.GetWidth(plane); + int height = picture.GetHeight(plane); + int bytesPerSample = picture.GetBitDepth(plane) > 8 ? 2 : 1; + byte[] rowBytes = new byte[width * bytesPerSample]; + using IncrementalHash hash = IncrementalHash.CreateHash(HashAlgorithmName.MD5); + for (int y = 0; y < height; y++) + { + Span samples = picture.GetRowSpan(plane, y)[..width]; + if (bytesPerSample == 1) + { + for (int x = 0; x < width; x++) + { + rowBytes[x] = (byte)samples[x]; + } + } + else + { + for (int x = 0; x < width; x++) + { + BinaryPrimitives.WriteUInt16LittleEndian(rowBytes.AsSpan(x * 2), samples[x]); + } + } + + hash.AppendData(rowBytes); + } + + return Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant(); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 78e4029f1..f47200038 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1294,6 +1294,16 @@ public static class TestImages public const string DwsampleTileHvcConfiguration = "Heif/Hevc/dwsample-tile.hvcc"; public const string DwsampleTilePayload = "Heif/Hevc/dwsample-tile.hvc1"; public const string DwsampleTileReferenceYuv = "Heif/Hevc/dwsample-tile-hm.yuv"; + public const string General8BitMonochrome = "Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1.bit"; + public const string General8Bit420 = "Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1.bit"; + public const string General8Bit444 = "Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2.bit"; + public const string General10Bit420 = "Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1.bit"; + public const string General10Bit422 = "Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1.bit"; + public const string General10Bit444 = "Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2.bit"; + public const string General12BitMonochrome = "Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1.bit"; + public const string General12Bit420 = "Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1.bit"; + public const string General12Bit422 = "Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1.bit"; + public const string General12Bit444 = "Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2.bit"; public const string Image1 = "Heif/image1.heic"; public const string Image2 = "Heif/image2.heic"; public const string Image3 = "Heif/image3.heic"; diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1.bit new file mode 100644 index 000000000..180d8704c --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de0f153bde98066c6d7368a51e0907cc9a829b34a6b998672999ce0b283cb212 +size 30436 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..0658c18a4 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_420_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9b784954f44b512caef67c967c873ed469643f0fe03b62b67fa00abc8c74fa3 +size 460800 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1.bit new file mode 100644 index 000000000..8caa27c30 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6c012634574009542d4046b24a2c29fbabaa7351a62b19300df5eec3336e810 +size 71890 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..54665f629 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_422_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:210797850418d6544948c375f65d34e8a58000d52a868c24088701691f6f2573 +size 614400 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2.bit new file mode 100644 index 000000000..5becd373e --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab9c7d7c0554e6342836fa0212e6334ecc894141f1cb90cd4512c108dabcb936 +size 510412 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2_frame0.yuv new file mode 100644 index 000000000..d00cde11b --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_10b_444_RExt_Sony_2_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e2af46b75d97462b7fd8ab67238705747fe74571efcb564ff13468c42ae1158 +size 921600 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1.bit new file mode 100644 index 000000000..f00d1b802 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42ba06081165645c28b1f38ca1addd62eaf48925bb64e3439b404f32a2b50952 +size 38232 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..40387f55d --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_400_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01880b5616174d54d5030ce8726374d00242158bba7b3b312141eb48be2625e4 +size 307200 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1.bit new file mode 100644 index 000000000..ce3120197 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b1f3e683bc1c6b6abcaa26fe540d6ec26a3734255010b27ae650b44b52654e9 +size 57176 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..1c0c6f70c --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_420_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c394390723d177e5421eac9428efcb2ead2d94af967aa1937572b689095ac850 +size 460800 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1.bit new file mode 100644 index 000000000..62b683a49 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3f534275c0c3d317a3dafc30a3e0eb89d3e7d6aa2f2c0df0e9d27712354700d +size 139893 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..b54899758 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_422_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b5930a097a0acd815b1b6575d7cbbf9c9921b49cb3cfb12f46026174fc6a173 +size 614400 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2.bit new file mode 100644 index 000000000..d47c45005 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197023ccc20fd340c108646fd06b08b92fc29a613609714f6983e15d3770d669 +size 1185165 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2_frame0.yuv new file mode 100644 index 000000000..84ab716c1 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_12b_444_RExt_Sony_2_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:751dd2deb76632024b523289d945e3b3fb32d27d1eac22bf1de0b8c2ff9ebbc9 +size 921600 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1.bit new file mode 100644 index 000000000..775b131c5 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6151ab4b8fdc2a8643d169f98d9878e8221f9d23d97c71447bd4d7de5e79bd4b +size 10349 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..6272d9842 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_400_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5ca4e944cc21944edbf1507e2a1a178eb878b31f0f05eeb65a1d3a81aae3dbf +size 153600 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1.bit new file mode 100644 index 000000000..a7eb7b2fc --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44b43ba732bae1b05c217661f7ea9049a52288bd5e4a966252b44406122b6ea4 +size 14092 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1_frame0.yuv new file mode 100644 index 000000000..2822f7f52 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_420_RExt_Sony_1_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3503c56768f9de92a7dd6debbd959ed4636ea1c9e35b767d5f881dee42a572a6 +size 230400 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2.bit b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2.bit new file mode 100644 index 000000000..85c7483ce --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef4e0d887ad0a02e1f8218e3fbf93258c1b1e3923d079296df8df69fac56e538 +size 258456 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2_frame0.yuv b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2_frame0.yuv new file mode 100644 index 000000000..5424e1a83 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/GENERAL_8b_444_RExt_Sony_2_frame0.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6974f93431618d623c8e9168ea259b5280e37dd22fd059eb5b2e25318de61ff7 +size 460800 diff --git a/tests/Images/Input/Heif/Hevc/Conformance/README.md b/tests/Images/Input/Heif/Hevc/Conformance/README.md new file mode 100644 index 000000000..098a490c3 --- /dev/null +++ b/tests/Images/Input/Heif/Hevc/Conformance/README.md @@ -0,0 +1,9 @@ +# HEVC Range Extensions conformance fixtures + +These fixtures come from the electronic attachment to [ITU-T H.265.1 (10/2018)](https://www.itu.int/rec/T-REC-H.265.1-201810-I/en), the conformance specification aligned with ISO/IEC 23008-8. Table 4 identifies the Sony `GENERAL_*_RExt` streams and their required profiles. + +The `.bit` files are the original Annex B conformance streams. Each `_frame0.yuv` file contains the first decoded picture extracted byte-for-byte from the corresponding published planar reference output; it was not generated by ImageSharp. + +`HevcPictureDecoderTests.DecodeOfficialRangeExtensionsPictureMatchesPublishedDigest` limits each stream to its first independently coded picture, adapts that picture to the bounded `hvc1` item contract, compares every reconstructed sample with `_frame0.yuv`, and verifies the published decoded-picture MD5 value for every present plane. + +The retained matrix covers the ImageSharp 8, 10, and 12-bit still-image precision boundary across monochrome, 4:2:0, 4:2:2, and 4:4:4. Tool-specific H.265.1 streams remain necessary before every exposed Range Extensions tool can be marked complete.