Skip to content

Commit d60ac76

Browse files
committed
Bypass PCS adjustment when not needed
1 parent d20fddb commit d60ac76

File tree

2 files changed

+115
-40
lines changed

2 files changed

+115
-40
lines changed

src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Numerics;
77
using System.Runtime.CompilerServices;
88
using SixLabors.ImageSharp.ColorProfiles.Icc;
9+
using SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;
910
using SixLabors.ImageSharp.Memory;
1011
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
1112

@@ -28,44 +29,34 @@ internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverte
2829
throw new InvalidOperationException("Target ICC profile is missing.");
2930
}
3031

32+
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
33+
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
34+
3135
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
3236
{
3337
MemoryAllocator = converter.Options.MemoryAllocator,
3438
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
3539
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
3640
});
3741

38-
IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile);
39-
IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile);
40-
IccProfileHeader sourceHeader = converter.Options.SourceIccProfile.Header;
41-
IccProfileHeader targetHeader = converter.Options.TargetIccProfile.Header;
42-
IccRenderingIntent sourceIntent = sourceHeader.RenderingIntent;
43-
IccRenderingIntent targetIntent = targetHeader.RenderingIntent;
44-
IccVersion sourceVersion = sourceHeader.Version;
45-
IccVersion targetVersion = targetHeader.Version;
46-
47-
Vector4 sourcePcs = sourceConverter.Calculate(source.ToScaledVector4());
42+
Vector4 sourcePcs = sourceParams.Converter.Calculate(source.ToScaledVector4());
4843
Vector4 targetPcs;
4944

5045
// if both profiles need PCS adjustment, they both share the same unadjusted PCS space
51-
// effectively cancelling out the need to make the adjustment
46+
// cancelling out the need to make the adjustment
5247
// TODO: handle PCS adjustment for absolute intent? would make this a lot more complicated
5348
// TODO: alternatively throw unsupported error, since most profiles headers contain perceptual (i've encountered a couple of relative intent, but so far no saturation or absolute)
54-
bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent.Perceptual && sourceVersion.Major == 2;
55-
bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent.Perceptual && targetVersion.Major == 2;
56-
if (adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual)
49+
if (sourceParams.AdjustPcsForPerceptual ^ targetParams.AdjustPcsForPerceptual)
5750
{
58-
targetPcs = GetTargetPcsWithPerceptualV2Adjustment(converter, sourcePcs, adjustSourcePcsForPerceptual, adjustTargetPcsForPerceptual, pcsConverter);
51+
targetPcs = GetTargetPcsWithPerceptualV2Adjustment(sourcePcs, sourceParams, targetParams, pcsConverter);
5952
}
6053
else
6154
{
62-
// TODO: replace with function that bypasses PCS adjustment
63-
targetPcs = GetTargetPcsWithPerceptualV2Adjustment(converter, sourcePcs, adjustSourcePcsForPerceptual, adjustTargetPcsForPerceptual, pcsConverter);
55+
targetPcs = GetTargetPcsWithoutAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter);
6456
}
6557

6658
// Convert to the target space.
67-
Vector4 targetValue = targetConverter.Calculate(targetPcs);
68-
return TTo.FromScaledVector4(targetValue);
59+
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
6960
}
7061

7162
// TODO: update to match workflow of the function above
@@ -181,68 +172,129 @@ internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConvert
181172
TTo.FromScaledVector4(pcsNormalized, destination);
182173
}
183174

184-
private static Vector4 GetTargetPcsWithPerceptualV2Adjustment(
185-
ColorProfileConverter converter,
175+
private static Vector4 GetTargetPcsWithoutAdjustment(
186176
Vector4 sourcePcs,
187-
bool adjustSource,
188-
bool adjustTarget,
177+
ConversionParams sourceParams,
178+
ConversionParams targetParams,
189179
ColorProfileConverter pcsConverter)
190180
{
191-
IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile!);
192-
IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile!);
193-
IccProfileHeader sourceHeader = converter.Options.SourceIccProfile!.Header;
194-
IccProfileHeader targetHeader = converter.Options.TargetIccProfile!.Header;
195-
IccColorSpaceType sourcePcsType = sourceHeader.ProfileConnectionSpace;
196-
IccColorSpaceType targetPcsType = targetHeader.ProfileConnectionSpace;
181+
// Profile connecting spaces can only be Lab, XYZ.
182+
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
183+
// so ensure that Lab is using the correct encoding when a 16-bit LUT is used
184+
switch (sourceParams.PcsType)
185+
{
186+
// Convert from Lab to XYZ.
187+
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieXyz:
188+
{
189+
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
190+
CieLab lab = CieLab.FromScaledVector4(sourcePcs);
191+
CieXyz xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
192+
return xyz.ToScaledVector4();
193+
}
194+
195+
// Convert from XYZ to Lab.
196+
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab:
197+
{
198+
CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs);
199+
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
200+
Vector4 targetPcs = lab.ToScaledVector4();
201+
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
202+
}
203+
204+
// Convert from XYZ to XYZ.
205+
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz:
206+
{
207+
CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs);
208+
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
209+
return targetXyz.ToScaledVector4();
210+
}
211+
212+
// Convert from Lab to Lab.
213+
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab:
214+
{
215+
// if both source and target LUT use same v2 LAB encoding, no need to correct them
216+
if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry)
217+
{
218+
CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs);
219+
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in sourceLab);
220+
return targetLab.ToScaledVector4();
221+
}
222+
else
223+
{
224+
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
225+
CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs);
226+
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in sourceLab);
227+
Vector4 targetPcs = targetLab.ToScaledVector4();
228+
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
229+
}
230+
}
231+
232+
default:
233+
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported");
234+
}
235+
}
197236

237+
/// <summary>
238+
/// Effectively this is <see cref="GetTargetPcsWithoutAdjustment"/> with an extra step in the middle.
239+
/// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles.
240+
/// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions.
241+
/// Not required if both spaces need adjustment, since they both have the same understanding of the PCS.
242+
/// Not compatible with PCS adjustment for absolute intent.
243+
/// </summary>
244+
private static Vector4 GetTargetPcsWithPerceptualV2Adjustment(
245+
Vector4 sourcePcs,
246+
ConversionParams sourceParams,
247+
ConversionParams targetParams,
248+
ColorProfileConverter pcsConverter)
249+
{
198250
// all conversions are funneled through XYZ in case PCS adjustments need to be made
199251
CieXyz xyz;
200252

201-
switch (sourcePcsType)
253+
switch (sourceParams.PcsType)
202254
{
203255
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
204256
// so convert Lab to modern v4 encoding when returned from a 16-bit LUT
205257
case IccColorSpaceType.CieLab:
206-
sourcePcs = sourceConverter.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
258+
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
207259
CieLab lab = CieLab.FromScaledVector4(sourcePcs);
208260
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
209261
break;
210262
case IccColorSpaceType.CieXyz:
211263
xyz = CieXyz.FromScaledVector4(sourcePcs);
212264
break;
213265
default:
214-
throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported");
266+
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported");
215267
}
216268

217269
// when converting from device to PCS with v2 perceptual intent
218270
// the black point needs to be adjusted to v4 after converting the PCS values
219-
if (adjustSource)
271+
if (sourceParams.AdjustPcsForPerceptual)
220272
{
221273
xyz = new CieXyz(AdjustPcsFromV2BlackPoint(xyz.ToVector3()));
222274
}
223275

224276
// when converting from PCS to device with v2 perceptual intent
225277
// the black point needs to be adjusted to v2 before converting the PCS values
226-
if (adjustTarget)
278+
if (targetParams.AdjustPcsForPerceptual)
227279
{
228280
xyz = new CieXyz(AdjustPcsToV2BlackPoint(xyz.ToVector3()));
229281
}
230282

231283
Vector4 targetPcs;
232-
switch (targetPcsType)
284+
switch (targetParams.PcsType)
233285
{
234286
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
235287
// so convert Lab back to legacy encoding before using in a 16-bit LUT
236288
case IccColorSpaceType.CieLab:
237289
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
238290
targetPcs = lab.ToScaledVector4();
239-
targetPcs = targetConverter.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
291+
targetPcs = targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
240292
break;
241293
case IccColorSpaceType.CieXyz:
242294
targetPcs = xyz.ToScaledVector4();
243295
break;
244296
default:
245-
throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported");
297+
throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported");
246298
}
247299

248300
return targetPcs;
@@ -314,4 +366,29 @@ private static void LabToLab(Span<Vector4> source, Span<Vector4> destination, [C
314366
}
315367
}
316368
}
369+
370+
private class ConversionParams
371+
{
372+
private readonly IccProfile profile;
373+
374+
internal ConversionParams(IccProfile profile, bool toPcs)
375+
{
376+
this.profile = profile;
377+
this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile);
378+
}
379+
380+
internal IccConverterBase Converter { get; }
381+
382+
internal IccProfileHeader Header => this.profile.Header;
383+
384+
internal IccRenderingIntent Intent => this.Header.RenderingIntent;
385+
386+
internal IccColorSpaceType PcsType => this.Header.ProfileConnectionSpace;
387+
388+
internal IccVersion Version => this.Header.Version;
389+
390+
internal bool AdjustPcsForPerceptual => this.Intent == IccRenderingIntent.Perceptual && this.Version.Major == 2;
391+
392+
internal bool Is16BitLutEntry => this.Converter.Is16BitLutEntry;
393+
}
317394
}

tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ public class ColorProfileConverterTests
2929
// [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
3030
public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile)
3131
{
32-
// TODO: delete after testing
33-
float[] input = [0.734798908f, 0.887050927f, 0.476583719f, 0.547810674f];
34-
// float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()];
32+
float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()];
3533
double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input);
3634

3735
ColorProfileConverter converter = new(new ColorConversionOptions

0 commit comments

Comments
 (0)