mirror of https://github.com/SixLabors/ImageSharp
5 changed files with 926 additions and 8 deletions
@ -0,0 +1,349 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.ColorProfiles; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the presentation and exposed metadata carried by prefix SEI NAL units for one bounded still picture.
|
||||
|
/// </summary>
|
||||
|
internal sealed class HevcSupplementalEnhancementInformation |
||||
|
{ |
||||
|
private const int DisplayOrientationPayloadType = 47; |
||||
|
private const int MasteringDisplayColorVolumePayloadType = 137; |
||||
|
private const int NoDisplayPayloadType = 135; |
||||
|
private const int ContentLightLevelPayloadType = 144; |
||||
|
private const int AlternativeTransferCharacteristicsPayloadType = 147; |
||||
|
private const int AmbientViewingEnvironmentPayloadType = 148; |
||||
|
private const int ContentColorVolumePayloadType = 149; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the selected still picture is marked as unavailable for display.
|
||||
|
/// </summary>
|
||||
|
public bool NoDisplay { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether an active display-orientation message is present.
|
||||
|
/// </summary>
|
||||
|
public bool HasDisplayOrientation { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the cropped decoded picture is flipped horizontally before rotation.
|
||||
|
/// </summary>
|
||||
|
public bool HorizontalFlip { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the cropped decoded picture is flipped vertically before rotation.
|
||||
|
/// </summary>
|
||||
|
public bool VerticalFlip { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the unsigned fraction of one complete anticlockwise turn applied after flipping.
|
||||
|
/// </summary>
|
||||
|
public ushort AnticlockwiseRotation { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the preferred CICP transfer-characteristics code, when signaled.
|
||||
|
/// </summary>
|
||||
|
public byte? PreferredTransferCharacteristics { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the content light-level description, when signaled.
|
||||
|
/// </summary>
|
||||
|
public HeifContentLightLevel? ContentLightLevel { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the mastering-display color volume, when signaled.
|
||||
|
/// </summary>
|
||||
|
public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the content color volume, when signaled and not cancelled.
|
||||
|
/// </summary>
|
||||
|
public HeifContentColorVolume? ContentColorVolume { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the ambient viewing environment, when signaled.
|
||||
|
/// </summary>
|
||||
|
public HeifAmbientViewingEnvironment? AmbientViewingEnvironment { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads every byte-aligned message from one prefix SEI RBSP in bitstream order.
|
||||
|
/// </summary>
|
||||
|
/// <param name="rbsp">The decoded NAL payload, including its RBSP trailing byte.</param>
|
||||
|
public void ReadPrefixNalUnit(ReadOnlySpan<byte> rbsp) |
||||
|
{ |
||||
|
if (rbsp.IsEmpty) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC prefix SEI NAL unit is missing RBSP trailing bits."); |
||||
|
} |
||||
|
|
||||
|
int offset = 0; |
||||
|
while (rbsp.Length - offset > 1) |
||||
|
{ |
||||
|
int payloadType = ReadExtendedValue(rbsp, ref offset, "payload type"); |
||||
|
int payloadSize = ReadExtendedValue(rbsp, ref offset, "payload size"); |
||||
|
if (payloadSize > rbsp.Length - offset) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC prefix SEI message payload is truncated."); |
||||
|
} |
||||
|
|
||||
|
ReadOnlySpan<byte> payload = rbsp.Slice(offset, payloadSize); |
||||
|
offset += payloadSize; |
||||
|
switch (payloadType) |
||||
|
{ |
||||
|
case DisplayOrientationPayloadType: |
||||
|
this.ReadDisplayOrientation(payload); |
||||
|
break; |
||||
|
case NoDisplayPayloadType: |
||||
|
this.ReadNoDisplay(payload); |
||||
|
break; |
||||
|
case MasteringDisplayColorVolumePayloadType: |
||||
|
this.ReadMasteringDisplayColorVolume(payload); |
||||
|
break; |
||||
|
case ContentLightLevelPayloadType: |
||||
|
this.ReadContentLightLevel(payload); |
||||
|
break; |
||||
|
case AlternativeTransferCharacteristicsPayloadType: |
||||
|
this.ReadAlternativeTransferCharacteristics(payload); |
||||
|
break; |
||||
|
case AmbientViewingEnvironmentPayloadType: |
||||
|
this.ReadAmbientViewingEnvironment(payload); |
||||
|
break; |
||||
|
case ContentColorVolumePayloadType: |
||||
|
this.ReadContentColorVolume(payload); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (offset != rbsp.Length - 1 || rbsp[offset] != 0x80) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC prefix SEI NAL unit has invalid RBSP trailing bits."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the legacy HEVC display-orientation payload retained by pinned HM.
|
||||
|
/// </summary>
|
||||
|
private void ReadDisplayOrientation(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
HevcBitReader reader = new(payload); |
||||
|
bool cancel = reader.ReadFlag(); |
||||
|
if (cancel) |
||||
|
{ |
||||
|
this.HasDisplayOrientation = false; |
||||
|
this.HorizontalFlip = false; |
||||
|
this.VerticalFlip = false; |
||||
|
this.AnticlockwiseRotation = 0; |
||||
|
ValidatePayloadExtension(ref reader, "display orientation"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.HorizontalFlip = reader.ReadFlag(); |
||||
|
this.VerticalFlip = reader.ReadFlag(); |
||||
|
this.AnticlockwiseRotation = (ushort)reader.ReadBits(16); |
||||
|
_ = reader.ReadFlag(); |
||||
|
ValidatePayloadExtension(ref reader, "display orientation"); |
||||
|
this.HasDisplayOrientation = true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Records that the selected picture is not intended for display.
|
||||
|
/// </summary>
|
||||
|
private void ReadNoDisplay(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
// Pinned HM writes no syntax bits for this message, producing a zero-byte payload. A nonempty payload can
|
||||
|
// contain only the generic reserved extension and payload-alignment marker handled by the shared validator.
|
||||
|
ValidateByteAlignedPayloadExtension(payload, "no-display"); |
||||
|
this.NoDisplay = true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads mastering-display metadata in its HEVC fixed-point representation.
|
||||
|
/// </summary>
|
||||
|
private void ReadMasteringDisplayColorVolume(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
const int syntaxLength = 24; |
||||
|
if (payload.Length < syntaxLength) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC mastering-display color-volume SEI payload is truncated."); |
||||
|
} |
||||
|
|
||||
|
this.MasteringDisplayColorVolume = HeifPropertyParser.ParseMasteringDisplayColorVolume(payload[..syntaxLength]); |
||||
|
ValidateByteAlignedPayloadExtension(payload[syntaxLength..], "mastering-display color-volume"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads content light-level metadata in its HEVC fixed-width representation.
|
||||
|
/// </summary>
|
||||
|
private void ReadContentLightLevel(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
const int syntaxLength = 4; |
||||
|
if (payload.Length < syntaxLength) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC content light-level SEI payload is truncated."); |
||||
|
} |
||||
|
|
||||
|
this.ContentLightLevel = HeifPropertyParser.ParseContentLightLevel(payload[..syntaxLength]); |
||||
|
ValidateByteAlignedPayloadExtension(payload[syntaxLength..], "content light-level"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the preferred transfer function applied when the container does not provide one.
|
||||
|
/// </summary>
|
||||
|
private void ReadAlternativeTransferCharacteristics(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
if (payload.IsEmpty) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC alternative-transfer-characteristics SEI payload is truncated."); |
||||
|
} |
||||
|
|
||||
|
this.PreferredTransferCharacteristics = payload[0]; |
||||
|
ValidateByteAlignedPayloadExtension(payload[1..], "alternative transfer characteristics"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the nominal ambient viewing environment.
|
||||
|
/// </summary>
|
||||
|
private void ReadAmbientViewingEnvironment(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
const int syntaxLength = 8; |
||||
|
if (payload.Length < syntaxLength) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC ambient-viewing-environment SEI payload is truncated."); |
||||
|
} |
||||
|
|
||||
|
this.AmbientViewingEnvironment = HeifPropertyParser.ParseAmbientViewingEnvironment(payload[..syntaxLength]); |
||||
|
ValidateByteAlignedPayloadExtension(payload[syntaxLength..], "ambient viewing environment"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the bit-packed content color-volume syntax and applies cancellation in message order.
|
||||
|
/// </summary>
|
||||
|
private void ReadContentColorVolume(ReadOnlySpan<byte> payload) |
||||
|
{ |
||||
|
HevcBitReader reader = new(payload); |
||||
|
bool cancel = reader.ReadFlag(); |
||||
|
if (cancel) |
||||
|
{ |
||||
|
this.ContentColorVolume = null; |
||||
|
ValidatePayloadExtension(ref reader, "content color-volume"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_ = reader.ReadFlag(); |
||||
|
bool primariesPresent = reader.ReadFlag(); |
||||
|
bool minimumLuminancePresent = reader.ReadFlag(); |
||||
|
bool maximumLuminancePresent = reader.ReadFlag(); |
||||
|
bool averageLuminancePresent = reader.ReadFlag(); |
||||
|
RgbPrimariesChromaticityCoordinates? primaries = null; |
||||
|
if (primariesPresent) |
||||
|
{ |
||||
|
int greenX = unchecked((int)reader.ReadBits(32)); |
||||
|
int greenY = unchecked((int)reader.ReadBits(32)); |
||||
|
int blueX = unchecked((int)reader.ReadBits(32)); |
||||
|
int blueY = unchecked((int)reader.ReadBits(32)); |
||||
|
int redX = unchecked((int)reader.ReadBits(32)); |
||||
|
int redY = unchecked((int)reader.ReadBits(32)); |
||||
|
const int maximumChromaticityValue = 5_000_000; |
||||
|
if (greenX is < -maximumChromaticityValue or > maximumChromaticityValue |
||||
|
|| greenY is < -maximumChromaticityValue or > maximumChromaticityValue |
||||
|
|| blueX is < -maximumChromaticityValue or > maximumChromaticityValue |
||||
|
|| blueY is < -maximumChromaticityValue or > maximumChromaticityValue |
||||
|
|| redX is < -maximumChromaticityValue or > maximumChromaticityValue |
||||
|
|| redY is < -maximumChromaticityValue or > maximumChromaticityValue) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC content color-volume SEI payload has an out-of-range primary coordinate."); |
||||
|
} |
||||
|
|
||||
|
const float chromaticityScale = 1F / 50000F; |
||||
|
|
||||
|
// H.274 stores signed primary coordinates in G, B, R order. Reorder them once at the codec boundary so
|
||||
|
// the retained value has the same observable RGB coordinate contract as the equivalent item property.
|
||||
|
primaries = new RgbPrimariesChromaticityCoordinates( |
||||
|
new CieXyChromaticityCoordinates(redX * chromaticityScale, redY * chromaticityScale), |
||||
|
new CieXyChromaticityCoordinates(greenX * chromaticityScale, greenY * chromaticityScale), |
||||
|
new CieXyChromaticityCoordinates(blueX * chromaticityScale, blueY * chromaticityScale)); |
||||
|
} |
||||
|
|
||||
|
uint? minimumLuminance = minimumLuminancePresent ? reader.ReadBits(32) : null; |
||||
|
uint? maximumLuminance = maximumLuminancePresent ? reader.ReadBits(32) : null; |
||||
|
uint? averageLuminance = averageLuminancePresent ? reader.ReadBits(32) : null; |
||||
|
if ((minimumLuminance is not null && averageLuminance is not null && minimumLuminance.Value > averageLuminance.Value) |
||||
|
|| (averageLuminance is not null && maximumLuminance is not null && averageLuminance.Value > maximumLuminance.Value) |
||||
|
|| (minimumLuminance is not null && maximumLuminance is not null && minimumLuminance.Value > maximumLuminance.Value)) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEVC content color-volume SEI luminance values are not in ascending order."); |
||||
|
} |
||||
|
|
||||
|
ValidatePayloadExtension(ref reader, "content color-volume"); |
||||
|
const double luminanceScale = 1D / 10000000D; |
||||
|
|
||||
|
this.ContentColorVolume = new HeifContentColorVolume( |
||||
|
primaries, |
||||
|
minimumLuminance * luminanceScale, |
||||
|
maximumLuminance * luminanceScale, |
||||
|
averageLuminance * luminanceScale); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads an extended SEI payload type or size whose continuation bytes are all 255.
|
||||
|
/// </summary>
|
||||
|
private static int ReadExtendedValue(ReadOnlySpan<byte> data, ref int offset, string valueName) |
||||
|
{ |
||||
|
int value = 0; |
||||
|
while (true) |
||||
|
{ |
||||
|
if ((uint)offset >= (uint)data.Length) |
||||
|
{ |
||||
|
throw new InvalidImageContentException($"The HEVC prefix SEI {valueName} is truncated."); |
||||
|
} |
||||
|
|
||||
|
int current = data[offset++]; |
||||
|
if (value > int.MaxValue - current) |
||||
|
{ |
||||
|
throw new InvalidImageContentException($"The HEVC prefix SEI {valueName} is too large."); |
||||
|
} |
||||
|
|
||||
|
value += current; |
||||
|
if (current != byte.MaxValue) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Validates an optional extension following fixed byte-aligned SEI syntax.
|
||||
|
/// </summary>
|
||||
|
private static void ValidateByteAlignedPayloadExtension(ReadOnlySpan<byte> extension, string payloadName) |
||||
|
{ |
||||
|
if (extension.IsEmpty) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
HevcBitReader reader = new(extension); |
||||
|
ValidatePayloadExtension(ref reader, payloadName); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Validates reserved payload-extension data followed by its final one bit and zero padding.
|
||||
|
/// </summary>
|
||||
|
private static void ValidatePayloadExtension(ref HevcBitReader reader, string payloadName) |
||||
|
{ |
||||
|
bool foundMarker = false; |
||||
|
while (reader.BitsRemaining > 0) |
||||
|
{ |
||||
|
foundMarker |= reader.ReadFlag(); |
||||
|
} |
||||
|
|
||||
|
// The final set bit is payload_bit_equal_to_one; any preceding bits are the reserved extension data that
|
||||
|
// pinned HM deliberately skips. An all-zero remainder has no marker and is therefore not a complete payload.
|
||||
|
if (!foundMarker) |
||||
|
{ |
||||
|
throw new InvalidImageContentException($"The HEVC {payloadName} SEI payload has invalid trailing bits."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue