/******************************************************************************/
/* sRGB.cls - A Standard RGB class */
/* =============================== */
/* */
/* This program is part of the Rexx Parser package */
/* [See https://rexx.epbcn.com/rexx-parser/] */
/* */
/* Copyright (c) 2025 Josep Maria Blasco <josep.maria.blasco@epbcn.com> */
/* */
/* NOTICE: This program is distributed as part of the Rexx Parser package, */
/* but it can be used independently. */
/* */
/* This program provides a sRGB class that encapsulates a small part of */
/* the CSS Color Module Level 4 definition of colours. See */
/* https://www.w3.org/TR/css-color-4/ for details. */
/* */
/* sRGB colors can be created from three-, four-, six- or eight-digits */
/* hexadecimal strings, or by providing three blank-separated values, */
/* which can be percentages ended with a "%" character, between 0 and 100, */
/* or (non-necessarily integer) numbers between 0 and 255, optionally */
/* followed by a slash and an alpha value between 0 and 1, or a percentage. */
/* */
/* The class provides conversions to: */
/* + Linear RGB */
/* + CIE XYZ */
/* + OKLab */
/* and a DeltaEOK function returning the distance between any two sRGB points */
/* as measured in the OKLab color space. */
/* */
/* License: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0) */
/* */
/* Version history: */
/* */
/* Date Version Details */
/* -------- ------- --------------------------------------------------------- */
/* 20250621 0.2c First version */
/* 20250622 0.2d Add support for alpha values */
/* */
/******************************************************************************/
--------------------------------------------------------------------------------
-- Work at the maximum precision that RxMath allows (assume 64 bits), plus --
-- two digits for rounding --
--------------------------------------------------------------------------------
::Options Digits 18
--------------------------------------------------------------------------------
-- Provide nice names for the RxMath functions we will be using --
--------------------------------------------------------------------------------
::Routine exp External "LIBRARY rxmath RxCalcExp"
::Routine log External "LIBRARY rxmath RxCalcLog"
::Routine power External "LIBRARY rxmath RxCalcPower"
::Routine sqrt External "LIBRARY rxmath RxCalcSqrt"
--------------------------------------------------------------------------------
-- CLASS sRGB --
--------------------------------------------------------------------------------
---
--- Represents a Standard RGB (sRGB) color value
---
--- Please note that sRGB is a _non-linear_ color space.
---
::Class sRGB Public
--------------------------------------------------------------------------------
-- METHOD INIT --
--------------------------------------------------------------------------------
---
--- Creates a sRGB color value
---
--- @param r_g_b_a Three blank-separated primaries, optionally followed by a
-- slash and an alpha value, or a single three, four, six or eight
--- characters hexadecimal value, with an optional leading "#" character.
---
--- When providing blank-separated primaries, each primary can be specified
--- as a number between 0 and 255, or as a percentage between 0 and 100.
---
--- Alpha values default to 1 (100%), and they can be specified
--- as a number between 0 and 1, or as a percentage between 0 and 100.
---
--- Non-integers and exponentials are accepted.
---
::Method init
Expose R G B A
A = 1 -- Default is no transparency
Use Strict Arg r_g_b_a
If Pos("/", r_g_b_a) > 1 Then Do
Parse Var r_g_b_a r_g_b "/" A
End
Else r_g_b = r_g_b_a
Select Case Words(r_g_b)
When 1 Then Signal Hex
When 3 Then Signal Three
Otherwise Signal OneOrThree
End
Three:
Parse Var r_g_b R G B .
-- Validate and construct R, G and B
Call Validate >R, "Red"
Call Validate >G, "Green"
Call Validate >B, "Blue"
Call ValidateAlpha
Return
Validate: Procedure
Use Arg >value, Color
If right(value,1) == "%" Then Do
val = Left(value,Length(value)-1)
If DataType(val) \== "NUM" Then Signal NoNumber
If val < 0 Then Signal Negative
If val > 100 Then Signal TooBig
value = val / 100
Return
End
If DataType(value) \== "NUM" Then Signal NoNumber
If value < 0 Then Signal Negative
If value > 255 Then Signal TooBig
value = value / 255
Return
ValidateAlpha:
If right(A,1) == "%" Then Do
val = Left(A,Length(A)-1)
If DataType(val) \== "NUM" Then Signal AlphaNoNumber
If val < 0 Then Signal AlphaNegative
If val > 100 Then Signal AlphaTooBig
A = val / 100
Return
End
val = A
If DataType(A) \== "NUM" Then Signal AlphaNoNumber
If A < 0 Then Signal AlphaNegative
If A > 1 Then Signal AlphaTooBig
Return
Hex:
hex = r_g_b_a
If hex[1] == "#" Then hex = SubStr(hex,2)
If \DataType(hex,"X") Then Signal BadHex
A = "FF"
Select Case Length(hex)
When 6 Then Parse Var hex r +2 g +2 b
When 8 Then Parse Var hex r +2 g +2 b +2 a
When 3 Then Do
Parse Var hex r +1 g +1 b
R = R || R
G = G || G
B = B || B
End
When 3 Then Do
Parse Var hex r +1 g +1 b +1 a
R = R || R
G = G || G
B = B || B
A = A || A
End
Otherwise Signal BadHex
End
R = R~x2d / 255
G = G~x2d / 255
B = B~x2d / 255
A = A~x2d / 255
Return
AlphaNoNumber: Call 93.900Alpha "a number or a percentage"
AlphaNegative: Call 93.900Alpha "zero or positive"
AlphaTooBig: Call 93.900Alpha -
"smaller than or equal to 1 (or 100 if specified as a percentage)"
BadHex: Raise Syntax 93.900 -
Array( "Argument should be a 3-. 4-. 6- or 8-digit hexadecimal number, optionally" -
"preceded by a '#' character. Found '"r_g_b_a"'" )
OneOrThree: Raise Syntax 93.900 -
Array( "One or three words expected, found '"r_g_b_a"'." )
NoNumber: Call 93.900 "primary should be a number or a percentage"
Negative: Call 93.900 "primary should be zero or positive"
TooBig: Call 93.900 "primary should be smaller or equal to 255" -
"(or 100 if specified as a percentage)"
93.900:
Raise Syntax 93.900 Array(Color Arg(1)", found '"value"'")
93.900Alpha:
Raise Syntax 93.900 Array( "Alpha should be" Arg(1)", found '"val"'" )
--------------------------------------------------------------------------------
-- METHOD MAKESTRING --
--------------------------------------------------------------------------------
---
--- Converts to a string with a certain precision (default is 2)
---
--- @param precision Optional precision, between 0 and 18.
---
--- @return A string containing the blank separated R, G, and B values,
--- formatted to the specified precision.
---
::Method makeString Public
Expose R G B A
Use Strict Arg precision = 2
If \DataType(precision, "W") Then Raise Syntax 93.905 Array(1, precision)
If precision < 0 Then Raise Syntax 93.906 Array(1, precision)
If precision > 18 Then Raise Syntax 93.908 Array(1, 18, precision)
Return P(R) P(G) P(B) "/" PA()"%"
P: Return Format( Arg(1) * 255,,precision)
PA: Return Format( A * 100,,precision)
--------------------------------------------------------------------------------
-- METHOD TOLINEAR --
--------------------------------------------------------------------------------
---
--- Converts a sRGB value to linear RGB
---
--- @return A string containing the linearized R, G and B primaries.
---
::Method toLinear Public
Expose R G B
Return Linear(R) Linear(G) Linear(B)
Linear:
Use Arg primary
If primary <= 0.04045 Then Return primary / 12.92
t = (primary + 0.055) / 1.055
Return Power( t, 2.4 )
--------------------------------------------------------------------------------
-- METHOD TOOKLAB --
--------------------------------------------------------------------------------
---
--- Converts a sRGB value to Oklab
---
--- @return The L, a, b components of the OKLab conversion
---
--- See function XYZ_to_OKLab(XYZ) in https://www.w3.org/TR/css-color-4/
---
::Method toOKlab Public
-- Transform sRGB to CIE XYZ (D65)
Parse Value self~toXYZ With X Y Z
-- Transform to LMS
L = 0.8190224379967030*X + 0.3619062600528904*Y -0.1288737815209879*Z
M = 0.0329836539323885*X + 0.9292868615863434*Y + 0.0361446663506424*Z
S = 0.0481771893596242*X + 0.2642395317527308*Y + 0.6335478284694309*Z
Lc = Power( L, 1/3 )
Mc = Power( M, 1/3 )
Sc = Power( S, 1/3 )
Return -
0.2104542683093140*Lc + 0.7936177747023054*Mc - 0.0040720430116193*Sc -
1.9779985324311684*Lc - 2.4285922420485799*Mc + 0.4505937096174110*Sc -
0.0259040424655478*Lc + 0.7827717124575296*Mc - 0.8086757549230774*Sc
--------------------------------------------------------------------------------
-- METHOD TOXYZ --
--------------------------------------------------------------------------------
---
--- Convert sRGB to CIE XYZ using the D65 illuminant
---
--- @return A string containing the blank-separated X, Y and Z values
---
::Method toXYZ Public
------------------------------------------------------------------------------
-- Transform sRGB to linear RGB --
------------------------------------------------------------------------------
Parse Value self~toLinear With R G B
------------------------------------------------------------------------------
-- Convert linear RGB to XYZ --
------------------------------------------------------------------------------
-- See function lin_sRGB_to_XYZ(rgb) in https://www.w3.org/TR/css-color-4/
X = 506752 * R / 1228815 + 87881 * G / 245763 + 12673 * B / 70218
Y = 87098 * R / 409605 + 175762 * G / 245763 + 12673 * B / 175545
Z = 7918 * R / 409605 + 87881 * G / 737289 + 1001167 * B / 1053270
Return X Y Z
--------------------------------------------------------------------------------
-- METHOD DELTAEOK --
--------------------------------------------------------------------------------
---
--- Returns the distance between two sRGB values, as measured in the OKLab
--- color space
---
--- @param aColor The color to compare with
--- @return The OKLab DeltaE distance between this color and aColor
--- Experimentation shows that this distance ranges between 0 and 1 when
--- operating inside the sRGB color cube.
---
--- See https://www.w3.org/TR/css-color-4/#color-difference-OK
---
::Method DeltaEOK Public
Use Strict Arg aColor
.Validate~classType("aColor", aColor, self~class)
Parse Value self ~toOKLab With L1 a1 b1
Parse Value aColor~toOKLab With L2 a2 b2
Return sqrt( (L1-L2)**2 + (a1-a2)**2 + (b1-b2)**2 )