Browse Source

Merge branch 'main' into bp/openExr

# Conflicts:
#	src/ImageSharp/Configuration.cs
#	src/ImageSharp/Formats/ImageDecoderUtilities.cs
#	tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
pull/3096/head
Brian Popow 2 months ago
parent
commit
7748467e84
  1. 20
      .editorconfig
  2. 11
      .gitattributes
  3. 2
      .github/CONTRIBUTING.md
  4. 56
      .github/ISSUE_TEMPLATE/commercial-bug-report.yml
  5. 4
      .github/ISSUE_TEMPLATE/oss-bug-report.yml
  6. 153
      .github/workflows/build-and-test.yml
  7. 45
      .github/workflows/code-coverage.yml
  8. 9
      Directory.Build.props
  9. 24
      ImageSharp.sln
  10. 23
      README.md
  11. 2
      SixLabors.ImageSharp.props
  12. 2
      shared-infrastructure
  13. 4
      src/ImageSharp.ruleset
  14. 40
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  15. 56
      src/ImageSharp/Advanced/AotCompilerTools.cs
  16. 2
      src/ImageSharp/Advanced/IConfigurationProvider.cs
  17. 8
      src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs
  18. 48
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  19. 240
      src/ImageSharp/Color/Color.Conversions.cs
  20. 288
      src/ImageSharp/Color/Color.NamedColors.cs
  21. 8
      src/ImageSharp/Color/Color.WebSafePalette.cs
  22. 8
      src/ImageSharp/Color/Color.WernerPalette.cs
  23. 548
      src/ImageSharp/Color/Color.cs
  24. 40
      src/ImageSharp/Color/ColorHexFormat.cs
  25. 20
      src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs
  26. 21
      src/ImageSharp/ColorProfiles/CieConstants.cs
  27. 220
      src/ImageSharp/ColorProfiles/CieLab.cs
  28. 220
      src/ImageSharp/ColorProfiles/CieLch.cs
  29. 219
      src/ImageSharp/ColorProfiles/CieLchuv.cs
  30. 232
      src/ImageSharp/ColorProfiles/CieLuv.cs
  31. 87
      src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs
  32. 189
      src/ImageSharp/ColorProfiles/CieXyy.cs
  33. 203
      src/ImageSharp/ColorProfiles/CieXyz.cs
  34. 202
      src/ImageSharp/ColorProfiles/Cmyk.cs
  35. 94
      src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
  36. 48
      src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
  37. 98
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
  38. 95
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
  39. 100
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
  40. 95
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
  41. 87
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
  42. 95
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
  43. 772
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  44. 192
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
  45. 100
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
  46. 95
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
  47. 98
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
  48. 182
      src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs
  49. 56
      src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs
  50. 71
      src/ImageSharp/ColorProfiles/Companding/LCompanding.cs
  51. 75
      src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs
  52. 71
      src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs
  53. 71
      src/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs
  54. 287
      src/ImageSharp/ColorProfiles/Hsl.cs
  55. 273
      src/ImageSharp/ColorProfiles/Hsv.cs
  56. 243
      src/ImageSharp/ColorProfiles/HunterLab.cs
  57. 103
      src/ImageSharp/ColorProfiles/IColorProfile.cs
  58. 18
      src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs
  59. 506
      src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
  60. 65
      src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs
  61. 14
      src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs
  62. 47
      src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs
  63. 19
      src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs
  64. 17
      src/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs
  65. 19
      src/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs
  66. 23
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
  67. 158
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  68. 77
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs
  69. 80
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  70. 26
      src/ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs
  71. 130
      src/ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs
  72. 41
      src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
  73. 42
      src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs
  74. 155
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs
  75. 66
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs
  76. 109
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  77. 49
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs
  78. 22
      src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
  79. 22
      src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
  80. 22
      src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
  81. 22
      src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
  82. 135
      src/ImageSharp/ColorProfiles/KnownChromaticAdaptationMatrices.cs
  83. 77
      src/ImageSharp/ColorProfiles/KnownIlluminants.cs
  84. 113
      src/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs
  85. 62
      src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs
  86. 178
      src/ImageSharp/ColorProfiles/Lms.cs
  87. 463
      src/ImageSharp/ColorProfiles/Rgb.cs
  88. 82
      src/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs
  89. 95
      src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
  90. 68
      src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
  91. 35
      src/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs
  92. 35
      src/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs
  93. 35
      src/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs
  94. 85
      src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs
  95. 35
      src/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs
  96. 142
      src/ImageSharp/ColorProfiles/Y.cs
  97. 194
      src/ImageSharp/ColorProfiles/YCbCr.cs
  98. 61
      src/ImageSharp/ColorProfiles/YCbCrTransform.cs
  99. 206
      src/ImageSharp/ColorProfiles/YccK.cs
  100. 135
      src/ImageSharp/ColorSpaces/CieLab.cs

20
.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
@ -172,6 +175,11 @@ dotnet_diagnostic.IDE0063.severity = suggestion
csharp_using_directive_placement = outside_namespace:warning
# Modifier preferences
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

11
.gitattributes

@ -118,6 +118,7 @@
*.bmp filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.qoi filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
@ -132,3 +133,13 @@
*.pnm filter=lfs diff=lfs merge=lfs -text
*.wbmp filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.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

2
.github/CONTRIBUTING.md

@ -29,7 +29,6 @@
#### **Running tests and Debugging**
* Expected test output is pulled in as a submodule from the [ImageSharp.Tests.Images repository](https://github.com/SixLabors/Imagesharp.Tests.Images/tree/main/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules!
* Debugging (running tests in Debug mode) is only supported on .NET Core 2.1+, because of JIT Code Generation bugs like [dotnet/coreclr#16443](https://github.com/dotnet/coreclr/issues/16443) or [dotnet/coreclr#20657](https://github.com/dotnet/coreclr/issues/20657)
#### **Do you have questions about consuming the library or the source code?**
@ -37,7 +36,6 @@
#### Code of Conduct
This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community.
For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
And please remember. SixLabors.ImageSharp is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible image processing available to all. Open Source can only exist with your help.

56
.github/ISSUE_TEMPLATE/commercial-bug-report.yml

@ -1,56 +0,0 @@
name: "Commercial License : Bug Report"
description: |
Create a report to help us improve the project. For Commercial License holders only.
Please contact help@sixlabors.com for issues requiring private support.
labels: ["commercial", "needs triage"]
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label: I have bought a Commercial License
required: true
- label: I have written a descriptive issue title
required: true
- label: I have verified that I am running the latest version of ImageSharp
required: true
- label: I have verified if the problem exist in both `DEBUG` and `RELEASE` mode
required: true
- label: I have searched [open](https://github.com/SixLabors/ImageSharp/issues) and [closed](https://github.com/SixLabors/ImageSharp/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported
required: true
- type: input
attributes:
label: ImageSharp version
validations:
required: true
- type: input
attributes:
label: Other ImageSharp packages and versions
validations:
required: true
- type: input
attributes:
label: Environment (Operating system, version and so on)
validations:
required: true
- type: input
attributes:
label: .NET Framework version
validations:
required: true
- type: textarea
attributes:
label: Description
description: A description of the bug
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: List of steps, sample code, failing test or link to a project that reproduces the behavior. Make sure you place a stack trace inside a code (```) block to avoid linking unrelated issues.
validations:
required: true
- type: textarea
attributes:
label: Images
description: Please upload images that can be used to reproduce issues in the area below. If the file type is not supported the file can be zipped and then uploaded instead.

4
.github/ISSUE_TEMPLATE/oss-bug-report.yml

@ -1,5 +1,5 @@
name: "OSS : Bug Report"
description: Create a report to help us improve the project. OSS Issues are not guaranteed to be triaged.
name: "Bug Report"
description: Create a report to help us improve the project. Issues are not guaranteed to be triaged.
labels: ["needs triage"]
body:
- type: checkboxes

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

@ -4,69 +4,131 @@ on:
push:
branches:
- main
- release/*
tags:
- "v*"
pull_request:
branches:
- main
types: [ labeled, opened, synchronize, reopened ]
- release/*
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@v6
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@v5
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: net7.0
sdk: 7.0.x
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-latest
framework: net7.0
sdk: 7.0.x
- os: macos-26
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net7.0
sdk: 7.0.x
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net7.0
sdk: 7.0.x
- os: ubuntu-22.04-arm
framework: net10.0
sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: net6.0
sdk: 6.0.x
framework: net8.0
sdk: 8.0.x
runtime: -x64
codecov: false
- os: macos-latest
framework: net6.0
sdk: 6.0.x
- os: macos-26
framework: net8.0
sdk: 8.0.x
runtime: -x64
codecov: false
- os: windows-latest
framework: net6.0
sdk: 6.0.x
framework: net8.0
sdk: 8.0.x
runtime: -x64
codecov: false
- 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
@ -75,30 +137,27 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
with:
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@v3
id: lfs-cache
uses: actions/cache@v5
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
uses: NuGet/setup-nuget@v1
uses: NuGet/setup-nuget@v2
- name: NuGet Setup Cache
uses: actions/cache@v3
uses: actions/cache@v5
id: nuget-cache
with:
path: ~/.nuget
@ -107,17 +166,17 @@ jobs:
- name: DotNet Setup
if: ${{ matrix.options.sdk-preview != true }}
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
6.0.x
8.0.x
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
7.0.x
10.0.x
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
@ -150,7 +209,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
@ -158,11 +217,8 @@ jobs:
Publish:
needs: [Build]
runs-on: ubuntu-latest
if: (github.event_name == 'push')
steps:
- name: Git Config
shell: bash
@ -171,16 +227,16 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: NuGet Install
uses: NuGet/setup-nuget@v1
uses: NuGet/setup-nuget@v2
- name: NuGet Setup Cache
uses: actions/cache@v3
uses: actions/cache@v5
id: nuget-cache
with:
path: ~/.nuget
@ -203,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

45
.github/workflows/code-coverage.yml

@ -4,19 +4,26 @@ on:
schedule:
# 2AM every Tuesday/Thursday
- cron: "0 2 * * 2,4"
jobs:
Build:
strategy:
matrix:
options:
- os: ubuntu-latest
framework: net6.0
framework: net8.0
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: |
@ -24,30 +31,35 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
with:
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@v3
uses: actions/cache@v5
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
- name: NuGet Install
uses: NuGet/setup-nuget@v1
uses: NuGet/setup-nuget@v2
- name: NuGet Setup Cache
uses: actions/cache@v3
uses: actions/cache@v5
id: nuget-cache
with:
path: ~/.nuget
@ -55,33 +67,34 @@ jobs:
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
6.0.x
8.0.x
- 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
- name: Export Failed Output
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
path: tests/Images/ActualOutput/
- name: Codecov Update
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests
token: ${{ secrets.CODECOV_TOKEN }}

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

24
ImageSharp.sln

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1
Directory.Build.targets = Directory.Build.targets
LICENSE = LICENSE
README.md = README.md
SixLabors.ImageSharp.props = SixLabors.ImageSharp.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1799C43E-5C54-4A8F-8D64-B1475241DB0D}"
@ -28,7 +29,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1799
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{FBE8C1AD-5AEC-4514-9B64-091D8E145865}"
ProjectSection(SolutionItems) = preProject
.github\ISSUE_TEMPLATE\commercial-bug-report.yml = .github\ISSUE_TEMPLATE\commercial-bug-report.yml
.github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml
.github\ISSUE_TEMPLATE\oss-bug-report.yml = .github\ISSUE_TEMPLATE\oss-bug-report.yml
EndProjectSection
@ -38,6 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{815C0625-CD3
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets
src\README.md = src\README.md
src\ImageSharp.ruleset = src\ImageSharp.ruleset
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageSharp\ImageSharp.csproj", "{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}"
@ -237,6 +238,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"
@ -647,6 +649,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tga", "Tga", "{5DFC394F-136
tests\Images\Input\Tga\targa_8bit_rle.tga = tests\Images\Input\Tga\targa_8bit_rle.tga
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-4935-41CD-BA85-CF11BFF55A45}"
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Qoi\dice.qoi = tests\Images\Input\Qoi\dice.qoi
tests\Images\Input\Qoi\edgecase.qoi = tests\Images\Input\Qoi\edgecase.qoi
tests\Images\Input\Qoi\kodim10.qoi = tests\Images\Input\Qoi\kodim10.qoi
tests\Images\Input\Qoi\kodim23.qoi = tests\Images\Input\Qoi\kodim23.qoi
tests\Images\Input\Qoi\qoi_logo.qoi = tests\Images\Input\Qoi\qoi_logo.qoi
tests\Images\Input\Qoi\testcard.qoi = tests\Images\Input\Qoi\testcard.qoi
tests\Images\Input\Qoi\testcard_rgba.qoi = tests\Images\Input\Qoi\testcard_rgba.qoi
tests\Images\Input\Qoi\wikipedia_008.qoi = tests\Images\Input\Qoi\wikipedia_008.qoi
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{95E45DDE-A67D-48AD-BBA8-5FAA151B860D}"
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Icon\aero_arrow.cur = tests\Images\Input\Icon\aero_arrow.cur
tests\Images\Input\Icon\flutter.ico = tests\Images\Input\Icon\flutter.ico
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -699,6 +719,8 @@ Global
{FC527290-2F22-432C-B77B-6E815726B02C} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{E801B508-4935-41CD-BA85-CF11BFF55A45} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{95E45DDE-A67D-48AD-BBA8-5FAA151B860D} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}

23
README.md

@ -8,20 +8,18 @@ SixLabors.ImageSharp
<div align="center">
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp)
[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
</div>
### **ImageSharp** is a new, fully featured, fully managed, cross-platform, 2D graphics API.
### **ImageSharp** is a high-performance, fully managed, cross-platform 2D graphics API.
ImageSharp is a new, fully featured, fully managed, cross-platform, 2D graphics library.
Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
ImageSharp is a mature, fully featured, high-performance image processing and graphics library for .NET, built for workloads across device, cloud, and embedded/IoT scenarios.
ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations.
Designed from the ground up to balance performance, portability, and ease of use, ImageSharp provides a powerful yet approachable API for common image processing tasks, along with the low-level building blocks needed to extend the library for specialized workflows.
Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp can be used in device, cloud, and embedded/IoT scenarios.
Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp can be used in device, cloud, and embedded/IoT scenarios.
## License
@ -64,7 +62,7 @@ If you prefer, you can compile ImageSharp yourself (please do and help!)
- Using [Visual Studio 2022](https://visualstudio.microsoft.com/vs/)
- Make sure you have the latest version installed
- Make sure you have [the .NET 7 SDK](https://www.microsoft.com/net/core#windows) installed
- Make sure you have [the .NET 8 SDK](https://www.microsoft.com/net/core#windows) installed
Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**:
@ -101,6 +99,8 @@ git submodule update --init --recursive
Please... Spread the word, contribute algorithms, submit performance improvements, unit tests, no input is too little. Make sure to read our [Contribution Guide](https://github.com/SixLabors/ImageSharp/blob/main/.github/CONTRIBUTING.md) before opening a PR.
Useful tools for development and links to specifications can be found in our wikipage: [Useful-tools-and-links](https://github.com/SixLabors/ImageSharp/wiki/Useful-tools-and-links).
## The ImageSharp Team
- [James Jackson-South](https://github.com/jimbobsquarepants)
@ -109,6 +109,11 @@ Please... Spread the word, contribute algorithms, submit performance improvement
- [Scott Williams](https://github.com/tocsoft)
- [Brian Popow](https://github.com/brianpopow)
---
<div>
<a href="https://www.jetbrains.com/?from=ImageSharp" align="right"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg" alt="JetBrains" class="logo-footer" width="72" align="left"></a>
<br/>
Special thanks to [JetBrains](https://www.jetbrains.com/?from=ImageSharp) for supporting us with open-source licenses for their IDEs.
</div>

2
SixLabors.ImageSharp.props

@ -2,7 +2,7 @@
<Project>
<!--Add common namespaces to implicit global usings if enabled.-->
<ItemGroup Condition="'$(ImplicitUsings)'=='enable' OR '$(ImplicitUsings)'=='true'">
<ItemGroup Condition="'$(UseImageSharp)'=='enable' OR '$(UseImageSharp)'=='true'">
<Using Include="SixLabors.ImageSharp" />
<Using Include="SixLabors.ImageSharp.PixelFormats" />
<Using Include="SixLabors.ImageSharp.Processing" />

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 9a6cf00d9a3d482bb08211dd8309f4724a2735cb
Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d

4
src/ImageSharp.ruleset

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="ImageSharp" ToolsVersion="17.0">
<Include Path="..\shared-infrastructure\sixlabors.ruleset" Action="Default" />
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA1011" Action="None" />
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.NetAnalyzers">
<Rule Id="CA2022" Action="Info" />
</Rules>
</RuleSet>

40
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -27,11 +27,11 @@ public static class AdvancedImageExtensions
Guard.NotNull(filePath, nameof(filePath));
string ext = Path.GetExtension(filePath);
if (!source.GetConfiguration().ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format))
if (!source.Configuration.ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format))
{
StringBuilder sb = new();
sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}'. Registered encoders include:");
foreach (IImageFormat fmt in source.GetConfiguration().ImageFormats)
foreach (IImageFormat fmt in source.Configuration.ImageFormats)
{
sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine);
}
@ -39,13 +39,13 @@ public static class AdvancedImageExtensions
throw new UnknownImageFormatException(sb.ToString());
}
IImageEncoder? encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format);
IImageEncoder? encoder = source.Configuration.ImageFormatsManager.GetEncoder(format);
if (encoder is null)
{
StringBuilder sb = new();
sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, IImageEncoder> enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders)
foreach (KeyValuePair<IImageFormat, IImageEncoder> enc in source.Configuration.ImageFormatsManager.ImageEncoders)
{
sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine);
}
@ -76,30 +76,6 @@ public static class AdvancedImageExtensions
public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default)
=> source.AcceptAsync(visitor, cancellationToken);
/// <summary>
/// Gets the configuration for the image.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>Returns the configuration.</returns>
public static Configuration GetConfiguration(this Image source)
=> GetConfiguration((IConfigurationProvider)source);
/// <summary>
/// Gets the configuration for the image frame.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>Returns the configuration.</returns>
public static Configuration GetConfiguration(this ImageFrame source)
=> GetConfiguration((IConfigurationProvider)source);
/// <summary>
/// Gets the configuration.
/// </summary>
/// <param name="source">The source image</param>
/// <returns>Returns the bounds of the image</returns>
private static Configuration GetConfiguration(IConfigurationProvider source)
=> source?.Configuration ?? Configuration.Default;
/// <summary>
/// Gets the representation of the pixels as a <see cref="IMemoryGroup{T}"/> containing the backing pixel data of the image
/// stored in row major order, as a list of contiguous <see cref="Memory{T}"/> blocks in the source image's pixel format.
@ -167,12 +143,4 @@ public static class AdvancedImageExtensions
return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex);
}
/// <summary>
/// Gets the <see cref="MemoryAllocator"/> assigned to 'source'.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>Returns the configuration.</returns>
internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source)
=> GetConfiguration(source).MemoryAllocator;
}

56
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -10,10 +10,13 @@ using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -129,15 +132,17 @@ internal static class AotCompilerTools
AotCompileImageDecoderInternals<TPixel>();
AotCompileImageEncoders<TPixel>();
AotCompileImageDecoders<TPixel>();
AotCompileSpectralConverter<TPixel>();
AotCompileImageProcessors<TPixel>();
AotCompileGenericImageProcessors<TPixel>();
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.
}
@ -195,39 +200,41 @@ internal static class AotCompilerTools
=> default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext<TPixel>(default, default, default);
/// <summary>
/// This method pre-seeds the all <see cref="IImageEncoderInternals"/> in the AoT compiler.
/// This method pre-seeds the all core encoders in the AoT compiler.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompileImageEncoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(WebpEncoderCore).Encode<TPixel>(default, default, default);
default(BmpEncoderCore).Encode<TPixel>(default, default, default);
default(GifEncoderCore).Encode<TPixel>(default, default, default);
default(JpegEncoderCore).Encode<TPixel>(default, default, default);
default(PbmEncoderCore).Encode<TPixel>(default, default, default);
default(PngEncoderCore).Encode<TPixel>(default, default, default);
default(QoiEncoderCore).Encode<TPixel>(default, default, default);
default(TgaEncoderCore).Encode<TPixel>(default, default, default);
default(TiffEncoderCore).Encode<TPixel>(default, default, default);
default(WebpEncoderCore).Encode<TPixel>(default, default, default);
}
/// <summary>
/// This method pre-seeds the all <see cref="IImageDecoderInternals"/> in the AoT compiler.
/// This method pre-seeds the all <see cref="ImageDecoderCore"/> in the AoT compiler.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompileImageDecoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(WebpDecoderCore).Decode<TPixel>(default, default);
default(BmpDecoderCore).Decode<TPixel>(default, default);
default(GifDecoderCore).Decode<TPixel>(default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default);
default(PngDecoderCore).Decode<TPixel>(default, default);
default(TgaDecoderCore).Decode<TPixel>(default, default);
default(TiffDecoderCore).Decode<TPixel>(default, default);
default(BmpDecoderCore).Decode<TPixel>(default, default, default);
default(GifDecoderCore).Decode<TPixel>(default, default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default, default);
default(PngDecoderCore).Decode<TPixel>(default, default, default);
default(QoiDecoderCore).Decode<TPixel>(default, default, default);
default(TgaDecoderCore).Decode<TPixel>(default, default, default);
default(TiffDecoderCore).Decode<TPixel>(default, default, default);
default(WebpDecoderCore).Decode<TPixel>(default, default, default);
}
/// <summary>
@ -266,6 +273,17 @@ internal static class AotCompilerTools
AotCompileImageDecoder<TPixel, TiffDecoder>();
}
[Preserve]
private static void AotCompileSpectralConverter<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
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>
/// This method pre-seeds the <see cref="IImageEncoder"/> in the AoT compiler.
/// </summary>
@ -497,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>

2
src/ImageSharp/Advanced/IConfigurationProvider.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Advanced;
/// <summary>
/// Defines the contract for objects that can provide access to configuration.
/// </summary>
internal interface IConfigurationProvider
public interface IConfigurationProvider
{
/// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library.

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

@ -51,7 +51,7 @@ public static partial class ParallelRowIterator
for (int y = yMin; y < yMax; y++)
{
// Skip the safety copy when invoking a potentially impure method on a readonly field
Unsafe.AsRef(this.action).Invoke(y);
Unsafe.AsRef(in this.action).Invoke(y);
}
}
}
@ -102,7 +102,7 @@ public static partial class ParallelRowIterator
for (int y = yMin; y < yMax; y++)
{
Unsafe.AsRef(this.action).Invoke(y, span);
Unsafe.AsRef(in this.action).Invoke(y, span);
}
}
}
@ -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);

48
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);
}
@ -50,7 +50,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@ -58,15 +58,15 @@ public static partial class ParallelRowIterator
{
for (int y = top; y < bottom; y++)
{
Unsafe.AsRef(operation).Invoke(y);
Unsafe.AsRef(in operation).Invoke(y);
}
return;
}
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);
}
@ -115,10 +115,10 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -128,15 +128,15 @@ public static partial class ParallelRowIterator
for (int y = top; y < bottom; y++)
{
Unsafe.AsRef(operation).Invoke(y, span);
Unsafe.AsRef(in operation).Invoke(y, span);
}
return;
}
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);
}
@ -180,20 +180,20 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// 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);
}
@ -242,25 +242,25 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
// 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(operation).Invoke(in rows, buffer.Memory.Span);
Unsafe.AsRef(in operation).Invoke(in rows, buffer.Memory.Span);
return;
}
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,
@ -270,7 +270,7 @@ public static partial class ParallelRowIterator
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
private static void ValidateRectangle(Rectangle rectangle)
{

240
src/ImageSharp/Color/Color.Conversions.cs

@ -1,240 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
/// <content>
/// Contains constructors and implicit conversion methods.
/// </content>
public readonly partial struct Color
{
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Rgba64"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Rgba64 pixel)
{
this.data = pixel;
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Rgb48"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Rgb48 pixel)
{
this.data = new Rgba64(pixel.R, pixel.G, pixel.B, ushort.MaxValue);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="La32"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(La32 pixel)
{
this.data = new Rgba64(pixel.L, pixel.L, pixel.L, pixel.A);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="L16"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(L16 pixel)
{
this.data = new Rgba64(pixel.PackedValue, pixel.PackedValue, pixel.PackedValue, ushort.MaxValue);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Rgba32"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Rgba32 pixel)
{
this.data = new Rgba64(pixel);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Argb32"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Argb32 pixel)
{
this.data = new Rgba64(pixel);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Bgra32"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Bgra32 pixel)
{
this.data = new Rgba64(pixel);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Abgr32"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Abgr32 pixel)
{
this.data = new Rgba64(pixel);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Rgb24"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Rgb24 pixel)
{
this.data = new Rgba64(pixel);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The <see cref="Bgr24"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Bgr24 pixel)
{
this.data = new Rgba64(pixel);
this.boxedHighPrecisionPixel = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="vector">The <see cref="Vector4"/> containing the color information.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public Color(Vector4 vector)
{
vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One);
this.boxedHighPrecisionPixel = new RgbaVector(vector.X, vector.Y, vector.Z, vector.W);
this.data = default;
}
/// <summary>
/// Converts a <see cref="Color"/> to <see cref="Vector4"/>.
/// </summary>
/// <param name="color">The <see cref="Color"/>.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static explicit operator Vector4(Color color) => color.ToVector4();
/// <summary>
/// Converts an <see cref="Vector4"/> to <see cref="Color"/>.
/// </summary>
/// <param name="source">The <see cref="Vector4"/>.</param>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static explicit operator Color(Vector4 source) => new(source);
[MethodImpl(InliningOptions.ShortMethod)]
internal Rgba32 ToRgba32()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToRgba32();
}
Rgba32 value = default;
this.boxedHighPrecisionPixel.ToRgba32(ref value);
return value;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Bgra32 ToBgra32()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToBgra32();
}
Bgra32 value = default;
value.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
return value;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Argb32 ToArgb32()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToArgb32();
}
Argb32 value = default;
value.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
return value;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Abgr32 ToAbgr32()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToAbgr32();
}
Abgr32 value = default;
value.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
return value;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Rgb24 ToRgb24()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToRgb24();
}
Rgb24 value = default;
value.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
return value;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Bgr24 ToBgr24()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToBgr24();
}
Bgr24 value = default;
value.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
return value;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Vector4 ToVector4()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.ToScaledVector4();
}
return this.boxedHighPrecisionPixel.ToScaledVector4();
}
}

288
src/ImageSharp/Color/Color.NamedColors.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
/// <content>
@ -9,107 +11,107 @@ namespace SixLabors.ImageSharp;
/// </content>
public readonly partial struct Color
{
private static readonly Lazy<Dictionary<string, Color>> NamedColorsLookupLazy = new Lazy<Dictionary<string, Color>>(CreateNamedColorsLookup, true);
private static readonly Lazy<Dictionary<string, Color>> NamedColorsLookupLazy = new(CreateNamedColorsLookup, true);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0F8FF.
/// </summary>
public static readonly Color AliceBlue = FromRgba(240, 248, 255, 255);
public static readonly Color AliceBlue = FromPixel(new Rgba32(240, 248, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FAEBD7.
/// </summary>
public static readonly Color AntiqueWhite = FromRgba(250, 235, 215, 255);
public static readonly Color AntiqueWhite = FromPixel(new Rgba32(250, 235, 215, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FFFF.
/// </summary>
public static readonly Color Aqua = FromRgba(0, 255, 255, 255);
public static readonly Color Aqua = FromPixel(new Rgba32(0, 255, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7FFFD4.
/// </summary>
public static readonly Color Aquamarine = FromRgba(127, 255, 212, 255);
public static readonly Color Aquamarine = FromPixel(new Rgba32(127, 255, 212, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0FFFF.
/// </summary>
public static readonly Color Azure = FromRgba(240, 255, 255, 255);
public static readonly Color Azure = FromPixel(new Rgba32(240, 255, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5F5DC.
/// </summary>
public static readonly Color Beige = FromRgba(245, 245, 220, 255);
public static readonly Color Beige = FromPixel(new Rgba32(245, 245, 220, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFE4C4.
/// </summary>
public static readonly Color Bisque = FromRgba(255, 228, 196, 255);
public static readonly Color Bisque = FromPixel(new Rgba32(255, 228, 196, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #000000.
/// </summary>
public static readonly Color Black = FromRgba(0, 0, 0, 255);
public static readonly Color Black = FromPixel(new Rgba32(0, 0, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFEBCD.
/// </summary>
public static readonly Color BlanchedAlmond = FromRgba(255, 235, 205, 255);
public static readonly Color BlanchedAlmond = FromPixel(new Rgba32(255, 235, 205, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #0000FF.
/// </summary>
public static readonly Color Blue = FromRgba(0, 0, 255, 255);
public static readonly Color Blue = FromPixel(new Rgba32(0, 0, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8A2BE2.
/// </summary>
public static readonly Color BlueViolet = FromRgba(138, 43, 226, 255);
public static readonly Color BlueViolet = FromPixel(new Rgba32(138, 43, 226, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A52A2A.
/// </summary>
public static readonly Color Brown = FromRgba(165, 42, 42, 255);
public static readonly Color Brown = FromPixel(new Rgba32(165, 42, 42, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DEB887.
/// </summary>
public static readonly Color BurlyWood = FromRgba(222, 184, 135, 255);
public static readonly Color BurlyWood = FromPixel(new Rgba32(222, 184, 135, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #5F9EA0.
/// </summary>
public static readonly Color CadetBlue = FromRgba(95, 158, 160, 255);
public static readonly Color CadetBlue = FromPixel(new Rgba32(95, 158, 160, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7FFF00.
/// </summary>
public static readonly Color Chartreuse = FromRgba(127, 255, 0, 255);
public static readonly Color Chartreuse = FromPixel(new Rgba32(127, 255, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D2691E.
/// </summary>
public static readonly Color Chocolate = FromRgba(210, 105, 30, 255);
public static readonly Color Chocolate = FromPixel(new Rgba32(210, 105, 30, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF7F50.
/// </summary>
public static readonly Color Coral = FromRgba(255, 127, 80, 255);
public static readonly Color Coral = FromPixel(new Rgba32(255, 127, 80, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #6495ED.
/// </summary>
public static readonly Color CornflowerBlue = FromRgba(100, 149, 237, 255);
public static readonly Color CornflowerBlue = FromPixel(new Rgba32(100, 149, 237, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFF8DC.
/// </summary>
public static readonly Color Cornsilk = FromRgba(255, 248, 220, 255);
public static readonly Color Cornsilk = FromPixel(new Rgba32(255, 248, 220, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DC143C.
/// </summary>
public static readonly Color Crimson = FromRgba(220, 20, 60, 255);
public static readonly Color Crimson = FromPixel(new Rgba32(220, 20, 60, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FFFF.
@ -119,27 +121,27 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00008B.
/// </summary>
public static readonly Color DarkBlue = FromRgba(0, 0, 139, 255);
public static readonly Color DarkBlue = FromPixel(new Rgba32(0, 0, 139, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #008B8B.
/// </summary>
public static readonly Color DarkCyan = FromRgba(0, 139, 139, 255);
public static readonly Color DarkCyan = FromPixel(new Rgba32(0, 139, 139, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B8860B.
/// </summary>
public static readonly Color DarkGoldenrod = FromRgba(184, 134, 11, 255);
public static readonly Color DarkGoldenrod = FromPixel(new Rgba32(184, 134, 11, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A9A9A9.
/// </summary>
public static readonly Color DarkGray = FromRgba(169, 169, 169, 255);
public static readonly Color DarkGray = FromPixel(new Rgba32(169, 169, 169, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #006400.
/// </summary>
public static readonly Color DarkGreen = FromRgba(0, 100, 0, 255);
public static readonly Color DarkGreen = FromPixel(new Rgba32(0, 100, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A9A9A9.
@ -149,52 +151,52 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #BDB76B.
/// </summary>
public static readonly Color DarkKhaki = FromRgba(189, 183, 107, 255);
public static readonly Color DarkKhaki = FromPixel(new Rgba32(189, 183, 107, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8B008B.
/// </summary>
public static readonly Color DarkMagenta = FromRgba(139, 0, 139, 255);
public static readonly Color DarkMagenta = FromPixel(new Rgba32(139, 0, 139, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #556B2F.
/// </summary>
public static readonly Color DarkOliveGreen = FromRgba(85, 107, 47, 255);
public static readonly Color DarkOliveGreen = FromPixel(new Rgba32(85, 107, 47, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF8C00.
/// </summary>
public static readonly Color DarkOrange = FromRgba(255, 140, 0, 255);
public static readonly Color DarkOrange = FromPixel(new Rgba32(255, 140, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9932CC.
/// </summary>
public static readonly Color DarkOrchid = FromRgba(153, 50, 204, 255);
public static readonly Color DarkOrchid = FromPixel(new Rgba32(153, 50, 204, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8B0000.
/// </summary>
public static readonly Color DarkRed = FromRgba(139, 0, 0, 255);
public static readonly Color DarkRed = FromPixel(new Rgba32(139, 0, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #E9967A.
/// </summary>
public static readonly Color DarkSalmon = FromRgba(233, 150, 122, 255);
public static readonly Color DarkSalmon = FromPixel(new Rgba32(233, 150, 122, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8FBC8F.
/// </summary>
public static readonly Color DarkSeaGreen = FromRgba(143, 188, 143, 255);
public static readonly Color DarkSeaGreen = FromPixel(new Rgba32(143, 188, 143, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #483D8B.
/// </summary>
public static readonly Color DarkSlateBlue = FromRgba(72, 61, 139, 255);
public static readonly Color DarkSlateBlue = FromPixel(new Rgba32(72, 61, 139, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #2F4F4F.
/// </summary>
public static readonly Color DarkSlateGray = FromRgba(47, 79, 79, 255);
public static readonly Color DarkSlateGray = FromPixel(new Rgba32(47, 79, 79, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #2F4F4F.
@ -204,27 +206,27 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00CED1.
/// </summary>
public static readonly Color DarkTurquoise = FromRgba(0, 206, 209, 255);
public static readonly Color DarkTurquoise = FromPixel(new Rgba32(0, 206, 209, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9400D3.
/// </summary>
public static readonly Color DarkViolet = FromRgba(148, 0, 211, 255);
public static readonly Color DarkViolet = FromPixel(new Rgba32(148, 0, 211, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF1493.
/// </summary>
public static readonly Color DeepPink = FromRgba(255, 20, 147, 255);
public static readonly Color DeepPink = FromPixel(new Rgba32(255, 20, 147, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00BFFF.
/// </summary>
public static readonly Color DeepSkyBlue = FromRgba(0, 191, 255, 255);
public static readonly Color DeepSkyBlue = FromPixel(new Rgba32(0, 191, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #696969.
/// </summary>
public static readonly Color DimGray = FromRgba(105, 105, 105, 255);
public static readonly Color DimGray = FromPixel(new Rgba32(105, 105, 105, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #696969.
@ -234,62 +236,62 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #1E90FF.
/// </summary>
public static readonly Color DodgerBlue = FromRgba(30, 144, 255, 255);
public static readonly Color DodgerBlue = FromPixel(new Rgba32(30, 144, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B22222.
/// </summary>
public static readonly Color Firebrick = FromRgba(178, 34, 34, 255);
public static readonly Color Firebrick = FromPixel(new Rgba32(178, 34, 34, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFAF0.
/// </summary>
public static readonly Color FloralWhite = FromRgba(255, 250, 240, 255);
public static readonly Color FloralWhite = FromPixel(new Rgba32(255, 250, 240, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #228B22.
/// </summary>
public static readonly Color ForestGreen = FromRgba(34, 139, 34, 255);
public static readonly Color ForestGreen = FromPixel(new Rgba32(34, 139, 34, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF00FF.
/// </summary>
public static readonly Color Fuchsia = FromRgba(255, 0, 255, 255);
public static readonly Color Fuchsia = FromPixel(new Rgba32(255, 0, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DCDCDC.
/// </summary>
public static readonly Color Gainsboro = FromRgba(220, 220, 220, 255);
public static readonly Color Gainsboro = FromPixel(new Rgba32(220, 220, 220, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F8F8FF.
/// </summary>
public static readonly Color GhostWhite = FromRgba(248, 248, 255, 255);
public static readonly Color GhostWhite = FromPixel(new Rgba32(248, 248, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFD700.
/// </summary>
public static readonly Color Gold = FromRgba(255, 215, 0, 255);
public static readonly Color Gold = FromPixel(new Rgba32(255, 215, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DAA520.
/// </summary>
public static readonly Color Goldenrod = FromRgba(218, 165, 32, 255);
public static readonly Color Goldenrod = FromPixel(new Rgba32(218, 165, 32, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #808080.
/// </summary>
public static readonly Color Gray = FromRgba(128, 128, 128, 255);
public static readonly Color Gray = FromPixel(new Rgba32(128, 128, 128, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #008000.
/// </summary>
public static readonly Color Green = FromRgba(0, 128, 0, 255);
public static readonly Color Green = FromPixel(new Rgba32(0, 128, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #ADFF2F.
/// </summary>
public static readonly Color GreenYellow = FromRgba(173, 255, 47, 255);
public static readonly Color GreenYellow = FromPixel(new Rgba32(173, 255, 47, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #808080.
@ -299,82 +301,82 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0FFF0.
/// </summary>
public static readonly Color Honeydew = FromRgba(240, 255, 240, 255);
public static readonly Color Honeydew = FromPixel(new Rgba32(240, 255, 240, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF69B4.
/// </summary>
public static readonly Color HotPink = FromRgba(255, 105, 180, 255);
public static readonly Color HotPink = FromPixel(new Rgba32(255, 105, 180, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #CD5C5C.
/// </summary>
public static readonly Color IndianRed = FromRgba(205, 92, 92, 255);
public static readonly Color IndianRed = FromPixel(new Rgba32(205, 92, 92, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #4B0082.
/// </summary>
public static readonly Color Indigo = FromRgba(75, 0, 130, 255);
public static readonly Color Indigo = FromPixel(new Rgba32(75, 0, 130, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFF0.
/// </summary>
public static readonly Color Ivory = FromRgba(255, 255, 240, 255);
public static readonly Color Ivory = FromPixel(new Rgba32(255, 255, 240, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0E68C.
/// </summary>
public static readonly Color Khaki = FromRgba(240, 230, 140, 255);
public static readonly Color Khaki = FromPixel(new Rgba32(240, 230, 140, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #E6E6FA.
/// </summary>
public static readonly Color Lavender = FromRgba(230, 230, 250, 255);
public static readonly Color Lavender = FromPixel(new Rgba32(230, 230, 250, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFF0F5.
/// </summary>
public static readonly Color LavenderBlush = FromRgba(255, 240, 245, 255);
public static readonly Color LavenderBlush = FromPixel(new Rgba32(255, 240, 245, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7CFC00.
/// </summary>
public static readonly Color LawnGreen = FromRgba(124, 252, 0, 255);
public static readonly Color LawnGreen = FromPixel(new Rgba32(124, 252, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFACD.
/// </summary>
public static readonly Color LemonChiffon = FromRgba(255, 250, 205, 255);
public static readonly Color LemonChiffon = FromPixel(new Rgba32(255, 250, 205, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #ADD8E6.
/// </summary>
public static readonly Color LightBlue = FromRgba(173, 216, 230, 255);
public static readonly Color LightBlue = FromPixel(new Rgba32(173, 216, 230, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F08080.
/// </summary>
public static readonly Color LightCoral = FromRgba(240, 128, 128, 255);
public static readonly Color LightCoral = FromPixel(new Rgba32(240, 128, 128, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #E0FFFF.
/// </summary>
public static readonly Color LightCyan = FromRgba(224, 255, 255, 255);
public static readonly Color LightCyan = FromPixel(new Rgba32(224, 255, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FAFAD2.
/// </summary>
public static readonly Color LightGoldenrodYellow = FromRgba(250, 250, 210, 255);
public static readonly Color LightGoldenrodYellow = FromPixel(new Rgba32(250, 250, 210, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D3D3D3.
/// </summary>
public static readonly Color LightGray = FromRgba(211, 211, 211, 255);
public static readonly Color LightGray = FromPixel(new Rgba32(211, 211, 211, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #90EE90.
/// </summary>
public static readonly Color LightGreen = FromRgba(144, 238, 144, 255);
public static readonly Color LightGreen = FromPixel(new Rgba32(144, 238, 144, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D3D3D3.
@ -384,27 +386,27 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFB6C1.
/// </summary>
public static readonly Color LightPink = FromRgba(255, 182, 193, 255);
public static readonly Color LightPink = FromPixel(new Rgba32(255, 182, 193, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFA07A.
/// </summary>
public static readonly Color LightSalmon = FromRgba(255, 160, 122, 255);
public static readonly Color LightSalmon = FromPixel(new Rgba32(255, 160, 122, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #20B2AA.
/// </summary>
public static readonly Color LightSeaGreen = FromRgba(32, 178, 170, 255);
public static readonly Color LightSeaGreen = FromPixel(new Rgba32(32, 178, 170, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #87CEFA.
/// </summary>
public static readonly Color LightSkyBlue = FromRgba(135, 206, 250, 255);
public static readonly Color LightSkyBlue = FromPixel(new Rgba32(135, 206, 250, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #778899.
/// </summary>
public static readonly Color LightSlateGray = FromRgba(119, 136, 153, 255);
public static readonly Color LightSlateGray = FromPixel(new Rgba32(119, 136, 153, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #778899.
@ -414,27 +416,27 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B0C4DE.
/// </summary>
public static readonly Color LightSteelBlue = FromRgba(176, 196, 222, 255);
public static readonly Color LightSteelBlue = FromPixel(new Rgba32(176, 196, 222, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFE0.
/// </summary>
public static readonly Color LightYellow = FromRgba(255, 255, 224, 255);
public static readonly Color LightYellow = FromPixel(new Rgba32(255, 255, 224, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FF00.
/// </summary>
public static readonly Color Lime = FromRgba(0, 255, 0, 255);
public static readonly Color Lime = FromPixel(new Rgba32(0, 255, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #32CD32.
/// </summary>
public static readonly Color LimeGreen = FromRgba(50, 205, 50, 255);
public static readonly Color LimeGreen = FromPixel(new Rgba32(50, 205, 50, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FAF0E6.
/// </summary>
public static readonly Color Linen = FromRgba(250, 240, 230, 255);
public static readonly Color Linen = FromPixel(new Rgba32(250, 240, 230, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF00FF.
@ -444,237 +446,237 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #800000.
/// </summary>
public static readonly Color Maroon = FromRgba(128, 0, 0, 255);
public static readonly Color Maroon = FromPixel(new Rgba32(128, 0, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #66CDAA.
/// </summary>
public static readonly Color MediumAquamarine = FromRgba(102, 205, 170, 255);
public static readonly Color MediumAquamarine = FromPixel(new Rgba32(102, 205, 170, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #0000CD.
/// </summary>
public static readonly Color MediumBlue = FromRgba(0, 0, 205, 255);
public static readonly Color MediumBlue = FromPixel(new Rgba32(0, 0, 205, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #BA55D3.
/// </summary>
public static readonly Color MediumOrchid = FromRgba(186, 85, 211, 255);
public static readonly Color MediumOrchid = FromPixel(new Rgba32(186, 85, 211, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9370DB.
/// </summary>
public static readonly Color MediumPurple = FromRgba(147, 112, 219, 255);
public static readonly Color MediumPurple = FromPixel(new Rgba32(147, 112, 219, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #3CB371.
/// </summary>
public static readonly Color MediumSeaGreen = FromRgba(60, 179, 113, 255);
public static readonly Color MediumSeaGreen = FromPixel(new Rgba32(60, 179, 113, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7B68EE.
/// </summary>
public static readonly Color MediumSlateBlue = FromRgba(123, 104, 238, 255);
public static readonly Color MediumSlateBlue = FromPixel(new Rgba32(123, 104, 238, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FA9A.
/// </summary>
public static readonly Color MediumSpringGreen = FromRgba(0, 250, 154, 255);
public static readonly Color MediumSpringGreen = FromPixel(new Rgba32(0, 250, 154, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #48D1CC.
/// </summary>
public static readonly Color MediumTurquoise = FromRgba(72, 209, 204, 255);
public static readonly Color MediumTurquoise = FromPixel(new Rgba32(72, 209, 204, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #C71585.
/// </summary>
public static readonly Color MediumVioletRed = FromRgba(199, 21, 133, 255);
public static readonly Color MediumVioletRed = FromPixel(new Rgba32(199, 21, 133, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #191970.
/// </summary>
public static readonly Color MidnightBlue = FromRgba(25, 25, 112, 255);
public static readonly Color MidnightBlue = FromPixel(new Rgba32(25, 25, 112, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5FFFA.
/// </summary>
public static readonly Color MintCream = FromRgba(245, 255, 250, 255);
public static readonly Color MintCream = FromPixel(new Rgba32(245, 255, 250, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFE4E1.
/// </summary>
public static readonly Color MistyRose = FromRgba(255, 228, 225, 255);
public static readonly Color MistyRose = FromPixel(new Rgba32(255, 228, 225, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFE4B5.
/// </summary>
public static readonly Color Moccasin = FromRgba(255, 228, 181, 255);
public static readonly Color Moccasin = FromPixel(new Rgba32(255, 228, 181, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFDEAD.
/// </summary>
public static readonly Color NavajoWhite = FromRgba(255, 222, 173, 255);
public static readonly Color NavajoWhite = FromPixel(new Rgba32(255, 222, 173, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #000080.
/// </summary>
public static readonly Color Navy = FromRgba(0, 0, 128, 255);
public static readonly Color Navy = FromPixel(new Rgba32(0, 0, 128, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FDF5E6.
/// </summary>
public static readonly Color OldLace = FromRgba(253, 245, 230, 255);
public static readonly Color OldLace = FromPixel(new Rgba32(253, 245, 230, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #808000.
/// </summary>
public static readonly Color Olive = FromRgba(128, 128, 0, 255);
public static readonly Color Olive = FromPixel(new Rgba32(128, 128, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #6B8E23.
/// </summary>
public static readonly Color OliveDrab = FromRgba(107, 142, 35, 255);
public static readonly Color OliveDrab = FromPixel(new Rgba32(107, 142, 35, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFA500.
/// </summary>
public static readonly Color Orange = FromRgba(255, 165, 0, 255);
public static readonly Color Orange = FromPixel(new Rgba32(255, 165, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF4500.
/// </summary>
public static readonly Color OrangeRed = FromRgba(255, 69, 0, 255);
public static readonly Color OrangeRed = FromPixel(new Rgba32(255, 69, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DA70D6.
/// </summary>
public static readonly Color Orchid = FromRgba(218, 112, 214, 255);
public static readonly Color Orchid = FromPixel(new Rgba32(218, 112, 214, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #EEE8AA.
/// </summary>
public static readonly Color PaleGoldenrod = FromRgba(238, 232, 170, 255);
public static readonly Color PaleGoldenrod = FromPixel(new Rgba32(238, 232, 170, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #98FB98.
/// </summary>
public static readonly Color PaleGreen = FromRgba(152, 251, 152, 255);
public static readonly Color PaleGreen = FromPixel(new Rgba32(152, 251, 152, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #AFEEEE.
/// </summary>
public static readonly Color PaleTurquoise = FromRgba(175, 238, 238, 255);
public static readonly Color PaleTurquoise = FromPixel(new Rgba32(175, 238, 238, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DB7093.
/// </summary>
public static readonly Color PaleVioletRed = FromRgba(219, 112, 147, 255);
public static readonly Color PaleVioletRed = FromPixel(new Rgba32(219, 112, 147, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFEFD5.
/// </summary>
public static readonly Color PapayaWhip = FromRgba(255, 239, 213, 255);
public static readonly Color PapayaWhip = FromPixel(new Rgba32(255, 239, 213, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFDAB9.
/// </summary>
public static readonly Color PeachPuff = FromRgba(255, 218, 185, 255);
public static readonly Color PeachPuff = FromPixel(new Rgba32(255, 218, 185, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #CD853F.
/// </summary>
public static readonly Color Peru = FromRgba(205, 133, 63, 255);
public static readonly Color Peru = FromPixel(new Rgba32(205, 133, 63, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFC0CB.
/// </summary>
public static readonly Color Pink = FromRgba(255, 192, 203, 255);
public static readonly Color Pink = FromPixel(new Rgba32(255, 192, 203, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DDA0DD.
/// </summary>
public static readonly Color Plum = FromRgba(221, 160, 221, 255);
public static readonly Color Plum = FromPixel(new Rgba32(221, 160, 221, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B0E0E6.
/// </summary>
public static readonly Color PowderBlue = FromRgba(176, 224, 230, 255);
public static readonly Color PowderBlue = FromPixel(new Rgba32(176, 224, 230, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #800080.
/// </summary>
public static readonly Color Purple = FromRgba(128, 0, 128, 255);
public static readonly Color Purple = FromPixel(new Rgba32(128, 0, 128, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #663399.
/// </summary>
public static readonly Color RebeccaPurple = FromRgba(102, 51, 153, 255);
public static readonly Color RebeccaPurple = FromPixel(new Rgba32(102, 51, 153, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF0000.
/// </summary>
public static readonly Color Red = FromRgba(255, 0, 0, 255);
public static readonly Color Red = FromPixel(new Rgba32(255, 0, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #BC8F8F.
/// </summary>
public static readonly Color RosyBrown = FromRgba(188, 143, 143, 255);
public static readonly Color RosyBrown = FromPixel(new Rgba32(188, 143, 143, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #4169E1.
/// </summary>
public static readonly Color RoyalBlue = FromRgba(65, 105, 225, 255);
public static readonly Color RoyalBlue = FromPixel(new Rgba32(65, 105, 225, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8B4513.
/// </summary>
public static readonly Color SaddleBrown = FromRgba(139, 69, 19, 255);
public static readonly Color SaddleBrown = FromPixel(new Rgba32(139, 69, 19, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FA8072.
/// </summary>
public static readonly Color Salmon = FromRgba(250, 128, 114, 255);
public static readonly Color Salmon = FromPixel(new Rgba32(250, 128, 114, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F4A460.
/// </summary>
public static readonly Color SandyBrown = FromRgba(244, 164, 96, 255);
public static readonly Color SandyBrown = FromPixel(new Rgba32(244, 164, 96, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #2E8B57.
/// </summary>
public static readonly Color SeaGreen = FromRgba(46, 139, 87, 255);
public static readonly Color SeaGreen = FromPixel(new Rgba32(46, 139, 87, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFF5EE.
/// </summary>
public static readonly Color SeaShell = FromRgba(255, 245, 238, 255);
public static readonly Color SeaShell = FromPixel(new Rgba32(255, 245, 238, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A0522D.
/// </summary>
public static readonly Color Sienna = FromRgba(160, 82, 45, 255);
public static readonly Color Sienna = FromPixel(new Rgba32(160, 82, 45, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #C0C0C0.
/// </summary>
public static readonly Color Silver = FromRgba(192, 192, 192, 255);
public static readonly Color Silver = FromPixel(new Rgba32(192, 192, 192, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #87CEEB.
/// </summary>
public static readonly Color SkyBlue = FromRgba(135, 206, 235, 255);
public static readonly Color SkyBlue = FromPixel(new Rgba32(135, 206, 235, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #6A5ACD.
/// </summary>
public static readonly Color SlateBlue = FromRgba(106, 90, 205, 255);
public static readonly Color SlateBlue = FromPixel(new Rgba32(106, 90, 205, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #708090.
/// </summary>
public static readonly Color SlateGray = FromRgba(112, 128, 144, 255);
public static readonly Color SlateGray = FromPixel(new Rgba32(112, 128, 144, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #708090.
@ -684,81 +686,80 @@ public readonly partial struct Color
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFAFA.
/// </summary>
public static readonly Color Snow = FromRgba(255, 250, 250, 255);
public static readonly Color Snow = FromPixel(new Rgba32(255, 250, 250, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FF7F.
/// </summary>
public static readonly Color SpringGreen = FromRgba(0, 255, 127, 255);
public static readonly Color SpringGreen = FromPixel(new Rgba32(0, 255, 127, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #4682B4.
/// </summary>
public static readonly Color SteelBlue = FromRgba(70, 130, 180, 255);
public static readonly Color SteelBlue = FromPixel(new Rgba32(70, 130, 180, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D2B48C.
/// </summary>
public static readonly Color Tan = FromRgba(210, 180, 140, 255);
public static readonly Color Tan = FromPixel(new Rgba32(210, 180, 140, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #008080.
/// </summary>
public static readonly Color Teal = FromRgba(0, 128, 128, 255);
public static readonly Color Teal = FromPixel(new Rgba32(0, 128, 128, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D8BFD8.
/// </summary>
public static readonly Color Thistle = FromRgba(216, 191, 216, 255);
public static readonly Color Thistle = FromPixel(new Rgba32(216, 191, 216, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF6347.
/// </summary>
public static readonly Color Tomato = FromRgba(255, 99, 71, 255);
public static readonly Color Tomato = FromPixel(new Rgba32(255, 99, 71, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00000000.
/// </summary>
public static readonly Color Transparent = FromRgba(0, 0, 0, 0);
public static readonly Color Transparent = FromPixel(new Rgba32(0, 0, 0, 0));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #40E0D0.
/// </summary>
public static readonly Color Turquoise = FromRgba(64, 224, 208, 255);
public static readonly Color Turquoise = FromPixel(new Rgba32(64, 224, 208, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #EE82EE.
/// </summary>
public static readonly Color Violet = FromRgba(238, 130, 238, 255);
public static readonly Color Violet = FromPixel(new Rgba32(238, 130, 238, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5DEB3.
/// </summary>
public static readonly Color Wheat = FromRgba(245, 222, 179, 255);
public static readonly Color Wheat = FromPixel(new Rgba32(245, 222, 179, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFFF.
/// </summary>
public static readonly Color White = FromRgba(255, 255, 255, 255);
public static readonly Color White = FromPixel(new Rgba32(255, 255, 255, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5F5F5.
/// </summary>
public static readonly Color WhiteSmoke = FromRgba(245, 245, 245, 255);
public static readonly Color WhiteSmoke = FromPixel(new Rgba32(245, 245, 245, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFF00.
/// </summary>
public static readonly Color Yellow = FromRgba(255, 255, 0, 255);
public static readonly Color Yellow = FromPixel(new Rgba32(255, 255, 0, 255));
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9ACD32.
/// </summary>
public static readonly Color YellowGreen = FromRgba(154, 205, 50, 255);
public static readonly Color YellowGreen = FromPixel(new Rgba32(154, 205, 50, 255));
private static Dictionary<string, Color> CreateNamedColorsLookup()
{
return new Dictionary<string, Color>(StringComparer.OrdinalIgnoreCase)
=> new(StringComparer.OrdinalIgnoreCase)
{
{ nameof(AliceBlue), AliceBlue },
{ nameof(AntiqueWhite), AntiqueWhite },
@ -910,5 +911,4 @@ public readonly partial struct Color
{ nameof(Yellow), Yellow },
{ nameof(YellowGreen), YellowGreen }
};
}
}

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

548
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;
@ -18,34 +19,25 @@ namespace SixLabors.ImageSharp;
/// </remarks>
public readonly partial struct Color : IEquatable<Color>
{
private readonly Rgba64 data;
private readonly Vector4 data;
private readonly IPixel? boxedHighPrecisionPixel;
[MethodImpl(InliningOptions.ShortMethod)]
private Color(byte r, byte g, byte b, byte a)
{
this.data = new Rgba64(
ColorNumerics.UpscaleFrom8BitTo16Bit(r),
ColorNumerics.UpscaleFrom8BitTo16Bit(g),
ColorNumerics.UpscaleFrom8BitTo16Bit(b),
ColorNumerics.UpscaleFrom8BitTo16Bit(a));
this.boxedHighPrecisionPixel = null;
}
[MethodImpl(InliningOptions.ShortMethod)]
private Color(byte r, byte g, byte b)
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="vector">The <see cref="Vector4"/> containing the color information.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Color(Vector4 vector)
{
this.data = new Rgba64(
ColorNumerics.UpscaleFrom8BitTo16Bit(r),
ColorNumerics.UpscaleFrom8BitTo16Bit(g),
ColorNumerics.UpscaleFrom8BitTo16Bit(b),
ushort.MaxValue);
this.data = Numerics.Clamp(vector, Vector4.Zero, Vector4.One);
this.boxedHighPrecisionPixel = null;
}
[MethodImpl(InliningOptions.ShortMethod)]
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The pixel containing color information.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Color(IPixel pixel)
{
this.boxedHighPrecisionPixel = pixel;
@ -61,7 +53,7 @@ public readonly partial struct Color : IEquatable<Color>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter;
/// otherwise, false.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Color left, Color right) => left.Equals(right);
/// <summary>
@ -73,131 +65,168 @@ public readonly partial struct Color : IEquatable<Color>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter;
/// otherwise, false.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Color left, Color right) => !left.Equals(right);
/// <summary>
/// Creates a <see cref="Color"/> from RGBA bytes.
/// Creates a <see cref="Color"/> from the given <typeparamref name="TPixel"/>.
/// </summary>
/// <param name="r">The red component (0-255).</param>
/// <param name="g">The green component (0-255).</param>
/// <param name="b">The blue component (0-255).</param>
/// <param name="a">The alpha component (0-255).</param>
/// <param name="source">The pixel to convert from.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Color FromRgba(byte r, byte g, byte b, byte a) => new(r, g, b, a);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromPixel<TPixel>(TPixel source)
where TPixel : unmanaged, IPixel<TPixel>
{
// Avoid boxing in case we can convert to Vector4 safely and efficiently
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{
return new Color(source.ToScaledVector4());
}
return new Color(source);
}
/// <summary>
/// Creates a <see cref="Color"/> from RGB bytes.
/// Creates a <see cref="Color"/> from a generic scaled <see cref="Vector4"/>.
/// </summary>
/// <param name="r">The red component (0-255).</param>
/// <param name="g">The green component (0-255).</param>
/// <param name="b">The blue component (0-255).</param>
/// <param name="source">The vector to load the pixel from.</param>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Color FromRgb(byte r, byte g, byte b) => new(r, g, b);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source) => new(source);
/// <summary>
/// Creates a <see cref="Color"/> from the given <typeparamref name="TPixel"/>.
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
/// </summary>
/// <param name="pixel">The pixel to convert from.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Color FromPixel<TPixel>(TPixel pixel)
where TPixel : unmanaged, IPixel<TPixel>
/// <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)
{
// Avoid boxing in case we can convert to Rgba64 safely and efficently
if (typeof(TPixel) == typeof(Rgba64))
{
return new((Rgba64)(object)pixel);
}
else if (typeof(TPixel) == typeof(Rgb48))
{
return new((Rgb48)(object)pixel);
}
else if (typeof(TPixel) == typeof(La32))
{
return new((La32)(object)pixel);
}
else if (typeof(TPixel) == typeof(L16))
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
return new((L16)(object)pixel);
destination[i] = FromScaledVector(source[i]);
}
else if (Unsafe.SizeOf<TPixel>() <= Unsafe.SizeOf<Rgba32>())
}
/// <summary>
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel type to convert to.</typeparam>
/// <param name="source">The source pixel span.</param>
/// <param name="destination">The destination color span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromPixel<TPixel>(ReadOnlySpan<TPixel> source, Span<Color> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// Avoid boxing in case we can convert to Vector4 safely and efficiently
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{
Rgba32 p = default;
pixel.ToRgba32(ref p);
return new(p);
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i].ToScaledVector4());
}
}
else
{
return new(pixel);
for (int i = 0; i < source.Length; 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(InliningOptions.ShortMethod)]
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);
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 new Color(rgba);
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(InliningOptions.ShortMethod)]
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 = new Color(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));
}
@ -206,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;
@ -231,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>
@ -239,29 +277,48 @@ 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 = (Vector4)this;
Vector4 v = this.ToScaledVector4();
v.W = alpha;
return new Color(v);
return FromScaledVector(v);
}
/// <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>
[MethodImpl(InliningOptions.ShortMethod)]
public string ToHex() => this.data.ToRgba32().ToHex();
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="format"/> is not supported.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba)
{
Rgba32 rgba = (this.boxedHighPrecisionPixel is not null)
? this.boxedHighPrecisionPixel.ToRgba32()
: Rgba32.FromScaledVector4(this.data);
uint hexOrder = format switch
{
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 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.
/// </summary>
/// <typeparam name="TPixel">The pixel type to convert to.</typeparam>
/// <returns>The pixel value.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
/// <returns>The <typeparamref name="TPixel"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TPixel ToPixel<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
@ -272,14 +329,27 @@ public readonly partial struct Color : IEquatable<Color>
if (this.boxedHighPrecisionPixel is null)
{
pixel = default;
pixel.FromRgba64(this.data);
return pixel;
return TPixel.FromScaledVector4(this.data);
}
pixel = default;
pixel.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
return pixel;
return TPixel.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
}
/// <summary>
/// Expands the color 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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data;
}
return this.boxedHighPrecisionPixel.ToScaledVector4();
}
/// <summary>
@ -288,11 +358,12 @@ public readonly partial struct Color : IEquatable<Color>
/// <typeparam name="TPixel">The pixel type to convert to.</typeparam>
/// <param name="source">The source color span.</param>
/// <param name="destination">The destination pixel span.</param>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToPixel<TPixel>(ReadOnlySpan<Color> source, Span<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Investigate bulk operations utilizing configuration parameter here.
// We cannot use bulk pixel operations here as there is no guarantee that the source colors are
// created from pixel formats which fit into the unboxed vector data.
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
@ -301,12 +372,12 @@ public readonly partial struct Color : IEquatable<Color>
}
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Color other)
{
if (this.boxedHighPrecisionPixel is null && other.boxedHighPrecisionPixel is null)
{
return this.data.PackedValue == other.data.PackedValue;
return this.data == other.data;
}
return this.boxedHighPrecisionPixel?.Equals(other.boxedHighPrecisionPixel) == true;
@ -316,14 +387,251 @@ public readonly partial struct Color : IEquatable<Color>
public override bool Equals(object? obj) => obj is Color other && this.Equals(other);
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.PackedValue.GetHashCode();
return this.data.GetHashCode();
}
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
}

20
src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Enumerate the possible sources of the white point used in chromatic adaptation.
/// </summary>
public enum ChromaticAdaptionWhitePointSource
{
/// <summary>
/// The white point of the source color space.
/// </summary>
WhitePoint,
/// <summary>
/// The white point of the source working space.
/// </summary>
RgbWorkingSpace
}

21
src/ImageSharp/ColorProfiles/CieConstants.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Constants use for Cie conversion calculations
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html"/>
/// </summary>
internal static class CieConstants
{
/// <summary>
/// 216F / 24389F
/// </summary>
public const float Epsilon = 216f / 24389f;
/// <summary>
/// 24389F / 27F
/// </summary>
public const float Kappa = 24389f / 27f;
}

220
src/ImageSharp/ColorProfiles/CieLab.cs

@ -0,0 +1,220 @@
// 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 CIE L*a*b* 1976 color.
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="a">The a (green - magenta) component.</param>
/// <param name="b">The b (blue - yellow) component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLab(float l, float a, float b)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.L = l;
this.A = a;
this.B = b;
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, a, b components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLab(Vector3 vector)
{
this.L = vector.X;
this.A = vector.Y;
this.B = vector.Z;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
/// </summary>
public float L { get; }
/// <summary>
/// Gets the a color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is green, positive magenta.</remarks>
/// </summary>
public float A { get; }
/// <summary>
/// Gets the b color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is blue, positive is yellow</remarks>
/// </summary>
public float B { get; }
/// <summary>
/// Compares two <see cref="CieLab"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLab"/> 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 ==(CieLab left, CieLab right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLab"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLab"/> 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 !=(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)
{
// Conversion algorithm described here:
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
CieXyz whitePoint = options.TargetWhitePoint;
float wx = whitePoint.X, wy = whitePoint.Y, wz = whitePoint.Z;
float xr = source.X / wx, yr = source.Y / wy, zr = source.Z / wz;
const float inv116 = 1 / 116F;
float fx = xr > CieConstants.Epsilon ? MathF.Pow(xr, 0.3333333F) : ((CieConstants.Kappa * xr) + 16F) * inv116;
float fy = yr > CieConstants.Epsilon ? MathF.Pow(yr, 0.3333333F) : ((CieConstants.Kappa * yr) + 16F) * inv116;
float fz = zr > CieConstants.Epsilon ? MathF.Pow(zr, 0.3333333F) : ((CieConstants.Kappa * zr) + 16F) * inv116;
float l = (116F * fy) - 16F;
float a = 500F * (fx - fy);
float b = 200F * (fy - fz);
return new CieLab(l, a, b);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLab> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
float l = this.L, a = this.A, b = this.B;
float fy = (l + 16) / 116F;
float fx = (a / 500F) + fy;
float fz = fy - (b / 200F);
float fx3 = Numerics.Pow3(fx);
float fz3 = Numerics.Pow3(fz);
float xr = fx3 > CieConstants.Epsilon ? fx3 : ((116F * fx) - 16F) / CieConstants.Kappa;
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.SourceWhitePoint;
Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z);
Vector3 xyzr = new(xr, yr, zr);
return new CieXyz(xyzr * wxyz);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLab> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLab lab = source[i];
destination[i] = lab.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieLab other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLab other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<CieLab, Vector3>(ref Unsafe.AsRef(in this));
}

220
src/ImageSharp/ColorProfiles/CieLch.cs

@ -0,0 +1,220 @@
// 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 the CIE L*C*h°, cylindrical form of the CIE L*a*b* 1976 color.
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieLch : IColorProfile<CieLch, CieLab>
{
private static readonly Vector3 Min = new(0, -200, 0);
private static readonly Vector3 Max = new(100, 200, 360);
/// <summary>
/// Initializes a new instance of the <see cref="CieLch"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="c">The chroma, relative saturation.</param>
/// <param name="h">The hue in degrees.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLch(float l, float c, float h)
: this(new Vector3(l, c, h))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLch"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, c, h components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLch(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
this.C = vector.Y;
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>
/// </summary>
public float L { get; }
/// <summary>
/// Gets the a chroma component.
/// <remarks>A value ranging from -200 to 200.</remarks>
/// </summary>
public float C { get; }
/// <summary>
/// Gets the h° hue component in degrees.
/// <remarks>A value ranging from 0 to 360.</remarks>
/// </summary>
public float H { get; }
/// <summary>
/// Compares two <see cref="CieLch"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLch"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLch"/> 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 ==(CieLch left, CieLch right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLch"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieLch"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLch"/> 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 !=(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)
{
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
float l = source.L, a = source.A, b = source.B;
float c = MathF.Sqrt((a * a) + (b * b));
float hRadians = MathF.Atan2(b, a);
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
// Wrap the angle round at 360.
hDegrees %= 360;
// Make sure it's not negative.
while (hDegrees < 0)
{
hDegrees += 360;
}
return new CieLch(l, c, hDegrees);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLab> source, Span<CieLch> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLab lab = source[i];
destination[i] = FromProfileConnectingSpace(options, in lab);
}
}
/// <inheritdoc/>
public CieLab ToProfileConnectingSpace(ColorConversionOptions options)
{
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
float l = this.L, c = this.C, hDegrees = this.H;
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
float a = c * MathF.Cos(hRadians);
float b = c * MathF.Sin(hRadians);
return new CieLab(l, a, b);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLch> source, Span<CieLab> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLch lch = source[i];
destination[i] = lch.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.L, this.C, this.H);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLch({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool Equals(object? obj) => obj is CieLch other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLch other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<CieLch, Vector3>(ref Unsafe.AsRef(in this));
}

219
src/ImageSharp/ColorProfiles/CieLchuv.cs

@ -0,0 +1,219 @@
// 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 the CIE L*C*h°, cylindrical form of the CIE L*u*v* 1976 color.
/// <see href="https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
{
private static readonly Vector3 Min = new(0, -200, 0);
private static readonly Vector3 Max = new(100, 200, 360);
/// <summary>
/// Initializes a new instance of the <see cref="CieLchuv"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="c">The chroma, relative saturation.</param>
/// <param name="h">The hue in degrees.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLchuv(float l, float c, float h)
: this(new Vector3(l, c, h))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLchuv"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, c, h components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLchuv(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
this.C = vector.Y;
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>
/// </summary>
public float L { get; }
/// <summary>
/// Gets the a chroma component.
/// <remarks>A value ranging from -200 to 200.</remarks>
/// </summary>
public float C { get; }
/// <summary>
/// Gets the h° hue component in degrees.
/// <remarks>A value ranging from 0 to 360.</remarks>
/// </summary>
public float H { get; }
/// <summary>
/// Compares two <see cref="CieLchuv"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLchuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLchuv"/> 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>
public static bool operator ==(CieLchuv left, CieLchuv right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLchuv"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieLchuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLchuv"/> 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>
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)
{
CieLuv luv = CieLuv.FromProfileConnectingSpace(options, source);
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
float l = luv.L, u = luv.U, v = luv.V;
float c = MathF.Sqrt((u * u) + (v * v));
float hRadians = MathF.Atan2(v, u);
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
// Wrap the angle round at 360.
hDegrees %= 360;
// Make sure it's not negative.
while (hDegrees < 0)
{
hDegrees += 360;
}
return new CieLchuv(l, c, hDegrees);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLchuv> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
float l = this.L, c = this.C, hDegrees = this.H;
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
float u = c * MathF.Cos(hRadians);
float v = c * MathF.Sin(hRadians);
CieLuv luv = new(l, u, v);
return luv.ToProfileConnectingSpace(options);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLchuv> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLchuv lch = source[i];
destination[i] = lch.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.L, this.C, this.H);
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"CieLchuv({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is CieLchuv other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLchuv other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<CieLchuv, Vector3>(ref Unsafe.AsRef(in this));
}

232
src/ImageSharp/ColorProfiles/CieLuv.cs

@ -0,0 +1,232 @@
// 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>
/// The CIE 1976 (L*, u*, v*) color space, commonly known by its abbreviation CIELUV, is a color space adopted by the International
/// Commission on Illumination (CIE) in 1976, as a simple-to-compute transformation of the 1931 CIE XYZ color space, but which
/// attempted perceptual uniformity
/// <see href="https://en.wikipedia.org/wiki/CIELUV"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieLuv : IColorProfile<CieLuv, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieLuv"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="u">The blue-yellow chromaticity coordinate of the given white point.</param>
/// <param name="v">The red-green chromaticity coordinate of the given white point.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLuv(float l, float u, float v)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.L = l;
this.U = u;
this.V = v;
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLuv"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, u, v components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLuv(Vector3 vector)
{
this.L = vector.X;
this.U = vector.Y;
this.V = vector.Z;
}
/// <summary>
/// Gets the lightness dimension
/// <remarks>A value usually ranging between 0 and 100.</remarks>
/// </summary>
public float L { get; }
/// <summary>
/// Gets the blue-yellow chromaticity coordinate of the given white point.
/// <remarks>A value usually ranging between -100 and 100.</remarks>
/// </summary>
public float U { get; }
/// <summary>
/// Gets the red-green chromaticity coordinate of the given white point.
/// <remarks>A value usually ranging between -100 and 100.</remarks>
/// </summary>
public float V { get; }
/// <summary>
/// Compares two <see cref="CieLuv"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLuv"/> 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 ==(CieLuv left, CieLuv right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLuv"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="CieLuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLuv"/> 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 !=(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)
{
// Use doubles here for accuracy.
// Conversion algorithm described here:
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Luv.html
CieXyz whitePoint = options.TargetWhitePoint;
double yr = source.Y / whitePoint.Y;
double den = source.X + (15 * source.Y) + (3 * source.Z);
double up = den > 0 ? ComputeU(in source) : 0;
double vp = den > 0 ? ComputeV(in source) : 0;
double upr = ComputeU(in whitePoint);
double vpr = ComputeV(in whitePoint);
const double e = 1 / 3d;
double l = yr > CieConstants.Epsilon
? ((116 * Math.Pow(yr, e)) - 16d)
: (CieConstants.Kappa * yr);
if (double.IsNaN(l) || l == -0d)
{
l = 0;
}
double u = 13 * l * (up - upr);
double v = 13 * l * (vp - vpr);
if (double.IsNaN(u) || u == -0d)
{
u = 0;
}
if (double.IsNaN(v) || v == -0d)
{
v = 0;
}
return new CieLuv((float)l, (float)u, (float)v);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLuv> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// Use doubles here for accuracy.
// Conversion algorithm described here:
// http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html
CieXyz whitePoint = options.SourceWhitePoint;
double l = this.L, u = this.U, v = this.V;
double u0 = ComputeU(in whitePoint);
double v0 = ComputeV(in whitePoint);
double y = l > CieConstants.Kappa * CieConstants.Epsilon
? Numerics.Pow3((l + 16) / 116d)
: l / CieConstants.Kappa;
double a = ((52 * l / (u + (13 * l * u0))) - 1) / 3;
double b = -5 * y;
const double c = -1 / 3d;
double d = y * ((39 * l / (v + (13 * l * v0))) - 5);
double x = (d - b) / (a - c);
double z = (x * a) + b;
if (double.IsNaN(x) || x == -0d)
{
x = 0;
}
if (double.IsNaN(y) || y == -0d)
{
y = 0;
}
if (double.IsNaN(z) || z == -0d)
{
z = 0;
}
return new CieXyz((float)x, (float)y, (float)z);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLuv> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLuv luv = source[i];
destination[i] = luv.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.U, this.V);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLuv({this.L:#0.##}, {this.U:#0.##}, {this.V:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieLuv other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLuv other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<CieLuv, Vector3>(ref Unsafe.AsRef(in this));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeU(in CieXyz source)
=> (4 * source.X) / (source.X + (15 * source.Y) + (3 * source.Z));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeV(in CieXyz source)
=> (9 * source.Y) / (source.X + (15 * source.Y) + (3 * source.Z));
}

87
src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs

@ -0,0 +1,87 @@
// 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 the coordinates of CIEXY chromaticity space.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieXyChromaticityCoordinates : IEquatable<CieXyChromaticityCoordinates>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieXyChromaticityCoordinates"/> struct.
/// </summary>
/// <param name="x">Chromaticity coordinate x (usually from 0 to 1)</param>
/// <param name="y">Chromaticity coordinate y (usually from 0 to 1)</param>
[MethodImpl(InliningOptions.ShortMethod)]
public CieXyChromaticityCoordinates(float x, float y)
{
this.X = x;
this.Y = y;
}
/// <summary>
/// Gets the chromaticity X-coordinate.
/// </summary>
/// <remarks>
/// Ranges usually from 0 to 1.
/// </remarks>
public float X { get; }
/// <summary>
/// Gets the chromaticity Y-coordinate
/// </summary>
/// <remarks>
/// Ranges usually from 0 to 1.
/// </remarks>
public float Y { get; }
/// <summary>
/// Compares two <see cref="CieXyChromaticityCoordinates"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieXyChromaticityCoordinates"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyChromaticityCoordinates"/> 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(InliningOptions.ShortMethod)]
public static bool operator ==(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right)
=> left.Equals(right);
/// <summary>
/// Compares two <see cref="CieXyChromaticityCoordinates"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieXyChromaticityCoordinates"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyChromaticityCoordinates"/> 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(InliningOptions.ShortMethod)]
public static bool operator !=(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right)
=> !left.Equals(right);
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public override int GetHashCode()
=> HashCode.Combine(this.X, this.Y);
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"CieXyChromaticityCoordinates({this.X:#0.##}, {this.Y:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is CieXyChromaticityCoordinates other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool Equals(CieXyChromaticityCoordinates other)
=> this.AsVector2Unsafe() == other.AsVector2Unsafe();
private Vector2 AsVector2Unsafe() => Unsafe.As<CieXyChromaticityCoordinates, Vector2>(ref Unsafe.AsRef(in this));
}

189
src/ImageSharp/ColorProfiles/CieXyy.cs

@ -0,0 +1,189 @@
// 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 an CIE xyY 1931 color
/// <see href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieXyy"/> struct.
/// </summary>
/// <param name="x">The x chroma component.</param>
/// <param name="y">The y chroma component.</param>
/// <param name="yl">The y luminance component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(float x, float y, float yl)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.X = x;
this.Y = y;
this.Yl = yl;
}
/// <summary>
/// Initializes a new instance of the <see cref="CieXyy"/> struct.
/// </summary>
/// <param name="vector">The vector representing the x, y, Y components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(Vector3 vector)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.X = vector.X;
this.Y = vector.Y;
this.Yl = vector.Z;
}
/// <summary>
/// Gets the X chrominance component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float X { get; }
/// <summary>
/// Gets the Y chrominance component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float Y { get; }
/// <summary>
/// Gets the Y luminance component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float Yl { get; }
/// <summary>
/// Compares two <see cref="CieXyy"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieXyy"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyy"/> 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 ==(CieXyy left, CieXyy right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieXyy"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="CieXyy"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyy"/> 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 !=(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)
{
float x = source.X / (source.X + source.Y + source.Z);
float y = source.Y / (source.X + source.Y + source.Z);
if (float.IsNaN(x) || float.IsNaN(y))
{
return new CieXyy(0, 0, source.Y);
}
return new CieXyy(x, y, source.Y);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyy> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
if (MathF.Abs(this.Y) < Constants.Epsilon)
{
return new CieXyz(0, 0, this.Yl);
}
float x = (this.X * this.Yl) / this.Y;
float y = this.Yl;
float z = ((1 - this.X - this.Y) * y) / this.Y;
return new CieXyz(x, y, z);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyy> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyy xyz = source[i];
destination[i] = xyz.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.X, this.Y, this.Yl);
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"CieXyy({this.X:#0.##}, {this.Y:#0.##}, {this.Yl:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieXyy other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieXyy other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<CieXyy, Vector3>(ref Unsafe.AsRef(in this));
}

203
src/ImageSharp/ColorProfiles/CieXyz.cs

@ -0,0 +1,203 @@
// 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 an CIE XYZ 1931 color
/// <see href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct CieXyz : IProfileConnectingSpace<CieXyz, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieXyz"/> struct.
/// </summary>
/// <param name="x">X is a mix (a linear combination) of cone response curves chosen to be nonnegative</param>
/// <param name="y">The y luminance component.</param>
/// <param name="z">Z is quasi-equal to blue stimulation, or the S cone of the human eye.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyz(float x, float y, float z)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.X = x;
this.Y = y;
this.Z = z;
}
/// <summary>
/// Initializes a new instance of the <see cref="CieXyz"/> struct.
/// </summary>
/// <param name="vector">The vector representing the x, y, z components.</param>
public CieXyz(Vector3 vector)
{
this.X = vector.X;
this.Y = vector.Y;
this.Z = vector.Z;
}
/// <summary>
/// Gets the X component. A mix (a linear combination) of cone response curves chosen to be nonnegative.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float X { get; }
/// <summary>
/// Gets the Y luminance component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float Y { get; }
/// <summary>
/// Gets the Z component. Quasi-equal to blue stimulation, or the S cone response.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float Z { get; }
/// <summary>
/// Compares two <see cref="CieXyz"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieXyz"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyz"/> 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 ==(CieXyz left, CieXyz right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieXyz"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="CieXyz"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyz"/> 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 !=(CieXyz left, CieXyz right) => !left.Equals(right);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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)
=> new(source.X, source.Y, source.Z);
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
source.CopyTo(destination[..source.Length]);
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
=> new(this.X, this.Y, this.Z);
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
source.CopyTo(destination[..source.Length]);
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieXyz({this.X:#0.##}, {this.Y:#0.##}, {this.Z:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieXyz other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieXyz other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
internal Vector3 AsVector3Unsafe() => Unsafe.As<CieXyz, Vector3>(ref Unsafe.AsRef(in this));
}

202
src/ImageSharp/ColorProfiles/Cmyk.cs

@ -0,0 +1,202 @@
// 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 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>
{
private static readonly Vector4 Min = Vector4.Zero;
private static readonly Vector4 Max = Vector4.One;
/// <summary>
/// Initializes a new instance of the <see cref="Cmyk"/> struct.
/// </summary>
/// <param name="c">The cyan component.</param>
/// <param name="m">The magenta component.</param>
/// <param name="y">The yellow component.</param>
/// <param name="k">The keyline black component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmyk(float c, float m, float y, float k)
: this(new Vector4(c, m, y, k))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Cmyk"/> struct.
/// </summary>
/// <param name="vector">The vector representing the c, m, y, k components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmyk(Vector4 vector)
{
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;
this.K = vector.W;
}
/// <summary>
/// Gets the cyan color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float C { get; }
/// <summary>
/// Gets the magenta color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float M { get; }
/// <summary>
/// Gets the yellow color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float Y { 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="Cmyk"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Cmyk"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Cmyk"/> 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 ==(Cmyk left, Cmyk right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Cmyk"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Cmyk"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Cmyk"/> 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 !=(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.AsVector3Unsafe();
// To CMYK
Vector3 k = new(MathF.Min(cmy.X, MathF.Min(cmy.Y, cmy.Z)));
if (k.X >= 1F - Constants.Epsilon)
{
return new Cmyk(0, 0, 0, 1F);
}
cmy = (cmy - k) / (Vector3.One - k);
return new Cmyk(cmy.X, cmy.Y, cmy.Z, k.X);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<Cmyk> 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 Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (1F - this.K);
return Rgb.FromScaledVector3(rgb);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Cmyk> 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 ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
=> HashCode.Combine(this.C, this.M, this.Y, this.K);
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"Cmyk({this.C:#0.##}, {this.M:#0.##}, {this.Y:#0.##}, {this.K:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is Cmyk other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Cmyk other)
=> this.AsVector4Unsafe() == other.AsVector4Unsafe();
private Vector4 AsVector4Unsafe() => Unsafe.As<Cmyk, Vector4>(ref Unsafe.AsRef(in this));
}

94
src/ImageSharp/ColorProfiles/ColorConversionOptions.cs

@ -0,0 +1,94 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Provides options for color profile conversion.
/// </summary>
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;
this.YCbCrTransform = KnownYCbCrMatrices.BT601;
}
/// <summary>
/// Gets the memory allocator.
/// </summary>
public MemoryAllocator MemoryAllocator { get; init; } = MemoryAllocator.Default;
/// <summary>
/// Gets the source white point used for chromatic adaptation in conversions from/to XYZ color space.
/// </summary>
public CieXyz SourceWhitePoint { get; init; } = KnownIlluminants.D50;
/// <summary>
/// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space.
/// </summary>
public CieXyz TargetWhitePoint { get; init; } = KnownIlluminants.D50;
/// <summary>
/// Gets the source working space used for companding in conversions from/to XYZ color space.
/// </summary>
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.
/// </summary>
public Matrix4x4 AdaptationMatrix
{
get => this.adaptationMatrix;
init
{
this.adaptationMatrix = value;
_ = Matrix4x4.Invert(value, out Matrix4x4 inverted);
this.InverseAdaptationMatrix = inverted;
}
}
internal YCbCrTransform TransposedYCbCrTransform { get; private set; }
internal Matrix4x4 InverseAdaptationMatrix { get; private set; }
}

48
src/ImageSharp/ColorProfiles/ColorProfileConverter.cs

@ -0,0 +1,48 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Allows the conversion of color profiles.
/// </summary>
public class ColorProfileConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorProfileConverter"/> class.
/// </summary>
public ColorProfileConverter()
: this(new ColorConversionOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorProfileConverter"/> class.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
public ColorProfileConverter(ColorConversionOptions options)
=> this.Options = options;
/// <summary>
/// Gets the color profile conversion options.
/// </summary>
public ColorConversionOptions Options { get; }
internal (CieXyz From, CieXyz To) GetChromaticAdaptionWhitePoints<TFrom, TTo>()
where TFrom : struct, IColorProfile
where TTo : struct, IColorProfile
{
CieXyz sourceWhitePoint = TFrom.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint
? this.Options.SourceWhitePoint
: this.Options.SourceRgbWorkingSpace.WhitePoint;
CieXyz targetWhitePoint = TTo.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint
? this.Options.TargetWhitePoint
: this.Options.TargetRgbWorkingSpace.WhitePoint;
return (sourceWhitePoint, targetWhitePoint);
}
internal bool ShouldUseIccProfiles()
=> this.Options.SourceIccProfile != null && this.Options.TargetIccProfile != null;
}

98
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs

@ -0,0 +1,98 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
CieLab pcsFromA = source.ToProfileConnectingSpace(options);
CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
// Convert between PCS
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB);
// Convert to output from PCS
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.
using IMemoryOwner<CieLab> pcsFromToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsFromTo = pcsFromToOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFromTo);
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
CieLab.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert between PCS.
CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsFromTo, destination);
}
}

95
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
CieLab pcsFrom = source.ToProfileConnectingSpace(options);
// Convert between PCS
CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsTo = VonKriesChromaticAdaptation.Transform(in pcsTo, whitePoints, options.AdaptationMatrix);
// Convert to output from PCS
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.
using IMemoryOwner<CieLab> pcsFromOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Convert between PCS.
using IMemoryOwner<CieXyz> pcsToOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsTo = pcsToOwner.GetSpan();
CieLab.ToProfileConnectionSpace(options, pcsFrom, pcsTo);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsTo, pcsTo, whitePoints, options.AdaptationMatrix);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

100
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs

@ -0,0 +1,100 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
CieLab pcsFromA = source.ToProfileConnectingSpace(options);
CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
// Convert between PCS
Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB);
// Convert to output from PCS
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.
using IMemoryOwner<CieLab> pcsFromAOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsFromA = pcsFromAOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFromA);
using IMemoryOwner<CieXyz> pcsFromBOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFromB = pcsFromBOwner.GetSpan();
CieLab.ToProfileConnectionSpace(options, pcsFromA, pcsFromB);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFromB, pcsFromB, whitePoints, options.AdaptationMatrix);
// Convert between PCS.
using IMemoryOwner<Rgb> pcsToOwner = options.MemoryAllocator.Allocate<Rgb>(source.Length);
Span<Rgb> pcsTo = pcsToOwner.GetSpan();
Rgb.FromProfileConnectionSpace(options, pcsFromB, pcsTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

95
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert between PCS
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom);
// Convert to output from PCS
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.
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert between PCS.
using IMemoryOwner<CieLab> pcsToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsTo = pcsToOwner.GetSpan();
CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

87
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

@ -0,0 +1,87 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert to output from PCS
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.
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsFrom, destination);
}
}

95
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert between PCS
Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFrom);
// Convert to output from PCS
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.
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert between PCS.
using IMemoryOwner<Rgb> pcsToOwner = options.MemoryAllocator.Allocate<Rgb>(source.Length);
Span<Rgb> pcsTo = pcsToOwner.GetSpan();
Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

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

100
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs

@ -0,0 +1,100 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
Rgb pcsFromA = source.ToProfileConnectingSpace(options);
CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
// Convert between PCS
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB);
// Convert to output from PCS
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.
using IMemoryOwner<Rgb> pcsFromAOwner = options.MemoryAllocator.Allocate<Rgb>(source.Length);
Span<Rgb> pcsFromA = pcsFromAOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFromA);
using IMemoryOwner<CieXyz> pcsFromBOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFromB = pcsFromBOwner.GetSpan();
Rgb.ToProfileConnectionSpace(options, pcsFromA, pcsFromB);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFromB, pcsFromB, whitePoints, options.AdaptationMatrix);
// Convert between PCS.
using IMemoryOwner<CieLab> pcsToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsTo = pcsToOwner.GetSpan();
CieLab.FromProfileConnectionSpace(options, pcsFromB, pcsTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

95
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
Rgb pcsFrom = source.ToProfileConnectingSpace(options);
// Convert between PCS
CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsTo = VonKriesChromaticAdaptation.Transform(in pcsTo, whitePoints, options.AdaptationMatrix);
// Convert to output from PCS
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.
using IMemoryOwner<Rgb> pcsFromOwner = options.MemoryAllocator.Allocate<Rgb>(source.Length);
Span<Rgb> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Convert between PCS.
using IMemoryOwner<CieXyz> pcsToOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsTo = pcsToOwner.GetSpan();
Rgb.ToProfileConnectionSpace(options, pcsFrom, pcsTo);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsTo, pcsTo, whitePoints, options.AdaptationMatrix);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

98
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

@ -0,0 +1,98 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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
Rgb pcsFromA = source.ToProfileConnectingSpace(options);
CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
// Convert between PCS
Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB);
// Convert to output from PCS
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.
using IMemoryOwner<Rgb> pcsFromToOwner = options.MemoryAllocator.Allocate<Rgb>(source.Length);
Span<Rgb> pcsFromTo = pcsFromToOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFromTo);
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
Rgb.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom);
// Adapt to target white point
(CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints<TFrom, TTo>();
VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
// Convert between PCS.
Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsFromTo, destination);
}
}

182
src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs

@ -0,0 +1,182 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
/// <summary>
/// Companding utilities that allow the accelerated compression-expansion of color channels.
/// </summary>
public static class CompandingUtilities
{
private const int Length = Scale + 2; // 256kb @ 16bit precision.
private const int Scale = (1 << 16) - 1;
private static readonly ConcurrentDictionary<(Type, double), float[]> CompressLookupTables = new();
private static readonly ConcurrentDictionary<(Type, double), float[]> ExpandLookupTables = new();
/// <summary>
/// Lazily creates and stores a companding compression lookup table using the given function and modifier.
/// </summary>
/// <typeparam name="T">The type of companding function.</typeparam>
/// <param name="compandingFunction">The companding function.</param>
/// <param name="modifier">A modifier to pass to the function.</param>
/// <returns>The <see cref="float"/> array.</returns>
public static float[] GetCompressLookupTable<T>(Func<double, double, double> compandingFunction, double modifier = 0)
=> CompressLookupTables.GetOrAdd((typeof(T), modifier), args => CreateLookupTableImpl(compandingFunction, args.Item2));
/// <summary>
/// Lazily creates and stores a companding expanding lookup table using the given function and modifier.
/// </summary>
/// <typeparam name="T">The type of companding function.</typeparam>
/// <param name="compandingFunction">The companding function.</param>
/// <param name="modifier">A modifier to pass to the function.</param>
/// <returns>The <see cref="float"/> array.</returns>
public static float[] GetExpandLookupTable<T>(Func<double, double, double> compandingFunction, double modifier = 0)
=> ExpandLookupTables.GetOrAdd((typeof(T), modifier), args => CreateLookupTableImpl(compandingFunction, args.Item2));
/// <summary>
/// Creates a companding lookup table using the given function.
/// </summary>
/// <param name="compandingFunction">The companding function.</param>
/// <param name="modifier">A modifier to pass to the function.</param>
/// <returns>The <see cref="float"/> array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float[] CreateLookupTableImpl(Func<double, double, double> compandingFunction, double modifier = 0)
{
float[] result = new float[Length];
for (int i = 0; i < result.Length; i++)
{
double d = (double)i / Scale;
d = compandingFunction(d, modifier);
result[i] = (float)d;
}
return result;
}
/// <summary>
/// Performs the companding operation on the given vectors using the given table.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
/// <param name="table">The lookup table.</param>
public static void Compand(Span<Vector4> vectors, float[] table)
{
DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table));
if (Avx2.IsSupported && vectors.Length >= 2)
{
CompandAvx2(vectors, table);
if (Numerics.Modulo2(vectors.Length) != 0)
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
ref Vector4 last = ref MemoryMarshal.GetReference(vectors[^1..]);
last = Compand(last, table);
}
}
else
{
CompandScalar(vectors, table);
}
}
/// <summary>
/// Performs the companding operation on the given vector using the given table.
/// </summary>
/// <param name="vector">The vector.</param>
/// <param name="table">The lookup table.</param>
/// <returns>The <see cref="Vector4"/></returns>
public static Vector4 Compand(Vector4 vector, float[] table)
{
DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table));
Vector4 zero = Vector4.Zero;
Vector4 scale = new(Scale);
Vector4 multiplied = Numerics.Clamp(vector * Scale, zero, scale);
float f0 = multiplied.X;
float f1 = multiplied.Y;
float f2 = multiplied.Z;
uint i0 = (uint)f0;
uint i1 = (uint)f1;
uint i2 = (uint)f2;
// Alpha is already a linear representation of opacity so we do not want to convert it.
vector.X = Numerics.Lerp(table[i0], table[i0 + 1], f0 - (int)i0);
vector.Y = Numerics.Lerp(table[i1], table[i1 + 1], f1 - (int)i1);
vector.Z = Numerics.Lerp(table[i2], table[i2 + 1], f2 - (int)i2);
return vector;
}
private static unsafe void CompandAvx2(Span<Vector4> vectors, float[] table)
{
fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table))
{
Vector256<float> scale = Vector256.Create((float)Scale);
Vector256<float> zero = Vector256<float>.Zero;
Vector256<int> offset = Vector256.Create(1);
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
{
Vector256<float> multiplied = Avx.Multiply(scale, vectorsBase);
multiplied = Avx.Min(Avx.Max(zero, multiplied), scale);
Vector256<int> truncated = Avx.ConvertToVector256Int32WithTruncation(multiplied);
Vector256<float> truncatedF = Avx.ConvertToVector256Single(truncated);
Vector256<float> low = Avx2.GatherVector256(tablePointer, truncated, sizeof(float));
Vector256<float> high = Avx2.GatherVector256(tablePointer, Avx2.Add(truncated, offset), sizeof(float));
// Alpha is already a linear representation of opacity so we do not want to convert it.
Vector256<float> companded = Numerics.Lerp(low, high, Avx.Subtract(multiplied, truncatedF));
vectorsBase = Avx.Blend(companded, vectorsBase, Numerics.BlendAlphaControl);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
}
}
private static unsafe void CompandScalar(Span<Vector4> vectors, float[] table)
{
fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table))
{
Vector4 zero = Vector4.Zero;
Vector4 scale = new(Scale);
ref Vector4 vectorsBase = ref MemoryMarshal.GetReference(vectors);
ref Vector4 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
{
Vector4 multiplied = Numerics.Clamp(vectorsBase * Scale, zero, scale);
float f0 = multiplied.X;
float f1 = multiplied.Y;
float f2 = multiplied.Z;
uint i0 = (uint)f0;
uint i1 = (uint)f1;
uint i2 = (uint)f2;
// Alpha is already a linear representation of opacity so we do not want to convert it.
vectorsBase.X = Numerics.Lerp(tablePointer[i0], tablePointer[i0 + 1], f0 - (int)i0);
vectorsBase.Y = Numerics.Lerp(tablePointer[i1], tablePointer[i1 + 1], f1 - (int)i1);
vectorsBase.Z = Numerics.Lerp(tablePointer[i2], tablePointer[i2 + 1], f2 - (int)i2);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
}
}
}

56
src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs

@ -0,0 +1,56 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
/// <summary>
/// Implements gamma companding.
/// </summary>
/// <remarks>
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html"/>
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html"/>
/// </remarks>
public static class GammaCompanding
{
private static Func<double, double, double> CompressFunction => (d, m) => Math.Pow(d, 1 / m);
private static Func<double, double, double> ExpandFunction => Math.Pow;
/// <summary>
/// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
/// <param name="gamma">The gamma value.</param>
public static void Compress(Span<Vector4> vectors, double gamma)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable<GammaCompandingKey>(CompressFunction, gamma));
/// <summary>
/// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
/// <param name="gamma">The gamma value.</param>
public static void Expand(Span<Vector4> vectors, double gamma)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable<GammaCompandingKey>(ExpandFunction, gamma));
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <param name="gamma">The gamma value.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Compress(Vector4 vector, double gamma)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable<GammaCompandingKey>(CompressFunction, gamma));
/// <summary>
/// Expands the nonlinear vector to its linear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <param name="gamma">The gamma value.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Expand(Vector4 vector, double gamma)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable<GammaCompandingKey>(ExpandFunction, gamma));
private class GammaCompandingKey;
}

71
src/ImageSharp/ColorProfiles/Companding/LCompanding.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
/// <summary>
/// Implements L* companding.
/// </summary>
/// <remarks>
/// For more info see:
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html"/>
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html"/>
/// </remarks>
public static class LCompanding
{
private static Func<double, double, double> CompressFunction
=> (d, _) =>
{
if (d <= CieConstants.Epsilon)
{
return (d * CieConstants.Kappa) / 100;
}
return (1.16 * Math.Pow(d, 0.3333333)) - 0.16;
};
private static Func<double, double, double> ExpandFunction
=> (d, _) =>
{
if (d <= 0.08)
{
return (100 * d) / CieConstants.Kappa;
}
return Numerics.Pow3(((float)(d + 0.16f)) / 1.16f);
};
/// <summary>
/// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Compress(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable<LCompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Expand(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable<LCompandingKey>(ExpandFunction));
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Compress(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable<LCompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vector to its linear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Expand(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable<LCompandingKey>(ExpandFunction));
private class LCompandingKey;
}

75
src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
/// <summary>
/// Implements Rec. 2020 companding function.
/// </summary>
/// <remarks>
/// <see href="http://en.wikipedia.org/wiki/Rec._2020"/>
/// </remarks>
public static class Rec2020Companding
{
private const double Alpha = 1.09929682680944;
private const double AlphaMinusOne = Alpha - 1;
private const double Beta = 0.018053968510807;
private const double InverseBeta = Beta * 4.5;
private const double Epsilon = 1 / 0.45;
private static Func<double, double, double> CompressFunction
=> (d, _) =>
{
if (d < Beta)
{
return 4.5 * d;
}
return (Alpha * Math.Pow(d, 0.45)) - AlphaMinusOne;
};
private static Func<double, double, double> ExpandFunction
=> (d, _) =>
{
if (d < InverseBeta)
{
return d / 4.5;
}
return Math.Pow((d + AlphaMinusOne) / Alpha, Epsilon);
};
/// <summary>
/// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Compress(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable<Rec2020CompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Expand(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable<Rec2020CompandingKey>(ExpandFunction));
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Compress(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable<Rec2020CompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vector to its linear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Expand(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable<Rec2020CompandingKey>(ExpandFunction));
private class Rec2020CompandingKey;
}

71
src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
/// <summary>
/// Implements the Rec. 709 companding function.
/// </summary>
/// <remarks>
/// http://en.wikipedia.org/wiki/Rec._709
/// </remarks>
public static class Rec709Companding
{
private const double Epsilon = 1 / 0.45;
private static Func<double, double, double> CompressFunction
=> (d, _) =>
{
if (d < 0.018)
{
return 4.5 * d;
}
return (1.099 * Math.Pow(d, 0.45)) - 0.099;
};
private static Func<double, double, double> ExpandFunction
=> (d, _) =>
{
if (d < 0.081)
{
return d / 4.5;
}
return Math.Pow((d + 0.099) / 1.099, Epsilon);
};
/// <summary>
/// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Compress(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable<Rec2020CompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Expand(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable<Rec2020CompandingKey>(ExpandFunction));
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Compress(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable<Rec2020CompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vector to its linear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Expand(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable<Rec2020CompandingKey>(ExpandFunction));
private class Rec2020CompandingKey;
}

71
src/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
/// <summary>
/// Implements sRGB companding.
/// </summary>
/// <remarks>
/// For more info see:
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html"/>
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html"/>
/// </remarks>
public static class SRgbCompanding
{
private static Func<double, double, double> CompressFunction
=> (d, _) =>
{
if (d <= (0.04045 / 12.92))
{
return d * 12.92;
}
return (1.055 * Math.Pow(d, 1.0 / 2.4)) - 0.055;
};
private static Func<double, double, double> ExpandFunction
=> (d, _) =>
{
if (d <= 0.04045)
{
return d / 12.92;
}
return Math.Pow((d + 0.055) / 1.055, 2.4);
};
/// <summary>
/// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Compress(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable<SRgbCompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public static void Expand(Span<Vector4> vectors)
=> CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable<SRgbCompandingKey>(ExpandFunction));
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Compress(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable<SRgbCompandingKey>(CompressFunction));
/// <summary>
/// Expands the nonlinear vector to its linear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static Vector4 Expand(Vector4 vector)
=> CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable<SRgbCompandingKey>(ExpandFunction));
private class SRgbCompandingKey;
}

287
src/ImageSharp/ColorProfiles/Hsl.cs

@ -0,0 +1,287 @@
// 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 Hsl (hue, saturation, lightness) color.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct Hsl : IColorProfile<Hsl, Rgb>
{
private static readonly Vector3 Min = Vector3.Zero;
private static readonly Vector3 Max = new(360, 1, 1);
/// <summary>
/// Initializes a new instance of the <see cref="Hsl"/> struct.
/// </summary>
/// <param name="h">The h hue component.</param>
/// <param name="s">The s saturation component.</param>
/// <param name="l">The l value (lightness) component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Hsl(float h, float s, float l)
: this(new Vector3(h, s, l))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Hsl"/> struct.
/// </summary>
/// <param name="vector">The vector representing the h, s, l components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Hsl(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
this.H = vector.X;
this.S = vector.Y;
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>
/// </summary>
public float H { get; }
/// <summary>
/// Gets the saturation component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float S { get; }
/// <summary>
/// Gets the lightness component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float L { get; }
/// <summary>
/// Compares two <see cref="Hsl"/> objects for equality.
/// </summary>
/// <param name="left">
/// The <see cref="Hsl"/> on the left side of the operand.
/// </param>
/// <param name="right">The <see cref="Hsl"/> 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 ==(Hsl left, Hsl right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Hsl"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Hsl"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Hsl"/> 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 !=(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)
{
float r = source.R;
float g = source.G;
float b = source.B;
float max = MathF.Max(r, MathF.Max(g, b));
float min = MathF.Min(r, MathF.Min(g, b));
float chroma = max - min;
float h = 0F;
float s = 0F;
float l = (max + min) / 2F;
if (MathF.Abs(chroma) < Constants.Epsilon)
{
return new Hsl(0F, s, l);
}
if (MathF.Abs(r - max) < Constants.Epsilon)
{
h = (g - b) / chroma;
}
else if (MathF.Abs(g - max) < Constants.Epsilon)
{
h = 2F + ((b - r) / chroma);
}
else if (MathF.Abs(b - max) < Constants.Epsilon)
{
h = 4F + ((r - g) / chroma);
}
h *= 60F;
if (h < -Constants.Epsilon)
{
h += 360F;
}
if (l <= .5F)
{
s = chroma / (max + min);
}
else
{
s = chroma / (2F - max - min);
}
return new Hsl(h, s, l);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<Hsl> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
Rgb rgb = source[i];
destination[i] = FromProfileConnectingSpace(options, in rgb);
}
}
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
float rangedH = this.H / 360F;
float r = 0;
float g = 0;
float b = 0;
float s = this.S;
float l = this.L;
if (MathF.Abs(l) > Constants.Epsilon)
{
if (MathF.Abs(s) < Constants.Epsilon)
{
r = g = b = l;
}
else
{
float temp2 = (l < .5F) ? l * (1F + s) : l + s - (l * s);
float temp1 = (2F * l) - temp2;
r = GetColorComponent(temp1, temp2, rangedH + 0.3333333F);
g = GetColorComponent(temp1, temp2, rangedH);
b = GetColorComponent(temp1, temp2, rangedH - 0.3333333F);
}
}
return new Rgb(r, g, b);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Hsl> source, Span<Rgb> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
Hsl hsl = source[i];
destination[i] = hsl.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(this.H, this.S, this.L);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"Hsl({this.H:#0.##}, {this.S:#0.##}, {this.L:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Hsl other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Hsl other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<Hsl, Vector3>(ref Unsafe.AsRef(in this));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float GetColorComponent(float first, float second, float third)
{
third = MoveIntoRange(third);
if (third < 0.1666667F)
{
return first + ((second - first) * 6F * third);
}
if (third < .5F)
{
return second;
}
if (third < 0.6666667F)
{
return first + ((second - first) * (0.6666667F - third) * 6F);
}
return first;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float MoveIntoRange(float value)
{
if (value < 0F)
{
value++;
}
else if (value > 1F)
{
value--;
}
return value;
}
}

273
src/ImageSharp/ColorProfiles/Hsv.cs

@ -0,0 +1,273 @@
// 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 HSV (hue, saturation, value) color. Also known as HSB (hue, saturation, brightness).
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct Hsv : IColorProfile<Hsv, Rgb>
{
private static readonly Vector3 Min = Vector3.Zero;
private static readonly Vector3 Max = new(360, 1, 1);
/// <summary>
/// Initializes a new instance of the <see cref="Hsv"/> struct.
/// </summary>
/// <param name="h">The h hue component.</param>
/// <param name="s">The s saturation component.</param>
/// <param name="v">The v value (brightness) component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Hsv(float h, float s, float v)
: this(new Vector3(h, s, v))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Hsv"/> struct.
/// </summary>
/// <param name="vector">The vector representing the h, s, v components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Hsv(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
this.H = vector.X;
this.S = vector.Y;
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>
/// </summary>
public float H { get; }
/// <summary>
/// Gets the saturation component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float S { get; }
/// <summary>
/// Gets the value (brightness) component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public float V { get; }
/// <summary>
/// Compares two <see cref="Hsv"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Hsv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Hsv"/> 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 ==(Hsv left, Hsv right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Hsv"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Hsv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Hsv"/> 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 !=(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)
{
float r = source.R;
float g = source.G;
float b = source.B;
float max = MathF.Max(r, MathF.Max(g, b));
float min = MathF.Min(r, MathF.Min(g, b));
float chroma = max - min;
float h = 0;
float s = 0;
float v = max;
if (MathF.Abs(chroma) < Constants.Epsilon)
{
return new Hsv(0, s, v);
}
if (MathF.Abs(r - max) < Constants.Epsilon)
{
h = (g - b) / chroma;
}
else if (MathF.Abs(g - max) < Constants.Epsilon)
{
h = 2 + ((b - r) / chroma);
}
else if (MathF.Abs(b - max) < Constants.Epsilon)
{
h = 4 + ((r - g) / chroma);
}
h *= 60F;
if (h < -Constants.Epsilon)
{
h += 360F;
}
s = chroma / v;
return new Hsv(h, s, v);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<Hsv> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
Rgb rgb = source[i];
destination[i] = FromProfileConnectingSpace(options, in rgb);
}
}
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
float s = this.S;
float v = this.V;
if (MathF.Abs(s) < Constants.Epsilon)
{
return new Rgb(v, v, v);
}
float h = (MathF.Abs(this.H - 360) < Constants.Epsilon) ? 0 : this.H / 60;
int i = (int)Math.Truncate(h);
float f = h - i;
float p = v * (1F - s);
float q = v * (1F - (s * f));
float t = v * (1F - (s * (1F - f)));
float r, g, b;
switch (i)
{
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
default:
r = v;
g = p;
b = q;
break;
}
return new Rgb(r, g, b);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Hsv> source, Span<Rgb> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
Hsv hsv = source[i];
destination[i] = hsv.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(this.H, this.S, this.V);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"Hsv({this.H:#0.##}, {this.S:#0.##}, {this.V:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Hsv other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Hsv other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<Hsv, Vector3>(ref Unsafe.AsRef(in this));
}

243
src/ImageSharp/ColorProfiles/HunterLab.cs

@ -0,0 +1,243 @@
// 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 an Hunter LAB color.
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct HunterLab : IColorProfile<HunterLab, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="HunterLab"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="a">The a (green - magenta) component.</param>
/// <param name="b">The b (blue - yellow) component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public HunterLab(float l, float a, float b)
{
this.L = l;
this.A = a;
this.B = b;
}
/// <summary>
/// Initializes a new instance of the <see cref="HunterLab"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l a b components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public HunterLab(Vector3 vector)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.L = vector.X;
this.A = vector.Y;
this.B = vector.Z;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
/// </summary>
public float L { get; }
/// <summary>
/// Gets the a color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is green, positive magenta.</remarks>
/// </summary>
public float A { get; }
/// <summary>
/// Gets the b color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is blue, positive is yellow</remarks>
/// </summary>
public float B { get; }
/// <summary>
/// Compares two <see cref="HunterLab"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="HunterLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="HunterLab"/> 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>
public static bool operator ==(HunterLab left, HunterLab right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="HunterLab"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="HunterLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="HunterLab"/> 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 !=(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)
{
// Conversion algorithm described here:
// http://en.wikipedia.org/wiki/Lab_color_space#Hunter_Lab
CieXyz whitePoint = options.TargetWhitePoint;
float x = source.X, y = source.Y, z = source.Z;
float xn = whitePoint.X, yn = whitePoint.Y, zn = whitePoint.Z;
float ka = ComputeKa(in whitePoint);
float kb = ComputeKb(in whitePoint);
float yByYn = y / yn;
float sqrtYbyYn = MathF.Sqrt(yByYn);
float l = 100 * sqrtYbyYn;
float a = ka * (((x / xn) - yByYn) / sqrtYbyYn);
float b = kb * ((yByYn - (z / zn)) / sqrtYbyYn);
if (float.IsNaN(a))
{
a = 0;
}
if (float.IsNaN(b))
{
b = 0;
}
return new HunterLab(l, a, b);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<HunterLab> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// Conversion algorithm described here:
// http://en.wikipedia.org/wiki/Lab_color_space#Hunter_Lab
CieXyz whitePoint = options.SourceWhitePoint;
float l = this.L, a = this.A, b = this.B;
float xn = whitePoint.X, yn = whitePoint.Y, zn = whitePoint.Z;
float ka = ComputeKa(in whitePoint);
float kb = ComputeKb(in whitePoint);
float pow = Numerics.Pow2(l / 100F);
float sqrtPow = MathF.Sqrt(pow);
float y = pow * yn;
float x = (((a / ka) * sqrtPow) + pow) * xn;
float z = (((b / kb) * sqrtPow) - pow) * (-zn);
return new CieXyz(x, y, z);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<HunterLab> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
HunterLab lab = source[i];
destination[i] = lab.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"HunterLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is HunterLab other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(HunterLab other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<HunterLab, Vector3>(ref Unsafe.AsRef(in this));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float ComputeKa(in CieXyz whitePoint)
{
if (whitePoint.Equals(KnownIlluminants.C))
{
return 175F;
}
return 100F * (175F / 198.04F) * (whitePoint.X + whitePoint.Y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float ComputeKb(in CieXyz whitePoint)
{
if (whitePoint == KnownIlluminants.C)
{
return 70F;
}
return 100F * (70F / 218.11F) * (whitePoint.Y + whitePoint.Z);
}
}

103
src/ImageSharp/ColorProfiles/IColorProfile.cs

@ -0,0 +1,103 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Defines the contract for all color profiles.
/// </summary>
public interface IColorProfile
{
/// <summary>
/// Gets the chromatic adaption white point source.
/// </summary>
/// <returns>The <see cref="ChromaticAdaptionWhitePointSource"/>.</returns>
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<TSelf>
where TSelf : IColorProfile<TSelf, TProfileSpace>
where TProfileSpace : struct, IProfileConnectingSpace
{
#pragma warning disable CA1000 // Do not declare static members on generic types
/// <summary>
/// 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>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
public static abstract TSelf FromProfileConnectingSpace(ColorConversionOptions options, in TProfileSpace source);
/// <summary>
/// Converts the span of colors from the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The color profile span to convert from.</param>
/// <param name="destination">The color span to write the results to.</param>
public static abstract void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<TProfileSpace> source, Span<TSelf> destination);
/// <summary>
/// Converts the color to the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <returns>The <typeparamref name="TProfileSpace"/>.</returns>
public TProfileSpace ToProfileConnectingSpace(ColorConversionOptions options);
/// <summary>
/// Converts the span of colors to the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The color span to convert from.</param>
/// <param name="destination">The color profile span to write the results to.</param>
public static abstract void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<TSelf> source, Span<TProfileSpace> destination);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

18
src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs

@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Defines the contract for all color profile connection spaces.
/// </summary>
public interface IProfileConnectingSpace;
/// <summary>
/// Defines the contract for all color profile connection spaces.
/// </summary>
/// <typeparam name="TSelf">The type of color profile.</typeparam>
/// <typeparam name="TProfileSpace">The type of color profile connecting space.</typeparam>
public interface IProfileConnectingSpace<TSelf, TProfileSpace> : IColorProfile<TSelf, TProfileSpace>, IProfileConnectingSpace
where TSelf : struct, IColorProfile<TSelf, TProfileSpace>, IProfileConnectingSpace
where TProfileSpace : struct, IProfileConnectingSpace;

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

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

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
internal partial class LutABCalculator
{
/// <summary>
/// Identifies the transform direction for the configured LUT calculator.
/// </summary>
private enum CalculationType
{
/// <summary>
/// Converts from device space to PCS using ICC <c>mAB</c> stage order.
/// </summary>
AtoB,
/// <summary>
/// Converts from PCS to device space using ICC <c>mBA</c> stage order.
/// </summary>
BtoA,
}
}

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

@ -0,0 +1,158 @@
// 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;
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
/// </summary>
/// <param name="entry">The parsed A-to-B LUT entry.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
/// </summary>
/// <param name="entry">The parsed B-to-A LUT entry.</param>
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;
}
/// <summary>
/// Calculates the transformed value by applying the configured ICC LUT stages in specification order.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>The transformed value.</returns>
public Vector4 Calculate(Vector4 value)
{
switch (this.type)
{
case CalculationType.AtoB:
// ICC mAB order: A, CLUT, M, Matrix, B.
if (this.curveACalculator != null)
{
value = this.curveACalculator.Calculate(value);
}
if (this.clutCalculator != null)
{
value = this.clutCalculator.Calculate(value);
}
if (this.curveMCalculator != null)
{
value = this.curveMCalculator.Calculate(value);
}
if (this.matrixCalculator != null)
{
value = this.matrixCalculator.Calculate(value);
}
if (this.curveBCalculator != null)
{
value = this.curveBCalculator.Calculate(value);
}
return value;
case CalculationType.BtoA:
// ICC mBA order: B, Matrix, M, CLUT, A.
if (this.curveBCalculator != null)
{
value = this.curveBCalculator.Calculate(value);
}
if (this.matrixCalculator != null)
{
value = this.matrixCalculator.Calculate(value);
}
if (this.curveMCalculator != null)
{
value = this.curveMCalculator.Calculate(value);
}
if (this.clutCalculator != null)
{
value = this.clutCalculator.Calculate(value);
}
if (this.curveACalculator != null)
{
value = this.curveACalculator.Calculate(value);
}
return value;
default:
throw new InvalidOperationException("Invalid calculation type");
}
}
/// <summary>
/// Creates calculators for the processing stages present in the LUT entry.
/// </summary>
/// <remarks>
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
/// </remarks>
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;
Guard.IsTrue(
hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut,
"entry",
"AToB or BToA tag must contain at least one processing element");
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 {tag}."),
};
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)
{
}
}

135
src/ImageSharp/ColorProfiles/KnownChromaticAdaptationMatrices.cs

@ -0,0 +1,135 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Provides matrices for chromatic adaptation, facilitating the adjustment of color values
/// under different light sources to maintain color constancy. This class supports common
/// adaptation transforms based on the von Kries coefficient law, which assumes independent
/// scaling of the cone responses in the human eye. These matrices can be applied to convert
/// color coordinates between different illuminants, ensuring consistent color appearance
/// across various lighting conditions.
/// </summary>
/// <remarks>
/// Supported adaptation matrices include the Bradford, von Kries, and Sharp transforms.
/// These matrices are typically used in conjunction with color space conversions, such as from XYZ
/// to RGB, to achieve accurate color rendition in digital imaging applications.
/// </remarks>
public static class KnownChromaticAdaptationMatrices
{
/// <summary>
/// von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65)
/// </summary>
public static readonly Matrix4x4 VonKriesHPEAdjusted
= Matrix4x4.Transpose(new Matrix4x4
{
M11 = 0.40024F,
M12 = 0.7076F,
M13 = -0.08081F,
M21 = -0.2263F,
M22 = 1.16532F,
M23 = 0.0457F,
M31 = 0,
M32 = 0,
M33 = 0.91822F,
M44 = 1F // Important for inverse transforms.
});
/// <summary>
/// von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez for equal energy)
/// </summary>
public static readonly Matrix4x4 VonKriesHPE
= Matrix4x4.Transpose(new Matrix4x4
{
M11 = 0.3897F,
M12 = 0.6890F,
M13 = -0.0787F,
M21 = -0.2298F,
M22 = 1.1834F,
M23 = 0.0464F,
M31 = 0,
M32 = 0,
M33 = 1F,
M44 = 1F
});
/// <summary>
/// XYZ scaling chromatic adaptation transform matrix
/// </summary>
public static readonly Matrix4x4 XyzScaling = Matrix4x4.Transpose(Matrix4x4.Identity);
/// <summary>
/// Bradford chromatic adaptation transform matrix (used in CMCCAT97)
/// </summary>
public static readonly Matrix4x4 Bradford
= Matrix4x4.Transpose(new Matrix4x4
{
M11 = 0.8951F,
M12 = 0.2664F,
M13 = -0.1614F,
M21 = -0.7502F,
M22 = 1.7135F,
M23 = 0.0367F,
M31 = 0.0389F,
M32 = -0.0685F,
M33 = 1.0296F,
M44 = 1F
});
/// <summary>
/// Spectral sharpening and the Bradford transform
/// </summary>
public static readonly Matrix4x4 BradfordSharp
= Matrix4x4.Transpose(new Matrix4x4
{
M11 = 1.2694F,
M12 = -0.0988F,
M13 = -0.1706F,
M21 = -0.8364F,
M22 = 1.8006F,
M23 = 0.0357F,
M31 = 0.0297F,
M32 = -0.0315F,
M33 = 1.0018F,
M44 = 1F
});
/// <summary>
/// CMCCAT2000 (fitted from all available color data sets)
/// </summary>
public static readonly Matrix4x4 CMCCAT2000
= Matrix4x4.Transpose(new Matrix4x4
{
M11 = 0.7982F,
M12 = 0.3389F,
M13 = -0.1371F,
M21 = -0.5918F,
M22 = 1.5512F,
M23 = 0.0406F,
M31 = 0.0008F,
M32 = 0.239F,
M33 = 0.9753F,
M44 = 1F
});
/// <summary>
/// CAT02 (optimized for minimizing CIELAB differences)
/// </summary>
public static readonly Matrix4x4 CAT02
= Matrix4x4.Transpose(new Matrix4x4
{
M11 = 0.7328F,
M12 = 0.4296F,
M13 = -0.1624F,
M21 = -0.7036F,
M22 = 1.6975F,
M23 = 0.0061F,
M31 = 0.0030F,
M32 = 0.0136F,
M33 = 0.9834F,
M44 = 1F
});
}

77
src/ImageSharp/ColorProfiles/KnownIlluminants.cs

@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// The well known standard illuminants.
/// Standard illuminants provide a basis for comparing images or colors recorded under different lighting
/// </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>
public static class KnownIlluminants
{
/// <summary>
/// Gets the Incandescent / Tungsten illuminant.
/// </summary>
public static CieXyz A { get; } = new(1.09850F, 1F, 0.35585F);
/// <summary>
/// Gets the Direct sunlight at noon (obsoleteF) illuminant.
/// </summary>
public static CieXyz B { get; } = new(0.99072F, 1F, 0.85223F);
/// <summary>
/// Gets the Average / North sky Daylight (obsoleteF) illuminant.
/// </summary>
public static CieXyz C { get; } = new(0.98074F, 1F, 1.18232F);
/// <summary>
/// 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>
public static CieXyz D55 { get; } = new(0.95682F, 1F, 0.92149F);
/// <summary>
/// Gets the Noon Daylight: TelevisionF, sRGB color space illuminant.
/// </summary>
public static CieXyz D65 { get; } = new(0.95047F, 1F, 1.08883F);
/// <summary>
/// Gets the North sky Daylight illuminant.
/// </summary>
public static CieXyz D75 { get; } = new(0.94972F, 1F, 1.22638F);
/// <summary>
/// Gets the Equal energy illuminant.
/// </summary>
public static CieXyz E { get; } = new(1F, 1F, 1F);
/// <summary>
/// Gets the Cool White Fluorescent illuminant.
/// </summary>
public static CieXyz F2 { get; } = new(0.99186F, 1F, 0.67393F);
/// <summary>
/// Gets the D65 simulatorF, Daylight simulator illuminant.
/// </summary>
public static CieXyz F7 { get; } = new(0.95041F, 1F, 1.08747F);
/// <summary>
/// Gets the Philips TL84F, Ultralume 40 illuminant.
/// </summary>
public static CieXyz F11 { get; } = new(1.00962F, 1F, 0.64350F);
}

113
src/ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs

@ -0,0 +1,113 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles.Companding;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Chromaticity coordinates based on: <see href="http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html"/>
/// </summary>
public static class KnownRgbWorkingSpaces
{
/// <summary>
/// sRgb working space.
/// </summary>
/// <remarks>
/// Uses proper companding function, according to:
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_Rgb_to_XYZ.html"/>
/// </remarks>
public static readonly RgbWorkingSpace SRgb = new SRgbWorkingSpace(KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.3000F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F)));
/// <summary>
/// Simplified sRgb working space (uses <see cref="GammaCompanding">gamma companding</see> instead of <see cref="SRgbCompanding"/>).
/// See also <see cref="SRgb"/>.
/// </summary>
public static readonly RgbWorkingSpace SRgbSimplified = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.3000F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F)));
/// <summary>
/// Rec. 709 (ITU-R Recommendation BT.709) working space.
/// </summary>
public static readonly RgbWorkingSpace Rec709 = new Rec709WorkingSpace(KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.64F, 0.33F), new CieXyChromaticityCoordinates(0.30F, 0.60F), new CieXyChromaticityCoordinates(0.15F, 0.06F)));
/// <summary>
/// Rec. 2020 (ITU-R Recommendation BT.2020F) working space.
/// </summary>
public static readonly RgbWorkingSpace Rec2020 = new Rec2020WorkingSpace(KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.708F, 0.292F), new CieXyChromaticityCoordinates(0.170F, 0.797F), new CieXyChromaticityCoordinates(0.131F, 0.046F)));
/// <summary>
/// ECI Rgb v2 working space.
/// </summary>
public static readonly RgbWorkingSpace ECIRgbv2 = new LWorkingSpace(KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6700F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1400F, 0.0800F)));
/// <summary>
/// Adobe Rgb (1998) working space.
/// </summary>
public static readonly RgbWorkingSpace AdobeRgb1998 = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F)));
/// <summary>
/// Apple sRgb working space.
/// </summary>
public static readonly RgbWorkingSpace ApplesRgb = new GammaWorkingSpace(1.8F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6250F, 0.3400F), new CieXyChromaticityCoordinates(0.2800F, 0.5950F), new CieXyChromaticityCoordinates(0.1550F, 0.0700F)));
/// <summary>
/// Best Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace BestRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7347F, 0.2653F), new CieXyChromaticityCoordinates(0.2150F, 0.7750F), new CieXyChromaticityCoordinates(0.1300F, 0.0350F)));
/// <summary>
/// Beta Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace BetaRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6888F, 0.3112F), new CieXyChromaticityCoordinates(0.1986F, 0.7551F), new CieXyChromaticityCoordinates(0.1265F, 0.0352F)));
/// <summary>
/// Bruce Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace BruceRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2800F, 0.6500F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F)));
/// <summary>
/// CIE Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace CIERgb = new GammaWorkingSpace(2.2F, KnownIlluminants.E, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7350F, 0.2650F), new CieXyChromaticityCoordinates(0.2740F, 0.7170F), new CieXyChromaticityCoordinates(0.1670F, 0.0090F)));
/// <summary>
/// ColorMatch Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace ColorMatchRgb = new GammaWorkingSpace(1.8F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6300F, 0.3400F), new CieXyChromaticityCoordinates(0.2950F, 0.6050F), new CieXyChromaticityCoordinates(0.1500F, 0.0750F)));
/// <summary>
/// Don Rgb 4 working space.
/// </summary>
public static readonly RgbWorkingSpace DonRgb4 = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6960F, 0.3000F), new CieXyChromaticityCoordinates(0.2150F, 0.7650F), new CieXyChromaticityCoordinates(0.1300F, 0.0350F)));
/// <summary>
/// Ekta Space PS5 working space.
/// </summary>
public static readonly RgbWorkingSpace EktaSpacePS5 = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6950F, 0.3050F), new CieXyChromaticityCoordinates(0.2600F, 0.7000F), new CieXyChromaticityCoordinates(0.1100F, 0.0050F)));
/// <summary>
/// NTSC Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace NTSCRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.C, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6700F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1400F, 0.0800F)));
/// <summary>
/// PAL/SECAM Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace PALSECAMRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2900F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F)));
/// <summary>
/// ProPhoto Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace ProPhotoRgb = new GammaWorkingSpace(1.8F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7347F, 0.2653F), new CieXyChromaticityCoordinates(0.1596F, 0.8404F), new CieXyChromaticityCoordinates(0.0366F, 0.0001F)));
/// <summary>
/// SMPTE-C Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace SMPTECRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6300F, 0.3400F), new CieXyChromaticityCoordinates(0.3100F, 0.5950F), new CieXyChromaticityCoordinates(0.1550F, 0.0700F)));
/// <summary>
/// Wide Gamut Rgb working space.
/// </summary>
public static readonly RgbWorkingSpace WideGamutRgb = new GammaWorkingSpace(2.2F, KnownIlluminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7350F, 0.2650F), new CieXyChromaticityCoordinates(0.1150F, 0.8260F), new CieXyChromaticityCoordinates(0.1570F, 0.0180F)));
}

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

178
src/ImageSharp/ColorProfiles/Lms.cs

@ -0,0 +1,178 @@
// 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>
/// LMS is a color space represented by the response of the three types of cones of the human eye,
/// named after their responsivity (sensitivity) at long, medium and short wavelengths.
/// <see href="https://en.wikipedia.org/wiki/LMS_color_space"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct Lms : IColorProfile<Lms, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="Lms"/> struct.
/// </summary>
/// <param name="l">L represents the responsivity at long wavelengths.</param>
/// <param name="m">M represents the responsivity at medium wavelengths.</param>
/// <param name="s">S represents the responsivity at short wavelengths.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Lms(float l, float m, float s)
{
this.L = l;
this.M = m;
this.S = s;
}
/// <summary>
/// Initializes a new instance of the <see cref="Lms"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, m, s components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Lms(Vector3 vector)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.L = vector.X;
this.M = vector.Y;
this.S = vector.Z;
}
/// <summary>
/// Gets the L long component.
/// <remarks>A value usually ranging between -1 and 1.</remarks>
/// </summary>
public float L { get; }
/// <summary>
/// Gets the M medium component.
/// <remarks>A value usually ranging between -1 and 1.</remarks>
/// </summary>
public float M { get; }
/// <summary>
/// Gets the S short component.
/// <remarks>A value usually ranging between -1 and 1.</remarks>
/// </summary>
public float S { get; }
/// <summary>
/// Compares two <see cref="Lms"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Lms"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Lms"/> 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 ==(Lms left, Lms right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Lms"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Lms"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Lms"/> 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 !=(Lms left, Lms right) => !left.Equals(right);
/// <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 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)
{
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)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
=> new(Vector3.Transform(this.AsVector3Unsafe(), options.InverseAdaptationMatrix));
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Lms> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
Lms lms = source[i];
destination[i] = lms.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.M, this.S);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"Lms({this.L:#0.##}, {this.M:#0.##}, {this.S:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Lms other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Lms other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<Lms, Vector3>(ref Unsafe.AsRef(in this));
}

463
src/ImageSharp/ColorProfiles/Rgb.cs

@ -0,0 +1,463 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Represents an RGB (red, green, blue) color profile.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="Rgb"/> struct.
/// </summary>
/// <param name="r">The red component usually ranging between 0 and 1.</param>
/// <param name="g">The green component usually ranging between 0 and 1.</param>
/// <param name="b">The blue component usually ranging between 0 and 1.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rgb(float r, float g, float b)
{
// Not clamping as this space can exceed "usual" ranges
this.R = r;
this.G = g;
this.B = b;
}
/// <summary>
/// Initializes a new instance of the <see cref="Rgb"/> struct.
/// </summary>
/// <param name="source">The vector representing the r, g, b components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rgb(Vector3 source)
{
this.R = source.X;
this.G = source.Y;
this.B = source.Z;
}
/// <summary>
/// Gets the red component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float R { get; }
/// <summary>
/// Gets the green component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float G { get; }
/// <summary>
/// Gets the blue component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float B { get; }
/// <summary>
/// Compares two <see cref="Rgb"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgb"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Rgb"/> 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 ==(Rgb left, Rgb right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Rgb"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Rgb"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Rgb"/> 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 !=(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.AsVector3Unsafe(), GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace)));
return FromScaledVector4(options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4()));
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<Rgb> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Matrix4x4 matrix = GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace);
for (int i = 0; i < source.Length; i++)
{
// Convert to linear rgb then compress.
Rgb linear = new(Vector3.Transform(source[i].AsVector3Unsafe(), matrix));
Vector4 nonlinear = options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4());
destination[i] = FromScaledVector4(nonlinear);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// First expand to linear rgb
Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(this.ToScaledVector4()));
// Then convert to xyz
return new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace)));
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
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.SourceRgbWorkingSpace.Expand(rgb.ToScaledVector4()));
// Then convert to xyz
destination[i] = new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), matrix));
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <summary>
/// Initializes the color instance from a generic scaled <see cref="Vector3"/>.
/// </summary>
/// <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(source);
/// <summary>
/// Initializes the color instance for a source clamped between <value>0</value> and <value>1</value>
/// </summary>
/// <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(source.AsVector3Unsafe(), Vector3.Zero, Vector3.One));
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector3"/> 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="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToScaledVector3()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
return v3;
}
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"Rgb({this.R:#0.##}, {this.G:#0.##}, {this.B:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Rgb other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Rgb other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
internal Vector3 AsVector3Unsafe() => Unsafe.As<Rgb, Vector3>(ref Unsafe.AsRef(in this));
private static Matrix4x4 GetCieXyzToRgbMatrix(RgbWorkingSpace workingSpace)
{
Matrix4x4 matrix = GetRgbToCieXyzMatrix(workingSpace);
Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix);
return inverseMatrix;
}
private static Matrix4x4 GetRgbToCieXyzMatrix(RgbWorkingSpace workingSpace)
{
DebugGuard.NotNull(workingSpace, nameof(workingSpace));
RgbPrimariesChromaticityCoordinates chromaticity = workingSpace.ChromaticityCoordinates;
float xr = chromaticity.R.X;
float xg = chromaticity.G.X;
float xb = chromaticity.B.X;
float yr = chromaticity.R.Y;
float yg = chromaticity.G.Y;
float yb = chromaticity.B.Y;
float mXr = xr / yr;
float mZr = (1 - xr - yr) / yr;
float mXg = xg / yg;
float mZg = (1 - xg - yg) / yg;
float mXb = xb / yb;
float mZb = (1 - xb - yb) / yb;
Matrix4x4 xyzMatrix = new()
{
M11 = mXr,
M21 = mXg,
M31 = mXb,
M12 = 1F,
M22 = 1F,
M32 = 1F,
M13 = mZr,
M23 = mZg,
M33 = mZb,
M44 = 1F
};
Matrix4x4.Invert(xyzMatrix, out Matrix4x4 inverseXyzMatrix);
Vector3 vector = Vector3.Transform(workingSpace.WhitePoint.AsVector3Unsafe(), inverseXyzMatrix);
// Use transposed Rows/Columns
return new Matrix4x4
{
M11 = vector.X * mXr,
M21 = vector.Y * mXg,
M31 = vector.Z * mXb,
M12 = vector.X,
M22 = vector.Y,
M32 = vector.Z,
M13 = vector.X * mZr,
M23 = vector.Y * mZg,
M33 = vector.Z * mZb,
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);
}
}

82
src/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs

@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Represents the chromaticity coordinates of RGB primaries.
/// One of the specifiers of <see cref="RgbWorkingSpace"/>.
/// </summary>
public readonly struct RgbPrimariesChromaticityCoordinates : IEquatable<RgbPrimariesChromaticityCoordinates>
{
/// <summary>
/// Initializes a new instance of the <see cref="RgbPrimariesChromaticityCoordinates"/> struct.
/// </summary>
/// <param name="r">The chromaticity coordinates of the red channel.</param>
/// <param name="g">The chromaticity coordinates of the green channel.</param>
/// <param name="b">The chromaticity coordinates of the blue channel.</param>
public RgbPrimariesChromaticityCoordinates(CieXyChromaticityCoordinates r, CieXyChromaticityCoordinates g, CieXyChromaticityCoordinates b)
{
this.R = r;
this.G = g;
this.B = b;
}
/// <summary>
/// Gets the chromaticity coordinates of the red channel.
/// </summary>
public CieXyChromaticityCoordinates R { get; }
/// <summary>
/// Gets the chromaticity coordinates of the green channel.
/// </summary>
public CieXyChromaticityCoordinates G { get; }
/// <summary>
/// Gets the chromaticity coordinates of the blue channel.
/// </summary>
public CieXyChromaticityCoordinates B { get; }
/// <summary>
/// Compares two <see cref="RgbPrimariesChromaticityCoordinates"/> objects for equality.
/// </summary>
/// <param name="left">
/// The <see cref="RgbPrimariesChromaticityCoordinates"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="RgbPrimariesChromaticityCoordinates"/> 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>
public static bool operator ==(RgbPrimariesChromaticityCoordinates left, RgbPrimariesChromaticityCoordinates right)
=> left.Equals(right);
/// <summary>
/// Compares two <see cref="RgbPrimariesChromaticityCoordinates"/> objects for inequality
/// </summary>
/// <param name="left">
/// The <see cref="RgbPrimariesChromaticityCoordinates"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="RgbPrimariesChromaticityCoordinates"/> 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>
public static bool operator !=(RgbPrimariesChromaticityCoordinates left, RgbPrimariesChromaticityCoordinates right)
=> !left.Equals(right);
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is RgbPrimariesChromaticityCoordinates other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(RgbPrimariesChromaticityCoordinates other)
=> this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B);
}

95
src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs

@ -0,0 +1,95 @@
// 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>
/// Implementation of the Von Kries chromatic adaptation model.
/// </summary>
/// <remarks>
/// Transformation described here:
/// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
/// </remarks>
public static class VonKriesChromaticAdaptation
{
/// <summary>
/// Performs a linear transformation of a source color in to the destination color.
/// </summary>
/// <remarks>Doesn't crop the resulting color space coordinates (e.g. allows negative values for XYZ coordinates).</remarks>
/// <param name="source">The source color.</param>
/// <param name="whitePoints">The conversion white points.</param>
/// <param name="matrix">The chromatic adaptation matrix.</param>
/// <returns>The <see cref="CieXyz"/></returns>
public static CieXyz Transform(in CieXyz source, (CieXyz From, CieXyz To) whitePoints, Matrix4x4 matrix)
{
CieXyz from = whitePoints.From;
CieXyz to = whitePoints.To;
if (from.Equals(to))
{
return new CieXyz(source.X, source.Y, source.Z);
}
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);
Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix);
return new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix));
}
/// <summary>
/// Performs a bulk linear transformation of a source color in to the destination color.
/// </summary>
/// <remarks>Doesn't crop the resulting color space coordinates (e. g. allows negative values for XYZ coordinates).</remarks>
/// <param name="source">The span to the source colors.</param>
/// <param name="destination">The span to the destination colors.</param>
/// <param name="whitePoints">The conversion white points.</param>
/// <param name="matrix">The chromatic adaptation matrix.</param>
public static void Transform(
ReadOnlySpan<CieXyz> source,
Span<CieXyz> destination,
(CieXyz From, CieXyz To) whitePoints,
Matrix4x4 matrix)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
int count = source.Length;
CieXyz from = whitePoints.From;
CieXyz to = whitePoints.To;
if (from.Equals(to))
{
source.CopyTo(destination[..count]);
return;
}
Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix);
ref CieXyz sourceBase = ref MemoryMarshal.GetReference(source);
ref CieXyz destinationBase = ref MemoryMarshal.GetReference(destination);
Vector3 sourceWhitePointLms = Vector3.Transform(from.AsVector3Unsafe(), matrix);
Vector3 targetWhitePointLms = Vector3.Transform(to.AsVector3Unsafe(), matrix);
Vector3 vector = targetWhitePointLms / sourceWhitePointLms;
for (nuint i = 0; i < (uint)count; i++)
{
ref CieXyz sp = ref Unsafe.Add(ref sourceBase, i);
ref CieXyz dp = ref Unsafe.Add(ref destinationBase, i);
Vector3 sourceColorLms = Vector3.Transform(sp.AsVector3Unsafe(), matrix);
Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms);
dp = new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix));
}
}
}

68
src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs

@ -0,0 +1,68 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Companding;
namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
/// <summary>
/// The gamma working space.
/// </summary>
public sealed class GammaWorkingSpace : RgbWorkingSpace
{
/// <summary>
/// Initializes a new instance of the <see cref="GammaWorkingSpace" /> class.
/// </summary>
/// <param name="gamma">The gamma value.</param>
/// <param name="referenceWhite">The reference white point.</param>
/// <param name="chromaticityCoordinates">The chromaticity of the rgb primaries.</param>
public GammaWorkingSpace(float gamma, CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates)
: base(referenceWhite, chromaticityCoordinates) => this.Gamma = gamma;
/// <summary>
/// Gets the gamma value.
/// </summary>
public float Gamma { get; }
/// <inheritdoc/>
public override void Compress(Span<Vector4> vectors) => GammaCompanding.Compress(vectors, this.Gamma);
/// <inheritdoc/>
public override void Expand(Span<Vector4> vectors) => GammaCompanding.Expand(vectors, this.Gamma);
/// <inheritdoc/>
public override Vector4 Compress(Vector4 vector) => GammaCompanding.Compress(vector, this.Gamma);
/// <inheritdoc/>
public override Vector4 Expand(Vector4 vector) => GammaCompanding.Expand(vector, this.Gamma);
/// <inheritdoc/>
public override bool Equals(object? obj)
{
if (obj is null)
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj is GammaWorkingSpace other)
{
return this.Gamma.Equals(other.Gamma)
&& this.WhitePoint.Equals(other.WhitePoint)
&& this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates);
}
return false;
}
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(
this.WhitePoint,
this.ChromaticityCoordinates,
this.Gamma);
}

35
src/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Companding;
namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
/// <summary>
/// L* working space.
/// </summary>
public sealed class LWorkingSpace : RgbWorkingSpace
{
/// <summary>
/// Initializes a new instance of the <see cref="LWorkingSpace" /> class.
/// </summary>
/// <param name="referenceWhite">The reference white point.</param>
/// <param name="chromaticityCoordinates">The chromaticity of the rgb primaries.</param>
public LWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates)
: base(referenceWhite, chromaticityCoordinates)
{
}
/// <inheritdoc/>
public override void Compress(Span<Vector4> vectors) => LCompanding.Compress(vectors);
/// <inheritdoc/>
public override void Expand(Span<Vector4> vectors) => LCompanding.Expand(vectors);
/// <inheritdoc/>
public override Vector4 Compress(Vector4 vector) => LCompanding.Compress(vector);
/// <inheritdoc/>
public override Vector4 Expand(Vector4 vector) => LCompanding.Expand(vector);
}

35
src/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Companding;
namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
/// <summary>
/// Rec. 2020 (ITU-R Recommendation BT.2020F) working space.
/// </summary>
public sealed class Rec2020WorkingSpace : RgbWorkingSpace
{
/// <summary>
/// Initializes a new instance of the <see cref="Rec2020WorkingSpace" /> class.
/// </summary>
/// <param name="referenceWhite">The reference white point.</param>
/// <param name="chromaticityCoordinates">The chromaticity of the rgb primaries.</param>
public Rec2020WorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates)
: base(referenceWhite, chromaticityCoordinates)
{
}
/// <inheritdoc/>
public override void Compress(Span<Vector4> vectors) => Rec2020Companding.Compress(vectors);
/// <inheritdoc/>
public override void Expand(Span<Vector4> vectors) => Rec2020Companding.Expand(vectors);
/// <inheritdoc/>
public override Vector4 Compress(Vector4 vector) => Rec2020Companding.Compress(vector);
/// <inheritdoc/>
public override Vector4 Expand(Vector4 vector) => Rec2020Companding.Expand(vector);
}

35
src/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Companding;
namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
/// <summary>
/// Rec. 709 (ITU-R Recommendation BT.709) working space.
/// </summary>
public sealed class Rec709WorkingSpace : RgbWorkingSpace
{
/// <summary>
/// Initializes a new instance of the <see cref="Rec709WorkingSpace" /> class.
/// </summary>
/// <param name="referenceWhite">The reference white point.</param>
/// <param name="chromaticityCoordinates">The chromaticity of the rgb primaries.</param>
public Rec709WorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates)
: base(referenceWhite, chromaticityCoordinates)
{
}
/// <inheritdoc/>
public override void Compress(Span<Vector4> vectors) => Rec709Companding.Compress(vectors);
/// <inheritdoc/>
public override void Expand(Span<Vector4> vectors) => Rec709Companding.Expand(vectors);
/// <inheritdoc/>
public override Vector4 Compress(Vector4 vector) => Rec709Companding.Compress(vector);
/// <inheritdoc/>
public override Vector4 Expand(Vector4 vector) => Rec709Companding.Expand(vector);
}

85
src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs

@ -0,0 +1,85 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
/// <summary>
/// Base class for all implementations of <see cref="RgbWorkingSpace"/>.
/// </summary>
public abstract class RgbWorkingSpace
{
/// <summary>
/// Initializes a new instance of the <see cref="RgbWorkingSpace"/> class.
/// </summary>
/// <param name="referenceWhite">The reference white point.</param>
/// <param name="chromaticityCoordinates">The chromaticity of the rgb primaries.</param>
protected RgbWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates)
{
this.WhitePoint = referenceWhite;
this.ChromaticityCoordinates = chromaticityCoordinates;
}
/// <summary>
/// Gets the reference white point
/// </summary>
public CieXyz WhitePoint { get; }
/// <summary>
/// Gets the chromaticity of the rgb primaries.
/// </summary>
public RgbPrimariesChromaticityCoordinates ChromaticityCoordinates { get; }
/// <summary>
/// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public abstract void Compress(Span<Vector4> vectors);
/// <summary>
/// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
/// </summary>
/// <param name="vectors">The span of vectors.</param>
public abstract void Expand(Span<Vector4> vectors);
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public abstract Vector4 Compress(Vector4 vector);
/// <summary>
/// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
/// </summary>
/// <param name="vector">The vector.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public abstract Vector4 Expand(Vector4 vector);
/// <inheritdoc/>
public override bool Equals(object? obj)
{
if (obj is null)
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj is RgbWorkingSpace other)
{
return this.WhitePoint.Equals(other.WhitePoint)
&& this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates);
}
return false;
}
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates);
}

35
src/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Companding;
namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
/// <summary>
/// The sRgb working space.
/// </summary>
public sealed class SRgbWorkingSpace : RgbWorkingSpace
{
/// <summary>
/// Initializes a new instance of the <see cref="SRgbWorkingSpace" /> class.
/// </summary>
/// <param name="referenceWhite">The reference white point.</param>
/// <param name="chromaticityCoordinates">The chromaticity of the rgb primaries.</param>
public SRgbWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates)
: base(referenceWhite, chromaticityCoordinates)
{
}
/// <inheritdoc/>
public override void Compress(Span<Vector4> vectors) => SRgbCompanding.Compress(vectors);
/// <inheritdoc/>
public override void Expand(Span<Vector4> vectors) => SRgbCompanding.Expand(vectors);
/// <inheritdoc/>
public override Vector4 Compress(Vector4 vector) => SRgbCompanding.Compress(vector);
/// <inheritdoc/>
public override Vector4 Expand(Vector4 vector) => SRgbCompanding.Expand(vector);
}

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

194
src/ImageSharp/ColorProfiles/YCbCr.cs

@ -0,0 +1,194 @@
// 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 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 = Vector3.One;
/// <summary>
/// Initializes a new instance of the <see cref="YCbCr"/> 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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public YCbCr(float y, float cb, float cr)
: this(new Vector3(y, cb, cr))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="YCbCr"/> struct.
/// </summary>
/// <param name="vector">The vector representing the y, cb, cr components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public YCbCr(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
this.Y = vector.X;
this.Cb = vector.Y;
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 1.</remarks>
/// </summary>
public float Y { get; }
/// <summary>
/// Gets the Cb chroma component.
/// <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 1.</remarks>
/// </summary>
public float Cr { get; }
/// <summary>
/// Compares two <see cref="YCbCr"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="YCbCr"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="YCbCr"/> 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>
public static bool operator ==(YCbCr left, YCbCr right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="YCbCr"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="YCbCr"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="YCbCr"/> 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 !=(YCbCr left, YCbCr right) => !left.Equals(right);
/// <inheritdoc/>
public Vector4 ToScaledVector4()
{
Vector3 v3 = default;
v3 += this.AsVector3Unsafe();
return new Vector4(v3, 1F);
}
/// <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));
// 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/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<YCbCr> 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 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));
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<YCbCr> 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 ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(this.Y, this.Cb, this.Cr);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"YCbCr({this.Y}, {this.Cb}, {this.Cr})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is YCbCr other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(YCbCr other)
=> this.AsVector3Unsafe() == other.AsVector3Unsafe();
private Vector3 AsVector3Unsafe() => Unsafe.As<YCbCr, Vector3>(ref Unsafe.AsRef(in this));
}

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

135
src/ImageSharp/ColorSpaces/CieLab.cs

@ -1,135 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.ColorSpaces;
/// <summary>
/// Represents a CIE L*a*b* 1976 color.
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space"/>
/// </summary>
public readonly struct CieLab : IEquatable<CieLab>
{
/// <summary>
/// D50 standard illuminant.
/// Used when reference white is not specified explicitly.
/// </summary>
public static readonly CieXyz DefaultWhitePoint = Illuminants.D50;
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="a">The a (green - magenta) component.</param>
/// <param name="b">The b (blue - yellow) component.</param>
/// <remarks>Uses <see cref="DefaultWhitePoint"/> as white point.</remarks>
[MethodImpl(InliningOptions.ShortMethod)]
public CieLab(float l, float a, float b)
: this(l, a, b, DefaultWhitePoint)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="a">The a (green - magenta) component.</param>
/// <param name="b">The b (blue - yellow) component.</param>
/// <param name="whitePoint">The reference white point. <see cref="Illuminants"/></param>
[MethodImpl(InliningOptions.ShortMethod)]
public CieLab(float l, float a, float b, CieXyz whitePoint)
: this(new Vector3(l, a, b), whitePoint)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, a, b components.</param>
/// <remarks>Uses <see cref="DefaultWhitePoint"/> as white point.</remarks>
[MethodImpl(InliningOptions.ShortMethod)]
public CieLab(Vector3 vector)
: this(vector, DefaultWhitePoint)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, a, b components.</param>
/// <param name="whitePoint">The reference white point. <see cref="Illuminants"/></param>
[MethodImpl(InliningOptions.ShortMethod)]
public CieLab(Vector3 vector, CieXyz whitePoint)
: this()
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.L = vector.X;
this.A = vector.Y;
this.B = vector.Z;
this.WhitePoint = whitePoint;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
/// </summary>
public readonly float L { get; }
/// <summary>
/// Gets the a color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is green, positive magenta.</remarks>
/// </summary>
public readonly float A { get; }
/// <summary>
/// Gets the b color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is blue, positive is yellow</remarks>
/// </summary>
public readonly float B { get; }
/// <summary>
/// Gets the reference white point of this color
/// </summary>
public readonly CieXyz WhitePoint { get; }
/// <summary>
/// Compares two <see cref="CieLab"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLab"/> 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(InliningOptions.ShortMethod)]
public static bool operator ==(CieLab left, CieLab right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLab"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLab"/> 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(InliningOptions.ShortMethod)]
public static bool operator !=(CieLab left, CieLab right) => !left.Equals(right);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B, this.WhitePoint);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieLab other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool Equals(CieLab other) =>
this.L.Equals(other.L)
&& this.A.Equals(other.A)
&& this.B.Equals(other.B)
&& this.WhitePoint.Equals(other.WhitePoint);
}

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

Loading…
Cancel
Save