diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index c25db001f..f9ab31410 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -28,7 +28,7 @@ internal static class ColorProfileConverterExtensionsIcc throw new InvalidOperationException("Target ICC profile is missing."); } - ColorProfileConverter pcsConverter = new(new ColorConversionOptions() + ColorProfileConverter pcsConverter = new(new ColorConversionOptions { MemoryAllocator = converter.Options.MemoryAllocator, SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant), @@ -39,90 +39,28 @@ internal static class ColorProfileConverterExtensionsIcc IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile); 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 funneled through XYZ in case PCS adjustments need to be made - CieXyz xyz; - Vector4 sourcePcs = sourceConverter.Calculate(source.ToScaledVector4()); - switch (sourcePcsType) - { - case IccColorSpaceType.CieLab: - if (sourceConverter.Is16BitLutEntry) - { - sourcePcs = LabV2ToLab(sourcePcs); - } - - CieLab lab = CieLab.FromScaledVector4(sourcePcs); - xyz = pcsConverter.Convert(in lab); - break; - case IccColorSpaceType.CieXyz: - xyz = CieXyz.FromScaledVector4(sourcePcs); - break; - default: - throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported"); - } - - // 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; + Vector4 targetPcs; // 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) + // TODO: handle PCS adjustment for absolute intent? would make this a lot more complicated + // 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) + bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent.Perceptual && sourceVersion.Major == 2; + bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent.Perceptual && targetVersion.Major == 2; + if (adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual) { - // 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) - { - Vector3 iccXyz = xyz.ToScaledVector4().AsVector3(); - Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()); - Vector3 offset = refBlack.ToScaledVector4().AsVector3(); - Vector3 adjustedXyz = (iccXyz * scale) + offset; - xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F)); - } - - if (adjustTargetPcsForPerceptual) - { - Vector3 iccXyz = xyz.ToScaledVector4().AsVector3(); - Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3())); - Vector3 offset = -refBlack.ToScaledVector4().AsVector3() * scale; - Vector3 adjustedXyz = (iccXyz * scale) + offset; - xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F)); - } + targetPcs = GetTargetPcsWithPerceptualV2Adjustment(converter, sourcePcs, adjustSourcePcsForPerceptual, adjustTargetPcsForPerceptual, pcsConverter); } - - Vector4 targetPcs; - switch (targetPcsType) + else { - case IccColorSpaceType.CieLab: - CieLab lab = pcsConverter.Convert(in xyz); - targetPcs = lab.ToScaledVector4(); - if (adjustTargetPcsForPerceptual) - { - targetPcs = LabToLabV2(targetPcs); - } - else if (targetConverter.Is16BitLutEntry) - { - // TODO: find a way to test? needs a non-perceptual-intent v2 profile with 8-bit LUT... - targetPcs = LabToLabV2(targetPcs); - } - - break; - case IccColorSpaceType.CieXyz: - targetPcs = xyz.ToScaledVector4(); - break; - default: - throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported"); + // TODO: replace with function that bypasses PCS adjustment + targetPcs = GetTargetPcsWithPerceptualV2Adjustment(converter, sourcePcs, adjustSourcePcsForPerceptual, adjustTargetPcsForPerceptual, pcsConverter); } // Convert to the target space. @@ -243,6 +181,85 @@ internal static class ColorProfileConverterExtensionsIcc TTo.FromScaledVector4(pcsNormalized, destination); } + private static Vector4 GetTargetPcsWithPerceptualV2Adjustment( + ColorProfileConverter converter, + Vector4 sourcePcs, + bool adjustSource, + bool adjustTarget, + ColorProfileConverter pcsConverter) + { + IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile!); + IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile!); + IccProfileHeader sourceHeader = converter.Options.SourceIccProfile!.Header; + IccProfileHeader targetHeader = converter.Options.TargetIccProfile!.Header; + IccColorSpaceType sourcePcsType = sourceHeader.ProfileConnectionSpace; + IccColorSpaceType targetPcsType = targetHeader.ProfileConnectionSpace; + + // all conversions are funneled through XYZ in case PCS adjustments need to be made + CieXyz xyz; + + switch (sourcePcsType) + { + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so convert Lab to modern v4 encoding when returned from a 16-bit LUT + case IccColorSpaceType.CieLab: + sourcePcs = sourceConverter.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; + CieLab lab = CieLab.FromScaledVector4(sourcePcs); + xyz = pcsConverter.Convert(in lab); + break; + case IccColorSpaceType.CieXyz: + xyz = CieXyz.FromScaledVector4(sourcePcs); + break; + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported"); + } + + // 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); + + // when converting from device to PCS with v2 perceptual intent + // the black point needs to be adjusted to v4 after converting the PCS values + if (adjustSource) + { + Vector3 iccXyz = xyz.ToScaledVector4().AsVector3(); + Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()); + Vector3 offset = refBlack.ToScaledVector4().AsVector3(); + Vector3 adjustedXyz = (iccXyz * scale) + offset; + xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F)); + } + + // when converting from PCS to device with v2 perceptual intent + // the black point needs to be adjusted to v2 before converting the PCS values + if (adjustTarget) + { + Vector3 iccXyz = xyz.ToScaledVector4().AsVector3(); + Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3())); + Vector3 offset = -refBlack.ToScaledVector4().AsVector3() * scale; + Vector3 adjustedXyz = (iccXyz * scale) + offset; + xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F)); + } + + Vector4 targetPcs; + switch (targetPcsType) + { + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so convert Lab back to legacy encoding before using in a 16-bit LUT + case IccColorSpaceType.CieLab: + CieLab lab = pcsConverter.Convert(in xyz); + targetPcs = lab.ToScaledVector4(); + targetPcs = targetConverter.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; + break; + case IccColorSpaceType.CieXyz: + targetPcs = xyz.ToScaledVector4(); + break; + default: + throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported"); + } + + return targetPcs; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector4 LabToLabV2(Vector4 input) => input * 65280F / 65535F; diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs index 87d5e82e5..50eae309a 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs @@ -17,7 +17,6 @@ public class ColorProfileConverterTests [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) - [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2011)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit) [InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit) [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4) [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4) @@ -25,6 +24,7 @@ public class ColorProfileConverterTests [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M) [InlineData(TestIccProfiles.RommRgb, TestIccProfiles.StandardRgbV4)] // RGB -> XYZ -> LAB -> RGB (different LUT elements, B-Matrix-M vs B-Matrix-M-CLUT-A) // TODO: enable once supported by Unicolour - in the meantime, manually test known values + // [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2003)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit) // [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2)] // CMYK -> XYZ -> LAB -> RGB (different LUT tags, A2B vs TRC) // [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B) public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile) diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs index 201026e79..b9e19be05 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs @@ -22,10 +22,15 @@ internal static class TestIccProfiles public const string Swop2006 = "SWOP2006_Coated5v2.icc"; /// - /// v2 CMYK -> LAB, output, lut8 + /// v2 CMYK -> LAB, output, lut8 (A2B tags) /// public const string JapanColor2011 = "JapanColor2011Coated.icc"; + /// + /// v2 CMYK -> LAB, output, lut8 (B2A tags) + /// + public const string JapanColor2003 = "JapanColor2003WebCoated.icc"; + /// /// v4 CMYK -> LAB, output, lutAToB: B-CLUT-A /// diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2003WebCoated.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2003WebCoated.icc new file mode 100644 index 000000000..0cc501bea Binary files /dev/null and b/tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2003WebCoated.icc differ