1
0
mirror of https://github.com/aluxnimm/outlookcaldavsynchronizer.git synced 2025-10-05 16:02:49 +02:00

Use Unicolour library for colour comparison and remove ColorMine package.

This commit is contained in:
nimm
2024-01-20 21:18:40 +01:00
parent 365019167c
commit c7d349a632
62 changed files with 5023 additions and 122 deletions

View File

@@ -77,9 +77,6 @@
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ColorMineStandard">
<Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="DDay.iCal">
<Version>1.0.2.575</Version>
</PackageReference>

View File

@@ -1,106 +1,113 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer", "CalDavSynchronizer\CalDavSynchronizer.csproj", "{64937844-752B-49A4-9B3F-3526601E93E1}"
ProjectSection(ProjectDependencies) = postProject
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF} = {A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.UnitTests", "CalDavSynchronizer.UnitTest\CalDavSynchronizer.UnitTests.csproj", "{60B1D6FA-B087-42D8-B330-7C147B62FC7A}"
EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "CalDavSynchronizer.Setup", "CalDavSynchronizer.Setup\CalDavSynchronizer.Setup.vdproj", "{C05F2805-55A8-4ABB-9892-515781049EB6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync", "GenSync\GenSync.csproj", "{76C932E7-ECA5-4010-B602-2104327EE5EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync.UnitTests", "GenSync.UnitTests\GenSync.UnitTests.csproj", "{EDB87679-422D-4297-8F46-6F7B220CDF37}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavDataAccessIntegrationTests", "CalDavDataAccessIntegrationTests\CalDavDataAccessIntegrationTests.csproj", "{BE747238-9388-43CE-AF64-536C64442EB9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Google", "CalDavSynchronizer.OAuth.Google\CalDavSynchronizer.OAuth.Google.csproj", "{230914CF-6FF1-4B09-A831-C37C6B41D127}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.CustomInstaller", "CalDavSynchronizer.CustomInstaller\CalDavSynchronizer.CustomInstaller.csproj", "{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions", "CalDavSynchronizer.Conversions\CalDavSynchronizer.Conversions.csproj", "{71021DE1-8DC8-4414-AF56-FE821DD92F47}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions.UnitTests", "CalDavSynchronizer.Conversions.UnitTests\CalDavSynchronizer.Conversions.UnitTests.csproj", "{85C64408-3019-4097-9508-89E46F2E9738}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards", "Thought.vCards\Thought.vCards.csproj", "{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards.UnitTests", "Thought.vCards.UnitTests\Thought.vCards.UnitTests.csproj", "{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.IntegrationTests", "CalDavSynchronizer.IntegrationTests\CalDavSynchronizer.IntegrationTests.csproj", "{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Swisscom", "CalDavSynchronizer.OAuth.Swisscom\CalDavSynchronizer.OAuth.Swisscom.csproj", "{424F803E-B641-4473-B9C3-B8DF40E0B3B3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.Build.0 = Release|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.Build.0 = Release|Any CPU
{C05F2805-55A8-4ABB-9892-515781049EB6}.Debug|Any CPU.ActiveCfg = Debug
{C05F2805-55A8-4ABB-9892-515781049EB6}.Release|Any CPU.ActiveCfg = Release
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.Build.0 = Release|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.Build.0 = Release|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.Build.0 = Release|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.Build.0 = Debug|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.ActiveCfg = Release|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.Build.0 = Release|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.Build.0 = Release|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.Build.0 = Release|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.Build.0 = Release|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.Build.0 = Release|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.Build.0 = Release|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.Build.0 = Release|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_AutoCreateNewLanguageFiles = True
RESX_SortFileContentOnSave = True
SolutionGuid = {C6CA0E38-AF86-45D6-BDCA-263A18B40738}
EndGlobalSection
EndGlobal

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.33130.400
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer", "CalDavSynchronizer\CalDavSynchronizer.csproj", "{64937844-752B-49A4-9B3F-3526601E93E1}"
ProjectSection(ProjectDependencies) = postProject
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55} = {A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF} = {A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.UnitTests", "CalDavSynchronizer.UnitTest\CalDavSynchronizer.UnitTests.csproj", "{60B1D6FA-B087-42D8-B330-7C147B62FC7A}"
EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "CalDavSynchronizer.Setup", "CalDavSynchronizer.Setup\CalDavSynchronizer.Setup.vdproj", "{C05F2805-55A8-4ABB-9892-515781049EB6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync", "GenSync\GenSync.csproj", "{76C932E7-ECA5-4010-B602-2104327EE5EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync.UnitTests", "GenSync.UnitTests\GenSync.UnitTests.csproj", "{EDB87679-422D-4297-8F46-6F7B220CDF37}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavDataAccessIntegrationTests", "CalDavDataAccessIntegrationTests\CalDavDataAccessIntegrationTests.csproj", "{BE747238-9388-43CE-AF64-536C64442EB9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Google", "CalDavSynchronizer.OAuth.Google\CalDavSynchronizer.OAuth.Google.csproj", "{230914CF-6FF1-4B09-A831-C37C6B41D127}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.CustomInstaller", "CalDavSynchronizer.CustomInstaller\CalDavSynchronizer.CustomInstaller.csproj", "{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions", "CalDavSynchronizer.Conversions\CalDavSynchronizer.Conversions.csproj", "{71021DE1-8DC8-4414-AF56-FE821DD92F47}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions.UnitTests", "CalDavSynchronizer.Conversions.UnitTests\CalDavSynchronizer.Conversions.UnitTests.csproj", "{85C64408-3019-4097-9508-89E46F2E9738}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards", "Thought.vCards\Thought.vCards.csproj", "{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards.UnitTests", "Thought.vCards.UnitTests\Thought.vCards.UnitTests.csproj", "{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.IntegrationTests", "CalDavSynchronizer.IntegrationTests\CalDavSynchronizer.IntegrationTests.csproj", "{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Swisscom", "CalDavSynchronizer.OAuth.Swisscom\CalDavSynchronizer.OAuth.Swisscom.csproj", "{424F803E-B641-4473-B9C3-B8DF40E0B3B3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wacton.Unicolour", "Unicolour\Wacton.Unicolour.csproj", "{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.Build.0 = Release|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.Build.0 = Release|Any CPU
{C05F2805-55A8-4ABB-9892-515781049EB6}.Debug|Any CPU.ActiveCfg = Debug
{C05F2805-55A8-4ABB-9892-515781049EB6}.Release|Any CPU.ActiveCfg = Release
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.Build.0 = Release|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.Build.0 = Release|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.Build.0 = Release|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.Build.0 = Debug|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.ActiveCfg = Release|Any CPU
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.Build.0 = Release|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.Build.0 = Release|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.Build.0 = Release|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.Build.0 = Release|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.Build.0 = Release|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.Build.0 = Release|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.Build.0 = Release|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.Build.0 = Release|Any CPU
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C6CA0E38-AF86-45D6-BDCA-263A18B40738}
RESX_SortFileContentOnSave = True
RESX_AutoCreateNewLanguageFiles = True
EndGlobalSection
EndGlobal

View File

@@ -910,14 +910,15 @@
<Project>{a53d3ceb-f7af-41ab-aa04-4d3cf684bbaf}</Project>
<Name>Thought.vCards</Name>
</ProjectReference>
<ProjectReference Include="..\Unicolour\Wacton.Unicolour.csproj">
<Project>{a8f93f45-42ea-4d4c-8dd1-b03410cc7c55}</Project>
<Name>Wacton.Unicolour</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle">
<Version>1.8.9</Version>
</PackageReference>
<PackageReference Include="ColorMineStandard">
<Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="DDay.iCal">
<Version>1.0.2.575</Version>
</PackageReference>

View File

@@ -26,8 +26,7 @@ using System.Data;
using System.Reflection;
using CalDavSynchronizer.DataAccess;
using log4net;
using ColorMine;
using ColorMine.ColorSpaces;
using Wacton.Unicolour;
using Microsoft.Office.Interop.Outlook;
namespace CalDavSynchronizer.Utilities
@@ -67,8 +66,7 @@ namespace CalDavSynchronizer.Utilities
{OlCategoryColor.olCategoryColorDarkMaroon, ArgbColor.FromRgb(0x93446b)}
};
public static OlCategoryColor FindMatchingCategoryColor(Color color)
public static OlCategoryColor FindMatchingCategoryColor (Color color)
{
var minDistance = double.MaxValue;
var matchingCategoryColor = OlCategoryColor.olCategoryColorNone;
@@ -77,10 +75,10 @@ namespace CalDavSynchronizer.Utilities
{
var catColor = Color.FromArgb(cat.Value.ArgbValue);
var a = new Rgb {R = color.R, G = color.G, B = color.B};
var b = new Rgb {R = catColor.R, G = catColor.G, B = catColor.B};
var curDistance = a.Compare(b, new ColorMine.ColorSpaces.Comparisons.CieDe2000Comparison());
var unicolour_a = new Unicolour(ColourSpace.Rgb255, color.R, color.G, color.B);
var unicolour_b = new Unicolour(ColourSpace.Rgb255, catColor.R, catColor.G, catColor.B);
var curDistance = unicolour_a.Difference(DeltaE.Ciede2000, unicolour_b);
if (curDistance < minDistance)
{
@@ -91,7 +89,7 @@ namespace CalDavSynchronizer.Utilities
return matchingCategoryColor;
}
public static OlCategoryColor FindMatchingCategoryColor(ArgbColor argbColor)
{
var color = Color.FromArgb(argbColor.ArgbValue);

View File

@@ -105,7 +105,7 @@ see [https://www.davx5.com](https://www.davx5.com), so we can really recommend i
- [Apache log4net](https://logging.apache.org/log4net/)
- [Thought.vCard](http://nugetmusthaves.com/Package/Thought.vCards)
- [NodaTime](http://nodatime.org/)
- [ColorMine](https://www.nuget.org/packages/ColorMine/)
- [Unicolour](https://github.com/waacton/Unicolour)
### Install instructions ###

45
Unicolour/Adaptation.cs Normal file
View File

@@ -0,0 +1,45 @@
namespace Wacton.Unicolour
{
internal static class Adaptation
{
private static readonly Matrix Bradford = new(new[,]
{
{ +0.8951, +0.2664, -0.1614 },
{ -0.7502, +1.7135, +0.0367 },
{ +0.0389, -0.0685, +1.0296 }
});
internal static Matrix WhitePoint(Matrix matrix, WhitePoint sourceWhitePoint, WhitePoint destinationWhitePoint)
{
if (sourceWhitePoint == destinationWhitePoint)
{
return matrix;
}
var adaptedBradford = AdaptedBradfordMatrix(sourceWhitePoint, destinationWhitePoint);
return adaptedBradford.Multiply(matrix);
}
// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
private static Matrix AdaptedBradfordMatrix(WhitePoint sourceWhitePoint, WhitePoint destinationWhitePoint)
{
var sourceWhite = sourceWhitePoint.AsXyzMatrix();
var destinationWhite = destinationWhitePoint.AsXyzMatrix();
var sourceLms = Bradford.Multiply(sourceWhite).ToTriplet();
var destinationLms = Bradford.Multiply(destinationWhite).ToTriplet();
var lmsRatios = new Matrix(new[,]
{
{ destinationLms.First / sourceLms.First, 0, 0 },
{ 0, destinationLms.Second / sourceLms.Second, 0 },
{ 0, 0, destinationLms.Third / sourceLms.Third }
});
var inverseBradford = Bradford.Inverse();
var adaptedBradfordMatrix = inverseBradford.Multiply(lmsRatios).Multiply(Bradford);
return adaptedBradfordMatrix;
}
}
}

15
Unicolour/Alpha.cs Normal file
View File

@@ -0,0 +1,15 @@
using System;
namespace Wacton.Unicolour
{
public record Alpha(double A)
{
public double A { get; } = A;
public double ConstrainedA => double.IsNaN(A) ? 0 : A.Clamp(0.0, 1.0);
public int A255 => (int)Math.Round(ConstrainedA * 255);
public string Hex => $"{A255:X2}";
public override string ToString() => $"{A:F2}";
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Wacton.Unicolour
{
internal static class BoundingLines
{
internal static double CalculateMaxChroma(double lightness, double hue)
{
var hueRad = hue / 360 * Math.PI * 2;
return GetBoundingLines(lightness).Select(x => DistanceFromOriginAngle(hueRad, x)).Min();
}
internal static double CalculateMaxChroma(double lightness)
{
return GetBoundingLines(lightness).Select(DistanceFromOrigin).Min();
}
// https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L249
private static IEnumerable<Line> GetBoundingLines(double l)
{
const double kappa = 903.2962962;
const double epsilon = 0.0088564516;
var matrixR = Matrix.FromTriplet(3.240969941904521, -1.537383177570093, -0.498610760293);
var matrixG = Matrix.FromTriplet(-0.96924363628087, 1.87596750150772, 0.041555057407175);
var matrixB = Matrix.FromTriplet(0.055630079696993, -0.20397695888897, 1.056971514242878);
var sub1 = Math.Pow(l + 16, 3) / 1560896;
var sub2 = sub1 > epsilon ? sub1 : l / kappa;
IEnumerable<Line> CalculateLines(Matrix matrix)
{
var s1 = sub2 * (284517 * matrix[0, 0] - 94839 * matrix[2, 0]);
var s2 = sub2 * (838422 * matrix[2, 0] + 769860 * matrix[1, 0] + 731718 * matrix[0, 0]);
var s3 = sub2 * (632260 * matrix[2, 0] - 126452 * matrix[1, 0]);
var slope0 = s1 / s3;
var intercept0 = s2 * l / s3;
var slope1 = s1 / (s3 + 126452);
var intercept1 = (s2 - 769860) * l / (s3 + 126452);
return new[] { new Line(slope0, intercept0), new Line(slope1, intercept1) };
}
var lines = new List<Line>();
lines.AddRange(CalculateLines(matrixR));
lines.AddRange(CalculateLines(matrixG));
lines.AddRange(CalculateLines(matrixB));
return lines;
}
private static double DistanceFromOriginAngle(double theta, Line line)
{
var distance = line.Intercept / (Math.Sin(theta) - line.Slope * Math.Cos(theta));
return distance < 0 ? double.PositiveInfinity : distance;
}
private static double DistanceFromOrigin(Line line) =>
Math.Abs(line.Intercept) / Math.Sqrt(Math.Pow(line.Slope, 2) + 1);
private record Line(double Slope, double Intercept)
{
public double Slope { get; } = Slope;
public double Intercept { get; } = Intercept;
}
}
}

127
Unicolour/Cam.cs Normal file
View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
namespace Wacton.Unicolour
{
using static Utils;
public static class Cam
{
public record Model(double J, double C, double H, double M, double S, double Q)
{
public double J { get; } = J;
public double C { get; } = C;
public double H { get; } = H;
public string Hc { get; } = HueData.GetHueComposition(H); // technically not in CAM02 but 🤷
public double M { get; } = M;
public double S { get; } = S;
public double Q { get; } = Q;
public double Lightness => J;
public double Chroma => C;
public double HueAngle => H;
public string HueComposition => Hc;
public double Colourfulness => M;
public double Saturation => S;
public double Brightness => Q;
internal Ucs ToUcs()
{
var j = 1.7 * J / (1 + 0.007 * J);
var m = Math.Log(1 + 0.0228 * M) / 0.0228;
(j, var a, var b) = FromLchTriplet(new(j, m, H));
return new Ucs(j, a, b);
}
}
/*
* NOTE: if ever want to support CAM02-LCD & CAM02-SCD (Large / Small Colour Difference)
* see Table 2.2 in "CIECAM02 and Its Recent Developments" (https://doi.org/10.1007/978-1-4419-6190-7_2)
* essentially a small tweak to how j / m / ΔE' are calculated
* but would need to consider how it affects the structure of Unicolour objects
*/
public record Ucs(double J, double A, double B)
{
public double J { get; } = J;
public double A { get; } = A;
public double B { get; } = B;
internal Model ToModel(ViewingConditions view)
{
var j = J / (1.7 - 0.007 * J);
(j, var m, var h) = ToLchTriplet(j, A, B);
m = (Math.Exp(0.0228 * m) - 1) / 0.0228;
var q = 4 / view.C * Math.Pow(j / 100.0, 0.5) * (view.Aw + 4) * Math.Pow(view.Fl, 0.25);
var c = m / Math.Pow(view.Fl, 0.25);
var s = 100 * Math.Pow(m / q, 0.5);
return new Model(j, c, h, m, s, q);
}
}
internal record ViewingConditions(
double C, double Nc, double Dr, double Dg, double Db,
double Fl, double N, double Z, double Nbb, double Ncb, double Aw)
{
public double C { get; } = C;
public double Nc { get; } = Nc;
public double Dr { get; } = Dr;
public double Dg { get; } = Dg;
public double Db { get; } = Db;
public double Fl { get; } = Fl;
public double N { get; } = N;
public double Z { get; } = Z;
public double Nbb { get; } = Nbb;
public double Ncb { get; } = Ncb;
public double Aw { get; } = Aw;
}
internal static class HueData
{
private const double Angle1 = 20.14;
private const double Angle2 = 90.00;
private const double Angle3 = 164.25;
private const double Angle4 = 237.53;
private const double Angle5 = 380.14;
private static readonly string[] Names = { "R", "Y", "G", "B", "R" };
private static readonly double[] Angles = { Angle1, Angle2, Angle3, Angle4, Angle5 };
private static readonly double[] Es = { 0.8, 0.7, 1.0, 1.2, 0.8 };
private static readonly double[] Quads = { 0.0, 100.0, 200.0, 300.0, 400.0 };
private static string Name(int i) => Get(Names, i);
private static double Angle(int i) => Get(Angles, i);
private static double E(int i) => Get(Es, i);
private static double Quad(int i) => Get(Quads, i);
private static T Get<T>(IReadOnlyList<T> array, int i) => array[i - 1];
private static double GetHPrime(double h) => h < Angle1 ? h + 360 : h;
internal static double GetEccentricity(double h) => 0.25 * (Math.Cos(ToRadians(GetHPrime(h)) + 2) + 3.8);
internal static string GetHueComposition(double h)
{
if (double.IsNaN(h)) return "-";
var hPrime = GetHPrime(h);
int? index = hPrime switch
{
>= Angle1 and < Angle2 => 1,
>= Angle2 and < Angle3 => 2,
>= Angle3 and < Angle4 => 3,
>= Angle4 and < Angle5 => 4,
_ => null
};
if (index == null) return "-";
var i = index.Value;
var hQuad = Quad(i) +
100 * E(i + 1) * (hPrime - Angle(i)) /
(E(i + 1) * (hPrime - Angle(i)) + E(i) * (Angle(i + 1) - hPrime));
var pl = Quad(i + 1) - hQuad;
var pr = hQuad - Quad(i);
var hc = $"{pl:f0}{Name(i)}{pr:f0}{Name(i + 1)}";
return hc;
}
}
}
}

225
Unicolour/Cam02.cs Normal file
View File

@@ -0,0 +1,225 @@
using System;
namespace Wacton.Unicolour
{
using static Cam;
using static Utils;
public record Cam02 : ColourRepresentation
{
protected override int? HueIndex => null;
public double J => First;
public double A => Second;
public double B => Third;
public Ucs Ucs { get; }
public Model Model { get; }
// J lightness bounds not clear (and is different between Model and UCS)
// presumably also greyscale when A.Equals(0.0) && B.Equals(0.0)
internal override bool IsGreyscale => Model.Chroma <= 0;
public Cam02(double j, double a, double b, CamConfiguration camConfig) : this(new Ucs(j, a, b), camConfig,
ColourHeritage.None)
{
}
internal Cam02(Model model, CamConfiguration camConfig, ColourHeritage heritage) : this(model.ToUcs(),
camConfig, heritage)
{
Model = model;
}
internal Cam02(Ucs ucs, CamConfiguration camConfig, ColourHeritage heritage) : base(ucs.J, ucs.A, ucs.B,
heritage)
{
// Model will only be non-null if the constructor that takes Model is called (currently not possible from external code)
Ucs = ucs;
Model ??= ucs.ToModel(ViewingConditions(camConfig));
}
protected override string FirstString => $"{J:F2}";
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
public override string ToString() => base.ToString();
/*
* CAM02 is a transform of XYZ
* Forward: https://doi.org/10.1007/978-1-4419-6190-7_2 · https://doi.org/10.1002/col.20227 · https://doi.org/10.48550/arXiv.1802.06067
* Reverse: https://doi.org/10.1007/978-1-4419-6190-7_2 · https://doi.org/10.1002/col.20227 · https://doi.org/10.48550/arXiv.1802.06067
*/
private static readonly Matrix MCAT02 = new(new[,]
{
{ +0.7328, +0.4296, -0.1624 },
{ -0.7036, +1.6975, +0.0061 },
{ +0.0030, +0.0136, +0.9834 }
});
private static readonly Matrix MHPE = new(new[,]
{
{ +0.38971, +0.68898, -0.07868 },
{ -0.22981, +1.18340, +0.04641 },
{ 0.00000, 0.00000, +1.00000 }
});
private static readonly Matrix ForwardStep4 = new(new[,]
{
{ 2, 1, 1 / 20.0 },
{ 1, -12 / 11.0, 1 / 11.0 },
{ 1 / 9.0, 1 / 9.0, -2 / 9.0 },
{ 1, 1, 21 / 20.0 }
});
private static readonly Matrix ReverseStep4 = new Matrix(new double[,]
{
{ 460, 451, 288 },
{ 460, -891, -261 },
{ 460, -220, -6300 }
}).Scale(1 / 1403.0);
private static ViewingConditions ViewingConditions(CamConfiguration camConfig)
{
var (xw, yw, zw) = (camConfig.WhitePoint.X, camConfig.WhitePoint.Y, camConfig.WhitePoint.Z);
var la = camConfig.AdaptingLuminance;
var yb = camConfig.BackgroundLuminance;
var c = camConfig.C;
var f = camConfig.F;
var nc = camConfig.Nc;
// step 0
var xyzWhitePointMatrix = Matrix.FromTriplet(xw, yw, zw);
var (rw, gw, bw) = MCAT02.Multiply(xyzWhitePointMatrix).ToTriplet();
var d = (f * (1 - 1 / 3.6 * Math.Exp((-la - 42) / 92.0))).Clamp(0, 1);
var (dr, dg, db) = (D(rw), D(gw), D(bw));
double D(double input) => d * (yw / input) + 1 - d;
var k = 1 / (5 * la + 1);
var fl = 0.2 * Math.Pow(k, 4) * (5 * la) + 0.1 * Math.Pow(1 - Math.Pow(k, 4), 2) * CubeRoot(5 * la);
var n = yb / yw;
var z = 1.48 + Math.Sqrt(n);
var nbb = 0.725 * Math.Pow(1 / n, 0.2);
var ncb = nbb;
// slightly different to CAM16
var wcMatrix = Matrix.FromTriplet(dr * rw, dg * gw, db * bw);
var (rwPrime, gwPrime, bwPrime) = MHPE.Multiply(MCAT02.Inverse()).Multiply(wcMatrix).ToTriplet();
var (raw, gaw, baw) = (Aw(rwPrime), Aw(gwPrime), Aw(bwPrime));
double Aw(double input)
{
var power = Math.Pow(fl * input / 100.0, 0.42);
return 400 * (power / (power + 27.13)) + 0.1;
}
var aw = (2 * raw + gaw + baw / 20.0 - 0.305) * nbb;
return new ViewingConditions(c, nc, dr, dg, db, fl, n, z, nbb, ncb, aw);
}
public static Cam02 FromXyz(Xyz xyz, CamConfiguration camConfig, XyzConfiguration xyzConfig)
{
var view = ViewingConditions(camConfig);
// step 1
var xyzMatrix = Matrix.FromTriplet(xyz.X, xyz.Y, xyz.Z);
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, camConfig.WhitePoint)
.Select(x => x * 100);
var rgb = MCAT02.Multiply(xyzMatrix).ToTriplet();
// step 2
var cMatrix = Matrix.FromTriplet(view.Dr * rgb.First, view.Dg * rgb.Second, view.Db * rgb.Third);
// step 3, slightly different to CAM16
var (rPrime, gPrime, bPrime) = MHPE.Multiply(MCAT02.Inverse()).Multiply(cMatrix).ToTriplet();
// step 4
var (ra, ga, ba) = (A(rPrime), A(gPrime), A(bPrime));
double A(double input)
{
if (double.IsNaN(input)) return double.NaN;
var power = Math.Pow(view.Fl * Math.Abs(input) / 100.0, 0.42);
return 400 * Math.Sign(input) * (power / (power + 27.13));
}
// step 5
var aMatrix = Matrix.FromTriplet(ra, ga, ba);
var components = ForwardStep4.Multiply(aMatrix);
var (p2, a, b, u) = (components[0, 0], components[1, 0], components[2, 0], components[3, 0]);
var h = ToDegrees(Math.Atan2(b, a)).Modulo(360);
// step 6
var et = HueData.GetEccentricity(h);
// step 7
var achromatic = p2 * view.Nbb;
// step 8
var j = 100 * Math.Pow(achromatic / view.Aw, view.C * view.Z);
// step 9
var q = 4 / view.C * Math.Pow(j / 100.0, 0.5) * (view.Aw + 4) * Math.Pow(view.Fl, 0.25);
// step 10
var t = 50000 / 13.0 * view.Nc * view.Ncb * et * Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2)) / (u + 0.305);
var alpha = Math.Pow(t, 0.9) * Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73);
var c = alpha * Math.Sqrt(j / 100.0);
var m = c * Math.Pow(view.Fl, 0.25);
var s = 50 * Math.Sqrt(alpha * view.C / (view.Aw + 4));
return new Cam02(new Model(j, c, h, m, s, q), camConfig, ColourHeritage.From(xyz));
}
public static Xyz ToXyz(Cam02 cam, CamConfiguration camConfig, XyzConfiguration xyzConfig)
{
var view = ViewingConditions(camConfig);
var j = cam.Model.J;
var c = cam.Model.C;
var h = cam.Model.H;
// step 1
var alpha = j == 0 ? 0 : c / Math.Sqrt(j / 100.0);
var t = Math.Pow(alpha / Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73), 1 / 0.9);
// step 2
var et = 0.25 * (Math.Cos(ToRadians(h) + 2) + 3.8);
var achromatic = view.Aw * Math.Pow(j / 100.0, 1 / (view.C * view.Z));
var p1 = et * (50000 / 13.0) * view.Nc * view.Ncb;
var p2 = achromatic / view.Nbb;
// step 3
var gamma = 23 * (p2 + 0.305) * t /
(23 * p1 + 11 * t * Math.Cos(ToRadians(h)) + 108 * t * Math.Sin(ToRadians(h)));
var a = gamma * Math.Cos(ToRadians(h));
var b = gamma * Math.Sin(ToRadians(h));
// step 4
var components = Matrix.FromTriplet(p2, a, b);
var (ra, ga, ba) = ReverseStep4.Multiply(components).ToTriplet();
// step 5, slightly different to CAM16
var primeMatrix = Matrix.FromTriplet(C(ra), C(ga), C(ba));
double C(double input)
{
if (double.IsNaN(input)) return double.NaN;
return Math.Sign(input) * (100 / view.Fl) *
Math.Pow(27.13 * Math.Abs(input) / (400 - Math.Abs(input)), 1 / 0.42);
}
var (rc, gc, bc) = MCAT02.Multiply(MHPE.Inverse()).Multiply(primeMatrix).ToTriplet();
// step 6
var rgbMatrix = Matrix.FromTriplet(rc / view.Dr, gc / view.Dg, bc / view.Db);
// step 7
var xyzMatrix = MCAT02.Inverse().Multiply(rgbMatrix);
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, camConfig.WhitePoint, xyzConfig.WhitePoint)
.Select(x => x / 100.0);
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(cam));
}
}
}

209
Unicolour/Cam16.cs Normal file
View File

@@ -0,0 +1,209 @@
using System;
namespace Wacton.Unicolour
{
using static Cam;
using static Utils;
public record Cam16 : ColourRepresentation
{
protected override int? HueIndex => null;
public double J => First;
public double A => Second;
public double B => Third;
public Ucs Ucs { get; }
public Model Model { get; }
// J lightness bounds not clear (and is different between Model and UCS)
// presumably also greyscale when A.Equals(0.0) && B.Equals(0.0)
internal override bool IsGreyscale => Model.Chroma <= 0;
public Cam16(double j, double a, double b, CamConfiguration camConfig) : this(new Ucs(j, a, b), camConfig,
ColourHeritage.None)
{
}
internal Cam16(Model model, CamConfiguration camConfig, ColourHeritage heritage) : this(model.ToUcs(),
camConfig, heritage)
{
Model = model;
}
internal Cam16(Ucs ucs, CamConfiguration camConfig, ColourHeritage heritage) : base(ucs.J, ucs.A, ucs.B,
heritage)
{
// Model will only be non-null if the constructor that takes Cam16Model is called (currently not possible from external code)
Ucs = ucs;
Model ??= ucs.ToModel(ViewingConditions(camConfig));
}
protected override string FirstString => $"{J:F2}";
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
public override string ToString() => base.ToString();
/*
* CAM16 is a transform of XYZ
* Forward: https://doi.org/10.1002/col.22131 · https://doi.org/10.48550/arXiv.1802.06067
* Reverse: https://doi.org/10.1002/col.22131 · https://doi.org/10.48550/arXiv.1802.06067
*/
private static readonly Matrix M16 = new(new[,]
{
{ +0.401288, +0.650173, -0.051461 },
{ -0.250268, +1.204414, +0.045854 },
{ -0.002079, +0.048952, +0.953127 }
});
private static readonly Matrix ForwardStep4 = new(new[,]
{
{ 2, 1, 1 / 20.0 },
{ 1, -12 / 11.0, 1 / 11.0 },
{ 1 / 9.0, 1 / 9.0, -2 / 9.0 },
{ 1, 1, 21 / 20.0 }
});
private static readonly Matrix ReverseStep4 = new Matrix(new double[,]
{
{ 460, 451, 288 },
{ 460, -891, -261 },
{ 460, -220, -6300 }
}).Scale(1 / 1403.0);
private static ViewingConditions ViewingConditions(CamConfiguration camConfig)
{
var (xw, yw, zw) = (camConfig.WhitePoint.X, camConfig.WhitePoint.Y, camConfig.WhitePoint.Z);
var la = camConfig.AdaptingLuminance;
var yb = camConfig.BackgroundLuminance;
var c = camConfig.C;
var f = camConfig.F;
var nc = camConfig.Nc;
// step 0
var xyzWhitePointMatrix = Matrix.FromTriplet(xw, yw, zw);
var (rw, gw, bw) = M16.Multiply(xyzWhitePointMatrix).ToTriplet();
var d = (f * (1 - 1 / 3.6 * Math.Exp((-la - 42) / 92.0))).Clamp(0, 1);
var (dr, dg, db) = (D(rw), D(gw), D(bw));
double D(double input) => d * (yw / input) + 1 - d;
var k = 1 / (5 * la + 1);
var fl = 0.2 * Math.Pow(k, 4) * (5 * la) + 0.1 * Math.Pow(1 - Math.Pow(k, 4), 2) * CubeRoot(5 * la);
var n = yb / yw;
var z = 1.48 + Math.Sqrt(n);
var nbb = 0.725 * Math.Pow(1 / n, 0.2);
var ncb = nbb;
var (rwc, gwc, bwc) = (dr * rw, dg * gw, db * bw);
var (raw, gaw, baw) = (Aw(rwc), Aw(gwc), Aw(bwc));
double Aw(double input)
{
var power = Math.Pow(fl * input / 100.0, 0.42);
return 400 * (power / (power + 27.13)) + 0.1;
}
var aw = (2 * raw + gaw + baw / 20.0 - 0.305) * nbb;
return new ViewingConditions(c, nc, dr, dg, db, fl, n, z, nbb, ncb, aw);
}
public static Cam16 FromXyz(Xyz xyz, CamConfiguration camConfig, XyzConfiguration xyzConfig)
{
var view = ViewingConditions(camConfig);
// step 1
var xyzMatrix = Matrix.FromTriplet(xyz.X, xyz.Y, xyz.Z);
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, camConfig.WhitePoint)
.Select(x => x * 100);
var rgb = M16.Multiply(xyzMatrix).ToTriplet();
// step 2
var (rc, gc, bc) = (view.Dr * rgb.First, view.Dg * rgb.Second, view.Db * rgb.Third);
// step 3
var (ra, ga, ba) = (A(rc), A(gc), A(bc));
double A(double input)
{
if (double.IsNaN(input)) return double.NaN;
var power = Math.Pow(view.Fl * Math.Abs(input) / 100.0, 0.42);
return 400 * Math.Sign(input) * (power / (power + 27.13));
}
// step 4
var aMatrix = Matrix.FromTriplet(ra, ga, ba);
var components = ForwardStep4.Multiply(aMatrix);
var (p2, a, b, u) = (components[0, 0], components[1, 0], components[2, 0], components[3, 0]);
var h = ToDegrees(Math.Atan2(b, a)).Modulo(360);
// step 5
var et = HueData.GetEccentricity(h);
// step 6
var achromatic = p2 * view.Nbb;
// step 7
var j = 100 * Math.Pow(achromatic / view.Aw, view.C * view.Z);
// step 8
var q = 4 / view.C * Math.Pow(j / 100.0, 0.5) * (view.Aw + 4) * Math.Pow(view.Fl, 0.25);
// step 9
var t = 50000 / 13.0 * view.Nc * view.Ncb * et * Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2)) / (u + 0.305);
var alpha = Math.Pow(t, 0.9) * Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73);
var c = alpha * Math.Sqrt(j / 100.0);
var m = c * Math.Pow(view.Fl, 0.25);
var s = 50 * Math.Sqrt(alpha * view.C / (view.Aw + 4));
return new Cam16(new Model(j, c, h, m, s, q), camConfig, ColourHeritage.From(xyz));
}
public static Xyz ToXyz(Cam16 cam, CamConfiguration camConfig, XyzConfiguration xyzConfig)
{
var view = ViewingConditions(camConfig);
var j = cam.Model.J;
var c = cam.Model.C;
var h = cam.Model.H;
// step 1 (NOTE: currently not supporting partial models - all model values will be present)
var alpha = j == 0 ? 0 : c / Math.Sqrt(j / 100.0);
var t = Math.Pow(alpha / Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73), 1 / 0.9);
// step 2
var et = 0.25 * (Math.Cos(ToRadians(h) + 2) + 3.8);
var achromatic = view.Aw * Math.Pow(j / 100.0, 1 / (view.C * view.Z));
var p1 = et * (50000 / 13.0) * view.Nc * view.Ncb;
var p2 = achromatic / view.Nbb;
// step 3
var gamma = 23 * (p2 + 0.305) * t /
(23 * p1 + 11 * t * Math.Cos(ToRadians(h)) + 108 * t * Math.Sin(ToRadians(h)));
var a = gamma * Math.Cos(ToRadians(h));
var b = gamma * Math.Sin(ToRadians(h));
// step 4
var components = Matrix.FromTriplet(p2, a, b);
var (ra, ga, ba) = ReverseStep4.Multiply(components).ToTriplet();
// step 5
var (rc, gc, bc) = (C(ra), C(ga), C(ba));
double C(double input)
{
if (double.IsNaN(input)) return double.NaN;
return Math.Sign(input) * (100 / view.Fl) *
Math.Pow(27.13 * Math.Abs(input) / (400 - Math.Abs(input)), 1 / 0.42);
}
// step 6
var rgbMatrix = Matrix.FromTriplet(rc / view.Dr, gc / view.Dg, bc / view.Db);
// step 7
var xyzMatrix = M16.Inverse().Multiply(rgbMatrix);
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, camConfig.WhitePoint, xyzConfig.WhitePoint)
.Select(x => x / 100.0);
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(cam));
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
namespace Wacton.Unicolour
{
// NOTE: "discounting" parameter which I've noticed in other implementations
// is not a parameter in the CAM papers (there's probably some other paper I've missed 🤷)
// therefore D is always calculated
public class CamConfiguration
{
public WhitePoint WhitePoint { get; }
public double
AdaptingLuminance
{
get;
} // [L_A] Luminance of adapting field (brightness of the room where the colour is being viewed)
public double
BackgroundLuminance
{
get;
} // [Y_b] Luminance of background (brightness of the area surrounding the colour)
internal readonly Surround
Surround; // 0 = dark (movie theatre), 1 = dim (bright TV in dim room), 2 = average (surface colours)
internal double F => Surround switch
{
Surround.Dark => 0.8,
Surround.Dim => 0.9,
Surround.Average => 1.0,
_ => throw new ArgumentOutOfRangeException()
};
internal double C => Surround switch
{
Surround.Dark => 0.525,
Surround.Dim => 0.59,
Surround.Average => 0.69,
_ => throw new ArgumentOutOfRangeException()
};
internal double Nc => Surround switch
{
Surround.Dark => 0.8,
Surround.Dim => 0.9,
Surround.Average => 1.0,
_ => throw new ArgumentOutOfRangeException()
};
/*
* hard to find guidance for default CAM settings; this is based on data in "Usage guidelines for CIECAM97s" (Moroney, 2000)
* - sRGB standard ambient illumination level of 64 lux ~= 4
* - La = E * R / PI / 5 where E = lux & R = 1 --> 64 / PI / 5
* ----------
* I don't know why Google's HCT luminance calculations don't match the above
* they suggest 200 lux -> ~11.72 luminance, but the formula above gives ~12.73 luminance
* and they appear to ignore the division by 5 and incorporate XYZ luminance (Y)
*/
public static readonly CamConfiguration StandardRgb = new(WhitePoint.From(Illuminant.D65), LuxToLuminance(64),
20, Surround.Average);
public static readonly CamConfiguration Hct = new(WhitePoint.From(Illuminant.D65),
LuxToLuminance(200) * 5 * DefaultHctY(), DefaultHctY() * 100, Surround.Average);
internal static double LuxToLuminance(double lux) => lux / Math.PI / 5.0;
// just for HCT, use specific XYZ configuration
private const double DefaultHctLightness = 50;
private static double DefaultHctY() => Lab.ToXyz(new Lab(DefaultHctLightness, 0, 0), XyzConfiguration.D65).Y;
public CamConfiguration(WhitePoint whitePoint, double adaptingLuminance, double backgroundLuminance,
Surround surround)
{
WhitePoint = whitePoint;
AdaptingLuminance = adaptingLuminance;
BackgroundLuminance = backgroundLuminance;
Surround = surround;
}
public override string ToString() => $"CAM {AdaptingLuminance:f0} {BackgroundLuminance:f0} {Surround}";
}
public enum Surround
{
Dark = 0,
Dim = 1,
Average = 2
}
}

10
Unicolour/Chromaticity.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Wacton.Unicolour
{
public record Chromaticity(double X, double Y)
{
public double X { get; } = X;
public double Y { get; } = Y;
public override string ToString() => $"({X:F4}, {Y:F4})";
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
namespace Wacton.Unicolour
{
internal record ColourDescription(string description)
{
private readonly string description = description;
internal static readonly ColourDescription NotApplicable = new("-");
internal static readonly ColourDescription Black = new(nameof(Black));
internal static readonly ColourDescription Shadow = new(nameof(Shadow));
internal static readonly ColourDescription Dark = new(nameof(Dark));
internal static readonly ColourDescription Pure = new(nameof(Pure));
internal static readonly ColourDescription Light = new(nameof(Light));
internal static readonly ColourDescription Pale = new(nameof(Pale));
internal static readonly ColourDescription White = new(nameof(White));
internal static readonly ColourDescription Grey = new(nameof(Grey));
internal static readonly ColourDescription Faint = new(nameof(Faint));
internal static readonly ColourDescription Weak = new(nameof(Weak));
internal static readonly ColourDescription Mild = new(nameof(Mild));
internal static readonly ColourDescription Strong = new(nameof(Strong));
internal static readonly ColourDescription Vibrant = new(nameof(Vibrant));
internal static readonly ColourDescription Red = new(nameof(Red));
internal static readonly ColourDescription Orange = new(nameof(Orange));
internal static readonly ColourDescription Yellow = new(nameof(Yellow));
internal static readonly ColourDescription Chartreuse = new(nameof(Chartreuse));
internal static readonly ColourDescription Green = new(nameof(Green));
internal static readonly ColourDescription Mint = new(nameof(Mint));
internal static readonly ColourDescription Cyan = new(nameof(Cyan));
internal static readonly ColourDescription Azure = new(nameof(Azure));
internal static readonly ColourDescription Blue = new(nameof(Blue));
internal static readonly ColourDescription Violet = new(nameof(Violet));
internal static readonly ColourDescription Magenta = new(nameof(Magenta));
internal static readonly ColourDescription Rose = new(nameof(Rose));
internal static readonly List<ColourDescription> Lightnesses = new() { Shadow, Dark, Pure, Light, Pale };
internal static readonly List<ColourDescription> Saturations = new() { Faint, Weak, Mild, Strong, Vibrant };
internal static readonly List<ColourDescription> Hues = new()
{ Red, Orange, Yellow, Chartreuse, Green, Mint, Cyan, Azure, Blue, Violet, Magenta, Rose };
internal static readonly List<ColourDescription> Greyscales = new() { Black, Grey, White };
internal static IEnumerable<ColourDescription> Get(Hsl hsl)
{
if (hsl.UseAsNaN) return new List<ColourDescription> { NotApplicable };
var (h, s, l) = hsl.ConstrainedTriplet;
switch (l)
{
case <= 0: return new List<ColourDescription> { Black };
case >= 1: return new List<ColourDescription> { White };
}
var lightness = l switch
{
< 0.20 => Shadow,
< 0.40 => Dark,
< 0.60 => Pure,
< 0.80 => Light,
_ => Pale
};
if (hsl.UseAsGreyscale) return new List<ColourDescription> { lightness, Grey };
var strength = s switch
{
< 0.20 => Faint,
< 0.40 => Weak,
< 0.60 => Mild,
< 0.80 => Strong,
_ => Vibrant
};
var hue = h switch
{
< 15 => Red,
< 45 => Orange,
< 75 => Yellow,
< 105 => Chartreuse,
< 135 => Green,
< 165 => Mint,
< 195 => Cyan,
< 225 => Azure,
< 255 => Blue,
< 285 => Violet,
< 315 => Magenta,
< 345 => Rose,
_ => Red
};
return new List<ColourDescription> { lightness, strength, hue };
}
public override string ToString() => description.ToLower();
}
}

View File

@@ -0,0 +1,47 @@
namespace Wacton.Unicolour
{
internal record ColourHeritage(string description)
{
private readonly string description = description;
internal static readonly ColourHeritage None = new(nameof(None));
internal static readonly ColourHeritage NaN = new(nameof(NaN));
internal static readonly ColourHeritage Greyscale = new(nameof(Greyscale));
internal static readonly ColourHeritage Hued = new(nameof(Hued));
internal static readonly ColourHeritage GreyscaleAndHued = new(nameof(GreyscaleAndHued));
internal static ColourHeritage From(ColourRepresentation parent)
{
if (parent.UseAsNaN) return NaN;
return parent switch
{
{ UseAsGreyscale: true, UseAsHued: false } => Greyscale,
{ UseAsGreyscale: false, UseAsHued: true } => Hued,
{ UseAsGreyscale: true, UseAsHued: true } => GreyscaleAndHued,
_ => None
};
}
internal static ColourHeritage From(ColourRepresentation firstParent, ColourRepresentation secondParent)
{
var first = From(firstParent);
var second = From(secondParent);
var eitherNaN = first == NaN || second == NaN;
var bothGreyscale = (first == Greyscale || first == GreyscaleAndHued) &&
(second == Greyscale || second == GreyscaleAndHued);
var eitherHued = first == Hued || first == GreyscaleAndHued || second == Hued || second == GreyscaleAndHued;
if (eitherNaN) return NaN;
if (bothGreyscale)
{
return eitherHued ? GreyscaleAndHued : Greyscale;
}
return eitherHued ? Hued : None;
}
public override string ToString() => description;
}
}

View File

@@ -0,0 +1,67 @@
namespace Wacton.Unicolour
{
public abstract record ColourRepresentation
{
protected readonly double First;
protected readonly double Second;
protected readonly double Third;
protected abstract int? HueIndex { get; }
public ColourTriplet Triplet => new(First, Second, Third, HueIndex);
internal ColourHeritage Heritage { get; }
protected virtual double ConstrainedFirst => First;
protected virtual double ConstrainedSecond => Second;
protected virtual double ConstrainedThird => Third;
public ColourTriplet ConstrainedTriplet => new(ConstrainedFirst, ConstrainedSecond, ConstrainedThird, HueIndex);
internal bool IsNaN => double.IsNaN(First) || double.IsNaN(Second) || double.IsNaN(Third);
internal bool UseAsNaN => Heritage == ColourHeritage.NaN || IsNaN;
/*
* a representation may be non-greyscale according to its values
* but should be used as greyscale if it was generated from a representation that was greyscale
* e.g. RGB(1,1,1) is greyscale and converts to LAB(99.99999999999999, 0, -2.220446049250313E-14)
* LAB doesn't report as greyscale since B != 0 (and I don't want to make assumptions via precision-based tolerance comparison)
* but it should be considered greyscale since the source RGB representation definitely is
*/
internal abstract bool IsGreyscale { get; }
internal bool UseAsGreyscale => Heritage == ColourHeritage.Greyscale ||
Heritage == ColourHeritage.GreyscaleAndHued || (!UseAsNaN && IsGreyscale);
/*
* a representation is considered "hued" when it has a hue component (e.g. HSL / LCH) and is not greyscale
* enabling differentiation between representations where:
* a) a hue value is meaningful ------------------------------ e.g. HSB(0,0,0) = red with no saturation or brightness
* b) a hue value is used as a fallback when there is no hue - e.g. RGB(0,0,0) -> HSB(0,0,0) = black with no red
* which is essential for proper mixing;
* [RGB(0,0,0) black -> RGB(0,0,255) blue] via HSB is [HSB(0,0,0) red with no colour -> HSB(240,1,1) blue with full colour]
* but the mixing should only start at the red hue if the value 0 was provided by the user (FromHsb instead of FromRgb)
*/
internal bool HasHueComponent => HueIndex != null;
internal bool UseAsHued =>
(Heritage == ColourHeritage.None || Heritage == ColourHeritage.Hued ||
Heritage == ColourHeritage.GreyscaleAndHued) && !UseAsNaN && HasHueComponent;
internal ColourRepresentation(double first, double second, double third, ColourHeritage heritage)
{
First = first;
Second = second;
Third = third;
Heritage = heritage;
}
protected abstract string FirstString { get; }
protected abstract string SecondString { get; }
protected abstract string ThirdString { get; }
public override string ToString()
{
var values = $"{FirstString} {SecondString} {ThirdString}";
return UseAsNaN ? $"NaN [{values}]" : values;
}
}
}

29
Unicolour/ColourSpace.cs Normal file
View File

@@ -0,0 +1,29 @@
namespace Wacton.Unicolour
{
public enum ColourSpace
{
Rgb,
Rgb255,
RgbLinear,
Hsb,
Hsl,
Hwb,
Xyz,
Xyy,
Lab,
Lchab,
Luv,
Lchuv,
Hsluv,
Hpluv,
Ictcp,
Jzazbz,
Jzczhz,
Oklab,
Oklch,
Cam02,
Cam16,
Hct
}
}

View File

@@ -0,0 +1,84 @@
using System;
namespace Wacton.Unicolour
{
public record ColourTriplet(double First, double Second, double Third, int? HueIndex = null)
{
public double First { get; } = First;
public double Second { get; } = Second;
public double Third { get; } = Third;
public (double, double, double) Tuple => (First, Second, Third);
public int? HueIndex { get; } = HueIndex;
public double[] AsArray() => new[] { First, Second, Third };
internal double HueValue()
{
return HueIndex switch
{
0 => First,
2 => Third,
_ => throw new ArgumentOutOfRangeException()
};
}
internal ColourTriplet WithHueOverride(double hue)
{
return HueIndex switch
{
0 => new(hue, Second, Third, HueIndex),
2 => new(First, Second, hue, HueIndex),
_ => throw new ArgumentOutOfRangeException()
};
}
internal ColourTriplet WithHueModulo()
{
return HueIndex switch
{
0 => new(First.Modulo(360), Second, Third, HueIndex),
2 => new(First, Second, Third.Modulo(360), HueIndex),
null => new(First, Second, Third, HueIndex),
_ => throw new ArgumentOutOfRangeException()
};
}
internal ColourTriplet WithPremultipliedAlpha(double alpha)
{
return HueIndex switch
{
0 => new(First, Second * alpha, Third * alpha, HueIndex),
2 => new(First * alpha, Second * alpha, Third, HueIndex),
null => new(First * alpha, Second * alpha, Third * alpha, HueIndex),
_ => throw new ArgumentOutOfRangeException()
};
}
internal ColourTriplet WithUnpremultipliedAlpha(double alpha)
{
if (alpha == 0)
{
alpha = 1.0;
}
return HueIndex switch
{
0 => new(First, Second / alpha, Third / alpha, HueIndex),
2 => new(First / alpha, Second / alpha, Third, HueIndex),
null => new(First / alpha, Second / alpha, Third / alpha, HueIndex),
_ => throw new ArgumentOutOfRangeException()
};
}
// need a custom deconstruct to ignore the nullable hue index
public void Deconstruct(out double first, out double second, out double third)
{
first = First;
second = Second;
third = Third;
}
public override string ToString() => Tuple.ToString();
}
}

17
Unicolour/Companding.cs Normal file
View File

@@ -0,0 +1,17 @@
using System;
namespace Wacton.Unicolour
{
public static class Companding
{
public static double Gamma(double value, double gamma) => Math.Pow(value, 1 / gamma);
public static double InverseGamma(double value, double gamma) => Math.Pow(value, gamma);
public static double ReflectWhenNegative(double value, Func<double, double> function)
{
if (double.IsNaN(value)) return double.NaN;
return Math.Sign(value) * function(Math.Abs(value));
}
}
}

246
Unicolour/Comparison.cs Normal file
View File

@@ -0,0 +1,246 @@
using System;
namespace Wacton.Unicolour
{
using static Utils;
internal static class Comparison
{
// https://www.w3.org/WAI/WCAG21/Techniques/general/G18.html#tests
// minimal recommended contrast ratio is 4.5, or 3 for larger font-sizes
internal static double Contrast(Unicolour colour1, Unicolour colour2)
{
var luminance1 = colour1.RelativeLuminance;
var luminance2 = colour2.RelativeLuminance;
var l1 = Math.Max(luminance1, luminance2); // lighter of the colours
var l2 = Math.Min(luminance1, luminance2); // darker of the colours
return (l1 + 0.05) / (l2 + 0.05);
}
internal static double Difference(DeltaE deltaE, Unicolour reference, Unicolour sample)
{
return deltaE switch
{
DeltaE.Cie76 => DeltaE76(reference, sample),
DeltaE.Cie94 => DeltaE94(reference, sample),
DeltaE.Cie94Textiles => DeltaE94(reference, sample, isForTextiles: true),
DeltaE.Ciede2000 => DeltaE00(reference, sample),
DeltaE.CmcAcceptability => DeltaECmc(reference, sample, l: 1, c: 1),
DeltaE.CmcPerceptibility => DeltaECmc(reference, sample, l: 2, c: 1),
DeltaE.Itp => DeltaEItp(reference, sample),
DeltaE.Z => DeltaEz(reference, sample),
DeltaE.Hyab => DeltaEHyab(reference, sample),
DeltaE.Ok => DeltaEOk(reference, sample),
DeltaE.Cam02 => DeltaECam02(reference, sample),
DeltaE.Cam16 => DeltaECam16(reference, sample),
_ => throw new ArgumentOutOfRangeException(nameof(deltaE), deltaE, null)
};
}
// https://en.wikipedia.org/wiki/Color_difference#CIE76
private static double DeltaE76(Unicolour reference, Unicolour sample)
{
var (l1, a1, b1) = reference.Lab.Triplet;
var (l2, a2, b2) = sample.Lab.Triplet;
return Math.Sqrt(SquaredDiff(l1, l2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
}
// https://en.wikipedia.org/wiki/Color_difference#CIE94
private static double DeltaE94(Unicolour reference, Unicolour sample, bool isForTextiles = false)
{
var (l1, a1, b1) = reference.Lab.Triplet;
var (l2, a2, b2) = sample.Lab.Triplet;
var (_, c1, _) = reference.Lchab.Triplet;
var (_, c2, _) = sample.Lchab.Triplet;
var lDelta = l1 - l2;
var aDelta = a1 - a2;
var bDelta = b1 - b2;
var cDelta = c1 - c2;
var hDelta = Math.Sqrt(
Math.Pow(aDelta, 2) +
Math.Pow(bDelta, 2) -
Math.Pow(cDelta, 2)
);
var k1 = isForTextiles ? 0.048 : 0.045;
var k2 = isForTextiles ? 0.014 : 0.015;
var kl = isForTextiles ? 2 : 1;
const int kc = 1;
const int kh = 1;
const int sl = 1;
var sc = 1 + k1 * c1;
var sh = 1 + k2 * c1;
return Math.Sqrt(
Math.Pow(lDelta / (kl * sl), 2) +
Math.Pow(cDelta / (kc * sc), 2) +
Math.Pow(hDelta / (kh * sh), 2)
);
}
// https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
private static double DeltaE00(Unicolour reference, Unicolour sample)
{
var (l1, a1, b1) = reference.Lab.Triplet;
var (l2, a2, b2) = sample.Lab.Triplet;
var (_, c1, _) = reference.Lchab.Triplet;
var (_, c2, _) = sample.Lchab.Triplet;
double Power2(double value) => Math.Pow(value, 2);
double Power7(double value) => Math.Pow(value, 7);
double SqrtCPower7(double c) => Math.Sqrt(Power7(c) / (Power7(c) + Power7(25)));
var avgL = (l1 + l2) / 2.0;
var avgC = (c1 + c2) / 2.0;
var g = 0.5 * (1 - SqrtCPower7(avgC));
var a1Prime = a1 * (1 + g);
var a2Prime = a2 * (1 + g);
var c1Prime = Math.Sqrt(Power2(a1Prime) + Power2(b1));
var c2Prime = Math.Sqrt(Power2(a2Prime) + Power2(b2));
var avgCPrime = (c1Prime + c2Prime) / 2.0;
var h1Prime = ToDegrees(Math.Atan2(b1, a1Prime)).Modulo(360);
var h2Prime = ToDegrees(Math.Atan2(b2, a2Prime)).Modulo(360);
var hPrimeDelta = Math.Abs(h1Prime - h2Prime) switch
{
<= 180 => h2Prime - h1Prime,
> 180 when h2Prime <= h1Prime => h2Prime - h1Prime + 360,
> 180 when h2Prime > h1Prime => h2Prime - h1Prime - 360,
_ => double.NaN
};
var avgHPrime = Math.Abs(h1Prime - h2Prime) switch
{
<= 180 => (h1Prime + h2Prime) / 2.0,
> 180 when h1Prime + h2Prime < 360 => (h1Prime + h2Prime + 360) / 2.0,
> 180 when h1Prime + h2Prime >= 360 => (h1Prime + h2Prime - 360) / 2.0,
_ => double.NaN
};
var t = 1 -
0.17 * Math.Cos(ToRadians(avgHPrime - 30)) +
0.24 * Math.Cos(ToRadians(2 * avgHPrime)) +
0.32 * Math.Cos(ToRadians(3 * avgHPrime + 6)) -
0.20 * Math.Cos(ToRadians(4 * avgHPrime - 63));
var deltaLPrime = l2 - l1;
var deltaCPrime = c2 - c1;
var deltaHPrime = 2 * Math.Sqrt(c1Prime * c2Prime) * Math.Sin(ToRadians(hPrimeDelta / 2.0));
const int kl = 1;
const int kc = 1;
const int kh = 1;
var sl = 1 + (0.015 * Power2(avgL - 50)) / Math.Sqrt(20 + Power2(avgL - 50));
var sc = 1 + 0.045 * avgCPrime;
var sh = 1 + 0.015 * avgCPrime * t;
var deltaTheta = 30 * Math.Exp(-Power2((avgHPrime - 275) / 25.0));
var rc = 2 * SqrtCPower7(avgCPrime);
var rt = -rc * Math.Sin(ToRadians(2 * deltaTheta));
double Ratio(double delta, double k, double s) => delta / (k * s);
var ratioL = Ratio(deltaLPrime, kl, sl);
var ratioC = Ratio(deltaCPrime, kc, sc);
var ratioH = Ratio(deltaHPrime, kh, sh);
return Math.Sqrt(
Power2(ratioL) +
Power2(ratioC) +
Power2(ratioH) +
rt * ratioC * ratioH);
}
// https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_(1984)
private static double DeltaECmc(Unicolour reference, Unicolour sample, double l, double c)
{
var (l1, a1, b1) = reference.Lab.Triplet;
var (l2, a2, b2) = sample.Lab.Triplet;
var (_, c1, h1) = reference.Lchab.ConstrainedTriplet;
var (_, c2, _) = sample.Lchab.Triplet;
var lDelta = l1 - l2;
var aDelta = a1 - a2;
var bDelta = b1 - b2;
var cDelta = c1 - c2;
var hDelta = Math.Sqrt(
Math.Pow(aDelta, 2) +
Math.Pow(bDelta, 2) -
Math.Pow(cDelta, 2)
);
var f = Math.Sqrt(Math.Pow(c1, 4) / (Math.Pow(c1, 4) + 1900));
var t = h1 is > 165 and <= 345
? 0.56 + Math.Abs(0.2 * Math.Cos(ToRadians(h1 + 168)))
: 0.36 + Math.Abs(0.4 * Math.Cos(ToRadians(h1 + 35)));
var sl = l1 < 16 ? 0.511 : 0.040975 * l1 / (1 + 0.01765 * l1);
var sc = 0.0638 * c1 / (1 + 0.0131 * c1) + 0.638;
var sh = sc * (f * t + 1 - f);
return Math.Sqrt(
Math.Pow(lDelta / (l * sl), 2) +
Math.Pow(cDelta / (c * sc), 2) +
Math.Pow(hDelta / sh, 2)
);
}
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2124-0-201901-I!!PDF-E.pdf
private static double DeltaEItp(Unicolour reference, Unicolour sample)
{
ColourTriplet ToItp(Ictcp ictcp) => new(ictcp.I, 0.5 * ictcp.Ct, ictcp.Cp);
var (i1, t1, c1) = ToItp(reference.Ictcp);
var (i2, t2, c2) = ToItp(sample.Ictcp);
return 720 * Math.Sqrt(SquaredDiff(i1, i2) + SquaredDiff(t1, t2) + SquaredDiff(c1, c2));
}
// https://doi.org/10.1364/OE.25.015131
private static double DeltaEz(Unicolour reference, Unicolour sample)
{
var (jz1, cz1, hz1) = reference.Jzczhz.Triplet;
var (jz2, cz2, hz2) = sample.Jzczhz.Triplet;
var deltaHz = hz1 - hz2;
deltaHz = 2 * Math.Sqrt(reference.Jzczhz.C * sample.Jzczhz.C) * Math.Sin(ToRadians(deltaHz / 2.0));
deltaHz = Math.Pow(deltaHz, 2);
return Math.Sqrt(SquaredDiff(jz1, jz2) + SquaredDiff(cz1, cz2) + deltaHz);
}
// https://en.wikipedia.org/wiki/Color_difference#Other_geometric_constructions
private static double DeltaEHyab(Unicolour reference, Unicolour sample)
{
var (l1, a1, b1) = reference.Lab.Triplet;
var (l2, a2, b2) = sample.Lab.Triplet;
return Math.Sqrt(SquaredDiff(a1, a2) + SquaredDiff(b1, b2)) + Math.Abs(l2 - l1);
}
// https://www.w3.org/TR/css-color-4/#color-difference-OK
private static double DeltaEOk(Unicolour reference, Unicolour sample)
{
var (l1, a1, b1) = reference.Oklab.Triplet;
var (l2, a2, b2) = sample.Oklab.Triplet;
return Math.Sqrt(SquaredDiff(l1, l2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
}
// https://doi.org/10.1007/978-1-4419-6190-7_2
// currently only support UCS, not LCD or SCD - no need to handle ΔJ / kl since kl = 1
private static double DeltaECam02(Unicolour reference, Unicolour sample)
{
var (j1, a1, b1) = reference.Cam02.Triplet;
var (j2, a2, b2) = sample.Cam02.Triplet;
return Math.Sqrt(SquaredDiff(j1, j2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
}
// https://doi.org/10.1002/col.22131
private static double DeltaECam16(Unicolour reference, Unicolour sample)
{
var (j1, a1, b1) = reference.Cam16.Triplet;
var (j2, a2, b2) = sample.Cam16.Triplet;
var deltaE = Math.Sqrt(SquaredDiff(j1, j2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
return 1.41 * Math.Pow(deltaE, 0.63);
}
private static double SquaredDiff(double first, double second) => Math.Pow(second - first, 2);
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace Wacton.Unicolour
{
public class Configuration
{
internal readonly Guid Id = Guid.NewGuid();
public RgbConfiguration Rgb { get; }
public XyzConfiguration Xyz { get; }
public CamConfiguration Cam { get; }
public double IctcpScalar { get; }
public double JzazbzScalar { get; }
public static readonly Configuration Default = new();
public Configuration(
RgbConfiguration? rgbConfiguration = null,
XyzConfiguration? xyzConfiguration = null,
CamConfiguration? camConfiguration = null,
double ictcpScalar = 100,
double jzazbzScalar = 100)
{
Rgb = rgbConfiguration ?? RgbConfiguration.StandardRgb;
Xyz = xyzConfiguration ?? XyzConfiguration.D65;
Cam = camConfiguration ?? CamConfiguration.StandardRgb;
IctcpScalar = ictcpScalar;
JzazbzScalar = jzazbzScalar;
}
public override string ToString() => $"{Id}";
}
}

19
Unicolour/DeltaE.cs Normal file
View File

@@ -0,0 +1,19 @@
namespace Wacton.Unicolour
{
public enum DeltaE
{
Cie76,
Cie94,
Cie94Textiles,
Ciede2000,
CmcAcceptability,
CmcPerceptibility,
Itp,
Z,
Hyab,
Ok,
Cam02,
Cam16
}
}

91
Unicolour/GamutMapping.cs Normal file
View File

@@ -0,0 +1,91 @@
namespace Wacton.Unicolour
{
internal static class GamutMapping
{
/*
* adapted from https://www.w3.org/TR/css-color-4/#css-gamut-mapping & https://www.w3.org/TR/css-color-4/#binsearch
* the pseudocode doesn't appear to handle the edge case scenario where:
* a) origin colour OKLCH chroma < epsilon
* b) origin colour destination (RGB here) is out-of-gamut
* e.g. OKLCH (0.99999, 0, 0) --> RGB (1.00010, 0.99998, 0.99974)
* - the search never executes since chroma = 0 (min 0 - max 0 < epsilon 0.0001)
* - even if the search did execute, would not return clipped variant since ΔE is *too small*, and min never changes from 0
* so need to clip if the mapped colour is somehow out-of-gamut (i.e. not processed)
*/
internal static Unicolour ToRgbGamut(Unicolour unicolour)
{
var config = unicolour.Config;
var rgb = unicolour.Rgb;
var alpha = unicolour.Alpha.A;
if (unicolour.IsInDisplayGamut) return new Unicolour(ColourSpace.Rgb, config, rgb.Triplet.Tuple, alpha);
var oklch = unicolour.Oklch;
if (oklch.L >= 1.0) return new Unicolour(ColourSpace.Rgb, config, 1, 1, 1, alpha);
if (oklch.L <= 0.0) return new Unicolour(ColourSpace.Rgb, config, 0, 0, 0, alpha);
const double jnd = 0.02;
const double epsilon = 0.0001;
var minChroma = 0.0;
var maxChroma = oklch.C;
var minChromaInGamut = true;
// iteration count ensures the while loop doesn't get stuck in an endless cycle if bad input is provided
// e.g. double.Epsilon
var iterations = 0;
Unicolour? current = null;
bool HasChromaConverged() => maxChroma - minChroma <= epsilon;
while (!HasChromaConverged() && iterations < 1000)
{
iterations++;
var chroma = (minChroma + maxChroma) / 2.0;
current = FromOklchWithChroma(chroma);
if (minChromaInGamut && current.Rgb.IsInGamut)
{
minChroma = chroma;
continue;
}
var clipped = FromRgbWithClipping(current.Rgb);
var deltaE = clipped.Difference(DeltaE.Ok, current);
var isNoticeableDifference = deltaE >= jnd;
if (isNoticeableDifference)
{
maxChroma = chroma;
}
else
{
// not clear to me why a clipped colour must have ΔE from "current" colour between 0.0199 - 0.02
// effectively: only returning clipped when ΔE == JND, but continue if the non-noticeable ΔE is *too small*
// but I assume it's something to do with this comment about intersecting shallow and concave gamut boundaries
// https://github.com/w3c/csswg-drafts/issues/7653#issuecomment-1489096489
var isUnnoticeableDifferenceLargeEnough = jnd - deltaE < epsilon;
if (isUnnoticeableDifferenceLargeEnough)
{
return clipped;
}
minChromaInGamut = false;
minChroma = chroma;
}
}
// in case while loop never executes (e.g. Oklch.C == 0)
current ??= FromOklchWithChroma(oklch.C);
// it's possible for the "current" colour to still be out of RGB gamut, either because:
// a) the original OKLCH was not processed (chroma too low) and was already out of RGB gamut
// b) the algorithm converged on an OKLCH that is out of RGB gamut (happens ~5% of the time for me with using random OKLCH inputs)
return current.IsInDisplayGamut ? current : FromRgbWithClipping(current.Rgb);
Unicolour FromOklchWithChroma(double chroma) =>
new(ColourSpace.Oklch, config, oklch.L, chroma, oklch.H, alpha);
Unicolour FromRgbWithClipping(Rgb unclippedRgb) =>
new(ColourSpace.Rgb, config, unclippedRgb.ConstrainedTriplet.Tuple, alpha);
}
}
}

156
Unicolour/Hct.cs Normal file
View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
namespace Wacton.Unicolour
{
public record Hct : ColourRepresentation
{
protected override int? HueIndex => 0;
public double H => First;
public double C => Second;
public double T => Third;
public double ConstrainedH => ConstrainedFirst;
protected override double ConstrainedFirst => H.Modulo(360.0);
internal override bool IsGreyscale => C <= 0.0 || T is <= 0.0 or >= 100.0;
public Hct(double h, double c, double t) : this(h, c, t, ColourHeritage.None)
{
}
internal Hct(double h, double c, double t, ColourHeritage heritage) : base(h, c, t, heritage)
{
}
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
protected override string SecondString => $"{C:F2}";
protected override string ThirdString => $"{T:F2}";
public override string ToString() => base.ToString();
/*
* HCT is a transform of XYZ
* (just a combination of LAB & CAM16, but with specific XYZ & CAM configuration, so can't reuse existing colour space calculations)
* Forward: https://material.io/blog/science-of-color-design
* Reverse: n/a - no published reverse transform and I don't want to port Google code, so using my own naive search
*/
internal static Cam16 Cam16Component(Xyz xyz) => Cam16.FromXyz(xyz, CamConfiguration.Hct, XyzConfiguration.D65);
internal static Lab LabComponent(Xyz xyz) => Lab.FromXyz(xyz, XyzConfiguration.D65);
internal static Hct FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
{
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
var d65Xyz = new Xyz(d65Matrix.ToTriplet(), ColourHeritage.From(xyz));
var cam16 = Cam16Component(d65Xyz);
var lab = LabComponent(d65Xyz);
var h = cam16.Model.H;
var c = cam16.Model.C;
var t = lab.L;
return new Hct(h, c, t, ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Hct hct, XyzConfiguration xyzConfig)
{
var targetY = Lab.ToXyz(new Lab(hct.T, 0, 0), XyzConfiguration.D65).Y;
var result = FindBestJ(targetY, hct);
var d65Xyz = result.Converged ? result.Data.Xyz : new Xyz(double.NaN, double.NaN, double.NaN);
var d65Matrix = Matrix.FromTriplet(d65Xyz.Triplet);
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
var xyz = new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(hct))
{
HctToXyzSearchResult = result
};
return xyz;
}
// i'm sure some smart people have some fancy-pants algorithms to do this efficiently
// but until there's some kind of published reverse transformation algorithm, this gets the job done
// (albeit rather slowly...)
private static HctToXyzSearchResult FindBestJ(double targetY, Hct hct)
{
HctToXyzSearchData latest = GetStartingData(targetY, hct);
HctToXyzSearchData best = latest;
var step = latest.J;
var iterations = 0;
while (!double.IsNaN(latest.DeltaY) && Math.Abs(latest.DeltaY) > 0.000000001 && iterations < 100)
{
var j = latest.J + (latest.DeltaY > 0 ? -step : step);
var data = ProcessJ(targetY, j, hct);
var deltaY = data.DeltaY;
if (Math.Abs(deltaY) < Math.Abs(best.DeltaY))
{
best = data;
}
// change in sign of delta means target is now in the other direction
var overshot = double.IsNaN(deltaY) || Math.Sign(latest.DeltaY) != Math.Sign(deltaY);
if (overshot)
{
step /= 2.0;
}
latest = data;
iterations++;
}
var converged = !double.IsNaN(latest.DeltaY) && iterations < 100;
return new HctToXyzSearchResult(best, iterations, converged);
}
private static HctToXyzSearchData GetStartingData(double targetY, Hct hct)
{
var xzPairs = new List<(double, double)> { (0, 0), (0, 1), (1, 0), (1, 1) };
HctToXyzSearchData best = InitialData;
foreach (var (x, z) in xzPairs)
{
var j = Cam16.FromXyz(new Xyz(x, targetY, z), CamConfiguration.Hct, XyzConfiguration.D65).Model.J;
var data = ProcessJ(targetY, j, hct);
if (Math.Abs(data.DeltaY) < best.DeltaY)
{
best = data;
}
}
return best;
}
private static HctToXyzSearchData ProcessJ(double targetY, double j, Hct hct)
{
var camModel = new Cam.Model(j, hct.C, hct.H, 0, 0, 0);
var cam16 = new Cam16(camModel, CamConfiguration.Hct, ColourHeritage.None);
var xyz = Cam16.ToXyz(cam16, CamConfiguration.Hct, XyzConfiguration.D65);
var deltaY = xyz.Y - targetY;
return new HctToXyzSearchData(hct, j, cam16, xyz, targetY, deltaY);
}
private static readonly HctToXyzSearchData InitialData = new(
J: double.PositiveInfinity, DeltaY: double.PositiveInfinity,
Hct: null!, Cam16: null!, Xyz: null!, TargetY: double.NaN);
}
// only for potential debugging or diagnostics
// until there is an "official" HCT -> XYZ reverse transform
internal record HctToXyzSearchResult(HctToXyzSearchData Data, int Iterations, bool Converged)
{
internal HctToXyzSearchData Data { get; } = Data;
internal int Iterations { get; } = Iterations;
internal bool Converged { get; } = Converged;
public override string ToString() => $"{Data} · Iterations:{Iterations} · Converged:{Converged}";
}
internal record HctToXyzSearchData(Hct Hct, double J, Cam16 Cam16, Xyz Xyz, double TargetY, double DeltaY)
{
internal Hct Hct { get; } = Hct;
internal double J { get; } = J;
internal Cam16 Cam16 { get; } = Cam16;
internal Xyz Xyz { get; } = Xyz;
internal double TargetY { get; } = TargetY;
internal double DeltaY { get; } = DeltaY;
public override string ToString() => $"J:{J:F4} · ΔY:{DeltaY:F4}";
}
}

95
Unicolour/Hpluv.cs Normal file
View File

@@ -0,0 +1,95 @@
namespace Wacton.Unicolour
{
public record Hpluv : ColourRepresentation
{
protected override int? HueIndex => 0;
public double H => First;
public double S => Second;
public double L => Third;
public double ConstrainedH => ConstrainedFirst;
public double ConstrainedS => ConstrainedSecond;
public double ConstrainedL => ConstrainedThird;
protected override double ConstrainedFirst => H.Modulo(360.0);
protected override double ConstrainedSecond => S.Clamp(0.0, 100.0);
protected override double ConstrainedThird => L.Clamp(0.0, 100.0);
internal override bool IsGreyscale => S <= 0.0 || L is <= 0.0 or >= 100.0;
public Hpluv(double h, double s, double l) : this(h, s, l, ColourHeritage.None)
{
}
internal Hpluv(double h, double s, double l, ColourHeritage heritage) : base(h, s, l, heritage)
{
}
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
protected override string SecondString => $"{S:F1}%";
protected override string ThirdString => $"{L:F1}%";
public override string ToString() => base.ToString();
/*
* HPLUV is a transform of LCHUV
* Forward: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L397
* Reverse: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L380
*/
internal static Hpluv FromLchuv(Lchuv lchuv)
{
var (lchLightness, chroma, hue) = lchuv.ConstrainedTriplet;
double saturation;
double lightness;
switch (lchLightness)
{
case > 99.9999999:
saturation = 0.0;
lightness = 100.0;
break;
case < 0.00000001:
saturation = 0.0;
lightness = 0.0;
break;
default:
{
var maxChroma = BoundingLines.CalculateMaxChroma(lchLightness);
saturation = chroma / maxChroma * 100;
lightness = lchLightness;
break;
}
}
return new Hpluv(hue, saturation, lightness, ColourHeritage.From(lchuv));
}
internal static Lchuv ToLchuv(Hpluv hpluv)
{
var hue = hpluv.ConstrainedH;
var saturation = hpluv.S;
var hslLightness = hpluv.L;
double lightness;
double chroma;
switch (hslLightness)
{
case > 99.9999999:
lightness = 100.0;
chroma = 0.0;
break;
case < 0.00000001:
lightness = 0.0;
chroma = 0.0;
break;
default:
{
var maxChroma = BoundingLines.CalculateMaxChroma(hslLightness);
chroma = maxChroma / 100 * saturation;
lightness = hslLightness;
break;
}
}
return new Lchuv(lightness, chroma, hue, ColourHeritage.From(hpluv));
}
}
}

82
Unicolour/Hsb.cs Normal file
View File

@@ -0,0 +1,82 @@
using System;
using System.Linq;
namespace Wacton.Unicolour
{
public record Hsb : ColourRepresentation
{
protected override int? HueIndex => 0;
public double H => First;
public double S => Second;
public double B => Third;
public double ConstrainedH => ConstrainedFirst;
public double ConstrainedS => ConstrainedSecond;
public double ConstrainedB => ConstrainedThird;
protected override double ConstrainedFirst => H.Modulo(360.0);
protected override double ConstrainedSecond => S.Clamp(0.0, 1.0);
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
internal override bool IsGreyscale => S <= 0.0 || B <= 0.0;
public Hsb(double h, double s, double b) : this(h, s, b, ColourHeritage.None)
{
}
internal Hsb(double h, double s, double b, ColourHeritage heritage) : base(h, s, b, heritage)
{
}
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
protected override string SecondString => $"{S * 100:F1}%";
protected override string ThirdString => $"{B * 100:F1}%";
public override string ToString() => base.ToString();
/*
* HSB is a transform of RGB
* Forward: https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
* Reverse: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
*/
internal static Hsb FromRgb(Rgb rgb)
{
var (r, g, b) = rgb.ConstrainedTriplet;
var components = new[] { r, g, b };
var xMax = components.Max();
var xMin = components.Min();
var chroma = xMax - xMin;
double hue;
if (chroma == 0.0) hue = 0;
else if (xMax == r) hue = 60 * (0 + (g - b) / chroma);
else if (xMax == g) hue = 60 * (2 + (b - r) / chroma);
else if (xMax == b) hue = 60 * (4 + (r - g) / chroma);
else hue = double.NaN;
var brightness = xMax;
var saturation = brightness == 0 ? 0 : chroma / brightness;
return new Hsb(hue.Modulo(360.0), saturation, brightness, ColourHeritage.From(rgb));
}
internal static Rgb ToRgb(Hsb hsb)
{
var (hue, saturation, brightness) = hsb.ConstrainedTriplet;
var chroma = brightness * saturation;
var h = hue / 60;
var x = chroma * (1 - Math.Abs(h % 2 - 1));
var (r, g, b) = h switch
{
< 1 => (chroma, x, 0.0),
< 2 => (x, chroma, 0.0),
< 3 => (0.0, chroma, x),
< 4 => (0.0, x, chroma),
< 5 => (x, 0.0, chroma),
< 6 => (chroma, 0.0, x),
_ => (0.0, 0.0, 0.0)
};
var m = brightness - chroma;
var (red, green, blue) = (r + m, g + m, b + m);
return new Rgb(red, green, blue, ColourHeritage.From(hsb));
}
}
}

61
Unicolour/Hsl.cs Normal file
View File

@@ -0,0 +1,61 @@
using System;
namespace Wacton.Unicolour
{
public record Hsl : ColourRepresentation
{
protected override int? HueIndex => 0;
public double H => First;
public double S => Second;
public double L => Third;
public double ConstrainedH => ConstrainedFirst;
public double ConstrainedS => ConstrainedSecond;
public double ConstrainedL => ConstrainedThird;
protected override double ConstrainedFirst => H.Modulo(360.0);
protected override double ConstrainedSecond => S.Clamp(0.0, 1.0);
protected override double ConstrainedThird => L.Clamp(0.0, 1.0);
internal override bool IsGreyscale => S <= 0.0 || L is <= 0.0 or >= 1.0;
public Hsl(double h, double s, double l) : this(h, s, l, ColourHeritage.None)
{
}
internal Hsl(double h, double s, double l, ColourHeritage heritage) : base(h, s, l, heritage)
{
}
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
protected override string SecondString => $"{S * 100:F1}%";
protected override string ThirdString => $"{L * 100:F1}%";
public override string ToString() => base.ToString();
/*
* HSL is a transform of HSB (in terms of Unicolour implementation)
* Forward: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
* Reverse: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
*/
internal static Hsl FromHsb(Hsb hsb)
{
var (hue, hsbSaturation, brightness) = hsb.ConstrainedTriplet;
var lightness = brightness * (1 - hsbSaturation / 2);
var saturation = lightness is > 0.0 and < 1.0
? (brightness - lightness) / Math.Min(lightness, 1 - lightness)
: 0;
return new Hsl(hue, saturation, lightness, ColourHeritage.From(hsb));
}
internal static Hsb ToHsb(Hsl hsl)
{
var (hue, hslSaturation, lightness) = hsl.ConstrainedTriplet;
var brightness = lightness + hslSaturation * Math.Min(lightness, 1 - lightness);
var saturation = brightness > 0.0
? 2 * (1 - lightness / brightness)
: 0;
return new Hsb(hue, saturation, brightness, ColourHeritage.From(hsl));
}
}
}

95
Unicolour/Hsluv.cs Normal file
View File

@@ -0,0 +1,95 @@
namespace Wacton.Unicolour
{
public record Hsluv : ColourRepresentation
{
protected override int? HueIndex => 0;
public double H => First;
public double S => Second;
public double L => Third;
public double ConstrainedH => ConstrainedFirst;
public double ConstrainedS => ConstrainedSecond;
public double ConstrainedL => ConstrainedThird;
protected override double ConstrainedFirst => H.Modulo(360.0);
protected override double ConstrainedSecond => S.Clamp(0.0, 100.0);
protected override double ConstrainedThird => L.Clamp(0.0, 100.0);
internal override bool IsGreyscale => S <= 0.0 || L is <= 0.0 or >= 100.0;
public Hsluv(double h, double s, double l) : this(h, s, l, ColourHeritage.None)
{
}
internal Hsluv(double h, double s, double l, ColourHeritage heritage) : base(h, s, l, heritage)
{
}
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
protected override string SecondString => $"{S:F1}%";
protected override string ThirdString => $"{L:F1}%";
public override string ToString() => base.ToString();
/*
* HSLUV is a transform of LCHUV
* Forward: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L363
* Reverse: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L346
*/
internal static Hsluv FromLchuv(Lchuv lchuv)
{
var (lchLightness, chroma, hue) = lchuv.ConstrainedTriplet;
double saturation;
double lightness;
switch (lchLightness)
{
case > 99.9999999:
saturation = 0.0;
lightness = 100.0;
break;
case < 0.00000001:
saturation = 0.0;
lightness = 0.0;
break;
default:
{
var maxChroma = BoundingLines.CalculateMaxChroma(lchLightness, hue);
saturation = chroma / maxChroma * 100;
lightness = lchLightness;
break;
}
}
return new Hsluv(hue, saturation, lightness, ColourHeritage.From(lchuv));
}
internal static Lchuv ToLchuv(Hsluv hsluv)
{
var hue = hsluv.ConstrainedH;
var saturation = hsluv.S;
var hslLightness = hsluv.L;
double lightness;
double chroma;
switch (hslLightness)
{
case > 99.9999999:
lightness = 100.0;
chroma = 0.0;
break;
case < 0.00000001:
lightness = 0.0;
chroma = 0.0;
break;
default:
{
var maxChroma = BoundingLines.CalculateMaxChroma(hslLightness, hue);
chroma = maxChroma / 100 * saturation;
lightness = hslLightness;
break;
}
}
return new Lchuv(lightness, chroma, hue, ColourHeritage.From(hsluv));
}
}
}

65
Unicolour/Hwb.cs Normal file
View File

@@ -0,0 +1,65 @@
namespace Wacton.Unicolour
{
public record Hwb : ColourRepresentation
{
protected override int? HueIndex => 0;
public double H => First;
public double W => Second;
public double B => Third;
public double ConstrainedH => ConstrainedFirst;
public double ConstrainedW => ConstrainedSecond;
public double ConstrainedB => ConstrainedThird;
protected override double ConstrainedFirst => H.Modulo(360.0);
protected override double ConstrainedSecond => W.Clamp(0.0, 1.0);
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
internal override bool IsGreyscale => ConstrainedW + ConstrainedB >= 1.0;
public Hwb(double h, double w, double b) : this(h, w, b, ColourHeritage.None)
{
}
internal Hwb(double h, double w, double b, ColourHeritage heritage) : base(h, w, b, heritage)
{
}
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
protected override string SecondString => $"{W * 100:F1}%";
protected override string ThirdString => $"{B * 100:F1}%";
public override string ToString() => base.ToString();
/*
* HWB is a transform of HSB (in terms of Unicolour implementation)
* Forward: https://en.wikipedia.org/wiki/HWB_color_model#Conversion
* Reverse: https://en.wikipedia.org/wiki/HWB_color_model#Conversion
*/
internal static Hwb FromHsb(Hsb hsb)
{
var (hue, s, b) = hsb.ConstrainedTriplet;
var whiteness = (1 - s) * b;
var blackness = 1 - b;
return new Hwb(hue, whiteness, blackness, ColourHeritage.From(hsb));
}
internal static Hsb ToHsb(Hwb hwb)
{
var (hue, w, b) = hwb.ConstrainedTriplet;
double brightness;
double saturation;
if (hwb.IsGreyscale)
{
brightness = w / (w + b);
saturation = 0;
}
else
{
brightness = 1 - b;
saturation = brightness == 0.0 ? 0 : 1 - w / brightness;
}
return new Hsb(hue, saturation, brightness, ColourHeritage.From(hwb));
}
}
}

76
Unicolour/Ictcp.cs Normal file
View File

@@ -0,0 +1,76 @@
namespace Wacton.Unicolour
{
public record Ictcp : ColourRepresentation
{
protected override int? HueIndex => null;
public double I => First;
public double Ct => Second;
public double Cp => Third;
// no clear lightness upper-bound
internal override bool IsGreyscale => I <= 0.0 || (Ct.Equals(0.0) && Cp.Equals(0.0));
public Ictcp(double i, double ct, double cp) : this(i, ct, cp, ColourHeritage.None)
{
}
internal Ictcp(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
triplet.Third, heritage)
{
}
internal Ictcp(double i, double ct, double cp, ColourHeritage heritage) : base(i, ct, cp, heritage)
{
}
protected override string FirstString => $"{I:F2}";
protected override string SecondString => $"{Ct:+0.00;-0.00;0.00}";
protected override string ThirdString => $"{Cp:+0.00;-0.00;0.00}";
public override string ToString() => base.ToString();
/*
* ICTCP is a transform of XYZ
* Forward: https://professional.dolby.com/siteassets/pdfs/dolby-vision-measuring-perceptual-color-volume-v7.1.pdf
* Reverse: not specified in the above paper; implementation unit tested to confirm roundtrip conversion
* -------
* currently only support PQ transfer function, not HLG (https://en.wikipedia.org/wiki/Hybrid_log%E2%80%93gamma)
*/
private static readonly Matrix M1 = new(new[,]
{
{ 0.3593, 0.6976, -0.0359 },
{ -0.1921, 1.1005, 0.0754 },
{ 0.0071, 0.0748, 0.8433 }
});
private static readonly Matrix M2 = new Matrix(new double[,]
{
{ 2048, 2048, 0 },
{ 6610, -13613, 7003 },
{ 17933, -17390, -543 }
}).Scale(1 / 4096.0);
internal static Ictcp FromXyz(Xyz xyz, double ictcpScalar, XyzConfiguration xyzConfig)
{
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
var d65ScaledMatrix = d65Matrix.Scale(ictcpScalar);
var lmsMatrix = M1.Multiply(d65ScaledMatrix);
var lmsPrimeMatrix = lmsMatrix.Select(Pq.Smpte.InverseEotf);
var ictcpMatrix = M2.Multiply(lmsPrimeMatrix);
return new Ictcp(ictcpMatrix.ToTriplet(), ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Ictcp ictcp, double ictcpScalar, XyzConfiguration xyzConfig)
{
var ictcpMatrix = Matrix.FromTriplet(ictcp.Triplet);
var lmsPrimeMatrix = M2.Inverse().Multiply(ictcpMatrix);
var lmsMatrix = lmsPrimeMatrix.Select(Pq.Smpte.Eotf);
var d65ScaledMatrix = lmsMatrix.Scale(1 / ictcpScalar);
var d65Matrix = M1.Inverse().Multiply(d65ScaledMatrix);
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(ictcp));
}
}
}

123
Unicolour/Interpolation.cs Normal file
View File

@@ -0,0 +1,123 @@
using System;
namespace Wacton.Unicolour
{
internal static class Interpolation
{
internal static Unicolour Mix(ColourSpace colourSpace, Unicolour startColour, Unicolour endColour,
double distance, bool premultiplyAlpha)
{
GuardConfiguration(startColour, endColour);
var startRepresentation = startColour.GetRepresentation(colourSpace);
var startAlpha = startColour.Alpha;
var endRepresentation = endColour.GetRepresentation(colourSpace);
var endAlpha = endColour.Alpha;
(ColourTriplet start, ColourTriplet end) = GetTripletsToInterpolate(
(startRepresentation, startAlpha),
(endRepresentation, endAlpha),
premultiplyAlpha);
var triplet = InterpolateTriplet(start, end, distance).WithHueModulo();
var alpha = Interpolate(startColour.Alpha.ConstrainedA, endColour.Alpha.ConstrainedA, distance);
if (premultiplyAlpha)
{
triplet = triplet.WithUnpremultipliedAlpha(alpha);
}
var heritage = ColourHeritage.From(startRepresentation, endRepresentation);
var (first, second, third) = triplet;
return new Unicolour(colourSpace, startColour.Config, heritage, first, second, third, alpha);
}
private static (ColourTriplet start, ColourTriplet end) GetTripletsToInterpolate(
(ColourRepresentation representation, Alpha alpha) start,
(ColourRepresentation representation, Alpha alpha) end,
bool premultiplyAlpha)
{
ColourTriplet startTriplet;
ColourTriplet endTriplet;
// these can't give different answers since they use the same colour space
// (except by reflection, in which case an error would be thrown when later trying to read the hue component)
var hasHueComponent = start.representation.HasHueComponent || end.representation.HasHueComponent;
if (hasHueComponent)
{
(startTriplet, endTriplet) = GetTripletsWithHue(start.representation, end.representation);
}
else
{
startTriplet = start.representation.Triplet;
endTriplet = end.representation.Triplet;
}
if (premultiplyAlpha)
{
startTriplet = startTriplet.WithPremultipliedAlpha(start.alpha.ConstrainedA);
endTriplet = endTriplet.WithPremultipliedAlpha(end.alpha.ConstrainedA);
}
return (startTriplet, endTriplet);
}
private static (ColourTriplet start, ColourTriplet end) GetTripletsWithHue(
ColourRepresentation startRepresentation, ColourRepresentation endRepresentation)
{
var startTriplet = startRepresentation.Triplet;
var endTriplet = endRepresentation.Triplet;
(ColourTriplet, ColourTriplet) HueResult(double startHue, double endHue) => (
startTriplet.WithHueOverride(startHue),
endTriplet.WithHueOverride(endHue));
var startHasHue = startRepresentation.UseAsHued;
var endHasHue = endRepresentation.UseAsHued;
var ignoreHue = !startHasHue && !endHasHue;
// don't change hue if one colour is greyscale (e.g. black n/a° to green 120° should always stay at hue 120°)
var startHue = ignoreHue || startHasHue ? startTriplet.HueValue() : endTriplet.HueValue();
var endHue = ignoreHue || endHasHue ? endTriplet.HueValue() : startTriplet.HueValue();
if (startHue > endHue)
{
var endViaZero = endHue + 360;
var interpolateViaZero = Math.Abs(startHue - endViaZero) < Math.Abs(startHue - endHue);
return HueResult(startHue, interpolateViaZero ? endViaZero : endHue);
}
if (endHue > startHue)
{
var startViaZero = startHue + 360;
var interpolateViaZero = Math.Abs(endHue - startViaZero) < Math.Abs(endHue - startHue);
return HueResult(interpolateViaZero ? startViaZero : startHue, endHue);
}
return HueResult(startHue, endHue);
}
private static ColourTriplet InterpolateTriplet(ColourTriplet start, ColourTriplet end, double distance)
{
var first = Interpolate(start.First, end.First, distance);
var second = Interpolate(start.Second, end.Second, distance);
var third = Interpolate(start.Third, end.Third, distance);
return new(first, second, third, start.HueIndex);
}
internal static double Interpolate(double startValue, double endValue, double distance)
{
var difference = endValue - startValue;
return startValue + (difference * distance);
}
private static void GuardConfiguration(Unicolour unicolour1, Unicolour unicolour2)
{
if (unicolour1.Config != unicolour2.Config)
{
throw new InvalidOperationException("Can only mix unicolours with the same configuration reference");
}
}
}
}

98
Unicolour/Jzazbz.cs Normal file
View File

@@ -0,0 +1,98 @@
using System;
namespace Wacton.Unicolour
{
public record Jzazbz : ColourRepresentation
{
protected override int? HueIndex => null;
public double J => First;
public double A => Second;
public double B => Third;
// based on the figures from the paper, greyscale behaviour is the same as LAB
// i.e. non-lightness axes are zero
// but no clear lightness upper-bound
internal override bool IsGreyscale => J <= 0.0 || (A.Equals(0.0) && B.Equals(0.0));
public Jzazbz(double j, double a, double b) : this(j, a, b, ColourHeritage.None)
{
}
internal Jzazbz(double j, double a, double b, ColourHeritage heritage) : base(j, a, b, heritage)
{
}
protected override string FirstString => $"{J:F3}";
protected override string SecondString => $"{A:+0.000;-0.000;0.000}";
protected override string ThirdString => $"{B:+0.000;-0.000;0.000}";
public override string ToString() => base.ToString();
/*
* JZAZBZ is a transform of XYZ
* Forward: https://doi.org/10.1364/OE.25.015131
* Reverse: https://doi.org/10.1364/OE.25.015131
* -------
* also useful: https://opticapublishing.figshare.com/articles/software/JzAzBz_m/5016299
*/
// ReSharper disable InconsistentNaming
private const double b = 1.15;
private const double g = 0.66;
private const double d = -0.56;
private const double d0 = 1.6295499532821566e-11;
// ReSharper restore InconsistentNaming
private static readonly Matrix M1 = new(new[,]
{
{ +0.41478972, +0.579999, +0.0146480 },
{ -0.2015100, +1.120649, +0.0531008 },
{ -0.0166008, +0.264800, +0.6684799 }
});
private static readonly Matrix M2 = new(new[,]
{
{ +0.5, +0.5, 0 },
{ +3.524000, -4.066708, +0.542708 },
{ +0.199076, +1.096799, -1.295875 }
});
internal static Jzazbz FromXyz(Xyz xyz, double jzazbzScalar, XyzConfiguration xyzConfig)
{
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
var d65ScaledMatrix = d65Matrix.Scale(jzazbzScalar).Select(x => Math.Max(x, 0));
var (x65, y65, z65) = d65ScaledMatrix.ToTriplet();
var x65Prime = b * x65 - (b - 1) * z65;
var y65Prime = g * y65 - (g - 1) * x65;
var xyz65PrimeMatrix = Matrix.FromTriplet(x65Prime, y65Prime, z65);
var lmsMatrix = M1.Multiply(xyz65PrimeMatrix);
var lmsPrimeMatrix = lmsMatrix.Select(Pq.Jzazbz.InverseEotf);
var izazbzMatrix = M2.Multiply(lmsPrimeMatrix);
var (iz, az, bz) = izazbzMatrix.ToTriplet();
var jz = (1 + d) * iz / (1 + d * iz) - d0;
return new Jzazbz(jz, az, bz, ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Jzazbz jzazbz, double jzazbzScalar, XyzConfiguration xyzConfig)
{
var (jz, az, bz) = jzazbz.Triplet;
var iz = (jz + d0) / (1 + d - d * (jz + d0));
var izazbzMatrix = Matrix.FromTriplet(iz, az, bz);
var lmsPrimeMatrix = M2.Inverse().Multiply(izazbzMatrix);
var lmsMatrix = lmsPrimeMatrix.Select(Pq.Jzazbz.Eotf);
var xyz65PrimeMatrix = M1.Inverse().Multiply(lmsMatrix);
var (x65Prime, y65Prime, z65Prime) = xyz65PrimeMatrix.ToTriplet();
var x65 = (x65Prime + (b - 1) * z65Prime) / b;
var y65 = (y65Prime + (g - 1) * x65) / g;
var z65 = z65Prime;
var d65ScaledMatrix = Matrix.FromTriplet(x65, y65, z65);
var d65Matrix = d65ScaledMatrix.Scale(1 / jzazbzScalar);
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(jzazbz));
}
}
}

50
Unicolour/Jzczhz.cs Normal file
View File

@@ -0,0 +1,50 @@
namespace Wacton.Unicolour
{
using static Utils;
public record Jzczhz : ColourRepresentation
{
protected override int? HueIndex => 2;
public double J => First;
public double C => Second;
public double H => Third;
public double ConstrainedH => ConstrainedThird;
protected override double ConstrainedThird => H.Modulo(360.0);
// no clear lightness upper-bound
// (paper says lightness J is 0 - 1 but seems like it's a scaling of their plot of Rec.2020 gamut - in my tests maxes out after ~0.17)
internal override bool IsGreyscale => J <= 0.0 || C <= 0.0;
public Jzczhz(double j, double c, double h) : this(j, c, h, ColourHeritage.None)
{
}
internal Jzczhz(double j, double c, double h, ColourHeritage heritage) : base(j, c, h, heritage)
{
}
protected override string FirstString => $"{J:F3}";
protected override string SecondString => $"{C:F3}";
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
public override string ToString() => base.ToString();
/*
* JZCZHZ is a transform of JZAZBZ
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
*/
internal static Jzczhz FromJzazbz(Jzazbz jzazbz)
{
var (jz, cz, hz) = ToLchTriplet(jzazbz.J, jzazbz.A, jzazbz.B);
return new Jzczhz(jz, cz, hz, ColourHeritage.From(jzazbz));
}
internal static Jzazbz ToJzazbz(Jzczhz jzczhz)
{
var (jz, az, bz) = FromLchTriplet(jzczhz.ConstrainedTriplet);
return new Jzazbz(jz, az, bz, ColourHeritage.From(jzczhz));
}
}
}

21
Unicolour/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2023 William Acton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
Unicolour/Lab.cs Normal file
View File

@@ -0,0 +1,67 @@
using System;
namespace Wacton.Unicolour
{
using static Utils;
public record Lab : ColourRepresentation
{
protected override int? HueIndex => null;
public double L => First;
public double A => Second;
public double B => Third;
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || (A.Equals(0.0) && B.Equals(0.0));
public Lab(double l, double a, double b) : this(l, a, b, ColourHeritage.None)
{
}
internal Lab(double l, double a, double b, ColourHeritage heritage) : base(l, a, b, heritage)
{
}
protected override string FirstString => $"{L:F2}";
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
public override string ToString() => base.ToString();
/*
* LAB is a transform of XYZ
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIELAB_to_CIEXYZ
*/
// ReSharper disable InconsistentNaming
private const double delta = 6.0 / 29.0;
// ReSharper restore InconsistentNaming
internal static Lab FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
{
var (x, y, z) = xyz.Triplet;
var referenceWhite = xyzConfig.WhitePoint;
var xRatio = x * 100 / referenceWhite.X;
var yRatio = y * 100 / referenceWhite.Y;
var zRatio = z * 100 / referenceWhite.Z;
double F(double t) =>
t > Math.Pow(delta, 3) ? CubeRoot(t) : t * (1 / 3.0) * Math.Pow(delta, -2) + 4.0 / 29.0;
var l = 116 * F(yRatio) - 16;
var a = 500 * (F(xRatio) - F(yRatio));
var b = 200 * (F(yRatio) - F(zRatio));
return new Lab(l, a, b, ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Lab lab, XyzConfiguration xyzConfig)
{
var (l, a, b) = lab.Triplet;
var referenceWhite = xyzConfig.WhitePoint;
double F(double t) => t > delta ? Math.Pow(t, 3.0) : 3 * Math.Pow(delta, 2) * (t - 4.0 / 29.0);
var x = referenceWhite.X / 100.0 * F((l + 16) / 116.0 + a / 500.0);
var y = referenceWhite.Y / 100.0 * F((l + 16) / 116.0);
var z = referenceWhite.Z / 100.0 * F((l + 16) / 116.0 - b / 200.0);
return new Xyz(x, y, z, ColourHeritage.From(lab));
}
}
}

47
Unicolour/Lchab.cs Normal file
View File

@@ -0,0 +1,47 @@
namespace Wacton.Unicolour
{
using static Utils;
public record Lchab : ColourRepresentation
{
protected override int? HueIndex => 2;
public double L => First;
public double C => Second;
public double H => Third;
public double ConstrainedH => ConstrainedThird;
protected override double ConstrainedThird => H.Modulo(360.0);
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || C <= 0.0;
public Lchab(double l, double c, double h) : this(l, c, h, ColourHeritage.None)
{
}
internal Lchab(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage)
{
}
protected override string FirstString => $"{L:F2}";
protected override string SecondString => $"{C:F2}";
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
public override string ToString() => base.ToString();
/*
* LCHAB is a transform of LAB
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
*/
internal static Lchab FromLab(Lab lab)
{
var (l, c, h) = ToLchTriplet(lab.L, lab.A, lab.B);
return new Lchab(l, c, h, ColourHeritage.From(lab));
}
internal static Lab ToLab(Lchab lchab)
{
var (l, a, b) = FromLchTriplet(lchab.ConstrainedTriplet);
return new Lab(l, a, b, ColourHeritage.From(lchab));
}
}
}

47
Unicolour/Lchuv.cs Normal file
View File

@@ -0,0 +1,47 @@
namespace Wacton.Unicolour
{
using static Utils;
public record Lchuv : ColourRepresentation
{
protected override int? HueIndex => 2;
public double L => First;
public double C => Second;
public double H => Third;
public double ConstrainedH => ConstrainedThird;
protected override double ConstrainedThird => H.Modulo(360.0);
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || C <= 0.0;
public Lchuv(double l, double c, double h) : this(l, c, h, ColourHeritage.None)
{
}
internal Lchuv(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage)
{
}
protected override string FirstString => $"{L:F2}";
protected override string SecondString => $"{C:F2}";
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
public override string ToString() => base.ToString();
/*
* LCHUV is a transform of LUV
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
*/
internal static Lchuv FromLuv(Luv luv)
{
var (l, c, h) = ToLchTriplet(luv.L, luv.U, luv.V);
return new Lchuv(l, c, h, ColourHeritage.From(luv));
}
internal static Luv ToLuv(Lchuv lchuv)
{
var (l, u, v) = FromLchTriplet(lchuv.ConstrainedTriplet);
return new Luv(l, u, v, ColourHeritage.From(lchuv));
}
}
}

76
Unicolour/Luv.cs Normal file
View File

@@ -0,0 +1,76 @@
using System;
namespace Wacton.Unicolour
{
using static Utils;
public record Luv : ColourRepresentation
{
protected override int? HueIndex => null;
public double L => First;
public double U => Second;
public double V => Third;
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || (U.Equals(0.0) && V.Equals(0.0));
public Luv(double l, double u, double v) : this(l, u, v, ColourHeritage.None)
{
}
internal Luv(double l, double u, double v, ColourHeritage heritage) : base(l, u, v, heritage)
{
}
protected override string FirstString => $"{L:F2}";
protected override string SecondString => $"{U:+0.00;-0.00;0.00}";
protected override string ThirdString => $"{V:+0.00;-0.00;0.00}";
public override string ToString() => base.ToString();
/*
* LUV is a transform of XYZ
* Forward: https://en.wikipedia.org/wiki/CIELUV#The_forward_transformation
* Reverse: https://en.wikipedia.org/wiki/CIELUV#The_reverse_transformation
*/
internal static Luv FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
{
var (x, y, z) = xyz.Triplet;
var (xRef, yRef, zRef) = xyzConfig.WhitePoint;
double U(double xu, double yu, double zu) => 4 * xu / (xu + 15 * yu + 3 * zu);
double V(double xv, double yv, double zv) => 9 * yv / (xv + 15 * yv + 3 * zv);
var uPrime = U(x * 100, y * 100, z * 100);
var uPrimeRef = U(xRef, yRef, zRef);
var vPrime = V(x * 100, y * 100, z * 100);
var vPrimeRef = V(xRef, yRef, zRef);
var yRatio = y * 100 / yRef;
var l = yRatio > Math.Pow(6.0 / 29.0, 3) ? 116 * CubeRoot(yRatio) - 16 : Math.Pow(29 / 3.0, 3) * yRatio;
var u = 13 * l * (uPrime - uPrimeRef);
var v = 13 * l * (vPrime - vPrimeRef);
double ZeroNaN(double value) => double.IsNaN(value) ? 0.0 : value;
return new Luv(ZeroNaN(l), ZeroNaN(u), ZeroNaN(v), ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Luv luv, XyzConfiguration xyzConfig)
{
var (l, u, v) = luv.Triplet;
double U(double x, double y, double z) => 4 * x / (x + 15 * y + 3 * z);
double V(double x, double y, double z) => 9 * y / (x + 15 * y + 3 * z);
var (xRef, yRef, zRef) = xyzConfig.WhitePoint;
var uPrimeRef = U(xRef, yRef, zRef);
var uPrime = u / (13 * l) + uPrimeRef;
var vPrimeRef = V(xRef, yRef, zRef);
var vPrime = v / (13 * l) + vPrimeRef;
var y = (l > 8 ? yRef * Math.Pow((l + 16) / 116.0, 3) : yRef * l * Math.Pow(3 / 29.0, 3)) / 100.0;
var x = y * ((9 * uPrime) / (4 * vPrime));
var z = y * ((12 - 3 * uPrime - 20 * vPrime) / (4 * vPrime));
double ZeroNaN(double value) => double.IsNaN(value) || double.IsInfinity(value) ? 0.0 : value;
return new Xyz(ZeroNaN(x), ZeroNaN(y), ZeroNaN(z), ColourHeritage.From(luv));
}
}
}

126
Unicolour/Matrix.cs Normal file
View File

@@ -0,0 +1,126 @@
using System;
namespace Wacton.Unicolour
{
internal class Matrix
{
internal double[,] Data { get; }
internal double this[int row, int col] => Data[row, col];
private int Rows => Data.GetLength(0);
private int Cols => Data.GetLength(1);
internal Matrix(double[,] data)
{
Data = data;
}
internal Matrix Multiply(Matrix other)
{
if (other.Rows != Cols)
{
throw new ArgumentException(
$"Cannot multiply {this} matrix by {other} matrix, incompatible dimensions");
}
var dimension = Cols;
var rows = Rows;
var cols = other.Cols;
var result = new double[rows, cols];
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
result[row, col] = 0;
for (var i = 0; i < dimension; i++)
{
result[row, col] += this[row, i] * other[i, col];
}
}
}
return new Matrix(result);
}
internal Matrix Inverse()
{
if (Rows != 3 || Cols != 3)
{
throw new InvalidOperationException("Only inverse of 3x3 matrix is supported");
}
var a = this[0, 0];
var b = this[0, 1];
var c = this[0, 2];
var d = this[1, 0];
var e = this[1, 1];
var f = this[1, 2];
var g = this[2, 0];
var h = this[2, 1];
var i = this[2, 2];
var determinant = a * e * i + b * f * g + c * d * h - c * e * g - a * f * h - b * d * i;
var adjugate = new[,]
{
{ e * i - f * h, h * c - i * b, b * f - c * e },
{ g * f - d * i, a * i - g * c, d * c - a * f },
{ d * h - g * e, g * b - a * h, a * e - d * b }
};
var inverse = new double[3, 3];
for (var row = 0; row < 3; row++)
{
for (var col = 0; col < 3; col++)
{
inverse[row, col] = determinant == 0
? double.NaN
: adjugate[row, col] / determinant;
}
}
return new Matrix(inverse);
}
internal Matrix Scale(double scalar) => Select(x => x * scalar);
internal Matrix Select(Func<double, double> operation)
{
var result = new double[Rows, Cols];
for (var row = 0; row < Rows; row++)
{
for (var col = 0; col < Cols; col++)
{
result[row, col] = operation(this[row, col]);
}
}
return new Matrix(result);
}
internal ColourTriplet ToTriplet()
{
if (Rows != 3 || Cols != 1)
{
throw new InvalidOperationException("Can only create triplet from 3x1 matrix");
}
return new ColourTriplet(Data[0, 0], Data[1, 0], Data[2, 0]);
}
internal static Matrix FromTriplet(ColourTriplet triplet) =>
FromTriplet(triplet.First, triplet.Second, triplet.Third);
internal static Matrix FromTriplet(double first, double second, double third)
{
return new Matrix(new[,]
{
{ first },
{ second },
{ third }
});
}
public override string ToString() => $"{Rows}x{Cols}";
}
}

74
Unicolour/Oklab.cs Normal file
View File

@@ -0,0 +1,74 @@
using System;
namespace Wacton.Unicolour
{
using static Utils;
public record Oklab : ColourRepresentation
{
protected override int? HueIndex => null;
public double L => First;
public double A => Second;
public double B => Third;
internal override bool IsGreyscale => L is <= 0.0 or >= 1.0 || (A.Equals(0.0) && B.Equals(0.0));
public Oklab(double l, double a, double b) : this(l, a, b, ColourHeritage.None)
{
}
internal Oklab(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
triplet.Third, heritage)
{
}
internal Oklab(double l, double a, double b, ColourHeritage heritage) : base(l, a, b, heritage)
{
}
protected override string FirstString => $"{L:F2}";
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
public override string ToString() => base.ToString();
/*
* OKLAB is a transform of XYZ
* Forward: https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
* Reverse: https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
*/
private static readonly Matrix M1 = new(new[,]
{
{ +0.8189330101, +0.3618667424, -0.1288597137 },
{ +0.0329845436, +0.9293118715, +0.0361456387 },
{ +0.0482003018, +0.2643662691, +0.6338517070 }
});
private static readonly Matrix M2 = new(new[,]
{
{ +0.2104542553, +0.7936177850, -0.0040720468 },
{ +1.9779984951, -2.4285922050, +0.4505937099 },
{ +0.0259040371, +0.7827717662, -0.8086757660 }
});
internal static Oklab FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
{
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
var lmsMatrix = M1.Multiply(d65Matrix);
var lmsNonLinearMatrix = lmsMatrix.Select(CubeRoot);
var labMatrix = M2.Multiply(lmsNonLinearMatrix);
return new Oklab(labMatrix.ToTriplet(), ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Oklab oklab, XyzConfiguration xyzConfig)
{
var labMatrix = Matrix.FromTriplet(oklab.Triplet);
var lmsNonLinearMatrix = M2.Inverse().Multiply(labMatrix);
var lmsMatrix = lmsNonLinearMatrix.Select(x => Math.Pow(x, 3));
var d65Matrix = M1.Inverse().Multiply(lmsMatrix);
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(oklab));
}
}
}

47
Unicolour/Oklch.cs Normal file
View File

@@ -0,0 +1,47 @@
namespace Wacton.Unicolour
{
using static Utils;
public record Oklch : ColourRepresentation
{
protected override int? HueIndex => 2;
public double L => First;
public double C => Second;
public double H => Third;
public double ConstrainedH => ConstrainedThird;
protected override double ConstrainedThird => H.Modulo(360.0);
internal override bool IsGreyscale => L is <= 0.0 or >= 1.0 || C <= 0.0;
public Oklch(double l, double c, double h) : this(l, c, h, ColourHeritage.None)
{
}
internal Oklch(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage)
{
}
protected override string FirstString => $"{L:F2}";
protected override string SecondString => $"{C:F2}";
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
public override string ToString() => base.ToString();
/*
* OKLCH is a transform of OKLAB
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
*/
internal static Oklch FromOklab(Oklab oklab)
{
var (l, c, h) = ToLchTriplet(oklab.L, oklab.A, oklab.B);
return new Oklch(l, c, h, ColourHeritage.From(oklab));
}
internal static Oklab ToOklab(Oklch oklch)
{
var (l, a, b) = FromLchTriplet(oklch.ConstrainedTriplet);
return new Oklab(l, a, b, ColourHeritage.From(oklch));
}
}
}

59
Unicolour/Pq.cs Normal file
View File

@@ -0,0 +1,59 @@
using System;
namespace Wacton.Unicolour
{
internal static class Pq
{
internal static class Smpte
{
private const double M1 = 2610 / 16384.0;
private const double M2 = 2523 / 4096.0 * 128;
private const double C1 = 3424 / 4096.0;
private const double C2 = 2413 / 4096.0 * 32;
private const double C3 = 2392 / 4096.0 * 32;
internal static double Eotf(double ePrime)
{
var rootEPrime = Math.Pow(ePrime, 1 / M2);
var top = Math.Max(rootEPrime - C1, 0);
var bottom = C2 - C3 * rootEPrime;
return 10000 * Math.Pow(top / bottom, 1 / M1);
}
internal static double InverseEotf(double f)
{
var y = f / 10000.0;
var powerY = Math.Pow(y, M1);
var top = C1 + C2 * powerY;
var bottom = 1 + C3 * powerY;
return Math.Pow(top / bottom, M2);
}
}
internal static class Jzazbz
{
private static readonly double C1 = 3424 / Math.Pow(2, 12);
private static readonly double C2 = 2413 / Math.Pow(2, 7);
private static readonly double C3 = 2392 / Math.Pow(2, 7);
private static readonly double N = 2610 / Math.Pow(2, 14);
private static readonly double P = 1.7 * 2523 / Math.Pow(2, 5);
internal static double Eotf(double value)
{
var rootP = Math.Pow(value, 1 / P);
var top = C1 - rootP;
var bottom = C3 * rootP - C2;
return 10000 * Math.Pow(top / bottom, 1 / N);
}
internal static double InverseEotf(double value)
{
var powerN = Math.Pow(value / 10000.0, N);
var top = C1 + C2 * powerN;
var bottom = 1 + C3 * powerN;
return Math.Pow(top / bottom, P);
}
}
}
}

456
Unicolour/README.md Normal file
View File

@@ -0,0 +1,456 @@
# <img src="https://gitlab.com/Wacton/Unicolour/-/raw/main/Unicolour/Resources/Unicolour.png" width="32" height="32"> Unicolour
[![pipeline status](https://gitlab.com/Wacton/Unicolour/badges/main/pipeline.svg)](https://gitlab.com/Wacton/Unicolour/-/commits/main)
[![coverage report](https://gitlab.com/Wacton/Unicolour/badges/main/coverage.svg)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
[![tests passed](https://badgen.net/https/waacton.npkn.net/gitlab-test-badge/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
[![NuGet](https://badgen.net/nuget/v/Wacton.Unicolour?icon)](https://www.nuget.org/packages/Wacton.Unicolour/)
Unicolour is a .NET library written in C# for working with colour:
- Colour space conversion
- Colour mixing / colour interpolation
- Colour comparison
- Colour information
- Colour gamut mapping
Targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications.
**Contents**
1. 🧭 [Overview](#-overview)
2. ⚡ [Quickstart](#-quickstart)
3. 🔦 [Features](#-features)
4. 🌈 [How to use](#-how-to-use)
5. ✨ [Examples](#-examples)
6. 💡 [Configuration](#-configuration)
7. 🔮 [Datasets](#-datasets)
8. 🦺 [Work in progress](#-work-in-progress)
## 🧭 Overview
A `Unicolour` encapsulates a single colour and its representation across different colour spaces.
It supports:
- RGB
- Linear RGB
- HSB/HSV
- HSL
- HWB
- CIEXYZ
- CIExyY
- CIELAB
- CIELCh<sub>ab</sub>
- CIELUV
- CIELCh<sub>uv</sub>
- HSLuv
- HPLuv
- IC<sub>T</sub>C<sub>P</sub>
- J<sub>z</sub>a<sub>z</sub>b<sub>z</sub>
- J<sub>z</sub>C<sub>z</sub>h<sub>z</sub>
- Oklab
- Oklch
- CIECAM02
- CAM16
- HCT
<details>
<summary>Diagram of colour space relationships</summary>
```mermaid
%%{
init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#4C566A",
"primaryTextColor": "#ECEFF4",
"primaryBorderColor": "#2E3440",
"lineColor": "#8FBCBB",
"secondaryColor": "#404046",
"tertiaryColor": "#404046"
}
}
}%%
flowchart TD
XYY(xyY)
RGBLIN(Linear RGB)
RGB(RGB)
HSB(HSB)
HSL(HSL)
HWB(HWB)
XYZ(XYZ)
LAB(LAB)
LCHAB(LCHab)
LUV(LUV)
LCHUV(LCHuv)
HSLUV(HSLuv)
HPLUV(HPLuv)
ICTCP(ICtCp)
JZAZBZ(JzAzBz)
JZCZHZ(JzCzHz)
OKLAB(Oklab)
OKLCH(Oklch)
CAM02(CAM02)
CAM02UCS(CAM02-UCS)
CAM16(CAM16)
CAM16UCS(CAM16-UCS)
HCT(HCT)
XYZ --> XYY
XYZ --> RGBLIN
RGBLIN --> RGB
RGB --> HSB
HSB --> HSL
HSB --> HWB
XYZ --> LAB
LAB --> LCHAB
XYZ --> LUV
LUV --> LCHUV
LCHUV --> HSLUV
LCHUV --> HPLUV
XYZ --> ICTCP
XYZ --> JZAZBZ
JZAZBZ --> JZCZHZ
XYZ --> OKLAB
OKLAB --> OKLCH
XYZ --> CAM02
CAM02 -.-> CAM02UCS
XYZ --> CAM16
CAM16 -.-> CAM16UCS
XYZ --> HCT
```
This diagram summarises how colour space conversions are implemented in Unicolour.
Arrows indicate forward transformations from one space to another.
For each forward transformation there is a corresponding reverse transformation.
XYZ is considered the root colour space.
</details>
This library was initially written for personal projects since existing libraries had complex APIs or missing features.
The goal of this library is to be accurate, intuitive, and easy to use.
Although performance is not a priority, conversions are only calculated once; when first evaluated (either on access or as part of an intermediate conversion step) the result is stored for future use.
It is also [extensively tested](Unicolour.Tests), including verification of roundtrip conversions, validation using known colour values, and 100% line coverage and branch coverage.
## ⚡ Quickstart
| Colour&nbsp;space | Create | Get |
|-----------------------------------------|---------------------------------|----------------|
| RGB&nbsp;(Hex) | `new(hex)` | `.Hex` |
| RGB&nbsp;(0255) | `new(ColourSpace.Rgb255, ⋯)` | `.Rgb.Byte255` |
| RGB | `new(ColourSpace.Rgb, ⋯)` | `.Rgb` |
| Linear&nbsp;RGB | `new(ColourSpace.RgbLinear, ⋯)` | `.RgbLinear` |
| HSB/HSV | `new(ColourSpace.Hsb, ⋯)` | `.Hsb` |
| HSL | `new(ColourSpace.Hsl, ⋯)` | `.Hsl` |
| HWB | `new(ColourSpace.Hwb, ⋯)` | `.Hwb` |
| CIEXYZ | `new(ColourSpace.Xyz, ⋯)` | `.Xyz` |
| CIExyY | `new(ColourSpace.Xyy, ⋯)` | `.Xyy` |
| CIELAB | `new(ColourSpace.Lab, ⋯)` | `.Lab` |
| CIELCh<sub>ab</sub> | `new(ColourSpace.Lchab, ⋯)` | `.Lchab` |
| CIELUV | `new(ColourSpace.Luv, ⋯)` | `.Luv` |
| CIELCh<sub>uv</sub> | `new(ColourSpace.Lchuv, ⋯)` | `.Lchuv` |
| HSLuv | `new(ColourSpace.Hsluv, ⋯)` | `.Hsluv` |
| HPLuv | `new(ColourSpace.Hpluv, ⋯)` | `.Hpluv` |
| IC<sub>T</sub>C<sub>P</sub> | `new(ColourSpace.Ictcp, ⋯)` | `.Ictcp` |
| J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> | `new(ColourSpace.Jzazbz, ⋯)` | `.Jzazbz` |
| J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> | `new(ColourSpace.Jzczhz, ⋯)` | `.Jzczhz` |
| Oklab | `new(ColourSpace.Oklab, ⋯)` | `.Oklab` |
| Oklch | `new(ColourSpace.Oklch, ⋯)` | `.Oklch` |
| CIECAM02 | `new(ColourSpace.Cam02, ⋯)` | `.Cam02` |
| CAM16 | `new(ColourSpace.Cam16, ⋯)` | `.Cam16` |
| HCT | `new(ColourSpace.Hct, ⋯)` | `.Hct` |
## 🔦 Features
A `Unicolour` can be instantiated using any of the supported colour spaces.
Conversion to other colour spaces is handled by Unicolour, and the results can be accessed through properties.
Two colours can be mixed / interpolated through any colour space, with or without premultiplied alpha.
Colour difference / colour distance can be calculated using various delta E metrics:
- ΔE<sub>76</sub> (CIE76)
- ΔE<sub>94</sub> (CIE94)
- ΔE<sub>00</sub> (CIEDE2000)
- ΔE<sub>CMC</sub> (CMC l:c)
- ΔE<sub>ITP</sub>
- ΔE<sub>z</sub>
- ΔE<sub>HyAB</sub>
- ΔE<sub>OK</sub>
- ΔE<sub>CAM02</sub>
- ΔE<sub>CAM16</sub>
The following colour information is available:
- Hex representation
- Relative luminance
- Temperature (CCT and Duv)
Simulation of colour vision deficiency (CVD) / colour blindness is supported for:
- Protanopia (no red perception)
- Deuteranopia (no green perception)
- Tritanopia (no blue perception)
- Achromatopsia (no colour perception)
If a colour is outwith the display gamut, the closest in-gamut colour can be obtained using gamut mapping.
The algorithm implemented in Unicolour conforms to CSS specifications.
Unicolour uses sRGB as the default RGB model and standard illuminant D65 (2° observer) as the default white point of the XYZ colour space.
These [can be overridden](#-configuration) using the `Configuration` parameter.
## 🌈 How to use
1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour/)
```
dotnet add package Wacton.Unicolour
```
2. Import the package
```c#
using Wacton.Unicolour;
```
3. Create a `Unicolour`
```c#
var unicolour = new Unicolour("#FF1493");
var unicolour = new Unicolour(ColourSpace.Rgb255, 255, 20, 147);
var unicolour = new Unicolour(ColourSpace.Rgb, 1.00, 0.08, 0.58);
var unicolour = new Unicolour(ColourSpace.RgbLinear, 1.00, 0.01, 0.29);
var unicolour = new Unicolour(ColourSpace.Hsb, 327.6, 0.922, 1.000);
var unicolour = new Unicolour(ColourSpace.Hsl, 327.6, 1.000, 0.539);
var unicolour = new Unicolour(ColourSpace.Hwb, 327.6, 0.078, 0.000);
var unicolour = new Unicolour(ColourSpace.Xyz, 0.4676, 0.2387, 0.2974);
var unicolour = new Unicolour(ColourSpace.Xyy, 0.4658, 0.2378, 0.2387);
var unicolour = new Unicolour(ColourSpace.Lab, 55.96, 84.54, -5.7);
var unicolour = new Unicolour(ColourSpace.Lchab, 55.96, 84.73, 356.1);
var unicolour = new Unicolour(ColourSpace.Luv, 55.96, 131.47, -24.35);
var unicolour = new Unicolour(ColourSpace.Lchuv, 55.96, 133.71, 349.5);
var unicolour = new Unicolour(ColourSpace.Hsluv, 349.5, 100.0, 56.0);
var unicolour = new Unicolour(ColourSpace.Hpluv, 349.5, 303.2, 56.0);
var unicolour = new Unicolour(ColourSpace.Ictcp, 0.38, 0.12, 0.19);
var unicolour = new Unicolour(ColourSpace.Jzazbz, 0.106, 0.107, 0.005);
var unicolour = new Unicolour(ColourSpace.Jzczhz, 0.106, 0.107, 2.6);
var unicolour = new Unicolour(ColourSpace.Oklab, 0.65, 0.26, -0.01);
var unicolour = new Unicolour(ColourSpace.Oklch, 0.65, 0.26, 356.9);
var unicolour = new Unicolour(ColourSpace.Cam02, 62.86, 40.81, -1.18);
var unicolour = new Unicolour(ColourSpace.Cam16, 62.47, 42.60, -1.36);
var unicolour = new Unicolour(ColourSpace.Hct, 358.2, 100.38, 55.96);
```
4. Get colour space representations
```c#
var rgb = unicolour.Rgb;
var rgbLinear = unicolour.RgbLinear;
var hsb = unicolour.Hsb;
var hsl = unicolour.Hsl;
var hwb = unicolour.Hwb;
var xyz = unicolour.Xyz;
var xyy = unicolour.Xyy;
var lab = unicolour.Lab;
var lchab = unicolour.Lchab;
var luv = unicolour.Luv;
var lchuv = unicolour.Lchuv;
var hsluv = unicolour.Hsluv;
var hpluv = unicolour.Hpluv;
var ictcp = unicolour.Ictcp;
var jzazbz = unicolour.Jzazbz;
var jzczhz = unicolour.Jzczhz;
var oklab = unicolour.Oklab;
var oklch = unicolour.Oklch;
var cam02 = unicolour.Cam02;
var cam16 = unicolour.Cam16;
var hct = unicolour.Hct;
```
5. Get colour properties
```c#
var hex = unicolour.Hex;
var relativeLuminance = unicolour.RelativeLuminance;
var temperature = unicolour.Temperature;
var inGamut = unicolour.IsInDisplayGamut;
```
6. Mix colours (interpolate between them)
```c#
var mixed = unicolour1.Mix(ColourSpace.Rgb, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.RgbLinear, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Hsb, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Hsl, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Hwb, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Xyz, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Xyy, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Lab, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Lchab, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Luv, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Lchuv, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Hsluv, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Hpluv, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Ictcp, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Jzazbz, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Jzczhz, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Oklab, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Oklch, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Cam02, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Cam16, unicolour2);
var mixed = unicolour1.Mix(ColourSpace.Hct, unicolour2);
```
7. Compare colours
```c#
var contrast = unicolour1.Contrast(unicolour2);
var difference = unicolour1.Difference(DeltaE.Cie76, unicolour2);
var difference = unicolour1.Difference(DeltaE.Cie94, unicolour2);
var difference = unicolour1.Difference(DeltaE.Cie94Textiles, unicolour2);
var difference = unicolour1.Difference(DeltaE.Ciede2000, unicolour2);
var difference = unicolour1.Difference(DeltaE.CmcAcceptability, unicolour2);
var difference = unicolour1.Difference(DeltaE.CmcPerceptibility, unicolour2);
var difference = unicolour1.Difference(DeltaE.Itp, unicolour2);
var difference = unicolour1.Difference(DeltaE.Z, unicolour2);
var difference = unicolour1.Difference(DeltaE.Hyab, unicolour2);
var difference = unicolour1.Difference(DeltaE.Ok, unicolour2);
var difference = unicolour1.Difference(DeltaE.Cam02, unicolour2);
var difference = unicolour1.Difference(DeltaE.Cam16, unicolour2);
```
8. Map colour to display gamut
```c#
var mapped = unicolour.MapToGamut();
```
9. Simulate colour vision deficiency
```c#
var protanopia = unicolour.SimulateProtanopia();
var deuteranopia = unicolour.SimulateDeuteranopia();
var tritanopia = unicolour.SimulateTritanopia();
var achromatopsia = unicolour.SimulateAchromatopsia();
```
## ✨ Examples
This repo contains an [example project](Unicolour.Example/Program.cs) that uses `Unicolour` to:
1. Generate gradients through different colour spaces
2. Render the colour spectrum with different colour vision deficiencies
3. Demonstrate interpolation with and without premultiplied alpha
![Gradients through different colour spaces generated from Unicolour](Unicolour.Example/gradients.png)
![Gradients for different colour vision deficiencies generated from Unicolour](Unicolour.Example/vision-deficiency.png)
![Interpolation from red to transparent to blue, with and without premultiplied alpha](Unicolour.Example/alpha-interpolation.png)
There is also a [console application](Unicolour.Console/Program.cs) that uses `Unicolour` to show colour information for a given hex value:
![Colour information from hex value](Unicolour.Console/colour-info.png)
## 💡 Configuration
A `Configuration` parameter can be used to customise the RGB model (e.g. Display P3, Rec. 2020)
and the white point of the XYZ colour space (e.g. D50 reference white used by ICC profiles).
- RGB configuration requires red, green, and blue chromaticity coordinates, the reference white point, and the companding functions.
Default configuration for sRGB, Display P3, and Rec. 2020 is provided.
- XYZ configuration only requires the reference white point.
Default configuration for D65 and D50 (2° observer) is provided.
```c#
// built-in configuration for Rec. 2020 RGB + D65 XYZ
var config = new Configuration(RgbConfiguration.Rec2020, XyzConfiguration.D65);
var unicolour = new Unicolour(ColourSpace.Rgb255, config, 204, 64, 132);
```
```c#
// manual configuration for wide-gamut RGB
var rgbConfig = new RgbConfiguration(
chromaticityR: new(0.7347, 0.2653),
chromaticityG: new(0.1152, 0.8264),
chromaticityB: new(0.1566, 0.0177),
whitePoint: WhitePoint.From(Illuminant.D50),
fromLinear: value => Companding.Gamma(value, 2.19921875),
toLinear: value => Companding.InverseGamma(value, 2.19921875)
);
// manual configuration for Illuminant C (10° observer) XYZ
var xyzConfig = new XyzConfiguration(
whitePoint: WhitePoint.From(Illuminant.C, Observer.Supplementary10)
);
var config = new Configuration(rgbConfig, xyzConfig);
var unicolour = new Unicolour(ColourSpace.Rgb255, config, 255, 20, 147);
```
Configuration is also available for CAM02 & CAM16 viewing conditions,
IC<sub>T</sub>C<sub>P</sub> scalar,
and J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> scalar.
The default white point used by all colour spaces is D65.
This table lists which `Configuration` property determines the white point of each colour space.
| Colour&nbsp;space | White&nbsp;point&nbsp;configuration |
|-----------------------------------------|-------------------------------------|
| RGB | `RgbConfiguration` |
| Linear&nbsp;RGB | `RgbConfiguration` |
| HSB/HSV | `RgbConfiguration` |
| HSL | `RgbConfiguration` |
| HWB | `RgbConfiguration` |
| CIEXYZ | `XyzConfiguration` |
| CIExyY | `XyzConfiguration` |
| CIELAB | `XyzConfiguration` |
| CIELUV | `XyzConfiguration` |
| CIELCh<sub>uv</sub> | `XyzConfiguration` |
| HSLuv | `XyzConfiguration` |
| HPLuv | `XyzConfiguration` |
| IC<sub>T</sub>C<sub>P</sub> | None (always D65) |
| J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> | None (always D65) |
| J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> | None (always D65) |
| Oklab | None (always D65) |
| Oklch | None (always D65) |
| CIECAM02 | `CamConfiguration` |
| CAM16 | `CamConfiguration` |
| HCT | None (always D65) |
A `Unicolour` can be converted to a different configuration, which enables conversions between different RGB and XYZ models.
```c#
// pure sRGB green
var srgbConfig = new Configuration(RgbConfiguration.StandardRgb);
var unicolourSrgb = new Unicolour(ColourSpace.Rgb, srgbConfig, 0, 1, 0);
Console.WriteLine(unicolourSrgb.Rgb); // 0.00 1.00 0.00
// ⟶ Display P3
var displayP3Config = new Configuration(RgbConfiguration.DisplayP3);
var unicolourDisplayP3 = unicolourSrgb.ConvertToConfiguration(displayP3Config);
Console.WriteLine(unicolourDisplayP3.Rgb); // 0.46 0.99 0.30
// ⟶ Rec. 2020
var rec2020Config = new Configuration(RgbConfiguration.Rec2020);
var unicolourRec2020 = unicolourDisplayP3.ConvertToConfiguration(rec2020Config);
Console.WriteLine(unicolourRec2020.Rgb); // 0.57 0.96 0.27
```
## 🔮 Datasets
Some colour datasets have been compiled for convenience and are available as a [NuGet package](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/).
Commonly used sets of colours:
- [CSS specification](https://www.w3.org/TR/css-color-4/#named-colors) named colours
- [Macbeth ColorChecker](https://en.wikipedia.org/wiki/ColorChecker) colour rendition chart
Colour data used in academic literature:
- [Hung-Berns](https://doi.org/10.1002/col.5080200506) constant hue loci data
- [Ebner-Fairchild](https://doi.org/10.1117/12.298269) constant perceived-hue data
Example usage:
1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/)
```
dotnet add package Wacton.Unicolour.Datasets
```
2. Import the package
```c#
using Wacton.Unicolour.Datasets;
```
3. Reference the predefined `Unicolour`
```c#
var unicolour = Css.DeepPink;
```
## 🦺 Work in progress
Version 4 of Unicolour is in development and aims to provide more new features:
- 🌡️ Create a `Unicolour` from temperature (CCT and Duv)
- 🎯 More accurate calculation of temperature (CCT and Duv)
- 📈 Create a `Unicolour` from a spectral power distribution
- 🚥 More modes of hue interpolation
- 🎨 More default RGB models (e.g. A98, ProPhoto)
---
[Wacton.Unicolour](https://github.com/waacton/Unicolour) is licensed under the [MIT License](https://choosealicense.com/licenses/mit/), copyright © 2022-2023 William Acton.

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

66
Unicolour/Rgb.cs Normal file
View File

@@ -0,0 +1,66 @@
using System;
namespace Wacton.Unicolour
{
public record Rgb : ColourRepresentation
{
protected override int? HueIndex => null;
public double R => First;
public double G => Second;
public double B => Third;
public double ConstrainedR => ConstrainedFirst;
public double ConstrainedG => ConstrainedSecond;
public double ConstrainedB => ConstrainedThird;
protected override double ConstrainedFirst => R.Clamp(0.0, 1.0);
protected override double ConstrainedSecond => G.Clamp(0.0, 1.0);
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
internal override bool IsGreyscale => ConstrainedR.Equals(ConstrainedG) && ConstrainedG.Equals(ConstrainedB);
// for almost all cases, doing this check in linear RGB will return the same result
// but handling it here feels most natural as it is the intended "display" space
// and isn't concerned about questionable custom inverse-companding-to-linear functions (e.g. where where RGB <= 1.0 but RGB-Linear > 1.0)
internal bool IsInGamut => !UseAsNaN && R is >= 0 and <= 1.0 && G is >= 0 and <= 1.0 && B is >= 0 and <= 1.0;
public Rgb255 Byte255 => new(To255(R), To255(G), To255(B), ColourHeritage.From(this));
public Rgb(double r, double g, double b) : this(r, g, b, ColourHeritage.None)
{
}
internal Rgb(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
triplet.Third, heritage)
{
}
internal Rgb(double r, double g, double b, ColourHeritage heritage) : base(r, g, b, heritage)
{
}
private static double To255(double value) => Math.Round(value * 255);
protected override string FirstString => $"{R:F2}";
protected override string SecondString => $"{G:F2}";
protected override string ThirdString => $"{B:F2}";
public override string ToString() => base.ToString();
/*
* RGB is a transform of RGB Linear
* Forward: https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
* Reverse: https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
*/
internal static Rgb FromRgbLinear(RgbLinear rgbLinear, RgbConfiguration rgbConfig)
{
var rgbLinearMatrix = Matrix.FromTriplet(rgbLinear.Triplet);
var rgbMatrix = rgbLinearMatrix.Select(rgbConfig.CompandFromLinear);
return new Rgb(rgbMatrix.ToTriplet(), ColourHeritage.From(rgbLinear));
}
internal static RgbLinear ToRgbLinear(Rgb rgb, RgbConfiguration rgbConfig)
{
var rgbMatrix = Matrix.FromTriplet(rgb.Triplet);
var rgbLinearMatrix = rgbMatrix.Select(rgbConfig.InverseCompandToLinear);
return new RgbLinear(rgbLinearMatrix.ToTriplet(), ColourHeritage.From(rgb));
}
}
}

33
Unicolour/Rgb255.cs Normal file
View File

@@ -0,0 +1,33 @@
namespace Wacton.Unicolour
{
public record Rgb255 : ColourRepresentation
{
protected override int? HueIndex => null;
public int R => (int)First;
public int G => (int)Second;
public int B => (int)Third;
public int ConstrainedR => (int)ConstrainedFirst;
public int ConstrainedG => (int)ConstrainedSecond;
public int ConstrainedB => (int)ConstrainedThird;
protected override double ConstrainedFirst => R.Clamp(0, 255);
protected override double ConstrainedSecond => G.Clamp(0, 255);
protected override double ConstrainedThird => B.Clamp(0, 255);
internal override bool IsGreyscale => ConstrainedR.Equals(ConstrainedG) && ConstrainedG.Equals(ConstrainedB);
public string ConstrainedHex => UseAsNaN ? "-" : $"#{ConstrainedR:X2}{ConstrainedG:X2}{ConstrainedB:X2}";
public Rgb255(double r, double g, double b) : this(r, g, b, ColourHeritage.None)
{
}
internal Rgb255(double r, double g, double b, ColourHeritage heritage) : base(r, g, b, heritage)
{
}
protected override string FirstString => $"{R}";
protected override string SecondString => $"{G}";
protected override string ThirdString => $"{B}";
public override string ToString() => base.ToString();
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace Wacton.Unicolour
{
public class RgbConfiguration
{
public static readonly RgbConfiguration StandardRgb = RgbModels.StandardRgb.RgbConfiguration;
public static readonly RgbConfiguration DisplayP3 = RgbModels.DisplayP3.RgbConfiguration;
public static readonly RgbConfiguration Rec2020 = RgbModels.Rec2020.RgbConfiguration;
public static readonly RgbConfiguration A98 = RgbModels.A98.RgbConfiguration;
public static readonly RgbConfiguration ProPhoto = RgbModels.ProPhoto.RgbConfiguration;
public Chromaticity ChromaticityR { get; }
public Chromaticity ChromaticityG { get; }
public Chromaticity ChromaticityB { get; }
public WhitePoint WhitePoint { get; }
public Func<double, double> CompandFromLinear { get; }
public Func<double, double> InverseCompandToLinear { get; }
public RgbConfiguration(
Chromaticity chromaticityR,
Chromaticity chromaticityG,
Chromaticity chromaticityB,
WhitePoint whitePoint,
Func<double, double> fromLinear,
Func<double, double> toLinear)
{
ChromaticityR = chromaticityR;
ChromaticityG = chromaticityG;
ChromaticityB = chromaticityB;
WhitePoint = whitePoint;
CompandFromLinear = fromLinear;
InverseCompandToLinear = toLinear;
}
public override string ToString() => $"RGB {WhitePoint} {ChromaticityR} {ChromaticityG} {ChromaticityB}";
}
}

97
Unicolour/RgbLinear.cs Normal file
View File

@@ -0,0 +1,97 @@
namespace Wacton.Unicolour
{
public record RgbLinear : ColourRepresentation
{
protected override int? HueIndex => null;
public double R => First;
public double G => Second;
public double B => Third;
public double ConstrainedR => ConstrainedFirst;
public double ConstrainedG => ConstrainedSecond;
public double ConstrainedB => ConstrainedThird;
protected override double ConstrainedFirst => R.Clamp(0.0, 1.0);
protected override double ConstrainedSecond => G.Clamp(0.0, 1.0);
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
internal override bool IsGreyscale => ConstrainedR.Equals(ConstrainedG) && ConstrainedG.Equals(ConstrainedB);
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance - effectively an approximation of Y from XYZ, but will stick to the specification
internal double RelativeLuminance => UseAsNaN ? double.NaN : 0.2126 * R + 0.7152 * G + 0.0722 * B;
public RgbLinear(double r, double g, double b) : this(r, g, b, ColourHeritage.None)
{
}
internal RgbLinear(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
triplet.Third, heritage)
{
}
internal RgbLinear(double r, double g, double b, ColourHeritage heritage) : base(r, g, b, heritage)
{
}
protected override string FirstString => $"{R:F2}";
protected override string SecondString => $"{G:F2}";
protected override string ThirdString => $"{B:F2}";
public override string ToString() => base.ToString();
/*
* RGB Linear is a transform of XYZ
* Forward: https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
* Reverse: https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
*/
internal static RgbLinear FromXyz(Xyz xyz, RgbConfiguration rgbConfig, XyzConfiguration xyzConfig)
{
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
var transformationMatrix = RgbLinearToXyzMatrix(rgbConfig, xyzConfig).Inverse();
var rgbLinearMatrix = transformationMatrix.Multiply(xyzMatrix);
return new RgbLinear(rgbLinearMatrix.ToTriplet(), ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(RgbLinear rgbLinear, RgbConfiguration rgbConfig, XyzConfiguration xyzConfig)
{
var rgbLinearMatrix = Matrix.FromTriplet(rgbLinear.Triplet);
var transformationMatrix = RgbLinearToXyzMatrix(rgbConfig, xyzConfig);
var xyzMatrix = transformationMatrix.Multiply(rgbLinearMatrix);
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(rgbLinear));
}
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
internal static Matrix RgbLinearToXyzMatrix(RgbConfiguration rgbConfig, XyzConfiguration xyzConfig)
{
var cr = rgbConfig.ChromaticityR;
var cg = rgbConfig.ChromaticityG;
var cb = rgbConfig.ChromaticityB;
double X(Chromaticity c) => c.X / c.Y;
double Y(Chromaticity c) => 1;
double Z(Chromaticity c) => (1 - c.X - c.Y) / c.Y;
var (xr, yr, zr) = (X(cr), Y(cr), Z(cr));
var (xg, yg, zg) = (X(cg), Y(cg), Z(cg));
var (xb, yb, zb) = (X(cb), Y(cb), Z(cb));
var fromPrimaries = new Matrix(new[,]
{
{ xr, xg, xb },
{ yr, yg, yb },
{ zr, zg, zb }
});
var sourceWhite = rgbConfig.WhitePoint.AsXyzMatrix();
var (sr, sg, sb) = fromPrimaries.Inverse().Multiply(sourceWhite).ToTriplet();
var matrix = new Matrix(new[,]
{
{ sr * xr, sg * xg, sb * xb },
{ sr * yr, sg * yg, sb * yb },
{ sr * zr, sg * zg, sb * zb }
});
var adaptedMatrix = Adaptation.WhitePoint(matrix, rgbConfig.WhitePoint, xyzConfig.WhitePoint);
return adaptedMatrix;
}
}
}

126
Unicolour/RgbModels.cs Normal file
View File

@@ -0,0 +1,126 @@
using System;
namespace Wacton.Unicolour
{
public static class RgbModels
{
public static class StandardRgb
{
public static readonly Chromaticity R = new(0.6400, 0.3300);
public static readonly Chromaticity G = new(0.3000, 0.6000);
public static readonly Chromaticity B = new(0.1500, 0.0600);
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
public static double FromLinear(double linear)
{
return Companding.ReflectWhenNegative(linear, value =>
value <= 0.0031308
? 12.92 * value
: 1.055 * Companding.Gamma(value, 2.4) - 0.055);
}
public static double ToLinear(double nonlinear)
{
return Companding.ReflectWhenNegative(nonlinear, value =>
value <= 0.04045
? value / 12.92
: Companding.InverseGamma((value + 0.055) / 1.055, 2.4));
}
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
}
public static class DisplayP3
{
public static readonly Chromaticity R = new(0.680, 0.320);
public static readonly Chromaticity G = new(0.265, 0.690);
public static readonly Chromaticity B = new(0.150, 0.060);
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
public static double FromLinear(double value) => StandardRgb.FromLinear(value);
public static double ToLinear(double value) => StandardRgb.ToLinear(value);
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
}
public static class Rec2020
{
public static readonly Chromaticity R = new(0.708, 0.292);
public static readonly Chromaticity G = new(0.170, 0.797);
public static readonly Chromaticity B = new(0.131, 0.046);
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
private const double Alpha = 1.09929682680944;
private const double Beta = 0.018053968510807;
public static double FromLinear(double linear)
{
return Companding.ReflectWhenNegative(linear, e =>
{
if (e < Beta) return 4.5 * e;
return Alpha * Math.Pow(e, 0.45) - (Alpha - 1);
});
}
public static double ToLinear(double nonlinear)
{
return Companding.ReflectWhenNegative(nonlinear, ePrime =>
{
if (ePrime < Beta * 4.5) return ePrime / 4.5;
return Math.Pow((ePrime + (Alpha - 1)) / Alpha, 1 / 0.45);
});
}
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
}
public static class A98
{
public static readonly Chromaticity R = new(0.6400, 0.3300);
public static readonly Chromaticity G = new(0.2100, 0.7100);
public static readonly Chromaticity B = new(0.1500, 0.0600);
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
public static double FromLinear(double linear)
{
return Companding.ReflectWhenNegative(linear, value => Companding.Gamma(value, 563 / 256.0));
}
public static double ToLinear(double nonlinear)
{
return Companding.ReflectWhenNegative(nonlinear, value => Companding.InverseGamma(value, 563 / 256.0));
}
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
}
public static class ProPhoto
{
public static readonly Chromaticity R = new(0.734699, 0.265301);
public static readonly Chromaticity G = new(0.159597, 0.840403);
public static readonly Chromaticity B = new(0.036598, 0.000105);
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D50);
private const double Et = 1 / 512.0;
public static double FromLinear(double linear)
{
return Companding.ReflectWhenNegative(linear, value =>
value < Et
? 16 * value
: Companding.Gamma(value, 1.8));
}
public static double ToLinear(double nonlinear)
{
return Companding.ReflectWhenNegative(nonlinear, value =>
value < Et * 16
? value / 16.0
: Companding.InverseGamma(value, 1.8));
}
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
}
}
}

124
Unicolour/Temperature.cs Normal file
View File

@@ -0,0 +1,124 @@
using System;
namespace Wacton.Unicolour
{
public record Temperature(double Cct, double Duv)
{
public double Cct { get; } = Cct;
public double Duv { get; } = Duv;
private static Temperature NaN() => new(double.NaN, double.NaN);
// https://en.wikipedia.org/wiki/CIE_1960_color_space
internal static Temperature Get(Xyz xyz)
{
var (x, y, z) = xyz.Triplet;
var u = 4.0 * x / (x + 15.0 * y + 3.0 * z);
var v = 6.0 * y / (x + 15.0 * y + 3.0 * z);
return double.IsNaN(u) || double.IsNaN(v) ? NaN() : Get(u, v);
}
// https://en.wikipedia.org/wiki/Correlated_color_temperature#Robertson's_method
internal static Temperature Get(double u, double v)
{
for (var i = 0; i < Isotherms.Length - 1; i++)
{
var lowerIsotherm = Isotherms[i];
var upperIsotherm = Isotherms[i + 1];
// not actually the distance yet, but can be used to inform if between isotherms
var lowerDistance = v - lowerIsotherm.V - lowerIsotherm.M * (u - lowerIsotherm.U);
var upperDistance = v - upperIsotherm.V - upperIsotherm.M * (u - upperIsotherm.U);
// when distance to the 2 isotherms are in different directions, test point (u, v) is between them
var isBetweenIsotherms = Math.Sign(lowerDistance) != Math.Sign(upperDistance);
if (!isBetweenIsotherms)
{
continue;
}
// once between isotherms, can calculate the distance and interpolate
lowerDistance /= Math.Sqrt(1.0 + Math.Pow(lowerIsotherm.M, 2));
upperDistance /= Math.Sqrt(1.0 + Math.Pow(upperIsotherm.M, 2));
// signs are different, will add the distances together
var totalDistance = lowerDistance - upperDistance;
var interpolationDistance = lowerDistance / totalDistance;
var reciprocalMegakelvin = Interpolation.Interpolate(lowerIsotherm.ReciprocalMegakelvin,
upperIsotherm.ReciprocalMegakelvin, interpolationDistance);
var kelvins = 1000000 / reciprocalMegakelvin;
var duv = GetDuv(u, v);
return new Temperature(kelvins, duv);
}
return NaN();
}
// https://doi.org/10.1080/15502724.2014.839020
private static double GetDuv(double u, double v)
{
const double k6 = -0.00616793;
const double k5 = 0.0893944;
const double k4 = -0.5179722;
const double k3 = 1.5317403;
const double k2 = -2.4243787;
const double k1 = 1.925865;
const double k0 = -0.471106;
var lfp = Math.Sqrt(Math.Pow(u - 0.292, 2) + Math.Pow(v - 0.240, 2));
var a = Math.Acos((u - 0.292) / lfp);
var lbb = k6 * Math.Pow(a, 6) + k5 * Math.Pow(a, 5) + k4 * Math.Pow(a, 4) +
k3 * Math.Pow(a, 3) + k2 * Math.Pow(a, 2) + k1 * a + k0;
return lfp - lbb;
}
private record Isotherm(double U, double V, double M, double ReciprocalMegakelvin)
{
public double U { get; } = U;
public double V { get; } = V;
public double M { get; } = M;
public double ReciprocalMegakelvin { get; } = ReciprocalMegakelvin; // also known as mired
}
// http://www.brucelindbloom.com/Eqn_XYZ_to_T.html
private static readonly Isotherm[] Isotherms =
{
new(0.18006, 0.26352, -0.24341, double.Epsilon),
new(0.18066, 0.26589, -0.25479, 10),
new(0.18133, 0.26846, -0.26876, 20),
new(0.18208, 0.27119, -0.28539, 30),
new(0.18293, 0.27407, -0.30470, 40),
new(0.18388, 0.27709, -0.32675, 50),
new(0.18494, 0.28021, -0.35156, 60),
new(0.18611, 0.28342, -0.37915, 70),
new(0.18740, 0.28668, -0.40955, 80),
new(0.18880, 0.28997, -0.44278, 90),
new(0.19032, 0.29326, -0.47888, 100),
new(0.19462, 0.30141, -0.58204, 125),
new(0.19962, 0.30921, -0.70471, 150),
new(0.20525, 0.31647, -0.84901, 175),
new(0.21142, 0.32312, -1.0182, 200),
new(0.21807, 0.32909, -1.2168, 225),
new(0.22511, 0.33439, -1.4512, 250),
new(0.23247, 0.33904, -1.7298, 275),
new(0.24010, 0.34308, -2.0637, 300),
new(0.24792, 0.34655, -2.4681, 325),
new(0.25591, 0.34951, -2.9641, 350),
new(0.26400, 0.35200, -3.5814, 375),
new(0.27218, 0.35407, -4.3633, 400),
new(0.28039, 0.35577, -5.3762, 425),
new(0.28863, 0.35714, -6.7262, 450),
new(0.29685, 0.35823, -8.5955, 475),
new(0.30505, 0.35907, -11.324, 500),
new(0.31320, 0.35968, -15.628, 525),
new(0.32129, 0.36011, -23.325, 550),
new(0.32931, 0.36038, -40.770, 575),
new(0.33724, 0.36051, -116.45, 600)
};
public override string ToString() =>
double.IsNaN(Cct) || double.IsNaN(Duv) ? "-" : $"{Cct:F1} K (Δuv {Duv:F5})";
}
}

145
Unicolour/Unicolour.cs Normal file
View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
namespace Wacton.Unicolour
{
public partial class Unicolour : IEquatable<Unicolour>
{
private Rgb? rgb;
private RgbLinear? rgbLinear;
private Hsb? hsb;
private Hsl? hsl;
private Hwb? hwb;
private Xyz? xyz;
private Xyy? xyy;
private Lab? lab;
private Lchab? lchab;
private Luv? luv;
private Lchuv? lchuv;
private Hsluv? hsluv;
private Hpluv? hpluv;
private Ictcp? ictcp;
private Jzazbz? jzazbz;
private Jzczhz? jzczhz;
private Oklab? oklab;
private Oklch? oklch;
private Cam02? cam02;
private Cam16? cam16;
private Hct? hct;
internal readonly ColourRepresentation InitialRepresentation;
internal readonly ColourSpace InitialColourSpace;
public Rgb Rgb => Get<Rgb>(ColourSpace.Rgb);
public RgbLinear RgbLinear => Get<RgbLinear>(ColourSpace.RgbLinear);
public Hsb Hsb => Get<Hsb>(ColourSpace.Hsb);
public Hsl Hsl => Get<Hsl>(ColourSpace.Hsl);
public Hwb Hwb => Get<Hwb>(ColourSpace.Hwb);
public Xyz Xyz => Get<Xyz>(ColourSpace.Xyz);
public Xyy Xyy => Get<Xyy>(ColourSpace.Xyy);
public Lab Lab => Get<Lab>(ColourSpace.Lab);
public Lchab Lchab => Get<Lchab>(ColourSpace.Lchab);
public Luv Luv => Get<Luv>(ColourSpace.Luv);
public Lchuv Lchuv => Get<Lchuv>(ColourSpace.Lchuv);
public Hsluv Hsluv => Get<Hsluv>(ColourSpace.Hsluv);
public Hpluv Hpluv => Get<Hpluv>(ColourSpace.Hpluv);
public Ictcp Ictcp => Get<Ictcp>(ColourSpace.Ictcp);
public Jzazbz Jzazbz => Get<Jzazbz>(ColourSpace.Jzazbz);
public Jzczhz Jzczhz => Get<Jzczhz>(ColourSpace.Jzczhz);
public Oklab Oklab => Get<Oklab>(ColourSpace.Oklab);
public Oklch Oklch => Get<Oklch>(ColourSpace.Oklch);
public Cam02 Cam02 => Get<Cam02>(ColourSpace.Cam02);
public Cam16 Cam16 => Get<Cam16>(ColourSpace.Cam16);
public Hct Hct => Get<Hct>(ColourSpace.Hct);
public Alpha Alpha { get; }
public Configuration Config { get; }
public string Hex => !IsInDisplayGamut ? "-" : Rgb.Byte255.ConstrainedHex;
public bool IsInDisplayGamut => Rgb.IsInGamut;
public double RelativeLuminance => RgbLinear.RelativeLuminance;
public string Description => string.Join(" ", ColourDescription.Get(Hsl));
public Temperature Temperature => Temperature.Get(Xyz);
internal Unicolour(ColourSpace colourSpace, Configuration config, ColourHeritage heritage, double first,
double second, double third, double alpha = 1.0)
{
if (colourSpace == ColourSpace.Rgb255)
{
colourSpace = ColourSpace.Rgb;
first /= 255.0;
second /= 255.0;
third /= 255.0;
}
Config = config;
Alpha = new Alpha(alpha);
InitialRepresentation = CreateRepresentation(colourSpace, first, second, third, config, heritage);
InitialColourSpace = colourSpace;
SetBackingField(InitialColourSpace);
}
public double Contrast(Unicolour other) => Comparison.Contrast(this, other);
public double Difference(DeltaE deltaE, Unicolour reference) => Comparison.Difference(deltaE, this, reference);
public Unicolour Mix(ColourSpace colourSpace, Unicolour other, double amount = 0.5,
bool premultiplyAlpha = true)
{
return Interpolation.Mix(colourSpace, this, other, amount, premultiplyAlpha);
}
public Unicolour SimulateProtanopia() => VisionDeficiency.SimulateProtanopia(this);
public Unicolour SimulateDeuteranopia() => VisionDeficiency.SimulateDeuteranopia(this);
public Unicolour SimulateTritanopia() => VisionDeficiency.SimulateTritanopia(this);
public Unicolour SimulateAchromatopsia() => VisionDeficiency.SimulateAchromatopsia(this);
public Unicolour MapToGamut() => GamutMapping.ToRgbGamut(this);
public Unicolour ConvertToConfiguration(Configuration newConfig)
{
var xyzMatrix = Matrix.FromTriplet(Xyz.Triplet);
var adaptedMatrix = Adaptation.WhitePoint(xyzMatrix, Config.Xyz.WhitePoint, newConfig.Xyz.WhitePoint);
return new Unicolour(ColourSpace.Xyz, newConfig, adaptedMatrix.ToTriplet().Tuple, Alpha.A);
}
public override string ToString()
{
var parts = new List<string> { $"from {InitialColourSpace} {InitialRepresentation} alpha {Alpha}" };
if (Description != ColourDescription.NotApplicable.ToString())
{
parts.Add(Description);
}
return string.Join(" · ", parts);
}
// ----- the following is based on auto-generated code -----
public bool Equals(Unicolour? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return ColourSpaceEquals(other) && Alpha.Equals(other.Alpha);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Unicolour)obj);
}
private bool ColourSpaceEquals(Unicolour other)
{
return InitialRepresentation.Equals(other.InitialRepresentation);
}
public override int GetHashCode()
{
unchecked
{
return (InitialRepresentation.GetHashCode() * 397) ^ Alpha.GetHashCode();
}
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

View File

@@ -0,0 +1,45 @@
namespace Wacton.Unicolour
{
public partial class Unicolour
{
/* standard constructors */
public Unicolour(ColourSpace colourSpace, (double first, double second, double third) tuple,
double alpha = 1.0) :
this(colourSpace, tuple.first, tuple.second, tuple.third, alpha)
{
}
public Unicolour(ColourSpace colourSpace, double first, double second, double third, double alpha = 1.0) :
this(colourSpace, Configuration.Default, first, second, third, alpha)
{
}
public Unicolour(ColourSpace colourSpace, Configuration config,
(double first, double second, double third) tuple, double alpha = 1.0) :
this(colourSpace, config, tuple.first, tuple.second, tuple.third, alpha)
{
}
public Unicolour(ColourSpace colourSpace, Configuration config,
(double first, double second, double third, double alpha) tuple) :
this(colourSpace, config, tuple.first, tuple.second, tuple.third, tuple.alpha)
{
}
public Unicolour(ColourSpace colourSpace, Configuration config, double first, double second, double third,
double alpha = 1.0) :
this(colourSpace, config, ColourHeritage.None, first, second, third, alpha)
{
}
/* special variations for hex */
public Unicolour(string hex) : this(Configuration.Default, hex)
{
}
public Unicolour(Configuration config, string hex) : this(ColourSpace.Rgb, config, Utils.ParseColourHex(hex))
{
}
}
}

View File

@@ -0,0 +1,397 @@
using System;
namespace Wacton.Unicolour
{
/*
* ColourSpace information needs to be held outwith ColourRepresentation object
* otherwise the ColourRepresentation needs to be evaluated just to obtain the ColourSpace it represents
*/
public partial class Unicolour
{
private static ColourRepresentation CreateRepresentation(
ColourSpace colourSpace, double first, double second, double third,
Configuration config, ColourHeritage heritage)
{
return colourSpace switch
{
ColourSpace.Rgb => new Rgb(first, second, third, heritage),
ColourSpace.RgbLinear => new RgbLinear(first, second, third, heritage),
ColourSpace.Hsb => new Hsb(first, second, third, heritage),
ColourSpace.Hsl => new Hsl(first, second, third, heritage),
ColourSpace.Hwb => new Hwb(first, second, third, heritage),
ColourSpace.Xyz => new Xyz(first, second, third, heritage),
ColourSpace.Xyy => new Xyy(first, second, third, heritage),
ColourSpace.Lab => new Lab(first, second, third, heritage),
ColourSpace.Lchab => new Lchab(first, second, third, heritage),
ColourSpace.Luv => new Luv(first, second, third, heritage),
ColourSpace.Lchuv => new Lchuv(first, second, third, heritage),
ColourSpace.Hsluv => new Hsluv(first, second, third, heritage),
ColourSpace.Hpluv => new Hpluv(first, second, third, heritage),
ColourSpace.Ictcp => new Ictcp(first, second, third, heritage),
ColourSpace.Jzazbz => new Jzazbz(first, second, third, heritage),
ColourSpace.Jzczhz => new Jzczhz(first, second, third, heritage),
ColourSpace.Oklab => new Oklab(first, second, third, heritage),
ColourSpace.Oklch => new Oklch(first, second, third, heritage),
ColourSpace.Cam02 => new Cam02(new Cam.Ucs(first, second, third), config.Cam, heritage),
ColourSpace.Cam16 => new Cam16(new Cam.Ucs(first, second, third), config.Cam, heritage),
ColourSpace.Hct => new Hct(first, second, third, heritage),
_ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null)
};
}
public ColourRepresentation GetRepresentation(ColourSpace colourSpace)
{
return colourSpace switch
{
ColourSpace.Rgb => Rgb,
ColourSpace.Rgb255 => Rgb.Byte255,
ColourSpace.RgbLinear => RgbLinear,
ColourSpace.Hsb => Hsb,
ColourSpace.Hsl => Hsl,
ColourSpace.Hwb => Hwb,
ColourSpace.Xyz => Xyz,
ColourSpace.Xyy => Xyy,
ColourSpace.Lab => Lab,
ColourSpace.Lchab => Lchab,
ColourSpace.Luv => Luv,
ColourSpace.Lchuv => Lchuv,
ColourSpace.Hsluv => Hsluv,
ColourSpace.Hpluv => Hpluv,
ColourSpace.Ictcp => Ictcp,
ColourSpace.Jzazbz => Jzazbz,
ColourSpace.Jzczhz => Jzczhz,
ColourSpace.Oklab => Oklab,
ColourSpace.Oklch => Oklch,
ColourSpace.Cam02 => Cam02,
ColourSpace.Cam16 => Cam16,
ColourSpace.Hct => Hct,
_ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null)
};
}
/*
* getting a value will trigger a chain of gets and conversions if the intermediary values have not been calculated yet
* e.g. if Unicolour is created from RGB, and the first request is for LAB:
* - Get(ColourSpace.Lab); lab is null, execute: lab = Lab.FromXyz(Xyz, Config)
* - Get(ColourSpace.Xyz); xyz is null, execute: xyz = Rgb.ToXyz(Rgb, Config)
* - Get(ColourSpace.Rgb); rgb is not null, return value
* - xyz is evaluated from rgb and stored
* - lab is evaluated from xyz and stored
*/
private T Get<T>(ColourSpace targetSpace) where T : ColourRepresentation
{
var backingRepresentation = GetBackingField(targetSpace);
if (backingRepresentation == null)
{
SetBackingField(targetSpace);
backingRepresentation = GetBackingField(targetSpace);
}
return (backingRepresentation as T)!;
}
private ColourRepresentation? GetBackingField(ColourSpace colourSpace)
{
return colourSpace switch
{
ColourSpace.Rgb => rgb,
ColourSpace.RgbLinear => rgbLinear,
ColourSpace.Hsb => hsb,
ColourSpace.Hsl => hsl,
ColourSpace.Hwb => hwb,
ColourSpace.Xyz => xyz,
ColourSpace.Xyy => xyy,
ColourSpace.Lab => lab,
ColourSpace.Lchab => lchab,
ColourSpace.Luv => luv,
ColourSpace.Lchuv => lchuv,
ColourSpace.Hsluv => hsluv,
ColourSpace.Hpluv => hpluv,
ColourSpace.Ictcp => ictcp,
ColourSpace.Jzazbz => jzazbz,
ColourSpace.Jzczhz => jzczhz,
ColourSpace.Oklab => oklab,
ColourSpace.Oklch => oklch,
ColourSpace.Cam02 => cam02,
ColourSpace.Cam16 => cam16,
ColourSpace.Hct => hct,
_ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null)
};
}
private void SetBackingField(ColourSpace targetSpace)
{
Action setField = targetSpace switch
{
ColourSpace.Rgb => () => rgb = EvaluateRgb(),
ColourSpace.RgbLinear => () => rgbLinear = EvaluateRgbLinear(),
ColourSpace.Hsb => () => hsb = EvaluateHsb(),
ColourSpace.Hsl => () => hsl = EvaluateHsl(),
ColourSpace.Hwb => () => hwb = EvaluateHwb(),
ColourSpace.Xyz => () => xyz = EvaluateXyz(),
ColourSpace.Xyy => () => xyy = EvaluateXyy(),
ColourSpace.Lab => () => lab = EvaluateLab(),
ColourSpace.Lchab => () => lchab = EvaluateLchab(),
ColourSpace.Luv => () => luv = EvaluateLuv(),
ColourSpace.Lchuv => () => lchuv = EvaluateLchuv(),
ColourSpace.Hsluv => () => hsluv = EvaluateHsluv(),
ColourSpace.Hpluv => () => hpluv = EvaluateHpluv(),
ColourSpace.Ictcp => () => ictcp = EvaluateIctcp(),
ColourSpace.Jzazbz => () => jzazbz = EvaluateJzazbz(),
ColourSpace.Jzczhz => () => jzczhz = EvaluateJzczhz(),
ColourSpace.Oklab => () => oklab = EvaluateOklab(),
ColourSpace.Oklch => () => oklch = EvaluateOklch(),
ColourSpace.Cam02 => () => cam02 = EvaluateCam02(),
ColourSpace.Cam16 => () => cam16 = EvaluateCam16(),
ColourSpace.Hct => () => hct = EvaluateHct(),
_ => throw new ArgumentOutOfRangeException(nameof(targetSpace), targetSpace, null)
};
setField();
}
/*
* evaluation method switch expressions are arranged as follows:
* - first item = target space is the initial space, simply return initial representation
* - middle items = reverse transforms to the target space; only the immediate transforms
* - default item = forward transform from a base space
* -----------------
* only need to consider the transforms relative to the target-space, as subsequent transforms are handled recursively
* e.g. for target-space RGB...
* - starting at HSL:
* - transforms: HSL ==reverse==> HSB ==reverse==> RGB
* - only need to specify: HSB ==reverse==> RGB
* - function: Hsb.ToRgb()
* - starting at LAB:
* - LAB ==reverse==> XYZ ==forward==> RGB Linear ==forward==> RGB
* - only need to specify: RGB Linear ==forward==> RGB
* - function: Rgb.FromRgbLinear()
*/
private Rgb EvaluateRgb()
{
return InitialColourSpace switch
{
ColourSpace.Rgb => (Rgb)InitialRepresentation,
ColourSpace.Hsb => Hsb.ToRgb(Hsb),
ColourSpace.Hsl => Hsb.ToRgb(Hsb),
ColourSpace.Hwb => Hsb.ToRgb(Hsb),
_ => Rgb.FromRgbLinear(RgbLinear, Config.Rgb)
};
}
private RgbLinear EvaluateRgbLinear()
{
return InitialColourSpace switch
{
ColourSpace.RgbLinear => (RgbLinear)InitialRepresentation,
ColourSpace.Rgb => Rgb.ToRgbLinear(Rgb, Config.Rgb),
ColourSpace.Hsb => Rgb.ToRgbLinear(Rgb, Config.Rgb),
ColourSpace.Hsl => Rgb.ToRgbLinear(Rgb, Config.Rgb),
ColourSpace.Hwb => Rgb.ToRgbLinear(Rgb, Config.Rgb),
_ => RgbLinear.FromXyz(Xyz, Config.Rgb, Config.Xyz)
};
}
private Hsb EvaluateHsb()
{
return InitialColourSpace switch
{
ColourSpace.Hsb => (Hsb)InitialRepresentation,
ColourSpace.Hsl => Hsl.ToHsb(Hsl),
ColourSpace.Hwb => Hwb.ToHsb(Hwb),
_ => Hsb.FromRgb(Rgb)
};
}
private Hsl EvaluateHsl()
{
return InitialColourSpace switch
{
ColourSpace.Hsl => (Hsl)InitialRepresentation,
_ => Hsl.FromHsb(Hsb)
};
}
private Hwb EvaluateHwb()
{
return InitialColourSpace switch
{
ColourSpace.Hwb => (Hwb)InitialRepresentation,
_ => Hwb.FromHsb(Hsb)
};
}
private Xyz EvaluateXyz()
{
return InitialColourSpace switch
{
ColourSpace.Xyz => (Xyz)InitialRepresentation,
ColourSpace.Rgb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
ColourSpace.RgbLinear => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
ColourSpace.Hsb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
ColourSpace.Hsl => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
ColourSpace.Hwb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
ColourSpace.Xyy => Xyy.ToXyz(Xyy),
ColourSpace.Lab => Lab.ToXyz(Lab, Config.Xyz),
ColourSpace.Lchab => Lab.ToXyz(Lab, Config.Xyz),
ColourSpace.Luv => Luv.ToXyz(Luv, Config.Xyz),
ColourSpace.Lchuv => Luv.ToXyz(Luv, Config.Xyz),
ColourSpace.Hsluv => Luv.ToXyz(Luv, Config.Xyz),
ColourSpace.Hpluv => Luv.ToXyz(Luv, Config.Xyz),
ColourSpace.Ictcp => Ictcp.ToXyz(Ictcp, Config.IctcpScalar, Config.Xyz),
ColourSpace.Jzazbz => Jzazbz.ToXyz(Jzazbz, Config.JzazbzScalar, Config.Xyz),
ColourSpace.Jzczhz => Jzazbz.ToXyz(Jzazbz, Config.JzazbzScalar, Config.Xyz),
ColourSpace.Oklab => Oklab.ToXyz(Oklab, Config.Xyz),
ColourSpace.Oklch => Oklab.ToXyz(Oklab, Config.Xyz),
ColourSpace.Cam02 => Cam02.ToXyz(Cam02, Config.Cam, Config.Xyz),
ColourSpace.Cam16 => Cam16.ToXyz(Cam16, Config.Cam, Config.Xyz),
ColourSpace.Hct => Hct.ToXyz(Hct, Config.Xyz),
_ => throw new ArgumentOutOfRangeException()
};
}
private Xyy EvaluateXyy()
{
return InitialColourSpace switch
{
ColourSpace.Xyy => (Xyy)InitialRepresentation,
_ => Xyy.FromXyz(Xyz, Config.Xyz)
};
}
private Lab EvaluateLab()
{
return InitialColourSpace switch
{
ColourSpace.Lab => (Lab)InitialRepresentation,
ColourSpace.Lchab => Lchab.ToLab(Lchab),
_ => Lab.FromXyz(Xyz, Config.Xyz)
};
}
private Lchab EvaluateLchab()
{
return InitialColourSpace switch
{
ColourSpace.Lchab => (Lchab)InitialRepresentation,
_ => Lchab.FromLab(Lab)
};
}
private Luv EvaluateLuv()
{
return InitialColourSpace switch
{
ColourSpace.Luv => (Luv)InitialRepresentation,
ColourSpace.Lchuv => Lchuv.ToLuv(Lchuv),
ColourSpace.Hsluv => Lchuv.ToLuv(Lchuv),
ColourSpace.Hpluv => Lchuv.ToLuv(Lchuv),
_ => Luv.FromXyz(Xyz, Config.Xyz)
};
}
private Lchuv EvaluateLchuv()
{
return InitialColourSpace switch
{
ColourSpace.Lchuv => (Lchuv)InitialRepresentation,
ColourSpace.Hsluv => Hsluv.ToLchuv(Hsluv),
ColourSpace.Hpluv => Hpluv.ToLchuv(Hpluv),
_ => Lchuv.FromLuv(Luv)
};
}
private Hsluv EvaluateHsluv()
{
return InitialColourSpace switch
{
ColourSpace.Hsluv => (Hsluv)InitialRepresentation,
_ => Hsluv.FromLchuv(Lchuv)
};
}
private Hpluv EvaluateHpluv()
{
return InitialColourSpace switch
{
ColourSpace.Hpluv => (Hpluv)InitialRepresentation,
_ => Hpluv.FromLchuv(Lchuv)
};
}
private Ictcp EvaluateIctcp()
{
return InitialColourSpace switch
{
ColourSpace.Ictcp => (Ictcp)InitialRepresentation,
_ => Ictcp.FromXyz(Xyz, Config.IctcpScalar, Config.Xyz)
};
}
private Jzazbz EvaluateJzazbz()
{
return InitialColourSpace switch
{
ColourSpace.Jzazbz => (Jzazbz)InitialRepresentation,
ColourSpace.Jzczhz => Jzczhz.ToJzazbz(Jzczhz),
_ => Jzazbz.FromXyz(Xyz, Config.JzazbzScalar, Config.Xyz)
};
}
private Jzczhz EvaluateJzczhz()
{
return InitialColourSpace switch
{
ColourSpace.Jzczhz => (Jzczhz)InitialRepresentation,
_ => Jzczhz.FromJzazbz(Jzazbz)
};
}
private Oklab EvaluateOklab()
{
return InitialColourSpace switch
{
ColourSpace.Oklab => (Oklab)InitialRepresentation,
ColourSpace.Oklch => Oklch.ToOklab(Oklch),
_ => Oklab.FromXyz(Xyz, Config.Xyz)
};
}
private Oklch EvaluateOklch()
{
return InitialColourSpace switch
{
ColourSpace.Oklch => (Oklch)InitialRepresentation,
_ => Oklch.FromOklab(Oklab)
};
}
private Cam02 EvaluateCam02()
{
return InitialColourSpace switch
{
ColourSpace.Cam02 => (Cam02)InitialRepresentation,
_ => Cam02.FromXyz(Xyz, Config.Cam, Config.Xyz)
};
}
private Cam16 EvaluateCam16()
{
return InitialColourSpace switch
{
ColourSpace.Cam16 => (Cam16)InitialRepresentation,
_ => Cam16.FromXyz(Xyz, Config.Cam, Config.Xyz)
};
}
private Hct EvaluateHct()
{
return InitialColourSpace switch
{
ColourSpace.Hct => (Hct)InitialRepresentation,
_ => Hct.FromXyz(Xyz, Config.Xyz)
};
}
}
}

77
Unicolour/Utils.cs Normal file
View File

@@ -0,0 +1,77 @@
using System;
using System.Linq;
namespace Wacton.Unicolour
{
using System.Globalization;
internal static class Utils
{
internal static double Clamp(this double x, double min, double max) => x < min ? min : x > max ? max : x;
internal static double Clamp(this int x, int min, int max) => x < min ? min : x > max ? max : x;
internal static double CubeRoot(double x) => x < 0 ? -Math.Pow(-x, 1 / 3.0) : Math.Pow(x, 1 / 3.0);
internal static double ToDegrees(double radians) => radians * (180.0 / Math.PI);
internal static double ToRadians(double degrees) => degrees * (Math.PI / 180.0);
internal static double Modulo(this double value, double modulus)
{
if (double.IsNaN(value))
{
return double.NaN;
}
var remainder = value % modulus;
if (remainder == 0.0)
{
return remainder;
}
// handles negatives, e.g. -10 % 360 returns 350 instead of -10
// don't "add a negative" if both values are negative
var useSubtraction = remainder < 0 ^ modulus < 0;
return useSubtraction ? modulus + remainder : remainder;
}
internal static (double r, double g, double b, double a) ParseColourHex(string colourHex)
{
var hex = colourHex.TrimStart('#');
if (hex.Length is not (6 or 8))
{
throw new ArgumentException($"{colourHex} contains invalid number of characters");
}
var r = Parse(hex, 0) / 255.0;
var g = Parse(hex, 2) / 255.0;
var b = Parse(hex, 4) / 255.0;
var a = hex.Length == 8 ? Parse(hex, 6) / 255.0 : 1.0;
return (r, g, b, a);
}
private static int Parse(string hex, int startIndex)
{
var chars = hex.Substring(startIndex, 2).ToUpper();
if (chars.Any(x => !Uri.IsHexDigit(x)))
{
throw new ArgumentException($"{chars} cannot be parsed as hex");
}
return int.Parse(chars, NumberStyles.HexNumber);
}
internal static ColourTriplet ToLchTriplet(double lightness, double axis1, double axis2)
{
var chroma = Math.Sqrt(Math.Pow(axis1, 2) + Math.Pow(axis2, 2));
var hue = ToDegrees(Math.Atan2(axis2, axis1));
return new ColourTriplet(lightness, chroma, hue.Modulo(360.0));
}
internal static (double lightness, double axis1, double axis2) FromLchTriplet(ColourTriplet lchTriplet)
{
var (l, c, h) = lchTriplet;
var axis1 = c * Math.Cos(ToRadians(h));
var axis2 = c * Math.Sin(ToRadians(h));
return (l, axis1, axis2);
}
}
}

View File

@@ -0,0 +1,52 @@
namespace Wacton.Unicolour
{
// https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html
// only using full severity matrices to simulate extreme cases
internal static class VisionDeficiency
{
private static readonly Matrix Protanomaly = new(new[,]
{
{ +0.152286, +1.052583, -0.204868 },
{ +0.114503, +0.786281, +0.099216 },
{ -0.003882, -0.048116, +1.051998 }
});
private static readonly Matrix Deuteranomaly = new(new[,]
{
{ +0.367322, +0.860646, -0.227968 },
{ +0.280085, +0.672501, +0.047413 },
{ -0.011820, +0.042940, +0.968881 }
});
private static readonly Matrix Tritanomaly = new(new[,]
{
{ +1.255528, -0.076749, -0.178779 },
{ -0.078411, +0.930809, +0.147602 },
{ +0.004733, +0.691367, +0.303900 }
});
private static Unicolour SimulateCvd(Unicolour unicolour, Matrix cvdMatrix)
{
var config = unicolour.Config;
// since simulated RGB-Linear often results in values outwith 0 - 1, seems unnecessary to use constrained inputs
var rgbLinearMatrix = Matrix.FromTriplet(unicolour.RgbLinear.Triplet);
var simulatedRgbLinearMatrix = cvdMatrix.Multiply(rgbLinearMatrix);
return new Unicolour(ColourSpace.RgbLinear, config, simulatedRgbLinearMatrix.ToTriplet().Tuple);
}
internal static Unicolour SimulateProtanopia(Unicolour unicolour) => SimulateCvd(unicolour, Protanomaly);
internal static Unicolour SimulateDeuteranopia(Unicolour unicolour) => SimulateCvd(unicolour, Deuteranomaly);
internal static Unicolour SimulateTritanopia(Unicolour unicolour) => SimulateCvd(unicolour, Tritanomaly);
internal static Unicolour SimulateAchromatopsia(Unicolour unicolour)
{
var config = unicolour.Config;
// luminance is based on Linear RGB, so needs to be companded back into chosen RGB space
var rgbLuminance = config.Rgb.CompandFromLinear(unicolour.RelativeLuminance);
return new Unicolour(ColourSpace.Rgb, config, rgbLuminance, rgbLuminance, rgbLuminance);
}
}
}

View File

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Wacton.Unicolour</AssemblyName>
<RootNamespace>Wacton.Unicolour</RootNamespace>
<PackageId>$(AssemblyName)</PackageId>
<Authors>William Acton</Authors>
<Description>🌈 Colour / Color conversion, interpolation, and comparison for .NET</Description>
<Copyright>William Acton</Copyright>
<PackageProjectUrl>https://github.com/waacton/Unicolour</PackageProjectUrl>
<RepositoryUrl>https://github.com/waacton/Unicolour</RepositoryUrl>
<LangVersion>Preview</LangVersion>
<TargetFramework>NetStandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageIcon>Resources\Unicolour.png</PackageIcon>
<PackageVersion>3.0.0</PackageVersion>
<PackageTags>colour color RGB HSB HSV HSL HWB XYZ xyY LAB LUV LCH LCHab LCHuv HSLuv HPLuv ICtCp JzAzBz JzCzHz Oklab Oklch CAM02 CAM16 HCT converter colour-converter colour-conversion color-converter color-conversion colour-space colour-spaces color-space color-spaces interpolation colour-interpolation color-interpolation colour-mixing color-mixing comparison colour-comparison color-comparison contrast luminance deltaE chromaticity display-p3 rec-2020 gamut-mapping temperature cct duv cvd colour-vision-deficiency color-vision-deficiency colour-blindness color-blindness protanopia deuteranopia tritanopia achromatopsia</PackageTags>
<PackageReleaseNotes>Add gamut mapping, support premultiplied alpha interpolation, and improve API</PackageReleaseNotes>
<ApplicationIcon>Resources\Unicolour.ico</ApplicationIcon>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Update="Resources\Unicolour.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

80
Unicolour/WhitePoint.cs Normal file
View File

@@ -0,0 +1,80 @@
using System.Collections.Generic;
namespace Wacton.Unicolour
{
public record WhitePoint(double X, double Y, double Z)
{
public double X { get; } = X;
public double Y { get; } = Y;
public double Z { get; } = Z;
internal Matrix AsXyzMatrix() => Matrix.FromTriplet(X, Y, Z).Select(x => x / 100.0);
public override string ToString() => $"({X}, {Y}, {Z})";
public static WhitePoint From(Illuminant illuminant, Observer observer = Observer.Standard2)
{
return ByIlluminant[observer][illuminant];
}
public static WhitePoint From(Chromaticity chromaticity)
{
var xyz = Xyy.ToXyz(new(chromaticity.X, chromaticity.Y, 1));
return new WhitePoint(xyz.X * 100, xyz.Y * 100, xyz.Z * 100);
}
// as far as I'm aware, these are the latest ASTM standards
private static readonly Dictionary<Observer, Dictionary<Illuminant, WhitePoint>> ByIlluminant = new()
{
{
Observer.Standard2, new()
{
{ Illuminant.A, new(109.850, 100.000, 35.585) },
{ Illuminant.C, new(98.074, 100.000, 118.232) },
{ Illuminant.D50, new(96.422, 100.000, 82.521) },
{ Illuminant.D55, new(95.682, 100.000, 92.149) },
{ Illuminant.D65, new(95.047, 100.000, 108.883) },
{ Illuminant.D75, new(94.972, 100.000, 122.638) },
{ Illuminant.E, new(100.000, 100.000, 100.000) },
{ Illuminant.F2, new(99.186, 100.000, 67.393) },
{ Illuminant.F7, new(95.041, 100.000, 108.747) },
{ Illuminant.F11, new(100.962, 100.000, 64.350) }
}
},
{
Observer.Supplementary10, new()
{
{ Illuminant.A, new(111.144, 100.000, 35.200) },
{ Illuminant.C, new(97.285, 100.000, 116.145) },
{ Illuminant.D50, new(96.720, 100.000, 81.427) },
{ Illuminant.D55, new(95.799, 100.000, 90.926) },
{ Illuminant.D65, new(94.811, 100.000, 107.304) },
{ Illuminant.D75, new(94.416, 100.000, 120.641) },
{ Illuminant.E, new(100.000, 100.000, 100.000) },
{ Illuminant.F2, new(103.279, 100.000, 69.027) },
{ Illuminant.F7, new(95.792, 100.000, 107.686) },
{ Illuminant.F11, new(103.863, 100.000, 65.607) }
}
}
};
}
public enum Illuminant
{
A,
C,
D50,
D55,
D65,
D75,
E,
F2,
F7,
F11
}
public enum Observer
{
Standard2,
Supplementary10
}
}

65
Unicolour/Xyy.cs Normal file
View File

@@ -0,0 +1,65 @@
using System;
namespace Wacton.Unicolour
{
public record Xyy : ColourRepresentation
{
protected override int? HueIndex => null;
public Chromaticity Chromaticity => new(First, Second);
public double Luminance => Third;
public Chromaticity ConstrainedChromaticity => new(ConstrainedFirst, ConstrainedSecond);
public double ConstrainedLuminance => ConstrainedThird;
protected override double ConstrainedFirst => Math.Max(Chromaticity.X, 0);
protected override double ConstrainedSecond => Math.Max(Chromaticity.Y, 0);
protected override double ConstrainedThird => Math.Max(Luminance, 0);
// could compare chromaticity against config.ChromaticityWhite
// but requires making assumptions about floating-point comparison, which I don't want to do
internal override bool IsGreyscale => Luminance <= 0.0;
public Xyy(double x, double y, double upperY) : this(x, y, upperY, ColourHeritage.None)
{
}
internal Xyy(double x, double y, double upperY, ColourHeritage heritage) : base(x, y, upperY, heritage)
{
}
protected override string FirstString => $"{Chromaticity.X:F4}";
protected override string SecondString => $"{Chromaticity.Y:F4}";
protected override string ThirdString => $"{Luminance:F4}";
public override string ToString() => base.ToString();
/*
* XYY is a transform of XYZ (in terms of Unicolour implementation)
* Forward: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
* Reverse: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
*/
internal static Xyy FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
{
var (x, y, z) = xyz.Triplet;
var normalisation = x + y + z;
var isBlack = normalisation == 0.0;
var chromaticityX = isBlack ? xyzConfig.ChromaticityWhite.X : x / normalisation;
var chromaticityY = isBlack ? xyzConfig.ChromaticityWhite.Y : y / normalisation;
var luminance = isBlack ? 0 : y;
return new Xyy(chromaticityX, chromaticityY, luminance, ColourHeritage.From(xyz));
}
internal static Xyz ToXyz(Xyy xyy)
{
var chromaticity = xyy.ConstrainedChromaticity;
var luminance = xyy.ConstrainedLuminance;
var useZero = chromaticity.Y <= 0;
var factor = luminance / chromaticity.Y;
var x = useZero ? 0 : factor * chromaticity.X;
var y = useZero ? 0 : luminance;
var z = useZero ? 0 : factor * (1 - chromaticity.X - chromaticity.Y);
return new Xyz(x, y, z, ColourHeritage.From(xyy));
}
}
}

42
Unicolour/Xyz.cs Normal file
View File

@@ -0,0 +1,42 @@
namespace Wacton.Unicolour
{
public record Xyz : ColourRepresentation
{
protected override int? HueIndex => null;
public double X => First;
public double Y => Second;
public double Z => Third;
// no clear luminance upper-bound; usually Y >= 1 is max luminance
// but since custom white points can be provided, don't want to make the assumption
internal override bool IsGreyscale => Y <= 0;
public Xyz(double x, double y, double z) : this(x, y, z, ColourHeritage.None)
{
}
internal Xyz(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
triplet.Third, heritage)
{
}
internal Xyz(double x, double y, double z, ColourHeritage heritage) : base(x, y, z, heritage)
{
}
protected override string FirstString => $"{X:F4}";
protected override string SecondString => $"{Y:F4}";
protected override string ThirdString => $"{Z:F4}";
public override string ToString() => base.ToString();
/*
* XYZ is considered the root colour representation (in terms of Unicolour implementation)
* so does not contain any forward (from another space) or reverse (back to original space) functions
*/
// only for potential debugging or diagnostics
// until there is an "official" HCT -> XYZ reverse transform
internal HctToXyzSearchResult? HctToXyzSearchResult;
}
}

View File

@@ -0,0 +1,25 @@
namespace Wacton.Unicolour
{
public class XyzConfiguration
{
public WhitePoint WhitePoint { get; }
public Chromaticity ChromaticityWhite { get; }
public static readonly XyzConfiguration D65 = new(WhitePoint.From(Illuminant.D65));
public static readonly XyzConfiguration D50 = new(WhitePoint.From(Illuminant.D50));
public XyzConfiguration(WhitePoint whitePoint)
{
WhitePoint = whitePoint;
var x = WhitePoint.X / 100.0;
var y = WhitePoint.Y / 100.0;
var z = WhitePoint.Z / 100.0;
var normalisation = x + y + z;
ChromaticityWhite = new(x / normalisation, y / normalisation);
}
public override string ToString() => $"XYZ {WhitePoint}";
}
}