Browse Source

Decode HEIF mastering display metadata

pull/2633/head
James Jackson-South 1 week ago
parent
commit
1742dbc45c
  1. 4
      HEIF_IMPLEMENTATION_PLAN.md
  2. 225
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  3. 8
      src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
  4. 5
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  5. 1
      src/ImageSharp/Formats/Heif/Heif4CharCode.tt
  6. 54
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  7. 6
      src/ImageSharp/Formats/Heif/HeifItem.cs
  8. 84
      src/ImageSharp/Formats/Heif/HeifMasteringDisplayColorVolume.cs
  9. 7
      src/ImageSharp/Formats/Heif/HeifMetadata.cs

4
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, 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. |
| `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; AV1 sections 5.8.3, 5.8.4, 6.7.3, and 6.7.4 HDR metadata syntax and semantics; ISOBMFF mastering-display and content-light image properties; ITU-T H.274 section 8.9 mastering-display field semantics; MIAF section 7.3.11.4.1 grid input constraints | libavif `src/read.c` functions `avifParseCodecConfiguration`, `avifDecoderItemValidateProperties`, `avifReadCodecConfigProperty`, `avifParseContentLightLevelInformation`, and `avifSkipMasteringDisplayColourVolume` at `092276ce89098ead06db80975173191e5fee1826`; libaom `av1/decoder/obu.c` functions `read_metadata`, `read_metadata_hdr_cll`, and `read_metadata_hdr_mdcv` 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` and `mdcv` as still-image item properties, validate matching HDR CLL and HDR MDCV metadata OBUs from the combined configuration/item sequence, and account for the different primary order and fixed-point precision of the ISOBMFF and AV1 MDCV representations. Expose the effective HDR values without adding sample groups, tracks, or media metadata. Related still-image HDR properties 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<TPixel>`, 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. 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.
- 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. Related still-image HDR properties 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.

225
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
@ -37,6 +38,11 @@ internal sealed class Av1CodecConfiguration
/// </summary>
private readonly HeifContentLightLevel? configContentLightLevel;
/// <summary>
/// The mastering-display color volume carried by the configuration OBUs, or <see langword="null"/> when absent.
/// </summary>
private readonly HeifMasteringDisplayColorVolume? configMasteringDisplayColorVolume;
/// <summary>
/// Initializes a new instance of the <see cref="Av1CodecConfiguration"/> class from an AV1 codec-configuration
/// item-property payload.
@ -96,7 +102,8 @@ internal sealed class Av1CodecConfiguration
out this.configSequenceHeaderOffset,
out this.configSequenceHeaderLength,
out this.configSequenceHeaderExtension,
out this.configContentLightLevel);
out this.configContentLightLevel,
out this.configMasteringDisplayColorVolume);
if (sequenceHeaderCount > 1)
{
@ -166,13 +173,21 @@ internal sealed class Av1CodecConfiguration
/// <param name="itemContentLightLevel">
/// The content light-level property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <returns>
/// The content light-level metadata carried by the combined configuration and item OBUs, or
/// <see langword="null"/> when neither sequence carries it.
/// </returns>
public HeifContentLightLevel? ValidateItemData(
/// <param name="itemMasteringDisplayColorVolume">
/// The mastering-display property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and item OBUs.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the combined configuration and item OBUs.
/// </param>
public void ValidateItemData(
ReadOnlySpan<byte> itemData,
HeifContentLightLevel? itemContentLightLevel)
HeifContentLightLevel? itemContentLightLevel,
HeifMasteringDisplayColorVolume? itemMasteringDisplayColorVolume,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
int sequenceHeaderCount = ScanObus(
itemData,
@ -182,7 +197,8 @@ internal sealed class Av1CodecConfiguration
out int itemSequenceHeaderOffset,
out int itemSequenceHeaderLength,
out int itemSequenceHeaderExtension,
out HeifContentLightLevel? itemObuContentLightLevel);
out HeifContentLightLevel? itemObuContentLightLevel,
out HeifMasteringDisplayColorVolume? itemObuMasteringDisplayColorVolume);
if (sequenceHeaderCount != 1)
{
@ -219,6 +235,16 @@ internal sealed class Av1CodecConfiguration
itemContentLightLevel,
"AV1 image item");
ValidateMasteringDisplayColorVolume(
this.configMasteringDisplayColorVolume,
itemMasteringDisplayColorVolume,
"AV1 codec configuration");
ValidateMasteringDisplayColorVolume(
itemObuMasteringDisplayColorVolume,
itemMasteringDisplayColorVolume,
"AV1 image item");
if (this.configContentLightLevel is not null
&& itemObuContentLightLevel is not null
&& !ContentLightLevelsMatch(this.configContentLightLevel.Value, itemObuContentLightLevel.Value))
@ -226,9 +252,17 @@ internal sealed class Av1CodecConfiguration
throw new InvalidImageContentException("The AV1 codec configuration and image item contain conflicting content light-level metadata.");
}
if (this.configMasteringDisplayColorVolume is not null
&& itemObuMasteringDisplayColorVolume is not null
&& this.configMasteringDisplayColorVolume.Value != itemObuMasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException("The AV1 codec configuration and image item contain conflicting mastering-display 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;
contentLightLevel = itemObuContentLightLevel ?? this.configContentLightLevel;
masteringDisplayColorVolume = itemObuMasteringDisplayColorVolume ?? this.configMasteringDisplayColorVolume;
}
/// <summary>
@ -287,6 +321,9 @@ internal sealed class Av1CodecConfiguration
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the sequence, or <see langword="null"/> when absent.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the sequence, or <see langword="null"/> when absent.
/// </param>
/// <returns>The number of sequence-header OBUs in the sequence.</returns>
private static int ScanObus(
ReadOnlySpan<byte> data,
@ -296,12 +333,14 @@ internal sealed class Av1CodecConfiguration
out int sequenceHeaderOffset,
out int sequenceHeaderLength,
out int sequenceHeaderExtension,
out HeifContentLightLevel? contentLightLevel)
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
sequenceHeaderOffset = -1;
sequenceHeaderLength = 0;
sequenceHeaderExtension = -1;
contentLightLevel = null;
masteringDisplayColorVolume = null;
int sequenceHeaderCount = 0;
int obuIndex = 0;
int offset = 0;
@ -370,9 +409,11 @@ internal sealed class Av1CodecConfiguration
}
else if (type == ObuType.Metadata)
{
HeifContentLightLevel? obuContentLightLevel = ReadContentLightLevelMetadata(
ReadHdrMetadata(
data.Slice(offset, payloadLength),
sourceName);
sourceName,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
if (obuContentLightLevel is not null)
{
@ -384,6 +425,17 @@ internal sealed class Av1CodecConfiguration
contentLightLevel = obuContentLightLevel;
}
if (obuMasteringDisplayColorVolume is not null)
{
if (masteringDisplayColorVolume is not null
&& masteringDisplayColorVolume.Value != obuMasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException($"The {sourceName} contains conflicting mastering-display metadata OBUs.");
}
masteringDisplayColorVolume = obuMasteringDisplayColorVolume;
}
}
offset += payloadLength;
@ -412,32 +464,80 @@ internal sealed class Av1CodecConfiguration
}
/// <summary>
/// Reads content light-level data from an AV1 metadata OBU payload.
/// Reads still-image high-dynamic-range data from an AV1 metadata OBU payload.
/// </summary>
/// <param name="payload">The bounded metadata OBU payload.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <returns>
/// The decoded content light-level metadata, or <see langword="null"/> when the OBU carries another metadata type.
/// </returns>
private static HeifContentLightLevel? ReadContentLightLevelMetadata(
/// <param name="contentLightLevel">Receives decoded content light-level metadata when present.</param>
/// <param name="masteringDisplayColorVolume">Receives decoded mastering-display metadata when present.</param>
private static void ReadHdrMetadata(
ReadOnlySpan<byte> payload,
string sourceName)
string sourceName,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
contentLightLevel = null;
masteringDisplayColorVolume = null;
int offset = 0;
ulong metadataType = ReadLeb128(payload, ref offset, sourceName, "metadata type");
if (metadataType != (ulong)ObuMetadataType.HdrCll)
if (metadataType != (ulong)ObuMetadataType.HdrCll
&& metadataType != (ulong)ObuMetadataType.HdrMdcv)
{
return null;
return;
}
const int contentLightLevelLength = 4;
if (payload.Length - offset <= contentLightLevelLength)
int metadataLength = metadataType == (ulong)ObuMetadataType.HdrCll ? 4 : 24;
if (payload.Length - offset <= metadataLength)
{
throw new InvalidImageContentException($"The {sourceName} contains truncated content light-level metadata or no trailing bits.");
throw new InvalidImageContentException($"The {sourceName} contains truncated HDR metadata or no trailing bits.");
}
ReadOnlySpan<byte> metadataData = payload.Slice(offset, metadataLength);
ValidateByteAlignedMetadataTrailingBits(payload[(offset + metadataLength)..], sourceName);
if (metadataType == (ulong)ObuMetadataType.HdrCll)
{
contentLightLevel = new HeifContentLightLevel(
BinaryPrimitives.ReadUInt16BigEndian(metadataData),
BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]));
return;
}
ReadOnlySpan<byte> contentLightLevelData = payload.Slice(offset, contentLightLevelLength);
ReadOnlySpan<byte> trailingData = payload[(offset + contentLightLevelLength)..];
const float chromaticityScale = 1F / 65536F;
const double maximumLuminanceScale = 1D / 256D;
const double minimumLuminanceScale = 1D / 16384D;
// AV1 stores the primaries in R, G, B order and uses codec-specific fixed-point units that differ from the
// ISOBMFF mdcv property. Decode both representations to the same observable ImageSharp color coordinates.
CieXyChromaticityCoordinates redPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]) * chromaticityScale);
CieXyChromaticityCoordinates greenPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[4..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[6..]) * chromaticityScale);
CieXyChromaticityCoordinates bluePrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[8..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[10..]) * chromaticityScale);
masteringDisplayColorVolume = new HeifMasteringDisplayColorVolume(
new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary),
new CieXyChromaticityCoordinates(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[12..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[14..]) * chromaticityScale),
BinaryPrimitives.ReadUInt32BigEndian(metadataData[16..]) * maximumLuminanceScale,
BinaryPrimitives.ReadUInt32BigEndian(metadataData[20..]) * minimumLuminanceScale);
}
/// <summary>
/// Validates the trailing bits of byte-aligned fixed-length AV1 metadata.
/// </summary>
/// <param name="trailingData">The metadata payload bytes following its fixed fields.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
private static void ValidateByteAlignedMetadataTrailingBits(ReadOnlySpan<byte> trailingData, string sourceName)
{
byte lastNonzeroByte = 0;
for (int i = trailingData.Length - 1; i >= 0; i--)
{
@ -448,16 +548,12 @@ internal sealed class Av1CodecConfiguration
}
}
// HDR CLL fields end on a byte boundary. libaom accepts zero padding after the required 0x80 trailing byte,
// Both fixed HDR structures end on a byte boundary. libaom accepts zero padding after the required 0x80 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.");
throw new InvalidImageContentException($"The {sourceName} HDR metadata has invalid trailing bits.");
}
return new HeifContentLightLevel(
BinaryPrimitives.ReadUInt16BigEndian(contentLightLevelData),
BinaryPrimitives.ReadUInt16BigEndian(contentLightLevelData[2..]));
}
/// <summary>
@ -523,4 +619,71 @@ internal sealed class Av1CodecConfiguration
return left.MaximumContentLightLevel == right.MaximumContentLightLevel
&& left.MaximumPictureAverageLightLevel == right.MaximumPictureAverageLightLevel;
}
/// <summary>
/// Validates mastering-display metadata against the corresponding image-item property when both are present.
/// </summary>
/// <param name="obuColorVolume">The value carried by an AV1 metadata OBU.</param>
/// <param name="itemColorVolume">The value carried by the associated image-item property.</param>
/// <param name="sourceName">The OBU source description used by invalid-content errors.</param>
private static void ValidateMasteringDisplayColorVolume(
HeifMasteringDisplayColorVolume? obuColorVolume,
HeifMasteringDisplayColorVolume? itemColorVolume,
string sourceName)
{
if (obuColorVolume is not null
&& itemColorVolume is not null
&& !MasteringDisplayColorVolumesMatch(obuColorVolume.Value, itemColorVolume.Value))
{
throw new InvalidImageContentException($"The {sourceName} mastering-display metadata does not match the image-item property.");
}
}
/// <summary>
/// Determines whether AV1 and ISOBMFF mastering-display values agree within their fixed-point precision.
/// </summary>
/// <param name="obuColorVolume">The mastering-display values decoded from the AV1 representation.</param>
/// <param name="itemColorVolume">The mastering-display values decoded from the ISOBMFF representation.</param>
/// <returns><see langword="true"/> when all decoded values agree within their combined quantization error.</returns>
private static bool MasteringDisplayColorVolumesMatch(
HeifMasteringDisplayColorVolume obuColorVolume,
HeifMasteringDisplayColorVolume itemColorVolume)
{
const float chromaticityTolerance = ((1F / 65536F) + (1F / 50000F)) / 2F;
const double maximumLuminanceTolerance = ((1D / 256D) + (1D / 10000D)) / 2D;
const double minimumLuminanceTolerance = ((1D / 16384D) + (1D / 10000D)) / 2D;
return ChromaticitiesMatch(obuColorVolume.Primaries.R, itemColorVolume.Primaries.R, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.Primaries.G, itemColorVolume.Primaries.G, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.Primaries.B, itemColorVolume.Primaries.B, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.WhitePoint, itemColorVolume.WhitePoint, chromaticityTolerance)
&& ValuesMatch(obuColorVolume.MaximumLuminance, itemColorVolume.MaximumLuminance, maximumLuminanceTolerance)
&& ValuesMatch(obuColorVolume.MinimumLuminance, itemColorVolume.MinimumLuminance, minimumLuminanceTolerance);
}
/// <summary>
/// Determines whether two chromaticity-coordinate pairs agree within the supplied fixed-point tolerance.
/// </summary>
/// <param name="left">The first chromaticity-coordinate pair.</param>
/// <param name="right">The second chromaticity-coordinate pair.</param>
/// <param name="tolerance">The maximum permitted difference on either coordinate axis.</param>
/// <returns><see langword="true"/> when both coordinate differences are within the tolerance.</returns>
private static bool ChromaticitiesMatch(
CieXyChromaticityCoordinates left,
CieXyChromaticityCoordinates right,
float tolerance)
{
return ValuesMatch(left.X, right.X, tolerance)
&& ValuesMatch(left.Y, right.Y, tolerance);
}
/// <summary>
/// Determines whether two decoded fixed-point values agree within the supplied tolerance.
/// </summary>
/// <param name="left">The first decoded value.</param>
/// <param name="right">The second decoded value.</param>
/// <param name="tolerance">The maximum permitted absolute difference.</param>
/// <returns><see langword="true"/> when the absolute difference does not exceed the tolerance.</returns>
private static bool ValuesMatch(double left, double right, double tolerance)
=> Math.Abs(left - right) <= tolerance;
}

8
src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs

@ -54,9 +54,12 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
}
}
HeifContentLightLevel? obuContentLightLevel = codecConfiguration.ValidateItemData(
codecConfiguration.ValidateItemData(
data,
item.ContentLightLevel);
item.ContentLightLevel,
item.MasteringDisplayColorVolume,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
Av1Decoder decoder = new(configuration);
Image<TPixel> image = decoder.Decode<TPixel>(data, colorProfile, codecConfiguration);
@ -65,6 +68,7 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
metadata.BitDepth = codecConfiguration.BitDepth;
metadata.IsMonochrome = codecConfiguration.IsMonochrome;
metadata.ContentLightLevel = item.ContentLightLevel ?? obuContentLightLevel;
metadata.MasteringDisplayColorVolume = item.MasteringDisplayColorVolume ?? obuMasteringDisplayColorVolume;
return image;
}
}

5
src/ImageSharp/Formats/Heif/Heif4CharCode.cs

@ -78,6 +78,11 @@ public enum Heif4CharCode : uint
/// </summary>
Clli = 0x636C6C69U,
/// <summary>
/// Mastering display colour volume.
/// </summary>
Mdcv = 0x6D646376U,
/// <summary>
/// HVC configuration.
/// </summary>

1
src/ImageSharp/Formats/Heif/Heif4CharCode.tt

@ -20,6 +20,7 @@
"altt", "Alternative text",
"colr", "Colour information",
"clli", "Content light level information",
"mdcv", "Mastering display colour volume",
"hvcC", "HVC configuration",
"av1C", "AV1 configuration",
"imir", "Image Mirror",

54
src/ImageSharp/Formats/Heif/HeifDecoderCore.cs

@ -4,6 +4,7 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.IO;
@ -972,6 +973,42 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer),
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[2..]))));
break;
case Heif4CharCode.Mdcv:
EnsureBufferRemaining(boxBuffer, 0, 24, "mastering display color volume");
if (boxBuffer.Length != 24)
{
throw new InvalidImageContentException("The mastering display color-volume property has an invalid length.");
}
const float chromaticityScale = 1F / 50000F;
const double luminanceScale = 1D / 10000D;
// The registered mastering-display payload inherits the G, B, R primary order used by its
// mastering-display source syntax. Reorder it into ImageSharp's existing RGB coordinate type.
CieXyChromaticityCoordinates greenPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[2..]) * chromaticityScale);
CieXyChromaticityCoordinates bluePrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[4..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[6..]) * chromaticityScale);
CieXyChromaticityCoordinates redPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[8..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[10..]) * chromaticityScale);
properties.Add(
new KeyValuePair<Heif4CharCode, object>(
Heif4CharCode.Mdcv,
new HeifMasteringDisplayColorVolume(
new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary),
new CieXyChromaticityCoordinates(
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[12..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[14..]) * chromaticityScale),
BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[16..]) * luminanceScale,
BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[20..]) * luminanceScale)));
break;
case Heif4CharCode.Av1C:
EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration");
@ -1192,6 +1229,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
item.ContentLightLevel = (HeifContentLightLevel)prop.Value;
break;
case Heif4CharCode.Mdcv:
if (item.MasteringDisplayColorVolume is not null)
{
throw new InvalidImageContentException($"Item {itemId} associates more than one mastering display color-volume property.");
}
item.MasteringDisplayColorVolume = (HeifMasteringDisplayColorVolume)prop.Value;
break;
case Heif4CharCode.Clap:
if (item.CleanAperture is not null)
{
@ -1540,6 +1585,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
// 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;
meta.MasteringDisplayColorVolume = null;
}
return image;
@ -1596,6 +1642,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
{
metadata.GetHeifMetadata().ContentLightLevel = contentLightLevel;
}
HeifMasteringDisplayColorVolume? masteringDisplayColorVolume = imageItem.MasteringDisplayColorVolume
?? gridTile?.MasteringDisplayColorVolume;
if (masteringDisplayColorVolume is not null)
{
metadata.GetHeifMetadata().MasteringDisplayColorVolume = masteringDisplayColorVolume;
}
}
/// <summary>

6
src/ImageSharp/Formats/Heif/HeifItem.cs

@ -72,6 +72,12 @@ internal class HeifItem(Heif4CharCode type, uint id)
/// </summary>
public HeifContentLightLevel? ContentLightLevel { get; set; }
/// <summary>
/// Gets or sets the mastering-display color volume associated with this image item, or <see langword="null"/>
/// when the item has no mastering-display property.
/// </summary>
public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; set; }
/// <summary>
/// Gets or sets the AV1 codec configuration associated with this coded image item, or <see langword="null"/>
/// when the item has no AV1 codec-configuration property.

84
src/ImageSharp/Formats/Heif/HeifMasteringDisplayColorVolume.cs

@ -0,0 +1,84 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Describes the color volume of the display used to master a HEIF image.
/// </summary>
public readonly struct HeifMasteringDisplayColorVolume : IEquatable<HeifMasteringDisplayColorVolume>
{
/// <summary>
/// Initializes a new instance of the <see cref="HeifMasteringDisplayColorVolume"/> struct.
/// </summary>
/// <param name="primaries">The CIE 1931 chromaticity coordinates of the mastering display primaries.</param>
/// <param name="whitePoint">The CIE 1931 chromaticity coordinates of the mastering display white point.</param>
/// <param name="maximumLuminance">The nominal maximum mastering-display luminance in candelas per square metre.</param>
/// <param name="minimumLuminance">The nominal minimum mastering-display luminance in candelas per square metre.</param>
public HeifMasteringDisplayColorVolume(
RgbPrimariesChromaticityCoordinates primaries,
CieXyChromaticityCoordinates whitePoint,
double maximumLuminance,
double minimumLuminance)
{
this.Primaries = primaries;
this.WhitePoint = whitePoint;
this.MaximumLuminance = maximumLuminance;
this.MinimumLuminance = minimumLuminance;
}
/// <summary>
/// Gets the CIE 1931 chromaticity coordinates of the mastering display primaries.
/// </summary>
public RgbPrimariesChromaticityCoordinates Primaries { get; }
/// <summary>
/// Gets the CIE 1931 chromaticity coordinates of the mastering display white point.
/// </summary>
public CieXyChromaticityCoordinates WhitePoint { get; }
/// <summary>
/// Gets the nominal maximum mastering-display luminance in candelas per square metre.
/// </summary>
public double MaximumLuminance { get; }
/// <summary>
/// Gets the nominal minimum mastering-display luminance in candelas per square metre.
/// </summary>
public double MinimumLuminance { get; }
/// <summary>
/// Compares two mastering-display color volumes for equality.
/// </summary>
/// <param name="left">The first mastering-display color volume.</param>
/// <param name="right">The second mastering-display color volume.</param>
/// <returns><see langword="true"/> when every color-volume value is equal.</returns>
public static bool operator ==(HeifMasteringDisplayColorVolume left, HeifMasteringDisplayColorVolume right)
=> left.Equals(right);
/// <summary>
/// Compares two mastering-display color volumes for inequality.
/// </summary>
/// <param name="left">The first mastering-display color volume.</param>
/// <param name="right">The second mastering-display color volume.</param>
/// <returns><see langword="true"/> when any color-volume value differs.</returns>
public static bool operator !=(HeifMasteringDisplayColorVolume left, HeifMasteringDisplayColorVolume right)
=> !left.Equals(right);
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is HeifMasteringDisplayColorVolume other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(HeifMasteringDisplayColorVolume other)
=> this.Primaries.Equals(other.Primaries)
&& this.WhitePoint.Equals(other.WhitePoint)
&& this.MaximumLuminance.Equals(other.MaximumLuminance)
&& this.MinimumLuminance.Equals(other.MinimumLuminance);
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.Primaries, this.WhitePoint, this.MaximumLuminance, this.MinimumLuminance);
}

7
src/ImageSharp/Formats/Heif/HeifMetadata.cs

@ -29,6 +29,7 @@ public class HeifMetadata : IFormatMetadata<HeifMetadata>
this.IsMonochrome = other.IsMonochrome;
this.HasAlpha = other.HasAlpha;
this.ContentLightLevel = other.ContentLightLevel;
this.MasteringDisplayColorVolume = other.MasteringDisplayColorVolume;
}
/// <summary>
@ -57,6 +58,12 @@ public class HeifMetadata : IFormatMetadata<HeifMetadata>
/// </summary>
public HeifContentLightLevel? ContentLightLevel { get; set; }
/// <summary>
/// Gets or sets the mastering-display color volume for the primary image, or <see langword="null"/> when it is
/// not available.
/// </summary>
public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; set; }
/// <inheritdoc/>
public static HeifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => new()
{

Loading…
Cancel
Save