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
30 changes: 23 additions & 7 deletions src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverte
throw new InvalidOperationException("Target ICC profile is missing.");
}

ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
// The embedded source profile supplies the connection's intent. Use it for both transform
// selection and PCS adjustment; the destination header may recommend a different intent.
IccRenderingIntent renderingIntent = converter.Options.SourceIccProfile.Header.RenderingIntent;
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, renderingIntent);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, renderingIntent);

ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
Expand Down Expand Up @@ -141,8 +144,11 @@ internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConvert

Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination));

ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
// Resolve the connection's intent once for the entire span, before selecting either transform
// or deciding whether perceptual PCS adjustment is needed.
IccRenderingIntent renderingIntent = converter.Options.SourceIccProfile.Header.RenderingIntent;
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, renderingIntent);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, renderingIntent);

ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
Expand Down Expand Up @@ -691,17 +697,27 @@ private class ConversionParams
{
private readonly IccProfile profile;

internal ConversionParams(IccProfile profile, bool toPcs)
/// <summary>
/// Initializes a new instance of the <see cref="ConversionParams"/> class.
/// </summary>
/// <param name="profile">The profile used by this transform.</param>
/// <param name="toPcs">Whether this transform converts device values to the PCS.</param>
/// <param name="renderingIntent">The rendering intent shared by both profiles.</param>
internal ConversionParams(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent)
{
this.profile = profile;
this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile);
this.Intent = renderingIntent;
this.Converter = toPcs ? new IccDataToPcsConverter(profile, renderingIntent) : new IccPcsToDataConverter(profile, renderingIntent);
}

internal IccConverterBase Converter { get; }

internal IccProfileHeader Header => this.profile.Header;

internal IccRenderingIntent Intent => this.Header.RenderingIntent;
/// <summary>
/// Gets the rendering intent selected for this profile connection.
/// </summary>
internal IccRenderingIntent Intent { get; }

internal IccColorSpaceType PcsType => this.Header.ProfileConnectionSpace;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public float Calculate(float value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float Lookup(float value)
{
value = Math.Max(value, 0);
// Sampled ICC curves cover [0, 1]. Saturate before scaling so out-of-domain values
// select the endpoint instead of extrapolating or producing an invalid table index.
value = Numerics.Clamp(value, 0, 1);

float factor = value * (this.lut.Length - 1);
int index = (int)factor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ internal abstract partial class IccConverterBase
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
/// <param name="toPcs">True if the conversion is to the profile connection space (PCS); False if the conversion is to the data space</param>
protected IccConverterBase(IccProfile profile, bool toPcs)
/// <param name="renderingIntent">The rendering intent selected for the profile connection.</param>
protected IccConverterBase(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent)
{
Guard.NotNull(profile, nameof(profile));
this.Init(profile, toPcs, profile.Header.RenderingIntent);
this.Init(profile, toPcs, renderingIntent);
}

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ internal class IccDataToDataConverter : IccConverterBase
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccDataToDataConverter(IccProfile profile)
: base(profile, true) // toPCS is true because in this case the PCS space is also a data space

// toPCS is true because the PCS space is also a data space for a DeviceLink profile.
// The shared base constructor requires an intent. Pass the profile's header value;
// DeviceLink transform selection uses CheckMethod2 and ignores rendering intent.
: base(profile, true, profile.Header.RenderingIntent)
{
}
}
5 changes: 3 additions & 2 deletions src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ internal class IccDataToPcsConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccDataToPcsConverter"/> class.
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccDataToPcsConverter(IccProfile profile)
: base(profile, true)
/// <param name="renderingIntent">The rendering intent selected for the profile connection.</param>
public IccDataToPcsConverter(IccProfile profile, IccRenderingIntent renderingIntent)
: base(profile, true, renderingIntent)
{
}
}
5 changes: 3 additions & 2 deletions src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ internal class IccPcsToDataConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccPcsToDataConverter"/> class.
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccPcsToDataConverter(IccProfile profile)
: base(profile, false)
/// <param name="renderingIntent">The rendering intent selected for the profile connection.</param>
public IccPcsToDataConverter(IccProfile profile, IccRenderingIntent renderingIntent)
: base(profile, false, renderingIntent)
{
}
}
5 changes: 4 additions & 1 deletion src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ internal class IccPcsToPcsConverter : IccConverterBase
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccPcsToPcsConverter(IccProfile profile)
: base(profile, true)

// The shared base constructor requires an intent. Pass the profile's header value;
// Abstract transform selection uses CheckMethod2 and ignores rendering intent.
: base(profile, true, profile.Header.RenderingIntent)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc.Calculators;
[Trait("Color", "Conversion")]
public class LutCalculatorTests
{
/// <summary>
/// Verifies that forward lookup saturates at the table endpoints outside its normalized domain.
/// </summary>
/// <param name="input">The normalized lookup input.</param>
/// <param name="expected">The expected table value.</param>
[Theory]
[InlineData(float.NaN, 0.25F)]
[InlineData(float.NegativeInfinity, 0.25F)]
[InlineData(float.MinValue, 0.25F)]
[InlineData(-0.0001F, 0.25F)]
[InlineData(0F, 0.25F)]
[InlineData(0.5F, 0.5F)]
[InlineData(1F, 0.75F)]
[InlineData(1.0001F, 0.75F)]
[InlineData(2F, 0.75F)]
[InlineData(float.MaxValue, 0.75F)]
[InlineData(float.PositiveInfinity, 0.75F)]
public void ForwardLookup_ClampsToTableDomain(float input, float expected)
{
// Nonzero and nonunit endpoints distinguish table saturation from clamping the output to 0 or 1.
LutCalculator calculator = new([0.25F, 0.5F, 0.75F], inverse: false);

Assert.Equal(expected, calculator.Calculate(input));
}

[Theory]
[MemberData(nameof(IccConversionDataLut.LutConversionTestData), MemberType = typeof(IccConversionDataLut))]
internal void LutCalculator_WithLut_ReturnsResult(float[] lut, bool inverted, float input, float expected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,63 @@ public void CanBulkConvertIccProfiles(string sourceProfile, string targetProfile
AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper);
}

/// <summary>
/// Verifies that both transforms use the source rendering intent when the profile headers disagree.
/// </summary>
/// <param name="targetFile">The destination profile, using either curves or a LUT.</param>
/// <param name="tolerance">The normalized tolerance for differences in reference interpolation.</param>
[Theory]
[InlineData(TestIccProfiles.StandardRgbV2, 0.0005)]
[InlineData(TestIccProfiles.StandardRgbV4, 1D / ushort.MaxValue)] // One 16-bit LUT code value for float versus double interpolation.
public void Convert_UsesSourceRenderingIntentForBothProfiles(string targetFile, double tolerance)
{
IccProfile sourceProfile = TestIccProfiles.GetProfile(TestIccProfiles.Fogra39).DeepClone();
sourceProfile.Header.RenderingIntent = IccRenderingIntent.MediaRelativeColorimetric;
IccProfile targetProfile = TestIccProfiles.GetProfile(targetFile);
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = sourceProfile,
TargetIccProfile = targetProfile
});

// Explicitly select the same intent in the independent reference converter. Its unspecified
// intent uses each profile's header, which would reproduce the conflicting-intent defect.
Wacton.Unicolour.Configuration sourceConfig = new(iccConfig: new IccConfiguration(
Path.Combine("TestDataIcc", "Profiles", TestIccProfiles.Fogra39), Intent.RelativeColorimetric));

Wacton.Unicolour.Configuration targetConfig = new(iccConfig: new IccConfiguration(
Path.Combine("TestDataIcc", "Profiles", targetFile), Intent.RelativeColorimetric));

Cmyk[] inputs =
[
new(0, 0, 0, 0), new(0, 0, 0, 1),
new(0, 1, 1, 0), new(1, 0, 1, 0), new(1, 1, 0, 0), new(0, 0, 1, 0),
new(0.25F, 0.5F, 0.75F, 0.125F)
];

Rgb[] bulk = new Rgb[inputs.Length];
converter.Convert<Cmyk, Rgb>(inputs, bulk);

for (int i = 0; i < inputs.Length; i++)
{
Cmyk input = inputs[i];
Unicolour reference = new(sourceConfig, new Channels(input.C, input.M, input.Y, input.K));
Unicolour expected = reference.ConvertToConfiguration(targetConfig);
Assert.Null(expected.Icc.Error);
Vector4 scalar = converter.Convert<Cmyk, Rgb>(input).ToScaledVector4();
Vector4 span = bulk[i].ToScaledVector4();

for (int channel = 0; channel < 3; channel++)
{
Assert.Equal(expected.Icc.Values[channel], scalar[channel], tolerance);
Assert.Equal(expected.Icc.Values[channel], span[channel], tolerance);
}
}

// Conversion must not rewrite a profile that may be shared with other callers.
Assert.Equal(IccRenderingIntent.Perceptual, targetProfile.Header.RenderingIntent);
}

private static void AssertConversion(string sourceProfile, string targetProfile, List<Vector4> actual, double tolerance, ITestOutputHelper testOutputHelper)
{
List<double[]> expected = Inputs.ConvertAll(input => GetExpectedTargetValues(sourceProfile, targetProfile, input, testOutputHelper));
Expand Down
22 changes: 22 additions & 0 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,28 @@ public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile<TPixel>(Tes
Assert.Null(image.Metadata.IccProfile);
}

/// <summary>
/// Verifies conversion of the reporter's CMYK profile using its relative-colorimetric intent.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="provider">The image provider.</param>
[Theory]
[WithFile(Issue3198, PixelTypes.Rgba32)]
public void Decode_CmykIcc_Issue3198<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Convert };
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance, options);
image.DebugSave(provider);

// LittleCMS 2.19 generated the reference from the embedded profile to CompactSrgbV4Profile,
// using relative colorimetric intent without black-point compensation.
// Measured differences are at most one 8-bit channel value, totaling 0.000083% of the image.
image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0001F), provider);
Assert.Null(image.Metadata.IccProfile);
Assert.Null(image.Frames.RootFrame.Metadata.IccProfile);
}

[Theory]
[WithFile(Icc.PerceptualRgb8, PixelTypes.Rgba32)]
[WithFile(Icc.PerceptualRgb16, PixelTypes.Rgba32)]
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 @@ -1213,6 +1213,7 @@ public static class Tiff
public const string Issue2983 = "Tiff/Issues/Issue2983.tiff";
public const string Issue3182ColorMap8Bit = "Tiff/Issues/Issue3182ColorMap8Bit.tiff";
public const string Issue3182ColorMap16Bit = "Tiff/Issues/Issue3182ColorMap16Bit.tiff";
public const string Issue3198 = "Tiff/Issues/Issue3198.tiff";

public static readonly string[] Multiframes = [MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/
];
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/Tiff/Issues/Issue3198.tiff
Git LFS file not shown
Loading