Skip to content

Enable colorimetric normalization of JPEG image data during decode #2922

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 4, 2025
10 changes: 5 additions & 5 deletions src/ImageSharp/Advanced/AotCompilerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,11 @@ private static void AotCompileImageDecoders<TPixel>()
private static void AotCompileSpectralConverter<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(SpectralConverter<TPixel>).GetPixelBuffer(default);
default(GrayJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(RgbJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(TiffJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(TiffOldJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(SpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(GrayJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(RgbJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(TiffJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(TiffOldJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
}

/// <summary>
Expand Down
14 changes: 7 additions & 7 deletions src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ namespace SixLabors.ImageSharp.ColorProfiles;
public class ColorConversionOptions
{
private Matrix4x4 adaptationMatrix;
private YCbCrMatrix yCbCrMatrix;
private YCbCrTransform yCbCrTransform;

/// <summary>
/// Initializes a new instance of the <see cref="ColorConversionOptions"/> class.
/// </summary>
public ColorConversionOptions()
{
this.AdaptationMatrix = KnownChromaticAdaptationMatrices.Bradford;
this.YCbCrMatrix = KnownYCbCrMatrices.BT601;
this.YCbCrTransform = KnownYCbCrMatrices.BT601;
}

/// <summary>
Expand Down Expand Up @@ -53,13 +53,13 @@ public ColorConversionOptions()
/// <summary>
/// Gets the YCbCr matrix to used to perform conversions from/to RGB.
/// </summary>
public YCbCrMatrix YCbCrMatrix
public YCbCrTransform YCbCrTransform
{
get => this.yCbCrMatrix;
get => this.yCbCrTransform;
init
{
this.yCbCrMatrix = value;
this.TransposedYCbCrMatrix = value.Transpose();
this.yCbCrTransform = value;
this.TransposedYCbCrTransform = value.Transpose();
}
}

Expand Down Expand Up @@ -88,7 +88,7 @@ public Matrix4x4 AdaptationMatrix
}
}

internal YCbCrMatrix TransposedYCbCrMatrix { get; private set; }
internal YCbCrTransform TransposedYCbCrTransform { get; private set; }

internal Matrix4x4 InverseAdaptationMatrix { get; private set; }
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

// <auto-generated />

using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
namespace SixLabors.ImageSharp.ColorProfiles.Icc;

internal static class SrgbV4Profile
internal static class CompactSrgbV4Profile
{
private static readonly Lazy<IccProfile> LazyIccProfile = new(GetIccProfile);

// Generated using the sRGB-v4.icc profile found at https://github.com/saucecontrol/Compact-ICC-Profiles
private static ReadOnlySpan<byte> Data => new byte[]
{
private static ReadOnlySpan<byte> Data =>
[
0, 0, 1, 224, 108, 99, 109, 115, 4, 32, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, 226, 0, 3, 0,
20, 0, 9, 0, 14, 0, 29, 97, 99, 115, 112, 77, 83, 70, 84, 0, 0, 0, 0, 115, 97, 119, 115, 99, 116, 114, 108, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 104, 97, 110, 100, 163, 178, 171,
Expand All @@ -29,11 +29,9 @@ internal static class SrgbV4Profile
3, 143, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, 150, 0, 0, 183, 137, 0, 0, 24, 218, 88, 89, 90, 32, 0, 0, 0,
0, 0, 0, 36, 160, 0, 0, 15, 133, 0, 0, 182, 196, 112, 97, 114, 97, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 102, 105,
0, 0, 242, 167, 0, 0, 13, 89, 0, 0, 19, 208, 0, 0, 10, 91,
};
];

private static readonly Lazy<IccProfile> LazyIccProfile = new(() => GetIccProfile());

public static IccProfile GetProfile() => LazyIccProfile.Value;
public static IccProfile Profile => LazyIccProfile.Value;

private static IccProfile GetIccProfile()
{
Expand All @@ -42,4 +40,3 @@ private static IccProfile GetIccProfile()
return new IccProfile(buffer);
}
}

6 changes: 3 additions & 3 deletions src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static class KnownYCbCrMatrices
/// <summary>
/// ITU-R BT.601 (SD video standard).
/// </summary>
public static readonly YCbCrMatrix BT601 = new(
public static readonly YCbCrTransform BT601 = new(
new Matrix4x4(
0.299000F, 0.587000F, 0.114000F, 0F,
-0.168736F, -0.331264F, 0.500000F, 0F,
Expand All @@ -31,7 +31,7 @@ public static class KnownYCbCrMatrices
/// <summary>
/// ITU-R BT.709 (HD video, sRGB standard).
/// </summary>
public static readonly YCbCrMatrix BT709 = new(
public static readonly YCbCrTransform BT709 = new(
new Matrix4x4(
0.212600F, 0.715200F, 0.072200F, 0F,
-0.114572F, -0.385428F, 0.500000F, 0F,
Expand All @@ -47,7 +47,7 @@ public static class KnownYCbCrMatrices
/// <summary>
/// ITU-R BT.2020 (UHD/4K video standard).
/// </summary>
public static readonly YCbCrMatrix BT2020 = new(
public static readonly YCbCrTransform BT2020 = new(
new Matrix4x4(
0.262700F, 0.678000F, 0.059300F, 0F,
-0.139630F, -0.360370F, 0.500000F, 0F,
Expand Down
6 changes: 3 additions & 3 deletions src/ImageSharp/ColorProfiles/Rgb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static Rgb FromScaledVector4(Vector4 source)
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4()
=> new(this.ToScaledVector3(), 1F);
=> new(this.AsVector3Unsafe(), 1F);

/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<Rgb> source, Span<Vector4> destination)
Expand Down Expand Up @@ -154,7 +154,7 @@ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(this.ToScaledVector4()));

// Then convert to xyz
return new CieXyz(Vector3.Transform(linear.ToScaledVector3(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace)));
return new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace)));
}

/// <inheritdoc/>
Expand All @@ -171,7 +171,7 @@ public static void ToProfileConnectionSpace(ColorConversionOptions options, Read
Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(rgb.ToScaledVector4()));

// Then convert to xyz
destination[i] = new CieXyz(Vector3.Transform(linear.ToScaledVector3(), matrix));
destination[i] = new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), matrix));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/ImageSharp/ColorProfiles/Y.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
/// <inheritdoc/>
public static Y FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{
Matrix4x4 m = options.YCbCrMatrix.Forward;
float offset = options.YCbCrMatrix.Offset.X;
Matrix4x4 m = options.YCbCrTransform.Forward;
float offset = options.YCbCrTransform.Offset.X;
return new(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset);
}

Expand Down
8 changes: 4 additions & 4 deletions src/ImageSharp/ColorProfiles/YCbCr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<YCbCr> d
public static YCbCr FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{
Vector3 rgb = source.AsVector3Unsafe();
Matrix4x4 m = options.TransposedYCbCrMatrix.Forward;
Vector3 offset = options.TransposedYCbCrMatrix.Offset;
Matrix4x4 m = options.TransposedYCbCrTransform.Forward;
Vector3 offset = options.TransposedYCbCrTransform.Offset;

return new YCbCr(Vector3.Transform(rgb, m) + offset, true);
}
Expand All @@ -152,8 +152,8 @@ public static void FromProfileConnectionSpace(ColorConversionOptions options, Re
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
Matrix4x4 m = options.TransposedYCbCrMatrix.Inverse;
Vector3 offset = options.TransposedYCbCrMatrix.Offset;
Matrix4x4 m = options.TransposedYCbCrTransform.Inverse;
Vector3 offset = options.TransposedYCbCrTransform.Offset;
Vector3 normalized = this.AsVector3Unsafe() - offset;

return Rgb.FromScaledVector3(Vector3.Transform(normalized, m));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.ColorProfiles;

/// <summary>
/// <para>
/// Represents a YCbCr color matrix containing forward and inverse transformation matrices,
/// Represents a YCbCr color transform containing forward and inverse transformation matrices,
/// and the chrominance offsets to apply for full-range encoding
/// </para>
/// <para>
Expand All @@ -17,10 +17,10 @@ namespace SixLabors.ImageSharp.ColorProfiles;
/// working spaces will produce incorrect conversions.
/// </para>
/// </summary>
public readonly struct YCbCrMatrix
public readonly struct YCbCrTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="YCbCrMatrix"/> struct.
/// Initializes a new instance of the <see cref="YCbCrTransform"/> struct.
/// </summary>
/// <param name="forward">
/// The forward transformation matrix from RGB to YCbCr. The matrix must include the
Expand All @@ -34,7 +34,7 @@ public readonly struct YCbCrMatrix
/// The chrominance offsets to be added after the forward conversion,
/// and subtracted before the inverse conversion. Usually <c>(0, 0.5, 0.5)</c>.
/// </param>
public YCbCrMatrix(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset)
public YCbCrTransform(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset)
{
this.Forward = forward;
this.Inverse = inverse;
Expand All @@ -56,6 +56,6 @@ public YCbCrMatrix(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset)
/// </summary>
public Vector3 Offset { get; }

internal YCbCrMatrix Transpose()
internal YCbCrTransform Transpose()
=> new(Matrix4x4.Transpose(this.Forward), Matrix4x4.Transpose(this.Inverse), this.Offset);
}
8 changes: 4 additions & 4 deletions src/ImageSharp/ColorProfiles/YccK.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<YccK> de
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
Matrix4x4 m = options.TransposedYCbCrMatrix.Inverse;
Vector3 offset = options.TransposedYCbCrMatrix.Offset;
Matrix4x4 m = options.TransposedYCbCrTransform.Inverse;
Vector3 offset = options.TransposedYCbCrTransform.Offset;
Vector3 normalized = this.AsVector3Unsafe() - offset;

return Rgb.FromScaledVector3(Vector3.Transform(normalized, m) * (1F - this.K));
Expand All @@ -141,8 +141,8 @@ public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
/// <inheritdoc/>
public static YccK FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{
Matrix4x4 m = options.TransposedYCbCrMatrix.Forward;
Vector3 offset = options.TransposedYCbCrMatrix.Offset;
Matrix4x4 m = options.TransposedYCbCrTransform.Forward;
Vector3 offset = options.TransposedYCbCrTransform.Offset;

Vector3 rgb = source.AsVector3Unsafe();
float k = 1F - MathF.Max(rgb.X, MathF.Max(rgb.Y, rgb.Z));
Expand Down
7 changes: 6 additions & 1 deletion src/ImageSharp/Formats/ColorProfileHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ public enum ColorProfileHandling
/// </summary>
Preserve,

/// <summary>
/// Removes any embedded Standard sRGB ICC color profiles without transforming the pixels of the image.
/// </summary>
Compact,

/// <summary>
/// Transforms the pixels of the image based on the conversion of any embedded ICC color profiles to sRGB V4 profile.
/// The original profile is then replaced.
/// The original profile is then removed.
/// </summary>
Convert
}
43 changes: 41 additions & 2 deletions src/ImageSharp/Formats/DecoderOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;

Expand Down Expand Up @@ -62,9 +64,46 @@ public sealed class DecoderOptions

/// <summary>
/// Gets a value that controls how ICC profiles are handled during decode.
/// TODO: Implement this.
/// </summary>
internal ColorProfileHandling ColorProfileHandling { get; init; }
public ColorProfileHandling ColorProfileHandling { get; init; }

internal void SetConfiguration(Configuration configuration) => this.configuration = configuration;

internal bool TryGetIccProfileForColorConversion(IccProfile? profile, [NotNullWhen(true)] out IccProfile? value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return bool isn't currently used, easier to return the IccProfile? itself?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at the moment but I think it will be used in other formats.

{
value = null;

if (profile is null)
{
return false;
}

if (IccProfileHeader.IsLikelySrgb(profile.Header))
{
return false;
}

if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return false;
}

value = profile;
return true;
}

internal bool CanRemoveIccProfile(IccProfile? profile)
{
if (profile is null)
{
return false;
}

if (this.ColorProfileHandling == ColorProfileHandling.Compact && IccProfileHeader.IsLikelySrgb(profile.Header))
{
return true;
}

return this.ColorProfileHandling == ColorProfileHandling.Convert;
}
}
22 changes: 22 additions & 0 deletions src/ImageSharp/Formats/ImageDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream)
s => this.Decode<TPixel>(options, s, default));

this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);

return image;
}
Expand All @@ -37,6 +38,7 @@ public Image Decode(DecoderOptions options, Stream stream)
s => this.Decode(options, s, default));

this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);

return image;
}
Expand All @@ -52,6 +54,7 @@ public async Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Str
cancellationToken).ConfigureAwait(false);

this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);

return image;
}
Expand All @@ -66,6 +69,7 @@ public async Task<Image> DecodeAsync(DecoderOptions options, Stream stream, Canc
cancellationToken).ConfigureAwait(false);

this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);

return image;
}
Expand All @@ -79,6 +83,7 @@ public ImageInfo Identify(DecoderOptions options, Stream stream)
s => this.Identify(options, s, default));

this.SetDecoderFormat(options.Configuration, info);
HandleIccProfile(options, info);

return info;
}
Expand All @@ -93,6 +98,7 @@ public async Task<ImageInfo> IdentifyAsync(DecoderOptions options, Stream stream
cancellationToken).ConfigureAwait(false);

this.SetDecoderFormat(options.Configuration, info);
HandleIccProfile(options, info);

return info;
}
Expand Down Expand Up @@ -315,4 +321,20 @@ internal void SetDecoderFormat(Configuration configuration, ImageInfo info)
}
}
}

private static void HandleIccProfile(DecoderOptions options, Image image)
{
if (options.CanRemoveIccProfile(image.Metadata.IccProfile))
{
image.Metadata.IccProfile = null;
}
}

private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
{
if (options.CanRemoveIccProfile(image.Metadata.IccProfile))
{
image.Metadata.IccProfile = null;
}
}
}
Loading
Loading