mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
1422 changed files with 31121 additions and 15085 deletions
@ -1 +1 @@ |
|||
Subproject commit 1dbfb576c83507645265c79e03369b66cdc0379f |
|||
Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947 |
|||
@ -0,0 +1,40 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <summary>
|
|||
/// Specifies the channel order when formatting or parsing a color as a hexadecimal string.
|
|||
/// </summary>
|
|||
public enum ColorHexFormat |
|||
{ |
|||
/// <summary>
|
|||
/// Uses <c>RRGGBBAA</c> channel order where the red, green, and blue components come first,
|
|||
/// followed by the alpha component. This matches the CSS Color Module Level 4 and common web standards.
|
|||
/// <para>
|
|||
/// When parsing, supports the following formats:
|
|||
/// <list type="bullet">
|
|||
/// <item><description><c>#RGB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
|
|||
/// <item><description><c>#RGBA</c> expands to <c>RRGGBBAA</c></description></item>
|
|||
/// <item><description><c>#RRGGBB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
|
|||
/// <item><description><c>#RRGGBBAA</c> used as-is</description></item>
|
|||
/// </list>
|
|||
/// </para>
|
|||
/// When formatting, outputs an 8-digit hex string in <c>RRGGBBAA</c> order.
|
|||
/// </summary>
|
|||
Rgba, |
|||
|
|||
/// <summary>
|
|||
/// Uses <c>AARRGGBB</c> channel order where the alpha component comes first,
|
|||
/// followed by the red, green, and blue components. This matches the Microsoft/XAML convention.
|
|||
/// <para>
|
|||
/// When parsing, supports the following formats:
|
|||
/// <list type="bullet">
|
|||
/// <item><description><c>#ARGB</c> expands to <c>AARRGGBB</c></description></item>
|
|||
/// <item><description><c>#AARRGGBB</c> used as-is</description></item>
|
|||
/// </list>
|
|||
/// </para>
|
|||
/// When formatting, outputs an 8-digit hex string in <c>AARRGGBB</c> order.
|
|||
/// </summary>
|
|||
Argb |
|||
} |
|||
@ -0,0 +1,772 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.Intrinsics; |
|||
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
using SixLabors.ImageSharp.ColorProfiles.Icc; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
internal static class ColorProfileConverterExtensionsIcc |
|||
{ |
|||
private static readonly float[] PcsV2FromBlackPointScale = |
|||
[0.9965153F, 0.9965269F, 0.9965208F, 1F, |
|||
0.9965153F, 0.9965269F, 0.9965208F, 1F, |
|||
0.9965153F, 0.9965269F, 0.9965208F, 1F, |
|||
0.9965153F, 0.9965269F, 0.9965208F, 1F]; |
|||
|
|||
private static readonly float[] PcsV2FromBlackPointOffset = |
|||
[0.00336F, 0.0034731F, 0.00287F, 0F, |
|||
0.00336F, 0.0034731F, 0.00287F, 0F, |
|||
0.00336F, 0.0034731F, 0.00287F, 0F, |
|||
0.00336F, 0.0034731F, 0.00287F, 0F]; |
|||
|
|||
private static readonly float[] PcsV2ToBlackPointScale = |
|||
[1.0034969F, 1.0034852F, 1.0034913F, 1F, |
|||
1.0034969F, 1.0034852F, 1.0034913F, 1F, |
|||
1.0034969F, 1.0034852F, 1.0034913F, 1F, |
|||
1.0034969F, 1.0034852F, 1.0034913F, 1F]; |
|||
|
|||
private static readonly float[] PcsV2ToBlackPointOffset = |
|||
[0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, |
|||
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, |
|||
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, |
|||
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F]; |
|||
|
|||
/// <summary>
|
|||
/// Converts a color value from one ICC color profile to another using the specified color profile converter.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method performs color conversion using ICC profiles, ensuring accurate color mapping
|
|||
/// between different color spaces. Both the source and target ICC profiles must be provided in the converter's
|
|||
/// options. The method supports perceptual adjustments when required by the profiles.
|
|||
/// </remarks>
|
|||
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
|
|||
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
|
|||
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
|
|||
/// <param name="source">The color value to convert, defined in the source color profile.</param>
|
|||
/// <returns>
|
|||
/// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value.
|
|||
/// </returns>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// Thrown if either the source or target ICC profile is missing from the converter options.
|
|||
/// </exception>
|
|||
internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source) |
|||
where TFrom : struct, IColorProfile<TFrom> |
|||
where TTo : struct, IColorProfile<TTo> |
|||
{ |
|||
// TODO: Validation of ICC Profiles against color profile. Is this possible?
|
|||
if (converter.Options.SourceIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Source ICC profile is missing."); |
|||
} |
|||
|
|||
if (converter.Options.TargetIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Target ICC profile is missing."); |
|||
} |
|||
|
|||
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, |
|||
SourceWhitePoint = KnownIlluminants.D50Icc, |
|||
TargetWhitePoint = KnownIlluminants.D50Icc |
|||
}); |
|||
|
|||
// 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
|
|||
// 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; |
|||
|
|||
Vector4 targetPcs = anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment |
|||
? GetTargetPcsWithPerceptualAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter) |
|||
: GetTargetPcsWithoutAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter); |
|||
|
|||
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a span of color values from a source color profile to a destination color profile using ICC profiles.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method performs color conversion by transforming the input values through the Profile
|
|||
/// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required
|
|||
/// by the profiles. The method does not support absolute colorimetric intent and will not perform such
|
|||
/// conversions.
|
|||
/// </remarks>
|
|||
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
|
|||
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
|
|||
/// <param name="converter">The color profile converter that provides conversion options and ICC profiles.</param>
|
|||
/// <param name="source">
|
|||
/// A read-only span containing the source color values to convert. The values must conform to the source color
|
|||
/// profile.
|
|||
/// </param>
|
|||
/// <param name="destination">
|
|||
/// A span to receive the converted color values in the destination color profile. Must be at least as large as the
|
|||
/// source span.
|
|||
/// </param>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// Thrown if the source or target ICC profile is missing from the converter options.
|
|||
/// </exception>
|
|||
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> |
|||
{ |
|||
// TODO: Validation of ICC Profiles against color profile. Is this possible?
|
|||
if (converter.Options.SourceIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Source ICC profile is missing."); |
|||
} |
|||
|
|||
if (converter.Options.TargetIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Target ICC profile is missing."); |
|||
} |
|||
|
|||
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, |
|||
SourceWhitePoint = KnownIlluminants.D50Icc, |
|||
TargetWhitePoint = KnownIlluminants.D50Icc |
|||
}); |
|||
|
|||
using IMemoryOwner<Vector4> pcsBuffer = converter.Options.MemoryAllocator.Allocate<Vector4>(source.Length); |
|||
Span<Vector4> pcs = pcsBuffer.GetSpan(); |
|||
|
|||
// Normalize the source, then convert to the PCS space.
|
|||
TFrom.ToScaledVector4(source, pcs); |
|||
sourceParams.Converter.Calculate(pcs, pcs); |
|||
|
|||
// 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; |
|||
|
|||
if (anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment) |
|||
{ |
|||
GetTargetPcsWithPerceptualAdjustment(pcs, sourceParams, targetParams, pcsConverter); |
|||
} |
|||
else |
|||
{ |
|||
GetTargetPcsWithoutAdjustment(pcs, sourceParams, targetParams, pcsConverter); |
|||
} |
|||
|
|||
// Convert to the target space.
|
|||
targetParams.Converter.Calculate(pcs, pcs); |
|||
TTo.FromScaledVector4(pcs, destination); |
|||
} |
|||
|
|||
private static Vector4 GetTargetPcsWithoutAdjustment( |
|||
Vector4 sourcePcs, |
|||
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: |
|||
{ |
|||
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; |
|||
CieLab lab = CieLab.FromScaledVector4(sourcePcs); |
|||
CieXyz xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab); |
|||
return xyz.ToScaledVector4(); |
|||
} |
|||
|
|||
// Convert from XYZ to Lab.
|
|||
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab: |
|||
{ |
|||
CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs); |
|||
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz); |
|||
Vector4 targetPcs = lab.ToScaledVector4(); |
|||
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; |
|||
} |
|||
|
|||
// Convert from XYZ to XYZ.
|
|||
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz: |
|||
{ |
|||
CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs); |
|||
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz); |
|||
return targetXyz.ToScaledVector4(); |
|||
} |
|||
|
|||
// Convert from Lab to Lab.
|
|||
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab: |
|||
{ |
|||
// if both source and target LUT use same v2 LAB encoding, no need to correct them
|
|||
if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry) |
|||
{ |
|||
CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs); |
|||
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in sourceLab); |
|||
return targetLab.ToScaledVector4(); |
|||
} |
|||
else |
|||
{ |
|||
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; |
|||
CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs); |
|||
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in sourceLab); |
|||
Vector4 targetPcs = targetLab.ToScaledVector4(); |
|||
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; |
|||
} |
|||
} |
|||
|
|||
default: |
|||
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported"); |
|||
} |
|||
} |
|||
|
|||
private static void GetTargetPcsWithoutAdjustment( |
|||
Span<Vector4> 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<CieLab> pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length); |
|||
Span<CieLab> pcsFrom = pcsFromBuffer.GetSpan(); |
|||
|
|||
using IMemoryOwner<CieXyz> pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length); |
|||
Span<CieXyz> pcsTo = pcsToBuffer.GetSpan(); |
|||
|
|||
CieLab.FromScaledVector4(pcs, pcsFrom); |
|||
pcsConverter.Convert<CieLab, CieXyz>(pcsFrom, pcsTo); |
|||
|
|||
CieXyz.ToScaledVector4(pcsTo, pcs); |
|||
break; |
|||
} |
|||
|
|||
// Convert from XYZ to Lab.
|
|||
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab: |
|||
{ |
|||
using IMemoryOwner<CieXyz> pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length); |
|||
Span<CieXyz> pcsFrom = pcsFromBuffer.GetSpan(); |
|||
|
|||
using IMemoryOwner<CieLab> pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length); |
|||
Span<CieLab> pcsTo = pcsToBuffer.GetSpan(); |
|||
|
|||
CieXyz.FromScaledVector4(pcs, pcsFrom); |
|||
pcsConverter.Convert<CieXyz, CieLab>(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<CieXyz> pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length); |
|||
Span<CieXyz> pcsFromTo = pcsFromToBuffer.GetSpan(); |
|||
|
|||
CieXyz.FromScaledVector4(pcs, pcsFromTo); |
|||
pcsConverter.Convert<CieXyz, CieXyz>(pcsFromTo, pcsFromTo); |
|||
|
|||
CieXyz.ToScaledVector4(pcsFromTo, pcs); |
|||
break; |
|||
} |
|||
|
|||
// Convert from Lab to Lab.
|
|||
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab: |
|||
{ |
|||
using IMemoryOwner<CieLab> pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length); |
|||
Span<CieLab> 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<CieLab, CieLab>(pcsFromTo, pcsFromTo); |
|||
CieLab.ToScaledVector4(pcsFromTo, pcs); |
|||
} |
|||
else |
|||
{ |
|||
if (sourceParams.Is16BitLutEntry) |
|||
{ |
|||
LabV2ToLab(pcs, pcs); |
|||
} |
|||
|
|||
CieLab.FromScaledVector4(pcs, pcsFromTo); |
|||
pcsConverter.Convert<CieLab, CieLab>(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"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Effectively this is <see cref="GetTargetPcsWithoutAdjustment(Vector4, ConversionParams, ConversionParams, ColorProfileConverter)"/> 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.
|
|||
/// </summary>
|
|||
/// <param name="sourcePcs">The source PCS values.</param>
|
|||
/// <param name="sourceParams">The source profile parameters.</param>
|
|||
/// <param name="targetParams">The target profile parameters.</param>
|
|||
/// <param name="pcsConverter">The converter to use for the PCS adjustments.</param>
|
|||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the source or target PCS is not supported.</exception>
|
|||
private static Vector4 GetTargetPcsWithPerceptualAdjustment( |
|||
Vector4 sourcePcs, |
|||
ConversionParams sourceParams, |
|||
ConversionParams targetParams, |
|||
ColorProfileConverter pcsConverter) |
|||
{ |
|||
// all conversions are funneled through XYZ in case PCS adjustments need to be made
|
|||
CieXyz xyz; |
|||
|
|||
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: |
|||
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs; |
|||
CieLab lab = CieLab.FromScaledVector4(sourcePcs); |
|||
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab); |
|||
break; |
|||
case IccColorSpaceType.CieXyz: |
|||
xyz = CieXyz.FromScaledVector4(sourcePcs); |
|||
break; |
|||
default: |
|||
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported"); |
|||
} |
|||
|
|||
bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling; |
|||
|
|||
// 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)) |
|||
{ |
|||
Vector3 vector = xyz.ToVector3(); |
|||
|
|||
// when using LAB PCS, negative values are clipped before PCS adjustment (in DemoIccMAX)
|
|||
if (sourceParams.PcsType == IccColorSpaceType.CieLab) |
|||
{ |
|||
vector = Vector3.Max(vector, Vector3.Zero); |
|||
} |
|||
|
|||
xyz = new CieXyz(AdjustPcsFromV2BlackPoint(vector)); |
|||
} |
|||
|
|||
// 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)) |
|||
{ |
|||
Vector3 vector = AdjustPcsToV2BlackPoint(xyz.AsVector3Unsafe()); |
|||
|
|||
// when using XYZ PCS, negative values are clipped after PCS adjustment (in DemoIccMAX)
|
|||
if (targetParams.PcsType == IccColorSpaceType.CieXyz) |
|||
{ |
|||
vector = Vector3.Max(vector, Vector3.Zero); |
|||
} |
|||
|
|||
xyz = new CieXyz(vector); |
|||
} |
|||
|
|||
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: |
|||
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz); |
|||
Vector4 targetPcs = lab.ToScaledVector4(); |
|||
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs; |
|||
case IccColorSpaceType.CieXyz: |
|||
return xyz.ToScaledVector4(); |
|||
default: |
|||
throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Effectively this is <see cref="GetTargetPcsWithoutAdjustment(Span{Vector4}, ConversionParams, ConversionParams, ColorProfileConverter)"/> 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.
|
|||
/// </summary>
|
|||
/// <param name="pcs">The PCS values from the source.</param>
|
|||
/// <param name="sourceParams">The source profile parameters.</param>
|
|||
/// <param name="targetParams">The target profile parameters.</param>
|
|||
/// <param name="pcsConverter">The converter to use for the PCS adjustments.</param>
|
|||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the source or target PCS is not supported.</exception>
|
|||
private static void GetTargetPcsWithPerceptualAdjustment( |
|||
Span<Vector4> pcs, |
|||
ConversionParams sourceParams, |
|||
ConversionParams targetParams, |
|||
ColorProfileConverter pcsConverter) |
|||
{ |
|||
// All conversions are funneled through XYZ in case PCS adjustments need to be made
|
|||
using IMemoryOwner<CieXyz> xyzBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length); |
|||
Span<CieXyz> 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<CieLab> pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length); |
|||
Span<CieLab> pcsFrom = pcsFromBuffer.GetSpan(); |
|||
CieLab.FromScaledVector4(pcs, pcsFrom); |
|||
pcsConverter.Convert<CieLab, CieXyz>(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<Vector4> vectorBuffer = pcsConverter.Options.MemoryAllocator.Allocate<Vector4>(pcs.Length); |
|||
Span<Vector4> 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<CieLab> pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length); |
|||
Span<CieLab> pcsTo = pcsToBuffer.GetSpan(); |
|||
pcsConverter.Convert<CieXyz, CieLab>(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
|
|||
// scale = 1 - (refBlack / refWhite)
|
|||
// offset = refBlack
|
|||
private static Vector3 AdjustPcsFromV2BlackPoint(Vector3 xyz) |
|||
=> (xyz * new Vector3(0.9965153F, 0.9965269F, 0.9965208F)) + new Vector3(0.00336F, 0.0034731F, 0.00287F); |
|||
|
|||
// as per DemoIccMAX icPerceptual values in IccCmm.h
|
|||
// refBlack = 0.00336F, 0.0034731F, 0.00287F
|
|||
// refWhite = 0.9642F, 1.0000F, 0.8249F
|
|||
// scale = 1 / (1 - (refBlack / refWhite))
|
|||
// offset = -refBlack * scale
|
|||
private static Vector3 AdjustPcsToV2BlackPoint(Vector3 xyz) |
|||
=> (xyz * new Vector3(1.0034969F, 1.0034852F, 1.0034913F)) - new Vector3(0.0033717495F, 0.0034852044F, 0.0028800198F); |
|||
|
|||
private static void AdjustPcsFromV2BlackPoint(Span<Vector4> source, Span<Vector4> destination) |
|||
{ |
|||
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported && |
|||
Vector<float>.Count <= Vector512<float>.Count && |
|||
source.Length * 4 >= Vector<float>.Count) |
|||
{ |
|||
// TODO: Check our constants. They may require scaling.
|
|||
Vector<float> vScale = new(PcsV2FromBlackPointScale.AsSpan()[..Vector<float>.Count]); |
|||
Vector<float> vOffset = new(PcsV2FromBlackPointOffset.AsSpan()[..Vector<float>.Count]); |
|||
|
|||
// SIMD loop
|
|||
int i = 0; |
|||
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
|
|||
for (; i <= source.Length - simdBatchSize; i += simdBatchSize) |
|||
{ |
|||
// Load the vector from source span
|
|||
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i])); |
|||
|
|||
// Scale and offset the vector
|
|||
v *= vScale; |
|||
v += vOffset; |
|||
|
|||
// Write the vector to the destination span
|
|||
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v); |
|||
} |
|||
|
|||
// Scalar fallback for remaining elements
|
|||
for (; i < source.Length; i++) |
|||
{ |
|||
Vector4 s = source[i]; |
|||
s *= new Vector4(0.9965153F, 0.9965269F, 0.9965208F, 1F); |
|||
s += new Vector4(0.00336F, 0.0034731F, 0.00287F, 0F); |
|||
destination[i] = s; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Scalar fallback if SIMD is not supported
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
Vector4 s = source[i]; |
|||
s *= new Vector4(0.9965153F, 0.9965269F, 0.9965208F, 1F); |
|||
s += new Vector4(0.00336F, 0.0034731F, 0.00287F, 0F); |
|||
destination[i] = s; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void AdjustPcsToV2BlackPoint(Span<Vector4> source, Span<Vector4> destination) |
|||
{ |
|||
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported && |
|||
Vector<float>.Count <= Vector512<float>.Count && |
|||
source.Length * 4 >= Vector<float>.Count) |
|||
{ |
|||
// TODO: Check our constants. They may require scaling.
|
|||
Vector<float> vScale = new(PcsV2ToBlackPointScale.AsSpan()[..Vector<float>.Count]); |
|||
Vector<float> vOffset = new(PcsV2ToBlackPointOffset.AsSpan()[..Vector<float>.Count]); |
|||
|
|||
// SIMD loop
|
|||
int i = 0; |
|||
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
|
|||
for (; i <= source.Length - simdBatchSize; i += simdBatchSize) |
|||
{ |
|||
// Load the vector from source span
|
|||
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i])); |
|||
|
|||
// Scale and offset the vector
|
|||
v *= vScale; |
|||
v -= vOffset; |
|||
|
|||
// Write the vector to the destination span
|
|||
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v); |
|||
} |
|||
|
|||
// Scalar fallback for remaining elements
|
|||
for (; i < source.Length; i++) |
|||
{ |
|||
Vector4 s = source[i]; |
|||
s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F); |
|||
s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); |
|||
destination[i] = s; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Scalar fallback if SIMD is not supported
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
Vector4 s = source[i]; |
|||
s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F); |
|||
s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); |
|||
destination[i] = s; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void ClipNegative(Span<Vector4> source) |
|||
{ |
|||
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported && Vector<float>.Count >= source.Length * 4) |
|||
{ |
|||
// SIMD loop
|
|||
int i = 0; |
|||
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
|
|||
for (; i <= source.Length - simdBatchSize; i += simdBatchSize) |
|||
{ |
|||
// Load the vector from source span
|
|||
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i])); |
|||
|
|||
v = Vector.Max(v, Vector<float>.Zero); |
|||
|
|||
// Write the vector to the destination span
|
|||
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(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; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static Vector4 LabV2ToLab(Vector4 input) |
|||
=> input * 65535F / 65280F; |
|||
|
|||
private static void LabToLabV2(Span<Vector4> source, Span<Vector4> destination) |
|||
=> LabToLab(source, destination, 65280F / 65535F); |
|||
|
|||
private static void LabV2ToLab(Span<Vector4> source, Span<Vector4> destination) |
|||
=> LabToLab(source, destination, 65535F / 65280F); |
|||
|
|||
private static void LabToLab(Span<Vector4> source, Span<Vector4> destination, [ConstantExpected] float scale) |
|||
{ |
|||
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported) |
|||
{ |
|||
Vector<float> vScale = new(scale); |
|||
int i = 0; |
|||
|
|||
// SIMD loop
|
|||
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
|
|||
for (; i <= source.Length - simdBatchSize; i += simdBatchSize) |
|||
{ |
|||
// Load the vector from source span
|
|||
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i])); |
|||
|
|||
// Scale the vector
|
|||
v *= vScale; |
|||
|
|||
// Write the scaled vector to the destination span
|
|||
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v); |
|||
} |
|||
|
|||
// Scalar fallback for remaining elements
|
|||
for (; i < source.Length; i++) |
|||
{ |
|||
destination[i] = source[i] * scale; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Scalar fallback if SIMD is not supported
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = source[i] * scale; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class ConversionParams |
|||
{ |
|||
private readonly IccProfile profile; |
|||
|
|||
internal ConversionParams(IccProfile profile, bool toPcs) |
|||
{ |
|||
this.profile = profile; |
|||
this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile); |
|||
} |
|||
|
|||
internal IccConverterBase Converter { get; } |
|||
|
|||
internal IccProfileHeader Header => this.profile.Header; |
|||
|
|||
internal IccRenderingIntent Intent => this.Header.RenderingIntent; |
|||
|
|||
internal IccColorSpaceType PcsType => this.Header.ProfileConnectionSpace; |
|||
|
|||
internal IccVersion Version => this.Header.Version; |
|||
|
|||
internal bool HasV2PerceptualHandling => this.Intent == IccRenderingIntent.Perceptual && this.Version.Major == 2; |
|||
|
|||
internal bool HasNoPerceptualHandling => this.Intent == IccRenderingIntent.Perceptual && this.Converter.IsTrc; |
|||
|
|||
internal bool Is16BitLutEntry => this.Converter.Is16BitLutEntry; |
|||
} |
|||
} |
|||
@ -0,0 +1,192 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
using System.Runtime.Intrinsics.X86; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
internal static class ColorProfileConverterExtensionsPixelCompatible |
|||
{ |
|||
/// <summary>
|
|||
/// Converts the pixel data of the specified image from the source color profile to the target color profile using
|
|||
/// the provided color profile converter.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method modifies the source image in place by converting its pixel data according to the
|
|||
/// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible;
|
|||
/// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC
|
|||
/// profiles are set on the converter before calling this method.
|
|||
/// </remarks>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
|
|||
/// <param name="source">
|
|||
/// The image whose pixel data will be converted. The conversion is performed in place, modifying the original
|
|||
/// image.
|
|||
/// </param>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// Thrown if the converter's source or target ICC profile is not specified.
|
|||
/// </exception>
|
|||
public static void Convert<TPixel>(this ColorProfileConverter converter, Image<TPixel> source) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
// These checks actually take place within the converter, but we want to fail fast here.
|
|||
// Note. we do not check to see whether the profiles themselves are RGB compatible,
|
|||
// if they are not, then the converter will simply produce incorrect results.
|
|||
if (converter.Options.SourceIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Source ICC profile is missing."); |
|||
} |
|||
|
|||
if (converter.Options.TargetIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Target ICC profile is missing."); |
|||
} |
|||
|
|||
// Process the rows in parallel chunks, the converter itself is thread safe.
|
|||
source.Mutate(o => o.ProcessPixelRowsAsVector4( |
|||
row => |
|||
{ |
|||
// Gather and convert the pixels in the row to Rgb.
|
|||
using IMemoryOwner<Rgb> rgbBuffer = converter.Options.MemoryAllocator.Allocate<Rgb>(row.Length); |
|||
Span<Rgb> rgbSpan = rgbBuffer.Memory.Span; |
|||
Rgb.FromScaledVector4(row, rgbSpan); |
|||
|
|||
// Perform the actual color conversion.
|
|||
converter.ConvertUsingIccProfile<Rgb, Rgb>(rgbSpan, rgbSpan); |
|||
|
|||
// Copy the converted Rgb pixels back to the row as TPixel.
|
|||
// Important: Preserve alpha from the existing row Vector4 values.
|
|||
// We merge RGB from rgbSpan into row, leaving W untouched.
|
|||
ref float srcRgb = ref Unsafe.As<Rgb, float>(ref MemoryMarshal.GetReference(rgbSpan)); |
|||
ref float dstRow = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(row)); |
|||
|
|||
int count = rgbSpan.Length; |
|||
int i = 0; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static Vector512<float> ReadVector512(ref float f) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
return Unsafe.ReadUnaligned<Vector512<float>>(ref b); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static void WriteVector512(ref float f, Vector512<float> v) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
Unsafe.WriteUnaligned(ref b, v); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static Vector256<float> ReadVector256(ref float f) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
return Unsafe.ReadUnaligned<Vector256<float>>(ref b); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static void WriteVector256(ref float f, Vector256<float> v) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
Unsafe.WriteUnaligned(ref b, v); |
|||
} |
|||
|
|||
if (Avx512F.IsSupported) |
|||
{ |
|||
// 4 pixels per iteration.
|
|||
//
|
|||
// Source layout (Rgb float stream, 12 floats):
|
|||
// [r0 g0 b0 r1 g1 b1 r2 g2 b2 r3 g3 b3]
|
|||
//
|
|||
// Destination layout (row Vector4 float stream, 16 floats):
|
|||
// [r0 g0 b0 a0 r1 g1 b1 a1 r2 g2 b2 a2 r3 g3 b3 a3]
|
|||
//
|
|||
// We use an overlapped load (16 floats) from the 3-float stride source.
|
|||
// The permute selects the RGB we need and inserts placeholders for alpha lanes.
|
|||
//
|
|||
// Then we blend RGB lanes into the existing destination, preserving alpha lanes.
|
|||
Vector512<int> rgbPerm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0); |
|||
|
|||
// BlendVariable selects from the second operand where the sign bit of the mask lane is set.
|
|||
// We want to overwrite lanes 0,1,2 then 4,5,6 then 8,9,10 then 12,13,14, and preserve lanes 3,7,11,15 (alpha).
|
|||
Vector512<float> rgbSelect = Vector512.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F); |
|||
|
|||
int quads = count >> 2; |
|||
int simdQuads = quads - 1; // Leave the last quad for the scalar tail to avoid the final overlapped load reading past the end.
|
|||
|
|||
for (int q = 0; q < simdQuads; q++) |
|||
{ |
|||
Vector512<float> dst = ReadVector512(ref dstRow); |
|||
Vector512<float> src = ReadVector512(ref srcRgb); |
|||
|
|||
Vector512<float> rgbx = Avx512F.PermuteVar16x32(src, rgbPerm); |
|||
Vector512<float> merged = Avx512F.BlendVariable(dst, rgbx, rgbSelect); |
|||
|
|||
WriteVector512(ref dstRow, merged); |
|||
|
|||
// Advance input by 4 pixels (4 * 3 = 12 floats)
|
|||
srcRgb = ref Unsafe.Add(ref srcRgb, 12); |
|||
|
|||
// Advance output by 4 pixels (4 * 4 = 16 floats)
|
|||
dstRow = ref Unsafe.Add(ref dstRow, 16); |
|||
|
|||
i += 4; |
|||
} |
|||
} |
|||
else if (Avx2.IsSupported) |
|||
{ |
|||
// 2 pixels per iteration.
|
|||
//
|
|||
// Same idea as AVX-512, but on 256-bit vectors.
|
|||
// We permute packed RGB into rgbx layout and blend into the existing destination,
|
|||
// preserving alpha lanes.
|
|||
Vector256<int> rgbPerm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0); |
|||
|
|||
Vector256<float> rgbSelect = Vector256.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F); |
|||
|
|||
int pairs = count >> 1; |
|||
int simdPairs = pairs - 1; // Leave the last pair for the scalar tail to avoid the final overlapped load reading past the end.
|
|||
|
|||
for (int p = 0; p < simdPairs; p++) |
|||
{ |
|||
Vector256<float> dst = ReadVector256(ref dstRow); |
|||
Vector256<float> src = ReadVector256(ref srcRgb); |
|||
|
|||
Vector256<float> rgbx = Avx2.PermuteVar8x32(src, rgbPerm); |
|||
Vector256<float> merged = Avx.BlendVariable(dst, rgbx, rgbSelect); |
|||
|
|||
WriteVector256(ref dstRow, merged); |
|||
|
|||
// Advance input by 2 pixels (2 * 3 = 6 floats)
|
|||
srcRgb = ref Unsafe.Add(ref srcRgb, 6); |
|||
|
|||
// Advance output by 2 pixels (2 * 4 = 8 floats)
|
|||
dstRow = ref Unsafe.Add(ref dstRow, 8); |
|||
|
|||
i += 2; |
|||
} |
|||
} |
|||
|
|||
// Scalar tail.
|
|||
// Handles:
|
|||
// - the last skipped SIMD block (quad or pair)
|
|||
// - any remainder
|
|||
//
|
|||
// Preserve alpha by writing Vector3 into the Vector4 storage.
|
|||
ref Vector4 rowRef = ref MemoryMarshal.GetReference(row); |
|||
for (; i < count; i++) |
|||
{ |
|||
Vector3 rgb = rgbSpan[i].AsVector3Unsafe(); |
|||
Unsafe.As<Vector4, Vector3>(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb; |
|||
} |
|||
}, |
|||
PixelConversionModifiers.Scale)); |
|||
} |
|||
} |
|||
@ -0,0 +1,506 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
/// <summary>
|
|||
/// Implements interpolation methods for color profile lookup tables.
|
|||
/// Adapted from ICC Reference implementation:
|
|||
/// https://github.com/InternationalColorConsortium/DemoIccMAX/blob/79ecb74135ad47bac7d42692905a079839b7e105/IccProfLib/IccTagLut.cpp
|
|||
/// </summary>
|
|||
internal class ClutCalculator : IVector4Calculator |
|||
{ |
|||
private readonly int inputCount; |
|||
private readonly int outputCount; |
|||
private readonly float[] lut; |
|||
private readonly byte[] gridPointCount; |
|||
private readonly byte[] maxGridPoint; |
|||
private readonly int[] indexFactor; |
|||
private readonly int[] dimSize; |
|||
private readonly int nodeCount; |
|||
private readonly float[][] nodes; |
|||
private readonly float[] g; |
|||
private readonly uint[] ig; |
|||
private readonly float[] s; |
|||
private readonly float[] df; |
|||
private readonly uint[] nPower; |
|||
private int n000; |
|||
private int n001; |
|||
private int n010; |
|||
private int n011; |
|||
private int n100; |
|||
private int n101; |
|||
private int n110; |
|||
private int n111; |
|||
private int n1000; |
|||
|
|||
public ClutCalculator(IccClut clut) |
|||
{ |
|||
Guard.NotNull(clut, nameof(clut)); |
|||
Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount)); |
|||
Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount)); |
|||
|
|||
this.inputCount = clut.InputChannelCount; |
|||
this.outputCount = clut.OutputChannelCount; |
|||
this.g = new float[this.inputCount]; |
|||
this.ig = new uint[this.inputCount]; |
|||
this.s = new float[this.inputCount]; |
|||
this.nPower = new uint[16]; |
|||
this.lut = clut.Values; |
|||
this.nodeCount = (int)Math.Pow(2, clut.InputChannelCount); |
|||
this.df = new float[this.nodeCount]; |
|||
this.nodes = new float[this.nodeCount][]; |
|||
this.dimSize = new int[this.inputCount]; |
|||
this.gridPointCount = clut.GridPointCount; |
|||
this.maxGridPoint = new byte[this.inputCount]; |
|||
for (int i = 0; i < this.inputCount; i++) |
|||
{ |
|||
this.maxGridPoint[i] = (byte)(this.gridPointCount[i] - 1); |
|||
} |
|||
|
|||
this.dimSize[this.inputCount - 1] = this.outputCount; |
|||
for (int i = this.inputCount - 2; i >= 0; i--) |
|||
{ |
|||
this.dimSize[i] = this.dimSize[i + 1] * this.gridPointCount[i + 1]; |
|||
} |
|||
|
|||
this.indexFactor = this.CalculateIndexFactor(); |
|||
} |
|||
|
|||
public unsafe Vector4 Calculate(Vector4 value) |
|||
{ |
|||
Vector4 result = default; |
|||
switch (this.inputCount) |
|||
{ |
|||
case 1: |
|||
this.Interpolate1d((float*)&value, (float*)&result); |
|||
break; |
|||
case 2: |
|||
this.Interpolate2d((float*)&value, (float*)&result); |
|||
break; |
|||
case 3: |
|||
this.Interpolate3d((float*)&value, (float*)&result); |
|||
break; |
|||
case 4: |
|||
this.Interpolate4d((float*)&value, (float*)&result); |
|||
break; |
|||
default: |
|||
this.InterpolateNd((float*)&value, (float*)&result); |
|||
break; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private int[] CalculateIndexFactor() |
|||
{ |
|||
int[] factors = new int[16]; |
|||
switch (this.inputCount) |
|||
{ |
|||
case 1: |
|||
factors[0] = this.n000 = 0; |
|||
factors[1] = this.n001 = this.dimSize[0]; |
|||
break; |
|||
case 2: |
|||
factors[0] = this.n000 = 0; |
|||
factors[1] = this.n001 = this.dimSize[0]; |
|||
factors[2] = this.n010 = this.dimSize[1]; |
|||
factors[3] = this.n011 = this.n001 + this.n010; |
|||
break; |
|||
case 3: |
|||
factors[0] = this.n000 = 0; |
|||
factors[1] = this.n001 = this.dimSize[0]; |
|||
factors[2] = this.n010 = this.dimSize[1]; |
|||
factors[3] = this.n011 = this.n001 + this.n010; |
|||
factors[4] = this.n100 = this.dimSize[2]; |
|||
factors[5] = this.n101 = this.n100 + this.n001; |
|||
factors[6] = this.n110 = this.n100 + this.n010; |
|||
factors[7] = this.n111 = this.n110 + this.n001; |
|||
break; |
|||
case 4: |
|||
factors[0] = 0; |
|||
factors[1] = this.n001 = this.dimSize[0]; |
|||
factors[2] = this.n010 = this.dimSize[1]; |
|||
factors[3] = factors[2] + factors[1]; |
|||
factors[4] = this.n100 = this.dimSize[2]; |
|||
factors[5] = factors[4] + factors[1]; |
|||
factors[6] = factors[4] + factors[2]; |
|||
factors[7] = factors[4] + factors[3]; |
|||
factors[8] = this.n1000 = this.dimSize[3]; |
|||
factors[9] = factors[8] + factors[1]; |
|||
factors[10] = factors[8] + factors[2]; |
|||
factors[11] = factors[8] + factors[3]; |
|||
factors[12] = factors[8] + factors[4]; |
|||
factors[13] = factors[8] + factors[5]; |
|||
factors[14] = factors[8] + factors[6]; |
|||
factors[15] = factors[8] + factors[7]; |
|||
break; |
|||
default: |
|||
// Initialize ND interpolation variables.
|
|||
factors[0] = 0; |
|||
int count; |
|||
for (count = 0; count < this.inputCount; count++) |
|||
{ |
|||
this.nPower[count] = (uint)(1 << (this.inputCount - 1 - count)); |
|||
} |
|||
|
|||
uint[] nPower = [0, 1]; |
|||
count = 0; |
|||
int nFlag = 1; |
|||
for (uint j = 1; j < this.nodeCount; j++) |
|||
{ |
|||
if (j == nPower[1]) |
|||
{ |
|||
factors[j] = this.dimSize[count]; |
|||
nPower[0] = (uint)(1 << count); |
|||
count++; |
|||
nPower[1] = (uint)(1 << count); |
|||
nFlag = 1; |
|||
} |
|||
else |
|||
{ |
|||
factors[j] = factors[nPower[0]] + factors[nFlag]; |
|||
nFlag++; |
|||
} |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
return factors; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// One dimensional interpolation function.
|
|||
/// </summary>
|
|||
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
|
|||
/// <param name="destPixel">The interpolated output pixels.</param>
|
|||
private unsafe void Interpolate1d(float* srcPixel, float* destPixel) |
|||
{ |
|||
byte mx = this.maxGridPoint[0]; |
|||
|
|||
float x = UnitClip(srcPixel[0]) * mx; |
|||
|
|||
uint ix = (uint)x; |
|||
|
|||
float u = x - ix; |
|||
|
|||
if (ix == mx) |
|||
{ |
|||
ix--; |
|||
u = 1.0f; |
|||
} |
|||
|
|||
float nu = (float)(1.0 - u); |
|||
|
|||
int i; |
|||
Span<float> p = this.lut.AsSpan((int)(ix * this.n001)); |
|||
|
|||
// Normalize grid units.
|
|||
float dF0 = nu; |
|||
float dF1 = u; |
|||
|
|||
int offset = 0; |
|||
for (i = 0; i < this.outputCount; i++) |
|||
{ |
|||
destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1)); |
|||
offset++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Two dimensional interpolation function.
|
|||
/// </summary>
|
|||
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
|
|||
/// <param name="destPixel">The interpolated output pixels.</param>
|
|||
private unsafe void Interpolate2d(float* srcPixel, float* destPixel) |
|||
{ |
|||
byte mx = this.maxGridPoint[0]; |
|||
byte my = this.maxGridPoint[1]; |
|||
|
|||
float x = UnitClip(srcPixel[0]) * mx; |
|||
float y = UnitClip(srcPixel[1]) * my; |
|||
|
|||
uint ix = (uint)x; |
|||
uint iy = (uint)y; |
|||
|
|||
float u = x - ix; |
|||
float t = y - iy; |
|||
|
|||
if (ix == mx) |
|||
{ |
|||
ix--; |
|||
u = 1.0f; |
|||
} |
|||
|
|||
if (iy == my) |
|||
{ |
|||
iy--; |
|||
t = 1.0f; |
|||
} |
|||
|
|||
float nt = (float)(1.0 - t); |
|||
float nu = (float)(1.0 - u); |
|||
|
|||
int i; |
|||
Span<float> p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010))); |
|||
|
|||
// Normalize grid units.
|
|||
float dF0 = nt * nu; |
|||
float dF1 = nt * u; |
|||
float dF2 = t * nu; |
|||
float dF3 = t * u; |
|||
|
|||
int offset = 0; |
|||
for (i = 0; i < this.outputCount; i++) |
|||
{ |
|||
destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1) + (p[offset + this.n010] * dF2) + (p[offset + this.n011] * dF3)); |
|||
offset++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Three dimensional interpolation function.
|
|||
/// </summary>
|
|||
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
|
|||
/// <param name="destPixel">The interpolated output pixels.</param>
|
|||
private unsafe void Interpolate3d(float* srcPixel, float* destPixel) |
|||
{ |
|||
byte mx = this.maxGridPoint[0]; |
|||
byte my = this.maxGridPoint[1]; |
|||
byte mz = this.maxGridPoint[2]; |
|||
|
|||
float x = UnitClip(srcPixel[0]) * mx; |
|||
float y = UnitClip(srcPixel[1]) * my; |
|||
float z = UnitClip(srcPixel[2]) * mz; |
|||
|
|||
uint ix = (uint)x; |
|||
uint iy = (uint)y; |
|||
uint iz = (uint)z; |
|||
|
|||
float u = x - ix; |
|||
float t = y - iy; |
|||
float s = z - iz; |
|||
|
|||
if (ix == mx) |
|||
{ |
|||
ix--; |
|||
u = 1.0f; |
|||
} |
|||
|
|||
if (iy == my) |
|||
{ |
|||
iy--; |
|||
t = 1.0f; |
|||
} |
|||
|
|||
if (iz == mz) |
|||
{ |
|||
iz--; |
|||
s = 1.0f; |
|||
} |
|||
|
|||
float ns = (float)(1.0 - s); |
|||
float nt = (float)(1.0 - t); |
|||
float nu = (float)(1.0 - u); |
|||
|
|||
Span<float> p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010) + (iz * this.n100))); |
|||
|
|||
// Normalize grid units
|
|||
float dF0 = ns * nt * nu; |
|||
float dF1 = ns * nt * u; |
|||
float dF2 = ns * t * nu; |
|||
float dF3 = ns * t * u; |
|||
float dF4 = s * nt * nu; |
|||
float dF5 = s * nt * u; |
|||
float dF6 = s * t * nu; |
|||
float dF7 = s * t * u; |
|||
|
|||
int offset = 0; |
|||
for (int i = 0; i < this.outputCount; i++) |
|||
{ |
|||
destPixel[i] = (float)((p[offset + this.n000] * dF0) + |
|||
(p[offset + this.n001] * dF1) + |
|||
(p[offset + this.n010] * dF2) + |
|||
(p[offset + this.n011] * dF3) + |
|||
(p[offset + this.n100] * dF4) + |
|||
(p[offset + this.n101] * dF5) + |
|||
(p[offset + this.n110] * dF6) + |
|||
(p[offset + this.n111] * dF7)); |
|||
offset++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Four dimensional interpolation function.
|
|||
/// </summary>
|
|||
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
|
|||
/// <param name="destPixel">The interpolated output pixels.</param>
|
|||
private unsafe void Interpolate4d(float* srcPixel, float* destPixel) |
|||
{ |
|||
byte mw = this.maxGridPoint[0]; |
|||
byte mx = this.maxGridPoint[1]; |
|||
byte my = this.maxGridPoint[2]; |
|||
byte mz = this.maxGridPoint[3]; |
|||
|
|||
float w = UnitClip(srcPixel[0]) * mw; |
|||
float x = UnitClip(srcPixel[1]) * mx; |
|||
float y = UnitClip(srcPixel[2]) * my; |
|||
float z = UnitClip(srcPixel[3]) * mz; |
|||
|
|||
uint iw = (uint)w; |
|||
uint ix = (uint)x; |
|||
uint iy = (uint)y; |
|||
uint iz = (uint)z; |
|||
|
|||
float v = w - iw; |
|||
float u = x - ix; |
|||
float t = y - iy; |
|||
float s = z - iz; |
|||
|
|||
if (iw == mw) |
|||
{ |
|||
iw--; |
|||
v = 1.0f; |
|||
} |
|||
|
|||
if (ix == mx) |
|||
{ |
|||
ix--; |
|||
u = 1.0f; |
|||
} |
|||
|
|||
if (iy == my) |
|||
{ |
|||
iy--; |
|||
t = 1.0f; |
|||
} |
|||
|
|||
if (iz == mz) |
|||
{ |
|||
iz--; |
|||
s = 1.0f; |
|||
} |
|||
|
|||
float ns = (float)(1.0 - s); |
|||
float nt = (float)(1.0 - t); |
|||
float nu = (float)(1.0 - u); |
|||
float nv = (float)(1.0 - v); |
|||
|
|||
Span<float> p = this.lut.AsSpan((int)((iw * this.n001) + (ix * this.n010) + (iy * this.n100) + (iz * this.n1000))); |
|||
|
|||
// Normalize grid units.
|
|||
float[] dF = |
|||
[ |
|||
ns * nt * nu * nv, |
|||
ns * nt * nu * v, |
|||
ns * nt * u * nv, |
|||
ns * nt * u * v, |
|||
ns * t * nu * nv, |
|||
ns * t * nu * v, |
|||
ns * t * u * nv, |
|||
ns * t * u * v, |
|||
s * nt * nu * nv, |
|||
s * nt * nu * v, |
|||
s * nt * u * nv, |
|||
s * nt * u * v, |
|||
s * t * nu * nv, |
|||
s * t * nu * v, |
|||
s * t * u * nv, |
|||
s * t * u * v, |
|||
]; |
|||
|
|||
int offset = 0; |
|||
for (int i = 0; i < this.outputCount; i++) |
|||
{ |
|||
float pv = 0.0f; |
|||
for (int j = 0; j < 16; j++) |
|||
{ |
|||
pv += p[offset + this.indexFactor[j]] * dF[j]; |
|||
} |
|||
|
|||
destPixel[i] = pv; |
|||
offset++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Generic N-dimensional interpolation function.
|
|||
/// </summary>
|
|||
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
|
|||
/// <param name="destPixel">The interpolated output pixels.</param>
|
|||
private unsafe void InterpolateNd(float* srcPixel, float* destPixel) |
|||
{ |
|||
int index = 0; |
|||
for (int i = 0; i < this.inputCount; i++) |
|||
{ |
|||
this.g[i] = UnitClip(srcPixel[i]) * this.maxGridPoint[i]; |
|||
this.ig[i] = (uint)this.g[i]; |
|||
this.s[this.inputCount - 1 - i] = this.g[i] - this.ig[i]; |
|||
if (this.ig[i] == this.maxGridPoint[i]) |
|||
{ |
|||
this.ig[i]--; |
|||
this.s[this.inputCount - 1 - i] = 1.0f; |
|||
} |
|||
|
|||
index += (int)this.ig[i] * this.dimSize[i]; |
|||
} |
|||
|
|||
Span<float> p = this.lut.AsSpan(index); |
|||
float[] temp = new float[2]; |
|||
bool nFlag = false; |
|||
|
|||
for (int i = 0; i < this.nodeCount; i++) |
|||
{ |
|||
this.df[i] = 1.0f; |
|||
} |
|||
|
|||
for (int i = 0; i < this.inputCount; i++) |
|||
{ |
|||
temp[0] = 1.0f - this.s[i]; |
|||
temp[1] = this.s[i]; |
|||
index = (int)this.nPower[i]; |
|||
for (int j = 0; j < this.nodeCount; j++) |
|||
{ |
|||
this.df[j] *= temp[nFlag ? 1 : 0]; |
|||
if ((j + 1) % index == 0) |
|||
{ |
|||
nFlag = !nFlag; |
|||
} |
|||
} |
|||
|
|||
nFlag = false; |
|||
} |
|||
|
|||
int offset = 0; |
|||
for (int i = 0; i < this.outputCount; i++) |
|||
{ |
|||
float pv = 0; |
|||
for (int j = 0; j < this.nodeCount; j++) |
|||
{ |
|||
pv += p[offset + this.indexFactor[j]] * this.df[j]; |
|||
} |
|||
|
|||
destPixel[i] = pv; |
|||
offset++; |
|||
} |
|||
} |
|||
|
|||
private static float UnitClip(float v) |
|||
{ |
|||
if (v < 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
if (v > 1.0) |
|||
{ |
|||
return 1.0f; |
|||
} |
|||
|
|||
return v; |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class ColorTrcCalculator : IVector4Calculator |
|||
{ |
|||
private readonly TrcCalculator curveCalculator; |
|||
private readonly Matrix4x4 matrix; |
|||
private readonly bool toPcs; |
|||
|
|||
public ColorTrcCalculator( |
|||
IccXyzTagDataEntry redMatrixColumn, |
|||
IccXyzTagDataEntry greenMatrixColumn, |
|||
IccXyzTagDataEntry blueMatrixColumn, |
|||
IccTagDataEntry redTrc, |
|||
IccTagDataEntry greenTrc, |
|||
IccTagDataEntry blueTrc, |
|||
bool toPcs) |
|||
{ |
|||
this.toPcs = toPcs; |
|||
this.curveCalculator = new TrcCalculator([redTrc, greenTrc, blueTrc], !toPcs); |
|||
|
|||
Vector3 mr = redMatrixColumn.Data[0]; |
|||
Vector3 mg = greenMatrixColumn.Data[0]; |
|||
Vector3 mb = blueMatrixColumn.Data[0]; |
|||
this.matrix = new Matrix4x4(mr.X, mr.Y, mr.Z, 0, mg.X, mg.Y, mg.Z, 0, mb.X, mb.Y, mb.Z, 0, 0, 0, 0, 1); |
|||
|
|||
if (!toPcs) |
|||
{ |
|||
Matrix4x4.Invert(this.matrix, out this.matrix); |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public Vector4 Calculate(Vector4 value) |
|||
{ |
|||
if (this.toPcs) |
|||
{ |
|||
// input is always linear RGB
|
|||
value = this.curveCalculator.Calculate(value); |
|||
CieXyz xyz = new(Vector4.Transform(value, this.matrix).AsVector3()); |
|||
|
|||
// when data to PCS, output from calculator is descaled XYZ
|
|||
// but downstream process requires scaled XYZ
|
|||
// (see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply)
|
|||
return xyz.ToScaledVector4(); |
|||
} |
|||
else |
|||
{ |
|||
// input is always XYZ
|
|||
Vector4 xyz = Vector4.Transform(value, this.matrix); |
|||
|
|||
// when data to PCS, upstream process provides scaled XYZ
|
|||
// but input to calculator is descaled XYZ
|
|||
// (see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply)
|
|||
xyz = new Vector4(CieXyz.FromScaledVector4(xyz).AsVector3Unsafe(), 1); |
|||
return this.curveCalculator.Calculate(xyz); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
internal partial class CurveCalculator |
|||
{ |
|||
private enum CalculationType |
|||
{ |
|||
Identity, |
|||
Gamma, |
|||
Lut, |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
#nullable disable |
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
internal partial class CurveCalculator : ISingleCalculator |
|||
{ |
|||
private readonly LutCalculator lutCalculator; |
|||
private readonly float gamma; |
|||
private readonly CalculationType type; |
|||
|
|||
public CurveCalculator(IccCurveTagDataEntry entry, bool inverted) |
|||
{ |
|||
if (entry.IsIdentityResponse) |
|||
{ |
|||
this.type = CalculationType.Identity; |
|||
} |
|||
else if (entry.IsGamma) |
|||
{ |
|||
this.gamma = entry.Gamma; |
|||
if (inverted) |
|||
{ |
|||
this.gamma = 1f / this.gamma; |
|||
} |
|||
|
|||
this.type = CalculationType.Gamma; |
|||
} |
|||
else |
|||
{ |
|||
this.lutCalculator = new LutCalculator(entry.CurveData, inverted); |
|||
this.type = CalculationType.Lut; |
|||
} |
|||
} |
|||
|
|||
public float Calculate(float value) |
|||
=> this.type switch |
|||
{ |
|||
CalculationType.Identity => value, |
|||
CalculationType.Gamma => MathF.Pow(value, this.gamma), // TODO: This could be optimized using a LUT. See SrgbCompanding
|
|||
CalculationType.Lut => this.lutCalculator.Calculate(value), |
|||
_ => throw new InvalidOperationException("Invalid calculation type"), |
|||
}; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class GrayTrcCalculator : IVector4Calculator |
|||
{ |
|||
private readonly TrcCalculator calculator; |
|||
|
|||
public GrayTrcCalculator(IccTagDataEntry grayTrc, bool toPcs) |
|||
=> this.calculator = new TrcCalculator(new IccTagDataEntry[] { grayTrc }, !toPcs); |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value); |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
/// <summary>
|
|||
/// Represents an ICC calculator with a single floating point value and result
|
|||
/// </summary>
|
|||
internal interface ISingleCalculator |
|||
{ |
|||
/// <summary>
|
|||
/// Calculates a result from the given value
|
|||
/// </summary>
|
|||
/// <param name="value">The input value</param>
|
|||
/// <returns>The calculated result</returns>
|
|||
float Calculate(float value); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
/// <summary>
|
|||
/// Represents an ICC calculator with <see cref="Vector4"/> values and results
|
|||
/// </summary>
|
|||
internal interface IVector4Calculator |
|||
{ |
|||
/// <summary>
|
|||
/// Calculates a result from the given values
|
|||
/// </summary>
|
|||
/// <param name="value">The input values</param>
|
|||
/// <returns>The calculated result</returns>
|
|||
Vector4 Calculate(Vector4 value); |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
internal partial class LutABCalculator |
|||
{ |
|||
private enum CalculationType |
|||
{ |
|||
AtoB = 1 << 3, |
|||
BtoA = 1 << 4, |
|||
|
|||
SingleCurve = 1, |
|||
CurveMatrix = 2, |
|||
CurveClut = 3, |
|||
Full = 4, |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
#nullable disable |
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
internal partial class LutABCalculator : IVector4Calculator |
|||
{ |
|||
private CalculationType type; |
|||
private TrcCalculator curveACalculator; |
|||
private TrcCalculator curveBCalculator; |
|||
private TrcCalculator curveMCalculator; |
|||
private MatrixCalculator matrixCalculator; |
|||
private ClutCalculator clutCalculator; |
|||
|
|||
public LutABCalculator(IccLutAToBTagDataEntry entry) |
|||
{ |
|||
Guard.NotNull(entry, nameof(entry)); |
|||
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); |
|||
this.type |= CalculationType.AtoB; |
|||
} |
|||
|
|||
public LutABCalculator(IccLutBToATagDataEntry entry) |
|||
{ |
|||
Guard.NotNull(entry, nameof(entry)); |
|||
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); |
|||
this.type |= CalculationType.BtoA; |
|||
} |
|||
|
|||
public Vector4 Calculate(Vector4 value) |
|||
{ |
|||
switch (this.type) |
|||
{ |
|||
case CalculationType.Full | CalculationType.AtoB: |
|||
value = this.curveACalculator.Calculate(value); |
|||
value = this.clutCalculator.Calculate(value); |
|||
value = this.curveMCalculator.Calculate(value); |
|||
value = this.matrixCalculator.Calculate(value); |
|||
return this.curveBCalculator.Calculate(value); |
|||
|
|||
case CalculationType.Full | CalculationType.BtoA: |
|||
value = this.curveBCalculator.Calculate(value); |
|||
value = this.matrixCalculator.Calculate(value); |
|||
value = this.curveMCalculator.Calculate(value); |
|||
value = this.clutCalculator.Calculate(value); |
|||
return this.curveACalculator.Calculate(value); |
|||
|
|||
case CalculationType.CurveClut | CalculationType.AtoB: |
|||
value = this.curveACalculator.Calculate(value); |
|||
value = this.clutCalculator.Calculate(value); |
|||
return this.curveBCalculator.Calculate(value); |
|||
|
|||
case CalculationType.CurveClut | CalculationType.BtoA: |
|||
value = this.curveBCalculator.Calculate(value); |
|||
value = this.clutCalculator.Calculate(value); |
|||
return this.curveACalculator.Calculate(value); |
|||
|
|||
case CalculationType.CurveMatrix | CalculationType.AtoB: |
|||
value = this.curveMCalculator.Calculate(value); |
|||
value = this.matrixCalculator.Calculate(value); |
|||
return this.curveBCalculator.Calculate(value); |
|||
|
|||
case CalculationType.CurveMatrix | CalculationType.BtoA: |
|||
value = this.curveBCalculator.Calculate(value); |
|||
value = this.matrixCalculator.Calculate(value); |
|||
return this.curveMCalculator.Calculate(value); |
|||
|
|||
case CalculationType.SingleCurve | CalculationType.AtoB: |
|||
case CalculationType.SingleCurve | CalculationType.BtoA: |
|||
return this.curveBCalculator.Calculate(value); |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid calculation type"); |
|||
} |
|||
} |
|||
|
|||
private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut) |
|||
{ |
|||
bool hasACurve = curveA != null; |
|||
bool hasBCurve = curveB != null; |
|||
bool hasMCurve = curveM != null; |
|||
bool hasMatrix = matrix3x1 != null && matrix3x3 != null; |
|||
bool hasClut = clut != null; |
|||
|
|||
if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve) |
|||
{ |
|||
this.type = CalculationType.Full; |
|||
} |
|||
else if (hasBCurve && hasClut && hasACurve) |
|||
{ |
|||
this.type = CalculationType.CurveClut; |
|||
} |
|||
else if (hasBCurve && hasMatrix && hasMCurve) |
|||
{ |
|||
this.type = CalculationType.CurveMatrix; |
|||
} |
|||
else if (hasBCurve) |
|||
{ |
|||
this.type = CalculationType.SingleCurve; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidIccProfileException("AToB or BToA tag has an invalid configuration"); |
|||
} |
|||
|
|||
if (hasACurve) |
|||
{ |
|||
this.curveACalculator = new TrcCalculator(curveA, false); |
|||
} |
|||
|
|||
if (hasBCurve) |
|||
{ |
|||
this.curveBCalculator = new TrcCalculator(curveB, false); |
|||
} |
|||
|
|||
if (hasMCurve) |
|||
{ |
|||
this.curveMCalculator = new TrcCalculator(curveM, false); |
|||
} |
|||
|
|||
if (hasMatrix) |
|||
{ |
|||
this.matrixCalculator = new MatrixCalculator(matrix3x3.Value, matrix3x1.Value); |
|||
} |
|||
|
|||
if (hasClut) |
|||
{ |
|||
this.clutCalculator = new ClutCalculator(clut); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class LutCalculator : ISingleCalculator |
|||
{ |
|||
private readonly float[] lut; |
|||
private readonly bool inverse; |
|||
|
|||
public LutCalculator(float[] lut, bool inverse) |
|||
{ |
|||
Guard.NotNull(lut, nameof(lut)); |
|||
|
|||
this.lut = lut; |
|||
this.inverse = inverse; |
|||
} |
|||
|
|||
public float Calculate(float value) |
|||
{ |
|||
if (this.inverse) |
|||
{ |
|||
return this.LookupInverse(value); |
|||
} |
|||
|
|||
return this.Lookup(value); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float Lookup(float value) |
|||
{ |
|||
value = Math.Max(value, 0); |
|||
|
|||
float factor = value * (this.lut.Length - 1); |
|||
int index = (int)factor; |
|||
float low = this.lut[index]; |
|||
|
|||
float high = 1F; |
|||
if (index < this.lut.Length - 1) |
|||
{ |
|||
high = this.lut[index + 1]; |
|||
} |
|||
|
|||
return low + ((high - low) * (factor - index)); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float LookupInverse(float value) |
|||
{ |
|||
int index = Array.BinarySearch(this.lut, value); |
|||
if (index >= 0) |
|||
{ |
|||
return index / (float)(this.lut.Length - 1); |
|||
} |
|||
|
|||
index = ~index; |
|||
if (index == 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
else if (index == this.lut.Length) |
|||
{ |
|||
return 1; |
|||
} |
|||
|
|||
float high = this.lut[index]; |
|||
float low = this.lut[index - 1]; |
|||
|
|||
float valuePercent = (value - low) / (high - low); |
|||
float lutRange = 1 / (float)(this.lut.Length - 1); |
|||
float lutLow = (index - 1) / (float)(this.lut.Length - 1); |
|||
|
|||
return lutLow + (valuePercent * lutRange); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
#nullable disable |
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class LutEntryCalculator : IVector4Calculator |
|||
{ |
|||
private LutCalculator[] inputCurve; |
|||
private LutCalculator[] outputCurve; |
|||
private ClutCalculator clutCalculator; |
|||
private Matrix4x4 matrix; |
|||
private bool doTransform; |
|||
|
|||
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) |
|||
{ |
|||
if (this.doTransform) |
|||
{ |
|||
value = Vector4.Transform(value, this.matrix); |
|||
} |
|||
|
|||
value = CalculateLut(this.inputCurve, value); |
|||
value = this.clutCalculator.Calculate(value); |
|||
return CalculateLut(this.outputCurve, value); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static Vector4 CalculateLut(LutCalculator[] lut, Vector4 value) |
|||
{ |
|||
ref float f = ref Unsafe.As<Vector4, float>(ref value); |
|||
for (int i = 0; i < lut.Length; i++) |
|||
{ |
|||
Unsafe.Add(ref f, i) = lut[i].Calculate(Unsafe.Add(ref f, i)); |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix) |
|||
{ |
|||
this.inputCurve = InitLut(inputCurve); |
|||
this.outputCurve = InitLut(outputCurve); |
|||
this.clutCalculator = new ClutCalculator(clut); |
|||
this.matrix = matrix; |
|||
|
|||
this.doTransform = !matrix.IsIdentity && inputCurve.Length == 3; |
|||
} |
|||
|
|||
private static LutCalculator[] InitLut(IccLut[] curves) |
|||
{ |
|||
LutCalculator[] calculators = new LutCalculator[curves.Length]; |
|||
for (int i = 0; i < curves.Length; i++) |
|||
{ |
|||
calculators[i] = new LutCalculator(curves[i].Values, false); |
|||
} |
|||
|
|||
return calculators; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class MatrixCalculator : IVector4Calculator |
|||
{ |
|||
private Matrix4x4 matrix2D; |
|||
private Vector4 matrix1D; |
|||
|
|||
public MatrixCalculator(Matrix4x4 matrix3x3, Vector3 matrix3x1) |
|||
{ |
|||
this.matrix2D = matrix3x3; |
|||
this.matrix1D = new Vector4(matrix3x1, 0); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public Vector4 Calculate(Vector4 value) |
|||
{ |
|||
Vector4 transformed = Vector4.Transform(value, this.matrix2D); |
|||
return Vector4.Add(this.matrix1D, transformed); |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class ParametricCurveCalculator : ISingleCalculator |
|||
{ |
|||
private readonly IccParametricCurve curve; |
|||
private readonly IccParametricCurveType type; |
|||
private const IccParametricCurveType InvertedFlag = (IccParametricCurveType)(1 << 3); |
|||
|
|||
public ParametricCurveCalculator(IccParametricCurveTagDataEntry entry, bool inverted) |
|||
{ |
|||
Guard.NotNull(entry, nameof(entry)); |
|||
this.curve = entry.Curve; |
|||
this.type = entry.Curve.Type; |
|||
|
|||
if (inverted) |
|||
{ |
|||
this.type |= InvertedFlag; |
|||
} |
|||
} |
|||
|
|||
public float Calculate(float value) |
|||
=> this.type switch |
|||
{ |
|||
IccParametricCurveType.Type1 => this.CalculateGamma(value), |
|||
IccParametricCurveType.Cie122_1996 => this.CalculateCie122(value), |
|||
IccParametricCurveType.Iec61966_3 => this.CalculateIec61966(value), |
|||
IccParametricCurveType.SRgb => this.CalculateSRgb(value), |
|||
IccParametricCurveType.Type5 => this.CalculateType5(value), |
|||
IccParametricCurveType.Type1 | InvertedFlag => this.CalculateInvertedGamma(value), |
|||
IccParametricCurveType.Cie122_1996 | InvertedFlag => this.CalculateInvertedCie122(value), |
|||
IccParametricCurveType.Iec61966_3 | InvertedFlag => this.CalculateInvertedIec61966(value), |
|||
IccParametricCurveType.SRgb | InvertedFlag => this.CalculateInvertedSRgb(value), |
|||
IccParametricCurveType.Type5 | InvertedFlag => this.CalculateInvertedType5(value), |
|||
_ => throw new InvalidIccProfileException("ParametricCurve"), |
|||
}; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateGamma(float value) => MathF.Pow(value, this.curve.G); |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateCie122(float value) |
|||
{ |
|||
if (value >= -this.curve.B / this.curve.A) |
|||
{ |
|||
return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateIec61966(float value) |
|||
{ |
|||
if (value >= -this.curve.B / this.curve.A) |
|||
{ |
|||
return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G) + this.curve.C; |
|||
} |
|||
|
|||
return this.curve.C; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateSRgb(float value) |
|||
{ |
|||
if (value >= this.curve.D) |
|||
{ |
|||
return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G); |
|||
} |
|||
|
|||
return this.curve.C * value; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateType5(float value) |
|||
{ |
|||
if (value >= this.curve.D) |
|||
{ |
|||
return MathF.Pow((this.curve.A * value) + this.curve.B, this.curve.G) + this.curve.E; |
|||
} |
|||
|
|||
return (this.curve.C * value) + this.curve.F; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateInvertedGamma(float value) |
|||
=> MathF.Pow(value, 1 / this.curve.G); |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateInvertedCie122(float value) |
|||
=> (MathF.Pow(value, 1 / this.curve.G) - this.curve.B) / this.curve.A; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateInvertedIec61966(float value) |
|||
{ |
|||
if (value >= this.curve.C) |
|||
{ |
|||
return (MathF.Pow(value - this.curve.C, 1 / this.curve.G) - this.curve.B) / this.curve.A; |
|||
} |
|||
|
|||
return -this.curve.B / this.curve.A; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateInvertedSRgb(float value) |
|||
{ |
|||
if (value >= MathF.Pow((this.curve.A * this.curve.D) + this.curve.B, this.curve.G)) |
|||
{ |
|||
return (MathF.Pow(value, 1 / this.curve.G) - this.curve.B) / this.curve.A; |
|||
} |
|||
|
|||
return value / this.curve.C; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private float CalculateInvertedType5(float value) |
|||
{ |
|||
if (value >= (this.curve.C * this.curve.D) + this.curve.F) |
|||
{ |
|||
return (MathF.Pow(value - this.curve.E, 1 / this.curve.G) - this.curve.B) / this.curve.A; |
|||
} |
|||
|
|||
return (value - this.curve.F) / this.curve.C; |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
|
|||
internal class TrcCalculator : IVector4Calculator |
|||
{ |
|||
private readonly ISingleCalculator[] calculators; |
|||
|
|||
public TrcCalculator(IccTagDataEntry[] entries, bool inverted) |
|||
{ |
|||
Guard.NotNull(entries, nameof(entries)); |
|||
|
|||
this.calculators = new ISingleCalculator[entries.Length]; |
|||
for (int i = 0; i < entries.Length; i++) |
|||
{ |
|||
this.calculators[i] = entries[i] switch |
|||
{ |
|||
IccCurveTagDataEntry curve => new CurveCalculator(curve, inverted), |
|||
IccParametricCurveTagDataEntry parametricCurve => new ParametricCurveCalculator(parametricCurve, inverted), |
|||
_ => throw new InvalidIccProfileException("Invalid Entry."), |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public unsafe Vector4 Calculate(Vector4 value) |
|||
{ |
|||
ref float f = ref Unsafe.As<Vector4, float>(ref value); |
|||
for (int i = 0; i < this.calculators.Length; i++) |
|||
{ |
|||
Unsafe.Add(ref f, i) = this.calculators[i].Calculate(Unsafe.Add(ref f, i)); |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc; |
|||
|
|||
internal static class CompactSrgbV4Profile |
|||
{ |
|||
private static readonly Lazy<IccProfile> LazyIccProfile = new(GetIccProfile); |
|||
|
|||
// Generated using the sRGB-v4.icc profile found at https://github.com/saucecontrol/Compact-ICC-Profiles
|
|||
private static ReadOnlySpan<byte> Data => |
|||
[ |
|||
0, 0, 1, 224, 108, 99, 109, 115, 4, 32, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, 226, 0, 3, 0, |
|||
20, 0, 9, 0, 14, 0, 29, 97, 99, 115, 112, 77, 83, 70, 84, 0, 0, 0, 0, 115, 97, 119, 115, 99, 116, 114, 108, 0, 0, |
|||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 104, 97, 110, 100, 163, 178, 171, |
|||
223, 92, 167, 3, 18, 168, 85, 164, 236, 53, 122, 209, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 100, 101, 115, 99, 0, 0, 0, 252, 0, 0, 0, 36, 99, |
|||
112, 114, 116, 0, 0, 1, 32, 0, 0, 0, 34, 119, 116, 112, 116, 0, 0, 1, 68, 0, 0, 0, 20, 99, 104, 97, 100, 0, 0, |
|||
1, 88, 0, 0, 0, 44, 114, 88, 89, 90, 0, 0, 1, 132, 0, 0, 0, 20, 103, 88, 89, 90, 0, 0, 1, 152, 0, 0, 0, |
|||
20, 98, 88, 89, 90, 0, 0, 1, 172, 0, 0, 0, 20, 114, 84, 82, 67, 0, 0, 1, 192, 0, 0, 0, 32, 103, 84, 82, 67, |
|||
0, 0, 1, 192, 0, 0, 0, 32, 98, 84, 82, 67, 0, 0, 1, 192, 0, 0, 0, 32, 109, 108, 117, 99, 0, 0, 0, 0, 0, |
|||
0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 8, 0, 0, 0, 28, 0, 115, 0, 82, 0, 71, 0, 66, 109, 108, |
|||
117, 99, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 6, 0, 0, 0, 28, 0, 67, 0, |
|||
67, 0, 48, 0, 33, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 115, 102, 51, 50, |
|||
0, 0, 0, 0, 0, 1, 12, 63, 0, 0, 5, 221, 255, 255, 243, 38, 0, 0, 7, 144, 0, 0, 253, 146, 255, 255, 251, 161, 255, |
|||
255, 253, 162, 0, 0, 3, 220, 0, 0, 192, 113, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 111, 160, 0, 0, 56, 242, 0, 0, |
|||
3, 143, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, 150, 0, 0, 183, 137, 0, 0, 24, 218, 88, 89, 90, 32, 0, 0, 0, |
|||
0, 0, 0, 36, 160, 0, 0, 15, 133, 0, 0, 182, 196, 112, 97, 114, 97, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 102, 105, |
|||
0, 0, 242, 167, 0, 0, 13, 89, 0, 0, 19, 208, 0, 0, 10, 91, |
|||
]; |
|||
|
|||
public static IccProfile Profile => LazyIccProfile.Value; |
|||
|
|||
private static IccProfile GetIccProfile() |
|||
{ |
|||
byte[] buffer = new byte[Data.Length]; |
|||
Data.CopyTo(buffer); |
|||
return new IccProfile(buffer); |
|||
} |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
#nullable disable |
|||
|
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal abstract partial class IccConverterBase |
|||
{ |
|||
private static ConversionMethod GetConversionMethod(IccProfile profile, IccRenderingIntent renderingIntent) => profile.Header.Class switch |
|||
{ |
|||
IccProfileClass.InputDevice or |
|||
IccProfileClass.DisplayDevice or |
|||
IccProfileClass.OutputDevice or |
|||
IccProfileClass.ColorSpace => CheckMethod1(profile, renderingIntent), |
|||
IccProfileClass.DeviceLink or IccProfileClass.Abstract => CheckMethod2(profile), |
|||
_ => ConversionMethod.Invalid, |
|||
}; |
|||
|
|||
private static ConversionMethod CheckMethod1(IccProfile profile, IccRenderingIntent renderingIntent) |
|||
{ |
|||
ConversionMethod method = CheckMethodD(profile, renderingIntent); |
|||
if (method != ConversionMethod.Invalid) |
|||
{ |
|||
return method; |
|||
} |
|||
|
|||
method = CheckMethodA(profile, renderingIntent); |
|||
if (method != ConversionMethod.Invalid) |
|||
{ |
|||
return method; |
|||
} |
|||
|
|||
method = CheckMethodA0(profile); |
|||
if (method != ConversionMethod.Invalid) |
|||
{ |
|||
return method; |
|||
} |
|||
|
|||
method = CheckMethodTrc(profile); |
|||
if (method != ConversionMethod.Invalid) |
|||
{ |
|||
return method; |
|||
} |
|||
|
|||
return ConversionMethod.Invalid; |
|||
} |
|||
|
|||
private static ConversionMethod CheckMethodD(IccProfile profile, IccRenderingIntent renderingIntent) |
|||
{ |
|||
if ((HasTag(profile, IccProfileTag.DToB0) || HasTag(profile, IccProfileTag.BToD0)) |
|||
&& renderingIntent == IccRenderingIntent.Perceptual) |
|||
{ |
|||
return ConversionMethod.D0; |
|||
} |
|||
|
|||
if ((HasTag(profile, IccProfileTag.DToB1) || HasTag(profile, IccProfileTag.BToD1)) |
|||
&& renderingIntent == IccRenderingIntent.MediaRelativeColorimetric) |
|||
{ |
|||
return ConversionMethod.D1; |
|||
} |
|||
|
|||
if ((HasTag(profile, IccProfileTag.DToB2) || HasTag(profile, IccProfileTag.BToD2)) |
|||
&& renderingIntent == IccRenderingIntent.Saturation) |
|||
{ |
|||
return ConversionMethod.D2; |
|||
} |
|||
|
|||
if ((HasTag(profile, IccProfileTag.DToB3) || HasTag(profile, IccProfileTag.BToD3)) |
|||
&& renderingIntent == IccRenderingIntent.AbsoluteColorimetric) |
|||
{ |
|||
return ConversionMethod.D3; |
|||
} |
|||
|
|||
return ConversionMethod.Invalid; |
|||
} |
|||
|
|||
private static ConversionMethod CheckMethodA(IccProfile profile, IccRenderingIntent renderingIntent) |
|||
{ |
|||
if ((HasTag(profile, IccProfileTag.AToB0) || HasTag(profile, IccProfileTag.BToA0)) |
|||
&& renderingIntent == IccRenderingIntent.Perceptual) |
|||
{ |
|||
return ConversionMethod.A0; |
|||
} |
|||
|
|||
if ((HasTag(profile, IccProfileTag.AToB1) || HasTag(profile, IccProfileTag.BToA1)) |
|||
&& renderingIntent == IccRenderingIntent.MediaRelativeColorimetric) |
|||
{ |
|||
return ConversionMethod.A1; |
|||
} |
|||
|
|||
if ((HasTag(profile, IccProfileTag.AToB2) || HasTag(profile, IccProfileTag.BToA2)) |
|||
&& renderingIntent == IccRenderingIntent.Saturation) |
|||
{ |
|||
return ConversionMethod.A2; |
|||
} |
|||
|
|||
return ConversionMethod.Invalid; |
|||
} |
|||
|
|||
private static ConversionMethod CheckMethodA0(IccProfile profile) |
|||
{ |
|||
bool valid = HasTag(profile, IccProfileTag.AToB0) || HasTag(profile, IccProfileTag.BToA0); |
|||
return valid ? ConversionMethod.A0 : ConversionMethod.Invalid; |
|||
} |
|||
|
|||
private static ConversionMethod CheckMethodTrc(IccProfile profile) |
|||
{ |
|||
if (HasTag(profile, IccProfileTag.RedMatrixColumn) |
|||
&& HasTag(profile, IccProfileTag.GreenMatrixColumn) |
|||
&& HasTag(profile, IccProfileTag.BlueMatrixColumn) |
|||
&& HasTag(profile, IccProfileTag.RedTrc) |
|||
&& HasTag(profile, IccProfileTag.GreenTrc) |
|||
&& HasTag(profile, IccProfileTag.BlueTrc)) |
|||
{ |
|||
return ConversionMethod.ColorTrc; |
|||
} |
|||
|
|||
if (HasTag(profile, IccProfileTag.GrayTrc)) |
|||
{ |
|||
return ConversionMethod.GrayTrc; |
|||
} |
|||
|
|||
return ConversionMethod.Invalid; |
|||
} |
|||
|
|||
private static ConversionMethod CheckMethod2(IccProfile profile) |
|||
{ |
|||
if (HasTag(profile, IccProfileTag.DToB0) || HasTag(profile, IccProfileTag.BToD0)) |
|||
{ |
|||
return ConversionMethod.D0; |
|||
} |
|||
|
|||
if (HasTag(profile, IccProfileTag.AToB0) || HasTag(profile, IccProfileTag.AToB0)) |
|||
{ |
|||
return ConversionMethod.A0; |
|||
} |
|||
|
|||
return ConversionMethod.Invalid; |
|||
} |
|||
|
|||
private static bool HasTag(IccProfile profile, IccProfileTag tag) |
|||
=> profile.Entries.Any(t => t.TagSignature == tag); |
|||
|
|||
private static IccTagDataEntry GetTag(IccProfile profile, IccProfileTag tag) |
|||
=> Array.Find(profile.Entries, t => t.TagSignature == tag); |
|||
|
|||
private static T GetTag<T>(IccProfile profile, IccProfileTag tag) |
|||
where T : IccTagDataEntry |
|||
=> profile.Entries.OfType<T>().FirstOrDefault(t => t.TagSignature == tag); |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal abstract partial class IccConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Conversion methods with ICC profiles
|
|||
/// </summary>
|
|||
private enum ConversionMethod |
|||
{ |
|||
/// <summary>
|
|||
/// Conversion using anything but Multi Process Elements with perceptual rendering intent
|
|||
/// </summary>
|
|||
A0, |
|||
|
|||
/// <summary>
|
|||
/// Conversion using anything but Multi Process Elements with relative colorimetric rendering intent
|
|||
/// </summary>
|
|||
A1, |
|||
|
|||
/// <summary>
|
|||
/// Conversion using anything but Multi Process Elements with saturation rendering intent
|
|||
/// </summary>
|
|||
A2, |
|||
|
|||
/// <summary>
|
|||
/// Conversion using Multi Process Elements with perceptual rendering intent
|
|||
/// </summary>
|
|||
D0, |
|||
|
|||
/// <summary>
|
|||
/// Conversion using Multi Process Elements with relative colorimetric rendering intent
|
|||
/// </summary>
|
|||
D1, |
|||
|
|||
/// <summary>
|
|||
/// Conversion using Multi Process Elements with saturation rendering intent
|
|||
/// </summary>
|
|||
D2, |
|||
|
|||
/// <summary>
|
|||
/// Conversion using Multi Process Elements with absolute colorimetric rendering intent
|
|||
/// </summary>
|
|||
D3, |
|||
|
|||
/// <summary>
|
|||
/// Conversion of more than one channel using tone reproduction curves
|
|||
/// </summary>
|
|||
ColorTrc, |
|||
|
|||
/// <summary>
|
|||
/// Conversion of exactly one channel using a tone reproduction curve
|
|||
/// </summary>
|
|||
GrayTrc, |
|||
|
|||
/// <summary>
|
|||
/// No valid conversion method available or found
|
|||
/// </summary>
|
|||
Invalid, |
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal abstract partial class IccConverterBase |
|||
{ |
|||
private IVector4Calculator calculator; |
|||
|
|||
internal bool Is16BitLutEntry => this.calculator is LutEntryCalculator { Is16Bit: true }; |
|||
|
|||
internal bool IsTrc => this.calculator is ColorTrcCalculator or GrayTrcCalculator; |
|||
|
|||
/// <summary>
|
|||
/// Checks the profile for available conversion methods and gathers all the information's necessary for it.
|
|||
/// </summary>
|
|||
/// <param name="profile">The profile to use for the conversion.</param>
|
|||
/// <param name="toPcs">True if the conversion is to the Profile Connection Space.</param>
|
|||
/// <param name="renderingIntent">The wanted rendering intent. Can be ignored if not available.</param>
|
|||
/// <exception cref="InvalidIccProfileException">Invalid conversion method.</exception>
|
|||
protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent) |
|||
=> this.calculator = GetConversionMethod(profile, renderingIntent) switch |
|||
{ |
|||
ConversionMethod.D0 => toPcs ? |
|||
InitD(profile, IccProfileTag.DToB0) : |
|||
InitD(profile, IccProfileTag.BToD0), |
|||
ConversionMethod.D1 => toPcs ? |
|||
InitD(profile, IccProfileTag.DToB1) : |
|||
InitD(profile, IccProfileTag.BToD1), |
|||
ConversionMethod.D2 => toPcs ? |
|||
InitD(profile, IccProfileTag.DToB2) : |
|||
InitD(profile, IccProfileTag.BToD2), |
|||
ConversionMethod.D3 => toPcs ? |
|||
InitD(profile, IccProfileTag.DToB3) : |
|||
InitD(profile, IccProfileTag.BToD3), |
|||
ConversionMethod.A0 => toPcs ? |
|||
InitA(profile, IccProfileTag.AToB0) : |
|||
InitA(profile, IccProfileTag.BToA0), |
|||
ConversionMethod.A1 => toPcs ? |
|||
InitA(profile, IccProfileTag.AToB1) : |
|||
InitA(profile, IccProfileTag.BToA1), |
|||
ConversionMethod.A2 => toPcs ? |
|||
InitA(profile, IccProfileTag.AToB2) : |
|||
InitA(profile, IccProfileTag.BToA2), |
|||
ConversionMethod.ColorTrc => InitColorTrc(profile, toPcs), |
|||
ConversionMethod.GrayTrc => InitGrayTrc(profile, toPcs), |
|||
_ => throw new InvalidIccProfileException("Invalid conversion method."), |
|||
}; |
|||
|
|||
private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag) |
|||
=> GetTag(profile, tag) switch |
|||
{ |
|||
IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8), |
|||
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16), |
|||
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB), |
|||
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA), |
|||
_ => throw new InvalidIccProfileException("Invalid entry."), |
|||
}; |
|||
|
|||
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag) |
|||
{ |
|||
IccMultiProcessElementsTagDataEntry entry = GetTag<IccMultiProcessElementsTagDataEntry>(profile, tag) |
|||
?? throw new InvalidIccProfileException("Entry is null."); |
|||
|
|||
throw new NotImplementedException("Multi process elements are not supported"); |
|||
} |
|||
|
|||
private static ColorTrcCalculator InitColorTrc(IccProfile profile, bool toPcs) |
|||
{ |
|||
IccXyzTagDataEntry redMatrixColumn = GetTag<IccXyzTagDataEntry>(profile, IccProfileTag.RedMatrixColumn); |
|||
IccXyzTagDataEntry greenMatrixColumn = GetTag<IccXyzTagDataEntry>(profile, IccProfileTag.GreenMatrixColumn); |
|||
IccXyzTagDataEntry blueMatrixColumn = GetTag<IccXyzTagDataEntry>(profile, IccProfileTag.BlueMatrixColumn); |
|||
|
|||
IccTagDataEntry redTrc = GetTag(profile, IccProfileTag.RedTrc); |
|||
IccTagDataEntry greenTrc = GetTag(profile, IccProfileTag.GreenTrc); |
|||
IccTagDataEntry blueTrc = GetTag(profile, IccProfileTag.BlueTrc); |
|||
|
|||
if (redMatrixColumn == null || |
|||
greenMatrixColumn == null || |
|||
blueMatrixColumn == null || |
|||
redTrc == null || |
|||
greenTrc == null || |
|||
blueTrc == null) |
|||
{ |
|||
throw new InvalidIccProfileException("Missing matrix column or channel."); |
|||
} |
|||
|
|||
return new ColorTrcCalculator( |
|||
redMatrixColumn, |
|||
greenMatrixColumn, |
|||
blueMatrixColumn, |
|||
redTrc, |
|||
greenTrc, |
|||
blueTrc, |
|||
toPcs); |
|||
} |
|||
|
|||
private static GrayTrcCalculator InitGrayTrc(IccProfile profile, bool toPcs) |
|||
{ |
|||
IccTagDataEntry entry = GetTag(profile, IccProfileTag.GrayTrc); |
|||
return new GrayTrcCalculator(entry, toPcs); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
#nullable disable |
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal abstract partial class IccConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="IccConverterBase"/> class.
|
|||
/// </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) |
|||
{ |
|||
Guard.NotNull(profile, nameof(profile)); |
|||
this.Init(profile, toPcs, profile.Header.RenderingIntent); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts colors with the initially provided ICC profile
|
|||
/// </summary>
|
|||
/// <param name="value">The value to convert</param>
|
|||
/// <returns>The converted value</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value); |
|||
|
|||
/// <summary>
|
|||
/// Converts colors with the initially provided ICC profile
|
|||
/// </summary>
|
|||
/// <param name="source">The source colors</param>
|
|||
/// <param name="destination">The destination colors</param>
|
|||
public void Calculate(ReadOnlySpan<Vector4> source, Span<Vector4> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = this.Calculate(source[i]); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal class IccDataToDataConverter : IccConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="IccDataToDataConverter"/> class.
|
|||
/// </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
|
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal class IccDataToPcsConverter : IccConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// 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) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal class IccPcsToDataConverter : IccConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// 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) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles.Icc; |
|||
|
|||
/// <summary>
|
|||
/// Color converter for ICC profiles
|
|||
/// </summary>
|
|||
internal class IccPcsToPcsConverter : IccConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="IccPcsToPcsConverter"/> class.
|
|||
/// </summary>
|
|||
/// <param name="profile">The ICC profile to use for the conversions</param>
|
|||
public IccPcsToPcsConverter(IccProfile profile) |
|||
: base(profile, true) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Provides standard YCbCr matrices for RGB to YCbCr conversion.
|
|||
/// </summary>
|
|||
public static class KnownYCbCrMatrices |
|||
{ |
|||
#pragma warning disable SA1137 // Elements should have the same indentation
|
|||
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
|
|||
/// <summary>
|
|||
/// ITU-R BT.601 (SD video standard).
|
|||
/// </summary>
|
|||
public static readonly YCbCrTransform BT601 = new( |
|||
new Matrix4x4( |
|||
0.299000F, 0.587000F, 0.114000F, 0F, |
|||
-0.168736F, -0.331264F, 0.500000F, 0F, |
|||
0.500000F, -0.418688F, -0.081312F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Matrix4x4( |
|||
1.000000F, 0.000000F, 1.402000F, 0F, |
|||
1.000000F, -0.344136F, -0.714136F, 0F, |
|||
1.000000F, 1.772000F, 0.000000F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Vector3(0F, 0.5F, 0.5F)); |
|||
|
|||
/// <summary>
|
|||
/// ITU-R BT.709 (HD video, sRGB standard).
|
|||
/// </summary>
|
|||
public static readonly YCbCrTransform BT709 = new( |
|||
new Matrix4x4( |
|||
0.212600F, 0.715200F, 0.072200F, 0F, |
|||
-0.114572F, -0.385428F, 0.500000F, 0F, |
|||
0.500000F, -0.454153F, -0.045847F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Matrix4x4( |
|||
1.000000F, 0.000000F, 1.574800F, 0F, |
|||
1.000000F, -0.187324F, -0.468124F, 0F, |
|||
1.000000F, 1.855600F, 0.000000F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Vector3(0F, 0.5F, 0.5F)); |
|||
|
|||
/// <summary>
|
|||
/// ITU-R BT.2020 (UHD/4K video standard).
|
|||
/// </summary>
|
|||
public static readonly YCbCrTransform BT2020 = new( |
|||
new Matrix4x4( |
|||
0.262700F, 0.678000F, 0.059300F, 0F, |
|||
-0.139630F, -0.360370F, 0.500000F, 0F, |
|||
0.500000F, -0.459786F, -0.040214F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Matrix4x4( |
|||
1.000000F, 0.000000F, 1.474600F, 0F, |
|||
1.000000F, -0.164553F, -0.571353F, 0F, |
|||
1.000000F, 1.881400F, 0.000000F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Vector3(0F, 0.5F, 0.5F)); |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Represents a Y (luminance) color.
|
|||
/// </summary>
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
public readonly struct Y : IColorProfile<Y, Rgb> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Y"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="l">The luminance component.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public Y(float l) => this.L = Numerics.Clamp(l, 0, 1); |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
private Y(float l, bool _) => this.L = l; |
|||
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
/// <summary>
|
|||
/// Gets the luminance component.
|
|||
/// </summary>
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
public float L { get; } |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="Y"/> objects for equality.
|
|||
/// </summary>
|
|||
/// <param name="left">The <see cref="Y"/> on the left side of the operand.</param>
|
|||
/// <param name="right">The <see cref="Y"/> on the right side of the operand.</param>
|
|||
/// <returns>
|
|||
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static bool operator ==(Y left, Y right) => left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="Y"/> objects for inequality.
|
|||
/// </summary>
|
|||
/// <param name="left">The <see cref="Y"/> on the left side of the operand.</param>
|
|||
/// <param name="right">The <see cref="Y"/> on the right side of the operand.</param>
|
|||
/// <returns>
|
|||
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static bool operator !=(Y left, Y right) => !left.Equals(right); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Vector4 ToScaledVector4() => new(this.L); |
|||
|
|||
/// <inheritdoc/>
|
|||
public static Y FromScaledVector4(Vector4 source) => new(source.X, true); |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void ToScaledVector4(ReadOnlySpan<Y> source, Span<Vector4> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
|
|||
// TODO: Optimize via SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = source[i].ToScaledVector4(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<Y> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
|
|||
// TODO: Optimize via SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = FromScaledVector4(source[i]); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Rgb ToProfileConnectingSpace(ColorConversionOptions options) |
|||
=> new(this.L, this.L, this.L); |
|||
|
|||
/// <inheritdoc/>
|
|||
public static Y FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) |
|||
{ |
|||
Matrix4x4 m = options.YCbCrTransform.Forward; |
|||
float offset = options.YCbCrTransform.Offset.X; |
|||
return new Y(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Y> source, Span<Rgb> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
|
|||
// TODO: We can optimize this by using SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = source[i].ToProfileConnectingSpace(options); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<Y> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
|
|||
// TODO: We can optimize this by using SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
Rgb rgb = source[i]; |
|||
destination[i] = FromProfileConnectingSpace(options, in rgb); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() |
|||
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace; |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int GetHashCode() |
|||
=> this.L.GetHashCode(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() |
|||
=> FormattableString.Invariant($"Y({this.L:#0.##})"); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object? obj) |
|||
=> obj is Y other && this.Equals(other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(Y other) => this.L == other.L; |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// Represents a YCbCr color transform containing forward and inverse transformation matrices,
|
|||
/// and the chrominance offsets to apply for full-range encoding
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// These matrices must be selected to match the characteristics of the associated <see cref="RgbWorkingSpace"/>,
|
|||
/// including its transfer function (gamma or companding) and chromaticity coordinates. Using mismatched matrices and
|
|||
/// working spaces will produce incorrect conversions.
|
|||
/// </para>
|
|||
/// </summary>
|
|||
public readonly struct YCbCrTransform |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="YCbCrTransform"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="forward">
|
|||
/// The forward transformation matrix from RGB to YCbCr. The matrix must include the
|
|||
/// standard chrominance offsets in the fourth column, such as <c>(0, 0.5, 0.5)</c>.
|
|||
/// </param>
|
|||
/// <param name="inverse">
|
|||
/// The inverse transformation matrix from YCbCr to RGB. This matrix expects that
|
|||
/// chrominance offsets have already been subtracted prior to application.
|
|||
/// </param>
|
|||
/// <param name="offset">
|
|||
/// The chrominance offsets to be added after the forward conversion,
|
|||
/// and subtracted before the inverse conversion. Usually <c>(0, 0.5, 0.5)</c>.
|
|||
/// </param>
|
|||
public YCbCrTransform(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset) |
|||
{ |
|||
this.Forward = forward; |
|||
this.Inverse = inverse; |
|||
this.Offset = offset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the matrix used to convert gamma-encoded RGB to YCbCr.
|
|||
/// </summary>
|
|||
public Matrix4x4 Forward { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the matrix used to convert YCbCr back to gamma-encoded RGB.
|
|||
/// </summary>
|
|||
public Matrix4x4 Inverse { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the chrominance offset vector to apply during encoding (add) or decoding (subtract).
|
|||
/// </summary>
|
|||
public Vector3 Offset { get; } |
|||
|
|||
internal YCbCrTransform Transpose() |
|||
=> new(Matrix4x4.Transpose(this.Forward), Matrix4x4.Transpose(this.Inverse), this.Offset); |
|||
} |
|||
@ -0,0 +1,206 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Represents a YCCK (luminance, blue chroma, red chroma, black) color.
|
|||
/// YCCK is not a true color space but a reversible transform of CMYK, where the CMY components
|
|||
/// are converted to YCbCr using the ITU-R BT.601 standard, and the K (black) component is preserved separately.
|
|||
/// </summary>
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
public readonly struct YccK : IColorProfile<YccK, Rgb> |
|||
{ |
|||
private static readonly Vector4 Min = Vector4.Zero; |
|||
private static readonly Vector4 Max = Vector4.One; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="YccK"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="y">The y luminance component.</param>
|
|||
/// <param name="cb">The cb chroma component.</param>
|
|||
/// <param name="cr">The cr chroma component.</param>
|
|||
/// <param name="k">The keyline black component.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public YccK(float y, float cb, float cr, float k) |
|||
: this(new Vector4(y, cb, cr, k)) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="YccK"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector representing the c, m, y, k components.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public YccK(Vector4 vector) |
|||
{ |
|||
vector = Vector4.Clamp(vector, Min, Max); |
|||
this.Y = vector.X; |
|||
this.Cb = vector.Y; |
|||
this.Cr = vector.Z; |
|||
this.K = vector.W; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
private YccK(Vector4 vector, bool _) |
|||
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
|
|||
{ |
|||
this.Y = vector.X; |
|||
this.Cb = vector.Y; |
|||
this.Cr = vector.Z; |
|||
this.K = vector.W; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the Y luminance component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float Y { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the C (blue) chroma component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float Cb { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the C (red) chroma component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float Cr { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the keyline black color component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float K { get; } |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="YccK"/> objects for equality.
|
|||
/// </summary>
|
|||
/// <param name="left">The <see cref="YccK"/> on the left side of the operand.</param>
|
|||
/// <param name="right">The <see cref="YccK"/> on the right side of the operand.</param>
|
|||
/// <returns>
|
|||
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static bool operator ==(YccK left, YccK right) => left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="YccK"/> objects for inequality.
|
|||
/// </summary>
|
|||
/// <param name="left">The <see cref="YccK"/> on the left side of the operand.</param>
|
|||
/// <param name="right">The <see cref="YccK"/> on the right side of the operand.</param>
|
|||
/// <returns>
|
|||
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static bool operator !=(YccK left, YccK right) => !left.Equals(right); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Vector4 ToScaledVector4() |
|||
{ |
|||
Vector4 v4 = default; |
|||
v4 += this.AsVector4Unsafe(); |
|||
return v4; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static YccK FromScaledVector4(Vector4 source) |
|||
=> new(source, true); |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void ToScaledVector4(ReadOnlySpan<YccK> source, Span<Vector4> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
MemoryMarshal.Cast<YccK, Vector4>(source).CopyTo(destination); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<YccK> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
MemoryMarshal.Cast<Vector4, YccK>(source).CopyTo(destination); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Rgb ToProfileConnectingSpace(ColorConversionOptions options) |
|||
{ |
|||
Matrix4x4 m = options.TransposedYCbCrTransform.Inverse; |
|||
Vector3 offset = options.TransposedYCbCrTransform.Offset; |
|||
Vector3 normalized = this.AsVector3Unsafe() - offset; |
|||
|
|||
return Rgb.FromScaledVector3(Vector3.Transform(normalized, m) * (1F - this.K)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static YccK FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) |
|||
{ |
|||
Matrix4x4 m = options.TransposedYCbCrTransform.Forward; |
|||
Vector3 offset = options.TransposedYCbCrTransform.Offset; |
|||
|
|||
Vector3 rgb = source.AsVector3Unsafe(); |
|||
float k = 1F - MathF.Max(rgb.X, MathF.Max(rgb.Y, rgb.Z)); |
|||
|
|||
if (k >= 1F - Constants.Epsilon) |
|||
{ |
|||
return new YccK(new Vector4(0F, 0.5F, 0.5F, 1F), true); |
|||
} |
|||
|
|||
rgb /= 1F - k; |
|||
return new YccK(new Vector4(Vector3.Transform(rgb, m), k) + new Vector4(offset, 0F)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<YccK> source, Span<Rgb> destination) |
|||
{ |
|||
// TODO: We can possibly optimize this by using SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = source[i].ToProfileConnectingSpace(options); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<YccK> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
|
|||
// TODO: We can optimize this by using SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
Rgb rgb = source[i]; |
|||
destination[i] = FromProfileConnectingSpace(options, in rgb); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() |
|||
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace; |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int GetHashCode() |
|||
=> HashCode.Combine(this.Y, this.Cb, this.Cr, this.K); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() |
|||
=> FormattableString.Invariant($"YccK({this.Y:#0.##}, {this.Cb:#0.##}, {this.Cr:#0.##}, {this.K:#0.##})"); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object? obj) |
|||
=> obj is YccK other && this.Equals(other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(YccK other) |
|||
=> this.AsVector4Unsafe() == other.AsVector4Unsafe(); |
|||
|
|||
private Vector3 AsVector3Unsafe() => Unsafe.As<YccK, Vector3>(ref Unsafe.AsRef(in this)); |
|||
|
|||
private Vector4 AsVector4Unsafe() => Unsafe.As<YccK, Vector4>(ref Unsafe.AsRef(in this)); |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
#if !NET9_0_OR_GREATER
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.Intrinsics; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
internal static class Vector4Extensions |
|||
{ |
|||
/// <summary>
|
|||
/// Reinterprets a <see cref="Vector4" /> as a new <see cref="Vector3" />.
|
|||
/// </summary>
|
|||
/// <param name="value">The vector to reinterpret.</param>
|
|||
/// <returns><paramref name="value" /> reinterpreted as a new <see cref="Vector3" />.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static Vector3 AsVector3(this Vector4 value) => value.AsVector128().AsVector3(); |
|||
} |
|||
#endif
|
|||
File diff suppressed because it is too large
@ -0,0 +1,38 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
// <auto-generated />
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <summary>
|
|||
/// Represents a safe, fixed sized buffer of 4 elements.
|
|||
/// </summary>
|
|||
[InlineArray(4)] |
|||
internal struct InlineArray4<T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a safe, fixed sized buffer of 8 elements.
|
|||
/// </summary>
|
|||
[InlineArray(8)] |
|||
internal struct InlineArray8<T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a safe, fixed sized buffer of 16 elements.
|
|||
/// </summary>
|
|||
[InlineArray(16)] |
|||
internal struct InlineArray16<T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,38 @@ |
|||
<#@ template debug="false" hostspecific="false" language="C#" #> |
|||
<#@ assembly name="System.Core" #> |
|||
<#@ import namespace="System.Linq" #> |
|||
<#@ import namespace="System.Text" #> |
|||
<#@ import namespace="System.Collections.Generic" #> |
|||
// Copyright (c) Six Labors. |
|||
// Licensed under the Six Labors Split License. |
|||
|
|||
// <auto-generated /> |
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
<#GenerateInlineArrays();#> |
|||
|
|||
<#+ |
|||
private static int[] Lengths = [4, 8, 16 ]; |
|||
|
|||
void GenerateInlineArrays() |
|||
{ |
|||
foreach (int length in Lengths) |
|||
{ |
|||
#> |
|||
/// <summary> |
|||
/// Represents a safe, fixed sized buffer of <#=length#> elements. |
|||
/// </summary> |
|||
[InlineArray(<#=length#>)] |
|||
internal struct InlineArray<#=length#><T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
<#+ |
|||
} |
|||
} |
|||
#> |
|||
@ -0,0 +1,19 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats; |
|||
|
|||
/// <summary>
|
|||
/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency.
|
|||
/// </summary>
|
|||
public abstract class AlphaAwareImageEncoder : ImageEncoder |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or initializes the mode that determines how transparent pixels are handled during encoding.
|
|||
/// This overrides any other settings that may affect the encoding of transparent pixels
|
|||
/// including those passed via <see cref="QuantizerOptions"/>.
|
|||
/// </summary>
|
|||
public TransparentColorMode TransparentColorMode { get; init; } |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats; |
|||
|
|||
/// <summary>
|
|||
/// Provides enumeration of methods that control how ICC profiles are handled during decode.
|
|||
/// </summary>
|
|||
public enum ColorProfileHandling |
|||
{ |
|||
/// <summary>
|
|||
/// Leaves any embedded ICC color profiles intact.
|
|||
/// </summary>
|
|||
Preserve, |
|||
|
|||
/// <summary>
|
|||
/// Removes any embedded Standard sRGB ICC color profiles without transforming the pixels of the image.
|
|||
/// </summary>
|
|||
Compact, |
|||
|
|||
/// <summary>
|
|||
/// Transforms the pixels of the image based on the conversion of any embedded ICC color profiles to sRGB V4 profile.
|
|||
/// The original profile is then removed.
|
|||
/// </summary>
|
|||
Convert |
|||
} |
|||
@ -0,0 +1,169 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats; |
|||
|
|||
/// <summary>
|
|||
/// Provides utilities for encoding images.
|
|||
/// </summary>
|
|||
internal static class EncodingUtilities |
|||
{ |
|||
/// <summary>
|
|||
/// Determines if transparent pixels can be replaced based on the specified color mode and pixel type.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
|
|||
/// <param name="mode">Indicates the color mode used to assess the ability to replace transparent pixels.</param>
|
|||
/// <returns>Returns true if transparent pixels can be replaced; otherwise, false.</returns>
|
|||
public static bool ShouldReplaceTransparentPixels<TPixel>(TransparentColorMode mode) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
=> mode == TransparentColorMode.Clear && TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated; |
|||
|
|||
/// <summary>
|
|||
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
|
|||
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
|
|||
public static void ReplaceTransparentPixels<TPixel>(ImageFrame<TPixel> frame) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
=> ReplaceTransparentPixels(frame.Configuration, frame.PixelBuffer); |
|||
|
|||
/// <summary>
|
|||
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="buffer">The <see cref="Buffer2D{TPixel}"/> where the transparent pixels will be changed.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void ReplaceTransparentPixels<TPixel>(Configuration configuration, Buffer2D<TPixel> buffer) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
Buffer2DRegion<TPixel> region = buffer.GetRegion(); |
|||
ReplaceTransparentPixels(configuration, in region); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="region">The <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
|
|||
public static void ReplaceTransparentPixels<TPixel>( |
|||
Configuration configuration, |
|||
in Buffer2DRegion<TPixel> region) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(region.Width); |
|||
Span<Vector4> vectorsSpan = vectors.GetSpan(); |
|||
for (int y = 0; y < region.Height; y++) |
|||
{ |
|||
Span<TPixel> span = region.DangerousGetRowSpan(y); |
|||
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale); |
|||
ReplaceTransparentPixels(vectorsSpan); |
|||
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
|
|||
/// </summary>
|
|||
/// <param name="source">A span of color vectors that will be checked for transparency and potentially modified.</param>
|
|||
public static void ReplaceTransparentPixels(Span<Vector4> source) |
|||
{ |
|||
if (Vector512.IsHardwareAccelerated && source.Length >= 4) |
|||
{ |
|||
Span<Vector512<float>> source512 = MemoryMarshal.Cast<Vector4, Vector512<float>>(source); |
|||
for (int i = 0; i < source512.Length; i++) |
|||
{ |
|||
ref Vector512<float> v = ref source512[i]; |
|||
|
|||
// Do `vector < threshold`
|
|||
Vector512<float> mask = Vector512.Equals(v, Vector512<float>.Zero); |
|||
|
|||
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
|
|||
mask = Vector512.Shuffle(mask, Vector512.Create(3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15)); |
|||
|
|||
// Use the mask to select the replacement vector
|
|||
// (replacement & mask) | (v512 & ~mask)
|
|||
v = Vector512.ConditionalSelect(mask, Vector512<float>.Zero, v); |
|||
} |
|||
|
|||
int m = Numerics.Modulo4(source.Length); |
|||
if (m != 0) |
|||
{ |
|||
for (int i = source.Length - m; i < source.Length; i++) |
|||
{ |
|||
if (source[i].W == 0) |
|||
{ |
|||
source[i] = Vector4.Zero; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else if (Vector256.IsHardwareAccelerated && source.Length >= 2) |
|||
{ |
|||
Span<Vector256<float>> source256 = MemoryMarshal.Cast<Vector4, Vector256<float>>(source); |
|||
for (int i = 0; i < source256.Length; i++) |
|||
{ |
|||
ref Vector256<float> v = ref source256[i]; |
|||
|
|||
// Do `vector < threshold`
|
|||
Vector256<float> mask = Vector256.Equals(v, Vector256<float>.Zero); |
|||
|
|||
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
|
|||
mask = Vector256.Shuffle(mask, Vector256.Create(3, 3, 3, 3, 7, 7, 7, 7)); |
|||
|
|||
// Use the mask to select the replacement vector
|
|||
// (replacement & mask) | (v256 & ~mask)
|
|||
v = Vector256.ConditionalSelect(mask, Vector256<float>.Zero, v); |
|||
} |
|||
|
|||
int m = Numerics.Modulo2(source.Length); |
|||
if (m != 0) |
|||
{ |
|||
for (int i = source.Length - m; i < source.Length; i++) |
|||
{ |
|||
if (source[i].W == 0) |
|||
{ |
|||
source[i] = Vector4.Zero; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
ref Vector4 v = ref source[i]; |
|||
Vector128<float> v128 = v.AsVector128(); |
|||
|
|||
// Do `vector == 0`
|
|||
Vector128<float> mask = Vector128.Equals(v128, Vector128<float>.Zero); |
|||
|
|||
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
|
|||
mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3)); |
|||
|
|||
// Use the mask to select the replacement vector
|
|||
// (replacement & mask) | (v128 & ~mask)
|
|||
v = Vector128.ConditionalSelect(mask, Vector128<float>.Zero, v128).AsVector4(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
if (source[i].W == 0F) |
|||
{ |
|||
source[i] = Vector4.Zero; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue