Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,70 @@

using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;

/// <summary>
/// Converts between a grayscale device channel and the achromatic axis of its ICC PCS.
/// </summary>
internal class GrayTrcCalculator : IVector4Calculator
{
private readonly TrcCalculator calculator;
// Encode the fixed D50 white point once so each XYZ sample needs only the gray multiplication.
private static readonly Vector3 ScaledD50 = KnownIlluminants.D50Icc.ToScaledVector4().AsVector3();

public GrayTrcCalculator(IccTagDataEntry grayTrc, bool toPcs)
=> this.calculator = new TrcCalculator(new IccTagDataEntry[] { grayTrc }, !toPcs);
private readonly ISingleCalculator calculator;
private readonly bool toPcs;
private readonly bool isLab;

/// <summary>
/// Initializes a new instance of the <see cref="GrayTrcCalculator"/> class.
/// </summary>
/// <param name="grayTrc">The grayscale tone response curve.</param>
/// <param name="pcsType">The profile's XYZ or Lab connection space.</param>
/// <param name="toPcs">Whether to convert device gray to the PCS instead of the reverse.</param>
public GrayTrcCalculator(IccTagDataEntry grayTrc, IccColorSpaceType pcsType, bool toPcs)
{
// A grayscale profile has one curve, so use its scalar calculator without channel arrays or traversal.
this.calculator = grayTrc switch
{
IccCurveTagDataEntry curve => new CurveCalculator(curve, !toPcs),
IccParametricCurveTagDataEntry parametricCurve => new ParametricCurveCalculator(parametricCurve, !toPcs),
_ => throw new InvalidIccProfileException("Invalid Entry."),
};

this.toPcs = toPcs;
this.isLab = pcsType == IccColorSpaceType.CieLab;
}

/// <summary>
/// Converts a normalized gray channel to encoded PCS values, or encoded PCS values to device gray.
/// </summary>
/// <param name="value">The device value in X, or the encoded PCS components.</param>
/// <returns>The encoded PCS value, or the device gray value in X.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value);
public Vector4 Calculate(Vector4 value)
{
if (this.toPcs)
{
float gray = this.calculator.Calculate(value.X);

// ICC monochrome TRCs describe the achromatic PCS axis: relative Y for XYZ or L*/100 for Lab.
// Construct all three PCS components from that one result; the unused device lanes are not XYZ.
if (this.isLab)
{
// L*/100 is already normalized; neutral a* and b* encode as (0 + 128)/255.
return new Vector4(gray, 128F / 255F, 128F / 255F, 1F);
}

return new Vector4(ScaledD50 * gray, 1F);
}

// Encoded Lab already stores L*/100 in X. XYZ must be decoded before selecting its Y component;
// applying the inverse curve to X would instead interpret the tristimulus X value as luminance.
// The 65535/32768 factor reverses the ICC XYZ encoding; the unused X and Z components need no scaling.
float luminance = this.isLab ? value.X : value.Y * (65535F / 32768F);
return new Vector4(this.calculator.Calculate(luminance));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ private static ColorTrcCalculator InitColorTrc(IccProfile profile, bool toPcs)
private static GrayTrcCalculator InitGrayTrc(IccProfile profile, bool toPcs)
{
IccTagDataEntry entry = GetTag(profile, IccProfileTag.GrayTrc);
return new GrayTrcCalculator(entry, toPcs);
return new GrayTrcCalculator(entry, profile.Header.ProfileConnectionSpace, toPcs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc.Calculators;

/// <summary>
/// Tests grayscale ICC conversion against the XYZ and Lab achromatic axes.
/// </summary>
[Trait("Color", "Conversion")]
public class GrayTrcCalculatorTests
{
/// <summary>
/// Verifies that a gamma curve produces neutral PCS black, midtones, and white.
/// </summary>
/// <param name="pcsType">The profile connection space.</param>
/// <param name="gray">The device gray value.</param>
/// <param name="connection">The expected normalized achromatic PCS value.</param>
[Theory]
[InlineData(IccColorSpaceType.CieXyz, 0F, 0F)]
[InlineData(IccColorSpaceType.CieXyz, 0.5F, 0.25F)]
[InlineData(IccColorSpaceType.CieXyz, 1F, 1F)]
[InlineData(IccColorSpaceType.CieLab, 0F, 0F)]
[InlineData(IccColorSpaceType.CieLab, 0.5F, 0.25F)]
[InlineData(IccColorSpaceType.CieLab, 1F, 1F)]
internal void ToPcs_ProducesNeutralColor(IccColorSpaceType pcsType, float gray, float connection)
{
GrayTrcCalculator calculator = new(new IccCurveTagDataEntry(2F), pcsType, toPcs: true);

// Only the first device channel is meaningful. Distinct unused lanes detect accidental pass-through.
Vector4 actual = calculator.Calculate(new Vector4(gray, 0.3F, 0.7F, 1F));
Vector4 expected = pcsType == IccColorSpaceType.CieLab
? new CieLab(connection * 100F, 0, 0).ToScaledVector4()
: new CieXyz(KnownIlluminants.D50Icc.ToVector3() * connection).ToScaledVector4();

VectorAssert.Equal(expected, actual, 5);
}

/// <summary>
/// Verifies that the inverse curve uses PCS luminance or lightness, independently of chromatic components.
/// </summary>
/// <param name="pcsType">The profile connection space.</param>
/// <param name="connection">The normalized achromatic PCS value.</param>
/// <param name="gray">The expected device gray value.</param>
[Theory]
[InlineData(IccColorSpaceType.CieXyz, 0F, 0F)]
[InlineData(IccColorSpaceType.CieXyz, 0.25F, 0.5F)]
[InlineData(IccColorSpaceType.CieXyz, 1F, 1F)]
[InlineData(IccColorSpaceType.CieLab, 0F, 0F)]
[InlineData(IccColorSpaceType.CieLab, 0.25F, 0.5F)]
[InlineData(IccColorSpaceType.CieLab, 1F, 1F)]
internal void FromPcs_UsesAchromaticComponent(IccColorSpaceType pcsType, float connection, float gray)
{
GrayTrcCalculator calculator = new(new IccCurveTagDataEntry(2F), pcsType, toPcs: false);

// Use non-neutral inputs so selecting XYZ.X instead of XYZ.Y cannot pass as a round-trip would.
Vector4 input = pcsType == IccColorSpaceType.CieLab
? new CieLab(connection * 100F, 10F, 20F).ToScaledVector4()
: new CieXyz(0.3F, connection, 0.7F).ToScaledVector4();

Assert.Equal(gray, calculator.Calculate(input).X, 5);
}
}
20 changes: 20 additions & 0 deletions tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,26 @@ public void Decode_RGB_ICC_Jpeg_Issue3064<TPixel>(TestImageProvider<TPixel> prov
image.CompareToReferenceOutput(provider);
}

/// <summary>
/// Verifies that converting the grayscale ICC profile preserves the sample's distinct neutral tones.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="provider">The image provider.</param>
[Theory]
[WithFile(TestImages.Jpeg.ICC.Issue3197, PixelTypes.Rgba32)]
public void Decode_Grayscale_ICC_Jpeg_Issue3197<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
JpegDecoderOptions options = new()
{
GeneralOptions = new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert }
};

using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance, options);
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}

// https://github.com/SixLabors/ImageSharp/issues/2948
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2948, PixelTypes.Rgb24)]
Expand Down
1 change: 1 addition & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ public static class ICC
public const string Perceptual = "Jpg/icc-profiles/Perceptual.jpg";
public const string PerceptualcLUTOnly = "Jpg/icc-profiles/Perceptual-cLUT-only.jpg";
public const string Issue3064 = "Jpg/icc-profiles/issue-3064.jpg";
public const string Issue3197 = "Jpg/icc-profiles/issue-3197.jpg";
}

public static class Progressive
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/icc-profiles/issue-3197.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading