Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CICP color metadata and PNG support #2592

Merged
merged 11 commits into from
Dec 1, 2023
6 changes: 6 additions & 0 deletions src/ImageSharp/Formats/Png/PngChunkType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ internal enum PngChunkType : uint
/// <remarks>cHRM (Single)</remarks>
Chroma = 0x6348524d,

/// <summary>
/// If this chunk is present, it specifies the color space, transfer function, matrix coefficients of the image
/// using the code points specified in [ITU-T-H.273]
/// </summary>
Cicp = 0x63494350,

/// <summary>
/// This chunk is an ancillary chunk as defined in the PNG Specification.
/// It must appear before the first IDAT chunk within a valid PNG stream.
Expand Down
33 changes: 33 additions & 0 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Memory.Internals;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
Expand Down Expand Up @@ -191,6 +192,9 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
case PngChunkType.Gamma:
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Cicp:
ReadCicpChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.FrameControl:
frameCount++;
if (frameCount == this.maxFrames)
Expand Down Expand Up @@ -360,6 +364,15 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat

ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Cicp:
if (this.colorMetadataOnly)
{
this.SkipChunkDataAndCrc(chunk);
break;
}

ReadCicpChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.FrameControl:
++frameCount;
if (frameCount == this.maxFrames)
Expand Down Expand Up @@ -1426,6 +1439,26 @@ private static bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string
return false;
}

/// <summary>
/// Reads the CICP color profile chunk.
/// </summary>
/// <param name="metadata">The metadata.</param>
/// <param name="data">The bytes containing the profile.</param>
private static void ReadCicpChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
{
if (data.Length < 4)
{
// Ignore invalid cICP chunks.
return;
}

byte colorPrimaries = data[0];
byte transferFunction = data[1];
byte matrixCoefficients = data[2];
bool? fullRange = data[3] == 1 ? true : data[3] == 0 ? false : null;
metadata.CicpProfile = new CicpProfile(colorPrimaries, transferFunction, matrixCoefficients, fullRange);
}

/// <summary>
/// Reads exif data encoded into a text chunk with the name "raw profile type exif".
/// This method was used by ImageMagick, exiftool, exiv2, digiKam, etc, before the
Expand Down
27 changes: 27 additions & 0 deletions src/ImageSharp/Formats/Png/PngEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken

this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
this.WriteCicpChunk(stream, metadata);
this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
Expand Down Expand Up @@ -852,6 +853,32 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta)
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}

/// <summary>
/// Writes the CICP profile chunk
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="metaData">The image meta data.</param>
private void WriteCicpChunk(Stream stream, ImageMetadata metaData)
{
if (metaData.CicpProfile is null)
{
return;
}

// by spec, the matrix coefficients must be set to Identity
if (metaData.CicpProfile.MatrixCoefficients != Metadata.Profiles.Cicp.CicpMatrixCoefficients.Identity)
{
throw new NotSupportedException("CICP matrix coefficients other than Identity are not supported in PNG");
}

Span<byte> outputBytes = this.chunkDataBuffer.Span[..4];
outputBytes[0] = (byte)metaData.CicpProfile.ColorPrimaries;
outputBytes[1] = (byte)metaData.CicpProfile.TransferCharacteristics;
outputBytes[2] = (byte)metaData.CicpProfile.MatrixCoefficients;
outputBytes[3] = (byte)(metaData.CicpProfile.FullRange ? 1 : 0);
this.WriteChunk(stream, PngChunkType.Cicp, outputBytes);
}

/// <summary>
/// Writes the color profile chunk.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/ImageSharp/Metadata/ImageFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
Expand Down Expand Up @@ -43,6 +44,7 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile?.DeepClone();
this.CicpProfile = other.CicpProfile?.DeepClone();
}

/// <summary>
Expand All @@ -65,6 +67,11 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
/// </summary>
public IptcProfile? IptcProfile { get; set; }

/// <summary>
/// Gets or sets the CICP profile
/// </summary>
public CicpProfile? CicpProfile { get; set; }

/// <inheritdoc/>
public ImageFrameMetadata DeepClone() => new(this);

Expand Down
7 changes: 7 additions & 0 deletions src/ImageSharp/Metadata/ImageMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
Expand Down Expand Up @@ -68,6 +69,7 @@ private ImageMetadata(ImageMetadata other)
this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile?.DeepClone();
this.CicpProfile = other.CicpProfile?.DeepClone();

// NOTE: This clone is actually shallow but we share the same format
// instances for all images in the configuration.
Expand Down Expand Up @@ -157,6 +159,11 @@ public double VerticalResolution
/// </summary>
public IptcProfile? IptcProfile { get; set; }

/// <summary>
/// Gets or sets the CICP profile.
/// </summary>
public CicpProfile? CicpProfile { get; set; }

/// <summary>
/// Gets the original format, if any, the image was decode from.
/// </summary>
Expand Down
72 changes: 72 additions & 0 deletions src/ImageSharp/Metadata/Profiles/CICP/CicpProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp;

/// <summary>
/// Represents a Cicp profile as per ITU-T H.273 / ISO/IEC 23091-2_2019 providing access to color space information
/// </summary>
public sealed class CicpProfile : IDeepCloneable<CicpProfile>
{
/// <summary>
/// Initializes a new instance of the <see cref="CicpProfile"/> class.
/// </summary>
public CicpProfile()
: this(2, 2, 2, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CicpProfile"/> class.
/// </summary>
/// <param name="colorPrimaries">The color primaries as number according to ITU-T H.273 / ISO/IEC 23091-2_2019.</param>
/// <param name="transferCharacteristics">The transfer characteristics as number according to ITU-T H.273 / ISO/IEC 23091-2_2019.</param>
/// <param name="matrixCoefficients">The matrix coefficients as number according to ITU-T H.273 / ISO/IEC 23091-2_2019.</param>
/// <param name="fullRange">The full range flag, or null if unknown.</param>
public CicpProfile(byte colorPrimaries, byte transferCharacteristics, byte matrixCoefficients, bool? fullRange)
{
this.ColorPrimaries = Enum.IsDefined(typeof(CicpColorPrimaries), colorPrimaries) ? (CicpColorPrimaries)colorPrimaries : CicpColorPrimaries.Unspecified;
this.TransferCharacteristics = Enum.IsDefined(typeof(CicpTransferCharacteristics), transferCharacteristics) ? (CicpTransferCharacteristics)transferCharacteristics : CicpTransferCharacteristics.Unspecified;
this.MatrixCoefficients = Enum.IsDefined(typeof(CicpMatrixCoefficients), matrixCoefficients) ? (CicpMatrixCoefficients)matrixCoefficients : CicpMatrixCoefficients.Unspecified;
this.FullRange = fullRange ?? (this.MatrixCoefficients == CicpMatrixCoefficients.Identity);
}

/// <summary>
/// Initializes a new instance of the <see cref="CicpProfile"/> class
/// by making a copy from another CICP profile.
/// </summary>
/// <param name="other">The other CICP profile, where the clone should be made from.</param>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>>
private CicpProfile(CicpProfile other)
{
Guard.NotNull(other, nameof(other));

this.ColorPrimaries = other.ColorPrimaries;
this.TransferCharacteristics = other.TransferCharacteristics;
this.MatrixCoefficients = other.MatrixCoefficients;
this.FullRange = other.FullRange;
}

/// <summary>
/// Gets or sets the color primaries
/// </summary>
public CicpColorPrimaries ColorPrimaries { get; set; }

/// <summary>
/// Gets or sets the transfer characteristics
/// </summary>
public CicpTransferCharacteristics TransferCharacteristics { get; set; }

/// <summary>
/// Gets or sets the matrix coefficients
/// </summary>
public CicpMatrixCoefficients MatrixCoefficients { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the colors use the full numeric range
/// </summary>
public bool FullRange { get; set; }

/// <inheritdoc/>
public CicpProfile DeepClone() => new(this);
}
86 changes: 86 additions & 0 deletions src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpColorPrimaries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp;

#pragma warning disable CA1707 // Underscores in enum members

/// <summary>
/// Color primaries according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.1
/// </summary>
public enum CicpColorPrimaries : byte
{
/// <summary>
/// Rec. ITU-R BT.709-6
/// IEC 61966-2-1 sRGB or sYCC
/// IEC 61966-2-4
/// SMPTE RP 177 (1993) Annex B
/// </summary>
ItuRBt709_6 = 1,

/// <summary>
/// Image characteristics are unknown or are determined by the application.
/// </summary>
Unspecified = 2,

/// <summary>
/// Rec. ITU-R BT.470-6 System M (historical)
/// </summary>
ItuRBt470_6M = 4,

/// <summary>
/// Rec. ITU-R BT.601-7 625
/// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
/// </summary>
ItuRBt601_7_625 = 5,

/// <summary>
/// Rec. ITU-R BT.601-7 525
/// Rec. ITU-R BT.1700-0 NTSC
/// SMPTE ST 170 (2004)
/// (functionally the same as the value 7)
/// </summary>
ItuRBt601_7_525 = 6,

/// <summary>
/// SMPTE ST 240 (1999)
/// (functionally the same as the value 6)
/// </summary>
SmpteSt240 = 7,

/// <summary>
/// Generic film (colour filters using Illuminant C)
/// </summary>
GenericFilm = 8,

/// <summary>
/// Rec. ITU-R BT.2020-2
/// Rec. ITU-R BT.2100-2
/// </summary>
ItuRBt2020_2 = 9,

/// <summary>
/// SMPTE ST 428-1 (2019)
/// (CIE 1931 XYZ as in ISO 11664-1)
/// </summary>
SmpteSt428_1 = 10,

/// <summary>
/// SMPTE RP 431-2 (2011)
/// DCI P3
/// </summary>
SmpteRp431_2 = 11,

/// <summary>
/// SMPTE ST 432-1 (2010)
/// P3 D65 / Display P3
/// </summary>
SmpteEg432_1 = 12,

/// <summary>
/// EBU Tech.3213-E
/// </summary>
EbuTech3213E = 22,
}

#pragma warning restore CA1707 // Underscores in enum members