mirror of https://github.com/SixLabors/ImageSharp
7 changed files with 761 additions and 352 deletions
@ -0,0 +1,369 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers.Binary; |
||||
|
using SixLabors.ImageSharp.ColorProfiles; |
||||
|
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
||||
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses the image properties shared by HEIF items and image-sequence sample entries.
|
||||
|
/// </summary>
|
||||
|
internal static class HeifPropertyParser |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Parses relative horizontal and vertical pixel spacing.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete pixel-aspect-ratio payload.</param>
|
||||
|
/// <returns>The validated relative pixel spacing.</returns>
|
||||
|
public static HeifPixelAspectRatio ParsePixelAspectRatio(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 8, "pixel aspect ratio"); |
||||
|
uint horizontalSpacing = BinaryPrimitives.ReadUInt32BigEndian(data); |
||||
|
uint verticalSpacing = BinaryPrimitives.ReadUInt32BigEndian(data[4..]); |
||||
|
if (horizontalSpacing == 0 || verticalSpacing == 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The pixel aspect ratio property has zero spacing."); |
||||
|
} |
||||
|
|
||||
|
return new HeifPixelAspectRatio(horizontalSpacing, verticalSpacing); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses and validates an embedded ICC profile.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete ICC profile bytes.</param>
|
||||
|
/// <returns>The validated ICC profile.</returns>
|
||||
|
public static IccProfile ParseIccProfile(byte[] data) |
||||
|
{ |
||||
|
if (data.Length == 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEIF ICC color property contains an empty profile."); |
||||
|
} |
||||
|
|
||||
|
IccProfile profile = new(data); |
||||
|
if (!profile.CheckIsValid()) |
||||
|
{ |
||||
|
throw new InvalidIccProfileException("Invalid HEIF ICC profile."); |
||||
|
} |
||||
|
|
||||
|
return profile; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a CICP color description from an <c>nclx</c> color-information payload.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete payload following the <c>nclx</c> color type.</param>
|
||||
|
/// <returns>The CICP color description.</returns>
|
||||
|
public static CicpProfile ParseCicpProfile(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 7, "CICP color information"); |
||||
|
ushort colorPrimaries = BinaryPrimitives.ReadUInt16BigEndian(data); |
||||
|
ushort transferCharacteristics = BinaryPrimitives.ReadUInt16BigEndian(data[2..]); |
||||
|
ushort matrixCoefficients = BinaryPrimitives.ReadUInt16BigEndian(data[4..]); |
||||
|
byte rangeAndReserved = data[6]; |
||||
|
if ((rangeAndReserved & 0x7F) != 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The HEIF CICP color property has nonzero reserved bits."); |
||||
|
} |
||||
|
|
||||
|
// The box fields are 16-bit so future registrations remain representable. ImageSharp's CICP profile exposes
|
||||
|
// the currently registered byte-sized H.273 values and maps larger future values to unspecified.
|
||||
|
byte colorPrimariesValue = colorPrimaries <= byte.MaxValue ? (byte)colorPrimaries : (byte)CicpColorPrimaries.Unspecified; |
||||
|
byte transferCharacteristicsValue = transferCharacteristics <= byte.MaxValue |
||||
|
? (byte)transferCharacteristics |
||||
|
: (byte)CicpTransferCharacteristics.Unspecified; |
||||
|
|
||||
|
byte matrixCoefficientsValue = matrixCoefficients <= byte.MaxValue |
||||
|
? (byte)matrixCoefficients |
||||
|
: (byte)CicpMatrixCoefficients.Unspecified; |
||||
|
|
||||
|
return new CicpProfile(colorPrimariesValue, transferCharacteristicsValue, matrixCoefficientsValue, (rangeAndReserved & 0x80) != 0); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses content light-level information.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete content-light-level payload.</param>
|
||||
|
/// <returns>The content light-level information.</returns>
|
||||
|
public static HeifContentLightLevel ParseContentLightLevel(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 4, "content light level"); |
||||
|
return new HeifContentLightLevel(BinaryPrimitives.ReadUInt16BigEndian(data), BinaryPrimitives.ReadUInt16BigEndian(data[2..])); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses mastering-display color-volume information.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete mastering-display color-volume payload.</param>
|
||||
|
/// <returns>The mastering-display color-volume information.</returns>
|
||||
|
public static HeifMasteringDisplayColorVolume ParseMasteringDisplayColorVolume(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 24, "mastering display color volume"); |
||||
|
const float chromaticityScale = 1F / 50000F; |
||||
|
const double luminanceScale = 1D / 10000D; |
||||
|
|
||||
|
// The registered payload inherits the G, B, R primary order used by its mastering-display source syntax.
|
||||
|
// Reorder it into ImageSharp's existing RGB coordinate type at the shared container boundary.
|
||||
|
CieXyChromaticityCoordinates greenPrimary = new( |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data) * chromaticityScale, |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[2..]) * chromaticityScale); |
||||
|
|
||||
|
CieXyChromaticityCoordinates bluePrimary = new( |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[4..]) * chromaticityScale, |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[6..]) * chromaticityScale); |
||||
|
|
||||
|
CieXyChromaticityCoordinates redPrimary = new( |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[8..]) * chromaticityScale, |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[10..]) * chromaticityScale); |
||||
|
|
||||
|
return new HeifMasteringDisplayColorVolume( |
||||
|
new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary), |
||||
|
new CieXyChromaticityCoordinates( |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[12..]) * chromaticityScale, |
||||
|
BinaryPrimitives.ReadUInt16BigEndian(data[14..]) * chromaticityScale), |
||||
|
BinaryPrimitives.ReadUInt32BigEndian(data[16..]) * luminanceScale, |
||||
|
BinaryPrimitives.ReadUInt32BigEndian(data[20..]) * luminanceScale); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses content color-volume information.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete content color-volume payload.</param>
|
||||
|
/// <returns>The content color-volume information.</returns>
|
||||
|
public static HeifContentColorVolume ParseContentColorVolume(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
if (data.IsEmpty) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The content color-volume property is truncated."); |
||||
|
} |
||||
|
|
||||
|
byte flags = data[0]; |
||||
|
if ((flags & 0xC3) != 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The content color-volume property has nonzero reserved flags."); |
||||
|
} |
||||
|
|
||||
|
bool primariesPresent = (flags & 0x20) != 0; |
||||
|
bool minimumLuminancePresent = (flags & 0x10) != 0; |
||||
|
bool maximumLuminancePresent = (flags & 0x08) != 0; |
||||
|
bool averageLuminancePresent = (flags & 0x04) != 0; |
||||
|
if (!primariesPresent && !minimumLuminancePresent && !maximumLuminancePresent && !averageLuminancePresent) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The content color-volume property does not describe any values."); |
||||
|
} |
||||
|
|
||||
|
int expectedLength = 1 |
||||
|
+ (primariesPresent ? 24 : 0) |
||||
|
+ (minimumLuminancePresent ? 4 : 0) |
||||
|
+ (maximumLuminancePresent ? 4 : 0) |
||||
|
+ (averageLuminancePresent ? 4 : 0); |
||||
|
|
||||
|
EnsureExactLength(data, expectedLength, "content color volume"); |
||||
|
int offset = 1; |
||||
|
RgbPrimariesChromaticityCoordinates? primaries = null; |
||||
|
if (primariesPresent) |
||||
|
{ |
||||
|
int greenX = BinaryPrimitives.ReadInt32BigEndian(data[offset..]); |
||||
|
int greenY = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 4)..]); |
||||
|
int blueX = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 8)..]); |
||||
|
int blueY = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 12)..]); |
||||
|
int redX = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 16)..]); |
||||
|
int redY = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 20)..]); |
||||
|
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 content color-volume property has an out-of-range primary coordinate."); |
||||
|
} |
||||
|
|
||||
|
const float chromaticityScale = 1F / 50000F; |
||||
|
|
||||
|
// Content-color-volume syntax also stores signed coordinates in G, B, R order.
|
||||
|
primaries = new RgbPrimariesChromaticityCoordinates( |
||||
|
new CieXyChromaticityCoordinates(redX * chromaticityScale, redY * chromaticityScale), |
||||
|
new CieXyChromaticityCoordinates(greenX * chromaticityScale, greenY * chromaticityScale), |
||||
|
new CieXyChromaticityCoordinates(blueX * chromaticityScale, blueY * chromaticityScale)); |
||||
|
|
||||
|
offset += 24; |
||||
|
} |
||||
|
|
||||
|
uint? minimumLuminance = minimumLuminancePresent ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : null; |
||||
|
offset += minimumLuminancePresent ? 4 : 0; |
||||
|
uint? maximumLuminance = maximumLuminancePresent ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : null; |
||||
|
offset += maximumLuminancePresent ? 4 : 0; |
||||
|
uint? averageLuminance = averageLuminancePresent ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : 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 content color-volume luminance values are not in ascending order."); |
||||
|
} |
||||
|
|
||||
|
const double luminanceScale = 1D / 10000000D; |
||||
|
|
||||
|
// These values are normalized according to the signaled transfer characteristics. Preserve that
|
||||
|
// unitless meaning instead of presenting them as physical display luminance.
|
||||
|
return new HeifContentColorVolume( |
||||
|
primaries, |
||||
|
minimumLuminance * luminanceScale, |
||||
|
maximumLuminance * luminanceScale, |
||||
|
averageLuminance * luminanceScale); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses an ambient viewing environment.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete ambient viewing-environment payload.</param>
|
||||
|
/// <returns>The ambient viewing environment.</returns>
|
||||
|
public static HeifAmbientViewingEnvironment ParseAmbientViewingEnvironment(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 8, "ambient viewing environment"); |
||||
|
uint illuminance = BinaryPrimitives.ReadUInt32BigEndian(data); |
||||
|
ushort lightX = BinaryPrimitives.ReadUInt16BigEndian(data[4..]); |
||||
|
ushort lightY = BinaryPrimitives.ReadUInt16BigEndian(data[6..]); |
||||
|
if (illuminance == 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The ambient viewing-environment property has zero illuminance."); |
||||
|
} |
||||
|
|
||||
|
if (lightX > 50000 || lightY > 50000) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The ambient viewing-environment property has an out-of-range chromaticity coordinate."); |
||||
|
} |
||||
|
|
||||
|
const double illuminanceScale = 1D / 10000D; |
||||
|
const float chromaticityScale = 1F / 50000F; |
||||
|
|
||||
|
// The property inherits H.274's fixed-point units: 0.0001 lux for illuminance and 0.00002 for each
|
||||
|
// normalized CIE chromaticity coordinate.
|
||||
|
return new HeifAmbientViewingEnvironment( |
||||
|
illuminance * illuminanceScale, |
||||
|
new CieXyChromaticityCoordinates(lightX * chromaticityScale, lightY * chromaticityScale)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a reference viewing environment.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete reference viewing-environment payload.</param>
|
||||
|
/// <returns>The reference viewing environment.</returns>
|
||||
|
public static HeifReferenceViewingEnvironment ParseReferenceViewingEnvironment(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 20, "reference viewing environment"); |
||||
|
if (BinaryPrimitives.ReadUInt32BigEndian(data) != 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The reference viewing-environment property has an unsupported version or flags."); |
||||
|
} |
||||
|
|
||||
|
ushort surroundX = BinaryPrimitives.ReadUInt16BigEndian(data[8..]); |
||||
|
ushort surroundY = BinaryPrimitives.ReadUInt16BigEndian(data[10..]); |
||||
|
ushort peripheryX = BinaryPrimitives.ReadUInt16BigEndian(data[16..]); |
||||
|
ushort peripheryY = BinaryPrimitives.ReadUInt16BigEndian(data[18..]); |
||||
|
if (surroundX > 10000 || surroundY > 10000 || peripheryX > 10000 || peripheryY > 10000) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The reference viewing-environment property has an out-of-range chromaticity coordinate."); |
||||
|
} |
||||
|
|
||||
|
const double luminanceScale = 1D / 10000D; |
||||
|
const float chromaticityScale = 1F / 10000F; |
||||
|
|
||||
|
// The full-box header is followed by display-surround and wider-periphery fields. Both groups use
|
||||
|
// 0.0001 increments, but luminance is physical cd/m2 while the CIE coordinates are normalized.
|
||||
|
return new HeifReferenceViewingEnvironment( |
||||
|
BinaryPrimitives.ReadUInt32BigEndian(data[4..]) * luminanceScale, |
||||
|
new CieXyChromaticityCoordinates(surroundX * chromaticityScale, surroundY * chromaticityScale), |
||||
|
BinaryPrimitives.ReadUInt32BigEndian(data[12..]) * luminanceScale, |
||||
|
new CieXyChromaticityCoordinates(peripheryX * chromaticityScale, peripheryY * chromaticityScale)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses nominal diffuse-white information.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete nominal diffuse-white payload.</param>
|
||||
|
/// <returns>The nominal diffuse-white information.</returns>
|
||||
|
public static HeifNominalDiffuseWhite ParseNominalDiffuseWhite(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 8, "nominal diffuse white"); |
||||
|
if (BinaryPrimitives.ReadUInt32BigEndian(data) != 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The nominal diffuse-white property has an unsupported version or flags."); |
||||
|
} |
||||
|
|
||||
|
uint luminance = BinaryPrimitives.ReadUInt32BigEndian(data[4..]); |
||||
|
const double luminanceScale = 1D / 10000D; |
||||
|
|
||||
|
// A zero coded value requests the standard-defined default rather than describing black diffuse white.
|
||||
|
return new HeifNominalDiffuseWhite(luminance == 0 ? null : luminance * luminanceScale); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a clean-aperture crop description.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete clean-aperture payload.</param>
|
||||
|
/// <returns>The clean-aperture crop description.</returns>
|
||||
|
public static HeifCleanAperture ParseCleanAperture(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 32, "clean aperture"); |
||||
|
return new HeifCleanAperture( |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[4..]), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[8..]), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[12..]), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[16..]), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[20..]), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[24..]), |
||||
|
BinaryPrimitives.ReadInt32BigEndian(data[28..])); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses the number of counter-clockwise quarter turns applied to an image.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete image-rotation payload.</param>
|
||||
|
/// <returns>The number of counter-clockwise quarter turns.</returns>
|
||||
|
public static byte ParseRotation(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 1, "image rotation"); |
||||
|
if ((data[0] & 0xFC) != 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The image rotation property has nonzero reserved bits."); |
||||
|
} |
||||
|
|
||||
|
return (byte)(data[0] & 3); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses the horizontal or vertical image-mirror axis.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete image-mirror payload.</param>
|
||||
|
/// <returns>Zero for the horizontal axis or one for the vertical axis.</returns>
|
||||
|
public static byte ParseMirrorAxis(ReadOnlySpan<byte> data) |
||||
|
{ |
||||
|
EnsureExactLength(data, 1, "image mirror"); |
||||
|
if ((data[0] & 0xFE) != 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The image mirror property has nonzero reserved bits."); |
||||
|
} |
||||
|
|
||||
|
return (byte)(data[0] & 1); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Requires a fixed-size image property to contain exactly its registered payload length.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The complete property payload.</param>
|
||||
|
/// <param name="length">The registered payload length.</param>
|
||||
|
/// <param name="name">The property name used in malformed-image diagnostics.</param>
|
||||
|
private static void EnsureExactLength(ReadOnlySpan<byte> data, int length, string name) |
||||
|
{ |
||||
|
if (data.Length != length) |
||||
|
{ |
||||
|
throw new InvalidImageContentException($"The {name} property has an invalid length."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue