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

Already on GitHub? Sign in to your account

Color conversion with ICC profiles #1567

Draft
wants to merge 94 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
16179b3
Copy original PR changes.
JimBobSquarePants Feb 27, 2021
eeb40f5
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Apr 23, 2021
8b96d37
Merge branch 'master' into icc-color-conversion
JimBobSquarePants May 22, 2021
aa2f1e9
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Jul 7, 2021
3debb26
Merge branch 'master' into icc-color-conversion
brianpopow Jul 12, 2021
0812085
Fix warnings
brianpopow Jul 12, 2021
1094dc6
Add color conversion trait to be able to filter for those tests
brianpopow Jul 12, 2021
759f053
Fix failing MatrixCalculator test
brianpopow Jul 13, 2021
4cde4ef
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Nov 9, 2021
a973713
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Nov 11, 2021
33339d0
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Jan 18, 2022
cdb495c
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Jan 30, 2022
5f7acc1
Merge remote-tracking branch 'origin/main' into icc-color-conversion
brianpopow May 15, 2022
7e2bc20
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Nov 23, 2022
dc166a7
Fix build
JimBobSquarePants Nov 23, 2022
dcc8147
Cleanup icc tests
brianpopow Nov 24, 2022
73c8d8f
Fix Copyright notice
brianpopow Nov 24, 2022
d229fed
Fix icc namespaces
brianpopow Nov 24, 2022
0a08da0
Use file scoped namespaces
brianpopow Nov 27, 2022
daf366b
Cleanup and add conversion tests
JimBobSquarePants Nov 30, 2022
54856ff
Fix reader and out of range exception
JimBobSquarePants Dec 4, 2022
eee14c6
Remove invalid test.
JimBobSquarePants Dec 4, 2022
f60d4b8
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 14, 2022
8de137e
Cleanup code style
JimBobSquarePants Dec 15, 2022
fb8003c
Remove double clamping
JimBobSquarePants Dec 15, 2022
9f0f9cb
Optimize matrix read/write
JimBobSquarePants Dec 15, 2022
ece11eb
Create ColorProfileHandling.cs
JimBobSquarePants Dec 17, 2022
bd7257b
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 17, 2022
9a21485
Nullable disable
JimBobSquarePants Dec 17, 2022
ba76964
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 18, 2022
98d1758
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 2, 2023
66554cb
Add ability to convert ICC profile on decode
JimBobSquarePants Jan 4, 2023
e90f165
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 4, 2023
8c580a7
Handle nullability in decoder base
JimBobSquarePants Jan 5, 2023
a81dac9
Add reference files.
JimBobSquarePants Jan 9, 2023
902ed99
Add tolerance for Mac
JimBobSquarePants Jan 9, 2023
e3aa452
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 10, 2023
0dd68fe
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 15, 2023
6e3dc81
Update decoder bases following merge
JimBobSquarePants Jan 15, 2023
b7833a4
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 16, 2023
c3984aa
Add failing tests
JimBobSquarePants Jan 16, 2023
0fff06d
Port 1d, 2d, 3d, 4d and nd interpolation from reference implementation
brianpopow Jan 22, 2023
f1c05ee
Remove not used IccClut constructors
brianpopow Jan 22, 2023
3be31c3
Preserve alpha component
JimBobSquarePants Jan 22, 2023
6c2ee90
Fix CieLab docs
JimBobSquarePants Jan 22, 2023
20e9b7f
Fix out of bounds error
brianpopow Jan 22, 2023
d6fbc01
Change clut values from jagged array to flat array
brianpopow Jan 22, 2023
67ed4ce
Fix warnings
brianpopow Jan 22, 2023
b036cc3
Fix mistake reading the clut values
brianpopow Jan 22, 2023
52f88c8
Fix oob in n-dimension calculator.
JimBobSquarePants Jan 23, 2023
ed47678
Add Lab<=>Xyz conversion
JimBobSquarePants Jan 23, 2023
10bea86
Add ICC reader tests
brianpopow Jan 23, 2023
5b131ad
Add reference output for issue-129
brianpopow Feb 3, 2023
ed8091b
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Feb 3, 2023
6225db3
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Feb 9, 2023
a65c599
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Mar 30, 2023
60f3d9d
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Sep 2, 2023
c940b86
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Sep 25, 2023
a567613
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Nov 1, 2023
3e06687
Update IccReader.cs
JimBobSquarePants Nov 1, 2023
c1ebbfe
Fix build
JimBobSquarePants Nov 1, 2023
d89d8c5
Update IccReader.cs
JimBobSquarePants Nov 1, 2023
63c89ca
Use scaled Vector4 conversion and optimize
JimBobSquarePants Nov 1, 2023
3389d7a
Add some debugging helpers to the converter
JimBobSquarePants Nov 9, 2023
7b0ff3b
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jun 11, 2024
29ed2b4
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jul 9, 2024
5f975e5
Update to latest main build
JimBobSquarePants Jul 9, 2024
79f5dfa
Update IccProfileConverterTests.cs
JimBobSquarePants Jul 9, 2024
b88b2a9
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jul 10, 2024
bca4cad
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Aug 12, 2024
6e2c29c
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 2, 2024
5b374da
Fix IccClut test setup and calculator guards
JimBobSquarePants Dec 3, 2024
441f07e
Update ColorProfileConverter to handle ICCProfiles
JimBobSquarePants Dec 3, 2024
6654218
Suppress warning
JimBobSquarePants Dec 3, 2024
21fec4e
Demonstrate ICC conversion comparison to Unicolour
waacton Dec 6, 2024
fd2e8a9
Migrate tests
JimBobSquarePants Dec 10, 2024
19ff69d
Add ICC files to LFS
JimBobSquarePants Dec 10, 2024
3cd7d67
Update TestIccProfiles.cs
JimBobSquarePants Dec 10, 2024
0327dca
Adjust PCS values for v2 profiles using perceptual intent
waacton Dec 10, 2024
9de3935
Remove TODO
waacton Dec 10, 2024
8e92f20
Fix XYZ PCS conversions
waacton Dec 10, 2024
312b55e
Cleanup
JimBobSquarePants Dec 11, 2024
44aae40
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 11, 2024
df3d230
Extract conversion for v2 perceptual intent
waacton Dec 11, 2024
d20fddb
Precalculate v2 perceptual PCS adjustment
waacton Dec 12, 2024
d60ac76
Bypass PCS adjustment when not needed
waacton Dec 14, 2024
cfa2760
Add failing tests for CMYK to RGB using Matrix TRC
waacton Dec 14, 2024
369bf5f
Fix CMYK to RGB using TRCs
waacton Dec 14, 2024
7d4a742
Add RGB to CMYK tests and fix TRC calculator
waacton Dec 15, 2024
f4e9509
Handle tests in cases where PCS adjustment is bypassed
waacton Dec 16, 2024
9ceed23
Fix expected values of CLUT unit tests
waacton Dec 17, 2024
f21c0c2
Fix LUT entry calculator for XYZ PCS with non-identity matrix
waacton Dec 17, 2024
f147aad
Minor cleanup
JimBobSquarePants Dec 18, 2024
7492109
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 18, 2024
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
124 changes: 83 additions & 41 deletions src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
Expand Down Expand Up @@ -33,64 +34,103 @@
MemoryAllocator = converter.Options.MemoryAllocator,

// TODO: Double check this but I think these are normalized values.
SourceWhitePoint = CieXyz.FromScaledVector4(new(converter.Options.SourceIccProfile.Header.PcsIlluminant, 1F)),
TargetWhitePoint = CieXyz.FromScaledVector4(new(converter.Options.TargetIccProfile.Header.PcsIlluminant, 1F)),
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
Copy link
Member Author

Choose a reason for hiding this comment

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

Are the illuminant values not given using the ICC scaling? I would have assumed they were given we need to pass them as such.

Copy link
Collaborator

@waacton waacton Dec 11, 2024

Choose a reason for hiding this comment

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

This has sent me down a rabbit hole and I don't feel any closer to understanding why, but the illuminant values are stored in the header in the range [0, 1], no scaling needed.

I can't find solid information why the scaling is even needed for XYZ LUTs other than "that's what the DemoIccMAX code does". The closest thing I can find in the v4 spec itself is this footnote in Annex F.3 page 102:

NOTE A three-component Matrix-based model can alternatively be represented in a lutAToBType tag with M curves, a matrix with zero offsets, and identity B curves. While the M curves are set to the corresponding TRC curves, matrix values from the three-component Matrix-based model need to be scaled by (32 768/65 535) before being stored in the lutAToBType matrix in order to produce equivalent PCS values. (32 768/65 535) represents the encoding factor for the PCS PCSXYZ encoding.

(The spec is so cumbersome, the information I'm looking for could easily be buried elsewhere...)

At this point I'm assuming either

  • XYZ LUT data is in [0, ~0.5] by convention (or by something in the spec I can't find)
  • XYZ LUT data range is profile-specific, and I've not encountered one that isn't [0, ~0.5] (or DemoIccMAX doesn't account for the possibility)

🤕

One other note, as far as I understand the PCS illuminant must be D50 (in case that enables any further optimisation)

7.2.16 PCS illuminant field (Bytes 68 to 79)

The PCS illuminant field shall contain the nCIEXYZ values of the illuminant of the PCS, encoded as an
XYZNumber. The value, when rounded to four decimals, shall be X = 0,9642, Y = 1,0 and Z = 0,8249.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking about it a bit more, it's going to be related to LUTs storing uInt16 [0, 65535] but XYZ values being encoded as s15Fixed16 [-32768, ~32768], and needing to account for that.

TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
});

IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile);
IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile);
IccColorSpaceType sourcePcsType = converter.Options.SourceIccProfile.Header.ProfileConnectionSpace;
IccColorSpaceType targetPcsType = converter.Options.TargetIccProfile.Header.ProfileConnectionSpace;
IccVersion sourceVersion = converter.Options.SourceIccProfile.Header.Version;
IccVersion targetVersion = converter.Options.TargetIccProfile.Header.Version;

Vector4 pcs = sourceConverter.Calculate(source.ToScaledVector4());

// Profile connecting spaces can only be Lab, XYZ.
if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieXyz)
{
// Convert from Lab to XYZ.
CieLab lab = CieLab.FromScaledVector4(pcs);
CieXyz xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
pcs = xyz.ToScaledVector4();
}
else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieLab)
IccProfileHeader sourceHeader = converter.Options.SourceIccProfile.Header;
IccProfileHeader targetHeader = converter.Options.TargetIccProfile.Header;
IccColorSpaceType sourcePcsType = sourceHeader.ProfileConnectionSpace;
IccColorSpaceType targetPcsType = targetHeader.ProfileConnectionSpace;
IccRenderingIntent sourceIntent = sourceHeader.RenderingIntent;
IccRenderingIntent targetIntent = targetHeader.RenderingIntent;
IccVersion sourceVersion = sourceHeader.Version;
IccVersion targetVersion = targetHeader.Version;

// all conversions are funnelled through XYZ in case PCS adjustments need to be made
CieXyz xyz;
Copy link
Member Author

Choose a reason for hiding this comment

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

Reading below we only need this if adjustPcsForPerceptual is true. I'd rather avoid the overhead of additional conversions when not necessary. We'll be using this code in our decoder which must be fast.

Copy link
Collaborator

@waacton waacton Dec 11, 2024

Choose a reason for hiding this comment

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

I'll take a shot at avoiding the overhead when unnecessary. I expect it will result in functions that look very similar like ConvertIcc() and ConvertIccWithPerceptualAdjustment() - I can't see a natural if (adjustmentNeeded) { PerformExtraStep() } at the moment


Vector4 sourcePcs = sourceConverter.Calculate(source.ToScaledVector4());
switch (sourcePcsType)
{
// Convert from XYZ to Lab.
CieXyz xyz = CieXyz.FromScaledVector4(pcs);
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
pcs = lab.ToScaledVector4();
case IccColorSpaceType.CieLab:
if (sourceConverter.Is16BitLutEntry)
Copy link
Member Author

Choose a reason for hiding this comment

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

Why is a 16bit LUT calculator treated differently and why is that not version specific?

Copy link
Collaborator

@waacton waacton Dec 11, 2024

Choose a reason for hiding this comment

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

LAB encodings only changed for 16-bit representations:

  • 16-bit max values for 100 & 127 were FF00 in v2 and became FFFF in v4
  • 8-bit max values for 100 & 127 were FF in v2 and stayed FF in v4

But for the LUTs, the 16-bit type continues to use the legacy encoding:

For colour values that are in the PCSLAB colour space on the PCS side of the tag, this tag uses the legacy 16-
bit PCSLAB encoding defined in Tables 42 and 43, not the 16-bit PCSLAB encoding defined in 6.3.4.2. This
encoding is retained for backwards compatibility with profile version 2.

{
sourcePcs = LabV2ToLab(sourcePcs);
}

CieLab lab = CieLab.FromScaledVector4(sourcePcs);
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
break;
case IccColorSpaceType.CieXyz:
xyz = new CieXyz(sourcePcs[0], sourcePcs[1], sourcePcs[2]);
xyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
break;
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported");
}
else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieXyz)
{
// Convert from XYZ to XYZ.
CieXyz xyz = CieXyz.FromScaledVector4(pcs);
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
pcs = targetXyz.ToScaledVector4();
}
else if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieLab)

// TODO: handle PCS adjustment for absolute intent?
// TODO: or throw unsupported error, since most profiles headers contain perceptual (i've encountered a couple of relative, but so far no saturation or absolute)
bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent.Perceptual && sourceVersion.Major == 2;
bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent.Perceptual && targetVersion.Major == 2;

// if both profiles need PCS adjustment, they both share the same unadjusted PCS space
// effectively cancelling out the need to make the adjustment
bool adjustPcsForPerceptual = adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual;
if (adjustPcsForPerceptual)
{
// Convert from Lab to Lab.
if (sourceVersion.Major == 4 && targetVersion.Major == 2)
// as per DemoIccMAX icPerceptual values in IccCmm.h
CieXyz refBlack = new(0.00336F, 0.0034731F, 0.00287F);
CieXyz refWhite = new(0.9642F, 1.0000F, 0.8249F);

if (adjustSourcePcsForPerceptual)
{
// Convert from Lab v4 to Lab v2.
pcs = LabToLabV2(pcs);
Vector3 iccXyz = xyz.ToScaledVector4().AsVector128().AsVector3();
Copy link
Member Author

Choose a reason for hiding this comment

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

It looks like we're mixing up normalized and standard values here and it's a little confusing.

// We use the original ref values here...
Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3());

// But scale them here?
Vector3 offset = refBlack.ToScaledVector4().AsVector128().AsVector3();

I would extract the methods out with an explanation of the theory behind them also. For example, I don't understand why the math for source and targeted PCS adjustments is different. We're going to need to vectorize these also. (Which may mean providing your reference colors as Vector4)

Copy link
Collaborator

@waacton waacton Dec 11, 2024

Choose a reason for hiding this comment

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

Yep, happy to refactor to methods with explanations. I think I need to do some reading on best practices regarding Vectors etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Tried to make this clearer and have also precalculated the scale and offset vectors.

Also realised converting XYZ values to XYZ-scaled values (the other type of scaling 😃) was unnecessary - a small saving I can take back over to Unicolour.

Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3());
Vector3 offset = refBlack.ToScaledVector4().AsVector128().AsVector3();
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
else if (sourceVersion.Major == 2 && targetVersion.Major == 4)

if (adjustTargetPcsForPerceptual)
{
// Convert from Lab v2 to Lab v4.
pcs = LabV2ToLab(pcs);
Vector3 iccXyz = xyz.ToScaledVector4().AsVector128().AsVector3();
Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()));
Vector3 offset = -refBlack.ToScaledVector4().AsVector128().AsVector3() * scale;
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
}

CieLab lab = CieLab.FromScaledVector4(pcs);
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in lab);
pcs = targetLab.ToScaledVector4();
Vector4 targetPcs;
switch (targetPcsType)
{
case IccColorSpaceType.CieLab:
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
targetPcs = lab.ToScaledVector4();
if (adjustTargetPcsForPerceptual)
{
targetPcs = LabToLabV2(targetPcs);
}

break;
case IccColorSpaceType.CieXyz:
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
targetPcs = targetXyz.ToScaledVector4();
break;
default:
throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported");
}

// Convert to the target space.
return TTo.FromScaledVector4(targetConverter.Calculate(pcs));
Vector4 targetValue = targetConverter.Calculate(targetPcs);
return TTo.FromScaledVector4(targetValue);
}

// TODO: update to match workflow of the function above
internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
Expand Down Expand Up @@ -202,7 +242,9 @@
targetConverter.Calculate(pcsNormalized, pcsNormalized);
TTo.FromScaledVector4(pcsNormalized, destination);
}

Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

View workflow job for this annotation

GitHub Actions / Build (false, ubuntu-latest, net9.0, 9.0.x, true, -x64, false)

Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

View workflow job for this annotation

GitHub Actions / Build (false, ubuntu-latest, net9.0, 9.0.x, true, -x64, false)

Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

View workflow job for this annotation

GitHub Actions / Build (false, ubuntu-latest, net8.0, 8.0.x, -x64, false)

Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

View workflow job for this annotation

GitHub Actions / Build (false, ubuntu-latest, net8.0, 8.0.x, -x64, false)



[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 LabToLabV2(Vector4 input)
=> input * 65280F / 65535F;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ public LutEntryCalculator(IccLut8TagDataEntry lut)
{
Guard.NotNull(lut, nameof(lut));
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
this.Is16Bit = false;
}

public LutEntryCalculator(IccLut16TagDataEntry lut)
{
Guard.NotNull(lut, nameof(lut));
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
this.Is16Bit = true;
}

internal bool Is16Bit { get; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal abstract partial class IccConverterBase
{
private IVector4Calculator calculator;

public bool Is16BitLutEntry => this.calculator is LutEntryCalculator { Is16Bit: true };

/// <summary>
/// Checks the profile for available conversion methods and gathers all the information's necessary for it.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles;
using Wacton.Unicolour;
using Wacton.Unicolour.Icc;
using Rgb = SixLabors.ImageSharp.ColorProfiles.Rgb;

namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc;

public class ColorProfileConverterTests
{
[Theory]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)]
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39)]
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006)]
public void CanConvertCmykIccProfiles(string sourceProfileName, string targetProfileName)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2011, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit)
[InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4, IccConversion.CmykToRgb)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39, IccConversion.RgbToCmyk)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb, IccConversion.RgbToRgb)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M)
// TODO: enable once supported by Unicolour - in the meantime, manually test known values
// [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, IccConversion.CmykToRgb)] // CMYK -> XYZ -> LAB -> RGB (different LUT tags, A2B vs TRC)
// [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39, IccConversion.RgbToCmyk)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile, IccConversion iccConversion)
{
Cmyk input = new(GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue());
// TODO: delete after testing
float[] input = [0.734798908f, 0.887050927f, 0.476583719f, 0.547810674f];
// float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()];
double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input);

ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfileName),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfileName),
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfile)
});

Cmyk expectedTargetValues = GetExpectedTargetCmyk(sourceProfileName, targetProfileName, input);
Cmyk actualTargetValues = converter.Convert<Cmyk, Cmyk>(input);
Vector4 actualTargetValues = iccConversion switch
{
IccConversion.CmykToCmyk => converter.Convert<Cmyk, Cmyk>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccConversion.CmykToRgb => converter.Convert<Cmyk, Rgb>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccConversion.RgbToCmyk => converter.Convert<Rgb, Cmyk>(new Rgb(new Vector3(input))).ToScaledVector4(),
IccConversion.RgbToRgb => converter.Convert<Rgb, Rgb>(new Rgb(new Vector3(input))).ToScaledVector4(),
_ => throw new ArgumentOutOfRangeException(nameof(iccConversion), iccConversion, null)
};

const double tolerance = 0.0000005;
Assert.Equal(expectedTargetValues.C, actualTargetValues.C, tolerance);
Assert.Equal(expectedTargetValues.M, actualTargetValues.M, tolerance);
Assert.Equal(expectedTargetValues.Y, actualTargetValues.Y, tolerance);
Assert.Equal(expectedTargetValues.K, actualTargetValues.K, tolerance);
const double tolerance = 0.000005;
for (int i = 0; i < expectedTargetValues.Length; i++)
{
Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
}
}

private static Cmyk GetExpectedTargetCmyk(string sourceProfileName, string targetProfileName, Cmyk sourceCmyk)
private static double[] GetExpectedTargetValues(string sourceProfile, string targetProfile, float[] input)
{
Wacton.Unicolour.Configuration sourceConfig = TestIccProfiles.GetUnicolourConfiguration(sourceProfileName);
Wacton.Unicolour.Configuration targetConfig = TestIccProfiles.GetUnicolourConfiguration(targetProfileName);
Wacton.Unicolour.Configuration sourceConfig = TestIccProfiles.GetUnicolourConfiguration(sourceProfile);
Wacton.Unicolour.Configuration targetConfig = TestIccProfiles.GetUnicolourConfiguration(targetProfile);

if (sourceConfig.Icc.Error != null || targetConfig.Icc.Error != null)
{
Assert.Fail("Unicolour does not support the ICC profile - test values manually in the meantime");
}

Channels channels = new(sourceCmyk.C, sourceCmyk.M, sourceCmyk.Y, sourceCmyk.K);
Channels channels = new(input.Select(value => (double)value).ToArray());

Unicolour source = new(sourceConfig, channels);
ColourSpace pcs = sourceConfig.Icc.Profile!.Header.Pcs == "Lab " ? ColourSpace.Lab : ColourSpace.Xyz;
ColourTriplet pcsTriplet = pcs == ColourSpace.Lab ? source.Lab.Triplet : source.Xyz.Triplet;
Unicolour target = new(targetConfig, pcs, pcsTriplet.Tuple);
double[] targetCmyk = target.Icc.Values;
return new Cmyk((float)targetCmyk[0], (float)targetCmyk[1], (float)targetCmyk[2], (float)targetCmyk[3]);
Unicolour target = source.ConvertToConfiguration(targetConfig);
return target.Icc.Values;
}

private static float GetNormalizedRandomValue()
Expand All @@ -60,4 +82,12 @@ private static float GetNormalizedRandomValue()
// Clamp the result between 0 and 1 to ensure it does not exceed the bounds.
return value == 0 ? 0F : Math.Clamp((float)value + 0.0000001F, 0, 1);
}

public enum IccConversion
{
CmykToCmyk,
CmykToRgb,
RgbToCmyk,
RgbToRgb
}
}
12 changes: 8 additions & 4 deletions tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ internal static class TestIccProfiles
private static readonly ConcurrentDictionary<string, IccProfile> ProfileCache = new();
private static readonly ConcurrentDictionary<string, Wacton.Unicolour.Configuration> UnicolourConfigurationCache = new();

public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc";

public const string Swop2006 = "SWOP2006_Coated5v2.icc";
public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc"; // v2 CMYK -> LAB, output, lut16
public const string Swop2006 = "SWOP2006_Coated5v2.icc"; // v2 CMYK -> LAB, output, lut16
public const string JapanColor2011 = "JapanColor2011Coated.icc"; // v2 CMYK -> LAB, output, lut8
public const string Cgats21 = "CGATS21_CRPC7.icc"; // v4 CMYK -> LAB, output, lutAToB: B-CLUT-A
public const string RommRgb = "ISO22028-2_ROMM-RGB.icc"; // v4 RGB -> XYZ, colorspace, lutAToB: B-Matrix-M [only intent 0]
public const string StandardRgbV4 = "sRGB_v4_ICC_preference.icc"; // v4 RGB -> LAB, colorspace, lutAToB: B-Matrix-M-CLUT-A [only intent 0 & 1]
public const string StandardRgbV2 = "sRGB2014.icc"; // v2 RGB -> XYZ, display, TRCs

public static IccProfile GetProfile(string file)
=> ProfileCache.GetOrAdd(file, f => new IccProfile(File.ReadAllBytes(GetFullPath(f))));

public static Wacton.Unicolour.Configuration GetUnicolourConfiguration(string file)
=> UnicolourConfigurationCache.GetOrAdd(
file,
f => new Wacton.Unicolour.Configuration(iccConfiguration: new(GetFullPath(f), Intent.Unspecified)));
f => new Wacton.Unicolour.Configuration(iccConfiguration: new(GetFullPath(f), Intent.Unspecified, file)));

private static string GetFullPath(string file)
=> Path.GetFullPath(Path.Combine(".", "TestDataIcc", "Profiles", file));
Expand Down
3 changes: 3 additions & 0 deletions tests/ImageSharp.Tests/TestDataIcc/Profiles/CGATS21_CRPC7.icc
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB2014.icc
Git LFS file not shown
Git LFS file not shown
Loading