From a34c1f09b743568dc575f9ecf56220fd70215696 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 04:22:55 +1000 Subject: [PATCH] Decode HEIF content light metadata --- HEIF_IMPLEMENTATION_PLAN.md | 4 +- .../Formats/Heif/Av1/Av1CodecConfiguration.cs | 186 ++++++++++++++++-- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 2 - .../Formats/Heif/Av1HeifItemDecoder.cs | 5 + src/ImageSharp/Formats/Heif/Heif4CharCode.cs | 5 + src/ImageSharp/Formats/Heif/Heif4CharCode.tt | 1 + .../Formats/Heif/HeifContentLightLevel.cs | 35 ++++ .../Formats/Heif/HeifDecoderCore.cs | 52 +++++ src/ImageSharp/Formats/Heif/HeifItem.cs | 6 + src/ImageSharp/Formats/Heif/HeifMetadata.cs | 7 + 10 files changed, 285 insertions(+), 18 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index af3452620..e2cd1933d 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -94,7 +94,7 @@ This snapshot pins or classifies the available references and failures; it does | `HeifDecoderCore.ApplyAssociatedMetadata` | HEIF Annex A Exif item data, MIME metadata items, and `cdsc` item references | libavif `src/read.c` function `avifDecoderFindMetadata`, `src/exif.c` function `avifGetExifTiffHeaderOffset`, and the Exif/XMP item writing paths in `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Resolve only metadata items whose `cdsc` reference identifies the decoded primary image, validate the Exif TIFF-header offset, and attach Exif or `application/rdf+xml` XMP through ImageSharp's existing profile types. This is a bounded still-image metadata path; it does not introduce a generic ISO BMFF metadata, media, or track model. | | `HeifDecoderCore` color-property parsing/association, `HeifItem` color profiles, and `Av1Decoder` container color override | ISO/IEC 14496-12 section 12.1.5 color information; HEIF section 6.5.5.1 color-information properties; AV1-ISOBMFF section 2.3.4 configuration semantics | libavif `src/read.c` functions `avifParseColourInformationBox`, `avifReadColorNclxProperty`, and `avifReadColorProperties`, plus `src/write.c` function `avifEncoderWriteColorProperties`, at `092276ce89098ead06db80975173191e5fee1826` | Associate at most one ICC and one `nclx` property with each presented color image item, validate ICC payloads and CICP reserved bits, expose them through ImageSharp's existing profile types, inherit a grid's CICP description only for tiles that do not declare one, and let container CICP values override the matching AV1 sequence-header fields before still-image reconstruction and YUV-to-RGB conversion. Retain only the two image color profiles; do not add a reusable color-box, sample-entry, track, or media model. | | `HeifPixelAspectRatio`, `HeifItem.PixelAspectRatio`, and `HeifDecoderCore.ApplyItemPixelAspectRatioMetadata` | ISO/IEC 14496-12 section 12.1.4.3 pixel aspect ratio | libavif `src/read.c` function `avifParsePixelAspectRatioBox`, `src/write.c` function `avifEncoderWritePaspProperty`, and the presented-image property selection in `src/read.c` at `092276ce89098ead06db80975173191e5fee1826` | Preserve the two unsigned 32-bit relative spacings on the associated image item, reject zero or duplicate ratios, and map the displayed pixel width-to-height ratio into ImageSharp's existing unitless resolution metadata. Exchange the metadata axes after a quarter-turn presentation rotation and fall back from a derived grid to its first decodable tile only when the grid does not declare `pasp`. This remains one still-image presentation property and introduces no generic transform, sample-entry, or display model. | -| `Av1CodecConfiguration`, `HeifItem.Av1CodecConfiguration`, `Av1HeifItemDecoder`, and AV1 grid configuration checks | AV1-ISOBMFF sections 2.3.3 and 2.3.4 codec-configuration record syntax and semantics; AVIF sections 2.1 and 2.2.1 AV1 image-item and item-configuration constraints; MIAF section 7.3.11.4.1 grid input constraints | libavif `src/read.c` functions `avifParseCodecConfiguration`, `avifDecoderItemValidateProperties`, and `avifReadCodecConfigProperty` at `092276ce89098ead06db80975173191e5fee1826` | Associate exactly one `av1C` property with each decoded `av01` image item, validate the fixed record and its bit depth/chroma fields against the item's AV1 sequence header and optional `pixi` channel depths, require matching configurations across grid tiles, and report the encoded image precision and monochrome shape through `HeifMetadata`. Validate low-overhead OBU framing, require exactly one sequence header in the image item, allow at most one first-position sequence header in `configOBUs`, and compare a repeated header's extension and payload exactly while ignoring only its legal size-field representation. Metadata OBU equivalence with `clli`, `mdcv`, and related image properties remains required. Consume but do not retain presentation-delay syntax, and introduce no sample entry, sample description, track, timing, or generic decoder-configuration model. | +| `Av1CodecConfiguration`, `HeifItem.Av1CodecConfiguration`, `Av1HeifItemDecoder`, and AV1 grid configuration checks | AV1-ISOBMFF sections 2.3.3 and 2.3.4 codec-configuration record syntax and semantics; AVIF sections 2.1, 2.2.1, and 2.2.3 AV1 image-item, item-configuration, and HDR metadata constraints; MIAF section 7.3.11.4.1 grid input constraints | libavif `src/read.c` functions `avifParseCodecConfiguration`, `avifDecoderItemValidateProperties`, `avifReadCodecConfigProperty`, and `avifParseContentLightLevelInformation` at `092276ce89098ead06db80975173191e5fee1826`; libaom `av1/decoder/obu.c` functions `read_metadata` and `read_metadata_hdr_cll` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Associate exactly one `av1C` property with each decoded `av01` image item, validate the fixed record and its bit depth/chroma fields against the item's AV1 sequence header and optional `pixi` channel depths, require matching configurations across grid tiles, and report the encoded image precision and monochrome shape through `HeifMetadata`. Validate low-overhead OBU framing, require exactly one sequence header in the image item, allow at most one first-position sequence header in `configOBUs`, and compare a repeated header's extension and payload exactly while ignoring only its legal size-field representation. Decode `clli` as a still-image item property, validate matching HDR CLL metadata OBUs from the combined configuration/item sequence, and expose the effective content-light values without adding sample groups, tracks, or media metadata. `mdcv` and related image-property equivalence remain required. Consume but do not retain presentation-delay syntax, and introduce no sample entry, sample description, track, timing, or generic decoder-configuration 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. | | `HeifDecoderCore` alpha auxiliary selection/composition and `GridHeifItemDecoder` auxiliary tile ordering | ISO/IEC 23008-12 alpha auxiliary image semantics, `auxC`, `auxl`, `prem`, and per-grid-tile alpha relationships | libavif `src/read.c` functions `avifParseAuxiliaryTypeProperty`, `avifDecoderItemIsAlphaAux`, `avifMetaFindAlphaItem`, and `avifDecoderCheckAlphaProperties`, plus `src/scale.c` box-filter scaling at `092276ce89098ead06db80975173191e5fee1826` | Recognize both registered alpha URNs, decode a direct alpha image/grid or the complete row-major set of per-color-tile alpha auxiliaries, normalize through `L16`, box-resample differing auxiliary dimensions, compose through `Rgba64` and `PixelOperations`, and unassociate `prem` color samples with transparent-black handling. This remains an image-item relationship only; no track or generic media-reference model is introduced. | @@ -126,7 +126,7 @@ 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, exposed on Decode and Identify through the existing ImageSharp profiles, and used to override matching AV1 bitstream color fields before still-image color conversion. Independent ICC/CICP fixtures, decoded AV1 bitstream-CICP fallback metadata, 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. Metadata OBU equivalence with `clli`, `mdcv`, and related still-image properties remains required; HEVC `hvcC` remains unimplemented. +- 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 information is decoded from the bounded `clli` image property and matching HDR CLL metadata OBUs, with grid/property precedence and `SkipMetadata` behavior preserved. `mdcv` and related still-image metadata equivalence remain required; HEVC `hvcC` remains unimplemented. - 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. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs index bf4395bf3..de945cb96 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; namespace SixLabors.ImageSharp.Formats.Heif.Av1; @@ -31,6 +32,11 @@ internal sealed class Av1CodecConfiguration /// private readonly int configSequenceHeaderExtension; + /// + /// The content light-level metadata carried by the configuration OBUs, or when absent. + /// + private readonly HeifContentLightLevel? configContentLightLevel; + /// /// Initializes a new instance of the class from an AV1 codec-configuration /// item-property payload. @@ -89,7 +95,8 @@ internal sealed class Av1CodecConfiguration "AV1 codec configuration", out this.configSequenceHeaderOffset, out this.configSequenceHeaderLength, - out this.configSequenceHeaderExtension); + out this.configSequenceHeaderExtension, + out this.configContentLightLevel); if (sequenceHeaderCount > 1) { @@ -153,10 +160,19 @@ internal sealed class Av1CodecConfiguration public ReadOnlyMemory ConfigObus => this.configObus; /// - /// Validates the AV1 image item OBU layout and any sequence header repeated by the configuration record. + /// Validates the AV1 image item OBU layout and metadata against its item properties and configuration record. /// /// The complete AV1 image item payload. - public void ValidateItemData(ReadOnlySpan itemData) + /// + /// The content light-level property associated with the image item, or when absent. + /// + /// + /// The content light-level metadata carried by the combined configuration and item OBUs, or + /// when neither sequence carries it. + /// + public HeifContentLightLevel? ValidateItemData( + ReadOnlySpan itemData, + HeifContentLightLevel? itemContentLightLevel) { int sequenceHeaderCount = ScanObus( itemData, @@ -165,7 +181,8 @@ internal sealed class Av1CodecConfiguration "AV1 image item", out int itemSequenceHeaderOffset, out int itemSequenceHeaderLength, - out int itemSequenceHeaderExtension); + out int itemSequenceHeaderExtension, + out HeifContentLightLevel? itemObuContentLightLevel); if (sequenceHeaderCount != 1) { @@ -191,6 +208,27 @@ internal sealed class Av1CodecConfiguration throw new InvalidImageContentException("The AV1 codec configuration sequence header does not match the image item sequence header."); } } + + ValidateContentLightLevel( + this.configContentLightLevel, + itemContentLightLevel, + "AV1 codec configuration"); + + ValidateContentLightLevel( + itemObuContentLightLevel, + itemContentLightLevel, + "AV1 image item"); + + if (this.configContentLightLevel is not null + && itemObuContentLightLevel is not null + && !ContentLightLevelsMatch(this.configContentLightLevel.Value, itemObuContentLightLevel.Value)) + { + throw new InvalidImageContentException("The AV1 codec configuration and image item contain conflicting content light-level metadata."); + } + + // Configuration OBUs precede the image-item OBUs in the combined AV1 stream, so an item OBU supplies the + // effective value when both sequences repeat the same metadata type. + return itemObuContentLightLevel ?? this.configContentLightLevel; } /// @@ -235,7 +273,7 @@ internal sealed class Av1CodecConfiguration } /// - /// Scans a low-overhead AV1 OBU sequence and locates its first sequence-header payload. + /// Scans a low-overhead AV1 OBU sequence and locates its still-image description metadata. /// /// The complete bounded OBU sequence. /// Indicates that every OBU must carry its registered payload-size field. @@ -246,6 +284,9 @@ internal sealed class Av1CodecConfiguration /// Receives the first sequence-header payload offset, or -1. /// Receives the first sequence-header payload length. /// Receives the first sequence-header extension byte, or -1. + /// + /// Receives the content light-level metadata carried by the sequence, or when absent. + /// /// The number of sequence-header OBUs in the sequence. private static int ScanObus( ReadOnlySpan data, @@ -254,11 +295,13 @@ internal sealed class Av1CodecConfiguration string sourceName, out int sequenceHeaderOffset, out int sequenceHeaderLength, - out int sequenceHeaderExtension) + out int sequenceHeaderExtension, + out HeifContentLightLevel? contentLightLevel) { sequenceHeaderOffset = -1; sequenceHeaderLength = 0; sequenceHeaderExtension = -1; + contentLightLevel = null; int sequenceHeaderCount = 0; int obuIndex = 0; int offset = 0; @@ -325,6 +368,23 @@ internal sealed class Av1CodecConfiguration sequenceHeaderExtension = extension; } } + else if (type == ObuType.Metadata) + { + HeifContentLightLevel? obuContentLightLevel = ReadContentLightLevelMetadata( + data.Slice(offset, payloadLength), + sourceName); + + if (obuContentLightLevel is not null) + { + if (contentLightLevel is not null + && !ContentLightLevelsMatch(contentLightLevel.Value, obuContentLightLevel.Value)) + { + throw new InvalidImageContentException($"The {sourceName} contains conflicting content light-level metadata OBUs."); + } + + contentLightLevel = obuContentLightLevel; + } + } offset += payloadLength; obuIndex++; @@ -341,28 +401,126 @@ internal sealed class Av1CodecConfiguration /// The source description used by invalid-content errors. /// The payload length representable by the current item buffer. private static int ReadObuPayloadLength(ReadOnlySpan data, ref int offset, string sourceName) + { + ulong value = ReadLeb128(data, ref offset, sourceName, "OBU payload length"); + if (value > int.MaxValue) + { + throw new InvalidImageContentException($"The {sourceName} contains an OBU payload too large to buffer."); + } + + return (int)value; + } + + /// + /// Reads content light-level data from an AV1 metadata OBU payload. + /// + /// The bounded metadata OBU payload. + /// The source description used by invalid-content errors. + /// + /// The decoded content light-level metadata, or when the OBU carries another metadata type. + /// + private static HeifContentLightLevel? ReadContentLightLevelMetadata( + ReadOnlySpan payload, + string sourceName) + { + int offset = 0; + ulong metadataType = ReadLeb128(payload, ref offset, sourceName, "metadata type"); + if (metadataType != (ulong)ObuMetadataType.HdrCll) + { + return null; + } + + const int contentLightLevelLength = 4; + if (payload.Length - offset <= contentLightLevelLength) + { + throw new InvalidImageContentException($"The {sourceName} contains truncated content light-level metadata or no trailing bits."); + } + + ReadOnlySpan contentLightLevelData = payload.Slice(offset, contentLightLevelLength); + ReadOnlySpan trailingData = payload[(offset + contentLightLevelLength)..]; + byte lastNonzeroByte = 0; + for (int i = trailingData.Length - 1; i >= 0; i--) + { + if (trailingData[i] != 0) + { + lastNonzeroByte = trailingData[i]; + break; + } + } + + // HDR CLL fields end on a byte boundary. libaom accepts zero padding after the required 0x80 trailing byte, + // so locate the last nonzero byte rather than assuming the OBU payload ends immediately after trailing_bits(). + if (lastNonzeroByte != 0x80) + { + throw new InvalidImageContentException($"The {sourceName} content light-level metadata has invalid trailing bits."); + } + + return new HeifContentLightLevel( + BinaryPrimitives.ReadUInt16BigEndian(contentLightLevelData), + BinaryPrimitives.ReadUInt16BigEndian(contentLightLevelData[2..])); + } + + /// + /// Reads a bounded AV1 little-endian base-128 value. + /// + /// The complete bounded byte sequence. + /// The current byte offset, advanced past the encoded value. + /// The source description used by invalid-content errors. + /// The value description used by invalid-content errors. + /// The decoded unsigned value. + private static ulong ReadLeb128( + ReadOnlySpan data, + ref int offset, + string sourceName, + string valueName) { ulong value = 0; for (int byteIndex = 0; byteIndex < 8; byteIndex++) { if (offset >= data.Length) { - throw new InvalidImageContentException($"The {sourceName} contains a truncated OBU payload length."); + throw new InvalidImageContentException($"The {sourceName} contains a truncated {valueName}."); } byte current = data[offset++]; value |= (ulong)(current & 0x7F) << (byteIndex * 7); if ((current & 0x80) == 0) { - if (value > int.MaxValue) - { - throw new InvalidImageContentException($"The {sourceName} contains an OBU payload too large to buffer."); - } - - return (int)value; + return value; } } - throw new InvalidImageContentException($"The {sourceName} contains an unterminated OBU payload length."); + throw new InvalidImageContentException($"The {sourceName} contains an unterminated {valueName}."); + } + + /// + /// Validates content light-level metadata against the corresponding image-item property when both are present. + /// + /// The value carried by an AV1 metadata OBU. + /// The value carried by the associated image-item property. + /// The OBU source description used by invalid-content errors. + private static void ValidateContentLightLevel( + HeifContentLightLevel? obuContentLightLevel, + HeifContentLightLevel? itemContentLightLevel, + string sourceName) + { + if (obuContentLightLevel is not null + && itemContentLightLevel is not null + && !ContentLightLevelsMatch(obuContentLightLevel.Value, itemContentLightLevel.Value)) + { + throw new InvalidImageContentException($"The {sourceName} content light-level metadata does not match the image-item property."); + } + } + + /// + /// Determines whether two content light-level descriptions carry the same observable values. + /// + /// The first content light-level description. + /// The second content light-level description. + /// when both light-level fields are equal. + private static bool ContentLightLevelsMatch(HeifContentLightLevel left, HeifContentLightLevel right) + { + return left.MaximumContentLightLevel == right.MaximumContentLightLevel + && left.MaximumPictureAverageLightLevel == right.MaximumPictureAverageLightLevel; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index 8249b47dd..efd14b3df 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -73,8 +73,6 @@ internal class Av1Decoder : IAv1TileReader Av1CodecConfiguration? codecConfiguration = null) where TPixel : unmanaged, IPixel { - codecConfiguration?.ValidateItemData(buffer); - Av1BitStreamReader reader = new(buffer); this.obuReader.ReadAll(ref reader, buffer.Length, () => this, false); Guard.NotNull(this.tileReader, nameof(this.tileReader)); diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index 186c35f6a..d725ce74d 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -54,12 +54,17 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder } } + HeifContentLightLevel? obuContentLightLevel = codecConfiguration.ValidateItemData( + data, + item.ContentLightLevel); + Av1Decoder decoder = new(configuration); Image image = decoder.Decode(data, colorProfile, codecConfiguration); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); metadata.CompressionMethod = this.CompressionMethod; metadata.BitDepth = codecConfiguration.BitDepth; metadata.IsMonochrome = codecConfiguration.IsMonochrome; + metadata.ContentLightLevel = item.ContentLightLevel ?? obuContentLightLevel; return image; } } diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs index dc88fc0b0..338baff89 100644 --- a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs @@ -73,6 +73,11 @@ public enum Heif4CharCode : uint /// Colr = 0x636F6C72U, + /// + /// Content light level information. + /// + Clli = 0x636C6C69U, + /// /// HVC configuration. /// diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt index 2a891357f..92d34789f 100644 --- a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt @@ -19,6 +19,7 @@ "ispe", "Item Spatial Extent", "altt", "Alternative text", "colr", "Colour information", + "clli", "Content light level information", "hvcC", "HVC configuration", "av1C", "AV1 configuration", "imir", "Image Mirror", diff --git a/src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs b/src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs new file mode 100644 index 000000000..1c1c5f9c1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the maximum content and picture-average light levels of a HEIF image. +/// +public readonly struct HeifContentLightLevel +{ + /// + /// Initializes a new instance of the struct. + /// + /// + /// The maximum light level of any individual sample, in candelas per square metre, or zero when unspecified. + /// + /// + /// The maximum average light level of any picture, in candelas per square metre, or zero when unspecified. + /// + public HeifContentLightLevel(ushort maximumContentLightLevel, ushort maximumPictureAverageLightLevel) + { + this.MaximumContentLightLevel = maximumContentLightLevel; + this.MaximumPictureAverageLightLevel = maximumPictureAverageLightLevel; + } + + /// + /// Gets the maximum light level of any individual sample, in candelas per square metre, or zero when unspecified. + /// + public ushort MaximumContentLightLevel { get; } + + /// + /// Gets the maximum average light level of any picture, in candelas per square metre, or zero when unspecified. + /// + public ushort MaximumPictureAverageLightLevel { get; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 521dd8d10..1fadf01ce 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -280,6 +280,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore if (!this.Options.SkipMetadata) { this.ApplyItemColorMetadata(metadata, presentationItem); + this.ApplyItemHdrMetadata(metadata, presentationItem); this.ApplyItemPixelAspectRatioMetadata(metadata, presentationItem); } } @@ -956,6 +957,21 @@ internal sealed class HeifDecoderCore : ImageDecoderCore properties.Add(new KeyValuePair(Heif4CharCode.Colr, colorInformation)); + break; + case Heif4CharCode.Clli: + EnsureBufferRemaining(boxBuffer, 0, 4, "content light level information"); + if (boxBuffer.Length != 4) + { + throw new InvalidImageContentException("The content light level property has an invalid length."); + } + + properties.Add( + new KeyValuePair( + Heif4CharCode.Clli, + new HeifContentLightLevel( + BinaryPrimitives.ReadUInt16BigEndian(boxBuffer), + BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[2..])))); + break; case Heif4CharCode.Av1C: EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration"); @@ -1167,6 +1183,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore item.CicpProfile = cicpProfile; } + break; + case Heif4CharCode.Clli: + if (item.ContentLightLevel is not null) + { + throw new InvalidImageContentException($"Item {itemId} associates more than one content light level property."); + } + + item.ContentLightLevel = (HeifContentLightLevel)prop.Value; break; case Heif4CharCode.Clap: if (item.CleanAperture is not null) @@ -1492,6 +1516,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore if (!this.Options.SkipMetadata) { this.ApplyItemColorMetadata(image.Metadata, itemToDecode); + this.ApplyItemHdrMetadata(image.Metadata, itemToDecode); this.ApplyAssociatedMetadata(image.Metadata, rootItem, buffers); _ = this.TryConvertIccProfile(image); } @@ -1510,6 +1535,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore HeifMetadata meta = image.Metadata.GetHeifMetadata(); meta.CompressionMethod = itemDecoder.CompressionMethod; meta.HasAlpha = alphaImage is not null; + if (this.Options.SkipMetadata) + { + // AV1 item decoders still parse metadata OBUs to enforce codec/container equivalence. Remove the + // parsed value here so the public decoder option continues to suppress encoded metadata. + meta.ContentLightLevel = null; + } + return image; } catch @@ -1546,6 +1578,26 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } + /// + /// Applies high-dynamic-range metadata associated with a presented still-image item. + /// + /// The image metadata receiving the high-dynamic-range description. + /// The image item whose pixels are presented. + private void ApplyItemHdrMetadata(ImageMetadata metadata, HeifItem imageItem) + { + HeifItem? gridTile = imageItem.Type == Heif4CharCode.Grid + ? this.FindDecodableGridTile(imageItem) + : null; + + // A derived grid can describe the complete presentation. Fall back to the first coded tile only when the + // grid does not carry its own value, matching the precedence used for its color-profile properties. + HeifContentLightLevel? contentLightLevel = imageItem.ContentLightLevel ?? gridTile?.ContentLightLevel; + if (contentLightLevel is not null) + { + metadata.GetHeifMetadata().ContentLightLevel = contentLightLevel; + } + } + /// /// Applies the pixel aspect ratio associated with a presented still-image item. /// diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs index c2f03223a..6ba9b4ca5 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -66,6 +66,12 @@ internal class HeifItem(Heif4CharCode type, uint id) /// public CicpProfile? CicpProfile { get; set; } + /// + /// Gets or sets the content light-level information associated with this image item, or + /// when the item has no content light-level property. + /// + public HeifContentLightLevel? ContentLightLevel { get; set; } + /// /// Gets or sets the AV1 codec configuration associated with this coded image item, or /// when the item has no AV1 codec-configuration property. diff --git a/src/ImageSharp/Formats/Heif/HeifMetadata.cs b/src/ImageSharp/Formats/Heif/HeifMetadata.cs index e71797c32..34990eeb8 100644 --- a/src/ImageSharp/Formats/Heif/HeifMetadata.cs +++ b/src/ImageSharp/Formats/Heif/HeifMetadata.cs @@ -28,6 +28,7 @@ public class HeifMetadata : IFormatMetadata this.BitDepth = other.BitDepth; this.IsMonochrome = other.IsMonochrome; this.HasAlpha = other.HasAlpha; + this.ContentLightLevel = other.ContentLightLevel; } /// @@ -50,6 +51,12 @@ public class HeifMetadata : IFormatMetadata /// public bool HasAlpha { get; set; } + /// + /// Gets or sets the content light-level information for the primary image, or when it is + /// not available. + /// + public HeifContentLightLevel? ContentLightLevel { get; set; } + /// public static HeifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => new() {