diff --git a/src/ImageSharp/ColorProfiles/CieXyz.cs b/src/ImageSharp/ColorProfiles/CieXyz.cs index d64857606e..a74f03ba30 100644 --- a/src/ImageSharp/ColorProfiles/CieXyz.cs +++ b/src/ImageSharp/ColorProfiles/CieXyz.cs @@ -4,7 +4,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.ColorProfiles; @@ -89,6 +88,14 @@ public readonly struct CieXyz : IProfileConnectingSpace [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 ToVector3() => new(this.X, this.Y, this.Z); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Vector4 ToVector4() + { + Vector3 v3 = default; + v3 += this.AsVector3Unsafe(); + return new Vector4(v3, 1F); + } + /// public Vector4 ToScaledVector4() { @@ -98,6 +105,12 @@ public readonly struct CieXyz : IProfileConnectingSpace return new Vector4(v3, 1F); } + internal static CieXyz FromVector4(Vector4 source) + { + Vector3 v3 = source.AsVector3(); + return new CieXyz(v3); + } + /// public static CieXyz FromScaledVector4(Vector4 source) { @@ -130,6 +143,28 @@ public readonly struct CieXyz : IProfileConnectingSpace } } + internal static void FromVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = FromVector4(source[i]); + } + } + + internal static void ToVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: Optimize via SIMD + for (int i = 0; i < source.Length; i++) + { + destination[i] = source[i].ToVector4(); + } + } + /// public static CieXyz FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) => new(source.X, source.Y, source.Z); diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index 35f788a103..cb02472df0 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -64,9 +64,10 @@ internal static class ColorProfileConverterExtensionsIcc TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant), }); + // Normalize the source, then convert to the PCS space. Vector4 sourcePcs = sourceParams.Converter.Calculate(source.ToScaledVector4()); - // if both profiles need PCS adjustment, they both share the same unadjusted PCS space + // If both profiles need PCS adjustment, they both share the same unadjusted PCS space // cancelling out the need to make the adjustment // except if using TRC transforms, which always requires perceptual handling // TODO: this does not include adjustment for absolute intent, which would double existing complexity, suggest throwing exception and addressing in future update @@ -98,6 +99,9 @@ internal static class ColorProfileConverterExtensionsIcc Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination)); + ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true); + ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false); + ColorProfileConverter pcsConverter = new(new ColorConversionOptions { MemoryAllocator = converter.Options.MemoryAllocator, @@ -105,90 +109,32 @@ internal static class ColorProfileConverterExtensionsIcc 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; - using IMemoryOwner pcsBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsNormalized = pcsBuffer.GetSpan(); + Span pcs = pcsBuffer.GetSpan(); - // First normalize the values. - TFrom.ToScaledVector4(source, pcsNormalized); + // Normalize the source, then convert to the PCS space. + TFrom.ToScaledVector4(source, pcs); + sourceParams.Converter.Calculate(pcs, pcs); - // Now convert to the PCS space. - sourceConverter.Calculate(pcsNormalized, pcsNormalized); + // If both profiles need PCS adjustment, they both share the same unadjusted PCS space + // cancelling out the need to make the adjustment + // except if using TRC transforms, which always requires perceptual handling + // TODO: this does not include adjustment for absolute intent, which would double existing complexity, suggest throwing exception and addressing in future update + bool anyProfileNeedsPerceptualAdjustment = sourceParams.HasNoPerceptualHandling || targetParams.HasNoPerceptualHandling; + bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; - // Profile connecting spaces can only be Lab, XYZ. - if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieXyz) + if (anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment) { - // Convert from Lab to XYZ. - using IMemoryOwner pcsFromBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsFrom = pcsFromBuffer.GetSpan(); - CieLab.FromScaledVector4(pcsNormalized, pcsFrom); - - using IMemoryOwner pcsToBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsTo = pcsToBuffer.GetSpan(); - pcsConverter.Convert(pcsFrom, pcsTo); - - // Convert to the target normalized PCS space. - CieXyz.ToScaledVector4(pcsTo, pcsNormalized); + GetTargetPcsWithPerceptualAdjustment(pcs, sourceParams, targetParams, pcsConverter); } - else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieLab) + else { - // Convert from XYZ to Lab. - using IMemoryOwner pcsFromBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsFrom = pcsFromBuffer.GetSpan(); - CieXyz.FromScaledVector4(pcsNormalized, pcsFrom); - - using IMemoryOwner pcsToBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsTo = pcsToBuffer.GetSpan(); - pcsConverter.Convert(pcsFrom, pcsTo); - - // Convert to the target normalized PCS space. - CieLab.ToScaledVector4(pcsTo, pcsNormalized); - } - else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieXyz) - { - // Convert from XYZ to XYZ. - using IMemoryOwner pcsFromToBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsFromTo = pcsFromToBuffer.GetSpan(); - CieXyz.FromScaledVector4(pcsNormalized, pcsFromTo); - - pcsConverter.Convert(pcsFromTo, pcsFromTo); - - // Convert to the target normalized PCS space. - CieXyz.ToScaledVector4(pcsFromTo, pcsNormalized); - } - else if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieLab) - { - // Convert from Lab to Lab. - if (sourceVersion.Major == 4 && targetVersion.Major == 2) - { - // Convert from Lab v4 to Lab v2. - LabToLabV2(pcsNormalized, pcsNormalized); - } - else if (sourceVersion.Major == 2 && targetVersion.Major == 4) - { - // Convert from Lab v2 to Lab v4. - LabV2ToLab(pcsNormalized, pcsNormalized); - } - - using IMemoryOwner pcsFromToBuffer = converter.Options.MemoryAllocator.Allocate(source.Length); - Span pcsFromTo = pcsFromToBuffer.GetSpan(); - CieLab.FromScaledVector4(pcsNormalized, pcsFromTo); - - pcsConverter.Convert(pcsFromTo, pcsFromTo); - - // Convert to the target normalized PCS space. - CieLab.ToScaledVector4(pcsFromTo, pcsNormalized); + GetTargetPcsWithoutAdjustment(pcs, sourceParams, targetParams, pcsConverter); } // Convert to the target space. - targetConverter.Calculate(pcsNormalized, pcsNormalized); - TTo.FromScaledVector4(pcsNormalized, destination); + targetParams.Converter.Calculate(pcs, pcs); + TTo.FromScaledVector4(pcs, destination); } private static Vector4 GetTargetPcsWithoutAdjustment( @@ -253,8 +199,113 @@ internal static class ColorProfileConverterExtensionsIcc } } + private static void GetTargetPcsWithoutAdjustment( + Span pcs, + ConversionParams sourceParams, + ConversionParams targetParams, + ColorProfileConverter pcsConverter) + { + // Profile connecting spaces can only be Lab, XYZ. + // 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version + // so ensure that Lab is using the correct encoding when a 16-bit LUT is used + switch (sourceParams.PcsType) + { + // Convert from Lab to XYZ. + case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieXyz: + { + if (sourceParams.Is16BitLutEntry) + { + LabV2ToLab(pcs, pcs); + } + + using IMemoryOwner pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFrom = pcsFromBuffer.GetSpan(); + + using IMemoryOwner pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsTo = pcsToBuffer.GetSpan(); + + CieLab.FromScaledVector4(pcs, pcsFrom); + pcsConverter.Convert(pcsFrom, pcsTo); + + CieXyz.ToScaledVector4(pcsTo, pcs); + break; + } + + // Convert from XYZ to Lab. + case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab: + { + using IMemoryOwner pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFrom = pcsFromBuffer.GetSpan(); + + using IMemoryOwner pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsTo = pcsToBuffer.GetSpan(); + + CieXyz.FromScaledVector4(pcs, pcsFrom); + pcsConverter.Convert(pcsFrom, pcsTo); + + CieLab.ToScaledVector4(pcsTo, pcs); + + if (targetParams.Is16BitLutEntry) + { + LabToLabV2(pcs, pcs); + } + + break; + } + + // Convert from XYZ to XYZ. + case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz: + { + using IMemoryOwner pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFromTo = pcsFromToBuffer.GetSpan(); + + CieXyz.FromScaledVector4(pcs, pcsFromTo); + pcsConverter.Convert(pcsFromTo, pcsFromTo); + + CieXyz.ToScaledVector4(pcsFromTo, pcs); + break; + } + + // Convert from Lab to Lab. + case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab: + { + using IMemoryOwner pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFromTo = pcsFromToBuffer.GetSpan(); + + // if both source and target LUT use same v2 LAB encoding, no need to correct them + if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry) + { + CieLab.FromScaledVector4(pcs, pcsFromTo); + pcsConverter.Convert(pcsFromTo, pcsFromTo); + CieLab.ToScaledVector4(pcsFromTo, pcs); + } + else + { + if (sourceParams.Is16BitLutEntry) + { + LabV2ToLab(pcs, pcs); + } + + CieLab.FromScaledVector4(pcs, pcsFromTo); + pcsConverter.Convert(pcsFromTo, pcsFromTo); + CieLab.ToScaledVector4(pcsFromTo, pcs); + + if (targetParams.Is16BitLutEntry) + { + LabToLabV2(pcs, pcs); + } + } + + break; + } + + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported"); + } + } + /// - /// Effectively this is with an extra step in the middle. + /// Effectively this is with an extra step in the middle. /// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles. /// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions. /// Not required if both spaces need V2 correction, since they both have the same understanding of the PCS. @@ -339,6 +390,120 @@ internal static class ColorProfileConverterExtensionsIcc } } + /// + /// Effectively this is with an extra step in the middle. + /// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles. + /// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions. + /// Not required if both spaces need V2 correction, since they both have the same understanding of the PCS. + /// Not compatible with PCS adjustment for absolute intent. + /// + /// The PCS values from the source. + /// The source profile parameters. + /// The target profile parameters. + /// The converter to use for the PCS adjustments. + /// Thrown when the source or target PCS is not supported. + private static void GetTargetPcsWithPerceptualAdjustment( + Span pcs, + ConversionParams sourceParams, + ConversionParams targetParams, + ColorProfileConverter pcsConverter) + { + // All conversions are funneled through XYZ in case PCS adjustments need to be made + using IMemoryOwner xyzBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span xyz = xyzBuffer.GetSpan(); + + switch (sourceParams.PcsType) + { + // 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: + { + if (sourceParams.Is16BitLutEntry) + { + LabV2ToLab(pcs, pcs); + } + + using IMemoryOwner pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsFrom = pcsFromBuffer.GetSpan(); + CieLab.FromScaledVector4(pcs, pcsFrom); + pcsConverter.Convert(pcsFrom, xyz); + break; + } + + case IccColorSpaceType.CieXyz: + CieXyz.FromScaledVector4(pcs, xyz); + break; + default: + throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported"); + } + + bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; + + using IMemoryOwner vectorBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span vector = vectorBuffer.GetSpan(); + + // 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 (sourceParams.HasNoPerceptualHandling || + (oneProfileHasV2PerceptualAdjustment && sourceParams.HasV2PerceptualHandling)) + { + CieXyz.ToVector4(xyz, vector); + + // When using LAB PCS, negative values are clipped before PCS adjustment (in DemoIccMAX) + if (sourceParams.PcsType == IccColorSpaceType.CieLab) + { + ClipNegative(vector); + } + + AdjustPcsFromV2BlackPoint(vector, vector); + CieXyz.FromVector4(vector, xyz); + } + + // 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 (targetParams.HasNoPerceptualHandling || + (oneProfileHasV2PerceptualAdjustment && targetParams.HasV2PerceptualHandling)) + { + CieXyz.ToVector4(xyz, vector); + AdjustPcsToV2BlackPoint(vector, vector); + + // When using XYZ PCS, negative values are clipped after PCS adjustment (in DemoIccMAX) + if (targetParams.PcsType == IccColorSpaceType.CieXyz) + { + ClipNegative(vector); + } + + CieXyz.FromVector4(vector, xyz); + } + + switch (targetParams.PcsType) + { + // 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: + { + using IMemoryOwner pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate(pcs.Length); + Span pcsTo = pcsToBuffer.GetSpan(); + pcsConverter.Convert(xyz, pcsTo); + + CieLab.ToScaledVector4(pcsTo, pcs); + + if (targetParams.Is16BitLutEntry) + { + LabToLabV2(pcs, pcs); + } + + break; + } + + case IccColorSpaceType.CieXyz: + CieXyz.ToScaledVector4(xyz, pcs); + break; + default: + throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported"); + } + } + // as per DemoIccMAX icPerceptual values in IccCmm.h // refBlack = 0.00336F, 0.0034731F, 0.00287F // refWhite = 0.9642F, 1.0000F, 0.8249F @@ -451,6 +616,42 @@ internal static class ColorProfileConverterExtensionsIcc } } + private static void ClipNegative(Span source) + { + if (Vector.IsHardwareAccelerated && Vector.IsSupported && Vector.Count >= source.Length * 4) + { + // SIMD loop + int i = 0; + int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch + for (; i <= source.Length - simdBatchSize; i += simdBatchSize) + { + // Load the vector from source span + Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); + + v = Vector.Max(v, Vector.Zero); + + // Write the vector to the destination span + Unsafe.WriteUnaligned(ref Unsafe.As(ref source[i]), v); + } + + // Scalar fallback for remaining elements + for (; i < source.Length; i++) + { + ref Vector4 s = ref source[i]; + s = Vector4.Max(s, Vector4.Zero); + } + } + else + { + // Scalar fallback if SIMD is not supported + for (int i = 0; i < source.Length; i++) + { + ref Vector4 s = ref source[i]; + s = Vector4.Max(s, Vector4.Zero); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector4 LabToLabV2(Vector4 input) => input * 65280F / 65535F;