From 939718daac3eb56ae6a33c7b4e1b921f85796261 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 05:55:17 +1000 Subject: [PATCH] Parse HEVC sequence parameter sets --- HEIF_IMPLEMENTATION_PLAN.md | 1 + .../Heif/Hevc/HevcCodecConfiguration.cs | 40 ++ .../Heif/Hevc/HevcParameterSetSyntax.cs | 123 +++++ .../Formats/Heif/Hevc/HevcScalingList.cs | 250 ++++++++++ .../Heif/Hevc/HevcSequenceParameterSet.cs | 464 ++++++++++++++++++ .../Hevc/HevcShortTermReferencePictureSet.cs | 178 +++++++ .../Heif/Hevc/HevcVideoParameterSet.cs | 97 +--- .../Hevc/HevcVideoUsabilityInformation.cs | 247 ++++++++++ 8 files changed, 1304 insertions(+), 96 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcSequenceParameterSet.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcVideoUsabilityInformation.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 991674172..b1716d2c4 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -114,6 +114,7 @@ This snapshot pins or classifies the available references and failures; it does | `HevcCodecConfiguration`, `HevcNalUnitArray`, and `HeifItem.HevcCodecConfiguration` | ISO/IEC 14496-15 `HEVCDecoderConfigurationRecord`, HEVC NAL-unit header syntax, and HEVC image-item configuration requirements | HM `source/Lib/TLibDecoder/NALread.cpp` and parameter-header paths at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` v1.6.0 decoder NAL and parameter-header paths at `c83a76b084498d55f252f48b2e3786804cdf24b7`; independently produced HEIC fixtures for `hvcC` record layout | Associate exactly one `hvcC` property with each `hvc1` image item; validate the fixed profile, level, chroma, bit-depth, reserved, length-size, array, and NAL-header fields within the property boundary; and retain only the image description plus bounded configuration NAL units required to decode that item. Consume but do not retain average-frame-rate or temporal-layer presentation fields. Do not add visual sample entries, sample descriptions, tracks, timing, access-unit timelines, or a generic decoder-configuration model. BSD and Apache-2.0 notice preservation and separate HEVC patent/release clearance remain final integration gates. | | `HevcNalUnit`, `HevcNalUnitHeader`, `HevcRbspDecoder`, and `HevcBitReader` | HEVC sections 7.3.1.1 and 7.3.1.2 NAL-unit and header syntax, section 7.3.2.11 RBSP trailing bits, and section 9.1 Exp-Golomb parsing | HM `source/Lib/TLibDecoder/NALread.cpp` and `SyntaxElementParser.cpp` at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` NAL extraction and bitstream paths at `c83a76b084498d55f252f48b2e3786804cdf24b7` | Validate the fixed NAL header, remove only legal emulation-prevention bytes, and provide bounded fixed-width, flag, unsigned/signed Exp-Golomb, and RBSP-trailing-bit reads. Configuration arrays immediately use the shared parser. The primitives consume only NAL units delivered by a supported HEIF image item or its `hvcC` property and introduce no Annex B API, access-unit model, timeline, track, sample table, or generic ISO BMFF surface. | | `HevcProfileTierLevel`, `HevcVideoParameterSet`, and `HevcCodecConfiguration.VideoParameterSets` | HEVC sections 7.3.2.1 video parameter sets, 7.3.3 profile-tier-level syntax, and E.2.2 hypothetical-reference-decoder syntax; ISO/IEC 14496-15 `HEVCDecoderConfigurationRecord` matching semantics | HM `source/Lib/TLibDecoder/TDecCAVLC.cpp` functions `parseVPS`, `parsePTL`, `parseProfileTier`, and `parseHrdParameters` at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` `decoder/ihevcd_parse_headers.c` profile-tier-level and VPS paths at `c83a76b084498d55f252f48b2e3786804cdf24b7` | Retain the base-layer VPS identifier, temporal-sublayer count, nesting flag, and exact general profile/tier/constraint/level fields. Validate profile, tier, compatibility, level, temporal count, and nesting against `hvcC`, while tolerating the independently observed legacy-muxer practice of zeroing some or all profile-specific constraint bits in the configuration record; SPS validation remains authoritative for chroma and bit depth. Consume but do not retain sublayer ordering, timing, and HRD syntax. Reject multilayer and layer-set state because the supported contract is one independently coded image item; add no layer-selection, access-unit, DPB, scheduling, track, or sample model. | +| `HevcSequenceParameterSet`, `HevcVideoUsabilityInformation`, `HevcScalingList`, `HevcShortTermReferencePictureSet`, and `HevcParameterSetSyntax` | HEVC sections 7.3.2.2 sequence parameter sets, 7.3.4 scaling-list data, 7.3.7 short-term reference-picture sets, E.2.1 VUI syntax, and Range Extensions SPS syntax | HM `source/Lib/TLibDecoder/TDecCAVLC.cpp` functions `parseSPS`, `parseScalingList`, `xDecodeScalingList`, `parseShortTermRefPicSet`, `parseVUI`, and `parseHrdParameters`, plus `source/Lib/TLibCommon/TComRom.cpp` default matrices and diagonal scans, at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` `decoder/ihevcd_parse_headers.c` SPS, VUI, scaling-list, and reference-set paths at `c83a76b084498d55f252f48b2e3786804cdf24b7` | Retain coded/display dimensions, conformance crop, monochrome/4:2:0/4:2:2/4:4:4 and separate-plane layout, 8-through-16-bit precision, coding/transform/PCM geometry, effective scaling matrices, compression-tool flags, bounded reference-set declarations, Range Extensions tools, and still-image VUI aspect/color/range/chroma-location/display-window fields. Consume timing, HRD, ordering, and bitstream-restriction syntax without scheduling state. Reject interlaced fields and multilayer extensions because they do not represent the one progressive independently coded image-item contract; add no DPB pictures, playback state, tracks, or sample tables. | | `HeifContentColorVolume`, `HeifItem.ContentColorVolume`, and `HeifDecoderCore` content color-volume parsing and presentation | HEIF content color-volume item property; AVIF 1.2 content color-volume requirements; ITU-T H.274 (V4) content colour volume syntax and semantics | libavif `src/read.c` function `avifSkipContentColourVolume` at `092276ce89098ead06db80975173191e5fee1826`; official ITU-T H.274 (V4), January 2026 | Decode only the bounded per-image `cclv` property: require zero cancellation, persistence, and reserved bits; preserve optional signed G/B/R primary coordinates and normalized minimum, maximum, and average luminance values; and validate their registered ranges and ordering. Expose the effective grid-or-tile still-image value through `HeifMetadata`. Do not add SEI persistence, retained video state, tracks, samples, timing, or a generic ISO BMFF color-volume box model. | | `HeifAmbientViewingEnvironment`, `HeifReferenceViewingEnvironment`, `HeifNominalDiffuseWhite`, and their per-item presentation metadata | ISOBMFF ambient viewing environment; ITU-T H.274 (V4) section 8.13; HEIF Amendment 1 sections 6.5.44 and 6.5.45; AVIF 1.2 image-item box requirements | libavif `src/read.c` functions `avifSkipAmbientViewingEnvironment`, `avifSkipReferenceViewingEnvironment`, and `avifSkipNominalDiffuseWhite` at `092276ce89098ead06db80975173191e5fee1826`; official ITU-T H.274 (V4), January 2026 | Decode the fixed-size `amve`, version-zero `reve`, and version-zero `ndwt` properties only when associated with the presented still-image item. Preserve physical illuminance and luminance units, distinct surround/periphery chromaticities, and the coded zero that requests the standard nominal diffuse-white default. Validate registered coordinate ranges and duplicate associations. Retain no video-SEI persistence, visual sample entry, display pipeline, track, timing, or generic viewing-environment box model. | | `GridHeifItemDecoder` and `HeifDecoderCore` grid/thumbnail selection | ISO/IEC 23008-12 section 6.6.2.3 image-grid syntax and MIAF grid-cell constraints | libavif `src/read.c` functions `avifParseImageGridBox`, `avifDecoderDataAllocateImagePlanes`, and `avifDecoderDataCopyTileToImage` at `092276ce89098ead06db80975173191e5fee1826` | Parse version-zero 16-bit and 32-bit grid descriptors, preserve row-major `dimg` order, require the declared tile count and one coding format, validate canvas coverage and edge overlap, and crop only the rightmost column and bottom row while copying through ImageSharp pixel buffers. A primary grid whose tile codec is unavailable may use only a decodable thumbnail that explicitly references that grid. | diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs index 90497202c..eb0d258ff 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs @@ -164,6 +164,41 @@ internal sealed class HevcCodecConfiguration } this.VideoParameterSets = videoParameterSets; + + List sequenceParameterSets = new(); + foreach (HevcNalUnitArray nalUnitArray in this.nalUnitArrays) + { + const byte sequenceParameterSetNalUnitType = 33; + if (nalUnitArray.NalUnitType != sequenceParameterSetNalUnitType) + { + continue; + } + + foreach (HevcNalUnit nalUnit in nalUnitArray.NalUnits) + { + HevcSequenceParameterSet sequenceParameterSet = new(nalUnit); + bool referencesKnownVideoParameterSet = false; + foreach (HevcVideoParameterSet videoParameterSet in videoParameterSets) + { + referencesKnownVideoParameterSet |= videoParameterSet.Id == sequenceParameterSet.VideoParameterSetId; + } + + if (!referencesKnownVideoParameterSet + || !sequenceParameterSet.ProfileTierLevel.Matches(this) + || sequenceParameterSet.ChromaFormat != this.ChromaFormat + || sequenceParameterSet.BitDepthLuma != this.BitDepthLuma + || sequenceParameterSet.BitDepthChroma != this.BitDepthChroma + || (temporalLayerCount != 0 && sequenceParameterSet.MaxSubLayers != temporalLayerCount) + || (temporalLayerCount != 0 && sequenceParameterSet.TemporalIdNestingFlag != temporalIdNested)) + { + throw new InvalidImageContentException("The HEVC sequence parameter set does not match its codec configuration."); + } + + sequenceParameterSets.Add(sequenceParameterSet); + } + } + + this.SequenceParameterSets = sequenceParameterSets; } /// @@ -237,6 +272,11 @@ internal sealed class HevcCodecConfiguration /// public IReadOnlyList VideoParameterSets { get; } + /// + /// Gets the validated sequence parameter sets carried by the codec-configuration property. + /// + public IReadOnlyList SequenceParameterSets { get; } + /// /// Validates the associated pixel-information property against the coded luma and chroma sample precisions. /// diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs new file mode 100644 index 000000000..d47d2c3c8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Provides shared bounded syntax operations used by HEVC parameter-set readers. +/// +internal static class HevcParameterSetSyntax +{ + /// + /// Gets the horizontal conformance-window unit for an HEVC chroma layout. + /// + /// The chroma-format identifier. + /// Whether 4:4:4 components are coded as separate color planes. + /// The horizontal unit in luma samples. + public static int GetCropUnitWidth(byte chromaFormat, bool separateColorPlane) + => !separateColorPlane && chromaFormat is 1 or 2 ? 2 : 1; + + /// + /// Gets the vertical conformance-window unit for an HEVC chroma layout. + /// + /// The chroma-format identifier. + /// Whether 4:4:4 components are coded as separate color planes. + /// The vertical unit in luma samples. + public static int GetCropUnitHeight(byte chromaFormat, bool separateColorPlane) + => !separateColorPlane && chromaFormat == 1 ? 2 : 1; + + /// + /// Consumes hypothetical-reference-decoder syntax without adding playback state to the still-image model. + /// + /// The parameter-set raw byte sequence payload reader. + /// Whether common HRD flags are coded for this parameter set. + /// The highest declared temporal sublayer index. + /// The effective NAL HRD presence flag. + /// The effective VCL HRD presence flag. + /// The effective sub-picture HRD presence flag. + /// The HRD syntax is truncated or exceeds its registered bounds. + public static void SkipHrdParameters( + ref HevcBitReader reader, + bool commonInformationPresent, + int maxSubLayersMinusOne, + ref bool nalHrdParametersPresent, + ref bool vclHrdParametersPresent, + ref bool subPictureHrdParametersPresent) + { + if (commonInformationPresent) + { + nalHrdParametersPresent = reader.ReadFlag(); + vclHrdParametersPresent = reader.ReadFlag(); + subPictureHrdParametersPresent = false; + if (nalHrdParametersPresent || vclHrdParametersPresent) + { + subPictureHrdParametersPresent = reader.ReadFlag(); + if (subPictureHrdParametersPresent) + { + reader.ReadBits(8); + reader.ReadBits(5); + reader.ReadFlag(); + reader.ReadBits(5); + } + + reader.ReadBits(4); + reader.ReadBits(4); + if (subPictureHrdParametersPresent) + { + reader.ReadBits(4); + } + + reader.ReadBits(5); + reader.ReadBits(5); + reader.ReadBits(5); + } + } + + for (int subLayer = 0; subLayer <= maxSubLayersMinusOne; subLayer++) + { + bool fixedPictureRateGeneral = reader.ReadFlag(); + bool fixedPictureRateWithinCvs = fixedPictureRateGeneral || reader.ReadFlag(); + bool lowDelayHrd = false; + if (fixedPictureRateWithinCvs) + { + reader.ReadUnsignedExpGolomb(); + } + else + { + lowDelayHrd = reader.ReadFlag(); + } + + uint cpbCountMinusOne = 0; + if (!lowDelayHrd) + { + cpbCountMinusOne = reader.ReadUnsignedExpGolomb(); + if (cpbCountMinusOne > 31) + { + throw new InvalidImageContentException("The HEVC HRD syntax declares too many coded-picture buffers."); + } + } + + for (int hrdKind = 0; hrdKind < 2; hrdKind++) + { + bool parametersPresent = hrdKind == 0 ? nalHrdParametersPresent : vclHrdParametersPresent; + if (!parametersPresent) + { + continue; + } + + for (uint cpbIndex = 0; cpbIndex <= cpbCountMinusOne; cpbIndex++) + { + reader.ReadUnsignedExpGolomb(); + reader.ReadUnsignedExpGolomb(); + if (subPictureHrdParametersPresent) + { + reader.ReadUnsignedExpGolomb(); + reader.ReadUnsignedExpGolomb(); + } + + reader.ReadFlag(); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs new file mode 100644 index 000000000..e5e8d5061 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs @@ -0,0 +1,250 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Contains the HEVC quantization scaling matrices and their large-transform DC coefficients. +/// +internal sealed class HevcScalingList +{ + /// + /// The number of matrix identifiers defined for each transform-size category. + /// + private const int MatrixCount = 6; + + /// + /// The flat default matrix used by four-by-four transforms. + /// + private static readonly byte[] Default4x4 = + [ + 16, 16, 16, 16, + 16, 16, 16, 16, + 16, 16, 16, 16, + 16, 16, 16, 16, + ]; + + /// + /// The default intra-predicted matrix used by transforms of eight-by-eight and larger. + /// + private static readonly byte[] DefaultIntra8x8 = + [ + 16, 16, 16, 16, 17, 18, 21, 24, + 16, 16, 16, 16, 17, 19, 22, 25, + 16, 16, 17, 18, 20, 22, 25, 29, + 16, 16, 18, 21, 24, 27, 31, 36, + 17, 17, 20, 24, 30, 35, 41, 47, + 18, 19, 22, 27, 35, 44, 54, 65, + 21, 22, 25, 31, 41, 54, 70, 88, + 24, 25, 29, 36, 47, 65, 88, 115, + ]; + + /// + /// The default inter-predicted matrix used by transforms of eight-by-eight and larger. + /// + private static readonly byte[] DefaultInter8x8 = + [ + 16, 16, 16, 16, 17, 18, 20, 24, + 16, 16, 16, 17, 18, 20, 24, 25, + 16, 16, 17, 18, 20, 24, 25, 28, + 16, 17, 18, 20, 24, 25, 28, 33, + 17, 18, 20, 24, 25, 28, 33, 41, + 18, 20, 24, 25, 28, 33, 41, 54, + 20, 24, 25, 28, 33, 41, 54, 71, + 24, 25, 28, 33, 41, 54, 71, 91, + ]; + + /// + /// The diagonal coefficient order for four-by-four matrices. + /// + private static readonly byte[] DiagonalScan4x4 = CreateDiagonalScan(4); + + /// + /// The diagonal coefficient order for matrices of eight-by-eight and larger. + /// + private static readonly byte[] DiagonalScan8x8 = CreateDiagonalScan(8); + + /// + /// The decoded matrices, indexed by transform-size category and matrix identifier. + /// + private readonly byte[][] matrices = new byte[4 * MatrixCount][]; + + /// + /// The DC coefficients for sixteen-by-sixteen and thirty-two-by-thirty-two matrices. + /// + private readonly byte[] dcCoefficients = new byte[4 * MatrixCount]; + + /// + /// Initializes a new instance of the class with the normative default matrices. + /// + public HevcScalingList() + { + for (int sizeId = 0; sizeId < 4; sizeId++) + { + int coefficientCount = sizeId == 0 ? 16 : 64; + for (int matrixId = 0; matrixId < MatrixCount; matrixId++) + { + byte[] matrix = new byte[coefficientCount]; + ReadOnlySpan source = sizeId == 0 + ? Default4x4 + : matrixId < 3 ? DefaultIntra8x8 : DefaultInter8x8; + + source.CopyTo(matrix); + this.matrices[GetIndex(sizeId, matrixId)] = matrix; + this.dcCoefficients[GetIndex(sizeId, matrixId)] = 16; + } + } + } + + /// + /// Reads a complete scaling-list-data structure. + /// + /// The parameter-set raw byte sequence payload reader. + /// The decoded scaling matrices. + /// A prediction reference is outside its permitted matrix set. + public static HevcScalingList Parse(ref HevcBitReader reader) + { + HevcScalingList scalingList = new(); + for (int sizeId = 0; sizeId < 4; sizeId++) + { + int matrixStep = sizeId == 3 ? 3 : 1; + for (int matrixId = 0; matrixId < MatrixCount; matrixId += matrixStep) + { + int destinationIndex = GetIndex(sizeId, matrixId); + bool predictionMode = reader.ReadFlag(); + if (!predictionMode) + { + uint matrixIdDelta = reader.ReadUnsignedExpGolomb(); + if (sizeId == 3) + { + if (matrixIdDelta > matrixId / 3) + { + throw new InvalidImageContentException("The HEVC scaling list references an unavailable matrix."); + } + + matrixIdDelta *= 3; + } + + if (matrixIdDelta > matrixId) + { + throw new InvalidImageContentException("The HEVC scaling list references an unavailable matrix."); + } + + int referenceMatrixId = matrixId - (int)matrixIdDelta; + if (referenceMatrixId != matrixId) + { + int referenceIndex = GetIndex(sizeId, referenceMatrixId); + + // Span copying uses ImageSharp's runtime-optimized memory path and preserves one scalar + // behavior model for these small, infrequently parsed coefficient tables. + scalingList.matrices[referenceIndex].CopyTo(scalingList.matrices[destinationIndex], 0); + scalingList.dcCoefficients[destinationIndex] = scalingList.dcCoefficients[referenceIndex]; + } + + continue; + } + + int nextCoefficient = 8; + if (sizeId > 1) + { + nextCoefficient = (int)(((long)reader.ReadSignedExpGolomb() + 8) & 255); + scalingList.dcCoefficients[destinationIndex] = (byte)(nextCoefficient & 255); + } + + byte[] scan = sizeId == 0 ? DiagonalScan4x4 : DiagonalScan8x8; + byte[] matrix = scalingList.matrices[destinationIndex]; + for (int coefficient = 0; coefficient < matrix.Length; coefficient++) + { + nextCoefficient = (int)(((long)nextCoefficient + reader.ReadSignedExpGolomb()) & 255); + matrix[scan[coefficient]] = (byte)nextCoefficient; + } + } + + if (sizeId == 3) + { + // HEVC signals only luma matrices at 32x32. Chroma uses the corresponding 16x16 matrices. + for (int matrixId = 0; matrixId < MatrixCount; matrixId++) + { + if (matrixId is 0 or 3) + { + continue; + } + + int destinationIndex = GetIndex(sizeId, matrixId); + int referenceIndex = GetIndex(sizeId - 1, matrixId); + scalingList.matrices[referenceIndex].CopyTo(scalingList.matrices[destinationIndex], 0); + scalingList.dcCoefficients[destinationIndex] = scalingList.dcCoefficients[referenceIndex]; + } + } + } + + return scalingList; + } + + /// + /// Gets a decoded scaling matrix. + /// + /// The transform-size category from zero for 4x4 through three for 32x32. + /// The prediction and color-component matrix identifier. + /// The 16 or 64 decoded scaling coefficients in raster order. + public ReadOnlySpan GetMatrix(int sizeId, int matrixId) + { + DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId)); + DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId)); + return this.matrices[GetIndex(sizeId, matrixId)]; + } + + /// + /// Gets the DC scaling coefficient for a large transform matrix. + /// + /// The transform-size category. + /// The prediction and color-component matrix identifier. + /// The decoded DC coefficient. + public byte GetDcCoefficient(int sizeId, int matrixId) + { + DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId)); + DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId)); + return this.dcCoefficients[GetIndex(sizeId, matrixId)]; + } + + /// + /// Gets the flattened storage index for a size and matrix identifier. + /// + /// The transform-size category. + /// The matrix identifier. + /// The flattened storage index. + private static int GetIndex(int sizeId, int matrixId) => (sizeId * MatrixCount) + matrixId; + + /// + /// Creates the HEVC up-right diagonal scan for a square coefficient block. + /// + /// The coefficient block width and height. + /// The raster indices in coded order. + private static byte[] CreateDiagonalScan(int size) + { + byte[] scan = new byte[size * size]; + int row = 0; + int column = 0; + for (int position = 0; position < scan.Length; position++) + { + scan[position] = (byte)((row * size) + column); + if (column == size - 1 || row == 0) + { + row += column + 1; + column = 0; + if (row >= size) + { + column += row - (size - 1); + row = size - 1; + } + } + else + { + column++; + row--; + } + } + + return scan; + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSequenceParameterSet.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSequenceParameterSet.cs new file mode 100644 index 000000000..3d385d8a9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcSequenceParameterSet.cs @@ -0,0 +1,464 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Contains the HEVC sequence fields required to reconstruct one independently coded still image. +/// +internal sealed class HevcSequenceParameterSet +{ + /// + /// Initializes a new instance of the class. + /// + /// The decoded sequence-parameter-set NAL unit. + /// The sequence parameter set is malformed or outside the still-image profile. + public HevcSequenceParameterSet(HevcNalUnit nalUnit) + { + const byte sequenceParameterSetNalUnitType = 33; + if (nalUnit.Header.NalUnitType != sequenceParameterSetNalUnitType + || nalUnit.Header.LayerId != 0 + || nalUnit.Header.TemporalId != 0) + { + throw new InvalidImageContentException("The HEVC sequence parameter set has an invalid NAL-unit header."); + } + + HevcBitReader reader = new(nalUnit.Rbsp.Span); + this.VideoParameterSetId = (byte)reader.ReadBits(4); + int maxSubLayersMinusOne = (int)reader.ReadBits(3); + if (maxSubLayersMinusOne > 6) + { + throw new InvalidImageContentException("The HEVC sequence parameter set declares too many temporal sublayers."); + } + + this.MaxSubLayers = maxSubLayersMinusOne + 1; + this.TemporalIdNestingFlag = reader.ReadFlag(); + if (maxSubLayersMinusOne == 0 && !this.TemporalIdNestingFlag) + { + throw new InvalidImageContentException("The HEVC sequence parameter set has invalid temporal nesting."); + } + + this.ProfileTierLevel = new HevcProfileTierLevel(ref reader, maxSubLayersMinusOne); + uint sequenceParameterSetId = reader.ReadUnsignedExpGolomb(); + if (sequenceParameterSetId > 15) + { + throw new InvalidImageContentException("The HEVC sequence parameter set identifier is invalid."); + } + + this.Id = (byte)sequenceParameterSetId; + uint chromaFormat = reader.ReadUnsignedExpGolomb(); + if (chromaFormat > 3) + { + throw new InvalidImageContentException("The HEVC sequence parameter set has an invalid chroma format."); + } + + this.ChromaFormat = (byte)chromaFormat; + this.SeparateColorPlaneFlag = this.ChromaFormat == 3 && reader.ReadFlag(); + + uint width = reader.ReadUnsignedExpGolomb(); + uint height = reader.ReadUnsignedExpGolomb(); + if (width is 0 or > int.MaxValue || height is 0 or > int.MaxValue) + { + throw new InvalidImageContentException("The HEVC sequence parameter set has invalid coded dimensions."); + } + + this.Width = (int)width; + this.Height = (int)height; + if (reader.ReadFlag()) + { + int cropUnitWidth = HevcParameterSetSyntax.GetCropUnitWidth(this.ChromaFormat, this.SeparateColorPlaneFlag); + int cropUnitHeight = HevcParameterSetSyntax.GetCropUnitHeight(this.ChromaFormat, this.SeparateColorPlaneFlag); + this.ConformanceWindowLeftOffset = ReadScaledOffset(ref reader, cropUnitWidth); + this.ConformanceWindowRightOffset = ReadScaledOffset(ref reader, cropUnitWidth); + this.ConformanceWindowTopOffset = ReadScaledOffset(ref reader, cropUnitHeight); + this.ConformanceWindowBottomOffset = ReadScaledOffset(ref reader, cropUnitHeight); + } + + if ((long)this.ConformanceWindowLeftOffset + this.ConformanceWindowRightOffset >= this.Width + || (long)this.ConformanceWindowTopOffset + this.ConformanceWindowBottomOffset >= this.Height) + { + throw new InvalidImageContentException("The HEVC sequence parameter set has an invalid conformance window."); + } + + this.DisplayWidth = this.Width - this.ConformanceWindowLeftOffset - this.ConformanceWindowRightOffset; + this.DisplayHeight = this.Height - this.ConformanceWindowTopOffset - this.ConformanceWindowBottomOffset; + + this.BitDepthLuma = ReadBitDepth(ref reader); + this.BitDepthChroma = ReadBitDepth(ref reader); + uint log2MaxPictureOrderCountLsbMinusFour = reader.ReadUnsignedExpGolomb(); + if (log2MaxPictureOrderCountLsbMinusFour > 12) + { + throw new InvalidImageContentException("The HEVC picture-order-count width is invalid."); + } + + this.PictureOrderCountLsbBits = (int)log2MaxPictureOrderCountLsbMinusFour + 4; + + bool subLayerOrderingInfoPresent = reader.ReadFlag(); + int firstOrderingSubLayer = subLayerOrderingInfoPresent ? 0 : maxSubLayersMinusOne; + for (int subLayer = firstOrderingSubLayer; subLayer <= maxSubLayersMinusOne; subLayer++) + { + uint maxDecodedPictureBufferingMinusOne = reader.ReadUnsignedExpGolomb(); + uint maxNumReorderPictures = reader.ReadUnsignedExpGolomb(); + reader.ReadUnsignedExpGolomb(); + if (maxNumReorderPictures > maxDecodedPictureBufferingMinusOne) + { + throw new InvalidImageContentException("The HEVC sequence parameter set has invalid sublayer ordering limits."); + } + } + + uint minCodingBlockLog2MinusThree = reader.ReadUnsignedExpGolomb(); + if (minCodingBlockLog2MinusThree > 3) + { + throw new InvalidImageContentException("The HEVC minimum coding-block size is invalid."); + } + + this.MinCodingBlockLog2 = (int)minCodingBlockLog2MinusThree + 3; + uint codingBlockSizeDifference = reader.ReadUnsignedExpGolomb(); + if (codingBlockSizeDifference > 6 - this.MinCodingBlockLog2) + { + throw new InvalidImageContentException("The HEVC coding-tree-block size is invalid."); + } + + this.CodingTreeBlockLog2 = this.MinCodingBlockLog2 + (int)codingBlockSizeDifference; + + uint minTransformBlockLog2MinusTwo = reader.ReadUnsignedExpGolomb(); + if (minTransformBlockLog2MinusTwo > this.MinCodingBlockLog2 - 3) + { + throw new InvalidImageContentException("The HEVC minimum transform-block size is invalid."); + } + + this.MinTransformBlockLog2 = (int)minTransformBlockLog2MinusTwo + 2; + uint transformBlockSizeDifference = reader.ReadUnsignedExpGolomb(); + int maximumTransformBlockLog2 = Math.Min(5, this.CodingTreeBlockLog2); + if (transformBlockSizeDifference > maximumTransformBlockLog2 - this.MinTransformBlockLog2) + { + throw new InvalidImageContentException("The HEVC maximum transform-block size is invalid."); + } + + this.MaxTransformBlockLog2 = this.MinTransformBlockLog2 + (int)transformBlockSizeDifference; + uint maxTransformHierarchyDepthInter = reader.ReadUnsignedExpGolomb(); + uint maxTransformHierarchyDepthIntra = reader.ReadUnsignedExpGolomb(); + uint maxHierarchyDepth = (uint)(this.CodingTreeBlockLog2 - this.MinTransformBlockLog2); + if (maxTransformHierarchyDepthInter > maxHierarchyDepth || maxTransformHierarchyDepthIntra > maxHierarchyDepth) + { + throw new InvalidImageContentException("The HEVC transform hierarchy depth is invalid."); + } + + this.MaxTransformHierarchyDepthInter = (int)maxTransformHierarchyDepthInter + 1; + this.MaxTransformHierarchyDepthIntra = (int)maxTransformHierarchyDepthIntra + 1; + + this.ScalingListEnabled = reader.ReadFlag(); + this.ScalingList = new HevcScalingList(); + if (this.ScalingListEnabled && reader.ReadFlag()) + { + this.ScalingList = HevcScalingList.Parse(ref reader); + } + + this.AsymmetricMotionPartitionsEnabled = reader.ReadFlag(); + this.SampleAdaptiveOffsetEnabled = reader.ReadFlag(); + this.PcmEnabled = reader.ReadFlag(); + if (this.PcmEnabled) + { + this.PcmBitDepthLuma = (int)reader.ReadBits(4) + 1; + this.PcmBitDepthChroma = (int)reader.ReadBits(4) + 1; + if (this.PcmBitDepthLuma > this.BitDepthLuma || this.PcmBitDepthChroma > this.BitDepthChroma) + { + throw new InvalidImageContentException("The HEVC PCM bit depth exceeds the coded sample precision."); + } + + uint minPcmCodingBlockLog2MinusThree = reader.ReadUnsignedExpGolomb(); + this.MinPcmCodingBlockLog2 = (int)minPcmCodingBlockLog2MinusThree + 3; + int maximumPcmCodingBlockLog2 = Math.Min(this.CodingTreeBlockLog2, 5); + if (this.MinPcmCodingBlockLog2 < Math.Min(this.MinCodingBlockLog2, 5) + || this.MinPcmCodingBlockLog2 > maximumPcmCodingBlockLog2) + { + throw new InvalidImageContentException("The HEVC minimum PCM coding-block size is invalid."); + } + + uint pcmCodingBlockSizeDifference = reader.ReadUnsignedExpGolomb(); + if (pcmCodingBlockSizeDifference > maximumPcmCodingBlockLog2 - this.MinPcmCodingBlockLog2) + { + throw new InvalidImageContentException("The HEVC maximum PCM coding-block size is invalid."); + } + + this.MaxPcmCodingBlockLog2 = this.MinPcmCodingBlockLog2 + (int)pcmCodingBlockSizeDifference; + this.PcmLoopFilterDisabled = reader.ReadFlag(); + } + + uint shortTermReferencePictureSetCount = reader.ReadUnsignedExpGolomb(); + if (shortTermReferencePictureSetCount > 64) + { + throw new InvalidImageContentException("The HEVC sequence parameter set declares too many short-term reference-picture sets."); + } + + List shortTermReferencePictureSets = new((int)shortTermReferencePictureSetCount); + for (int referenceSet = 0; referenceSet < shortTermReferencePictureSetCount; referenceSet++) + { + shortTermReferencePictureSets.Add( + HevcShortTermReferencePictureSet.Parse(ref reader, shortTermReferencePictureSets, referenceSet)); + } + + this.ShortTermReferencePictureSets = shortTermReferencePictureSets; + + if (reader.ReadFlag()) + { + uint longTermReferencePictureCount = reader.ReadUnsignedExpGolomb(); + if (longTermReferencePictureCount > 32) + { + throw new InvalidImageContentException("The HEVC sequence parameter set declares too many long-term reference pictures."); + } + + uint[] pictureOrderCounts = new uint[longTermReferencePictureCount]; + bool[] usedByCurrentPicture = new bool[longTermReferencePictureCount]; + for (int reference = 0; reference < pictureOrderCounts.Length; reference++) + { + pictureOrderCounts[reference] = reader.ReadBits(this.PictureOrderCountLsbBits); + usedByCurrentPicture[reference] = reader.ReadFlag(); + } + + this.LongTermReferencePictureOrderCounts = pictureOrderCounts; + this.LongTermReferencePicturesUsedByCurrent = usedByCurrentPicture; + } + else + { + this.LongTermReferencePictureOrderCounts = Array.Empty(); + this.LongTermReferencePicturesUsedByCurrent = Array.Empty(); + } + + this.TemporalMotionVectorPredictionEnabled = reader.ReadFlag(); + this.StrongIntraSmoothingEnabled = reader.ReadFlag(); + if (reader.ReadFlag()) + { + this.VideoUsabilityInformation = new HevcVideoUsabilityInformation( + ref reader, + this.ChromaFormat, + this.SeparateColorPlaneFlag, + maxSubLayersMinusOne); + } + + if (reader.ReadFlag()) + { + Span extensionFlags = stackalloc bool[8]; + for (int extensionFlag = 0; extensionFlag < extensionFlags.Length; extensionFlag++) + { + extensionFlags[extensionFlag] = reader.ReadFlag(); + } + + if (extensionFlags[1]) + { + throw new InvalidImageContentException("Layered HEVC sequence extensions are not supported for still-image items."); + } + + if (extensionFlags[0]) + { + this.TransformSkipRotationEnabled = reader.ReadFlag(); + this.TransformSkipContextEnabled = reader.ReadFlag(); + this.ImplicitResidualDpcmEnabled = reader.ReadFlag(); + this.ExplicitResidualDpcmEnabled = reader.ReadFlag(); + this.ExtendedPrecisionProcessingEnabled = reader.ReadFlag(); + this.IntraSmoothingDisabled = reader.ReadFlag(); + this.HighPrecisionOffsetsEnabled = reader.ReadFlag(); + this.PersistentRiceAdaptationEnabled = reader.ReadFlag(); + this.CabacBypassAlignmentEnabled = reader.ReadFlag(); + } + + bool unknownExtensionPresent = false; + for (int extensionFlag = 2; extensionFlag < extensionFlags.Length; extensionFlag++) + { + unknownExtensionPresent |= extensionFlags[extensionFlag]; + } + + if (unknownExtensionPresent) + { + while (reader.HasMoreRbspData()) + { + reader.ReadFlag(); + } + } + } + + reader.ReadRbspTrailingBits(); + } + + /// Gets the referenced video-parameter-set identifier. + public byte VideoParameterSetId { get; } + + /// Gets the sequence-parameter-set identifier. + public byte Id { get; } + + /// Gets the declared number of temporal sublayers. + public int MaxSubLayers { get; } + + /// Gets a value indicating whether temporal identifiers are nested. + public bool TemporalIdNestingFlag { get; } + + /// Gets the general profile, tier, constraint, and level description. + public HevcProfileTierLevel ProfileTierLevel { get; } + + /// Gets the coded chroma format, from monochrome through YUV 4:4:4. + public byte ChromaFormat { get; } + + /// Gets a value indicating whether 4:4:4 components are coded as separate color planes. + public bool SeparateColorPlaneFlag { get; } + + /// Gets the coded luma width before conformance cropping. + public int Width { get; } + + /// Gets the coded luma height before conformance cropping. + public int Height { get; } + + /// Gets the displayed width after conformance cropping. + public int DisplayWidth { get; } + + /// Gets the displayed height after conformance cropping. + public int DisplayHeight { get; } + + /// Gets the conformance-window left offset in luma samples. + public int ConformanceWindowLeftOffset { get; } + + /// Gets the conformance-window right offset in luma samples. + public int ConformanceWindowRightOffset { get; } + + /// Gets the conformance-window top offset in luma samples. + public int ConformanceWindowTopOffset { get; } + + /// Gets the conformance-window bottom offset in luma samples. + public int ConformanceWindowBottomOffset { get; } + + /// Gets the luma sample precision in bits. + public int BitDepthLuma { get; } + + /// Gets the chroma sample precision in bits. + public int BitDepthChroma { get; } + + /// Gets the coded picture-order-count least-significant-bit width. + public int PictureOrderCountLsbBits { get; } + + /// Gets the base-two logarithm of the minimum luma coding-block size. + public int MinCodingBlockLog2 { get; } + + /// Gets the base-two logarithm of the coding-tree-block size. + public int CodingTreeBlockLog2 { get; } + + /// Gets the base-two logarithm of the minimum luma transform-block size. + public int MinTransformBlockLog2 { get; } + + /// Gets the base-two logarithm of the maximum luma transform-block size. + public int MaxTransformBlockLog2 { get; } + + /// Gets the maximum inter-predicted transform hierarchy depth. + public int MaxTransformHierarchyDepthInter { get; } + + /// Gets the maximum intra-predicted transform hierarchy depth. + public int MaxTransformHierarchyDepthIntra { get; } + + /// Gets a value indicating whether scaling lists affect inverse quantization. + public bool ScalingListEnabled { get; } + + /// Gets the effective quantization scaling matrices. + public HevcScalingList ScalingList { get; } + + /// Gets a value indicating whether asymmetric motion partitions are enabled. + public bool AsymmetricMotionPartitionsEnabled { get; } + + /// Gets a value indicating whether sample-adaptive offset filtering is enabled. + public bool SampleAdaptiveOffsetEnabled { get; } + + /// Gets a value indicating whether pulse-code-modulated coding blocks are enabled. + public bool PcmEnabled { get; } + + /// Gets the PCM luma sample precision in bits. + public int PcmBitDepthLuma { get; } + + /// Gets the PCM chroma sample precision in bits. + public int PcmBitDepthChroma { get; } + + /// Gets the base-two logarithm of the minimum PCM coding-block size. + public int MinPcmCodingBlockLog2 { get; } + + /// Gets the base-two logarithm of the maximum PCM coding-block size. + public int MaxPcmCodingBlockLog2 { get; } + + /// Gets a value indicating whether in-loop filtering is disabled for PCM blocks. + public bool PcmLoopFilterDisabled { get; } + + /// Gets the SPS short-term reference-picture sets. + public IReadOnlyList ShortTermReferencePictureSets { get; } + + /// Gets the long-term reference picture-order-count values. + public IReadOnlyList LongTermReferencePictureOrderCounts { get; } + + /// Gets the long-term reference-picture current-usage flags. + public IReadOnlyList LongTermReferencePicturesUsedByCurrent { get; } + + /// Gets a value indicating whether temporal motion-vector prediction is enabled. + public bool TemporalMotionVectorPredictionEnabled { get; } + + /// Gets a value indicating whether strong intra smoothing is enabled. + public bool StrongIntraSmoothingEnabled { get; } + + /// Gets the optional still-image VUI presentation description. + public HevcVideoUsabilityInformation? VideoUsabilityInformation { get; } + + /// Gets a value indicating whether transform-skip coefficient rotation is enabled. + public bool TransformSkipRotationEnabled { get; } + + /// Gets a value indicating whether transform-skip-specific entropy contexts are enabled. + public bool TransformSkipContextEnabled { get; } + + /// Gets a value indicating whether implicit residual DPCM is enabled. + public bool ImplicitResidualDpcmEnabled { get; } + + /// Gets a value indicating whether explicit residual DPCM is enabled. + public bool ExplicitResidualDpcmEnabled { get; } + + /// Gets a value indicating whether extended-precision processing is enabled. + public bool ExtendedPrecisionProcessingEnabled { get; } + + /// Gets a value indicating whether intra smoothing is disabled. + public bool IntraSmoothingDisabled { get; } + + /// Gets a value indicating whether high-precision prediction offsets are enabled. + public bool HighPrecisionOffsetsEnabled { get; } + + /// Gets a value indicating whether persistent Rice adaptation is enabled. + public bool PersistentRiceAdaptationEnabled { get; } + + /// Gets a value indicating whether CABAC bypass alignment is enabled. + public bool CabacBypassAlignmentEnabled { get; } + + /// + /// Reads a conformance-window offset and converts it to luma-sample units. + /// + /// The sequence-parameter-set raw byte sequence payload reader. + /// The chroma-dependent luma-sample unit. + /// The scaled offset. + /// The scaled offset exceeds the supported image dimension range. + private static int ReadScaledOffset(ref HevcBitReader reader, int unit) + { + uint offset = reader.ReadUnsignedExpGolomb(); + if (offset > int.MaxValue / unit) + { + throw new InvalidImageContentException("The HEVC conformance-window offset is too large."); + } + + return (int)offset * unit; + } + + /// + /// Reads and validates a coded HEVC sample precision. + /// + /// The sequence-parameter-set raw byte sequence payload reader. + /// The sample precision in bits. + /// The declared precision exceeds 16 bits. + private static int ReadBitDepth(ref HevcBitReader reader) + { + uint bitDepthMinusEight = reader.ReadUnsignedExpGolomb(); + if (bitDepthMinusEight > 8) + { + throw new InvalidImageContentException("The HEVC sample bit depth is invalid."); + } + + return (int)bitDepthMinusEight + 8; + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs new file mode 100644 index 000000000..ee8005a30 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Contains the bounded picture-order differences declared by one HEVC short-term reference-picture set. +/// +internal sealed class HevcShortTermReferencePictureSet +{ + /// + /// Initializes a new instance of the class. + /// + /// The signed picture-order differences in HEVC reference order. + /// The corresponding current-picture usage flags. + private HevcShortTermReferencePictureSet(int[] deltaPictureOrders, bool[] usedByCurrentPicture) + { + this.DeltaPictureOrders = deltaPictureOrders; + this.UsedByCurrentPicture = usedByCurrentPicture; + } + + /// + /// Gets the signed picture-order differences in HEVC reference order. + /// + public IReadOnlyList DeltaPictureOrders { get; } + + /// + /// Gets the flags indicating which reference pictures are used by the current picture. + /// + public IReadOnlyList UsedByCurrentPicture { get; } + + /// + /// Gets the number of pictures declared by the reference-picture set. + /// + public int Count => this.DeltaPictureOrders.Count; + + /// + /// Reads one SPS short-term reference-picture set. + /// + /// The sequence-parameter-set raw byte sequence payload reader. + /// The previously decoded sets available for inter-set prediction. + /// The zero-based index of the set being decoded. + /// The decoded reference-picture set. + /// The set exceeds the HEVC decoded-picture-buffer bound. + public static HevcShortTermReferencePictureSet Parse( + ref HevcBitReader reader, + IReadOnlyList previousSets, + int index) + { + Span deltaPictureOrders = stackalloc int[16]; + Span usedByCurrentPicture = stackalloc bool[16]; + int pictureCount = 0; + bool interSetPrediction = index > 0 && reader.ReadFlag(); + if (interSetPrediction) + { + HevcShortTermReferencePictureSet referenceSet = previousSets[index - 1]; + bool deltaPictureOrderSign = reader.ReadFlag(); + uint absoluteDeltaPictureOrderMinusOne = reader.ReadUnsignedExpGolomb(); + if (absoluteDeltaPictureOrderMinusOne >= int.MaxValue) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large."); + } + + int deltaReferencePictureSet = (deltaPictureOrderSign ? -1 : 1) + * ((int)absoluteDeltaPictureOrderMinusOne + 1); + + for (int referenceIndex = 0; referenceIndex <= referenceSet.Count; referenceIndex++) + { + bool used = reader.ReadFlag(); + bool useDelta = used || reader.ReadFlag(); + if (!useDelta) + { + continue; + } + + if (pictureCount == deltaPictureOrders.Length) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture set is too large."); + } + + int referenceDelta = referenceIndex < referenceSet.Count + ? referenceSet.DeltaPictureOrders[referenceIndex] + : 0; + + long deltaPictureOrder = (long)deltaReferencePictureSet + referenceDelta; + if (deltaPictureOrder is < int.MinValue or > int.MaxValue) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large."); + } + + deltaPictureOrders[pictureCount] = (int)deltaPictureOrder; + usedByCurrentPicture[pictureCount] = used; + pictureCount++; + } + + // HEVC orders negative differences nearest-first, followed by positive differences nearest-first. + for (int outer = 1; outer < pictureCount; outer++) + { + int delta = deltaPictureOrders[outer]; + bool used = usedByCurrentPicture[outer]; + int inner = outer - 1; + while (inner >= 0 && delta < deltaPictureOrders[inner]) + { + deltaPictureOrders[inner + 1] = deltaPictureOrders[inner]; + usedByCurrentPicture[inner + 1] = usedByCurrentPicture[inner]; + inner--; + } + + deltaPictureOrders[inner + 1] = delta; + usedByCurrentPicture[inner + 1] = used; + } + + int negativeCount = 0; + while (negativeCount < pictureCount && deltaPictureOrders[negativeCount] < 0) + { + negativeCount++; + } + + deltaPictureOrders[..negativeCount].Reverse(); + usedByCurrentPicture[..negativeCount].Reverse(); + } + else + { + uint negativePictureCount = reader.ReadUnsignedExpGolomb(); + uint positivePictureCount = reader.ReadUnsignedExpGolomb(); + if (negativePictureCount > 16 || positivePictureCount > 16 - negativePictureCount) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture set is too large."); + } + + int previousDelta = 0; + for (uint negativeIndex = 0; negativeIndex < negativePictureCount; negativeIndex++) + { + uint deltaMinusOne = reader.ReadUnsignedExpGolomb(); + if (deltaMinusOne >= int.MaxValue) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large."); + } + + long deltaPictureOrder = (long)previousDelta - deltaMinusOne - 1; + if (deltaPictureOrder < int.MinValue) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large."); + } + + previousDelta = (int)deltaPictureOrder; + deltaPictureOrders[pictureCount] = previousDelta; + usedByCurrentPicture[pictureCount] = reader.ReadFlag(); + pictureCount++; + } + + previousDelta = 0; + for (uint positiveIndex = 0; positiveIndex < positivePictureCount; positiveIndex++) + { + uint deltaMinusOne = reader.ReadUnsignedExpGolomb(); + if (deltaMinusOne >= int.MaxValue) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large."); + } + + long deltaPictureOrder = (long)previousDelta + deltaMinusOne + 1; + if (deltaPictureOrder > int.MaxValue) + { + throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large."); + } + + previousDelta = (int)deltaPictureOrder; + deltaPictureOrders[pictureCount] = previousDelta; + usedByCurrentPicture[pictureCount] = reader.ReadFlag(); + pictureCount++; + } + } + + return new HevcShortTermReferencePictureSet( + deltaPictureOrders[..pictureCount].ToArray(), + usedByCurrentPicture[..pictureCount].ToArray()); + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcVideoParameterSet.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcVideoParameterSet.cs index 92ed599a9..f3e20e9fc 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcVideoParameterSet.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcVideoParameterSet.cs @@ -113,7 +113,7 @@ internal sealed class HevcVideoParameterSet } bool commonInformationPresent = hrdIndex == 0 || reader.ReadFlag(); - SkipHrdParameters( + HevcParameterSetSyntax.SkipHrdParameters( ref reader, commonInformationPresent, maxSubLayersMinusOne, @@ -153,99 +153,4 @@ internal sealed class HevcVideoParameterSet /// Gets the general profile, tier, constraint, and level description. /// public HevcProfileTierLevel ProfileTierLevel { get; } - - /// - /// Consumes hypothetical-reference-decoder syntax without adding playback state to the still-image model. - /// - /// The video-parameter-set raw byte sequence payload reader. - /// Whether common HRD flags are coded for this parameter set. - /// The highest declared temporal sublayer index. - /// The effective NAL HRD presence flag. - /// The effective VCL HRD presence flag. - /// The effective sub-picture HRD presence flag. - /// The HRD syntax is truncated or exceeds its registered bounds. - private static void SkipHrdParameters( - ref HevcBitReader reader, - bool commonInformationPresent, - int maxSubLayersMinusOne, - ref bool nalHrdParametersPresent, - ref bool vclHrdParametersPresent, - ref bool subPictureHrdParametersPresent) - { - if (commonInformationPresent) - { - nalHrdParametersPresent = reader.ReadFlag(); - vclHrdParametersPresent = reader.ReadFlag(); - subPictureHrdParametersPresent = false; - if (nalHrdParametersPresent || vclHrdParametersPresent) - { - subPictureHrdParametersPresent = reader.ReadFlag(); - if (subPictureHrdParametersPresent) - { - reader.ReadBits(8); - reader.ReadBits(5); - reader.ReadFlag(); - reader.ReadBits(5); - } - - reader.ReadBits(4); - reader.ReadBits(4); - if (subPictureHrdParametersPresent) - { - reader.ReadBits(4); - } - - reader.ReadBits(5); - reader.ReadBits(5); - reader.ReadBits(5); - } - } - - for (int subLayer = 0; subLayer <= maxSubLayersMinusOne; subLayer++) - { - bool fixedPictureRateGeneral = reader.ReadFlag(); - bool fixedPictureRateWithinCvs = fixedPictureRateGeneral || reader.ReadFlag(); - bool lowDelayHrd = false; - if (fixedPictureRateWithinCvs) - { - reader.ReadUnsignedExpGolomb(); - } - else - { - lowDelayHrd = reader.ReadFlag(); - } - - uint cpbCountMinusOne = 0; - if (!lowDelayHrd) - { - cpbCountMinusOne = reader.ReadUnsignedExpGolomb(); - if (cpbCountMinusOne > 31) - { - throw new InvalidImageContentException("The HEVC HRD syntax declares too many coded-picture buffers."); - } - } - - for (int hrdKind = 0; hrdKind < 2; hrdKind++) - { - bool parametersPresent = hrdKind == 0 ? nalHrdParametersPresent : vclHrdParametersPresent; - if (!parametersPresent) - { - continue; - } - - for (uint cpbIndex = 0; cpbIndex <= cpbCountMinusOne; cpbIndex++) - { - reader.ReadUnsignedExpGolomb(); - reader.ReadUnsignedExpGolomb(); - if (subPictureHrdParametersPresent) - { - reader.ReadUnsignedExpGolomb(); - reader.ReadUnsignedExpGolomb(); - } - - reader.ReadFlag(); - } - } - } - } } diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcVideoUsabilityInformation.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcVideoUsabilityInformation.cs new file mode 100644 index 000000000..02d63277f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcVideoUsabilityInformation.cs @@ -0,0 +1,247 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Contains the still-image presentation fields declared by HEVC video-usability information. +/// +internal sealed class HevcVideoUsabilityInformation +{ + /// + /// Initializes a new instance of the class. + /// + /// The sequence-parameter-set raw byte sequence payload reader. + /// The sequence chroma-format identifier. + /// Whether 4:4:4 components are coded as separate planes. + /// The highest declared temporal sublayer index. + /// The VUI syntax is invalid for a still-image item. + public HevcVideoUsabilityInformation( + ref HevcBitReader reader, + byte chromaFormat, + bool separateColorPlane, + int maxSubLayersMinusOne) + { + this.AspectRatioInfoPresent = reader.ReadFlag(); + if (this.AspectRatioInfoPresent) + { + this.AspectRatioIdc = (byte)reader.ReadBits(8); + if (this.AspectRatioIdc == byte.MaxValue) + { + this.SarWidth = (ushort)reader.ReadBits(16); + this.SarHeight = (ushort)reader.ReadBits(16); + if (this.SarWidth == 0 || this.SarHeight == 0) + { + throw new InvalidImageContentException("The HEVC VUI declares an invalid extended sample aspect ratio."); + } + } + else if (this.AspectRatioIdc > 16) + { + throw new InvalidImageContentException("The HEVC VUI declares a reserved sample aspect ratio."); + } + } + + if (reader.ReadFlag()) + { + reader.ReadFlag(); + } + + this.VideoSignalTypePresent = reader.ReadFlag(); + if (this.VideoSignalTypePresent) + { + reader.ReadBits(3); + this.FullRange = reader.ReadFlag(); + this.ColorDescriptionPresent = reader.ReadFlag(); + if (this.ColorDescriptionPresent) + { + this.ColorPrimaries = (byte)reader.ReadBits(8); + this.TransferCharacteristics = (byte)reader.ReadBits(8); + this.MatrixCoefficients = (byte)reader.ReadBits(8); + } + } + + this.ChromaLocationInfoPresent = reader.ReadFlag(); + if (this.ChromaLocationInfoPresent) + { + uint topFieldLocation = reader.ReadUnsignedExpGolomb(); + uint bottomFieldLocation = reader.ReadUnsignedExpGolomb(); + if (topFieldLocation > 5 || bottomFieldLocation > 5) + { + throw new InvalidImageContentException("The HEVC VUI declares an invalid chroma sample location."); + } + + this.ChromaSampleLocationTopField = (byte)topFieldLocation; + this.ChromaSampleLocationBottomField = (byte)bottomFieldLocation; + } + + reader.ReadFlag(); + if (reader.ReadFlag()) + { + // A field sequence requires paired-field presentation state and is not a single HEIF image item. + throw new InvalidImageContentException("Interlaced HEVC field sequences are not supported as still-image items."); + } + + reader.ReadFlag(); + + this.DefaultDisplayWindowPresent = reader.ReadFlag(); + if (this.DefaultDisplayWindowPresent) + { + int cropUnitWidth = HevcParameterSetSyntax.GetCropUnitWidth(chromaFormat, separateColorPlane); + int cropUnitHeight = HevcParameterSetSyntax.GetCropUnitHeight(chromaFormat, separateColorPlane); + this.DefaultDisplayWindowLeftOffset = ReadScaledOffset(ref reader, cropUnitWidth); + this.DefaultDisplayWindowRightOffset = ReadScaledOffset(ref reader, cropUnitWidth); + this.DefaultDisplayWindowTopOffset = ReadScaledOffset(ref reader, cropUnitHeight); + this.DefaultDisplayWindowBottomOffset = ReadScaledOffset(ref reader, cropUnitHeight); + } + + if (reader.ReadFlag()) + { + // VUI timing and HRD fields affect scheduling, not the reconstructed still-image samples. + reader.ReadBits(32); + reader.ReadBits(32); + if (reader.ReadFlag()) + { + reader.ReadUnsignedExpGolomb(); + } + + if (reader.ReadFlag()) + { + bool nalHrdParametersPresent = false; + bool vclHrdParametersPresent = false; + bool subPictureHrdParametersPresent = false; + HevcParameterSetSyntax.SkipHrdParameters( + ref reader, + true, + maxSubLayersMinusOne, + ref nalHrdParametersPresent, + ref vclHrdParametersPresent, + ref subPictureHrdParametersPresent); + } + } + + if (reader.ReadFlag()) + { + reader.ReadFlag(); + reader.ReadFlag(); + reader.ReadFlag(); + uint minimumSpatialSegmentation = reader.ReadUnsignedExpGolomb(); + if (minimumSpatialSegmentation >= 4096) + { + throw new InvalidImageContentException("The HEVC VUI spatial-segmentation value is invalid."); + } + + reader.ReadUnsignedExpGolomb(); + reader.ReadUnsignedExpGolomb(); + reader.ReadUnsignedExpGolomb(); + reader.ReadUnsignedExpGolomb(); + } + } + + /// + /// Gets a value indicating whether sample-aspect-ratio information is present. + /// + public bool AspectRatioInfoPresent { get; } + + /// + /// Gets the registered sample-aspect-ratio identifier. + /// + public byte AspectRatioIdc { get; } + + /// + /// Gets the explicit horizontal sample spacing when is 255. + /// + public ushort SarWidth { get; } + + /// + /// Gets the explicit vertical sample spacing when is 255. + /// + public ushort SarHeight { get; } + + /// + /// Gets a value indicating whether video-signal-type information is present. + /// + public bool VideoSignalTypePresent { get; } + + /// + /// Gets a value indicating whether component samples use the full numeric range. + /// + public bool FullRange { get; } + + /// + /// Gets a value indicating whether color-description fields are present. + /// + public bool ColorDescriptionPresent { get; } + + /// + /// Gets the coded color-primary identifier. + /// + public byte ColorPrimaries { get; } + + /// + /// Gets the coded transfer-characteristic identifier. + /// + public byte TransferCharacteristics { get; } + + /// + /// Gets the coded matrix-coefficient identifier. + /// + public byte MatrixCoefficients { get; } + + /// + /// Gets a value indicating whether chroma sample-location information is present. + /// + public bool ChromaLocationInfoPresent { get; } + + /// + /// Gets the top-field chroma sample-location identifier. + /// + public byte ChromaSampleLocationTopField { get; } + + /// + /// Gets the bottom-field chroma sample-location identifier. + /// + public byte ChromaSampleLocationBottomField { get; } + + /// + /// Gets a value indicating whether a default display window is present. + /// + public bool DefaultDisplayWindowPresent { get; } + + /// + /// Gets the default display-window left offset in luma samples. + /// + public int DefaultDisplayWindowLeftOffset { get; } + + /// + /// Gets the default display-window right offset in luma samples. + /// + public int DefaultDisplayWindowRightOffset { get; } + + /// + /// Gets the default display-window top offset in luma samples. + /// + public int DefaultDisplayWindowTopOffset { get; } + + /// + /// Gets the default display-window bottom offset in luma samples. + /// + public int DefaultDisplayWindowBottomOffset { get; } + + /// + /// Reads a conformance-window offset and converts it to luma-sample units. + /// + /// The sequence-parameter-set raw byte sequence payload reader. + /// The chroma-dependent luma-sample unit. + /// The scaled offset. + /// The scaled offset exceeds the supported image dimension range. + private static int ReadScaledOffset(ref HevcBitReader reader, int unit) + { + uint offset = reader.ReadUnsignedExpGolomb(); + if (offset > int.MaxValue / unit) + { + throw new InvalidImageContentException("The HEVC VUI display-window offset is too large."); + } + + return (int)offset * unit; + } +}