Browse Source

Merge branch 'main' into dependabot/github_actions/codecov/codecov-action-5

pull/2840/head
James Jackson-South 4 days ago
committed by GitHub
parent
commit
0d4aa3ece3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      .editorconfig
  2. 7
      .gitattributes
  3. 113
      .github/workflows/build-and-test.yml
  4. 26
      .github/workflows/code-coverage.yml
  5. 9
      Directory.Build.props
  6. 2
      shared-infrastructure
  7. 27
      src/ImageSharp/Advanced/AotCompilerTools.cs
  8. 4
      src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs
  9. 28
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  10. 8
      src/ImageSharp/Color/Color.WebSafePalette.cs
  11. 8
      src/ImageSharp/Color/Color.WernerPalette.cs
  12. 394
      src/ImageSharp/Color/Color.cs
  13. 40
      src/ImageSharp/Color/ColorHexFormat.cs
  14. 48
      src/ImageSharp/ColorProfiles/CieLab.cs
  15. 56
      src/ImageSharp/ColorProfiles/CieLch.cs
  16. 56
      src/ImageSharp/ColorProfiles/CieLchuv.cs
  17. 15
      src/ImageSharp/ColorProfiles/CieLuv.cs
  18. 33
      src/ImageSharp/ColorProfiles/CieXyy.cs
  19. 86
      src/ImageSharp/ColorProfiles/CieXyz.cs
  20. 49
      src/ImageSharp/ColorProfiles/Cmyk.cs
  21. 39
      src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
  22. 9
      src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
  23. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
  24. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
  25. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
  26. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
  27. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
  28. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
  29. 772
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  30. 192
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
  31. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
  32. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
  33. 43
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
  34. 42
      src/ImageSharp/ColorProfiles/Hsl.cs
  35. 42
      src/ImageSharp/ColorProfiles/Hsv.cs
  36. 45
      src/ImageSharp/ColorProfiles/HunterLab.cs
  37. 48
      src/ImageSharp/ColorProfiles/IColorProfile.cs
  38. 506
      src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
  39. 65
      src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs
  40. 14
      src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs
  41. 47
      src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs
  42. 19
      src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs
  43. 17
      src/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs
  44. 19
      src/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs
  45. 18
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
  46. 135
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  47. 77
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs
  48. 80
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  49. 26
      src/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs
  50. 130
      src/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs
  51. 41
      src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
  52. 42
      src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs
  53. 155
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs
  54. 66
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs
  55. 109
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  56. 49
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs
  57. 22
      src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
  58. 22
      src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
  59. 22
      src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
  60. 22
      src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
  61. 8
      src/ImageSharp/ColorProfiles/KnownIlluminants.cs
  62. 62
      src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs
  63. 56
      src/ImageSharp/ColorProfiles/Lms.cs
  64. 278
      src/ImageSharp/ColorProfiles/Rgb.cs
  65. 14
      src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
  66. 142
      src/ImageSharp/ColorProfiles/Y.cs
  67. 86
      src/ImageSharp/ColorProfiles/YCbCr.cs
  68. 61
      src/ImageSharp/ColorProfiles/YCbCrTransform.cs
  69. 206
      src/ImageSharp/ColorProfiles/YccK.cs
  70. 21
      src/ImageSharp/Common/Extensions/Vector4Extensions.cs
  71. 8
      src/ImageSharp/Common/Helpers/HexConverter.cs
  72. 29
      src/ImageSharp/Common/Helpers/Numerics.cs
  73. 263
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  74. 37
      src/ImageSharp/Common/Helpers/SimdUtils.cs
  75. 2
      src/ImageSharp/Common/Helpers/TolerantMath.cs
  76. 1208
      src/ImageSharp/Common/Helpers/Vector128Utilities.cs
  77. 447
      src/ImageSharp/Common/Helpers/Vector256Utilities.cs
  78. 102
      src/ImageSharp/Common/Helpers/Vector512Utilities.cs
  79. 38
      src/ImageSharp/Common/InlineArray.cs
  80. 38
      src/ImageSharp/Common/InlineArray.tt
  81. 6
      src/ImageSharp/Compression/Zlib/Adler32.cs
  82. 10
      src/ImageSharp/Compression/Zlib/DeflaterConstants.cs
  83. 36
      src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
  84. 2
      src/ImageSharp/Configuration.cs
  85. 19
      src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
  86. 8
      src/ImageSharp/Formats/Bmp/BmpConstants.cs
  87. 2
      src/ImageSharp/Formats/Bmp/BmpDecoder.cs
  88. 10
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  89. 236
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  90. 2
      src/ImageSharp/Formats/Bmp/BmpFormat.cs
  91. 6
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  92. 26
      src/ImageSharp/Formats/ColorProfileHandling.cs
  93. 55
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  94. 12
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  95. 47
      src/ImageSharp/Formats/DecoderOptions.cs
  96. 169
      src/ImageSharp/Formats/EncodingUtilities.cs
  97. 5
      src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs
  98. 7
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  99. 22
      src/ImageSharp/Formats/Gif/GifConstants.cs
  100. 255
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs

18
.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,10 @@ 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
# ReSharper inspection severities
resharper_arrange_object_creation_when_type_evident_highlighting = error
resharper_arrange_object_creation_when_type_not_evident_highlighting = error
# "Null" checking preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning
@ -174,6 +177,9 @@ csharp_using_directive_placement = outside_namespace:warning
csharp_prefer_static_local_function = true:warning
# Primary constructor preferences
csharp_style_prefer_primary_constructors = false:none
# Collection preferences
dotnet_style_prefer_collection_expression = true:error
resharper_use_collection_expression_highlighting =true:error
##########################################
# Unnecessary Code Rules

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

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

@ -11,35 +11,79 @@ on:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
types: [ opened, synchronize, reopened ]
jobs:
# Prime a single LFS cache and expose the exact key for the matrix
WarmLFS:
runs-on: ubuntu-latest
outputs:
lfs_key: ${{ steps.expose-key.outputs.lfs_key }}
steps:
- name: Git Config
shell: bash
run: |
git config --global core.autocrlf false
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
# Deterministic list of LFS object IDs, then compute a portable key:
# - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256
# - `awk '{print $1}'` extracts just the SHA field
# - `sort` sorts in byte order (hex hashes sort the same everywhere)
# This ensures the file content is identical regardless of OS or locale
- name: Git Create LFS id list
shell: bash
run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id
- name: Git Expose LFS cache key
id: expose-key
shell: bash
env:
LFS_KEY: lfs-${{ hashFiles('.lfs-assets-id') }}-v1
run: echo "lfs_key=$LFS_KEY" >> "$GITHUB_OUTPUT"
- name: Git Setup LFS Cache
uses: actions/cache@v4
with:
path: .git/lfs
key: ${{ steps.expose-key.outputs.lfs_key }}
- name: Git Pull LFS
shell: bash
run: git lfs pull
Build:
needs: WarmLFS
strategy:
matrix:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options:
- os: ubuntu-latest
framework: net9.0
sdk: 9.0.x
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
framework: net9.0
sdk: 9.0.x
- os: macos-26
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net9.0
sdk: 9.0.x
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net9.0
sdk: 9.0.x
- os: ubuntu-22.04-arm
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
@ -49,7 +93,7 @@ jobs:
sdk: 8.0.x
runtime: -x64
codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
- os: macos-26
framework: net8.0
sdk: 8.0.x
runtime: -x64
@ -59,22 +103,32 @@ jobs:
sdk: 8.0.x
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
- os: ubuntu-22.04-arm
framework: net8.0
sdk: 8.0.x
runtime: -x64
codecov: false
exclude:
- isARM: false
options:
os: buildjet-4vcpu-ubuntu-2204-arm
runs-on: ${{matrix.options.os}}
runs-on: ${{ matrix.options.os }}
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: Install libgdi+, which is required for tests running on macos
if: ${{ contains(matrix.options.os, 'macos-26') }}
run: |
brew update
brew install mono-libgdiplus
# Create symlinks to make libgdiplus discoverable
sudo mkdir -p /usr/local/lib
sudo ln -sf $(brew --prefix)/lib/libgdiplus.dylib /usr/local/lib/libgdiplus.dylib
# Verify installation
ls -la $(brew --prefix)/lib/libgdiplus* || echo "libgdiplus not found in brew prefix"
ls -la /usr/local/lib/libgdiplus* || echo "libgdiplus not found in /usr/local/lib"
- name: Git Config
shell: bash
@ -88,18 +142,15 @@ jobs:
fetch-depth: 0
submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
# Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here.
- name: Git Setup LFS Cache
uses: actions/cache@v4
id: lfs-cache
with:
path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
key: ${{ needs.WarmLFS.outputs.lfs_key }}
- name: Git Pull LFS
shell: bash
run: git lfs pull
- name: NuGet Install
@ -125,7 +176,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
9.0.x
10.0.x
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
@ -166,11 +217,8 @@ jobs:
Publish:
needs: [Build]
runs-on: ubuntu-latest
if: (github.event_name == 'push')
steps:
- name: Git Config
shell: bash
@ -211,4 +259,3 @@ jobs:
run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate

26
.github/workflows/code-coverage.yml

@ -4,6 +4,7 @@ on:
schedule:
# 2AM every Tuesday/Thursday
- cron: "0 2 * * 2,4"
jobs:
Build:
strategy:
@ -14,9 +15,15 @@ jobs:
runtime: -x64
codecov: true
runs-on: ${{matrix.options.os}}
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: |
@ -29,16 +36,21 @@ jobs:
fetch-depth: 0
submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
# Deterministic list of LFS object IDs, then compute a portable key:
# - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256
# - `awk '{print $1}'` extracts just the SHA field
# - `sort` sorts in byte order (hex hashes sort the same everywhere)
# This ensures the file content is identical regardless of OS or locale
- name: Git Create LFS id list
shell: bash
run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id
- name: Git Setup LFS Cache
uses: actions/cache@v4
id: lfs-cache
with:
path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
key: lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS
run: git lfs pull
@ -62,13 +74,13 @@ jobs:
- name: DotNet Build
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
run: ./ci-build.ps1 "${{ matrix.options.framework }}"
env:
SIXLABORS_TESTING: True
- name: DotNet Test
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
run: ./ci-test.ps1 "${{ matrix.options.os }}" "${{ matrix.options.framework }}" "${{ matrix.options.runtime }}" "${{ matrix.options.codecov }}"
env:
SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit

9
Directory.Build.props

@ -21,9 +21,12 @@
<!-- 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 Condition="'$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0'">
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0'">
<LangVersion>14.0</LangVersion>
</PropertyGroup>
<!--

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 1dbfb576c83507645265c79e03369b66cdc0379f
Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947

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>

4
src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs

@ -139,7 +139,7 @@ public static partial class ParallelRowIterator
}
int yMax = Math.Min(yMin + this.stepY, this.maxY);
var rows = new RowInterval(yMin, yMax);
RowInterval rows = new(yMin, yMax);
// Skip the safety copy when invoking a potentially impure method on a readonly field
Unsafe.AsRef(in this.operation).Invoke(in rows);
@ -185,7 +185,7 @@ public static partial class ParallelRowIterator
}
int yMax = Math.Min(yMin + this.stepY, this.maxY);
var rows = new RowInterval(yMin, yMax);
RowInterval rows = new(yMin, yMax);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.bufferLength);

28
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -26,7 +26,7 @@ public static partial class ParallelRowIterator
public static void IterateRows<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowOperation
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation);
}
@ -65,8 +65,8 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T>(top, bottom, verticalStep, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
0,
@ -88,7 +88,7 @@ public static partial class ParallelRowIterator
where T : struct, IRowOperation<TBuffer>
where TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
@ -135,8 +135,8 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
0,
@ -156,7 +156,7 @@ public static partial class ParallelRowIterator
public static void IterateRowIntervals<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRowIntervals(rectangle, in parallelSettings, in operation);
}
@ -186,14 +186,14 @@ public static partial class ParallelRowIterator
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
RowInterval rows = new(top, bottom);
Unsafe.AsRef(in operation).Invoke(in rows);
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T>(top, bottom, verticalStep, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
0,
@ -215,7 +215,7 @@ public static partial class ParallelRowIterator
where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRowIntervals<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
@ -250,7 +250,7 @@ public static partial class ParallelRowIterator
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
RowInterval rows = new(top, bottom);
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(bufferLength);
Unsafe.AsRef(in operation).Invoke(in rows, buffer.Memory.Span);
@ -259,8 +259,8 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
0,

8
src/ImageSharp/Color/Color.WebSafePalette.cs

@ -8,15 +8,15 @@ namespace SixLabors.ImageSharp;
/// </content>
public partial struct Color
{
private static readonly Lazy<Color[]> WebSafePaletteLazy = new Lazy<Color[]>(CreateWebSafePalette, true);
private static readonly Lazy<Color[]> WebSafePaletteLazy = new(CreateWebSafePalette, true);
/// <summary>
/// Gets a collection of named, web safe colors as defined in the CSS Color Module Level 4.
/// </summary>
public static ReadOnlyMemory<Color> WebSafePalette => WebSafePaletteLazy.Value;
private static Color[] CreateWebSafePalette() => new[]
{
private static Color[] CreateWebSafePalette() =>
[
AliceBlue,
AntiqueWhite,
Aqua,
@ -159,5 +159,5 @@ public partial struct Color
WhiteSmoke,
Yellow,
YellowGreen
};
];
}

8
src/ImageSharp/Color/Color.WernerPalette.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp;
/// </content>
public partial struct Color
{
private static readonly Lazy<Color[]> WernerPaletteLazy = new Lazy<Color[]>(CreateWernerPalette, true);
private static readonly Lazy<Color[]> WernerPaletteLazy = new(CreateWernerPalette, true);
/// <summary>
/// Gets a collection of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821.
@ -16,8 +16,8 @@ public partial struct Color
/// </summary>
public static ReadOnlyMemory<Color> WernerPalette => WernerPaletteLazy.Value;
private static Color[] CreateWernerPalette() => new[]
{
private static Color[] CreateWernerPalette() =>
[
ParseHex("#f1e9cd"),
ParseHex("#f2e7cf"),
ParseHex("#ece6d0"),
@ -128,5 +128,5 @@ public partial struct Color
ParseHex("#9b856b"),
ParseHex("#766051"),
ParseHex("#453b32")
};
];
}

394
src/ImageSharp/Color/Color.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
@ -81,10 +82,10 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{
return new(source.ToScaledVector4());
return new Color(source.ToScaledVector4());
}
return new(source);
return new Color(source);
}
/// <summary>
@ -95,6 +96,21 @@ public readonly partial struct Color : IEquatable<Color>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source) => new(source);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
/// </summary>
/// <param name="source">The source vector span.</param>
/// <param name="destination">The destination color span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i]);
}
}
/// <summary>
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
/// </summary>
@ -111,81 +127,106 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{
for (int i = 0; i < destination.Length; i++)
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i].ToScaledVector4());
}
}
else
{
for (int i = 0; i < destination.Length; i++)
for (int i = 0; i < source.Length; i++)
{
destination[i] = new(source[i]);
destination[i] = new Color(source[i]);
}
}
}
/// <summary>
/// Creates a new instance of the <see cref="Color"/> struct
/// from the given hexadecimal string.
/// Gets a <see cref="Color"/> from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>
/// The <see cref="Color"/>.
/// The <see cref="Color"/> equivalent of the hexadecimal input.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color ParseHex(string hex)
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="hex"/> is not in the correct format.
/// </exception>
public static Color ParseHex(string hex, ColorHexFormat format = ColorHexFormat.Rgba)
{
Rgba32 rgba = Rgba32.ParseHex(hex);
return FromPixel(rgba);
Guard.NotNull(hex, nameof(hex));
if (!TryParseHex(hex, out Color color, format))
{
throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex));
}
return color;
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct
/// from the given hexadecimal string.
/// Gets a <see cref="Color"/> from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <param name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseHex(string hex, out Color result)
public static bool TryParseHex(string hex, out Color result, ColorHexFormat format = ColorHexFormat.Rgba)
{
result = default;
if (Rgba32.TryParseHex(hex, out Rgba32 rgba))
if (format == ColorHexFormat.Argb)
{
result = FromPixel(rgba);
return true;
if (TryParseArgbHex(hex, out Argb32 argb))
{
result = FromPixel(argb);
return true;
}
}
else if (format == ColorHexFormat.Rgba)
{
if (TryParseRgbaHex(hex, out Rgba32 rgba))
{
result = FromPixel(rgba);
return true;
}
}
return false;
}
/// <summary>
/// Creates a new instance of the <see cref="Color"/> struct
/// from the given input string.
/// Gets a <see cref="Color"/> from the given input string.
/// </summary>
/// <param name="input">
/// The name of the color or the hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The name of the color or the hexadecimal representation of the combined color components.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>
/// The <see cref="Color"/>.
/// The <see cref="Color"/> equivalent of the input string.
/// </returns>
/// <exception cref="ArgumentException">Input string is not in the correct format.</exception>
public static Color Parse(string input)
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="input"/> is not in the correct format.
/// </exception>
public static Color Parse(string input, ColorHexFormat format = ColorHexFormat.Rgba)
{
Guard.NotNull(input, nameof(input));
if (!TryParse(input, out Color color))
if (!TryParse(input, out Color color, format))
{
throw new ArgumentException("Input string is not in the correct format.", nameof(input));
}
@ -194,18 +235,21 @@ public readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct
/// from the given input string.
/// Tries to create a new instance of the <see cref="Color"/> struct from the given input string.
/// </summary>
/// <param name="input">
/// The name of the color or the hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The name of the color or the hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Color"/> equivalent of the input string.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <param name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryParse(string input, out Color result)
public static bool TryParse(string input, out Color result, ColorHexFormat format = ColorHexFormat.Rgba)
{
result = default;
@ -219,7 +263,13 @@ public readonly partial struct Color : IEquatable<Color>
return true;
}
return TryParseHex(input, out result);
result = default;
if (string.IsNullOrWhiteSpace(input))
{
return false;
}
return TryParseHex(input, out result, format);
}
/// <summary>
@ -227,6 +277,7 @@ public readonly partial struct Color : IEquatable<Color>
/// </summary>
/// <param name="alpha">The new value of alpha [0..1].</param>
/// <returns>The color having it's alpha channel altered.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Color WithAlpha(float alpha)
{
Vector4 v = this.ToScaledVector4();
@ -235,22 +286,32 @@ public readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Gets the hexadecimal representation of the color instance in rrggbbaa form.
/// Gets the hexadecimal string representation of the color instance.
/// </summary>
/// <param name="format">
/// The format of the hexadecimal string to return. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>A hexadecimal string representation of the value.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="format"/> is not supported.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToHex()
public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba)
{
if (this.boxedHighPrecisionPixel is not null)
Rgba32 rgba = (this.boxedHighPrecisionPixel is not null)
? this.boxedHighPrecisionPixel.ToRgba32()
: Rgba32.FromScaledVector4(this.data);
uint hexOrder = format switch
{
return this.boxedHighPrecisionPixel.ToRgba32().ToHex();
}
ColorHexFormat.Argb => (uint)((rgba.B << 0) | (rgba.G << 8) | (rgba.R << 16) | (rgba.A << 24)),
ColorHexFormat.Rgba => (uint)((rgba.A << 0) | (rgba.B << 8) | (rgba.G << 16) | (rgba.R << 24)),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported color hex format.")
};
return Rgba32.FromScaledVector4(this.data).ToHex();
return hexOrder.ToString("X8", CultureInfo.InvariantCulture);
}
/// <inheritdoc />
public override string ToString() => this.ToHex();
public override string ToString() => this.ToHex(ColorHexFormat.Rgba);
/// <summary>
/// Converts the color instance to a specified <typeparamref name="TPixel"/> type.
@ -336,4 +397,241 @@ public readonly partial struct Color : IEquatable<Color>
return this.boxedHighPrecisionPixel.GetHashCode();
}
/// <summary>
/// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Rgba32"/> equivalent of the hexadecimal input.
/// </param>
/// <returns>
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryParseRgbaHex(string? hex, out Rgba32 result)
{
result = default;
if (!TryConvertToRgbaUInt32(hex, out uint packedValue))
{
return false;
}
result = Unsafe.As<uint, Rgba32>(ref packedValue);
return true;
}
/// <summary>
/// Gets the hexadecimal string representation of the color instance in the format AARRGGBB.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Argb32"/> equivalent of the hexadecimal input.
/// </param>
/// <returns>
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryParseArgbHex(string? hex, out Argb32 result)
{
result = default;
if (!TryConvertToArgbUInt32(hex, out uint packedValue))
{
return false;
}
result = Unsafe.As<uint, Argb32>(ref packedValue);
return true;
}
private static bool TryConvertToRgbaUInt32(string? value, out uint result)
{
result = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
ReadOnlySpan<char> hex = value.AsSpan();
if (hex[0] == '#')
{
hex = hex[1..];
}
byte a = 255, r, g, b;
switch (hex.Length)
{
case 8:
if (!TryParseByte(hex[0], hex[1], out r) ||
!TryParseByte(hex[2], hex[3], out g) ||
!TryParseByte(hex[4], hex[5], out b) ||
!TryParseByte(hex[6], hex[7], out a))
{
return false;
}
break;
case 6:
if (!TryParseByte(hex[0], hex[1], out r) ||
!TryParseByte(hex[2], hex[3], out g) ||
!TryParseByte(hex[4], hex[5], out b))
{
return false;
}
break;
case 4:
if (!TryExpand(hex[0], out r) ||
!TryExpand(hex[1], out g) ||
!TryExpand(hex[2], out b) ||
!TryExpand(hex[3], out a))
{
return false;
}
break;
case 3:
if (!TryExpand(hex[0], out r) ||
!TryExpand(hex[1], out g) ||
!TryExpand(hex[2], out b))
{
return false;
}
break;
default:
return false;
}
result = (uint)(r | (g << 8) | (b << 16) | (a << 24)); // RGBA layout
return true;
}
private static bool TryConvertToArgbUInt32(string? value, out uint result)
{
result = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
ReadOnlySpan<char> hex = value.AsSpan();
if (hex[0] == '#')
{
hex = hex[1..];
}
byte a = 255, r, g, b;
switch (hex.Length)
{
case 8:
if (!TryParseByte(hex[0], hex[1], out a) ||
!TryParseByte(hex[2], hex[3], out r) ||
!TryParseByte(hex[4], hex[5], out g) ||
!TryParseByte(hex[6], hex[7], out b))
{
return false;
}
break;
case 6:
if (!TryParseByte(hex[0], hex[1], out r) ||
!TryParseByte(hex[2], hex[3], out g) ||
!TryParseByte(hex[4], hex[5], out b))
{
return false;
}
break;
case 4:
if (!TryExpand(hex[0], out a) ||
!TryExpand(hex[1], out r) ||
!TryExpand(hex[2], out g) ||
!TryExpand(hex[3], out b))
{
return false;
}
break;
case 3:
if (!TryExpand(hex[0], out r) ||
!TryExpand(hex[1], out g) ||
!TryExpand(hex[2], out b))
{
return false;
}
break;
default:
return false;
}
result = (uint)((b << 24) | (g << 16) | (r << 8) | a);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryParseByte(char hi, char lo, out byte value)
{
if (TryConvertHexCharToByte(hi, out byte high) && TryConvertHexCharToByte(lo, out byte low))
{
value = (byte)((high << 4) | low);
return true;
}
value = 0;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryExpand(char c, out byte value)
{
if (TryConvertHexCharToByte(c, out byte nibble))
{
value = (byte)((nibble << 4) | nibble);
return true;
}
value = 0;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryConvertHexCharToByte(char c, out byte value)
{
if ((uint)(c - '0') <= 9)
{
value = (byte)(c - '0');
return true;
}
char lower = (char)(c | 0x20); // Normalize to lowercase
if ((uint)(lower - 'a') <= 5)
{
value = (byte)(lower - 'a' + 10);
return true;
}
value = 0;
return false;
}
}

40
src/ImageSharp/Color/ColorHexFormat.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp;
/// <summary>
/// Specifies the channel order when formatting or parsing a color as a hexadecimal string.
/// </summary>
public enum ColorHexFormat
{
/// <summary>
/// Uses <c>RRGGBBAA</c> channel order where the red, green, and blue components come first,
/// followed by the alpha component. This matches the CSS Color Module Level 4 and common web standards.
/// <para>
/// When parsing, supports the following formats:
/// <list type="bullet">
/// <item><description><c>#RGB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
/// <item><description><c>#RGBA</c> expands to <c>RRGGBBAA</c></description></item>
/// <item><description><c>#RRGGBB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
/// <item><description><c>#RRGGBBAA</c> used as-is</description></item>
/// </list>
/// </para>
/// When formatting, outputs an 8-digit hex string in <c>RRGGBBAA</c> order.
/// </summary>
Rgba,
/// <summary>
/// Uses <c>AARRGGBB</c> channel order where the alpha component comes first,
/// followed by the red, green, and blue components. This matches the Microsoft/XAML convention.
/// <para>
/// When parsing, supports the following formats:
/// <list type="bullet">
/// <item><description><c>#ARGB</c> expands to <c>AARRGGBB</c></description></item>
/// <item><description><c>#AARRGGBB</c> used as-is</description></item>
/// </list>
/// </para>
/// When formatting, outputs an 8-digit hex string in <c>AARRGGBB</c> order.
/// </summary>
Argb
}

48
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,11 +178,11 @@ 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);
return new(xyzr * wxyz);
return new CieXyz(xyzr * wxyz);
}
/// <inheritdoc/>

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

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabCieLab
/// <summary>
/// Allows conversion between two color profiles based on the CIE Lab color space.
/// </summary>
public static class ColorProfileConverterExtensionsCieLabCieLab
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -29,10 +50,30 @@ internal static class ColorProfileConverterExtensionsCieLabCieLab
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabCieXyz
/// <summary>
/// Allows conversion between two color profiles based on the CIE Lab and CIE XYZ color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieLabCieXyz
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -28,10 +49,30 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabRgb
/// <summary>
/// Allows conversion between two color profiles based on the CIE Lab and RGB color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieLabRgb
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -29,10 +50,30 @@ internal static class ColorProfileConverterExtensionsCieLabRgb
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzCieLab
/// <summary>
/// Allows conversion between two color profiles based on the CIE XYZ and CIE Lab color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieXyzCieLab
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -28,10 +49,30 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzCieXyz
/// <summary>
/// Allows conversion between two color profiles based on the CIE XYZ color space.
/// </summary>
public static class ColorProfileConverterExtensionsCieXyzCieXyz
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -25,10 +46,30 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz
return TTo.FromProfileConnectingSpace(options, in pcsFrom);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzRgb
/// <summary>
/// Allows conversion between two color profiles based on the CIE XYZ and RGB color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieXyzRgb
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -28,10 +49,30 @@ internal static class ColorProfileConverterExtensionsCieXyzRgb
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

772
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -0,0 +1,772 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsIcc
{
private static readonly float[] PcsV2FromBlackPointScale =
[0.9965153F, 0.9965269F, 0.9965208F, 1F,
0.9965153F, 0.9965269F, 0.9965208F, 1F,
0.9965153F, 0.9965269F, 0.9965208F, 1F,
0.9965153F, 0.9965269F, 0.9965208F, 1F];
private static readonly float[] PcsV2FromBlackPointOffset =
[0.00336F, 0.0034731F, 0.00287F, 0F,
0.00336F, 0.0034731F, 0.00287F, 0F,
0.00336F, 0.0034731F, 0.00287F, 0F,
0.00336F, 0.0034731F, 0.00287F, 0F];
private static readonly float[] PcsV2ToBlackPointScale =
[1.0034969F, 1.0034852F, 1.0034913F, 1F,
1.0034969F, 1.0034852F, 1.0034913F, 1F,
1.0034969F, 1.0034852F, 1.0034913F, 1F,
1.0034969F, 1.0034852F, 1.0034913F, 1F];
private static readonly float[] PcsV2ToBlackPointOffset =
[0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F];
/// <summary>
/// Converts a color value from one ICC color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion using ICC profiles, ensuring accurate color mapping
/// between different color spaces. Both the source and target ICC profiles must be provided in the converter's
/// options. The method supports perceptual adjustments when required by the profiles.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">The color value to convert, defined in the source color profile.</param>
/// <returns>
/// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if either the source or target ICC profile is missing from the converter options.
/// </exception>
internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
{
// TODO: Validation of ICC Profiles against color profile. Is this possible?
if (converter.Options.SourceIccProfile is null)
{
throw new InvalidOperationException("Source ICC profile is missing.");
}
if (converter.Options.TargetIccProfile is null)
{
throw new InvalidOperationException("Target ICC profile is missing.");
}
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = KnownIlluminants.D50Icc,
TargetWhitePoint = KnownIlluminants.D50Icc
});
// Normalize the source, then convert to the PCS space.
Vector4 sourcePcs = sourceParams.Converter.Calculate(source.ToScaledVector4());
// If both profiles need PCS adjustment, they both share the same unadjusted PCS space
// cancelling out the need to make the adjustment
// except if using TRC transforms, which always requires perceptual handling
// TODO: this does not include adjustment for absolute intent, which would double existing complexity, suggest throwing exception and addressing in future update
bool anyProfileNeedsPerceptualAdjustment = sourceParams.HasNoPerceptualHandling || targetParams.HasNoPerceptualHandling;
bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling;
Vector4 targetPcs = anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment
? GetTargetPcsWithPerceptualAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter)
: GetTargetPcsWithoutAdjustment(sourcePcs, sourceParams, targetParams, pcsConverter);
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
}
/// <summary>
/// Converts a span of color values from a source color profile to a destination color profile using ICC profiles.
/// </summary>
/// <remarks>
/// This method performs color conversion by transforming the input values through the Profile
/// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required
/// by the profiles. The method does not support absolute colorimetric intent and will not perform such
/// conversions.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter that provides conversion options and ICC profiles.</param>
/// <param name="source">
/// A read-only span containing the source color values to convert. The values must conform to the source color
/// profile.
/// </param>
/// <param name="destination">
/// A span to receive the converted color values in the destination color profile. Must be at least as large as the
/// source span.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the source or target ICC profile is missing from the converter options.
/// </exception>
internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
{
// TODO: Validation of ICC Profiles against color profile. Is this possible?
if (converter.Options.SourceIccProfile is null)
{
throw new InvalidOperationException("Source ICC profile is missing.");
}
if (converter.Options.TargetIccProfile is null)
{
throw new InvalidOperationException("Target ICC profile is missing.");
}
Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination));
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = KnownIlluminants.D50Icc,
TargetWhitePoint = KnownIlluminants.D50Icc
});
using IMemoryOwner<Vector4> pcsBuffer = converter.Options.MemoryAllocator.Allocate<Vector4>(source.Length);
Span<Vector4> pcs = pcsBuffer.GetSpan();
// Normalize the source, then convert to the PCS space.
TFrom.ToScaledVector4(source, pcs);
sourceParams.Converter.Calculate(pcs, pcs);
// If both profiles need PCS adjustment, they both share the same unadjusted PCS space
// cancelling out the need to make the adjustment
// except if using TRC transforms, which always requires perceptual handling
// TODO: this does not include adjustment for absolute intent, which would double existing complexity, suggest throwing exception and addressing in future update
bool anyProfileNeedsPerceptualAdjustment = sourceParams.HasNoPerceptualHandling || targetParams.HasNoPerceptualHandling;
bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling;
if (anyProfileNeedsPerceptualAdjustment || oneProfileHasV2PerceptualAdjustment)
{
GetTargetPcsWithPerceptualAdjustment(pcs, sourceParams, targetParams, pcsConverter);
}
else
{
GetTargetPcsWithoutAdjustment(pcs, sourceParams, targetParams, pcsConverter);
}
// Convert to the target space.
targetParams.Converter.Calculate(pcs, pcs);
TTo.FromScaledVector4(pcs, destination);
}
private static Vector4 GetTargetPcsWithoutAdjustment(
Vector4 sourcePcs,
ConversionParams sourceParams,
ConversionParams targetParams,
ColorProfileConverter pcsConverter)
{
// Profile connecting spaces can only be Lab, XYZ.
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so ensure that Lab is using the correct encoding when a 16-bit LUT is used
switch (sourceParams.PcsType)
{
// Convert from Lab to XYZ.
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieXyz:
{
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
CieLab lab = CieLab.FromScaledVector4(sourcePcs);
CieXyz xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
return xyz.ToScaledVector4();
}
// Convert from XYZ to Lab.
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab:
{
CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs);
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
Vector4 targetPcs = lab.ToScaledVector4();
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
}
// Convert from XYZ to XYZ.
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz:
{
CieXyz xyz = CieXyz.FromScaledVector4(sourcePcs);
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
return targetXyz.ToScaledVector4();
}
// Convert from Lab to Lab.
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab:
{
// if both source and target LUT use same v2 LAB encoding, no need to correct them
if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry)
{
CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs);
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in sourceLab);
return targetLab.ToScaledVector4();
}
else
{
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
CieLab sourceLab = CieLab.FromScaledVector4(sourcePcs);
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in sourceLab);
Vector4 targetPcs = targetLab.ToScaledVector4();
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
}
}
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported");
}
}
private static void GetTargetPcsWithoutAdjustment(
Span<Vector4> pcs,
ConversionParams sourceParams,
ConversionParams targetParams,
ColorProfileConverter pcsConverter)
{
// Profile connecting spaces can only be Lab, XYZ.
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so ensure that Lab is using the correct encoding when a 16-bit LUT is used
switch (sourceParams.PcsType)
{
// Convert from Lab to XYZ.
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieXyz:
{
if (sourceParams.Is16BitLutEntry)
{
LabV2ToLab(pcs, pcs);
}
using IMemoryOwner<CieLab> pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length);
Span<CieLab> pcsFrom = pcsFromBuffer.GetSpan();
using IMemoryOwner<CieXyz> pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length);
Span<CieXyz> pcsTo = pcsToBuffer.GetSpan();
CieLab.FromScaledVector4(pcs, pcsFrom);
pcsConverter.Convert<CieLab, CieXyz>(pcsFrom, pcsTo);
CieXyz.ToScaledVector4(pcsTo, pcs);
break;
}
// Convert from XYZ to Lab.
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieLab:
{
using IMemoryOwner<CieXyz> pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length);
Span<CieXyz> pcsFrom = pcsFromBuffer.GetSpan();
using IMemoryOwner<CieLab> pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length);
Span<CieLab> pcsTo = pcsToBuffer.GetSpan();
CieXyz.FromScaledVector4(pcs, pcsFrom);
pcsConverter.Convert<CieXyz, CieLab>(pcsFrom, pcsTo);
CieLab.ToScaledVector4(pcsTo, pcs);
if (targetParams.Is16BitLutEntry)
{
LabToLabV2(pcs, pcs);
}
break;
}
// Convert from XYZ to XYZ.
case IccColorSpaceType.CieXyz when targetParams.PcsType is IccColorSpaceType.CieXyz:
{
using IMemoryOwner<CieXyz> pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length);
Span<CieXyz> pcsFromTo = pcsFromToBuffer.GetSpan();
CieXyz.FromScaledVector4(pcs, pcsFromTo);
pcsConverter.Convert<CieXyz, CieXyz>(pcsFromTo, pcsFromTo);
CieXyz.ToScaledVector4(pcsFromTo, pcs);
break;
}
// Convert from Lab to Lab.
case IccColorSpaceType.CieLab when targetParams.PcsType is IccColorSpaceType.CieLab:
{
using IMemoryOwner<CieLab> pcsFromToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length);
Span<CieLab> pcsFromTo = pcsFromToBuffer.GetSpan();
// if both source and target LUT use same v2 LAB encoding, no need to correct them
if (sourceParams.Is16BitLutEntry && targetParams.Is16BitLutEntry)
{
CieLab.FromScaledVector4(pcs, pcsFromTo);
pcsConverter.Convert<CieLab, CieLab>(pcsFromTo, pcsFromTo);
CieLab.ToScaledVector4(pcsFromTo, pcs);
}
else
{
if (sourceParams.Is16BitLutEntry)
{
LabV2ToLab(pcs, pcs);
}
CieLab.FromScaledVector4(pcs, pcsFromTo);
pcsConverter.Convert<CieLab, CieLab>(pcsFromTo, pcsFromTo);
CieLab.ToScaledVector4(pcsFromTo, pcs);
if (targetParams.Is16BitLutEntry)
{
LabToLabV2(pcs, pcs);
}
}
break;
}
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} to target PCS {targetParams.PcsType} is not supported");
}
}
/// <summary>
/// Effectively this is <see cref="GetTargetPcsWithoutAdjustment(Vector4, ConversionParams, ConversionParams, ColorProfileConverter)"/> with an extra step in the middle.
/// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles.
/// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions.
/// Not required if both spaces need V2 correction, since they both have the same understanding of the PCS.
/// Not compatible with PCS adjustment for absolute intent.
/// </summary>
/// <param name="sourcePcs">The source PCS values.</param>
/// <param name="sourceParams">The source profile parameters.</param>
/// <param name="targetParams">The target profile parameters.</param>
/// <param name="pcsConverter">The converter to use for the PCS adjustments.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the source or target PCS is not supported.</exception>
private static Vector4 GetTargetPcsWithPerceptualAdjustment(
Vector4 sourcePcs,
ConversionParams sourceParams,
ConversionParams targetParams,
ColorProfileConverter pcsConverter)
{
// all conversions are funneled through XYZ in case PCS adjustments need to be made
CieXyz xyz;
switch (sourceParams.PcsType)
{
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so convert Lab to modern v4 encoding when returned from a 16-bit LUT
case IccColorSpaceType.CieLab:
sourcePcs = sourceParams.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
CieLab lab = CieLab.FromScaledVector4(sourcePcs);
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
break;
case IccColorSpaceType.CieXyz:
xyz = CieXyz.FromScaledVector4(sourcePcs);
break;
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported");
}
bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling;
// when converting from device to PCS with v2 perceptual intent
// the black point needs to be adjusted to v4 after converting the PCS values
if (sourceParams.HasNoPerceptualHandling ||
(oneProfileHasV2PerceptualAdjustment && sourceParams.HasV2PerceptualHandling))
{
Vector3 vector = xyz.ToVector3();
// when using LAB PCS, negative values are clipped before PCS adjustment (in DemoIccMAX)
if (sourceParams.PcsType == IccColorSpaceType.CieLab)
{
vector = Vector3.Max(vector, Vector3.Zero);
}
xyz = new CieXyz(AdjustPcsFromV2BlackPoint(vector));
}
// when converting from PCS to device with v2 perceptual intent
// the black point needs to be adjusted to v2 before converting the PCS values
if (targetParams.HasNoPerceptualHandling ||
(oneProfileHasV2PerceptualAdjustment && targetParams.HasV2PerceptualHandling))
{
Vector3 vector = AdjustPcsToV2BlackPoint(xyz.AsVector3Unsafe());
// when using XYZ PCS, negative values are clipped after PCS adjustment (in DemoIccMAX)
if (targetParams.PcsType == IccColorSpaceType.CieXyz)
{
vector = Vector3.Max(vector, Vector3.Zero);
}
xyz = new CieXyz(vector);
}
switch (targetParams.PcsType)
{
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so convert Lab back to legacy encoding before using in a 16-bit LUT
case IccColorSpaceType.CieLab:
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
Vector4 targetPcs = lab.ToScaledVector4();
return targetParams.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
case IccColorSpaceType.CieXyz:
return xyz.ToScaledVector4();
default:
throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported");
}
}
/// <summary>
/// Effectively this is <see cref="GetTargetPcsWithoutAdjustment(Span{Vector4}, ConversionParams, ConversionParams, ColorProfileConverter)"/> with an extra step in the middle.
/// It adjusts PCS by compensating for the black point used for perceptual intent in v2 profiles.
/// The adjustment needs to be performed in XYZ space, potentially an overhead of 2 more conversions.
/// Not required if both spaces need V2 correction, since they both have the same understanding of the PCS.
/// Not compatible with PCS adjustment for absolute intent.
/// </summary>
/// <param name="pcs">The PCS values from the source.</param>
/// <param name="sourceParams">The source profile parameters.</param>
/// <param name="targetParams">The target profile parameters.</param>
/// <param name="pcsConverter">The converter to use for the PCS adjustments.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the source or target PCS is not supported.</exception>
private static void GetTargetPcsWithPerceptualAdjustment(
Span<Vector4> pcs,
ConversionParams sourceParams,
ConversionParams targetParams,
ColorProfileConverter pcsConverter)
{
// All conversions are funneled through XYZ in case PCS adjustments need to be made
using IMemoryOwner<CieXyz> xyzBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieXyz>(pcs.Length);
Span<CieXyz> xyz = xyzBuffer.GetSpan();
switch (sourceParams.PcsType)
{
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so convert Lab to modern v4 encoding when returned from a 16-bit LUT
case IccColorSpaceType.CieLab:
{
if (sourceParams.Is16BitLutEntry)
{
LabV2ToLab(pcs, pcs);
}
using IMemoryOwner<CieLab> pcsFromBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length);
Span<CieLab> pcsFrom = pcsFromBuffer.GetSpan();
CieLab.FromScaledVector4(pcs, pcsFrom);
pcsConverter.Convert<CieLab, CieXyz>(pcsFrom, xyz);
break;
}
case IccColorSpaceType.CieXyz:
CieXyz.FromScaledVector4(pcs, xyz);
break;
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourceParams.PcsType} is not supported");
}
bool oneProfileHasV2PerceptualAdjustment = sourceParams.HasV2PerceptualHandling ^ targetParams.HasV2PerceptualHandling;
using IMemoryOwner<Vector4> vectorBuffer = pcsConverter.Options.MemoryAllocator.Allocate<Vector4>(pcs.Length);
Span<Vector4> vector = vectorBuffer.GetSpan();
// When converting from device to PCS with v2 perceptual intent
// the black point needs to be adjusted to v4 after converting the PCS values
if (sourceParams.HasNoPerceptualHandling ||
(oneProfileHasV2PerceptualAdjustment && sourceParams.HasV2PerceptualHandling))
{
CieXyz.ToVector4(xyz, vector);
// When using LAB PCS, negative values are clipped before PCS adjustment (in DemoIccMAX)
if (sourceParams.PcsType == IccColorSpaceType.CieLab)
{
ClipNegative(vector);
}
AdjustPcsFromV2BlackPoint(vector, vector);
CieXyz.FromVector4(vector, xyz);
}
// When converting from PCS to device with v2 perceptual intent
// the black point needs to be adjusted to v2 before converting the PCS values
if (targetParams.HasNoPerceptualHandling ||
(oneProfileHasV2PerceptualAdjustment && targetParams.HasV2PerceptualHandling))
{
CieXyz.ToVector4(xyz, vector);
AdjustPcsToV2BlackPoint(vector, vector);
// When using XYZ PCS, negative values are clipped after PCS adjustment (in DemoIccMAX)
if (targetParams.PcsType == IccColorSpaceType.CieXyz)
{
ClipNegative(vector);
}
CieXyz.FromVector4(vector, xyz);
}
switch (targetParams.PcsType)
{
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so convert Lab back to legacy encoding before using in a 16-bit LUT
case IccColorSpaceType.CieLab:
{
using IMemoryOwner<CieLab> pcsToBuffer = pcsConverter.Options.MemoryAllocator.Allocate<CieLab>(pcs.Length);
Span<CieLab> pcsTo = pcsToBuffer.GetSpan();
pcsConverter.Convert<CieXyz, CieLab>(xyz, pcsTo);
CieLab.ToScaledVector4(pcsTo, pcs);
if (targetParams.Is16BitLutEntry)
{
LabToLabV2(pcs, pcs);
}
break;
}
case IccColorSpaceType.CieXyz:
CieXyz.ToScaledVector4(xyz, pcs);
break;
default:
throw new ArgumentOutOfRangeException($"Target PCS {targetParams.PcsType} is not supported");
}
}
// as per DemoIccMAX icPerceptual values in IccCmm.h
// refBlack = 0.00336F, 0.0034731F, 0.00287F
// refWhite = 0.9642F, 1.0000F, 0.8249F
// scale = 1 - (refBlack / refWhite)
// offset = refBlack
private static Vector3 AdjustPcsFromV2BlackPoint(Vector3 xyz)
=> (xyz * new Vector3(0.9965153F, 0.9965269F, 0.9965208F)) + new Vector3(0.00336F, 0.0034731F, 0.00287F);
// as per DemoIccMAX icPerceptual values in IccCmm.h
// refBlack = 0.00336F, 0.0034731F, 0.00287F
// refWhite = 0.9642F, 1.0000F, 0.8249F
// scale = 1 / (1 - (refBlack / refWhite))
// offset = -refBlack * scale
private static Vector3 AdjustPcsToV2BlackPoint(Vector3 xyz)
=> (xyz * new Vector3(1.0034969F, 1.0034852F, 1.0034913F)) - new Vector3(0.0033717495F, 0.0034852044F, 0.0028800198F);
private static void AdjustPcsFromV2BlackPoint(Span<Vector4> source, Span<Vector4> destination)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported &&
Vector<float>.Count <= Vector512<float>.Count &&
source.Length * 4 >= Vector<float>.Count)
{
// TODO: Check our constants. They may require scaling.
Vector<float> vScale = new(PcsV2FromBlackPointScale.AsSpan()[..Vector<float>.Count]);
Vector<float> vOffset = new(PcsV2FromBlackPointOffset.AsSpan()[..Vector<float>.Count]);
// SIMD loop
int i = 0;
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale and offset the vector
v *= vScale;
v += vOffset;
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
Vector4 s = source[i];
s *= new Vector4(0.9965153F, 0.9965269F, 0.9965208F, 1F);
s += new Vector4(0.00336F, 0.0034731F, 0.00287F, 0F);
destination[i] = s;
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
Vector4 s = source[i];
s *= new Vector4(0.9965153F, 0.9965269F, 0.9965208F, 1F);
s += new Vector4(0.00336F, 0.0034731F, 0.00287F, 0F);
destination[i] = s;
}
}
}
private static void AdjustPcsToV2BlackPoint(Span<Vector4> source, Span<Vector4> destination)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported &&
Vector<float>.Count <= Vector512<float>.Count &&
source.Length * 4 >= Vector<float>.Count)
{
// TODO: Check our constants. They may require scaling.
Vector<float> vScale = new(PcsV2ToBlackPointScale.AsSpan()[..Vector<float>.Count]);
Vector<float> vOffset = new(PcsV2ToBlackPointOffset.AsSpan()[..Vector<float>.Count]);
// SIMD loop
int i = 0;
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale and offset the vector
v *= vScale;
v -= vOffset;
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
Vector4 s = source[i];
s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F);
s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F);
destination[i] = s;
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
Vector4 s = source[i];
s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F);
s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F);
destination[i] = s;
}
}
}
private static void ClipNegative(Span<Vector4> source)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported && Vector<float>.Count >= source.Length * 4)
{
// SIMD loop
int i = 0;
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
v = Vector.Max(v, Vector<float>.Zero);
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref source[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
ref Vector4 s = ref source[i];
s = Vector4.Max(s, Vector4.Zero);
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
ref Vector4 s = ref source[i];
s = Vector4.Max(s, Vector4.Zero);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 LabToLabV2(Vector4 input)
=> input * 65280F / 65535F;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 LabV2ToLab(Vector4 input)
=> input * 65535F / 65280F;
private static void LabToLabV2(Span<Vector4> source, Span<Vector4> destination)
=> LabToLab(source, destination, 65280F / 65535F);
private static void LabV2ToLab(Span<Vector4> source, Span<Vector4> destination)
=> LabToLab(source, destination, 65535F / 65280F);
private static void LabToLab(Span<Vector4> source, Span<Vector4> destination, [ConstantExpected] float scale)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported)
{
Vector<float> vScale = new(scale);
int i = 0;
// SIMD loop
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale the vector
v *= vScale;
// Write the scaled vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
destination[i] = source[i] * scale;
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
destination[i] = source[i] * scale;
}
}
}
private class ConversionParams
{
private readonly IccProfile profile;
internal ConversionParams(IccProfile profile, bool toPcs)
{
this.profile = profile;
this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile);
}
internal IccConverterBase Converter { get; }
internal IccProfileHeader Header => this.profile.Header;
internal IccRenderingIntent Intent => this.Header.RenderingIntent;
internal IccColorSpaceType PcsType => this.Header.ProfileConnectionSpace;
internal IccVersion Version => this.Header.Version;
internal bool HasV2PerceptualHandling => this.Intent == IccRenderingIntent.Perceptual && this.Version.Major == 2;
internal bool HasNoPerceptualHandling => this.Intent == IccRenderingIntent.Perceptual && this.Converter.IsTrc;
internal bool Is16BitLutEntry => this.Converter.Is16BitLutEntry;
}
}

192
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs

@ -0,0 +1,192 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsPixelCompatible
{
/// <summary>
/// Converts the pixel data of the specified image from the source color profile to the target color profile using
/// the provided color profile converter.
/// </summary>
/// <remarks>
/// This method modifies the source image in place by converting its pixel data according to the
/// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible;
/// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC
/// profiles are set on the converter before calling this method.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">
/// The image whose pixel data will be converted. The conversion is performed in place, modifying the original
/// image.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the converter's source or target ICC profile is not specified.
/// </exception>
public static void Convert<TPixel>(this ColorProfileConverter converter, Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
// These checks actually take place within the converter, but we want to fail fast here.
// Note. we do not check to see whether the profiles themselves are RGB compatible,
// if they are not, then the converter will simply produce incorrect results.
if (converter.Options.SourceIccProfile is null)
{
throw new InvalidOperationException("Source ICC profile is missing.");
}
if (converter.Options.TargetIccProfile is null)
{
throw new InvalidOperationException("Target ICC profile is missing.");
}
// Process the rows in parallel chunks, the converter itself is thread safe.
source.Mutate(o => o.ProcessPixelRowsAsVector4(
row =>
{
// Gather and convert the pixels in the row to Rgb.
using IMemoryOwner<Rgb> rgbBuffer = converter.Options.MemoryAllocator.Allocate<Rgb>(row.Length);
Span<Rgb> rgbSpan = rgbBuffer.Memory.Span;
Rgb.FromScaledVector4(row, rgbSpan);
// Perform the actual color conversion.
converter.ConvertUsingIccProfile<Rgb, Rgb>(rgbSpan, rgbSpan);
// Copy the converted Rgb pixels back to the row as TPixel.
// Important: Preserve alpha from the existing row Vector4 values.
// We merge RGB from rgbSpan into row, leaving W untouched.
ref float srcRgb = ref Unsafe.As<Rgb, float>(ref MemoryMarshal.GetReference(rgbSpan));
ref float dstRow = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(row));
int count = rgbSpan.Length;
int i = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Vector512<float> ReadVector512(ref float f)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
return Unsafe.ReadUnaligned<Vector512<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void WriteVector512(ref float f, Vector512<float> v)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
Unsafe.WriteUnaligned(ref b, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Vector256<float> ReadVector256(ref float f)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
return Unsafe.ReadUnaligned<Vector256<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void WriteVector256(ref float f, Vector256<float> v)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
Unsafe.WriteUnaligned(ref b, v);
}
if (Avx512F.IsSupported)
{
// 4 pixels per iteration.
//
// Source layout (Rgb float stream, 12 floats):
// [r0 g0 b0 r1 g1 b1 r2 g2 b2 r3 g3 b3]
//
// Destination layout (row Vector4 float stream, 16 floats):
// [r0 g0 b0 a0 r1 g1 b1 a1 r2 g2 b2 a2 r3 g3 b3 a3]
//
// We use an overlapped load (16 floats) from the 3-float stride source.
// The permute selects the RGB we need and inserts placeholders for alpha lanes.
//
// Then we blend RGB lanes into the existing destination, preserving alpha lanes.
Vector512<int> rgbPerm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0);
// BlendVariable selects from the second operand where the sign bit of the mask lane is set.
// We want to overwrite lanes 0,1,2 then 4,5,6 then 8,9,10 then 12,13,14, and preserve lanes 3,7,11,15 (alpha).
Vector512<float> rgbSelect = Vector512.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F);
int quads = count >> 2;
int simdQuads = quads - 1; // Leave the last quad for the scalar tail to avoid the final overlapped load reading past the end.
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> dst = ReadVector512(ref dstRow);
Vector512<float> src = ReadVector512(ref srcRgb);
Vector512<float> rgbx = Avx512F.PermuteVar16x32(src, rgbPerm);
Vector512<float> merged = Avx512F.BlendVariable(dst, rgbx, rgbSelect);
WriteVector512(ref dstRow, merged);
// Advance input by 4 pixels (4 * 3 = 12 floats)
srcRgb = ref Unsafe.Add(ref srcRgb, 12);
// Advance output by 4 pixels (4 * 4 = 16 floats)
dstRow = ref Unsafe.Add(ref dstRow, 16);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration.
//
// Same idea as AVX-512, but on 256-bit vectors.
// We permute packed RGB into rgbx layout and blend into the existing destination,
// preserving alpha lanes.
Vector256<int> rgbPerm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0);
Vector256<float> rgbSelect = Vector256.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F);
int pairs = count >> 1;
int simdPairs = pairs - 1; // Leave the last pair for the scalar tail to avoid the final overlapped load reading past the end.
for (int p = 0; p < simdPairs; p++)
{
Vector256<float> dst = ReadVector256(ref dstRow);
Vector256<float> src = ReadVector256(ref srcRgb);
Vector256<float> rgbx = Avx2.PermuteVar8x32(src, rgbPerm);
Vector256<float> merged = Avx.BlendVariable(dst, rgbx, rgbSelect);
WriteVector256(ref dstRow, merged);
// Advance input by 2 pixels (2 * 3 = 6 floats)
srcRgb = ref Unsafe.Add(ref srcRgb, 6);
// Advance output by 2 pixels (2 * 4 = 8 floats)
dstRow = ref Unsafe.Add(ref dstRow, 8);
i += 2;
}
}
// Scalar tail.
// Handles:
// - the last skipped SIMD block (quad or pair)
// - any remainder
//
// Preserve alpha by writing Vector3 into the Vector4 storage.
ref Vector4 rowRef = ref MemoryMarshal.GetReference(row);
for (; i < count; i++)
{
Vector3 rgb = rgbSpan[i].AsVector3Unsafe();
Unsafe.As<Vector4, Vector3>(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb;
}
},
PixelConversionModifiers.Scale));
}
}

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbCieLab
/// <summary>
/// Allows conversion between two color profiles based on the RGB and CIE Lab color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsRgbCieLab
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -29,10 +50,30 @@ internal static class ColorProfileConverterExtensionsRgbCieLab
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbCieXyz
/// <summary>
/// Allows conversion between two color profiles based on the RGB and CIE XYZ color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsRgbCieXyz
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -28,10 +49,30 @@ internal static class ColorProfileConverterExtensionsRgbCieXyz
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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.

43
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

@ -6,12 +6,33 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbRgb
/// <summary>
/// Allows conversion between two color profiles based on the RGB color space.
/// </summary>
public static class ColorProfileConverterExtensionsRgbRgb
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
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
@ -29,10 +50,30 @@ internal static class ColorProfileConverterExtensionsRgbRgb
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
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 Vector4(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)
{
}
}

8
src/ImageSharp/ColorProfiles/KnownIlluminants.cs

@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.ColorProfiles;
/// </summary>
/// <remarks>
/// Coefficients taken from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
/// and https://color.org/specification/ICC.1-2022-05.pdf
/// <br />
/// Descriptions taken from: http://en.wikipedia.org/wiki/Standard_illuminant
/// </remarks>
@ -30,10 +31,15 @@ public static class KnownIlluminants
public static CieXyz C { get; } = new(0.98074F, 1F, 1.18232F);
/// <summary>
/// Gets the Horizon Light. ICC profile PCS illuminant.
/// Gets the Horizon Light.
/// </summary>
public static CieXyz D50 { get; } = new(0.96422F, 1F, 0.82521F);
/// <summary>
/// Gets the D50 illuminant used in the ICC profile specification.
/// </summary>
public static CieXyz D50Icc { get; } = new(0.9642F, 1F, 0.8249F);
/// <summary>
/// Gets the Mid-morning / Mid-afternoon Daylight illuminant.
/// </summary>

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)

278
src/ImageSharp/ColorProfiles/Rgb.cs

@ -4,6 +4,8 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.ColorProfiles;
@ -81,11 +83,196 @@ 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));
int length = source.Length;
if (length == 0)
{
return;
}
ref Rgb srcRgb = ref MemoryMarshal.GetReference(source);
ref Vector4 dstV4 = ref MemoryMarshal.GetReference(destination);
// Float streams:
// src: r0 g0 b0 r1 g1 b1 ...
// dst: r0 g0 b0 a0 r1 g1 b1 a1 ...
ref float src = ref Unsafe.As<Rgb, float>(ref srcRgb);
ref float dst = ref Unsafe.As<Vector4, float>(ref dstV4);
int i = 0;
if (Avx512F.IsSupported)
{
// 4 pixels per iteration. Using overlapped 16-float loads.
Vector512<int> perm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0);
Vector512<float> ones = Vector512.Create(1F);
// BlendVariable selects from 'ones' where the sign-bit of mask lane is set.
// Using -0f sets only the sign bit, producing an efficient "select lane" mask.
Vector512<float> alphaSelect = Vector512.Create(0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F);
int quads = length >> 2;
// Leave the last quad (4 pixels) for the scalar tail.
int simdQuads = quads - 1;
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> v = ReadVector512(ref src);
Vector512<float> rgbx = Avx512F.PermuteVar16x32(v, perm);
Vector512<float> rgba = Avx512F.BlendVariable(rgbx, ones, alphaSelect);
WriteVector512(ref dst, rgba);
src = ref Unsafe.Add(ref src, 12);
dst = ref Unsafe.Add(ref dst, 16);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration. Using overlapped 8-float loads.
Vector256<int> perm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0);
Vector256<float> ones = Vector256.Create(1F);
// vblendps mask: bit i selects lane i from 'ones' when set.
// We want lanes 3 and 7 -> 0b10001000 = 0x88.
const byte alphaMask = 0x88;
int pairs = length >> 1;
// Leave the last pair (2 pixels) for the scalar tail.
int simdPairs = pairs - 1;
for (int p = 0; p < simdPairs; p++)
{
Vector256<float> v = ReadVector256(ref src);
Vector256<float> rgbx = Avx2.PermuteVar8x32(v, perm);
Vector256<float> rgba = Avx.Blend(rgbx, ones, alphaMask);
WriteVector256(ref dst, rgba);
src = ref Unsafe.Add(ref src, 6);
dst = ref Unsafe.Add(ref dst, 8);
i += 2;
}
}
// Tail (and non-AVX paths)
for (; i < length; i++)
{
Unsafe.Add(ref dstV4, i) = Unsafe.Add(ref srcRgb, i).ToScaledVector4();
}
}
/// <inheritdoc/>
public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<Rgb> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
int length = source.Length;
if (length == 0)
{
return;
}
ref Vector4 srcV4 = ref MemoryMarshal.GetReference(source);
ref Rgb dstRgb = ref MemoryMarshal.GetReference(destination);
// Float streams:
// src: r0 g0 b0 a0 r1 g1 b1 a1 ...
// dst: r0 g0 b0 r1 g1 b1 ...
ref float src = ref Unsafe.As<Vector4, float>(ref srcV4);
ref float dst = ref Unsafe.As<Rgb, float>(ref dstRgb);
int i = 0;
if (Avx512F.IsSupported)
{
// 4 pixels per iteration. Using overlapped 16-float stores:
Vector512<int> idx = Vector512.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
// Number of 4-pixel groups in the input.
int quads = length >> 2;
// Leave the last quad (4 pixels) for the scalar tail.
int simdQuads = quads - 1;
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> v = ReadVector512(ref src);
Vector512<float> packed = Avx512F.PermuteVar16x32(v, idx);
WriteVector512(ref dst, packed);
src = ref Unsafe.Add(ref src, 16);
dst = ref Unsafe.Add(ref dst, 12);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration, using overlapped 8-float stores:
Vector256<int> idx = Vector256.Create(0, 1, 2, 4, 5, 6, 0, 0);
int pairs = length >> 1;
// Leave the last pair (2 pixels) for the scalar tail.
int simdPairs = pairs - 1;
int pairIndex = 0;
for (; pairIndex < simdPairs; pairIndex++)
{
Vector256<float> v = ReadVector256(ref src);
Vector256<float> packed = Avx2.PermuteVar8x32(v, idx);
WriteVector256(ref dst, packed);
src = ref Unsafe.Add(ref src, 8);
dst = ref Unsafe.Add(ref dst, 6);
i += 2;
}
}
// Tail (and non-AVX paths)
for (; i < length; i++)
{
Vector4 v = Unsafe.Add(ref srcV4, i);
Unsafe.Add(ref dstRgb, i) = FromScaledVector4(v);
}
}
/// <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 +285,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 +295,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 +306,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 +329,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 +338,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 +348,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 +369,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 +415,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
@ -266,4 +432,32 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
M44 = 1F
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ReadVector512(ref float src)
{
ref byte b = ref Unsafe.As<float, byte>(ref src);
return Unsafe.ReadUnaligned<Vector512<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ReadVector256(ref float src)
{
ref byte b = ref Unsafe.As<float, byte>(ref src);
return Unsafe.ReadUnaligned<Vector256<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteVector512(ref float dst, Vector512<float> value)
{
ref byte b = ref Unsafe.As<float, byte>(ref dst);
Unsafe.WriteUnaligned(ref b, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteVector256(ref float dst, Vector256<float> value)
{
ref byte b = ref Unsafe.As<float, byte>(ref dst);
Unsafe.WriteUnaligned(ref b, value);
}
}

14
src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs

@ -31,12 +31,12 @@ public static class VonKriesChromaticAdaptation
if (from.Equals(to))
{
return new(source.X, source.Y, source.Z);
return new CieXyz(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 Y(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Y> source, Span<Rgb> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: We can optimize this by using SIMD
for (int i = 0; i < source.Length; i++)
{
destination[i] = source[i].ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<Y> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: We can optimize this by using SIMD
for (int i = 0; i < source.Length; i++)
{
Rgb rgb = source[i];
destination[i] = FromProfileConnectingSpace(options, in rgb);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
=> this.L.GetHashCode();
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"Y({this.L:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is Y other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(Y other) => this.L == other.L;
}

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

8
src/ImageSharp/Common/Helpers/HexConverter.cs

@ -35,8 +35,8 @@ internal static class HexConverter
{
// Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.
// This doesn't actually allocate.
ReadOnlySpan<byte> charToHexLookup = new byte[]
{
ReadOnlySpan<byte> charToHexLookup =
[
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
@ -52,8 +52,8 @@ internal static class HexConverter
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 255
};
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
];
return (uint)c >= (uint)charToHexLookup.Length ? 0xFF : charToHexLookup[c];
}

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

@ -470,8 +470,8 @@ internal static class Numerics
where T : unmanaged
{
ref T sRef = ref MemoryMarshal.GetReference(span);
var vmin = new Vector<T>(min);
var vmax = new Vector<T>(max);
Vector<T> vmin = new(min);
Vector<T> vmax = new(max);
nint n = (nint)(uint)span.Length / Vector<T>.Count;
nint m = Modulo4(n);
@ -656,7 +656,7 @@ internal static class Numerics
return Sse.Shuffle(value.AsVector128(), value.AsVector128(), ShuffleAlphaControl).AsVector4();
}
return new(value.W);
return new Vector4(value.W);
}
/// <summary>
@ -726,12 +726,12 @@ internal static class Numerics
ref Vector128<float> vectors128Ref = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector128<float> vectors128End = ref Unsafe.Add(ref vectors128Ref, (uint)vectors.Length);
var v128_341 = Vector128.Create(341);
Vector128<int> v128_341 = Vector128.Create(341);
Vector128<int> v128_negativeZero = Vector128.Create(-0.0f).AsInt32();
Vector128<int> v128_one = Vector128.Create(1.0f).AsInt32();
var v128_13rd = Vector128.Create(1 / 3f);
var v128_23rds = Vector128.Create(2 / 3f);
Vector128<float> v128_13rd = Vector128.Create(1 / 3f);
Vector128<float> v128_23rds = Vector128.Create(2 / 3f);
while (Unsafe.IsAddressLessThan(ref vectors128Ref, ref vectors128End))
{
@ -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")]

2
src/ImageSharp/Common/Helpers/TolerantMath.cs

@ -20,7 +20,7 @@ internal readonly struct TolerantMath
/// It is a field so it can be passed as an 'in' parameter.
/// Does not necessarily fit all use cases!
/// </summary>
public static readonly TolerantMath Default = new TolerantMath(1e-8);
public static readonly TolerantMath Default = new(1e-8);
public TolerantMath(double epsilon)
{

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

File diff suppressed because it is too large

447
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,20 +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 multiplication and an addition of the <see cref="Vector256{Single}"/>.
/// </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(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 vector containing the results of multiplying and adding adjacent pairs of packed signed 16-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> MultiplyAddAdjacent(Vector256<short> left, Vector256<short> right)
{
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 Avx2.PackUnsignedSaturate(left, right);
}
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);
}
/// <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());
}
[DoesNotReturn]
private static void ThrowUnreachableException() => throw new UnreachableException();
/// <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()));
}
}

102
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.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 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>
@ -44,23 +27,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.
@ -71,15 +39,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>
@ -90,26 +57,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);
}
/// <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)
Vector512<float> sign = vector & Vector512.Create(-0.0f);
Vector512<float> val_2p23_f32 = sign | Vector512.Create(8388608.0f);
// 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);
val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32;
return Vector512.ConvertToInt32(val_2p23_f32 | sign);
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector512{Single}"/>.
/// </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(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> MultiplyAdd(
Vector512<float> va,
Vector512<float> vm0,
Vector512<float> vm1)
=> Avx512F.FusedMultiplyAdd(vm0, vm1, va);
[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 = [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;
}
<#+
}
}
#>

6
src/ImageSharp/Compression/Zlib/Adler32.cs

@ -32,11 +32,11 @@ internal static class Adler32
private const int BlockSize = 1 << 5;
// The C# compiler emits this as a compile-time constant embedded in the PE file.
private static ReadOnlySpan<byte> Tap1Tap2 => new byte[]
{
private static ReadOnlySpan<byte> Tap1Tap2 =>
[
32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1
16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2
};
];
/// <summary>
/// Calculates the Adler32 checksum with the bytes taken from the span.

10
src/ImageSharp/Compression/Zlib/DeflaterConstants.cs

@ -124,25 +124,25 @@ internal static class DeflaterConstants
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 };
public static int[] GOOD_LENGTH = [0, 4, 4, 4, 4, 8, 8, 8, 32, 32];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 };
public static int[] MAX_LAZY = [0, 4, 5, 6, 4, 16, 16, 32, 128, 258];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 };
public static int[] NICE_LENGTH = [0, 8, 16, 32, 16, 32, 128, 128, 258, 258];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 };
public static int[] MAX_CHAIN = [0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 };
public static int[] COMPR_FUNC = [0, 1, 1, 1, 1, 2, 2, 2, 2, 2];
}

36
src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs

@ -77,8 +77,8 @@ internal sealed unsafe class DeflaterHuffman : IDisposable
// See RFC 1951 3.2.6
// Literal codes
private static readonly short[] StaticLCodes = new short[]
{
private static readonly short[] StaticLCodes =
[
12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252,
2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242,
10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250,
@ -97,10 +97,10 @@ internal sealed unsafe class DeflaterHuffman : IDisposable
31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511,
0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36,
100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163
};
];
private static ReadOnlySpan<byte> StaticLLength => new byte[]
{
private static ReadOnlySpan<byte> StaticLLength =>
[
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
@ -119,34 +119,34 @@ internal sealed unsafe class DeflaterHuffman : IDisposable
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8
};
];
// Distance codes and lengths.
private static readonly short[] StaticDCodes = new short[]
{
private static readonly short[] StaticDCodes =
[
0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14,
30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23
};
];
private static ReadOnlySpan<byte> StaticDLength => new byte[]
{
private static ReadOnlySpan<byte> StaticDLength =>
[
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
};
];
#pragma warning restore SA1201 // Elements should appear in the correct order
/// <summary>
/// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes.
/// </summary>
private static ReadOnlySpan<byte> BitLengthOrder => new byte[]
{
private static ReadOnlySpan<byte> BitLengthOrder =>
[
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
};
];
private static ReadOnlySpan<byte> Bit4Reverse => new byte[]
{
private static ReadOnlySpan<byte> Bit4Reverse =>
[
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
};
];
/// <summary>
/// Gets the pending buffer to use.

2
src/ImageSharp/Configuration.cs

@ -122,7 +122,7 @@ public sealed class Configuration
/// <summary>
/// Gets or the <see cref="ImageFormatManager"/> that is currently in use.
/// </summary>
public ImageFormatManager ImageFormatsManager { get; private set; } = new ImageFormatManager();
public ImageFormatManager ImageFormatsManager { get; private set; } = new();
/// <summary>
/// Gets or sets the <see cref="Memory.MemoryAllocator"/> that is currently in use.

19
src/ImageSharp/Formats/AlphaAwareImageEncoder.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency.
/// </summary>
public abstract class AlphaAwareImageEncoder : ImageEncoder
{
/// <summary>
/// Gets or initializes the mode that determines how transparent pixels are handled during encoding.
/// This overrides any other settings that may affect the encoding of transparent pixels
/// including those passed via <see cref="QuantizerOptions"/>.
/// </summary>
public TransparentColorMode TransparentColorMode { get; init; }
}

8
src/ImageSharp/Formats/Bmp/BmpConstants.cs

@ -11,17 +11,17 @@ internal static class BmpConstants
/// <summary>
/// The list of mimetypes that equate to a bmp.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[]
{
public static readonly IEnumerable<string> MimeTypes =
[
"image/bmp",
"image/x-windows-bmp",
"image/x-win-bitmap"
};
];
/// <summary>
/// The list of file extensions that equate to a bmp.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "bm", "bmp", "dip" };
public static readonly IEnumerable<string> FileExtensions = ["bm", "bmp", "dip"];
/// <summary>
/// Valid magic bytes markers identifying a Bitmap file.

2
src/ImageSharp/Formats/Bmp/BmpDecoder.cs

@ -25,7 +25,7 @@ public sealed class BmpDecoder : SpecializedImageDecoder<BmpDecoderOptions>
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
return new BmpDecoderCore(new() { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
return new BmpDecoderCore(new BmpDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
}
/// <inheritdoc/>

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

@ -220,7 +220,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ReadImageHeaders(stream, out _, out _);
return new ImageInfo(new(this.infoHeader.Width, this.infoHeader.Height), this.metadata);
return new ImageInfo(new Size(this.infoHeader.Width, this.infoHeader.Height), this.metadata);
}
/// <summary>
@ -1270,7 +1270,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
byte g = (byte)((temp & greenMask) >> rightShiftGreenMask);
byte b = (byte)((temp & blueMask) >> rightShiftBlueMask);
byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : byte.MaxValue;
pixelRow[x] = TPixel.FromRgba32(new(r, g, b, a));
pixelRow[x] = TPixel.FromRgba32(new Rgba32(r, g, b, a));
}
offset += 4;
@ -1457,7 +1457,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.bmpMetadata.InfoHeaderType = infoHeaderType;
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
this.Dimensions = new(this.infoHeader.Width, this.infoHeader.Height);
this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
}
/// <summary>
@ -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;
}

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

@ -91,6 +91,11 @@ internal sealed class BmpEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The transparent color mode.
/// </summary>
private readonly TransparentColorMode transparentColorMode;
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
private readonly bool processedAlphaMask;
@ -113,6 +118,7 @@ internal sealed class BmpEncoderCore
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.processedAlphaMask = encoder.ProcessedAlphaMask;
this.skipFileHeader = encoder.SkipFileHeader;
@ -181,14 +187,14 @@ internal sealed class BmpEncoderCore
Span<byte> buffer = stackalloc byte[infoHeaderSize];
// for ico/cur encoder.
// For ico/cur encoder.
if (!this.skipFileHeader)
{
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
}
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image);
this.WriteImage(configuration, stream, image, cancellationToken);
WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush();
@ -345,44 +351,68 @@ internal sealed class BmpEncoderCore
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WriteImage<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WriteImage<TPixel>(
Configuration configuration,
Stream stream,
Image<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel)
ImageFrame<TPixel>? clonedFrame = null;
try
{
case BmpBitsPerPixel.Bit32:
this.Write32BitPixelData(configuration, stream, pixels);
break;
// 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.ReplaceTransparentPixels(clonedFrame);
}
case BmpBitsPerPixel.Bit24:
this.Write24BitPixelData(configuration, stream, pixels);
break;
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
Buffer2D<TPixel> pixels = encodingFrame.PixelBuffer;
case BmpBitsPerPixel.Bit16:
this.Write16BitPixelData(configuration, stream, pixels);
break;
switch (this.bitsPerPixel)
{
case BmpBitsPerPixel.Bit32:
this.Write32BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit8:
this.Write8BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Bit24:
this.Write24BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit4:
this.Write4BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Bit16:
this.Write16BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit2:
this.Write2BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Bit8:
this.Write8BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
case BmpBitsPerPixel.Bit1:
this.Write1BitPixelData(configuration, stream, image);
break;
}
case BmpBitsPerPixel.Bit4:
this.Write4BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
case BmpBitsPerPixel.Bit2:
this.Write2BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
if (this.processedAlphaMask)
case BmpBitsPerPixel.Bit1:
this.Write1BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
}
if (this.processedAlphaMask)
{
ProcessedAlphaMask(stream, encodingFrame);
}
}
finally
{
ProcessedAlphaMask(stream, image);
clonedFrame?.Dispose();
}
}
@ -396,7 +426,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write32BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
Buffer2D<TPixel> pixels,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
@ -404,6 +439,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes(
configuration,
@ -421,7 +458,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write24BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
Buffer2D<TPixel> pixels,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
@ -431,6 +473,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes(
configuration,
@ -448,7 +492,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write16BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
Buffer2D<TPixel> pixels,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
@ -458,6 +507,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
@ -476,21 +527,32 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
private void Write8BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write8BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
bool isL8 = typeof(TPixel) == typeof(L8);
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
bool is8BitLuminance =
info.BitsPerPixel == 8
&& info.ColorType == PixelColorType.Luminance
&& info.AlphaRepresentation == PixelAlphaRepresentation.None
&& info.ComponentInfo!.Value.ComponentCount == 1;
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize8Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
if (isL8)
if (is8BitLuminance)
{
this.Write8BitPixelData(stream, image, colorPalette);
this.Write8BitLuminancePixelData(stream, encodingFrame, colorPalette, cancellationToken);
}
else
{
this.Write8BitColor(configuration, stream, image, colorPalette);
this.Write8BitColor(configuration, stream, encodingFrame, colorPalette, cancellationToken);
}
}
@ -500,21 +562,29 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitColor<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image, Span<byte> colorPalette)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write8BitColor<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
Span<byte> colorPalette,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
ReadOnlySpan<byte> pixelSpan = quantized.DangerousGetRowSpan(y);
stream.Write(pixelSpan);
@ -530,9 +600,14 @@ internal sealed class BmpEncoderCore
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image, Span<byte> colorPalette)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write8BitLuminancePixelData<TPixel>(
Stream stream,
ImageFrame<TPixel> encodingFrame,
Span<byte> colorPalette,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Create a color palette with 256 different gray values.
@ -549,9 +624,11 @@ internal sealed class BmpEncoderCore
}
stream.Write(colorPalette);
Buffer2D<TPixel> imageBuffer = image.GetRootFramePixelBuffer();
for (int y = image.Height - 1; y >= 0; y--)
Buffer2D<TPixel> imageBuffer = encodingFrame.PixelBuffer;
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(y);
ReadOnlySpan<byte> outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow);
stream.Write(outputPixelRow);
@ -569,20 +646,25 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write4BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions
{
MaxColors = 16,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -591,8 +673,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1;
@ -619,20 +703,25 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write2BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write2BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions
{
MaxColors = 4,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -641,8 +730,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4;
@ -678,20 +769,25 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write1BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write1BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions
{
MaxColors = 2,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -700,8 +796,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
quantizedPixelRow = quantized.DangerousGetRowSpan(y);
int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
@ -766,10 +864,10 @@ internal sealed class BmpEncoderCore
stream.WriteByte(indices);
}
private static void ProcessedAlphaMask<TPixel>(Stream stream, Image<TPixel> image)
private static void ProcessedAlphaMask<TPixel>(Stream stream, ImageFrame<TPixel> encodingFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
int arrayWidth = image.Width / 8;
int arrayWidth = encodingFrame.Width / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
@ -777,10 +875,10 @@ internal sealed class BmpEncoderCore
}
Span<byte> mask = stackalloc byte[arrayWidth];
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
mask.Clear();
Span<TPixel> row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y);
Span<TPixel> row = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int i = 0; i < arrayWidth; i++)
{

2
src/ImageSharp/Formats/Bmp/BmpFormat.cs

@ -15,7 +15,7 @@ public sealed class BmpFormat : IImageFormat<BmpMetadata>
/// <summary>
/// Gets the shared instance.
/// </summary>
public static BmpFormat Instance { get; } = new BmpFormat();
public static BmpFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "BMP";

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
// TODO: Add color table information.
@ -156,8 +157,7 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
public BmpMetadata DeepClone() => new(this);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
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
}

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -48,13 +49,13 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
@ -80,20 +81,6 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
byte encodingWidth = metadata.EncodingWidth switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingWidth,
_ => 0
};
byte encodingHeight = metadata.EncodingHeight switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingHeight,
_ => 0
};
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
@ -116,9 +103,8 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = encodingWidth,
EncodingHeight = encodingHeight,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
};
}
@ -127,19 +113,19 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>
@ -156,16 +142,16 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
this.HotspotY = entry.BitCount;
}
internal IconDirEntry ToIconDirEntry()
internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
return new()
return new IconDirEntry
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),
Planes = this.HotspotX,
BitCount = this.HotspotY,
ColorCount = colorCount
@ -233,13 +219,22 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
private static byte Scale(byte? value, int destination, float ratio)
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
return ClampEncodingDimension(destination);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
}
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{
// Encoding dimensions can be between 0-256 where 0 means 256 or greater.
> 255 => 0,
<= 255 and >= 1 => (byte)dimension,
_ => 0
};
}

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -71,8 +72,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
return new CurMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
Compression = compression
};
}
@ -145,15 +145,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)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

47
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;
@ -11,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary>
public sealed class DecoderOptions
{
private static readonly Lazy<DecoderOptions> LazyOptions = new(() => new());
private static readonly Lazy<DecoderOptions> LazyOptions = new(() => new DecoderOptions());
private uint maxFrames = int.MaxValue;
@ -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 (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return false;
}
if (profile.IsCanonicalSrgbMatrixTrc())
{
return false;
}
value = profile;
return true;
}
internal bool CanRemoveIccProfile(IccProfile? profile)
{
if (profile is null)
{
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Convert)
{
return true;
}
return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc();
}
}

169
src/ImageSharp/Formats/EncodingUtilities.cs

@ -0,0 +1,169 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Provides utilities for encoding images.
/// </summary>
internal static class EncodingUtilities
{
/// <summary>
/// Determines if transparent pixels can be replaced based on the specified color mode and pixel type.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="mode">Indicates the color mode used to assess the ability to replace transparent pixels.</param>
/// <returns>Returns true if transparent pixels can be replaced; otherwise, false.</returns>
public static bool ShouldReplaceTransparentPixels<TPixel>(TransparentColorMode mode)
where TPixel : unmanaged, IPixel<TPixel>
=> mode == TransparentColorMode.Clear && TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;
/// <summary>
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
public static void ReplaceTransparentPixels<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
=> ReplaceTransparentPixels(frame.Configuration, frame.PixelBuffer);
/// <summary>
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="buffer">The <see cref="Buffer2D{TPixel}"/> where the transparent pixels will be changed.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReplaceTransparentPixels<TPixel>(Configuration configuration, Buffer2D<TPixel> buffer)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> region = buffer.GetRegion();
ReplaceTransparentPixels(configuration, in region);
}
/// <summary>
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="region">The <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
public static void ReplaceTransparentPixels<TPixel>(
Configuration configuration,
in Buffer2DRegion<TPixel> region)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(region.Width);
Span<Vector4> vectorsSpan = vectors.GetSpan();
for (int y = 0; y < region.Height; y++)
{
Span<TPixel> span = region.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
ReplaceTransparentPixels(vectorsSpan);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
}
}
/// <summary>
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <param name="source">A span of color vectors that will be checked for transparency and potentially modified.</param>
public static void ReplaceTransparentPixels(Span<Vector4> source)
{
if (Vector512.IsHardwareAccelerated && source.Length >= 4)
{
Span<Vector512<float>> source512 = MemoryMarshal.Cast<Vector4, Vector512<float>>(source);
for (int i = 0; i < source512.Length; i++)
{
ref Vector512<float> v = ref source512[i];
// Do `vector < threshold`
Vector512<float> mask = Vector512.Equals(v, Vector512<float>.Zero);
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
mask = Vector512.Shuffle(mask, Vector512.Create(3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15));
// Use the mask to select the replacement vector
// (replacement & mask) | (v512 & ~mask)
v = Vector512.ConditionalSelect(mask, Vector512<float>.Zero, v);
}
int m = Numerics.Modulo4(source.Length);
if (m != 0)
{
for (int i = source.Length - m; i < source.Length; i++)
{
if (source[i].W == 0)
{
source[i] = Vector4.Zero;
}
}
}
}
else if (Vector256.IsHardwareAccelerated && source.Length >= 2)
{
Span<Vector256<float>> source256 = MemoryMarshal.Cast<Vector4, Vector256<float>>(source);
for (int i = 0; i < source256.Length; i++)
{
ref Vector256<float> v = ref source256[i];
// Do `vector < threshold`
Vector256<float> mask = Vector256.Equals(v, Vector256<float>.Zero);
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
mask = Vector256.Shuffle(mask, Vector256.Create(3, 3, 3, 3, 7, 7, 7, 7));
// Use the mask to select the replacement vector
// (replacement & mask) | (v256 & ~mask)
v = Vector256.ConditionalSelect(mask, Vector256<float>.Zero, v);
}
int m = Numerics.Modulo2(source.Length);
if (m != 0)
{
for (int i = source.Length - m; i < source.Length; i++)
{
if (source[i].W == 0)
{
source[i] = Vector4.Zero;
}
}
}
}
else if (Vector128.IsHardwareAccelerated)
{
for (int i = 0; i < source.Length; i++)
{
ref Vector4 v = ref source[i];
Vector128<float> v128 = v.AsVector128();
// Do `vector == 0`
Vector128<float> mask = Vector128.Equals(v128, Vector128<float>.Zero);
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3));
// Use the mask to select the replacement vector
// (replacement & mask) | (v128 & ~mask)
v = Vector128.ConditionalSelect(mask, Vector128<float>.Zero, v128).AsVector4();
}
}
else
{
for (int i = 0; i < source.Length; i++)
{
if (source[i].W == 0F)
{
source[i] = Vector4.Zero;
}
}
}
}
}

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>

7
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>
@ -45,7 +40,7 @@ public class FormatConnectingMetadata
/// Gets the default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Color.Transparent"/>.

22
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -93,41 +93,41 @@ internal static class GifConstants
/// <summary>
/// The collection of mimetypes that equate to a Gif.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/gif" };
public static readonly IEnumerable<string> MimeTypes = ["image/gif"];
/// <summary>
/// The collection of file extensions that equate to a Gif.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "gif" };
public static readonly IEnumerable<string> FileExtensions = ["gif"];
/// <summary>
/// Gets the ASCII encoded bytes used to identify the GIF file (combining <see cref="FileType"/> and <see cref="FileVersion"/>).
/// </summary>
internal static ReadOnlySpan<byte> MagicNumber => new[]
{
internal static ReadOnlySpan<byte> MagicNumber =>
[
(byte)'G', (byte)'I', (byte)'F',
(byte)'8', (byte)'9', (byte)'a'
};
];
/// <summary>
/// Gets the ASCII encoded application identification bytes (representing <see cref="NetscapeApplicationIdentification"/>).
/// </summary>
internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes => new[]
{
internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes =>
[
(byte)'N', (byte)'E', (byte)'T',
(byte)'S', (byte)'C', (byte)'A',
(byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0'
};
];
/// <summary>
/// Gets the ASCII encoded application identification bytes.
/// </summary>
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[]
{
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes =>
[
(byte)'X', (byte)'M', (byte)'P',
(byte)' ', (byte)'D', (byte)'a',
(byte)'t', (byte)'a',
(byte)'X', (byte)'M', (byte)'P'
};
];
}

255
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
{
@ -242,7 +267,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
return new ImageInfo(
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
framesMetadata);
}
@ -280,7 +305,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
}
this.Dimensions = new(this.imageDescriptor.Width, this.imageDescriptor.Height);
this.Dimensions = new Size(this.imageDescriptor.Width, this.imageDescriptor.Height);
}
/// <summary>
@ -389,6 +414,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block");
}
if (length == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
if (this.skipMetadata)
{
stream.Seek(length, SeekOrigin.Current);
@ -416,26 +446,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 +476,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;
}
// We zero the alpha only when this frame declares transparency so that
// frames with a transparent index coalesce over a transparent canvas rather than
// baking the LSD background as a matte. When the flag is not set, this frame will
// write an opaque color for every addressed pixel; keeping the LSD background
// opaque here allows ReadFrameColors to show that background in uncovered areas
// for non-transparent GIFs that rely on it. We still do not prefill the canvas here.
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.
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
if (disposalMethod != FrameDisposalMode.RestoreToPrevious)
{
// Do not key this on the transparency flag. Disposal handling is determined by
// the previous frame's disposal, not by whether the current frame declares a transparent
// index. For editing we carry a transparent background so that RestoreToBackground clears
// remove pixels to transparent rather than painting an opaque matte. The LSD background
// color is display advice and should be used only when explicitly flattening or when
// rendering with an option to honor it.
backgroundColor = (this.backgroundColorIndex < colorTable.Length)
? Color.FromPixel(colorTable[this.backgroundColorIndex]).WithAlpha(0)
: Color.Transparent;
}
// Skip any remaining blocks
SkipBlock(stream);
return !hasLocalColorTable;
}
/// <summary>
@ -456,57 +531,88 @@ 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;
bool useTransparency = this.graphicsControlExtension.TransparencyFlag;
bool useBackground;
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)
{
// First frame: prefill with LSD background iff a GCT exists (policy: HonorBackgroundColor).
useBackground =
this.logicalScreenDescriptor.GlobalColorTableFlag
&& disposalMethod == FrameDisposalMode.RestoreToBackground;
if (previousFrame is null)
image = useBackground
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
currentFrame = image.Frames.RootFrame;
}
else
{
if (!transFlag)
// Subsequent frames: use LSD background iff previous disposal was RestoreToBackground and a GCT exists.
useBackground =
this.logicalScreenDescriptor.GlobalColorTableFlag
&& previousDisposalMode == FrameDisposalMode.RestoreToBackground;
if (previousFrame != null)
{
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
currentFrame = image!.Frames.AddFrame(previousFrame);
}
else if (useBackground)
{
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
}
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();
}
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, !useBackground);
}
}
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 Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
}
if (colorTable.Length == 0)
@ -528,7 +634,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,47 +677,48 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
lzwDecoder.DecodePixelRow(indicesRow);
ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
if (!transFlag)
// #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 (!useTransparency)
{
// #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];
// Treat any out of bounds values as background.
if (index > colorTableMaxIdx)
{
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.
// We explicitly set the pixel to transparent rather than alter the inbound
// color palette.
if (index > colorTableMaxIdx || index == transIndex)
{
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 +727,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 +740,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 +762,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// Skip any remaining blocks
SkipBlock(stream);
return !this.imageDescriptor.LocalColorTableFlag;
}
/// <summary>
@ -657,7 +771,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)
@ -665,9 +781,16 @@ internal sealed class GifDecoderCore : ImageDecoderCore
return;
}
Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
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 +899,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

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

Loading…
Cancel
Save