From 6bb8e70ef87d90b2354c04a073beee989ce5c16f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 06:10:04 +1000 Subject: [PATCH] Parse HEVC still image slice headers --- HEIF_IMPLEMENTATION_PLAN.md | 5 +- .../Formats/Heif/Hevc/HevcBitReader.cs | 22 + .../Heif/Hevc/HevcImageItemBitstream.cs | 123 ++++++ .../Formats/Heif/Hevc/HevcNalUnit.cs | 29 +- .../Formats/Heif/Hevc/HevcNalUnitHeader.cs | 10 + .../Heif/Hevc/HevcParameterSetSyntax.cs | 63 +++ .../Heif/Hevc/HevcPictureParameterSet.cs | 59 +-- .../Heif/Hevc/HevcSliceSegmentHeader.cs | 389 ++++++++++++++++++ .../Formats/Heif/Hevc/HevcSliceType.cs | 25 ++ 9 files changed, 670 insertions(+), 55 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcImageItemBitstream.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcSliceType.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 367cbd32d..2e0d67f90 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -116,6 +116,7 @@ This snapshot pins or classifies the available references and failures; it does | `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. | | `HevcPictureParameterSet` and `HevcCodecConfiguration.PictureParameterSets` | HEVC sections 7.3.2.3 picture parameter sets, 7.3.4 scaling-list data, and Range Extensions PPS syntax | HM `source/Lib/TLibDecoder/TDecCAVLC.cpp` function `parsePPS`, `source/Lib/TLibDecoder/TDecConformance.cpp` PPS range checks, and `source/Lib/TLibCommon/TComPicSym.cpp` tile derivation at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` `decoder/ihevcd_parse_headers.c` PPS and tile paths at `c83a76b084498d55f252f48b2e3786804cdf24b7` | Link each PPS to a configuration-local SPS and retain the CABAC, quantization, transform-skip, scaling-list, tile/wavefront, deblocking, SAO, slice-header, and Range Extensions controls required to reconstruct the independently coded picture. Derive the complete CTB tile grid and validate every SPS-dependent depth, size, precision, and offset bound during parsing. Reject multilayer extensions and add no access-unit, reference-picture lifetime, playback, track, timing, or sample-table model. | +| `HevcImageItemBitstream`, `HevcSliceSegmentHeader`, and `HevcSliceType` | HEVC sections 7.3.1.2 NAL-unit syntax, 7.3.6 slice-segment headers, and 7.4.7 slice-segment-header semantics; HEVC image-item length delimiting | HM `source/Lib/TLibDecoder/TDecCAVLC.cpp` function `parseSliceHeader`, `source/Lib/TLibDecoder/NALread.cpp`, and slice byte-alignment and entry-point handling at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` `decoder/ihevcd_nal.c`, `ihevcd_parse_slice_header.c`, and `ihevcd_parse_headers.c` at `c83a76b084498d55f252f48b2e3786804cdf24b7` | Split only the selected `hvc1` image item's one-through-four-byte length-delimited NAL units, reject in-band parameter-set replacement and end-of-sequence state, and require exactly one base-layer IDR picture. Retain the intra slice, separate-plane, SAO, quantization, deblocking, cross-slice filtering, tile/wavefront entry-point, extension, byte-alignment, and entropy-payload fields required by reconstruction. Keep emulation-prevention positions only to translate slice substream byte boundaries; add no Annex B scanner, access-unit collection, non-IDR reference lifetime, playback, timing, track, or sample model. | | `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. | @@ -149,14 +150,14 @@ This assessment is based on the current source after the upstream ImageSharp mer - Decode now resolves `cdsc`-associated Exif and `application/rdf+xml` XMP items for the primary still image, validates the declared Exif TIFF-header offset, and attaches the payloads through ImageSharp's existing profile types before presentation transforms. Independent AVIF, HEIC, and HIF metadata fixtures and Identify-time profile reporting remain required. - ICC and `nclx` CICP color properties are now associated with the presented color item instead of global parser state, validated, and exposed on Decode and Identify through the existing ImageSharp profiles. AV1 decode requires specified sequence-header fields and the range flag to match `nclx`, uses the property only for fields the bitstream leaves unspecified, and exposes the effective sequence-header CICP description when no property is present. Independent ICC/CICP fixtures, Identify-time bitstream fallback, ICC conversion coverage, and HEVC integration remain required. - Pixel aspect ratio now preserves the complete unsigned spacing pair, affects Decode and Identify through ImageSharp's existing unitless resolution metadata, and follows quarter-turn presentation rotation. Independent grid, rotation, and maximum-spacing fixtures remain required. -- AV1 codec configuration is now retained per `av01` image item rather than in decoder-global state. Decode requires the property, validates its fixed record against the item's sequence header and any associated `pixi` channel depths, and requires matching configurations across grid tiles. Identify now reports the configuration's 8/10/12-bit precision and monochrome shape. The optional `configOBUs` sequence is bounded and validated, including its mandatory size fields, first-position/at-most-one sequence-header rule, the image item's exactly-one sequence-header rule, and exact comparison of a repeated configuration header with the item header. Content light-level and mastering-display color-volume information are decoded from the bounded `clli` and `mdcv` image properties and matching AV1 HDR metadata OBUs, with the representations' distinct fixed-point precision, grid/property precedence, and `SkipMetadata` behavior preserved. The still-image `cclv`, `amve`, `reve`, and `ndwt` properties preserve content-volume, ambient-viewing, mastering-environment, and diffuse-white descriptions without importing their video-SEI or sample-entry state models. Independent fixtures and encoder preservation remain required. HEVC `hvcC` is now associated with each `hvc1` image item, structurally validated, and used to report encoded precision and monochrome shape; parameter-set validation and pixel reconstruction remain incomplete. +- AV1 codec configuration is now retained per `av01` image item rather than in decoder-global state. Decode requires the property, validates its fixed record against the item's sequence header and any associated `pixi` channel depths, and requires matching configurations across grid tiles. Identify now reports the configuration's 8/10/12-bit precision and monochrome shape. The optional `configOBUs` sequence is bounded and validated, including its mandatory size fields, first-position/at-most-one sequence-header rule, the image item's exactly-one sequence-header rule, and exact comparison of a repeated configuration header with the item header. Content light-level and mastering-display color-volume information are decoded from the bounded `clli` and `mdcv` image properties and matching AV1 HDR metadata OBUs, with the representations' distinct fixed-point precision, grid/property precedence, and `SkipMetadata` behavior preserved. The still-image `cclv`, `amve`, `reve`, and `ndwt` properties preserve content-volume, ambient-viewing, mastering-environment, and diffuse-white descriptions without importing their video-SEI or sample-entry state models. Independent fixtures and encoder preservation remain required. HEVC `hvcC` is now associated with each `hvc1` image item, structurally validated through its VPS/SPS/PPS arrays, and used to report encoded precision and monochrome shape; item-payload validation is implemented for the bounded decoder path, while pixel reconstruction remains incomplete. - Several image-item properties and relationships remain missing or parsed without fully affecting output. - Identify and decode now use the same bounded metadata parser and both validate the complete leading file type box. The parsed state is still mutable and Identify does not yet report the complete bit depth, color, profile, or transform model. ### HEVC decoder and encoder - `Heif4CharCode` recognizes `hvc1` image items, `HevcCodecConfiguration` validates and associates the bounded `hvcC` property, and Identify reports its HEVC precision and monochrome shape, but `HeifCompressionFactory` has no HEVC item decoder. -- There is no HEVC NAL/RBSP or parameter-set parser, CABAC decoder, coding-tree reconstruction, still-image intra prediction, inverse transform, deblocking, sample-adaptive offset, or high-bit-depth reconstruction path. +- The HEVC path now validates NAL headers and RBSP escaping, parses and links `hvcC` VPS/SPS/PPS structures through Range Extensions, and parses the selected image item's length-delimited base-layer IDR slice headers and entropy payload boundaries. It is deliberately not registered as an item decoder until CABAC, coding-tree reconstruction, still-image intra prediction, inverse quantization/transform, deblocking, sample-adaptive offset, color conversion, and high-bit-depth pixel output are implemented. - There is no HEVC encoder. The current HEIC-branded encoder writes a legacy JPEG payload and therefore cannot provide HEIC output. - Existing HEVC tests prove container identification only; they do not decode or compare HEIC pixels. diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcBitReader.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcBitReader.cs index 39cf978a8..cd2a587e2 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcBitReader.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcBitReader.cs @@ -176,6 +176,28 @@ internal ref struct HevcBitReader return (int)magnitude; } + /// + /// Reads the one-bit marker and zero padding that align slice data to the next byte boundary. + /// + /// + /// The alignment marker is zero or any following alignment bit is nonzero. + /// + public void ReadByteAlignment() + { + if (!this.ReadFlag()) + { + throw new InvalidImageContentException("The HEVC slice-header alignment marker is not set."); + } + + while (!this.IsByteAligned) + { + if (this.ReadFlag()) + { + throw new InvalidImageContentException("The HEVC slice header has a nonzero alignment bit."); + } + } + } + /// /// Reads and validates the stop bit and zero alignment bits that terminate an HEVC raw byte sequence payload. /// diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcImageItemBitstream.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcImageItemBitstream.cs new file mode 100644 index 000000000..0d3a0185e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcImageItemBitstream.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Contains the length-delimited NAL units and IDR slice segments carried by one HEVC still-image item. +/// +internal sealed class HevcImageItemBitstream +{ + /// + /// Initializes a new instance of the class. + /// + /// The complete bounded payload of one hvc1 image item. + /// The codec configuration associated with the same image item. + /// + /// NAL-unit framing is malformed, the payload contains sequence or layered coding, or the item does not contain + /// exactly one independently decodable IDR picture. + /// + public HevcImageItemBitstream(ReadOnlySpan data, HevcCodecConfiguration configuration) + { + List nalUnits = []; + List sliceSegments = []; + int offset = 0; + while (offset < data.Length) + { + if (data.Length - offset < configuration.NalUnitLengthSize) + { + throw new InvalidImageContentException("The HEVC image item has a truncated NAL-unit length."); + } + + int nalUnitLength = ReadNalUnitLength(data[offset..], configuration.NalUnitLengthSize); + offset += configuration.NalUnitLengthSize; + if (nalUnitLength < 2 || nalUnitLength > data.Length - offset) + { + throw new InvalidImageContentException("The HEVC image item has an invalid NAL-unit length."); + } + + HevcNalUnit nalUnit = new(data.Slice(offset, nalUnitLength)); + offset += nalUnitLength; + if (nalUnit.Header.LayerId != 0 || nalUnit.Header.TemporalId != 0) + { + throw new InvalidImageContentException("The HEVC image item contains layered or temporal-substream NAL units."); + } + + nalUnits.Add(nalUnit); + if (nalUnit.Header.IsVideoCodingLayer) + { + if (!nalUnit.Header.IsInstantaneousDecoderRefresh) + { + throw new InvalidImageContentException("The HEVC image item contains a coded picture that is not independently decodable."); + } + + HevcSliceSegmentHeader sliceSegment = new(nalUnit, configuration.PictureParameterSets); + if (sliceSegments.Count == 0 && !sliceSegment.FirstSliceSegmentInPicture) + { + throw new InvalidImageContentException("The first HEVC image-item slice is not marked as the first picture segment."); + } + + if (sliceSegments.Count != 0 && sliceSegment.FirstSliceSegmentInPicture) + { + throw new InvalidImageContentException("The HEVC image item contains more than one coded picture."); + } + + sliceSegments.Add(sliceSegment); + continue; + } + + if (nalUnit.Header.NalUnitType is 32 or 33 or 34) + { + // hvc1 image items obtain all parameter sets from the associated hvcC property. Accepting in-band + // replacements would silently apply the more permissive hev1 sample contract to this still image. + throw new InvalidImageContentException("The HEVC hvc1 image item contains an in-band parameter set."); + } + + if (nalUnit.Header.NalUnitType is 36 or 37) + { + throw new InvalidImageContentException("The HEVC image item contains an end-of-sequence NAL unit."); + } + } + + if (sliceSegments.Count == 0) + { + throw new InvalidImageContentException("The HEVC image item contains no independently decodable picture."); + } + + this.NalUnits = nalUnits; + this.SliceSegments = sliceSegments; + } + + /// + /// Gets every decoded NAL unit in item order, including permitted delimiter, filler, and supplemental units. + /// + public IReadOnlyList NalUnits { get; } + + /// + /// Gets the ordered slice segments that reconstruct the item's single IDR picture. + /// + public IReadOnlyList SliceSegments { get; } + + /// + /// Reads an unsigned one-through-four-byte NAL-unit length without assuming four-byte item framing. + /// + /// The item bytes beginning at the length field. + /// The codec-configuration-selected length-field width. + /// The bounded signed integer NAL-unit length. + /// The unsigned length exceeds the supported item-buffer range. + private static int ReadNalUnitLength(ReadOnlySpan data, int lengthSize) + { + uint value = 0; + for (int byteIndex = 0; byteIndex < lengthSize; byteIndex++) + { + value = (value << 8) | data[byteIndex]; + } + + if (value > int.MaxValue) + { + throw new InvalidImageContentException("The HEVC image-item NAL-unit length is too large."); + } + + return (int)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs index 8f5d64ba3..f9fe8a661 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs @@ -21,8 +21,14 @@ internal sealed class HevcNalUnit // parameter-set and slice parser observes the same validated RBSP representation. ReadOnlySpan encodedPayload = data[2..]; byte[] rbspBuffer = new byte[encodedPayload.Length]; - int rbspLength = HevcRbspDecoder.Decode(encodedPayload, rbspBuffer); + int rbspLength = HevcRbspDecoder.Decode( + encodedPayload, + rbspBuffer, + out int[] emulationPreventionBytePositions); + + this.EncodedPayloadLength = encodedPayload.Length; this.Rbsp = rbspBuffer.AsMemory(0, rbspLength); + this.EmulationPreventionBytePositions = emulationPreventionBytePositions; } /// @@ -34,6 +40,16 @@ internal sealed class HevcNalUnit /// Gets the raw byte sequence payload after removal of emulation-prevention bytes. /// public ReadOnlyMemory Rbsp { get; } + + /// + /// Gets the encoded byte-sequence payload length before removal of emulation-prevention bytes. + /// + public int EncodedPayloadLength { get; } + + /// + /// Gets the zero-based encoded-payload positions of removed emulation-prevention bytes. + /// + public IReadOnlyList EmulationPreventionBytePositions { get; } } /// @@ -46,16 +62,23 @@ internal static class HevcRbspDecoder /// /// The NAL payload following the two-byte header. /// A buffer at least as long as . + /// + /// Receives the zero-based encoded-payload positions of removed emulation-prevention bytes. + /// /// The number of decoded bytes written to . /// /// The payload contains a forbidden start-code-like byte sequence or an invalid emulation-prevention byte. /// - public static int Decode(ReadOnlySpan encodedPayload, Span destination) + public static int Decode( + ReadOnlySpan encodedPayload, + Span destination, + out int[] emulationPreventionBytePositions) { DebugGuard.MustBeGreaterThanOrEqualTo(destination.Length, encodedPayload.Length, nameof(destination)); int destinationOffset = 0; int consecutiveZeroBytes = 0; + List? preventionBytePositions = null; for (int sourceOffset = 0; sourceOffset < encodedPayload.Length; sourceOffset++) { byte value = encodedPayload[sourceOffset]; @@ -71,6 +94,7 @@ internal static class HevcRbspDecoder if (value == 3) { + (preventionBytePositions ??= []).Add(sourceOffset); sourceOffset++; if (sourceOffset == encodedPayload.Length || encodedPayload[sourceOffset] > 3) { @@ -88,6 +112,7 @@ internal static class HevcRbspDecoder consecutiveZeroBytes = value == 0 ? consecutiveZeroBytes + 1 : 0; } + emulationPreventionBytePositions = preventionBytePositions?.ToArray() ?? Array.Empty(); return destinationOffset; } } diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnitHeader.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnitHeader.cs index 897e88d08..3c6ee7921 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnitHeader.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnitHeader.cs @@ -36,6 +36,16 @@ internal readonly struct HevcNalUnitHeader /// public byte TemporalId { get; } + /// + /// Gets a value indicating whether the NAL unit contains coded slice-segment data. + /// + public bool IsVideoCodingLayer => this.NalUnitType <= 31; + + /// + /// Gets a value indicating whether the NAL unit begins an instantaneous decoder refresh picture. + /// + public bool IsInstantaneousDecoderRefresh => this.NalUnitType is 19 or 20; + /// /// Reads and validates an HEVC NAL-unit header. /// diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs index d47d2c3c8..90a3fbab3 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs @@ -26,6 +26,69 @@ internal static class HevcParameterSetSyntax public static int GetCropUnitHeight(byte chromaFormat, bool separateColorPlane) => !separateColorPlane && chromaFormat == 1 ? 2 : 1; + /// + /// Gets the number of coding-tree blocks needed to cover one coded picture dimension. + /// + /// The coded luma-sample count. + /// The base-two logarithm of the coding-tree-block size. + /// The covering coding-tree-block count. + public static int GetCodingTreeBlockCount(int sampleCount, int codingTreeBlockLog2) + => ((sampleCount - 1) >> codingTreeBlockLog2) + 1; + + /// + /// Gets the number of bits required to represent values below a positive exclusive upper bound. + /// + /// The positive exclusive upper bound. + /// The ceiling of the base-two logarithm, with zero returned for an upper bound of one. + public static int GetCeilingLog2(int exclusiveUpperBound) + { + DebugGuard.MustBeGreaterThan(exclusiveUpperBound, 0, nameof(exclusiveUpperBound)); + + int bitCount = 0; + int remaining = exclusiveUpperBound - 1; + while (remaining > 0) + { + bitCount++; + remaining >>= 1; + } + + return bitCount; + } + + /// + /// Reads a signed chroma quantization-parameter offset. + /// + /// The HEVC syntax reader. + /// The decoded offset in the registered range from negative twelve through twelve. + /// The offset is outside its registered range. + public static int ReadQuantizationParameterOffset(ref HevcBitReader reader) + { + int offset = reader.ReadSignedExpGolomb(); + if (offset is < -12 or > 12) + { + throw new InvalidImageContentException("The HEVC chroma quantization-parameter offset is invalid."); + } + + return offset; + } + + /// + /// Reads a signed deblocking-filter threshold offset. + /// + /// The HEVC syntax reader. + /// The decoded half-offset in the registered range from negative six through six. + /// The offset is outside its registered range. + public static int ReadDeblockingFilterOffset(ref HevcBitReader reader) + { + int offset = reader.ReadSignedExpGolomb(); + if (offset is < -6 or > 6) + { + throw new InvalidImageContentException("The HEVC deblocking-filter offset is invalid."); + } + + return offset; + } + /// /// Consumes hypothetical-reference-decoder syntax without adding playback state to the still-image model. /// diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureParameterSet.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureParameterSet.cs index 43bd1b9e1..16e3fa56f 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureParameterSet.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureParameterSet.cs @@ -95,8 +95,8 @@ internal sealed class HevcPictureParameterSet this.QuantizationParameterDeltaDepth = (int)quantizationParameterDeltaDepth; } - this.ChromaCbQuantizationParameterOffset = ReadQuantizationParameterOffset(ref reader); - this.ChromaCrQuantizationParameterOffset = ReadQuantizationParameterOffset(ref reader); + this.ChromaCbQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader); + this.ChromaCrQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader); this.SliceChromaQuantizationParameterOffsetsPresent = reader.ReadFlag(); this.WeightedPredictionEnabled = reader.ReadFlag(); this.WeightedBiPredictionEnabled = reader.ReadFlag(); @@ -104,11 +104,11 @@ internal sealed class HevcPictureParameterSet this.TilesEnabled = reader.ReadFlag(); this.EntropyCodingSynchronizationEnabled = reader.ReadFlag(); - int codingTreeBlockColumns = GetCodingTreeBlockCount( + int codingTreeBlockColumns = HevcParameterSetSyntax.GetCodingTreeBlockCount( sequenceParameterSet.Width, sequenceParameterSet.CodingTreeBlockLog2); - int codingTreeBlockRows = GetCodingTreeBlockCount( + int codingTreeBlockRows = HevcParameterSetSyntax.GetCodingTreeBlockCount( sequenceParameterSet.Height, sequenceParameterSet.CodingTreeBlockLog2); @@ -158,8 +158,8 @@ internal sealed class HevcPictureParameterSet this.DeblockingFilterDisabled = reader.ReadFlag(); if (!this.DeblockingFilterDisabled) { - this.DeblockingFilterBetaOffsetDiv2 = ReadDeblockingFilterOffset(ref reader); - this.DeblockingFilterTcOffsetDiv2 = ReadDeblockingFilterOffset(ref reader); + this.DeblockingFilterBetaOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader); + this.DeblockingFilterTcOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader); } } @@ -399,8 +399,8 @@ internal sealed class HevcPictureParameterSet int[] crOffsets = new int[chromaOffsetCount]; for (int offset = 0; offset < chromaOffsetCount; offset++) { - cbOffsets[offset] = ReadQuantizationParameterOffset(ref reader); - crOffsets[offset] = ReadQuantizationParameterOffset(ref reader); + cbOffsets[offset] = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader); + crOffsets[offset] = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader); } this.ChromaQuantizationParameterOffsetsCb = cbOffsets; @@ -420,49 +420,6 @@ internal sealed class HevcPictureParameterSet this.SampleAdaptiveOffsetScaleChromaLog2 = (int)chromaScale; } - /// - /// Reads a signed chroma quantization-parameter offset. - /// - /// The picture-parameter-set raw byte sequence payload reader. - /// The decoded offset in the registered range from negative twelve through twelve. - /// The offset is outside its registered range. - private static int ReadQuantizationParameterOffset(ref HevcBitReader reader) - { - int offset = reader.ReadSignedExpGolomb(); - if (offset is < -12 or > 12) - { - throw new InvalidImageContentException("The HEVC picture parameter set has an invalid chroma quantization-parameter offset."); - } - - return offset; - } - - /// - /// Reads a signed picture-level deblocking-filter offset. - /// - /// The picture-parameter-set raw byte sequence payload reader. - /// The decoded half-offset in the registered range from negative six through six. - /// The offset is outside its registered range. - private static int ReadDeblockingFilterOffset(ref HevcBitReader reader) - { - int offset = reader.ReadSignedExpGolomb(); - if (offset is < -6 or > 6) - { - throw new InvalidImageContentException("The HEVC picture parameter set has an invalid deblocking-filter offset."); - } - - return offset; - } - - /// - /// Gets the number of coding-tree blocks needed to cover one coded picture dimension. - /// - /// The coded luma-sample count. - /// The base-two logarithm of the coding-tree-block size. - /// The covering coding-tree-block count. - private static int GetCodingTreeBlockCount(int sampleCount, int codingTreeBlockLog2) - => ((sampleCount - 1) >> codingTreeBlockLog2) + 1; - /// /// Reads or derives one axis of the tile grid. /// diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs new file mode 100644 index 000000000..53fa3645b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs @@ -0,0 +1,389 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Contains the decoded header and entropy-coded payload of one HEVC still-picture slice segment. +/// +internal sealed class HevcSliceSegmentHeader +{ + /// + /// Initializes a new instance of the class. + /// + /// The item-local instantaneous-decoder-refresh NAL unit. + /// The picture parameter sets available to the coded image item. + /// + /// The NAL unit is not a base-layer IDR slice, references unavailable parameters, or contains malformed + /// still-picture slice-header syntax. + /// + public HevcSliceSegmentHeader( + HevcNalUnit nalUnit, + IReadOnlyList pictureParameterSets) + { + if (!nalUnit.Header.IsInstantaneousDecoderRefresh + || nalUnit.Header.LayerId != 0 + || nalUnit.Header.TemporalId != 0) + { + throw new InvalidImageContentException("The HEVC image item contains a non-IDR or layered coded slice."); + } + + this.NalUnit = nalUnit; + HevcBitReader reader = new(nalUnit.Rbsp.Span); + this.FirstSliceSegmentInPicture = reader.ReadFlag(); + + // An IDR item has no earlier picture whose output can affect the returned still image. Consume the required + // random-access flag without retaining sequence-output state in the image decoder. + reader.ReadFlag(); + + uint pictureParameterSetId = reader.ReadUnsignedExpGolomb(); + if (pictureParameterSetId > 63) + { + throw new InvalidImageContentException("The HEVC slice segment has an invalid picture-parameter-set identifier."); + } + + HevcPictureParameterSet? pictureParameterSet = null; + foreach (HevcPictureParameterSet candidate in pictureParameterSets) + { + if (candidate.Id == pictureParameterSetId) + { + pictureParameterSet = candidate; + break; + } + } + + if (pictureParameterSet is null) + { + throw new InvalidImageContentException("The HEVC slice segment references an unavailable picture parameter set."); + } + + this.PictureParameterSet = pictureParameterSet; + HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet; + if (pictureParameterSet.DependentSliceSegmentsEnabled && !this.FirstSliceSegmentInPicture) + { + this.DependentSliceSegment = reader.ReadFlag(); + } + + int codingTreeBlockColumns = HevcParameterSetSyntax.GetCodingTreeBlockCount( + sequenceParameterSet.Width, + sequenceParameterSet.CodingTreeBlockLog2); + + int codingTreeBlockRows = HevcParameterSetSyntax.GetCodingTreeBlockCount( + sequenceParameterSet.Height, + sequenceParameterSet.CodingTreeBlockLog2); + + int codingTreeBlockCount = codingTreeBlockColumns * codingTreeBlockRows; + if (!this.FirstSliceSegmentInPicture) + { + int addressBitCount = HevcParameterSetSyntax.GetCeilingLog2(codingTreeBlockCount); + uint address = reader.ReadBits(addressBitCount); + if (address >= codingTreeBlockCount) + { + throw new InvalidImageContentException("The HEVC slice segment address is outside the coded picture."); + } + + this.SliceSegmentAddress = (int)address; + } + + if (!this.DependentSliceSegment) + { + this.ReadIndependentHeader(ref reader); + } + + this.ReadEntryPoints(ref reader, codingTreeBlockCount); + if (pictureParameterSet.SliceSegmentHeaderExtensionPresent) + { + uint extensionLength = reader.ReadUnsignedExpGolomb(); + if (extensionLength > int.MaxValue || extensionLength > reader.BitsRemaining / 8) + { + throw new InvalidImageContentException("The HEVC slice-segment header extension is truncated."); + } + + for (int byteIndex = 0; byteIndex < extensionLength; byteIndex++) + { + reader.ReadBits(8); + } + } + + reader.ReadByteAlignment(); + this.HeaderLength = reader.BitPosition / 8; + this.SliceData = nalUnit.Rbsp[this.HeaderLength..]; + if (this.SliceData.IsEmpty) + { + throw new InvalidImageContentException("The HEVC slice segment contains no entropy-coded data."); + } + + int encodedHeaderLength = GetEncodedPayloadOffset( + this.HeaderLength, + nalUnit.EmulationPreventionBytePositions); + + int availableEncodedData = nalUnit.EncodedPayloadLength - encodedHeaderLength; + int cumulativeEntryPointOffset = 0; + foreach (int entryPointOffset in this.EntryPointOffsets) + { + if (cumulativeEntryPointOffset > availableEncodedData - entryPointOffset) + { + throw new InvalidImageContentException("The HEVC slice entry point extends beyond its NAL unit."); + } + + cumulativeEntryPointOffset += entryPointOffset; + } + } + + /// Gets the complete decoded NAL unit containing this slice segment. + public HevcNalUnit NalUnit { get; } + + /// Gets a value indicating whether this is the first slice segment of the coded picture. + public bool FirstSliceSegmentInPicture { get; } + + /// Gets a value indicating whether this segment inherits syntax from an earlier independent slice. + public bool DependentSliceSegment { get; } + + /// Gets the picture parameters selected by this slice segment. + public HevcPictureParameterSet PictureParameterSet { get; } + + /// Gets the raster-scan address of the first coding-tree block in this slice segment. + public int SliceSegmentAddress { get; } + + /// Gets the independent slice prediction type, or for a dependent segment. + public HevcSliceType? SliceType { get; private set; } + + /// Gets the selected color-plane identifier for separate-plane 4:4:4 coding. + public byte ColorPlaneId { get; private set; } + + /// Gets a value indicating whether luma sample-adaptive offset filtering is enabled. + public bool? SampleAdaptiveOffsetLumaEnabled { get; private set; } + + /// Gets a value indicating whether chroma sample-adaptive offset filtering is enabled. + public bool? SampleAdaptiveOffsetChromaEnabled { get; private set; } + + /// Gets the effective luma quantization parameter, or for a dependent segment. + public int? QuantizationParameter { get; private set; } + + /// Gets the slice-level Cb quantization-parameter offset. + public int ChromaCbQuantizationParameterOffset { get; private set; } + + /// Gets the slice-level Cr quantization-parameter offset. + public int ChromaCrQuantizationParameterOffset { get; private set; } + + /// Gets a value indicating whether coding units can select the PPS chroma-offset list. + public bool? ChromaQuantizationParameterOffsetListEnabled { get; private set; } + + /// Gets a value indicating whether deblocking is disabled for this independent slice. + public bool? DeblockingFilterDisabled { get; private set; } + + /// Gets half the effective deblocking beta-threshold offset. + public int DeblockingFilterBetaOffsetDiv2 { get; private set; } + + /// Gets half the effective deblocking clipping-threshold offset. + public int DeblockingFilterTcOffsetDiv2 { get; private set; } + + /// Gets a value indicating whether in-loop filtering crosses slice boundaries. + public bool? LoopFilterAcrossSlicesEnabled { get; private set; } + + /// + /// Gets the encoded-byte lengths that separate tile or wavefront entropy substreams after the first substream. + /// + public IReadOnlyList EntryPointOffsets { get; private set; } = Array.Empty(); + + /// Gets the slice-header length in decoded raw-byte-sequence payload bytes. + public int HeaderLength { get; } + + /// Gets the entropy-coded slice data following byte alignment. + public ReadOnlyMemory SliceData { get; } + + /// + /// Reads fields carried only by an independent slice-segment header. + /// + /// The slice-segment raw byte sequence payload reader. + /// + /// The slice is not intra-coded or its quantization and filter fields are outside the governing parameter bounds. + /// + private void ReadIndependentHeader(ref HevcBitReader reader) + { + HevcPictureParameterSet pictureParameterSet = this.PictureParameterSet; + HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet; + for (int extraBit = 0; extraBit < pictureParameterSet.ExtraSliceHeaderBitCount; extraBit++) + { + reader.ReadFlag(); + } + + uint sliceType = reader.ReadUnsignedExpGolomb(); + if (sliceType != (uint)HevcSliceType.Intra) + { + throw new InvalidImageContentException("An independently coded HEVC image item must contain intra IDR slices."); + } + + this.SliceType = HevcSliceType.Intra; + if (pictureParameterSet.OutputFlagPresent && !reader.ReadFlag()) + { + throw new InvalidImageContentException("The HEVC image-item slice is marked as unavailable for output."); + } + + if (sequenceParameterSet.SeparateColorPlaneFlag) + { + this.ColorPlaneId = (byte)reader.ReadBits(2); + if (this.ColorPlaneId > 2) + { + throw new InvalidImageContentException("The HEVC slice segment has an invalid separate color-plane identifier."); + } + } + + bool hasCombinedChromaPlanes = sequenceParameterSet.ChromaFormat != 0 + && !sequenceParameterSet.SeparateColorPlaneFlag; + + if (sequenceParameterSet.SampleAdaptiveOffsetEnabled) + { + this.SampleAdaptiveOffsetLumaEnabled = reader.ReadFlag(); + this.SampleAdaptiveOffsetChromaEnabled = hasCombinedChromaPlanes && reader.ReadFlag(); + } + + int sliceQuantizationParameterDelta = reader.ReadSignedExpGolomb(); + long quantizationParameter = 26L + + pictureParameterSet.InitialQuantizationParameterMinus26 + + sliceQuantizationParameterDelta; + + int minimumQuantizationParameter = -6 * (sequenceParameterSet.BitDepthLuma - 8); + if (quantizationParameter < minimumQuantizationParameter || quantizationParameter > 51) + { + throw new InvalidImageContentException("The HEVC slice segment has an invalid luma quantization parameter."); + } + + this.QuantizationParameter = (int)quantizationParameter; + if (pictureParameterSet.SliceChromaQuantizationParameterOffsetsPresent && hasCombinedChromaPlanes) + { + this.ChromaCbQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader); + this.ChromaCrQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader); + if (pictureParameterSet.ChromaCbQuantizationParameterOffset + this.ChromaCbQuantizationParameterOffset is < -12 or > 12 + || pictureParameterSet.ChromaCrQuantizationParameterOffset + this.ChromaCrQuantizationParameterOffset is < -12 or > 12) + { + throw new InvalidImageContentException("The HEVC slice and picture chroma quantization offsets have an invalid sum."); + } + } + + if (pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count != 0) + { + this.ChromaQuantizationParameterOffsetListEnabled = reader.ReadFlag(); + } + + this.ReadDeblockingFilterFields(ref reader); + bool sampleAdaptiveOffsetEnabled = this.SampleAdaptiveOffsetLumaEnabled == true + || this.SampleAdaptiveOffsetChromaEnabled == true; + + if (pictureParameterSet.LoopFilterAcrossSlicesEnabled + && (sampleAdaptiveOffsetEnabled || this.DeblockingFilterDisabled == false)) + { + this.LoopFilterAcrossSlicesEnabled = reader.ReadFlag(); + } + else + { + this.LoopFilterAcrossSlicesEnabled = pictureParameterSet.LoopFilterAcrossSlicesEnabled; + } + } + + /// + /// Resolves the independent slice's effective deblocking mode and threshold offsets. + /// + /// The slice-segment raw byte sequence payload reader. + private void ReadDeblockingFilterFields(ref HevcBitReader reader) + { + HevcPictureParameterSet pictureParameterSet = this.PictureParameterSet; + bool overrideFilter = false; + if (pictureParameterSet.DeblockingFilterControlPresent + && pictureParameterSet.DeblockingFilterOverrideEnabled) + { + overrideFilter = reader.ReadFlag(); + } + + if (overrideFilter) + { + this.DeblockingFilterDisabled = reader.ReadFlag(); + if (this.DeblockingFilterDisabled == false) + { + this.DeblockingFilterBetaOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader); + this.DeblockingFilterTcOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader); + } + + return; + } + + this.DeblockingFilterDisabled = pictureParameterSet.DeblockingFilterControlPresent + && pictureParameterSet.DeblockingFilterDisabled; + + this.DeblockingFilterBetaOffsetDiv2 = pictureParameterSet.DeblockingFilterBetaOffsetDiv2; + this.DeblockingFilterTcOffsetDiv2 = pictureParameterSet.DeblockingFilterTcOffsetDiv2; + } + + /// + /// Reads tile or wavefront substream entry-point byte lengths. + /// + /// The slice-segment raw byte sequence payload reader. + /// The number of coding-tree blocks in the coded picture. + /// + /// The entry-point count, field width, or byte length exceeds the bounded picture or integer range. + /// + private void ReadEntryPoints(ref HevcBitReader reader, int codingTreeBlockCount) + { + HevcPictureParameterSet pictureParameterSet = this.PictureParameterSet; + if (!pictureParameterSet.TilesEnabled && !pictureParameterSet.EntropyCodingSynchronizationEnabled) + { + return; + } + + uint entryPointCount = reader.ReadUnsignedExpGolomb(); + if (entryPointCount >= codingTreeBlockCount) + { + throw new InvalidImageContentException("The HEVC slice segment declares too many entropy entry points."); + } + + if (entryPointCount == 0) + { + return; + } + + uint offsetLengthMinusOne = reader.ReadUnsignedExpGolomb(); + if (offsetLengthMinusOne > 31) + { + throw new InvalidImageContentException("The HEVC slice entry-point offset width is invalid."); + } + + int offsetBitCount = (int)offsetLengthMinusOne + 1; + int[] entryPointOffsets = new int[entryPointCount]; + for (int entryPoint = 0; entryPoint < entryPointOffsets.Length; entryPoint++) + { + uint entryPointOffsetMinusOne = reader.ReadBits(offsetBitCount); + if (entryPointOffsetMinusOne >= int.MaxValue) + { + throw new InvalidImageContentException("The HEVC slice entry-point byte length is too large."); + } + + entryPointOffsets[entryPoint] = (int)entryPointOffsetMinusOne + 1; + } + + this.EntryPointOffsets = entryPointOffsets; + } + + /// + /// Converts an RBSP byte boundary to its corresponding encoded-payload boundary. + /// + /// The decoded raw-byte-sequence payload offset. + /// The removed encoded-payload byte positions. + /// The encoded byte-sequence payload offset at the same syntax boundary. + private static int GetEncodedPayloadOffset( + int rbspOffset, + IReadOnlyList emulationPreventionBytePositions) + { + int encodedOffset = rbspOffset; + foreach (int preventionBytePosition in emulationPreventionBytePositions) + { + if (preventionBytePosition >= encodedOffset) + { + break; + } + + encodedOffset++; + } + + return encodedOffset; + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSliceType.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceType.cs new file mode 100644 index 000000000..1ccbe2187 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Identifies the prediction structure signaled for an HEVC slice segment. +/// +internal enum HevcSliceType +{ + /// + /// The slice can use intra and bidirectional inter prediction. + /// + Bidirectional = 0, + + /// + /// The slice can use intra and forward inter prediction. + /// + Predictive = 1, + + /// + /// The slice uses only intra-picture prediction. + /// + Intra = 2 +}