Browse Source

Merge branch 'main' into js/resize-map-optimizations

pull/2793/head
James Jackson-South 11 months ago
parent
commit
5fa4c13d7a
  1. 12
      .editorconfig
  2. 7
      .gitattributes
  3. 6
      .github/workflows/build-and-test.yml
  4. 7
      .github/workflows/code-coverage.yml
  5. 5
      Directory.Build.props
  6. 2
      shared-infrastructure
  7. 27
      src/ImageSharp/Advanced/AotCompilerTools.cs
  8. 46
      src/ImageSharp/ColorProfiles/CieLab.cs
  9. 56
      src/ImageSharp/ColorProfiles/CieLch.cs
  10. 56
      src/ImageSharp/ColorProfiles/CieLchuv.cs
  11. 15
      src/ImageSharp/ColorProfiles/CieLuv.cs
  12. 33
      src/ImageSharp/ColorProfiles/CieXyy.cs
  13. 86
      src/ImageSharp/ColorProfiles/CieXyz.cs
  14. 49
      src/ImageSharp/ColorProfiles/Cmyk.cs
  15. 39
      src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
  16. 9
      src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
  17. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
  18. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
  19. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
  20. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
  21. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
  22. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
  23. 731
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  24. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
  25. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
  26. 11
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
  27. 42
      src/ImageSharp/ColorProfiles/Hsl.cs
  28. 42
      src/ImageSharp/ColorProfiles/Hsv.cs
  29. 45
      src/ImageSharp/ColorProfiles/HunterLab.cs
  30. 48
      src/ImageSharp/ColorProfiles/IColorProfile.cs
  31. 506
      src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
  32. 65
      src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs
  33. 14
      src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs
  34. 47
      src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs
  35. 19
      src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs
  36. 17
      src/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs
  37. 19
      src/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs
  38. 18
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
  39. 135
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  40. 77
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs
  41. 80
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  42. 26
      src/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs
  43. 130
      src/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs
  44. 41
      src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
  45. 42
      src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs
  46. 155
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs
  47. 66
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs
  48. 109
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  49. 49
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs
  50. 22
      src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
  51. 22
      src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
  52. 22
      src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
  53. 22
      src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
  54. 62
      src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs
  55. 56
      src/ImageSharp/ColorProfiles/Lms.cs
  56. 106
      src/ImageSharp/ColorProfiles/Rgb.cs
  57. 12
      src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
  58. 142
      src/ImageSharp/ColorProfiles/Y.cs
  59. 86
      src/ImageSharp/ColorProfiles/YCbCr.cs
  60. 61
      src/ImageSharp/ColorProfiles/YCbCrTransform.cs
  61. 206
      src/ImageSharp/ColorProfiles/YccK.cs
  62. 21
      src/ImageSharp/Common/Extensions/Vector4Extensions.cs
  63. 17
      src/ImageSharp/Common/Helpers/Numerics.cs
  64. 263
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  65. 37
      src/ImageSharp/Common/Helpers/SimdUtils.cs
  66. 1226
      src/ImageSharp/Common/Helpers/Vector128Utilities.cs
  67. 465
      src/ImageSharp/Common/Helpers/Vector256Utilities.cs
  68. 133
      src/ImageSharp/Common/Helpers/Vector512Utilities.cs
  69. 38
      src/ImageSharp/Common/InlineArray.cs
  70. 38
      src/ImageSharp/Common/InlineArray.tt
  71. 4
      src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
  72. 4
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  73. 7
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  74. 3
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  75. 26
      src/ImageSharp/Formats/ColorProfileHandling.cs
  76. 3
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  77. 9
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  78. 45
      src/ImageSharp/Formats/DecoderOptions.cs
  79. 138
      src/ImageSharp/Formats/EncodingUtilities.cs
  80. 5
      src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs
  81. 5
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  82. 217
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  83. 238
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  84. 29
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  85. 41
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  86. 325
      src/ImageSharp/Formats/Gif/LzwDecoder.cs
  87. 6
      src/ImageSharp/Formats/IAnimatedImageEncoder.cs
  88. 6
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  89. 8
      src/ImageSharp/Formats/IFormatMetadata.cs
  90. 4
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  91. 2
      src/ImageSharp/Formats/ISpecializedDecoderOptions.cs
  92. 5
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  93. 9
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  94. 16
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  95. 30
      src/ImageSharp/Formats/ImageDecoder.cs
  96. 4
      src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs
  97. 153
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs
  98. 103
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt
  99. 144
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Intrinsic.cs
  100. 93
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs

12
.editorconfig

@ -104,8 +104,8 @@ dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:war
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:warning
dotnet_style_collection_initializer = true:warning
dotnet_style_object_initializer = true:error
dotnet_style_collection_initializer = true:error
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
@ -135,9 +135,9 @@ csharp_style_prefer_null_check_over_type_check = true:warning
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
[*.{cs,csx,cake}]
# 'var' preferences
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning
csharp_style_var_for_built_in_types = false:error
csharp_style_var_when_type_is_apparent = false:error
csharp_style_var_elsewhere = false:error
# Expression-bodied members
csharp_style_expression_bodied_methods = true:warning
csharp_style_expression_bodied_constructors = true:warning
@ -160,7 +160,7 @@ csharp_style_pattern_local_over_anonymous_function = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:error
# "Null" checking preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning

7
.gitattributes

@ -136,3 +136,10 @@
*.ico filter=lfs diff=lfs merge=lfs -text
*.cur filter=lfs diff=lfs merge=lfs -text
*.ani filter=lfs diff=lfs merge=lfs -text
*.heic filter=lfs diff=lfs merge=lfs -text
*.hif filter=lfs diff=lfs merge=lfs -text
*.avif filter=lfs diff=lfs merge=lfs -text
###############################################################################
# Handle ICC files by git lfs
###############################################################################
*.icc filter=lfs diff=lfs merge=lfs -text

6
.github/workflows/build-and-test.yml

@ -73,8 +73,10 @@ jobs:
steps:
- name: Install libgdi+, which is required for tests running on ubuntu
if: ${{ matrix.options.os == 'buildjet-4vcpu-ubuntu-2204-arm' }}
run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
if: ${{ contains(matrix.options.os, 'ubuntu') }}
run: |
sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
- name: Git Config
shell: bash

7
.github/workflows/code-coverage.yml

@ -17,6 +17,13 @@ jobs:
runs-on: ${{matrix.options.os}}
steps:
- name: Install libgdi+, which is required for tests running on ubuntu
if: ${{ contains(matrix.options.os, 'ubuntu') }}
run: |
sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
- name: Git Config
shell: bash
run: |

5
Directory.Build.props

@ -21,9 +21,8 @@
<!-- Import the shared global .props file -->
<Import Project="$(MSBuildThisFileDirectory)shared-infrastructure\msbuild\props\SixLabors.Global.props" />
<PropertyGroup Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<!-- Workaround various issues bound to implicit language features. -->
<LangVersion>preview</LangVersion>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<!--

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 1dbfb576c83507645265c79e03369b66cdc0379f
Subproject commit 5e13cde851a3d6e95d0dfdde2a57071f1efda9c3

27
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -138,10 +138,11 @@ internal static class AotCompilerTools
AotCompileResamplers<TPixel>();
AotCompileQuantizers<TPixel>();
AotCompilePixelSamplingStrategys<TPixel>();
AotCompilePixelMaps<TPixel>();
AotCompileDithers<TPixel>();
AotCompileMemoryManagers<TPixel>();
Unsafe.SizeOf<TPixel>();
_ = Unsafe.SizeOf<TPixel>();
// TODO: Do the discovery work to figure out what works and what doesn't.
}
@ -276,11 +277,11 @@ internal static class AotCompilerTools
private static void AotCompileSpectralConverter<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(SpectralConverter<TPixel>).GetPixelBuffer(default);
default(GrayJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(RgbJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(TiffJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(TiffOldJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(SpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(GrayJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(RgbJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(TiffJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
default(TiffOldJpegSpectralConverter<TPixel>).GetPixelBuffer(default, default);
}
/// <summary>
@ -514,6 +515,20 @@ internal static class AotCompilerTools
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
}
/// <summary>
/// This method pre-seeds the all <see cref="IColorIndexCache{T}" /> in the AoT compiler.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompilePixelMaps<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
}
/// <summary>
/// This method pre-seeds the all <see cref="IDither" /> in the AoT compiler.
/// </summary>

46
src/ImageSharp/ColorProfiles/CieLab.cs

@ -35,7 +35,6 @@ public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
/// <param name="vector">The vector representing the l, a, b components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLab(Vector3 vector)
: this()
{
this.L = vector.X;
this.A = vector.Y;
@ -82,6 +81,49 @@ public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLab left, CieLab right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
v3 += new Vector3(0, 128F, 128F);
v3 /= new Vector3(100F, 255F, 255F);
return new Vector4(v3, 1F);
}
/// <inheritdoc/>
public static CieLab FromScaledVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
v3 *= new Vector3(100F, 255, 255);
v3 -= new Vector3(0, 128F, 128F);
return new CieLab(v3);
}
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<CieLab> 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<CieLab> 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/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static CieLab FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
@ -136,7 +178,7 @@ public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
float yr = l > CieConstants.Kappa * CieConstants.Epsilon ? Numerics.Pow3((l + 16F) / 116F) : l / CieConstants.Kappa;
float zr = fz3 > CieConstants.Epsilon ? fz3 : ((116F * fz) - 16F) / CieConstants.Kappa;
CieXyz whitePoint = options.WhitePoint;
CieXyz whitePoint = options.SourceWhitePoint;
Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z);
Vector3 xyzr = new(xr, yr, zr);

56
src/ImageSharp/ColorProfiles/CieLch.cs

@ -42,6 +42,17 @@ public readonly struct CieLch : IColorProfile<CieLch, CieLab>
this.H = vector.Z;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private CieLch(Vector3 vector, bool _)
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
this.C = vector.Y;
this.H = vector.Z;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
@ -50,7 +61,7 @@ public readonly struct CieLch : IColorProfile<CieLch, CieLab>
/// <summary>
/// Gets the a chroma component.
/// <remarks>A value ranging from 0 to 200.</remarks>
/// <remarks>A value ranging from -200 to 200.</remarks>
/// </summary>
public float C { get; }
@ -82,6 +93,49 @@ public readonly struct CieLch : IColorProfile<CieLch, CieLab>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLch left, CieLch right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
v3 += new Vector3(0, 200, 0);
v3 /= new Vector3(100, 400, 360);
return new Vector4(v3, 1F);
}
/// <inheritdoc/>
public static CieLch FromScaledVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
v3 *= new Vector3(100, 400, 360);
v3 -= new Vector3(0, 200, 0);
return new CieLch(v3, true);
}
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<CieLch> 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<CieLch> 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 static CieLch FromProfileConnectingSpace(ColorConversionOptions options, in CieLab source)
{

56
src/ImageSharp/ColorProfiles/CieLchuv.cs

@ -35,7 +35,6 @@ public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
/// <param name="vector">The vector representing the l, c, h components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLchuv(Vector3 vector)
: this()
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
@ -43,6 +42,16 @@ public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
this.H = vector.Z;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private CieLchuv(Vector3 vector, bool _)
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
{
this.L = vector.X;
this.C = vector.Y;
this.H = vector.Z;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
@ -51,7 +60,7 @@ public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
/// <summary>
/// Gets the a chroma component.
/// <remarks>A value ranging from 0 to 200.</remarks>
/// <remarks>A value ranging from -200 to 200.</remarks>
/// </summary>
public float C { get; }
@ -81,6 +90,49 @@ public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
/// </returns>
public static bool operator !=(CieLchuv left, CieLchuv right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
v3 += new Vector3(0, 200, 0);
v3 /= new Vector3(100, 400, 360);
return new Vector4(v3, 1F);
}
/// <inheritdoc/>
public static CieLchuv FromScaledVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
v3 *= new Vector3(100, 400, 360);
v3 -= new Vector3(0, 200, 0);
return new CieLchuv(v3, true);
}
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<CieLchuv> 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<CieLchuv> 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 static CieLchuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{

15
src/ImageSharp/ColorProfiles/CieLuv.cs

@ -37,7 +37,6 @@ public readonly struct CieLuv : IColorProfile<CieLuv, CieXyz>
/// <param name="vector">The vector representing the l, u, v components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLuv(Vector3 vector)
: this()
{
this.L = vector.X;
this.U = vector.Y;
@ -84,6 +83,18 @@ public readonly struct CieLuv : IColorProfile<CieLuv, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLuv left, CieLuv right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4() => throw new NotImplementedException();
/// <inheritdoc/>
public static CieLuv FromScaledVector4(Vector4 source) => throw new NotImplementedException();
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<CieLuv> source, Span<Vector4> destination) => throw new NotImplementedException();
/// <inheritdoc/>
public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<CieLuv> destination) => throw new NotImplementedException();
/// <inheritdoc/>
public static CieLuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{
@ -143,7 +154,7 @@ public readonly struct CieLuv : IColorProfile<CieLuv, CieXyz>
// Use doubles here for accuracy.
// Conversion algorithm described here:
// http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html
CieXyz whitePoint = options.WhitePoint;
CieXyz whitePoint = options.SourceWhitePoint;
double l = this.L, u = this.U, v = this.V;

33
src/ImageSharp/ColorProfiles/CieXyy.cs

@ -35,7 +35,6 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
/// <param name="vector">The vector representing the x, y, Y components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(Vector3 vector)
: this()
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.X = vector.X;
@ -83,6 +82,38 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieXyy left, CieXyy right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
=> new(this.AsVector3Unsafe(), 1F);
/// <inheritdoc/>
public static CieXyy FromScaledVector4(Vector4 source)
=> new(source.AsVector3());
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<CieXyy> 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<CieXyy> 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 static CieXyy FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{

86
src/ImageSharp/ColorProfiles/CieXyz.cs

@ -34,7 +34,6 @@ public readonly struct CieXyz : IProfileConnectingSpace<CieXyz, CieXyz>
/// </summary>
/// <param name="vector">The vector representing the x, y, z components.</param>
public CieXyz(Vector3 vector)
: this()
{
this.X = vector.X;
this.Y = vector.Y;
@ -81,12 +80,85 @@ public readonly struct CieXyz : IProfileConnectingSpace<CieXyz, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieXyz left, CieXyz right) => !left.Equals(right);
/// <summary>
/// Returns a new <see cref="Vector3"/> representing this instance.
/// </summary>
/// <returns>The <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3() => new(this.X, this.Y, this.Z);
internal Vector3 ToVector3() => new(this.X, this.Y, this.Z);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Vector4 ToVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
return new Vector4(v3, 1F);
}
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
v3 *= 32768F / 65535;
return new Vector4(v3, 1F);
}
internal static CieXyz FromVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
return new CieXyz(v3);
}
/// <inheritdoc/>
public static CieXyz FromScaledVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
v3 *= 65535 / 32768F;
return new CieXyz(v3);
}
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<CieXyz> 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<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: Optimize via SIMD
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector4(source[i]);
}
}
internal static void FromVector4(ReadOnlySpan<Vector4> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: Optimize via SIMD
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromVector4(source[i]);
}
}
internal static void ToVector4(ReadOnlySpan<CieXyz> 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].ToVector4();
}
}
/// <inheritdoc/>
public static CieXyz FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
@ -127,5 +199,5 @@ public readonly struct CieXyz : IProfileConnectingSpace<CieXyz, CieXyz>
public bool Equals(CieXyz other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<CieXyz, Vector3>(ref Unsafe.AsRef(in this));
internal Vector3 AsVector3Unsafe() => Unsafe.As<CieXyz, Vector3>(ref Unsafe.AsRef(in this));
}

49
src/ImageSharp/ColorProfiles/Cmyk.cs

@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Represents an CMYK (cyan, magenta, yellow, keyline) color.
/// <see href="https://en.wikipedia.org/wiki/CMYK_color_model"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct Cmyk : IColorProfile<Cmyk, Rgb>
@ -36,7 +37,18 @@ public readonly struct Cmyk : IColorProfile<Cmyk, Rgb>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmyk(Vector4 vector)
{
vector = Numerics.Clamp(vector, Min, Max);
vector = Vector4.Clamp(vector, Min, Max);
this.C = vector.X;
this.M = vector.Y;
this.Y = vector.Z;
this.K = vector.W;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private Cmyk(Vector4 vector, bool _)
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
{
this.C = vector.X;
this.M = vector.Y;
this.Y = vector.Z;
@ -89,16 +101,42 @@ public readonly struct Cmyk : IColorProfile<Cmyk, Rgb>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Cmyk left, Cmyk right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector4 v4 = default;
v4 += this.AsVector4Unsafe();
return v4;
}
/// <inheritdoc/>
public static Cmyk FromScaledVector4(Vector4 source)
=> new(source, true);
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<Cmyk> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
MemoryMarshal.Cast<Cmyk, Vector4>(source).CopyTo(destination);
}
/// <inheritdoc/>
public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<Cmyk> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
MemoryMarshal.Cast<Vector4, Cmyk>(source).CopyTo(destination);
}
/// <inheritdoc/>
public static Cmyk FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{
// To CMY
Vector3 cmy = Vector3.One - source.ToScaledVector3();
Vector3 cmy = Vector3.One - source.AsVector3Unsafe();
// To CMYK
Vector3 k = new(MathF.Min(cmy.X, MathF.Min(cmy.Y, cmy.Z)));
if (MathF.Abs(k.X - 1F) < Constants.Epsilon)
if (k.X >= 1F - Constants.Epsilon)
{
return new Cmyk(0, 0, 0, 1F);
}
@ -124,7 +162,7 @@ public readonly struct Cmyk : IColorProfile<Cmyk, Rgb>
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (Vector3.One - new Vector3(this.K));
Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (1F - this.K);
return Rgb.FromScaledVector3(rgb);
}
@ -134,8 +172,7 @@ public readonly struct Cmyk : IColorProfile<Cmyk, Rgb>
// TODO: We can possibly optimize this by using SIMD
for (int i = 0; i < source.Length; i++)
{
Cmyk cmyk = source[i];
destination[i] = cmyk.ToProfileConnectingSpace(options);
destination[i] = source[i].ToProfileConnectingSpace(options);
}
}

39
src/ImageSharp/ColorProfiles/ColorConversionOptions.cs

@ -4,6 +4,7 @@
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.ColorProfiles;
@ -13,11 +14,16 @@ namespace SixLabors.ImageSharp.ColorProfiles;
public class ColorConversionOptions
{
private Matrix4x4 adaptationMatrix;
private YCbCrTransform yCbCrTransform;
/// <summary>
/// Initializes a new instance of the <see cref="ColorConversionOptions"/> class.
/// </summary>
public ColorConversionOptions() => this.AdaptationMatrix = KnownChromaticAdaptationMatrices.Bradford;
public ColorConversionOptions()
{
this.AdaptationMatrix = KnownChromaticAdaptationMatrices.Bradford;
this.YCbCrTransform = KnownYCbCrMatrices.BT601;
}
/// <summary>
/// Gets the memory allocator.
@ -27,7 +33,7 @@ public class ColorConversionOptions
/// <summary>
/// Gets the source white point used for chromatic adaptation in conversions from/to XYZ color space.
/// </summary>
public CieXyz WhitePoint { get; init; } = KnownIlluminants.D50;
public CieXyz SourceWhitePoint { get; init; } = KnownIlluminants.D50;
/// <summary>
/// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space.
@ -37,13 +43,36 @@ public class ColorConversionOptions
/// <summary>
/// Gets the source working space used for companding in conversions from/to XYZ color space.
/// </summary>
public RgbWorkingSpace RgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb;
public RgbWorkingSpace SourceRgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb;
/// <summary>
/// Gets the destination working space used for companding in conversions from/to XYZ color space.
/// </summary>
public RgbWorkingSpace TargetRgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb;
/// <summary>
/// Gets the YCbCr matrix to used to perform conversions from/to RGB.
/// </summary>
public YCbCrTransform YCbCrTransform
{
get => this.yCbCrTransform;
init
{
this.yCbCrTransform = value;
this.TransposedYCbCrTransform = value.Transpose();
}
}
/// <summary>
/// Gets the source ICC profile.
/// </summary>
public IccProfile? SourceIccProfile { get; init; }
/// <summary>
/// Gets the target ICC profile.
/// </summary>
public IccProfile? TargetIccProfile { get; init; }
/// <summary>
/// Gets the transformation matrix used in conversion to perform chromatic adaptation.
/// <see cref="KnownChromaticAdaptationMatrices"/> for further information. Default is Bradford.
@ -54,10 +83,12 @@ public class ColorConversionOptions
init
{
this.adaptationMatrix = value;
Matrix4x4.Invert(value, out Matrix4x4 inverted);
_ = Matrix4x4.Invert(value, out Matrix4x4 inverted);
this.InverseAdaptationMatrix = inverted;
}
}
internal YCbCrTransform TransposedYCbCrTransform { get; private set; }
internal Matrix4x4 InverseAdaptationMatrix { get; private set; }
}

9
src/ImageSharp/ColorProfiles/ColorProfileConverter.cs

@ -12,7 +12,7 @@ public class ColorProfileConverter
/// Initializes a new instance of the <see cref="ColorProfileConverter"/> class.
/// </summary>
public ColorProfileConverter()
: this(new())
: this(new())
{
}
@ -33,8 +33,8 @@ public class ColorProfileConverter
where TTo : struct, IColorProfile
{
CieXyz sourceWhitePoint = TFrom.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint
? this.Options.WhitePoint
: this.Options.RgbWorkingSpace.WhitePoint;
? this.Options.SourceWhitePoint
: this.Options.SourceRgbWorkingSpace.WhitePoint;
CieXyz targetWhitePoint = TTo.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint
? this.Options.TargetWhitePoint
@ -42,4 +42,7 @@ public class ColorProfileConverter
return (sourceWhitePoint, targetWhitePoint);
}
internal bool ShouldUseIccProfiles()
=> this.Options.SourceIccProfile != null && this.Options.TargetIccProfile != null;
}

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsCieLabCieLab
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieLab>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -33,6 +38,12 @@ internal static class ColorProfileConverterExtensionsCieLabCieLab
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieLab>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -32,6 +37,12 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsCieLabRgb
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, Rgb>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -33,6 +38,12 @@ internal static class ColorProfileConverterExtensionsCieLabRgb
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, Rgb>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieLab>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -32,6 +37,12 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieLab>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -29,6 +34,12 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsCieXyzRgb
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, Rgb>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -32,6 +37,12 @@ internal static class ColorProfileConverterExtensionsCieXyzRgb
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, Rgb>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

731
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -0,0 +1,731 @@
// 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];
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 = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
});
// Normalize the source, then convert to the PCS space.
Vector4 sourcePcs = sourceParams.Converter.Calculate(source.ToScaledVector4());
// If both profiles need PCS adjustment, they both share the same unadjusted PCS space
// 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));
}
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 = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
});
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;
}
}

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsRgbCieLab
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieLab>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -33,6 +38,12 @@ internal static class ColorProfileConverterExtensionsRgbCieLab
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieLab>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsRgbCieXyz
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -32,6 +37,12 @@ internal static class ColorProfileConverterExtensionsRgbCieXyz
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

11
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

@ -12,6 +12,11 @@ internal static class ColorProfileConverterExtensionsRgbRgb
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, Rgb>
{
if (converter.ShouldUseIccProfiles())
{
return converter.ConvertUsingIccProfile<TFrom, TTo>(source);
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS
@ -33,6 +38,12 @@ internal static class ColorProfileConverterExtensionsRgbRgb
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, Rgb>
{
if (converter.ShouldUseIccProfiles())
{
converter.ConvertUsingIccProfile(source, destination);
return;
}
ColorConversionOptions options = converter.Options;
// Convert to input PCS.

42
src/ImageSharp/ColorProfiles/Hsl.cs

@ -41,6 +41,16 @@ public readonly struct Hsl : IColorProfile<Hsl, Rgb>
this.L = vector.Z;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private Hsl(Vector3 vector, bool _)
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
{
this.H = vector.X;
this.S = vector.Y;
this.L = vector.Z;
}
/// <summary>
/// Gets the hue component.
/// <remarks>A value ranging between 0 and 360.</remarks>
@ -83,6 +93,38 @@ public readonly struct Hsl : IColorProfile<Hsl, Rgb>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Hsl left, Hsl right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
=> new(this.AsVector3Unsafe() / 360F, 1F);
/// <inheritdoc/>
public static Hsl FromScaledVector4(Vector4 source)
=> new(source.AsVector3() * 360F, true);
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<Hsl> 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<Hsl> 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 static Hsl FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{

42
src/ImageSharp/ColorProfiles/Hsv.cs

@ -41,6 +41,16 @@ public readonly struct Hsv : IColorProfile<Hsv, Rgb>
this.V = vector.Z;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private Hsv(Vector3 vector, bool _)
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
{
this.H = vector.X;
this.S = vector.Y;
this.V = vector.Z;
}
/// <summary>
/// Gets the hue component.
/// <remarks>A value ranging between 0 and 360.</remarks>
@ -81,6 +91,38 @@ public readonly struct Hsv : IColorProfile<Hsv, Rgb>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Hsv left, Hsv right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
=> new(this.AsVector3Unsafe() / 360F, 1F);
/// <inheritdoc/>
public static Hsv FromScaledVector4(Vector4 source)
=> new(source.AsVector3() * 360F, true);
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<Hsv> 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<Hsv> 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 static Hsv FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{

45
src/ImageSharp/ColorProfiles/HunterLab.cs

@ -80,6 +80,49 @@ public readonly struct HunterLab : IColorProfile<HunterLab, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(HunterLab left, HunterLab right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
v3 += new Vector3(0, 128F, 128F);
v3 /= new Vector3(100F, 255F, 255F);
return new Vector4(v3, 1F);
}
/// <inheritdoc/>
public static HunterLab FromScaledVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
v3 *= new Vector3(100F, 255, 255);
v3 -= new Vector3(0, 128F, 128F);
return new HunterLab(v3);
}
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<HunterLab> 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<HunterLab> 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 static HunterLab FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{
@ -127,7 +170,7 @@ public readonly struct HunterLab : IColorProfile<HunterLab, CieXyz>
{
// Conversion algorithm described here:
// http://en.wikipedia.org/wiki/Lab_color_space#Hunter_Lab
CieXyz whitePoint = options.WhitePoint;
CieXyz whitePoint = options.SourceWhitePoint;
float l = this.L, a = this.A, b = this.B;
float xn = whitePoint.X, yn = whitePoint.Y, zn = whitePoint.Z;

48
src/ImageSharp/ColorProfiles/IColorProfile.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
@ -15,18 +17,60 @@ public interface IColorProfile
public static abstract ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource();
}
/// <summary>
/// Defines the contract for all color profiles.
/// </summary>
/// <typeparam name="TSelf">The type of color profile.</typeparam>
public interface IColorProfile<TSelf> : IColorProfile, IEquatable<TSelf>
where TSelf : IColorProfile<TSelf>
{
/// <summary>
/// Expands the pixel into a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// The vector components are typically expanded in least to greatest significance order.
/// </summary>
/// <returns>The <see cref="Vector4"/>.</returns>
public Vector4 ToScaledVector4();
#pragma warning disable CA1000 // Do not declare static members on generic types
/// <summary>
/// Initializes the color instance from a generic a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// </summary>
/// <param name="source">The vector to load the pixel from.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
public static abstract TSelf FromScaledVector4(Vector4 source);
/// <summary>
/// Converts the span of colors to a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// </summary>
/// <param name="source">The color span to convert from.</param>
/// <param name="destination">The vector span to write the results to.</param>
public static abstract void ToScaledVector4(ReadOnlySpan<TSelf> source, Span<Vector4> destination);
/// <summary>
/// Converts the span of colors from a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// </summary>
/// <param name="source">The vector span to convert from.</param>
/// <param name="destination">The color span to write the results to.</param>
public static abstract void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<TSelf> destination);
#pragma warning restore CA1000 // Do not declare static members on generic types
}
/// <summary>
/// Defines the contract for all color profiles.
/// </summary>
/// <typeparam name="TSelf">The type of color profile.</typeparam>
/// <typeparam name="TProfileSpace">The type of color profile connecting space.</typeparam>
public interface IColorProfile<TSelf, TProfileSpace> : IColorProfile, IEquatable<TSelf>
public interface IColorProfile<TSelf, TProfileSpace> : IColorProfile<TSelf>
where TSelf : IColorProfile<TSelf, TProfileSpace>
where TProfileSpace : struct, IProfileConnectingSpace
{
#pragma warning disable CA1000 // Do not declare static members on generic types
/// <summary>
/// Converts the color from the profile connection space.
/// Initializes the color instance from the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The color profile connecting space.</param>

506
src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs

@ -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;
}
}

65
src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs

@ -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(CieXyz.FromScaledVector4(xyz).AsVector3Unsafe(), 1);
return this.curveCalculator.Calculate(xyz);
}
}
}

14
src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs

@ -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,
}
}

47
src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs

@ -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"),
};
}

19
src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs

@ -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);
}

17
src/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs

@ -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);
}

19
src/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs

@ -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);
}

18
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs

@ -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,
}
}

135
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs

@ -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);
}
}
}

77
src/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs

@ -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);
}
}

80
src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs

@ -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;
}
}

26
src/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs

@ -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);
}
}

130
src/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs

@ -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;
}
}

41
src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs

@ -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;
}
}

42
src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs

@ -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);
}
}

155
src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs

@ -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);
}

66
src/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs

@ -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,
}
}

109
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs

@ -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);
}
}

49
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs

@ -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]);
}
}
}

22
src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs

@ -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
{
}
}

22
src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs

@ -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)
{
}
}

22
src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs

@ -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)
{
}
}

22
src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs

@ -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)
{
}
}

62
src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs

@ -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));
}

56
src/ImageSharp/ColorProfiles/Lms.cs

@ -82,20 +82,53 @@ public readonly struct Lms : IColorProfile<Lms, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Lms left, Lms right) => !left.Equals(right);
/// <summary>
/// Returns a new <see cref="Vector3"/> representing this instance.
/// </summary>
/// <returns>The <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3() => new(this.L, this.M, this.S);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
v3 += new Vector3(1F);
v3 /= 2F;
return new Vector4(v3, 1F);
}
/// <inheritdoc/>
public static Lms FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
public static Lms FromScaledVector4(Vector4 source)
{
Vector3 v3 = source.AsVector3();
v3 *= 2F;
v3 -= new Vector3(1F);
return new Lms(v3);
}
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<Lms> 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<Lms> destination)
{
Vector3 vector = Vector3.Transform(source.ToVector3(), options.AdaptationMatrix);
return new Lms(vector);
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 static Lms FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
=> new(Vector3.Transform(source.AsVector3Unsafe(), options.AdaptationMatrix));
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<Lms> destination)
{
@ -110,10 +143,7 @@ public readonly struct Lms : IColorProfile<Lms, CieXyz>
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
Vector3 vector = Vector3.Transform(this.ToVector3(), options.InverseAdaptationMatrix);
return new CieXyz(vector);
}
=> new(Vector3.Transform(this.AsVector3Unsafe(), options.InverseAdaptationMatrix));
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Lms> source, Span<CieXyz> destination)

106
src/ImageSharp/ColorProfiles/Rgb.cs

@ -81,11 +81,54 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Rgb left, Rgb right) => !left.Equals(right);
/// <summary>
/// Initializes the color instance from a generic scaled <see cref="Vector4"/>.
/// </summary>
/// <param name="source">The vector to load the color from.</param>
/// <returns>The <see cref="Rgb"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb FromScaledVector4(Vector4 source)
=> new(source.AsVector3());
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and usually clamped between <value>0</value> and <value>1</value>.
/// The vector components are typically expanded in least to greatest significance order.
/// </summary>
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4()
=> new(this.AsVector3Unsafe(), 1F);
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<Rgb> 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<Rgb> 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 static Rgb FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{
// Convert to linear rgb then compress.
Rgb linear = new(Vector3.Transform(source.ToVector3(), GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace)));
Rgb linear = new(Vector3.Transform(source.AsVector3Unsafe(), GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace)));
return FromScaledVector4(options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4()));
}
@ -98,7 +141,7 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
for (int i = 0; i < source.Length; i++)
{
// Convert to linear rgb then compress.
Rgb linear = new(Vector3.Transform(source[i].ToVector3(), matrix));
Rgb linear = new(Vector3.Transform(source[i].AsVector3Unsafe(), matrix));
Vector4 nonlinear = options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4());
destination[i] = FromScaledVector4(nonlinear);
}
@ -108,10 +151,10 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// First expand to linear rgb
Rgb linear = FromScaledVector4(options.RgbWorkingSpace.Expand(this.ToScaledVector4()));
Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(this.ToScaledVector4()));
// Then convert to xyz
return new CieXyz(Vector3.Transform(linear.ToScaledVector3(), GetRgbToCieXyzMatrix(options.RgbWorkingSpace)));
return new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace)));
}
/// <inheritdoc/>
@ -119,21 +162,22 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Matrix4x4 matrix = GetRgbToCieXyzMatrix(options.RgbWorkingSpace);
Matrix4x4 matrix = GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace);
for (int i = 0; i < source.Length; i++)
{
Rgb rgb = source[i];
// First expand to linear rgb
Rgb linear = FromScaledVector4(options.RgbWorkingSpace.Expand(rgb.ToScaledVector4()));
Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(rgb.ToScaledVector4()));
// Then convert to xyz
destination[i] = new CieXyz(Vector3.Transform(linear.ToScaledVector3(), matrix));
destination[i] = new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), matrix));
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <summary>
/// Initializes the color instance from a generic scaled <see cref="Vector3"/>.
@ -141,19 +185,8 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
/// <param name="source">The vector to load the color from.</param>
/// <returns>The <see cref="Rgb"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb FromScaledVector3(Vector3 source) => new(Vector3.Clamp(source, Vector3.Zero, Vector3.One));
/// <summary>
/// Initializes the color instance from a generic scaled <see cref="Vector4"/>.
/// </summary>
/// <param name="source">The vector to load the color from.</param>
/// <returns>The <see cref="Rgb"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb FromScaledVector4(Vector4 source)
{
source = Vector4.Clamp(source, Vector4.Zero, Vector4.One);
return new(source.X, source.Y, source.Z);
}
public static Rgb FromScaledVector3(Vector3 source)
=> new(source);
/// <summary>
/// Initializes the color instance for a source clamped between <value>0</value> and <value>1</value>
@ -161,7 +194,8 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
/// <param name="source">The source to load the color from.</param>
/// <returns>The <see cref="Rgb"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb Clamp(Rgb source) => new(Vector3.Clamp(new(source.R, source.G, source.B), Vector3.Zero, Vector3.One));
public static Rgb Clamp(Rgb source)
=> new(Vector3.Clamp(source.AsVector3Unsafe(), Vector3.Zero, Vector3.One));
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector3"/> representation
@ -170,24 +204,12 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
/// </summary>
/// <returns>The <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToScaledVector3() => Clamp(this).ToVector3();
/// <summary>
/// Expands the color into a generic <see cref="Vector3"/> representation.
/// The vector components are typically expanded in least to greatest significance order.
/// </summary>
/// <returns>The <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3() => new(this.R, this.G, this.B);
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and usually clamped between <value>0</value> and <value>1</value>.
/// The vector components are typically expanded in least to greatest significance order.
/// </summary>
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4() => new(this.ToScaledVector3(), 1f);
public Vector3 ToScaledVector3()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
return v3;
}
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B);
@ -203,7 +225,7 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
public bool Equals(Rgb other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<Rgb, Vector3>(ref Unsafe.AsRef(in this));
internal Vector3 AsVector3Unsafe() => Unsafe.As<Rgb, Vector3>(ref Unsafe.AsRef(in this));
private static Matrix4x4 GetCieXyzToRgbMatrix(RgbWorkingSpace workingSpace)
{
@ -249,7 +271,7 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
Matrix4x4.Invert(xyzMatrix, out Matrix4x4 inverseXyzMatrix);
Vector3 vector = Vector3.Transform(workingSpace.WhitePoint.ToVector3(), inverseXyzMatrix);
Vector3 vector = Vector3.Transform(workingSpace.WhitePoint.AsVector3Unsafe(), inverseXyzMatrix);
// Use transposed Rows/Columns
return new Matrix4x4

12
src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs

@ -34,9 +34,9 @@ public static class VonKriesChromaticAdaptation
return new(source.X, source.Y, source.Z);
}
Vector3 sourceColorLms = Vector3.Transform(source.ToVector3(), matrix);
Vector3 sourceWhitePointLms = Vector3.Transform(from.ToVector3(), matrix);
Vector3 targetWhitePointLms = Vector3.Transform(to.ToVector3(), matrix);
Vector3 sourceColorLms = Vector3.Transform(source.AsVector3Unsafe(), matrix);
Vector3 sourceWhitePointLms = Vector3.Transform(from.AsVector3Unsafe(), matrix);
Vector3 targetWhitePointLms = Vector3.Transform(to.AsVector3Unsafe(), matrix);
Vector3 vector = targetWhitePointLms / sourceWhitePointLms;
Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms);
@ -76,8 +76,8 @@ public static class VonKriesChromaticAdaptation
ref CieXyz sourceBase = ref MemoryMarshal.GetReference(source);
ref CieXyz destinationBase = ref MemoryMarshal.GetReference(destination);
Vector3 sourceWhitePointLms = Vector3.Transform(from.ToVector3(), matrix);
Vector3 targetWhitePointLms = Vector3.Transform(to.ToVector3(), matrix);
Vector3 sourceWhitePointLms = Vector3.Transform(from.AsVector3Unsafe(), matrix);
Vector3 targetWhitePointLms = Vector3.Transform(to.AsVector3Unsafe(), matrix);
Vector3 vector = targetWhitePointLms / sourceWhitePointLms;
@ -86,7 +86,7 @@ public static class VonKriesChromaticAdaptation
ref CieXyz sp = ref Unsafe.Add(ref sourceBase, i);
ref CieXyz dp = ref Unsafe.Add(ref destinationBase, i);
Vector3 sourceColorLms = Vector3.Transform(sp.ToVector3(), matrix);
Vector3 sourceColorLms = Vector3.Transform(sp.AsVector3Unsafe(), matrix);
Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms);
dp = new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix));

142
src/ImageSharp/ColorProfiles/Y.cs

@ -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(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;
}

86
src/ImageSharp/ColorProfiles/YCbCr.cs

@ -8,15 +8,13 @@ using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Represents an YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification for the JFIF use with Jpeg.
/// <see href="http://en.wikipedia.org/wiki/YCbCr"/>
/// <see href="http://www.ijg.org/files/T-REC-T.871-201105-I!!PDF-E.pdf"/>
/// Represents an YCbCr (luminance, blue chroma, red chroma) color.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
{
private static readonly Vector3 Min = Vector3.Zero;
private static readonly Vector3 Max = new(255);
private static readonly Vector3 Max = Vector3.One;
/// <summary>
/// Initializes a new instance of the <see cref="YCbCr"/> struct.
@ -43,21 +41,31 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
this.Cr = vector.Z;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private YCbCr(Vector3 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;
}
/// <summary>
/// Gets the Y luminance component.
/// <remarks>A value ranging between 0 and 255.</remarks>
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float Y { get; }
/// <summary>
/// Gets the Cb chroma component.
/// <remarks>A value ranging between 0 and 255.</remarks>
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float Cb { get; }
/// <summary>
/// Gets the Cr chroma component.
/// <remarks>A value ranging between 0 and 255.</remarks>
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float Cr { get; }
@ -83,18 +91,49 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
public static bool operator !=(YCbCr left, YCbCr right) => !left.Equals(right);
/// <inheritdoc/>
public static YCbCr FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
public Vector4 ToScaledVector4()
{
Vector3 rgb = source.ToScaledVector3() * Max;
float r = rgb.X;
float g = rgb.Y;
float b = rgb.Z;
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
return new Vector4(v3, 1F);
}
float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
float cr = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b));
/// <inheritdoc/>
public static YCbCr FromScaledVector4(Vector4 source)
=> new(source.AsVector3(), true);
/// <inheritdoc/>
public static void ToScaledVector4(ReadOnlySpan<YCbCr> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
return new YCbCr(y, cb, cr);
// 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<YCbCr> 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 static YCbCr FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{
Vector3 rgb = source.AsVector3Unsafe();
Matrix4x4 m = options.TransposedYCbCrTransform.Forward;
Vector3 offset = options.TransposedYCbCrTransform.Offset;
return new YCbCr(Vector3.Transform(rgb, m) + offset, true);
}
/// <inheritdoc/>
@ -113,15 +152,11 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
float y = this.Y;
float cb = this.Cb - 128F;
float cr = this.Cr - 128F;
float r = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero);
float g = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero);
float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero);
Matrix4x4 m = options.TransposedYCbCrTransform.Inverse;
Vector3 offset = options.TransposedYCbCrTransform.Offset;
Vector3 normalized = this.AsVector3Unsafe() - offset;
return Rgb.FromScaledVector3(new Vector3(r, g, b) / Max);
return Rgb.FromScaledVector3(Vector3.Transform(normalized, m));
}
/// <inheritdoc/>
@ -132,8 +167,7 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
// TODO: We can optimize this by using SIMD
for (int i = 0; i < source.Length; i++)
{
YCbCr ycbcr = source[i];
destination[i] = ycbcr.ToProfileConnectingSpace(options);
destination[i] = source[i].ToProfileConnectingSpace(options);
}
}

61
src/ImageSharp/ColorProfiles/YCbCrTransform.cs

@ -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);
}

206
src/ImageSharp/ColorProfiles/YccK.cs

@ -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));
}

21
src/ImageSharp/Common/Extensions/Vector4Extensions.cs

@ -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

17
src/ImageSharp/Common/Helpers/Numerics.cs

@ -884,23 +884,6 @@ internal static class Numerics
accumulator += intHigh;
}
/// <summary>
/// Reduces elements of the vector into one sum.
/// </summary>
/// <param name="accumulator">The accumulator to reduce.</param>
/// <returns>The sum of all elements.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReduceSum(Vector128<int> accumulator)
{
// Add odd to even.
Vector128<int> vsum = Sse2.Add(accumulator, Sse2.Shuffle(accumulator, 0b_11_11_01_01));
// Add high to low.
vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_10_11_10));
return Sse2.ConvertToInt32(vsum);
}
/// <summary>
/// Reduces elements of the vector into one sum.
/// </summary>

263
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -66,9 +66,9 @@ internal static partial class SimdUtils
ref Span<float> destination,
[ConstantExpected] byte control)
{
if ((Vector512.IsHardwareAccelerated && Vector512Utilities.SupportsShuffleFloat) ||
(Vector256.IsHardwareAccelerated && Vector256Utilities.SupportsShuffleFloat) ||
(Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleFloat))
if (Vector512.IsHardwareAccelerated ||
Vector256.IsHardwareAccelerated ||
Vector128.IsHardwareAccelerated)
{
int remainder = 0;
if (Vector512.IsHardwareAccelerated)
@ -112,9 +112,9 @@ internal static partial class SimdUtils
ref Span<byte> destination,
[ConstantExpected] byte control)
{
if ((Vector512.IsHardwareAccelerated && Vector512Utilities.SupportsShuffleByte) ||
(Vector256.IsHardwareAccelerated && Vector256Utilities.SupportsShuffleByte) ||
(Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte))
if (Vector512.IsHardwareAccelerated ||
Vector256.IsHardwareAccelerated ||
Vector128.IsHardwareAccelerated)
{
int remainder = 0;
if (Vector512.IsHardwareAccelerated)
@ -158,7 +158,7 @@ internal static partial class SimdUtils
ref Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte && Vector128Utilities.SupportsRightAlign)
if (Vector128.IsHardwareAccelerated)
{
int remainder = source.Length % (Vector128<byte>.Count * 3);
@ -190,7 +190,7 @@ internal static partial class SimdUtils
ref Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte && Vector128Utilities.SupportsShiftByte)
if (Vector128.IsHardwareAccelerated)
{
int remainder = source.Length % (Vector128<byte>.Count * 3);
@ -223,7 +223,7 @@ internal static partial class SimdUtils
ref Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte && Vector128Utilities.SupportsShiftByte)
if (Vector128.IsHardwareAccelerated)
{
int remainder = source.Length & ((Vector128<byte>.Count * 4) - 1); // bit-hack for modulo
@ -249,7 +249,7 @@ internal static partial class SimdUtils
Span<float> destination,
[ConstantExpected] byte control)
{
if (Vector512.IsHardwareAccelerated && Vector512Utilities.SupportsShuffleFloat)
if (Vector512.IsHardwareAccelerated)
{
ref Vector512<float> sourceBase = ref Unsafe.As<float, Vector512<float>>(ref MemoryMarshal.GetReference(source));
ref Vector512<float> destinationBase = ref Unsafe.As<float, Vector512<float>>(ref MemoryMarshal.GetReference(destination));
@ -263,21 +263,21 @@ internal static partial class SimdUtils
ref Vector512<float> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector512<float> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector512Utilities.Shuffle(vs0, control);
Unsafe.Add(ref vd0, (nuint)1) = Vector512Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)1), control);
Unsafe.Add(ref vd0, (nuint)2) = Vector512Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)2), control);
Unsafe.Add(ref vd0, (nuint)3) = Vector512Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)3), control);
vd0 = Vector512_.ShuffleNative(vs0, control);
Unsafe.Add(ref vd0, (nuint)1) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), control);
Unsafe.Add(ref vd0, (nuint)2) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), control);
Unsafe.Add(ref vd0, (nuint)3) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), control);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector512Utilities.Shuffle(Unsafe.Add(ref sourceBase, i), control);
Unsafe.Add(ref destinationBase, i) = Vector512_.ShuffleNative(Unsafe.Add(ref sourceBase, i), control);
}
}
}
else if (Vector256.IsHardwareAccelerated && Vector256Utilities.SupportsShuffleFloat)
else if (Vector256.IsHardwareAccelerated)
{
ref Vector256<float> sourceBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> destinationBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(destination));
@ -291,21 +291,21 @@ internal static partial class SimdUtils
ref Vector256<float> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector256<float> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector256Utilities.Shuffle(vs0, control);
Unsafe.Add(ref vd0, (nuint)1) = Vector256Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)1), control);
Unsafe.Add(ref vd0, (nuint)2) = Vector256Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)2), control);
Unsafe.Add(ref vd0, (nuint)3) = Vector256Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)3), control);
vd0 = Vector256_.ShuffleNative(vs0, control);
Unsafe.Add(ref vd0, (nuint)1) = Vector256_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), control);
Unsafe.Add(ref vd0, (nuint)2) = Vector256_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), control);
Unsafe.Add(ref vd0, (nuint)3) = Vector256_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), control);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector256Utilities.Shuffle(Unsafe.Add(ref sourceBase, i), control);
Unsafe.Add(ref destinationBase, i) = Vector256_.ShuffleNative(Unsafe.Add(ref sourceBase, i), control);
}
}
}
else if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleFloat)
else if (Vector128.IsHardwareAccelerated)
{
ref Vector128<float> sourceBase = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(source));
ref Vector128<float> destinationBase = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(destination));
@ -319,17 +319,17 @@ internal static partial class SimdUtils
ref Vector128<float> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector128<float> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector128Utilities.Shuffle(vs0, control);
Unsafe.Add(ref vd0, (nuint)1) = Vector128Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)1), control);
Unsafe.Add(ref vd0, (nuint)2) = Vector128Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)2), control);
Unsafe.Add(ref vd0, (nuint)3) = Vector128Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)3), control);
vd0 = Vector128_.ShuffleNative(vs0, control);
Unsafe.Add(ref vd0, (nuint)1) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), control);
Unsafe.Add(ref vd0, (nuint)2) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), control);
Unsafe.Add(ref vd0, (nuint)3) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), control);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector128Utilities.Shuffle(Unsafe.Add(ref sourceBase, i), control);
Unsafe.Add(ref destinationBase, i) = Vector128_.ShuffleNative(Unsafe.Add(ref sourceBase, i), control);
}
}
}
@ -341,7 +341,7 @@ internal static partial class SimdUtils
Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector512.IsHardwareAccelerated && Vector512Utilities.SupportsShuffleByte)
if (Vector512.IsHardwareAccelerated)
{
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
Shuffle.MMShuffleSpan(ref temp, control);
@ -359,22 +359,27 @@ internal static partial class SimdUtils
ref Vector512<byte> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector512<byte> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector512Utilities.Shuffle(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector512Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector512Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector512Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)3), mask);
vd0 = Vector512_.ShuffleNative(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector512Utilities.Shuffle(Unsafe.Add(ref sourceBase, i), mask);
Unsafe.Add(ref destinationBase, i) = Vector512_.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask);
}
}
}
else if (Vector256.IsHardwareAccelerated && Vector256Utilities.SupportsShuffleByte)
else if (Vector256.IsHardwareAccelerated)
{
// ShufflePerLane performs per-128-bit-lane shuffling using Avx2.Shuffle (vpshufb).
// MMShuffleSpan generates indices in the range [0, 31] and never sets bit 7 in any byte,
// so the shuffle will not zero elements. Because vpshufb uses only the low 4 bits (b[i] & 0x0F)
// for indexing within each lane, and ignores the upper bits unless bit 7 is set,
// this usage is guaranteed to remain within-lane and non-zeroing.
Span<byte> temp = stackalloc byte[Vector256<byte>.Count];
Shuffle.MMShuffleSpan(ref temp, control);
Vector256<byte> mask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(temp));
@ -391,21 +396,21 @@ internal static partial class SimdUtils
ref Vector256<byte> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector256<byte> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector256Utilities.Shuffle(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector256Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector256Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector256Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)3), mask);
vd0 = Vector256_.ShufflePerLane(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector256_.ShufflePerLane(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector256_.ShufflePerLane(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector256_.ShufflePerLane(Unsafe.Add(ref vs0, (nuint)3), mask);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector256Utilities.Shuffle(Unsafe.Add(ref sourceBase, i), mask);
Unsafe.Add(ref destinationBase, i) = Vector256_.ShufflePerLane(Unsafe.Add(ref sourceBase, i), mask);
}
}
}
else if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte)
else if (Vector128.IsHardwareAccelerated)
{
Span<byte> temp = stackalloc byte[Vector128<byte>.Count];
Shuffle.MMShuffleSpan(ref temp, control);
@ -423,17 +428,17 @@ internal static partial class SimdUtils
ref Vector128<byte> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector128<byte> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector128Utilities.Shuffle(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector128Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector128Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector128Utilities.Shuffle(Unsafe.Add(ref vs0, (nuint)3), mask);
vd0 = Vector128_.ShuffleNative(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector128Utilities.Shuffle(Unsafe.Add(ref sourceBase, i), mask);
Unsafe.Add(ref destinationBase, i) = Vector128_.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask);
}
}
}
@ -445,11 +450,11 @@ internal static partial class SimdUtils
Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte && Vector128Utilities.SupportsRightAlign)
if (Vector128.IsHardwareAccelerated)
{
Vector128<byte> maskPad4Nx16 = ShuffleMaskPad4Nx16();
Vector128<byte> maskSlice4Nx16 = ShuffleMaskSlice4Nx16();
Vector128<byte> maskE = Vector128Utilities.AlignRight(maskSlice4Nx16, maskSlice4Nx16, 12);
Vector128<byte> maskE = Vector128_.AlignRight(maskSlice4Nx16, maskSlice4Nx16, 12);
Span<byte> bytes = stackalloc byte[Vector128<byte>.Count];
Shuffle.MMShuffleSpan(ref bytes, control);
@ -467,28 +472,28 @@ internal static partial class SimdUtils
Vector128<byte> v0 = vs;
Vector128<byte> v1 = Unsafe.Add(ref vs, (nuint)1);
Vector128<byte> v2 = Unsafe.Add(ref vs, (nuint)2);
Vector128<byte> v3 = Vector128Utilities.ShiftRightBytesInVector(v2, 4);
Vector128<byte> v3 = Vector128_.ShiftRightBytesInVector(v2, 4);
v2 = Vector128Utilities.AlignRight(v2, v1, 8);
v1 = Vector128Utilities.AlignRight(v1, v0, 12);
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
v0 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v0, maskPad4Nx16), mask);
v1 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v1, maskPad4Nx16), mask);
v2 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v2, maskPad4Nx16), mask);
v3 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v3, maskPad4Nx16), mask);
v0 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, maskPad4Nx16), mask);
v1 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, maskPad4Nx16), mask);
v2 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, maskPad4Nx16), mask);
v3 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, maskPad4Nx16), mask);
v0 = Vector128Utilities.Shuffle(v0, maskE);
v1 = Vector128Utilities.Shuffle(v1, maskSlice4Nx16);
v2 = Vector128Utilities.Shuffle(v2, maskE);
v3 = Vector128Utilities.Shuffle(v3, maskSlice4Nx16);
v0 = Vector128_.ShuffleNative(v0, maskE);
v1 = Vector128_.ShuffleNative(v1, maskSlice4Nx16);
v2 = Vector128_.ShuffleNative(v2, maskE);
v3 = Vector128_.ShuffleNative(v3, maskSlice4Nx16);
v0 = Vector128Utilities.AlignRight(v1, v0, 4);
v3 = Vector128Utilities.AlignRight(v3, v2, 12);
v0 = Vector128_.AlignRight(v1, v0, 4);
v3 = Vector128_.AlignRight(v3, v2, 12);
v1 = Vector128Utilities.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128Utilities.ShiftRightBytesInVector(v2, 4);
v1 = Vector128_.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128_.ShiftRightBytesInVector(v2, 4);
v1 = Vector128Utilities.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v2, v1, 8);
ref Vector128<byte> vd = ref Unsafe.Add(ref destinationBase, i);
@ -505,7 +510,7 @@ internal static partial class SimdUtils
Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte && Vector128Utilities.SupportsShiftByte)
if (Vector128.IsHardwareAccelerated)
{
Vector128<byte> maskPad4Nx16 = ShuffleMaskPad4Nx16();
Vector128<byte> fill = Vector128.Create(0xff000000ff000000ul).AsByte();
@ -527,17 +532,17 @@ internal static partial class SimdUtils
ref Vector128<byte> v0 = ref Unsafe.Add(ref sourceBase, i);
Vector128<byte> v1 = Unsafe.Add(ref v0, 1);
Vector128<byte> v2 = Unsafe.Add(ref v0, 2);
Vector128<byte> v3 = Vector128Utilities.ShiftRightBytesInVector(v2, 4);
Vector128<byte> v3 = Vector128_.ShiftRightBytesInVector(v2, 4);
v2 = Vector128Utilities.AlignRight(v2, v1, 8);
v1 = Vector128Utilities.AlignRight(v1, v0, 12);
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
ref Vector128<byte> vd = ref Unsafe.Add(ref destinationBase, j);
vd = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v0, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 1) = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v1, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 2) = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v2, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 3) = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v3, maskPad4Nx16) | fill, mask);
vd = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 1) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 2) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 3) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, maskPad4Nx16) | fill, mask);
}
}
}
@ -548,10 +553,10 @@ internal static partial class SimdUtils
Span<byte> destination,
[ConstantExpected] byte control)
{
if (Vector128.IsHardwareAccelerated && Vector128Utilities.SupportsShuffleByte && Vector128Utilities.SupportsShiftByte)
if (Vector128.IsHardwareAccelerated)
{
Vector128<byte> maskSlice4Nx16 = ShuffleMaskSlice4Nx16();
Vector128<byte> maskE = Vector128Utilities.AlignRight(maskSlice4Nx16, maskSlice4Nx16, 12);
Vector128<byte> maskE = Vector128_.AlignRight(maskSlice4Nx16, maskSlice4Nx16, 12);
Span<byte> temp = stackalloc byte[Vector128<byte>.Count];
Shuffle.MMShuffleSpan(ref temp, control);
@ -574,18 +579,18 @@ internal static partial class SimdUtils
Vector128<byte> v2 = Unsafe.Add(ref vs, 2);
Vector128<byte> v3 = Unsafe.Add(ref vs, 3);
v0 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v0, mask), maskE);
v1 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v1, mask), maskSlice4Nx16);
v2 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v2, mask), maskE);
v3 = Vector128Utilities.Shuffle(Vector128Utilities.Shuffle(v3, mask), maskSlice4Nx16);
v0 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, mask), maskE);
v1 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, mask), maskSlice4Nx16);
v2 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, mask), maskE);
v3 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, mask), maskSlice4Nx16);
v0 = Vector128Utilities.AlignRight(v1, v0, 4);
v3 = Vector128Utilities.AlignRight(v3, v2, 12);
v0 = Vector128_.AlignRight(v1, v0, 4);
v3 = Vector128_.AlignRight(v3, v2, 12);
v1 = Vector128Utilities.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128Utilities.ShiftRightBytesInVector(v2, 4);
v1 = Vector128_.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128_.ShiftRightBytesInVector(v2, 4);
v1 = Vector128Utilities.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v2, v1, 8);
ref Vector128<byte> vd = ref Unsafe.Add(ref destinationBase, j);
@ -616,58 +621,7 @@ internal static partial class SimdUtils
return Fma.MultiplyAdd(vm1, vm0, va);
}
return Avx.Add(Avx.Multiply(vm0, vm1), va);
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector128{Single}"/>.
/// TODO: Fix. The arguments are in a different order to the FMA intrinsic.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector128<float> MultiplyAdd(
Vector128<float> va,
Vector128<float> vm0,
Vector128<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm1, vm0, va);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.Add(AdvSimd.Multiply(vm0, vm1), va);
}
return Sse.Add(Sse.Multiply(vm0, vm1), va);
}
/// <summary>
/// Performs a multiplication and a subtraction of the <see cref="Vector256{Single}"/>.
/// TODO: Fix. The arguments are in a different order to the FMA intrinsic.
/// </summary>
/// <remarks>ret = (vm0 * vm1) - vs</remarks>
/// <param name="vs">The vector to subtract from the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Vector256<float> MultiplySubtract(
Vector256<float> vs,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplySubtract(vm1, vm0, vs);
}
return Avx.Subtract(Avx.Multiply(vm0, vm1), vs);
return va + (vm0 * vm1);
}
/// <summary>
@ -993,10 +947,10 @@ internal static partial class SimdUtils
Vector512<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector512<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector512<int> w0 = Vector512Utilities.ConvertToInt32RoundToEven(f0);
Vector512<int> w1 = Vector512Utilities.ConvertToInt32RoundToEven(f1);
Vector512<int> w2 = Vector512Utilities.ConvertToInt32RoundToEven(f2);
Vector512<int> w3 = Vector512Utilities.ConvertToInt32RoundToEven(f3);
Vector512<int> w0 = Vector512_.ConvertToInt32RoundToEven(f0);
Vector512<int> w1 = Vector512_.ConvertToInt32RoundToEven(f1);
Vector512<int> w2 = Vector512_.ConvertToInt32RoundToEven(f2);
Vector512<int> w3 = Vector512_.ConvertToInt32RoundToEven(f3);
Vector512<short> u0 = Avx512BW.PackSignedSaturate(w0, w1);
Vector512<short> u1 = Avx512BW.PackSignedSaturate(w2, w3);
@ -1027,10 +981,10 @@ internal static partial class SimdUtils
Vector256<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector256<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector256<int> w0 = Vector256Utilities.ConvertToInt32RoundToEven(f0);
Vector256<int> w1 = Vector256Utilities.ConvertToInt32RoundToEven(f1);
Vector256<int> w2 = Vector256Utilities.ConvertToInt32RoundToEven(f2);
Vector256<int> w3 = Vector256Utilities.ConvertToInt32RoundToEven(f3);
Vector256<int> w0 = Vector256_.ConvertToInt32RoundToEven(f0);
Vector256<int> w1 = Vector256_.ConvertToInt32RoundToEven(f1);
Vector256<int> w2 = Vector256_.ConvertToInt32RoundToEven(f2);
Vector256<int> w3 = Vector256_.ConvertToInt32RoundToEven(f3);
Vector256<short> u0 = Avx2.PackSignedSaturate(w0, w1);
Vector256<short> u1 = Avx2.PackSignedSaturate(w2, w3);
@ -1040,9 +994,9 @@ internal static partial class SimdUtils
Unsafe.Add(ref destinationBase, i) = b;
}
}
else if (Sse2.IsSupported || AdvSimd.IsSupported)
else if (Vector128.IsHardwareAccelerated)
{
// Sse, AdvSimd
// Sse, AdvSimd, etc.
DebugVerifySpanInput(source, destination, Vector128<byte>.Count);
nuint n = destination.Vector128Count<byte>();
@ -1051,6 +1005,8 @@ internal static partial class SimdUtils
ref Vector128<byte> destinationBase = ref Unsafe.As<byte, Vector128<byte>>(ref MemoryMarshal.GetReference(destination));
Vector128<float> scale = Vector128.Create((float)byte.MaxValue);
Vector128<int> min = Vector128<int>.Zero;
Vector128<int> max = Vector128.Create((int)byte.MaxValue);
for (nuint i = 0; i < n; i++)
{
@ -1061,15 +1017,20 @@ internal static partial class SimdUtils
Vector128<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector128<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector128<int> w0 = Vector128Utilities.ConvertToInt32RoundToEven(f0);
Vector128<int> w1 = Vector128Utilities.ConvertToInt32RoundToEven(f1);
Vector128<int> w2 = Vector128Utilities.ConvertToInt32RoundToEven(f2);
Vector128<int> w3 = Vector128Utilities.ConvertToInt32RoundToEven(f3);
Vector128<int> w0 = Vector128_.ConvertToInt32RoundToEven(f0);
Vector128<int> w1 = Vector128_.ConvertToInt32RoundToEven(f1);
Vector128<int> w2 = Vector128_.ConvertToInt32RoundToEven(f2);
Vector128<int> w3 = Vector128_.ConvertToInt32RoundToEven(f3);
w0 = Vector128_.Clamp(w0, min, max);
w1 = Vector128_.Clamp(w1, min, max);
w2 = Vector128_.Clamp(w2, min, max);
w3 = Vector128_.Clamp(w3, min, max);
Vector128<short> u0 = Vector128Utilities.PackSignedSaturate(w0, w1);
Vector128<short> u1 = Vector128Utilities.PackSignedSaturate(w2, w3);
Vector128<ushort> u0 = Vector128.Narrow(w0, w1).AsUInt16();
Vector128<ushort> u1 = Vector128.Narrow(w2, w3).AsUInt16();
Unsafe.Add(ref destinationBase, i) = Vector128Utilities.PackUnsignedSaturate(u0, u1);
Unsafe.Add(ref destinationBase, i) = Vector128.Narrow(u0, u1);
}
}
}

37
src/ImageSharp/Common/Helpers/SimdUtils.cs

@ -1,11 +1,11 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp;
@ -36,30 +36,39 @@ internal static partial class SimdUtils
/// <summary>
/// Rounds all values in 'v' to the nearest integer following <see cref="MidpointRounding.ToEven"/> semantics.
/// Source:
/// <see>
/// <cref>https://github.com/g-truc/glm/blob/master/glm/simd/common.h#L110</cref>
/// </see>
/// </summary>
/// <param name="v">The vector</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector<float> FastRound(this Vector<float> v)
{
if (Avx2.IsSupported)
// .NET9+ has a built-in method for this Vector.Round
if (Avx2.IsSupported && Vector<float>.Count == Vector256<float>.Count)
{
ref Vector256<float> v256 = ref Unsafe.As<Vector<float>, Vector256<float>>(ref v);
Vector256<float> vRound = Avx.RoundToNearestInteger(v256);
return Unsafe.As<Vector256<float>, Vector<float>>(ref vRound);
}
else
if (Sse41.IsSupported && Vector<float>.Count == Vector128<float>.Count)
{
ref Vector128<float> v128 = ref Unsafe.As<Vector<float>, Vector128<float>>(ref v);
Vector128<float> vRound = Sse41.RoundToNearestInteger(v128);
return Unsafe.As<Vector128<float>, Vector<float>>(ref vRound);
}
if (AdvSimd.IsSupported && Vector<float>.Count == Vector128<float>.Count)
{
var magic0 = new Vector<int>(int.MinValue); // 0x80000000
var sgn0 = Vector.AsVectorSingle(magic0);
var and0 = Vector.BitwiseAnd(sgn0, v);
var or0 = Vector.BitwiseOr(and0, new Vector<float>(8388608.0f));
var add0 = Vector.Add(v, or0);
return Vector.Subtract(add0, or0);
ref Vector128<float> v128 = ref Unsafe.As<Vector<float>, Vector128<float>>(ref v);
Vector128<float> vRound = AdvSimd.RoundToNearest(v128);
return Unsafe.As<Vector128<float>, Vector<float>>(ref vRound);
}
// https://github.com/g-truc/glm/blob/master/glm/simd/common.h#L11
Vector<float> sign = v & new Vector<float>(-0F);
Vector<float> val_2p23_f32 = sign | new Vector<float>(8388608F);
val_2p23_f32 = (v + val_2p23_f32) - val_2p23_f32;
return val_2p23_f32 | sign;
}
[Conditional("DEBUG")]

1226
src/ImageSharp/Common/Helpers/Vector128Utilities.cs

File diff suppressed because it is too large

465
src/ImageSharp/Common/Helpers/Vector256Utilities.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
@ -17,26 +16,10 @@ namespace SixLabors.ImageSharp.Common.Helpers;
/// </list>
/// Should only be used if the intrinsics are available.
/// </summary>
internal static class Vector256Utilities
#pragma warning disable SA1649 // File name should match first type name
internal static class Vector256_
#pragma warning restore SA1649 // File name should match first type name
{
/// <summary>
/// Gets a value indicating whether shuffle byte operations are supported.
/// </summary>
public static bool SupportsShuffleFloat
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Avx.IsSupported || Sse.IsSupported;
}
/// <summary>
/// Gets a value indicating whether shuffle byte operations are supported.
/// </summary>
public static bool SupportsShuffleByte
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Avx2.IsSupported;
}
/// <summary>
/// Creates a new vector by selecting values from an input vector using a set of indices.
/// </summary>
@ -44,23 +27,8 @@ internal static class Vector256Utilities
/// <param name="control">The shuffle control byte.</param>
/// <returns>The <see cref="Vector256{Single}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> Shuffle(Vector256<float> vector, [ConstantExpected] byte control)
{
if (Avx.IsSupported)
{
return Avx.Shuffle(vector, vector, control);
}
if (Sse.IsSupported)
{
Vector128<float> lower = vector.GetLower();
Vector128<float> upper = vector.GetUpper();
return Vector256.Create(Sse.Shuffle(lower, lower, control), Sse.Shuffle(upper, upper, control));
}
ThrowUnreachableException();
return default;
}
public static Vector256<float> ShuffleNative(Vector256<float> vector, [ConstantExpected] byte control)
=> Avx.Shuffle(vector, vector, control);
/// <summary>
/// Creates a new vector by selecting values from an input vector using a set of indices.</summary>
@ -71,15 +39,17 @@ internal static class Vector256Utilities
/// </param>
/// <returns>The <see cref="Vector256{Single}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Shuffle(Vector256<byte> vector, Vector256<byte> indices)
public static Vector256<byte> ShufflePerLane(Vector256<byte> vector, Vector256<byte> indices)
{
if (Avx2.IsSupported)
{
return Avx2.Shuffle(vector, indices);
}
ThrowUnreachableException();
return default;
Vector128<byte> indicesLo = indices.GetLower();
Vector128<byte> lower = Vector128_.ShuffleNative(vector.GetLower(), indicesLo);
Vector128<byte> upper = Vector128_.ShuffleNative(vector.GetUpper(), indicesLo);
return Vector256.Create(lower, upper);
}
/// <summary>
@ -96,58 +66,399 @@ internal static class Vector256Utilities
return Avx.ConvertToVector256Int32(vector);
}
if (Sse2.IsSupported)
Vector256<float> sign = vector & Vector256.Create(-0F);
Vector256<float> val_2p23_f32 = sign | Vector256.Create(8388608F);
val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32;
return Vector256.ConvertToInt32(val_2p23_f32 | sign);
}
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// </summary>
/// <param name="vector">The vector</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> RoundToNearestInteger(Vector256<float> vector)
{
if (Avx.IsSupported)
{
Vector128<int> lower = Sse2.ConvertToVector128Int32(vector.GetLower());
Vector128<int> upper = Sse2.ConvertToVector128Int32(vector.GetUpper());
return Vector256.Create(lower, upper);
return Avx.RoundToNearestInteger(vector);
}
Vector256<float> sign = vector & Vector256.Create(-0.0f);
Vector256<float> val_2p23_f32 = sign | Vector256.Create(8388608.0f);
Vector256<float> sign = vector & Vector256.Create(-0F);
Vector256<float> val_2p23_f32 = sign | Vector256.Create(8388608F);
val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32;
return Vector256.ConvertToInt32(val_2p23_f32 | sign);
return val_2p23_f32 | sign;
}
/// <summary>
/// Performs a multiply-add operation on three vectors, where each element of the resulting vector is the
/// product of corresponding elements in <paramref name="a"/> and <paramref name="b"/> added to the
/// corresponding element in <paramref name="c"/>.
/// If the CPU supports FMA (Fused Multiply-Add) instructions, the operation is performed as a single
/// fused operation for better performance and precision.
/// Performs a multiplication and an addition of the <see cref="Vector256{Single}"/>.
/// </summary>
/// <param name="a">The first vector of single-precision floating-point numbers to be multiplied.</param>
/// <param name="b">The second vector of single-precision floating-point numbers to be multiplied.</param>
/// <param name="c">The vector of single-precision floating-point numbers to be added to the product of
/// <paramref name="a"/> and <paramref name="b"/>.</param>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> MultiplyAdd(
Vector256<float> va,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm0, vm1, va);
}
return va + (vm0 * vm1);
}
/// <summary>
/// Performs a multiplication and a subtraction of the <see cref="Vector256{Single}"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) - vs</remarks>
/// <param name="vs">The vector to subtract from the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> MultiplySubtract(
Vector256<float> vs,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplySubtract(vm1, vm0, vs);
}
return (vm0 * vm1) - vs;
}
/// <summary>
/// Multiply packed signed 16-bit integers in <paramref name="left"/> and <paramref name="right"/>, producing
/// intermediate signed 32-bit integers. Horizontally add adjacent pairs of intermediate 32-bit integers, and
/// pack the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed signed 16-bit integers to multiply and add.
/// </param>
/// <param name="right">
/// The second vector containing packed signed 16-bit integers to multiply and add.
/// </param>
/// <returns>
/// A <see cref="Vector256{Single}"/> where each element is the result of multiplying the corresponding elements
/// of <paramref name="a"/> and <paramref name="b"/>, and then adding the corresponding element from <paramref name="c"/>.
/// A vector containing the results of multiplying and adding adjacent pairs of packed signed 16-bit integers
/// </returns>
/// <remarks>
/// If the FMA (Fused Multiply-Add) instruction set is supported by the CPU, the operation is performed using
/// <see cref="Fma.MultiplyAdd(Vector256{float}, Vector256{float}, Vector256{float})"/>. This approach can result
/// in slightly different results compared to performing the multiplication and addition separately due to
/// differences in how floating-point
/// rounding is handled.
/// <para>
/// If FMA is not supported, the operation is performed as a separate multiplication and addition. This might lead
/// to a minor difference in precision compared to the fused operation, particularly in cases where numerical accuracy
/// is critical.
/// </para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> MultiplyAddEstimate(Vector256<float> a, Vector256<float> b, Vector256<float> c)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> MultiplyAddAdjacent(Vector256<short> left, Vector256<short> right)
{
if (Fma.IsSupported)
if (Avx2.IsSupported)
{
return Avx2.MultiplyAddAdjacent(left, right);
}
return Vector256.Create(
Vector128_.MultiplyAddAdjacent(left.GetLower(), right.GetLower()),
Vector128_.MultiplyAddAdjacent(left.GetUpper(), right.GetUpper()));
}
/// <summary>
/// Packs signed 32-bit integers to signed 16-bit integers and saturates.
/// </summary>
/// <param name="left">The left hand source vector.</param>
/// <param name="right">The right hand source vector.</param>
/// <returns>The <see cref="Vector256{UInt16}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> PackUnsignedSaturate(Vector256<int> left, Vector256<int> right)
{
if (Avx2.IsSupported)
{
return Fma.MultiplyAdd(a, b, c);
return Avx2.PackUnsignedSaturate(left, right);
}
return (a * b) + c;
Vector256<int> min = Vector256.Create((int)ushort.MinValue);
Vector256<int> max = Vector256.Create((int)ushort.MaxValue);
Vector256<uint> lefClamped = Clamp(left, min, max).AsUInt32();
Vector256<uint> rightClamped = Clamp(right, min, max).AsUInt32();
return Vector256.Narrow(lefClamped, rightClamped);
}
[DoesNotReturn]
private static void ThrowUnreachableException() => throw new UnreachableException();
/// <summary>
/// Packs signed 32-bit integers to signed 16-bit integers and saturates.
/// </summary>
/// <param name="left">The left hand source vector.</param>
/// <param name="right">The right hand source vector.</param>
/// <returns>The <see cref="Vector256{Int16}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> PackSignedSaturate(Vector256<int> left, Vector256<int> right)
{
if (Avx2.IsSupported)
{
return Avx2.PackSignedSaturate(left, right);
}
Vector256<int> min = Vector256.Create((int)short.MinValue);
Vector256<int> max = Vector256.Create((int)short.MaxValue);
Vector256<int> lefClamped = Clamp(left, min, max);
Vector256<int> rightClamped = Clamp(right, min, max);
return Vector256.Narrow(lefClamped, rightClamped);
}
/// <summary>
/// Packs signed 16-bit integers to signed 8-bit integers and saturates.
/// </summary>
/// <param name="left">The left hand source vector.</param>
/// <param name="right">The right hand source vector.</param>
/// <returns>The <see cref="Vector256{SByte}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<sbyte> PackSignedSaturate(Vector256<short> left, Vector256<short> right)
{
if (Avx2.IsSupported)
{
return Avx2.PackSignedSaturate(left, right);
}
Vector256<short> min = Vector256.Create((short)sbyte.MinValue);
Vector256<short> max = Vector256.Create((short)sbyte.MaxValue);
Vector256<short> lefClamped = Clamp(left, min, max);
Vector256<short> rightClamped = Clamp(right, min, max);
return Vector256.Narrow(lefClamped, rightClamped);
}
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
/// <typeparam name="T">The type of the elements in the vector.</typeparam>
/// <param name="value">The vector to restrict.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The restricted <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Clamp<T>(Vector256<T> value, Vector256<T> min, Vector256<T> max)
=> Vector256.Min(Vector256.Max(value, min), max);
/// <summary>
/// Widens a <see cref="Vector128{Int16}"/> to a <see cref="Vector256{Int32}"/>.
/// </summary>
/// <param name="value">The vector to widen.</param>
/// <returns>The widened <see cref="Vector256{Int32}"/>.</returns>
public static Vector256<int> Widen(Vector128<short> value)
{
if (Avx2.IsSupported)
{
return Avx2.ConvertToVector256Int32(value);
}
return Vector256.WidenLower(value.ToVector256());
}
/// <summary>
/// Multiply the packed 16-bit integers in <paramref name="left"/> and <paramref name="right"/>, producing
/// intermediate 32-bit integers, and store the low 16 bits of the intermediate integers in the result.
/// </summary>
/// <param name="left">
/// The first vector containing packed 16-bit integers to multiply.
/// </param>
/// <param name="right">
/// The second vector containing packed 16-bit integers to multiply.
/// </param>
/// <returns>
/// A vector containing the low 16 bits of the products of the packed 16-bit integers
/// from <paramref name="left"/> and <paramref name="right"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> MultiplyLow(Vector256<short> left, Vector256<short> right)
{
if (Avx2.IsSupported)
{
return Avx2.MultiplyLow(left, right);
}
// Widen each half of the short vectors into two int vectors
(Vector256<int> leftLower, Vector256<int> leftUpper) = Vector256.Widen(left);
(Vector256<int> rightLower, Vector256<int> rightUpper) = Vector256.Widen(right);
// Elementwise multiply: each int lane now holds the full 32-bit product
Vector256<int> prodLo = leftLower * rightLower;
Vector256<int> prodHi = leftUpper * rightUpper;
// Narrow the two int vectors back into one short vector
return Vector256.Narrow(prodLo, prodHi);
}
/// <summary>
/// Multiply the packed 16-bit integers in <paramref name="left"/> and <paramref name="right"/>, producing
/// intermediate 32-bit integers, and store the high 16 bits of the intermediate integers in the result.
/// </summary>
/// <param name="left">
/// The first vector containing packed 16-bit integers to multiply.
/// </param>
/// <param name="right">
/// The second vector containing packed 16-bit integers to multiply.
/// </param>
/// <returns>
/// A vector containing the high 16 bits of the products of the packed 16-bit integers
/// from <paramref name="left"/> and <paramref name="right"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> MultiplyHigh(Vector256<short> left, Vector256<short> right)
{
if (Avx2.IsSupported)
{
return Avx2.MultiplyHigh(left, right);
}
// Widen each half of the short vectors into two int vectors
(Vector256<int> leftLower, Vector256<int> leftUpper) = Vector256.Widen(left);
(Vector256<int> rightLower, Vector256<int> rightUpper) = Vector256.Widen(right);
// Elementwise multiply: each int lane now holds the full 32-bit product
Vector256<int> prodLo = leftLower * rightLower;
Vector256<int> prodHi = leftUpper * rightUpper;
// Arithmetic shift right by 16 bits to extract the high word
prodLo >>= 16;
prodHi >>= 16;
// Narrow the two int vectors back into one short vector
return Vector256.Narrow(prodLo, prodHi);
}
/// <summary>
/// Unpack and interleave 32-bit integers from the low half of <paramref name="left"/> and <paramref name="right"/>
/// and store the results in the result.
/// </summary>
/// <param name="left">
/// The first vector containing packed 32-bit integers to unpack from the low half.
/// </param>
/// <param name="right">
/// The second vector containing packed 32-bit integers to unpack from the low half.
/// </param>
/// <returns>
/// A vector containing the unpacked and interleaved 32-bit integers from the low
/// halves of <paramref name="left"/> and <paramref name="right"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> UnpackLow(Vector256<int> left, Vector256<int> right)
{
if (Avx2.IsSupported)
{
return Avx2.UnpackLow(left, right);
}
Vector128<int> lo = Vector128_.UnpackLow(left.GetLower(), right.GetLower());
Vector128<int> hi = Vector128_.UnpackLow(left.GetUpper(), right.GetUpper());
return Vector256.Create(lo, hi);
}
/// <summary>
/// Unpack and interleave 8-bit integers from the high half of <paramref name="left"/> and <paramref name="right"/>
/// and store the results in the result.
/// </summary>
/// <param name="left">
/// The first vector containing packed 8-bit integers to unpack from the high half.
/// </param>
/// <param name="right">
/// The second vector containing packed 8-bit integers to unpack from the high half.
/// </param>
/// <returns>
/// A vector containing the unpacked and interleaved 8-bit integers from the high
/// halves of <paramref name="left"/> and <paramref name="right"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> UnpackHigh(Vector256<byte> left, Vector256<byte> right)
{
if (Avx2.IsSupported)
{
return Avx2.UnpackHigh(left, right);
}
Vector128<byte> lo = Vector128_.UnpackHigh(left.GetLower(), right.GetLower());
Vector128<byte> hi = Vector128_.UnpackHigh(left.GetUpper(), right.GetUpper());
return Vector256.Create(lo, hi);
}
/// <summary>
/// Unpack and interleave 8-bit integers from the low half of <paramref name="left"/> and <paramref name="right"/>
/// and store the results in the result.
/// </summary>
/// <param name="left">
/// The first vector containing packed 8-bit integers to unpack from the low half.
/// </param>
/// <param name="right">
/// The second vector containing packed 8-bit integers to unpack from the low half.
/// </param>
/// <returns>
/// A vector containing the unpacked and interleaved 8-bit integers from the low
/// halves of <paramref name="left"/> and <paramref name="right"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> UnpackLow(Vector256<byte> left, Vector256<byte> right)
{
if (Avx2.IsSupported)
{
return Avx2.UnpackLow(left, right);
}
Vector128<byte> lo = Vector128_.UnpackLow(left.GetLower(), right.GetLower());
Vector128<byte> hi = Vector128_.UnpackLow(left.GetUpper(), right.GetUpper());
return Vector256.Create(lo, hi);
}
/// <summary>
/// Subtract packed signed 16-bit integers in <paramref name="right"/> from packed signed 16-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed signed 16-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed signed 16-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed unsigned 16-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> SubtractSaturate(Vector256<short> left, Vector256<short> right)
{
if (Avx2.IsSupported)
{
return Avx2.SubtractSaturate(left, right);
}
return Vector256.Create(
Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()),
Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
}
/// <summary>
/// Subtract packed unsigned 8-bit integers in <paramref name="right"/> from packed unsigned 8-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 8-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 8-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed unsigned 8-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> SubtractSaturate(Vector256<byte> left, Vector256<byte> right)
{
if (Avx2.IsSupported)
{
return Avx2.SubtractSaturate(left, right);
}
return Vector256.Create(
Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()),
Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
}
}

133
src/ImageSharp/Common/Helpers/Vector512Utilities.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
@ -18,26 +17,10 @@ namespace SixLabors.ImageSharp.Common.Helpers;
/// </list>
/// Should only be used if the intrinsics are available.
/// </summary>
internal static class Vector512Utilities
#pragma warning disable SA1649 // File name should match first type name
internal static class Vector512_
#pragma warning restore SA1649 // File name should match first type name
{
/// <summary>
/// Gets a value indicating whether shuffle float operations are supported.
/// </summary>
public static bool SupportsShuffleFloat
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Avx512F.IsSupported || Avx.IsSupported;
}
/// <summary>
/// Gets a value indicating whether shuffle byte operations are supported.
/// </summary>
public static bool SupportsShuffleByte
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Avx512BW.IsSupported;
}
/// <summary>
/// Creates a new vector by selecting values from an input vector using the control.
/// </summary>
@ -45,23 +28,8 @@ internal static class Vector512Utilities
/// <param name="control">The shuffle control byte.</param>
/// <returns>The <see cref="Vector512{Single}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> Shuffle(Vector512<float> vector, [ConstantExpected] byte control)
{
if (Avx512F.IsSupported)
{
return Avx512F.Shuffle(vector, vector, control);
}
if (Avx.IsSupported)
{
Vector256<float> lower = vector.GetLower();
Vector256<float> upper = vector.GetUpper();
return Vector512.Create(Avx.Shuffle(lower, lower, control), Avx.Shuffle(upper, upper, control));
}
ThrowUnreachableException();
return default;
}
public static Vector512<float> ShuffleNative(Vector512<float> vector, [ConstantExpected] byte control)
=> Avx512F.Shuffle(vector, vector, control);
/// <summary>
/// Creates a new vector by selecting values from an input vector using a set of indices.
@ -72,15 +40,14 @@ internal static class Vector512Utilities
/// </param>
/// <returns>The <see cref="Vector512{Byte}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Shuffle(Vector512<byte> vector, Vector512<byte> indices)
public static Vector512<byte> ShuffleNative(Vector512<byte> vector, Vector512<byte> indices)
{
if (Avx512BW.IsSupported)
{
return Avx512BW.Shuffle(vector, indices);
}
ThrowUnreachableException();
return default;
return Vector512.Shuffle(vector, indices);
}
/// <summary>
@ -91,63 +58,45 @@ internal static class Vector512Utilities
/// <returns>The <see cref="Vector128{Int32}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<int> ConvertToInt32RoundToEven(Vector512<float> vector)
{
if (Avx512F.IsSupported)
{
return Avx512F.ConvertToVector512Int32(vector);
}
=> Avx512F.ConvertToVector512Int32(vector);
if (Avx.IsSupported)
{
Vector256<int> lower = Avx.ConvertToVector256Int32(vector.GetLower());
Vector256<int> upper = Avx.ConvertToVector256Int32(vector.GetUpper());
return Vector512.Create(lower, upper);
}
Vector512<float> sign = vector & Vector512.Create(-0.0f);
Vector512<float> val_2p23_f32 = sign | Vector512.Create(8388608.0f);
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// </summary>
/// <param name="vector">The vector</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> RoundToNearestInteger(Vector512<float> vector)
val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32;
return Vector512.ConvertToInt32(val_2p23_f32 | sign);
}
// imm8 = 0b1000:
// imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers)
// imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions)
=> Avx512F.RoundScale(vector, 0b0000_1000);
/// <summary>
/// Performs a multiply-add operation on three vectors, where each element of the resulting vector is the
/// product of corresponding elements in <paramref name="a"/> and <paramref name="b"/> added to the
/// corresponding element in <paramref name="c"/>.
/// If the CPU supports FMA (Fused Multiply-Add) instructions, the operation is performed as a single
/// fused operation for better performance and precision.
/// Performs a multiplication and an addition of the <see cref="Vector512{Single}"/>.
/// </summary>
/// <param name="a">The first vector of single-precision floating-point numbers to be multiplied.</param>
/// <param name="b">The second vector of single-precision floating-point numbers to be multiplied.</param>
/// <param name="c">The vector of single-precision floating-point numbers to be added to the product of
/// <paramref name="a"/> and <paramref name="b"/>.</param>
/// <returns>
/// A <see cref="Vector512{Single}"/> where each element is the result of multiplying the corresponding elements
/// of <paramref name="a"/> and <paramref name="b"/>, and then adding the corresponding element from <paramref name="c"/>.
/// </returns>
/// <remarks>
/// If the FMA (Fused Multiply-Add) instruction set is supported by the CPU, the operation is performed using
/// <see cref="Fma.MultiplyAdd(Vector256{float}, Vector256{float}, Vector256{float})"/> against the upper and lower
/// buts. This approach can result in slightly different results compared to performing the multiplication and
/// addition separately due to differences in how floating-point rounding is handled.
/// <para>
/// If FMA is not supported, the operation is performed as a separate multiplication and addition. This might lead
/// to a minor difference in precision compared to the fused operation, particularly in cases where numerical accuracy
/// is critical.
/// </para>
/// </remarks>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> MultiplyAddEstimate(Vector512<float> a, Vector512<float> b, Vector512<float> c)
{
if (Avx512F.IsSupported)
{
return Avx512F.FusedMultiplyAdd(a, b, c);
}
public static Vector512<float> MultiplyAdd(
Vector512<float> va,
Vector512<float> vm0,
Vector512<float> vm1)
=> Avx512F.FusedMultiplyAdd(vm0, vm1, va);
return (a + b) * c;
}
[DoesNotReturn]
private static void ThrowUnreachableException() => throw new UnreachableException();
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
/// <typeparam name="T">The type of the elements in the vector.</typeparam>
/// <param name="value">The vector to restrict.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The restricted <see cref="Vector512{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Clamp<T>(Vector512<T> value, Vector512<T> min, Vector512<T> max)
=> Vector512.Min(Vector512.Max(value, min), max);
}

38
src/ImageSharp/Common/InlineArray.cs

@ -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;
}

38
src/ImageSharp/Common/InlineArray.tt

@ -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 = new int[] {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;
}
<#+
}
}
#>

4
src/ImageSharp/Formats/AlphaAwareImageEncoder.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
@ -10,6 +12,8 @@ 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; }
}

4
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -1597,8 +1597,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
if (palette.Length > 0)
{
Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf<Bgr24>()];
ReadOnlySpan<Bgr24> rgbTable = MemoryMarshal.Cast<byte, Bgr24>(palette);
Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf<Bgra32>()];
ReadOnlySpan<Bgra32> rgbTable = MemoryMarshal.Cast<byte, Bgra32>(palette);
Color.FromPixel(rgbTable, colorTable);
this.bmpMetadata.ColorTable = colorTable;
}

7
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -362,10 +362,13 @@ internal sealed class BmpEncoderCore
ImageFrame<TPixel>? clonedFrame = null;
try
{
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
// No need to clone when quantizing. The quantizer will do it for us.
// TODO: We should really try to avoid the clone entirely.
int bpp = this.bitsPerPixel != null ? (int)this.bitsPerPixel : 32;
if (bpp > 8 && EncodingUtilities.ShouldReplaceTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
EncodingUtilities.ReplaceTransparentPixels(clonedFrame);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;

3
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -158,6 +158,5 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
}

26
src/ImageSharp/Formats/ColorProfileHandling.cs

@ -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
}

3
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -104,7 +104,6 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
Compression = compression,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
@ -113,7 +112,6 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
@ -126,6 +124,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>

9
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -71,8 +71,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
return new CurMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
Compression = compression
};
}
@ -145,15 +144,13 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

45
src/ImageSharp/Formats/DecoderOptions.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -60,5 +62,48 @@ public sealed class DecoderOptions
/// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical;
/// <summary>
/// Gets a value that controls how ICC profiles are handled during decode.
/// </summary>
public ColorProfileHandling ColorProfileHandling { get; init; }
internal void SetConfiguration(Configuration configuration) => this.configuration = configuration;
internal bool TryGetIccProfileForColorConversion(IccProfile? profile, [NotNullWhen(true)] out IccProfile? value)
{
value = null;
if (profile is null)
{
return false;
}
if (IccProfileHeader.IsLikelySrgb(profile.Header))
{
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return false;
}
value = profile;
return true;
}
internal bool CanRemoveIccProfile(IccProfile? profile)
{
if (profile is null)
{
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Compact && IccProfileHeader.IsLikelySrgb(profile.Header))
{
return true;
}
return this.ColorProfileHandling == ColorProfileHandling.Convert;
}
}

138
src/ImageSharp/Formats/EncodingUtilities.cs

@ -3,6 +3,8 @@
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;
@ -14,62 +16,132 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary>
internal static class EncodingUtilities
{
public static bool ShouldClearTransparentPixels<TPixel>(TransparentColorMode mode)
/// <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>
=> mode == TransparentColorMode.Clear &&
TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;
=> ReplaceTransparentPixels(frame.Configuration, frame.PixelBuffer);
/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
/// <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> buffer = clone.PixelBuffer.GetRegion();
ClearTransparentPixels(clone.Configuration, ref buffer, color);
Buffer2DRegion<TPixel> region = buffer.GetRegion();
ReplaceTransparentPixels(configuration, in region);
}
/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// 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="clone">The cloned <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(
/// <param name="region">The <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
public static void ReplaceTransparentPixels<TPixel>(
Configuration configuration,
ref Buffer2DRegion<TPixel> clone,
Color color)
in Buffer2DRegion<TPixel> region)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(clone.Width);
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(region.Width);
Span<Vector4> vectorsSpan = vectors.GetSpan();
Vector4 replacement = color.ToScaledVector4();
for (int y = 0; y < clone.Height; y++)
for (int y = 0; y < region.Height; y++)
{
Span<TPixel> span = clone.DangerousGetRowSpan(y);
Span<TPixel> span = region.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
ClearTransparentPixelRow(vectorsSpan, replacement);
ReplaceTransparentPixels(vectorsSpan);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
}
}
private static void ClearTransparentPixelRow(
Span<Vector4> vectorsSpan,
Vector4 replacement)
/// <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 (Vector128.IsHardwareAccelerated)
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)
{
Vector128<float> replacement128 = replacement.AsVector128();
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));
for (int i = 0; i < vectorsSpan.Length; i++)
// 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 vectorsSpan[i];
ref Vector4 v = ref source[i];
Vector128<float> v128 = v.AsVector128();
// Do `vector == 0`
@ -80,16 +152,16 @@ internal static class EncodingUtilities
// Use the mask to select the replacement vector
// (replacement & mask) | (v128 & ~mask)
v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4();
v = Vector128.ConditionalSelect(mask, Vector128<float>.Zero, v128).AsVector4();
}
}
else
{
for (int i = 0; i < vectorsSpan.Length; i++)
for (int i = 0; i < source.Length; i++)
{
if (vectorsSpan[i].W == 0F)
if (source[i].W == 0F)
{
vectorsSpan[i] = replacement;
source[i] = Vector4.Zero;
}
}
}

5
src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs

@ -15,11 +15,6 @@ public class FormatConnectingFrameMetadata
/// </summary>
public PixelTypeInfo? PixelTypeInfo { get; init; }
/// <summary>
/// Gets the frame color table if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; }
/// <summary>
/// Gets the frame color table mode.
/// </summary>

5
src/ImageSharp/Formats/FormatConnectingMetadata.cs

@ -28,11 +28,6 @@ public class FormatConnectingMetadata
/// </summary>
public PixelTypeInfo PixelTypeInfo { get; init; }
/// <summary>
/// Gets the shared color table if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; }
/// <summary>
/// Gets the shared color table mode.
/// </summary>

217
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -89,6 +89,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// </summary>
private GifMetadata? gifMetadata;
/// <summary>
/// The background color index.
/// </summary>
private byte backgroundColorIndex;
/// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
/// </summary>
@ -108,6 +113,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
uint frameCount = 0;
Image<TPixel>? image = null;
ImageFrame<TPixel>? previousFrame = null;
FrameDisposalMode? previousDisposalMode = null;
bool globalColorTableUsed = false;
Color backgroundColor = Color.Transparent;
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@ -123,7 +132,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
this.ReadFrame(stream, ref image, ref previousFrame);
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMode, ref backgroundColor);
// Reset per-frame state.
this.imageDescriptor = default;
@ -158,6 +167,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
}
// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = FrameColorTableMode.Local;
}
}
finally
{
@ -179,6 +195,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
uint frameCount = 0;
ImageFrameMetadata? previousFrame = null;
List<ImageFrameMetadata> framesMetadata = [];
bool globalColorTableUsed = false;
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@ -194,7 +212,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
globalColorTableUsed |= this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
// Reset per-frame state.
this.imageDescriptor = default;
@ -229,6 +247,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
}
// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = FrameColorTableMode.Local;
}
}
finally
{
@ -416,26 +441,29 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame)
/// <param name="previousDisposalMode">The previous frame disposal mode.</param>
/// <param name="backgroundColor">The background color.</param>
/// <returns>Whether the frame has a global color table.</returns>
private bool ReadFrame<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref FrameDisposalMode? previousDisposalMode,
ref Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ReadImageDescriptor(stream);
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
Span<byte> rawColorTable = default;
if (hasLocalColorTable)
{
// Read and store the local color table. We allocate the maximum possible size and slice to match.
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
Span<byte> rawColorTable = default;
if (hasLocalColorTable)
{
rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
rawColorTable = this.currentLocalColorTable!.GetSpan()[..length];
}
else if (this.globalColorTable != null)
{
@ -443,10 +471,52 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);
// First frame
if (image is null)
{
if (this.backgroundColorIndex < colorTable.Length)
{
backgroundColor = Color.FromPixel(colorTable[this.backgroundColorIndex]);
}
else
{
backgroundColor = Color.Transparent;
}
if (this.graphicsControlExtension.TransparencyFlag)
{
backgroundColor = backgroundColor.WithAlpha(0);
}
}
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMode, colorTable, backgroundColor.ToPixel<TPixel>());
// Update from newly decoded frame.
if (this.graphicsControlExtension.DisposalMethod != FrameDisposalMode.RestoreToPrevious)
{
if (this.backgroundColorIndex < colorTable.Length)
{
backgroundColor = Color.FromPixel(colorTable[this.backgroundColorIndex]);
}
else
{
backgroundColor = Color.Transparent;
}
// TODO: I don't understand why this is always set to alpha of zero.
// This should be dependent on the transparency flag of the graphics
// control extension. ImageMagick does the same.
// if (this.graphicsControlExtension.TransparencyFlag)
{
backgroundColor = backgroundColor.WithAlpha(0);
}
}
// Skip any remaining blocks
SkipBlock(stream);
return !hasLocalColorTable;
}
/// <summary>
@ -456,57 +526,73 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="previousDisposalMode">The previous frame disposal mode.</param>
/// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
/// <param name="backgroundPixel">The background color pixel.</param>
private void ReadFrameColors<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref FrameDisposalMode? previousDisposalMode,
ReadOnlySpan<Rgb24> colorTable,
in GifImageDescriptor descriptor)
TPixel backgroundPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
GifImageDescriptor descriptor = this.imageDescriptor;
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
ImageFrame<TPixel> currentFrame;
ImageFrame<TPixel>? restoreFrame = null;
ImageFrame<TPixel>? prevFrame = null;
ImageFrame<TPixel>? currentFrame = null;
ImageFrame<TPixel> imageFrame;
if (previousFrame is null && previousDisposalMode is null)
{
image = transFlag
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata);
if (previousFrame is null)
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
currentFrame = image.Frames.RootFrame;
}
else
{
if (!transFlag)
if (previousFrame != null)
{
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
currentFrame = image!.Frames.AddFrame(previousFrame);
}
else
{
// This initializes the image to become fully transparent because the alpha channel is zero.
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
}
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
this.SetFrameMetadata(currentFrame.Metadata);
imageFrame = image.Frames.RootFrame;
}
else
{
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
prevFrame = previousFrame;
restoreFrame = previousFrame;
}
// We create a clone of the frame and add it.
// We will overpaint the difference of pixels on the current frame to create a complete image.
// This ensures that we have enough pixel data to process without distortion. #2450
currentFrame = image!.Frames.AddFrame(previousFrame);
if (previousDisposalMode == FrameDisposalMode.RestoreToBackground)
{
this.RestoreToBackground(currentFrame, backgroundPixel, transFlag);
}
}
this.SetFrameMetadata(currentFrame.Metadata);
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
previousFrame = restoreFrame;
}
else
{
previousFrame = currentFrame;
}
imageFrame = currentFrame;
previousDisposalMode = disposalMethod;
this.RestoreToBackground(imageFrame);
if (disposalMethod == FrameDisposalMode.RestoreToBackground)
{
this.restoreArea = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
}
if (colorTable.Length == 0)
@ -528,7 +614,6 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// However we have images that exceed this that can be decoded by other libraries. #1530
using IMemoryOwner<byte> indicesRowOwner = this.memoryAllocator.Allocate<byte>(descriptor.Width);
Span<byte> indicesRow = indicesRowOwner.Memory.Span;
ref byte indicesRowRef = ref MemoryMarshal.GetReference(indicesRow);
int minCodeSize = stream.ReadByte();
if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
@ -572,22 +657,28 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
lzwDecoder.DecodePixelRow(indicesRow);
ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
// #403 The left + width value can be larger than the image width
int maxX = Math.Min(descriptorRight, imageWidth);
Span<TPixel> row = currentFrame.PixelBuffer.DangerousGetRowSpan(writeY);
// Take the descriptorLeft..maxX slice of the row, so the loop can be simplified.
row = row[descriptorLeft..maxX];
if (!transFlag)
{
// #403 The left + width value can be larger than the image width
for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
for (int x = 0; x < row.Length; x++)
{
int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)), 0, colorTableMaxIdx);
Unsafe.Add(ref rowRef, (uint)x) = TPixel.FromRgb24(colorTable[index]);
int index = indicesRow[x];
index = Numerics.Clamp(index, 0, colorTableMaxIdx);
row[x] = TPixel.FromRgb24(colorTable[index]);
}
}
else
{
for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
for (int x = 0; x < row.Length; x++)
{
int index = Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft));
int index = indicesRow[x];
// Treat any out of bounds values as transparent.
if (index > colorTableMaxIdx || index == transIndex)
@ -595,24 +686,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
continue;
}
Unsafe.Add(ref rowRef, (uint)x) = TPixel.FromRgb24(colorTable[index]);
row[x] = TPixel.FromRgb24(colorTable[index]);
}
}
}
}
if (prevFrame != null)
{
previousFrame = prevFrame;
return;
}
previousFrame = currentFrame ?? image.Frames.RootFrame;
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToBackground)
{
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
}
}
/// <summary>
@ -621,7 +699,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="frameMetadata">The collection of frame metadata.</param>
/// <param name="previousFrame">The previous frame metadata.</param>
private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame)
/// <returns>Whether the frame has a global color table.</returns>
private bool ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame)
{
this.ReadImageDescriptor(stream);
@ -633,6 +712,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
else
{
this.currentLocalColorTable = null;
this.currentLocalColorTableSize = 0;
}
// Skip the frame indices. Pixels length + mincode size.
// The gif format does not tell us the length of the compressed data beforehand.
@ -650,6 +734,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// Skip any remaining blocks
SkipBlock(stream);
return !this.imageDescriptor.LocalColorTableFlag;
}
/// <summary>
@ -657,7 +743,9 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The frame.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
/// <param name="background">The background color.</param>
/// <param name="transparent">Whether the background is transparent.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame, TPixel background, bool transparent)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.restoreArea is null)
@ -667,7 +755,14 @@ internal sealed class GifDecoderCore : ImageDecoderCore
Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();
if (transparent)
{
pixelRegion.Clear();
}
else
{
pixelRegion.Fill(background);
}
this.restoreArea = null;
}
@ -776,7 +871,9 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
}
this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
this.backgroundColorIndex = index;
this.gifMetadata.BackgroundColorIndex = index;
}
private unsafe struct ScratchBuffer

238
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -19,6 +18,8 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// </summary>
internal sealed class GifEncoderCore
{
private readonly GifEncoder encoder;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
@ -34,16 +35,6 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
private IQuantizer? quantizer;
/// <summary>
/// Whether the quantizer was supplied via options.
/// </summary>
private readonly bool hasQuantizer;
/// <summary>
/// The color table mode: Global or local.
/// </summary>
@ -67,6 +58,9 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// The transparent color mode.
/// </summary>
private readonly TransparentColorMode transparentColorMode;
/// <summary>
@ -78,9 +72,8 @@ internal sealed class GifEncoderCore
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer;
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
@ -104,70 +97,76 @@ internal sealed class GifEncoderCore
GifMetadata gifMetadata = image.Metadata.CloneGifMetadata();
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global;
// Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null;
bool useGlobalTableForFirstFrame = useGlobalTable;
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
// correct value is set for the background index when quantizing.
GifFrameMetadata frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
if (frameMetadata.ColorTableMode == FrameColorTableMode.Local)
{
useGlobalTableForFirstFrame = false;
}
// Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null;
IQuantizer? globalQuantizer = this.encoder.Quantizer;
TransparentColorMode mode = this.transparentColorMode;
if (this.quantizer is null)
// Create a new quantizer options instance augmenting the transparent color mode to match the encoder.
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new()).DeepClone(o => o.TransparentColorMode = mode);
if (globalQuantizer is null)
{
// Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
// We avoid dithering by default to preserve the original colors.
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256)
{
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
// We avoid dithering by default to preserve the original colors.
globalQuantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, options.DeepClone(o => o.Dither = null));
}
else
{
this.quantizer = KnownQuantizers.Octree;
globalQuantizer = new OctreeQuantizer(options);
}
}
else
{
this.quantizer = KnownQuantizers.Octree;
globalQuantizer = new OctreeQuantizer(options);
}
}
// Quantize the first frame. Checking to see whether we can clear the transparent pixels
// to allow for a smaller color palette and encoded result.
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
ImageFrame<TPixel>? clonedFrame = null;
Configuration configuration = this.configuration;
TransparentColorMode mode = this.transparentColorMode;
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
clonedFrame = image.Frames.RootFrame.Clone();
GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata();
Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
EncodingUtilities.ClearTransparentPixels(clonedFrame, background);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
// Quantize the first frame.
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame;
if (useGlobalTableForFirstFrame)
{
using IQuantizer<TPixel> firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
if (useGlobalTable)
{
frameQuantizer.BuildPalette(configuration, mode, strategy, image);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
firstFrameQuantizer.BuildPalette(strategy, image);
}
else
{
frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
firstFrameQuantizer.BuildPalette(strategy, encodingFrame);
}
clonedFrame?.Dispose();
quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
}
else
{
quantized = this.QuantizeFrameAndUpdateMetadata(
encodingFrame,
globalQuantizer,
default,
encodingFrame.Bounds,
frameMetadata,
true,
false,
frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1,
Color.Transparent);
}
// Write the header.
@ -181,6 +180,7 @@ internal sealed class GifEncoderCore
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}
// TODO: We should be checking the metadata here also I think?
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
{
backgroundIndex = derivedTransparencyIndex >= 0
@ -216,13 +216,18 @@ internal sealed class GifEncoderCore
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
if (image.Frames.Count > 1)
{
using PaletteQuantizer<TPixel> globalFrameQuantizer = new(this.configuration, globalQuantizer.Options, quantized.Palette.ToArray());
this.EncodeAdditionalFrames(
stream,
image,
globalQuantizer,
globalFrameQuantizer,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
}
}
finally
{
@ -248,69 +253,43 @@ internal sealed class GifEncoderCore
private void EncodeAdditionalFrames<TPixel>(
Stream stream,
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
{
return;
}
PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false;
// Store the first frame as a reference for de-duplication comparison.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
try
for (int i = 1; i < image.Frames.Count; i++)
{
for (int i = 1; i < image.Frames.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
cancellationToken.ThrowIfCancellationRequested();
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
}
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
globalQuantizer,
globalFrameQuantizer,
useLocal,
gifMetadata,
previousDisposalMode);
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
}
finally
{
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
}
@ -346,9 +325,10 @@ internal sealed class GifEncoderCore
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> encodingFrame,
IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
bool useLocal,
GifFrameMetadata metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -375,19 +355,16 @@ internal sealed class GifEncoderCore
background,
true);
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
}
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
using IndexedImageFrame<TPixel> quantized = this.QuantizeFrameAndUpdateMetadata(
encodingFrame,
globalQuantizer,
globalFrameQuantizer,
bounds,
metadata,
useLocal,
globalPaletteQuantizer,
difference,
transparencyIndex);
transparencyIndex,
background);
this.WriteGraphicalControlExtension(metadata, stream);
@ -403,14 +380,16 @@ internal sealed class GifEncoderCore
this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
}
private IndexedImageFrame<TPixel> QuantizeAdditionalFrameAndUpdateMetadata<TPixel>(
private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>(
ImageFrame<TPixel> encodingFrame,
IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
Rectangle bounds,
GifFrameMetadata metadata,
bool useLocal,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
bool hasDuplicates,
int transparencyIndex)
int transparencyIndex,
Color transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> quantized;
@ -434,15 +413,19 @@ internal sealed class GifEncoderCore
transparencyIndex = palette.Length;
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
QuantizerOptions options = globalQuantizer.Options.DeepClone(o =>
{
o.MaxColors = palette.Length;
o.Dither = null;
});
PaletteQuantizer quantizer = new(palette, options, transparencyIndex, transparentColor);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
else
{
// We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
using IQuantizer<TPixel> frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
// The transparency index derived by the quantizer will differ from the index
@ -454,7 +437,12 @@ internal sealed class GifEncoderCore
else
{
// Just use the local palette.
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
QuantizerOptions paletteOptions = globalQuantizer.Options.DeepClone(o =>
{
o.MaxColors = palette.Length;
o.Dither = null;
});
PaletteQuantizer quantizer = new(palette, paletteOptions, transparencyIndex, transparentColor);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
@ -462,8 +450,7 @@ internal sealed class GifEncoderCore
else
{
// We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
using IQuantizer<TPixel> frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
// The transparency index derived by the quantizer might differ from the index
@ -486,18 +473,19 @@ internal sealed class GifEncoderCore
else
{
// Quantize the image using the global palette.
// Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
// Individual frames, though using the shared palette, can use a different transparent index
// to represent transparency.
// A difference was captured but the metadata does not have transparency.
if (hasDuplicates && !metadata.HasTransparency)
{
metadata.HasTransparency = true;
transparencyIndex = globalPaletteQuantizer.Palette.Length;
transparencyIndex = globalFrameQuantizer.Palette.Length;
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds);
globalFrameQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel<TPixel>());
quantized = globalFrameQuantizer.QuantizeFrame(encodingFrame, bounds);
}
return quantized;

29
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -77,34 +76,12 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
/// <inheritdoc />
public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
int index = -1;
const float background = 1f;
if (metadata.ColorTable.HasValue)
=> new()
{
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++)
{
Vector4 vector = colorTable[i].ToScaledVector4();
if (vector.W < background)
{
index = i;
}
}
}
bool hasTransparency = index >= 0;
return new()
{
LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMode = metadata.DisposalMode,
HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
};
}
/// <inheritdoc />
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
@ -118,7 +95,6 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
return new()
{
ColorTable = this.LocalColorTable,
ColorTableMode = this.ColorTableMode,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10),
DisposalMode = this.DisposalMode,
@ -129,8 +105,7 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.LocalColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

41
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -71,37 +71,19 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
/// <inheritdoc/>
public static GifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
int index = 0;
Color background = metadata.BackgroundColor;
if (metadata.ColorTable.HasValue)
=> new()
{
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++)
{
if (background != colorTable[i])
{
continue;
}
index = i;
break;
}
}
return new()
{
GlobalColorTable = metadata.ColorTable,
// Do not copy the color table or bit depth.
// This will lead to a mismatch when the image is comprised of frames
// extracted individually from a multi-frame image.
ColorTableMode = metadata.ColorTableMode,
RepeatCount = metadata.RepeatCount,
BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255),
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = this.GlobalColorTable.HasValue
int bpp = this.ColorTableMode == FrameColorTableMode.Global && this.GlobalColorTable.HasValue
? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8)
: 8;
@ -114,27 +96,18 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
{
Color color = this.GlobalColorTable.HasValue && this.GlobalColorTable.Value.Span.Length > this.BackgroundColorIndex
? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex]
: Color.Transparent;
return new()
=> new()
{
AnimateRootFrame = true,
BackgroundColor = color,
ColorTable = this.GlobalColorTable,
ColorTableMode = this.ColorTableMode,
PixelTypeInfo = this.GetPixelTypeInfo(),
RepeatCount = this.RepeatCount,
};
}
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.GlobalColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

325
src/ImageSharp/Formats/Gif/LzwDecoder.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@ -37,22 +36,22 @@ internal sealed class LzwDecoder : IDisposable
/// <summary>
/// The prefix buffer.
/// </summary>
private readonly IMemoryOwner<int> prefix;
private readonly IMemoryOwner<int> prefixOwner;
/// <summary>
/// The suffix buffer.
/// </summary>
private readonly IMemoryOwner<int> suffix;
private readonly IMemoryOwner<int> suffixOwner;
/// <summary>
/// The scratch buffer for reading data blocks.
/// </summary>
private readonly IMemoryOwner<byte> scratchBuffer;
private readonly IMemoryOwner<byte> bufferOwner;
/// <summary>
/// The pixel stack buffer.
/// </summary>
private readonly IMemoryOwner<int> pixelStack;
private readonly IMemoryOwner<int> pixelStackOwner;
private readonly int minCodeSize;
private readonly int clearCode;
private readonly int endCode;
@ -79,11 +78,12 @@ internal sealed class LzwDecoder : IDisposable
public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size.");
this.prefix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.suffix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.pixelStack = memoryAllocator.Allocate<int>(MaxStackSize + 1, AllocationOptions.Clean);
this.scratchBuffer = memoryAllocator.Allocate<byte>(byte.MaxValue, AllocationOptions.None);
this.prefixOwner = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.suffixOwner = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.pixelStackOwner = memoryAllocator.Allocate<int>(MaxStackSize + 1, AllocationOptions.Clean);
this.bufferOwner = memoryAllocator.Allocate<byte>(byte.MaxValue, AllocationOptions.None);
this.minCodeSize = minCodeSize;
// Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
@ -93,11 +93,15 @@ internal sealed class LzwDecoder : IDisposable
this.endCode = this.clearCode + 1;
this.availableCode = this.clearCode + 2;
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
for (this.code = 0; this.code < this.clearCode; this.code++)
// Fill the suffix buffer with the initial values represented by the number of colors.
Span<int> suffix = this.suffixOwner.GetSpan()[..this.clearCode];
int i;
for (i = 0; i < suffix.Length; i++)
{
Unsafe.Add(ref suffixRef, (uint)this.code) = (byte)this.code;
suffix[i] = i;
}
this.code = i;
}
/// <summary>
@ -112,8 +116,7 @@ internal sealed class LzwDecoder : IDisposable
// It is possible to specify a larger LZW minimum code size than the palette length in bits
// which may leave a gap in the codes where no colors are assigned.
// http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
int clearCode = 1 << minCodeSize;
if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize)
if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize)
{
// Don't attempt to decode the frame indices.
// Theoretically we could determine a min code size from the length of the provided
@ -132,112 +135,139 @@ internal sealed class LzwDecoder : IDisposable
{
indices.Clear();
ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices);
ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
Span<byte> buffer = this.scratchBuffer.GetSpan();
int x = 0;
int xyz = 0;
while (xyz < indices.Length)
// Get span values from the owners.
Span<int> prefix = this.prefixOwner.GetSpan();
Span<int> suffix = this.suffixOwner.GetSpan();
Span<int> pixelStack = this.pixelStackOwner.GetSpan();
Span<byte> buffer = this.bufferOwner.GetSpan();
// Cache frequently accessed instance fields into locals.
// This helps avoid repeated field loads inside the tight loop.
BufferedReadStream stream = this.stream;
int top = this.top;
int bits = this.bits;
int codeSize = this.codeSize;
int codeMask = this.codeMask;
int minCodeSize = this.minCodeSize;
int availableCode = this.availableCode;
int oldCode = this.oldCode;
int first = this.first;
int data = this.data;
int count = this.count;
int bufferIndex = this.bufferIndex;
int code = this.code;
int clearCode = this.clearCode;
int endCode = this.endCode;
int i = 0;
while (i < indices.Length)
{
if (this.top == 0)
if (top == 0)
{
if (this.bits < this.codeSize)
if (bits < codeSize)
{
// Load bytes until there are enough bits for a code.
if (this.count == 0)
if (count == 0)
{
// Read a new data block.
this.count = this.ReadBlock(buffer);
if (this.count == 0)
count = ReadBlock(stream, buffer);
if (count == 0)
{
break;
}
this.bufferIndex = 0;
bufferIndex = 0;
}
this.data += buffer[this.bufferIndex] << this.bits;
this.bits += 8;
this.bufferIndex++;
this.count--;
data += buffer[bufferIndex] << bits;
bits += 8;
bufferIndex++;
count--;
continue;
}
// Get the next code
this.code = this.data & this.codeMask;
this.data >>= this.codeSize;
this.bits -= this.codeSize;
code = data & codeMask;
data >>= codeSize;
bits -= codeSize;
// Interpret the code
if (this.code > this.availableCode || this.code == this.endCode)
if (code > availableCode || code == endCode)
{
break;
}
if (this.code == this.clearCode)
if (code == clearCode)
{
// Reset the decoder
this.codeSize = this.minCodeSize + 1;
this.codeMask = (1 << this.codeSize) - 1;
this.availableCode = this.clearCode + 2;
this.oldCode = NullCode;
codeSize = minCodeSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
continue;
}
if (this.oldCode == NullCode)
if (oldCode == NullCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.oldCode = this.code;
this.first = this.code;
pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
int inCode = this.code;
if (this.code == this.availableCode)
int inCode = code;
if (code == availableCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first;
this.code = this.oldCode;
pixelStack[top++] = first;
code = oldCode;
}
while (this.code > this.clearCode)
while (code > clearCode && top < MaxStackSize)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.code = Unsafe.Add(ref prefixRef, (uint)this.code);
pixelStack[top++] = suffix[code];
code = prefix[code];
}
int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code);
this.first = suffixCode;
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode;
int suffixCode = suffix[code];
first = suffixCode;
pixelStack[top++] = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here :
// Fix for GIFs that have "deferred clear code" as per:
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (this.availableCode < MaxStackSize)
if (availableCode < MaxStackSize)
{
Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode;
Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first;
this.availableCode++;
if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
prefix[availableCode] = oldCode;
suffix[availableCode] = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
this.codeSize++;
this.codeMask = (1 << this.codeSize) - 1;
codeSize++;
codeMask = (1 << codeSize) - 1;
}
}
this.oldCode = inCode;
oldCode = inCode;
}
// Pop a pixel off the pixel stack.
this.top--;
top--;
// Clear missing pixels
xyz++;
Unsafe.Add(ref pixelsRowRef, (uint)x++) = (byte)Unsafe.Add(ref pixelStackRef, (uint)this.top);
// Clear missing pixels.
indices[i++] = (byte)pixelStack[top];
}
// Write back the local values to the instance fields.
this.top = top;
this.bits = bits;
this.codeSize = codeSize;
this.codeMask = codeMask;
this.availableCode = availableCode;
this.oldCode = oldCode;
this.first = first;
this.data = data;
this.count = count;
this.bufferIndex = bufferIndex;
this.code = code;
}
/// <summary>
@ -246,130 +276,161 @@ internal sealed class LzwDecoder : IDisposable
/// <param name="length">The resulting index table length.</param>
public void SkipIndices(int length)
{
ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
Span<byte> buffer = this.scratchBuffer.GetSpan();
int xyz = 0;
while (xyz < length)
// Get span values from the owners.
Span<int> prefix = this.prefixOwner.GetSpan();
Span<int> suffix = this.suffixOwner.GetSpan();
Span<int> pixelStack = this.pixelStackOwner.GetSpan();
Span<byte> buffer = this.bufferOwner.GetSpan();
// Cache frequently accessed instance fields into locals.
// This helps avoid repeated field loads inside the tight loop.
BufferedReadStream stream = this.stream;
int top = this.top;
int bits = this.bits;
int codeSize = this.codeSize;
int codeMask = this.codeMask;
int minCodeSize = this.minCodeSize;
int availableCode = this.availableCode;
int oldCode = this.oldCode;
int first = this.first;
int data = this.data;
int count = this.count;
int bufferIndex = this.bufferIndex;
int code = this.code;
int clearCode = this.clearCode;
int endCode = this.endCode;
int i = 0;
while (i < length)
{
if (this.top == 0)
if (top == 0)
{
if (this.bits < this.codeSize)
if (bits < codeSize)
{
// Load bytes until there are enough bits for a code.
if (this.count == 0)
if (count == 0)
{
// Read a new data block.
this.count = this.ReadBlock(buffer);
if (this.count == 0)
count = ReadBlock(stream, buffer);
if (count == 0)
{
break;
}
this.bufferIndex = 0;
bufferIndex = 0;
}
this.data += buffer[this.bufferIndex] << this.bits;
this.bits += 8;
this.bufferIndex++;
this.count--;
data += buffer[bufferIndex] << bits;
bits += 8;
bufferIndex++;
count--;
continue;
}
// Get the next code
this.code = this.data & this.codeMask;
this.data >>= this.codeSize;
this.bits -= this.codeSize;
code = data & codeMask;
data >>= codeSize;
bits -= codeSize;
// Interpret the code
if (this.code > this.availableCode || this.code == this.endCode)
if (code > availableCode || code == endCode)
{
break;
}
if (this.code == this.clearCode)
if (code == clearCode)
{
// Reset the decoder
this.codeSize = this.minCodeSize + 1;
this.codeMask = (1 << this.codeSize) - 1;
this.availableCode = this.clearCode + 2;
this.oldCode = NullCode;
codeSize = minCodeSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
continue;
}
if (this.oldCode == NullCode)
if (oldCode == NullCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.oldCode = this.code;
this.first = this.code;
pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
int inCode = this.code;
if (this.code == this.availableCode)
int inCode = code;
if (code == availableCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first;
this.code = this.oldCode;
pixelStack[top++] = first;
code = oldCode;
}
while (this.code > this.clearCode)
while (code > clearCode && top < MaxStackSize)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.code = Unsafe.Add(ref prefixRef, (uint)this.code);
pixelStack[top++] = suffix[code];
code = prefix[code];
}
int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code);
this.first = suffixCode;
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode;
int suffixCode = suffix[code];
first = suffixCode;
pixelStack[top++] = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here :
// Fix for GIFs that have "deferred clear code" as per:
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (this.availableCode < MaxStackSize)
if (availableCode < MaxStackSize)
{
Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode;
Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first;
this.availableCode++;
if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
prefix[availableCode] = oldCode;
suffix[availableCode] = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
this.codeSize++;
this.codeMask = (1 << this.codeSize) - 1;
codeSize++;
codeMask = (1 << codeSize) - 1;
}
}
this.oldCode = inCode;
oldCode = inCode;
}
// Pop a pixel off the pixel stack.
this.top--;
top--;
// Clear missing pixels
xyz++;
// Skip missing pixels.
i++;
}
// Write back the local values to the instance fields.
this.top = top;
this.bits = bits;
this.codeSize = codeSize;
this.codeMask = codeMask;
this.availableCode = availableCode;
this.oldCode = oldCode;
this.first = first;
this.data = data;
this.count = count;
this.bufferIndex = bufferIndex;
this.code = code;
}
/// <summary>
/// Reads the next data block from the stream. A data block begins with a byte,
/// which defines the size of the block, followed by the block itself.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the block in.</param>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ReadBlock(Span<byte> buffer)
private static int ReadBlock(BufferedReadStream stream, Span<byte> buffer)
{
int bufferSize = this.stream.ReadByte();
int bufferSize = stream.ReadByte();
if (bufferSize < 1)
{
return 0;
}
int count = this.stream.Read(buffer, 0, bufferSize);
int count = stream.Read(buffer, 0, bufferSize);
return count != bufferSize ? 0 : bufferSize;
}
@ -377,9 +438,9 @@ internal sealed class LzwDecoder : IDisposable
/// <inheritdoc />
public void Dispose()
{
this.prefix.Dispose();
this.suffix.Dispose();
this.pixelStack.Dispose();
this.scratchBuffer.Dispose();
this.prefixOwner.Dispose();
this.suffixOwner.Dispose();
this.pixelStackOwner.Dispose();
this.bufferOwner.Dispose();
}
}

6
src/ImageSharp/Formats/IAnimatedImageEncoder.cs

@ -14,17 +14,17 @@ public interface IAnimatedImageEncoder
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
Color? BackgroundColor { get; }
public Color? BackgroundColor { get; }
/// <summary>
/// Gets the number of times any animation is repeated in supported encoders.
/// </summary>
ushort? RepeatCount { get; }
public ushort? RepeatCount { get; }
/// <summary>
/// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
/// </summary>
bool? AnimateRootFrame { get; }
public bool? AnimateRootFrame { get; }
}
/// <summary>

6
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -14,7 +14,7 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// Converts the metadata to a <see cref="FormatConnectingFrameMetadata"/> instance.
/// </summary>
/// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns>
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
/// <summary>
/// This method is called after a process has been applied to the image frame.
@ -22,7 +22,7 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -39,6 +39,6 @@ public interface IFormatFrameMetadata<TSelf> : IFormatFrameMetadata, IDeepClonea
/// <param name="metadata">The <see cref="FormatConnectingFrameMetadata"/>.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata);
public static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

8
src/ImageSharp/Formats/IFormatMetadata.cs

@ -14,20 +14,20 @@ public interface IFormatMetadata : IDeepCloneable
/// Converts the metadata to a <see cref="PixelTypeInfo"/> instance.
/// </summary>
/// <returns>The pixel type info.</returns>
PixelTypeInfo GetPixelTypeInfo();
public PixelTypeInfo GetPixelTypeInfo();
/// <summary>
/// Converts the metadata to a <see cref="FormatConnectingMetadata"/> instance.
/// </summary>
/// <returns>The <see cref="FormatConnectingMetadata"/>.</returns>
FormatConnectingMetadata ToFormatConnectingMetadata();
public FormatConnectingMetadata ToFormatConnectingMetadata();
/// <summary>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param>
void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -44,6 +44,6 @@ public interface IFormatMetadata<TSelf> : IFormatMetadata, IDeepCloneable<TSelf>
/// <param name="metadata">The <see cref="FormatConnectingMetadata"/>.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata);
public static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

4
src/ImageSharp/Formats/IQuantizingImageEncoder.cs

@ -13,12 +13,12 @@ public interface IQuantizingImageEncoder
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer? Quantizer { get; }
public IQuantizer? Quantizer { get; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; }
public IPixelSamplingStrategy PixelSamplingStrategy { get; }
}
/// <summary>

2
src/ImageSharp/Formats/ISpecializedDecoderOptions.cs

@ -11,5 +11,5 @@ public interface ISpecializedDecoderOptions
/// <summary>
/// Gets the general decoder options.
/// </summary>
DecoderOptions GeneralOptions { get; init; }
public DecoderOptions GeneralOptions { get; init; }
}

5
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -96,8 +96,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight)
};
}
@ -106,7 +105,6 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
@ -119,6 +117,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>

9
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -71,8 +71,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
return new IcoMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
Compression = compression
};
}
@ -145,15 +144,13 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

16
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -120,17 +120,17 @@ internal abstract class IconEncoderCore
this.entries = this.iconFileType switch
{
IconFileType.ICO =>
image.Frames.Select(i =>
[.. image.Frames.Select(i =>
{
IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata();
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size));
}).ToArray(),
})],
IconFileType.CUR =>
image.Frames.Select(i =>
[.. image.Frames.Select(i =>
{
CurFrameMetadata metadata = i.Metadata.GetCurMetadata();
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size));
}).ToArray(),
})],
_ => throw new NotSupportedException(),
};
}
@ -149,9 +149,15 @@ internal abstract class IconEncoderCore
if (metadata.ColorTable is null)
{
int count = metadata.Entry.ColorCount;
if (count == 0)
{
count = 256;
}
return new WuQuantizer(new()
{
MaxColors = metadata.Entry.ColorCount
MaxColors = count
});
}

30
src/ImageSharp/Formats/ImageDecoder.cs

@ -24,6 +24,7 @@ public abstract class ImageDecoder : IImageDecoder
s => this.Decode<TPixel>(options, s, default));
this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);
return image;
}
@ -37,6 +38,7 @@ public abstract class ImageDecoder : IImageDecoder
s => this.Decode(options, s, default));
this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);
return image;
}
@ -46,12 +48,13 @@ public abstract class ImageDecoder : IImageDecoder
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel> image = await WithSeekableMemoryStreamAsync(
options,
stream,
(s, ct) => this.Decode<TPixel>(options, s, ct),
cancellationToken).ConfigureAwait(false);
options,
stream,
(s, ct) => this.Decode<TPixel>(options, s, ct),
cancellationToken).ConfigureAwait(false);
this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);
return image;
}
@ -66,6 +69,7 @@ public abstract class ImageDecoder : IImageDecoder
cancellationToken).ConfigureAwait(false);
this.SetDecoderFormat(options.Configuration, image);
HandleIccProfile(options, image);
return image;
}
@ -79,6 +83,7 @@ public abstract class ImageDecoder : IImageDecoder
s => this.Identify(options, s, default));
this.SetDecoderFormat(options.Configuration, info);
HandleIccProfile(options, info);
return info;
}
@ -93,6 +98,7 @@ public abstract class ImageDecoder : IImageDecoder
cancellationToken).ConfigureAwait(false);
this.SetDecoderFormat(options.Configuration, info);
HandleIccProfile(options, info);
return info;
}
@ -315,4 +321,20 @@ public abstract class ImageDecoder : IImageDecoder
}
}
}
private static void HandleIccProfile(DecoderOptions options, Image image)
{
if (options.CanRemoveIccProfile(image.Metadata.IccProfile))
{
image.Metadata.IccProfile = null;
}
}
private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
{
if (options.CanRemoveIccProfile(image.Metadata.IccProfile))
{
image.Metadata.IccProfile = null;
}
}
}

4
src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs

@ -211,10 +211,10 @@ internal partial struct Block8x8
}
/// <summary>
/// Transpose the block inplace.
/// Transpose the block in place.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void TransposeInplace()
public void TransposeInPlace()
{
ref short elemRef = ref Unsafe.As<Block8x8, short>(ref this);

153
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs

@ -1,153 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
// <auto-generated />
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal partial struct Block8x8F
{
/// <summary>
/// Level shift by +maximum/2, clip to [0, maximum]
/// </summary>
public void NormalizeColorsInPlace(float maximum)
{
var CMin4 = new Vector4(0F);
var CMax4 = new Vector4(maximum);
var COff4 = new Vector4(MathF.Ceiling(maximum * 0.5F));
this.V0L = Numerics.Clamp(this.V0L + COff4, CMin4, CMax4);
this.V0R = Numerics.Clamp(this.V0R + COff4, CMin4, CMax4);
this.V1L = Numerics.Clamp(this.V1L + COff4, CMin4, CMax4);
this.V1R = Numerics.Clamp(this.V1R + COff4, CMin4, CMax4);
this.V2L = Numerics.Clamp(this.V2L + COff4, CMin4, CMax4);
this.V2R = Numerics.Clamp(this.V2R + COff4, CMin4, CMax4);
this.V3L = Numerics.Clamp(this.V3L + COff4, CMin4, CMax4);
this.V3R = Numerics.Clamp(this.V3R + COff4, CMin4, CMax4);
this.V4L = Numerics.Clamp(this.V4L + COff4, CMin4, CMax4);
this.V4R = Numerics.Clamp(this.V4R + COff4, CMin4, CMax4);
this.V5L = Numerics.Clamp(this.V5L + COff4, CMin4, CMax4);
this.V5R = Numerics.Clamp(this.V5R + COff4, CMin4, CMax4);
this.V6L = Numerics.Clamp(this.V6L + COff4, CMin4, CMax4);
this.V6R = Numerics.Clamp(this.V6R + COff4, CMin4, CMax4);
this.V7L = Numerics.Clamp(this.V7L + COff4, CMin4, CMax4);
this.V7R = Numerics.Clamp(this.V7R + COff4, CMin4, CMax4);
}
/// <summary>
/// AVX2-only variant for executing <see cref="NormalizeColorsInPlace"/> and <see cref="RoundInPlace"/> in one step.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void NormalizeColorsAndRoundInPlaceVector8(float maximum)
{
var off = new Vector<float>(MathF.Ceiling(maximum * 0.5F));
var max = new Vector<float>(maximum);
ref Vector<float> row0 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V0L);
row0 = NormalizeAndRound(row0, off, max);
ref Vector<float> row1 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V1L);
row1 = NormalizeAndRound(row1, off, max);
ref Vector<float> row2 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V2L);
row2 = NormalizeAndRound(row2, off, max);
ref Vector<float> row3 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V3L);
row3 = NormalizeAndRound(row3, off, max);
ref Vector<float> row4 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V4L);
row4 = NormalizeAndRound(row4, off, max);
ref Vector<float> row5 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V5L);
row5 = NormalizeAndRound(row5, off, max);
ref Vector<float> row6 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V6L);
row6 = NormalizeAndRound(row6, off, max);
ref Vector<float> row7 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V7L);
row7 = NormalizeAndRound(row7, off, max);
}
/// <summary>
/// Fill the block from 'source' doing short -> float conversion.
/// </summary>
public void LoadFromInt16Scalar(ref Block8x8 source)
{
ref short selfRef = ref Unsafe.As<Block8x8, short>(ref source);
this.V0L.X = Unsafe.Add(ref selfRef, 0);
this.V0L.Y = Unsafe.Add(ref selfRef, 1);
this.V0L.Z = Unsafe.Add(ref selfRef, 2);
this.V0L.W = Unsafe.Add(ref selfRef, 3);
this.V0R.X = Unsafe.Add(ref selfRef, 4);
this.V0R.Y = Unsafe.Add(ref selfRef, 5);
this.V0R.Z = Unsafe.Add(ref selfRef, 6);
this.V0R.W = Unsafe.Add(ref selfRef, 7);
this.V1L.X = Unsafe.Add(ref selfRef, 8);
this.V1L.Y = Unsafe.Add(ref selfRef, 9);
this.V1L.Z = Unsafe.Add(ref selfRef, 10);
this.V1L.W = Unsafe.Add(ref selfRef, 11);
this.V1R.X = Unsafe.Add(ref selfRef, 12);
this.V1R.Y = Unsafe.Add(ref selfRef, 13);
this.V1R.Z = Unsafe.Add(ref selfRef, 14);
this.V1R.W = Unsafe.Add(ref selfRef, 15);
this.V2L.X = Unsafe.Add(ref selfRef, 16);
this.V2L.Y = Unsafe.Add(ref selfRef, 17);
this.V2L.Z = Unsafe.Add(ref selfRef, 18);
this.V2L.W = Unsafe.Add(ref selfRef, 19);
this.V2R.X = Unsafe.Add(ref selfRef, 20);
this.V2R.Y = Unsafe.Add(ref selfRef, 21);
this.V2R.Z = Unsafe.Add(ref selfRef, 22);
this.V2R.W = Unsafe.Add(ref selfRef, 23);
this.V3L.X = Unsafe.Add(ref selfRef, 24);
this.V3L.Y = Unsafe.Add(ref selfRef, 25);
this.V3L.Z = Unsafe.Add(ref selfRef, 26);
this.V3L.W = Unsafe.Add(ref selfRef, 27);
this.V3R.X = Unsafe.Add(ref selfRef, 28);
this.V3R.Y = Unsafe.Add(ref selfRef, 29);
this.V3R.Z = Unsafe.Add(ref selfRef, 30);
this.V3R.W = Unsafe.Add(ref selfRef, 31);
this.V4L.X = Unsafe.Add(ref selfRef, 32);
this.V4L.Y = Unsafe.Add(ref selfRef, 33);
this.V4L.Z = Unsafe.Add(ref selfRef, 34);
this.V4L.W = Unsafe.Add(ref selfRef, 35);
this.V4R.X = Unsafe.Add(ref selfRef, 36);
this.V4R.Y = Unsafe.Add(ref selfRef, 37);
this.V4R.Z = Unsafe.Add(ref selfRef, 38);
this.V4R.W = Unsafe.Add(ref selfRef, 39);
this.V5L.X = Unsafe.Add(ref selfRef, 40);
this.V5L.Y = Unsafe.Add(ref selfRef, 41);
this.V5L.Z = Unsafe.Add(ref selfRef, 42);
this.V5L.W = Unsafe.Add(ref selfRef, 43);
this.V5R.X = Unsafe.Add(ref selfRef, 44);
this.V5R.Y = Unsafe.Add(ref selfRef, 45);
this.V5R.Z = Unsafe.Add(ref selfRef, 46);
this.V5R.W = Unsafe.Add(ref selfRef, 47);
this.V6L.X = Unsafe.Add(ref selfRef, 48);
this.V6L.Y = Unsafe.Add(ref selfRef, 49);
this.V6L.Z = Unsafe.Add(ref selfRef, 50);
this.V6L.W = Unsafe.Add(ref selfRef, 51);
this.V6R.X = Unsafe.Add(ref selfRef, 52);
this.V6R.Y = Unsafe.Add(ref selfRef, 53);
this.V6R.Z = Unsafe.Add(ref selfRef, 54);
this.V6R.W = Unsafe.Add(ref selfRef, 55);
this.V7L.X = Unsafe.Add(ref selfRef, 56);
this.V7L.Y = Unsafe.Add(ref selfRef, 57);
this.V7L.Z = Unsafe.Add(ref selfRef, 58);
this.V7L.W = Unsafe.Add(ref selfRef, 59);
this.V7R.X = Unsafe.Add(ref selfRef, 60);
this.V7R.Y = Unsafe.Add(ref selfRef, 61);
this.V7R.Z = Unsafe.Add(ref selfRef, 62);
this.V7R.W = Unsafe.Add(ref selfRef, 63);
}
}

103
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt

@ -1,103 +0,0 @@
<#
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#>
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
// <auto-generated />
<#
char[] coordz = {'X', 'Y', 'Z', 'W'};
#>
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal partial struct Block8x8F
{
/// <summary>
/// Level shift by +maximum/2, clip to [0, maximum]
/// </summary>
public void NormalizeColorsInPlace(float maximum)
{
var CMin4 = new Vector4(0F);
var CMax4 = new Vector4(maximum);
var COff4 = new Vector4(MathF.Ceiling(maximum * 0.5F));
<#
PushIndent(" ");
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 2; j++)
{
char side = j == 0 ? 'L' : 'R';
Write($"this.V{i}{side} = Numerics.Clamp(this.V{i}{side} + COff4, CMin4, CMax4);\r\n");
}
}
PopIndent();
#>
}
/// <summary>
/// AVX2-only variant for executing <see cref="NormalizeColorsInPlace"/> and <see cref="RoundInPlace"/> in one step.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void NormalizeColorsAndRoundInPlaceVector8(float maximum)
{
var off = new Vector<float>(MathF.Ceiling(maximum * 0.5F));
var max = new Vector<float>(maximum);
<#
for (int i = 0; i < 8; i++)
{
#>
ref Vector<float> row<#=i#> = ref Unsafe.As<Vector4, Vector<float>>(ref this.V<#=i#>L);
row<#=i#> = NormalizeAndRound(row<#=i#>, off, max);
<#
}
#>
}
/// <summary>
/// Fill the block from 'source' doing short -> float conversion.
/// </summary>
public void LoadFromInt16Scalar(ref Block8x8 source)
{
ref short selfRef = ref Unsafe.As<Block8x8, short>(ref source);
<#
PushIndent(" ");
for (int j = 0; j < 8; j++)
{
for (int i = 0; i < 8; i++)
{
char destCoord = coordz[i % 4];
char destSide = (i / 4) % 2 == 0 ? 'L' : 'R';
if(j > 0 && i == 0){
WriteLine("");
}
char srcCoord = coordz[j % 4];
char srcSide = (j / 4) % 2 == 0 ? 'L' : 'R';
var expression = $"this.V{j}{destSide}.{destCoord} = Unsafe.Add(ref selfRef, {j*8+i});\r\n";
Write(expression);
}
}
PopIndent();
#>
}
}

144
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Intrinsic.cs

@ -1,144 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal partial struct Block8x8F
{
/// <summary>
/// A number of rows of 8 scalar coefficients each in <see cref="Block8x8F"/>
/// </summary>
public const int RowCount = 8;
[FieldOffset(0)]
public Vector256<float> V0;
[FieldOffset(32)]
public Vector256<float> V1;
[FieldOffset(64)]
public Vector256<float> V2;
[FieldOffset(96)]
public Vector256<float> V3;
[FieldOffset(128)]
public Vector256<float> V4;
[FieldOffset(160)]
public Vector256<float> V5;
[FieldOffset(192)]
public Vector256<float> V6;
[FieldOffset(224)]
public Vector256<float> V7;
private static unsafe void MultiplyIntoInt16_Avx2(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest)
{
DebugGuard.IsTrue(Avx2.IsSupported, "Avx2 support is required to run this operation!");
ref Vector256<float> aBase = ref a.V0;
ref Vector256<float> bBase = ref b.V0;
ref Vector256<short> destRef = ref dest.V01;
Vector256<int> multiplyIntoInt16ShuffleMask = Vector256.Create(0, 1, 4, 5, 2, 3, 6, 7);
for (nuint i = 0; i < 8; i += 2)
{
Vector256<int> row0 = Avx.ConvertToVector256Int32(Avx.Multiply(Unsafe.Add(ref aBase, i + 0), Unsafe.Add(ref bBase, i + 0)));
Vector256<int> row1 = Avx.ConvertToVector256Int32(Avx.Multiply(Unsafe.Add(ref aBase, i + 1), Unsafe.Add(ref bBase, i + 1)));
Vector256<short> row = Avx2.PackSignedSaturate(row0, row1);
row = Avx2.PermuteVar8x32(row.AsInt32(), multiplyIntoInt16ShuffleMask).AsInt16();
Unsafe.Add(ref destRef, i / 2) = row;
}
}
private static void MultiplyIntoInt16_Sse2(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest)
{
DebugGuard.IsTrue(Sse2.IsSupported, "Sse2 support is required to run this operation!");
ref Vector128<float> aBase = ref Unsafe.As<Block8x8F, Vector128<float>>(ref a);
ref Vector128<float> bBase = ref Unsafe.As<Block8x8F, Vector128<float>>(ref b);
ref Vector128<short> destBase = ref Unsafe.As<Block8x8, Vector128<short>>(ref dest);
for (nuint i = 0; i < 16; i += 2)
{
Vector128<int> left = Sse2.ConvertToVector128Int32(Sse.Multiply(Unsafe.Add(ref aBase, i + 0), Unsafe.Add(ref bBase, i + 0)));
Vector128<int> right = Sse2.ConvertToVector128Int32(Sse.Multiply(Unsafe.Add(ref aBase, i + 1), Unsafe.Add(ref bBase, i + 1)));
Vector128<short> row = Sse2.PackSignedSaturate(left, right);
Unsafe.Add(ref destBase, i / 2) = row;
}
}
private void TransposeInplace_Avx()
{
// https://stackoverflow.com/questions/25622745/transpose-an-8x8-float-using-avx-avx2/25627536#25627536
Vector256<float> r0 = Avx.InsertVector128(
this.V0,
Unsafe.As<Vector4, Vector128<float>>(ref this.V4L),
1);
Vector256<float> r1 = Avx.InsertVector128(
this.V1,
Unsafe.As<Vector4, Vector128<float>>(ref this.V5L),
1);
Vector256<float> r2 = Avx.InsertVector128(
this.V2,
Unsafe.As<Vector4, Vector128<float>>(ref this.V6L),
1);
Vector256<float> r3 = Avx.InsertVector128(
this.V3,
Unsafe.As<Vector4, Vector128<float>>(ref this.V7L),
1);
Vector256<float> r4 = Avx.InsertVector128(
Unsafe.As<Vector4, Vector128<float>>(ref this.V0R).ToVector256(),
Unsafe.As<Vector4, Vector128<float>>(ref this.V4R),
1);
Vector256<float> r5 = Avx.InsertVector128(
Unsafe.As<Vector4, Vector128<float>>(ref this.V1R).ToVector256(),
Unsafe.As<Vector4, Vector128<float>>(ref this.V5R),
1);
Vector256<float> r6 = Avx.InsertVector128(
Unsafe.As<Vector4, Vector128<float>>(ref this.V2R).ToVector256(),
Unsafe.As<Vector4, Vector128<float>>(ref this.V6R),
1);
Vector256<float> r7 = Avx.InsertVector128(
Unsafe.As<Vector4, Vector128<float>>(ref this.V3R).ToVector256(),
Unsafe.As<Vector4, Vector128<float>>(ref this.V7R),
1);
Vector256<float> t0 = Avx.UnpackLow(r0, r1);
Vector256<float> t2 = Avx.UnpackLow(r2, r3);
Vector256<float> v = Avx.Shuffle(t0, t2, 0x4E);
this.V0 = Avx.Blend(t0, v, 0xCC);
this.V1 = Avx.Blend(t2, v, 0x33);
Vector256<float> t4 = Avx.UnpackLow(r4, r5);
Vector256<float> t6 = Avx.UnpackLow(r6, r7);
v = Avx.Shuffle(t4, t6, 0x4E);
this.V4 = Avx.Blend(t4, v, 0xCC);
this.V5 = Avx.Blend(t6, v, 0x33);
Vector256<float> t1 = Avx.UnpackHigh(r0, r1);
Vector256<float> t3 = Avx.UnpackHigh(r2, r3);
v = Avx.Shuffle(t1, t3, 0x4E);
this.V2 = Avx.Blend(t1, v, 0xCC);
this.V3 = Avx.Blend(t3, v, 0x33);
Vector256<float> t5 = Avx.UnpackHigh(r4, r5);
Vector256<float> t7 = Avx.UnpackHigh(r6, r7);
v = Avx.Shuffle(t5, t7, 0x4E);
this.V6 = Avx.Blend(t5, v, 0xCC);
this.V7 = Avx.Blend(t7, v, 0x33);
}
}

93
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs

@ -0,0 +1,93 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
/// <content>
/// <see cref="Vector128{Single}"/> version of <see cref="Block8x8F"/>.
/// </content>
internal partial struct Block8x8F
{
/// <summary>
/// <see cref="Vector128{Single}"/> version of <see cref="NormalizeColorsInPlace(float)"/> and <see cref="RoundInPlace()"/>.
/// </summary>
/// <param name="maximum">The maximum value to normalize to.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void NormalizeColorsAndRoundInPlaceVector128(float maximum)
{
Vector128<float> max = Vector128.Create(maximum);
Vector128<float> off = Vector128.Ceiling(max * .5F);
this.V0L = NormalizeAndRoundVector128(this.V0L.AsVector128(), off, max).AsVector4();
this.V0R = NormalizeAndRoundVector128(this.V0R.AsVector128(), off, max).AsVector4();
this.V1L = NormalizeAndRoundVector128(this.V1L.AsVector128(), off, max).AsVector4();
this.V1R = NormalizeAndRoundVector128(this.V1R.AsVector128(), off, max).AsVector4();
this.V2L = NormalizeAndRoundVector128(this.V2L.AsVector128(), off, max).AsVector4();
this.V2R = NormalizeAndRoundVector128(this.V2R.AsVector128(), off, max).AsVector4();
this.V3L = NormalizeAndRoundVector128(this.V3L.AsVector128(), off, max).AsVector4();
this.V3R = NormalizeAndRoundVector128(this.V3R.AsVector128(), off, max).AsVector4();
this.V4L = NormalizeAndRoundVector128(this.V4L.AsVector128(), off, max).AsVector4();
this.V4R = NormalizeAndRoundVector128(this.V4R.AsVector128(), off, max).AsVector4();
this.V5L = NormalizeAndRoundVector128(this.V5L.AsVector128(), off, max).AsVector4();
this.V5R = NormalizeAndRoundVector128(this.V5R.AsVector128(), off, max).AsVector4();
this.V6L = NormalizeAndRoundVector128(this.V6L.AsVector128(), off, max).AsVector4();
this.V6R = NormalizeAndRoundVector128(this.V6R.AsVector128(), off, max).AsVector4();
this.V7L = NormalizeAndRoundVector128(this.V7L.AsVector128(), off, max).AsVector4();
this.V7R = NormalizeAndRoundVector128(this.V7R.AsVector128(), off, max).AsVector4();
}
/// <summary>
/// Loads values from <paramref name="source"/> using extended AVX2 intrinsics.
/// </summary>
/// <param name="source">The source <see cref="Block8x8"/></param>
public void LoadFromInt16ExtendedVector128(ref Block8x8 source)
{
DebugGuard.IsTrue(Vector128.IsHardwareAccelerated, "Vector128 support is required to run this operation!");
ref Vector128<short> srcBase = ref Unsafe.As<Block8x8, Vector128<short>>(ref source);
ref Vector128<float> destBase = ref Unsafe.As<Block8x8F, Vector128<float>>(ref this);
// Only 8 iterations, one per 128b short block
for (nuint i = 0; i < 8; i++)
{
Vector128<short> src = Unsafe.Add(ref srcBase, i);
// Step 1: Widen short -> int
Vector128<int> lower = Vector128.WidenLower(src); // lower 4 shorts -> 4 ints
Vector128<int> upper = Vector128.WidenUpper(src); // upper 4 shorts -> 4 ints
// Step 2: Convert int -> float
Vector128<float> lowerF = Vector128.ConvertToSingle(lower);
Vector128<float> upperF = Vector128.ConvertToSingle(upper);
// Step 3: Store to destination (this is 16 lanes -> two Vector128<float> blocks)
Unsafe.Add(ref destBase, (i * 2) + 0) = lowerF;
Unsafe.Add(ref destBase, (i * 2) + 1) = upperF;
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private static Vector128<float> NormalizeAndRoundVector128(Vector128<float> value, Vector128<float> off, Vector128<float> max)
=> Vector128_.RoundToNearestInteger(Vector128_.Clamp(value + off, Vector128<float>.Zero, max));
private static void MultiplyIntoInt16Vector128(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest)
{
DebugGuard.IsTrue(Vector128.IsHardwareAccelerated, "Vector128 support is required to run this operation!");
ref Vector128<float> aBase = ref Unsafe.As<Block8x8F, Vector128<float>>(ref a);
ref Vector128<float> bBase = ref Unsafe.As<Block8x8F, Vector128<float>>(ref b);
ref Vector128<short> destBase = ref Unsafe.As<Block8x8, Vector128<short>>(ref dest);
for (nuint i = 0; i < 16; i += 2)
{
Vector128<int> left = Vector128_.ConvertToInt32RoundToEven(Unsafe.Add(ref aBase, i + 0) * Unsafe.Add(ref bBase, i + 0));
Vector128<int> right = Vector128_.ConvertToInt32RoundToEven(Unsafe.Add(ref aBase, i + 1) * Unsafe.Add(ref bBase, i + 1));
Unsafe.Add(ref destBase, i / 2) = Vector128_.PackSignedSaturate(left, right);
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save