Math: add epsilon compare for float, update Color Coordinate calls

Math has a compare with epsilon for float numbers.

Use this for fixing sligth color conversion issues.

NOTE: this might need some adjustment over time

All phpunint tests written and checked
This commit is contained in:
Clemens Schwaighofer
2024-11-15 18:13:16 +09:00
parent 3845bc7ff5
commit a9f1d878f7
15 changed files with 1246 additions and 152 deletions

View File

@@ -16,6 +16,8 @@ final class CoreLibsConvertColorTest extends TestCase
{
// 12 precision allowed, RGB and back has a lot of float imprecisions here
private const DELTA = 0.000000000001;
// rgb to oklab and back will have slight shift
private const DELTA_OKLAB = 1.05;
// sRGB base convert test, should round around and come out the same
// use for RGB 0, 0, 0 in 60 steps and then max 255
@@ -32,10 +34,12 @@ final class CoreLibsConvertColorTest extends TestCase
);
}
// MARK: single test
public function testSingle()
{
$this->assertTrue(true, 'Single test');
// $rgb = Color\Coordinates\RGB::__constructFromArray([0, 0, 60]);
// $rgb = new Color\Coordinates\RGB([0, 0, 60]);
// print "IN: " . print_r($rgb, true) . "\n";
// $hsl = Color\Color::rgbToHsl($rgb);
// print "to HSL: " . print_r($hsl, true) . "\n";
@@ -51,7 +55,7 @@ final class CoreLibsConvertColorTest extends TestCase
// $rgb_r = Color\Color::hslToRgb($hsl_r);
// print "R to RGB: " . print_r($rgb_r, true) . "\n";
// $hsl = Color\Coordinates\HSL::__constructFromArray([0, 0, 0]);
// $hsl = new Color\Coordinates\HSL([0, 0, 0]);
// print "IN HSL: " . print_r($hsl, true) . "\n";
// $hsb = Color\Color::hslToHsb($hsl);
// print "to HSB: " . print_r($hsb, true) . "\n";
@@ -63,7 +67,7 @@ final class CoreLibsConvertColorTest extends TestCase
// $hsl_r = Color\Color::hsbToHsl($hsb_r);
// print "R to HSL: " . print_r($hsl_r, true) . "\n";
// print "--------\n";
// $hsb = Color\Coordinates\HSB::__constructFromArray([0, 20, 0]);
// $hsb = new Color\Coordinates\HSB([0, 20, 0]);
// print "IN HSB: " . print_r($hsb, true) . "\n";
// $hsl = Color\Color::hsbToHsl($hsb);
// print "to HSL: " . print_r($hsl, true) . "\n";
@@ -75,7 +79,7 @@ final class CoreLibsConvertColorTest extends TestCase
// $hsb_r = Color\Color::hslToHsb($hsl_r);
// print "R to HSL: " . print_r($hsb_r, true) . "\n";
// print "--------\n";
// $hwb = Color\Coordinates\HWB::__constructFromArray([0, 20, 100]);
// $hwb = new Color\Coordinates\HWB([0, 20, 100]);
// print "IN: " . print_r($hwb, true) . "\n";
// $hsl = Color\Color::hwbToHsl($hwb);
// print "to HSL: " . print_r($hsl, true) . "\n";
@@ -87,6 +91,8 @@ final class CoreLibsConvertColorTest extends TestCase
// print "HSL to HWB: " . print_r($hwb_r, true) . "\n";
}
// MARK: RGB base
/**
* From/To RGB <-> ... conversion tests
*
@@ -116,7 +122,7 @@ final class CoreLibsConvertColorTest extends TestCase
$b = 255;
}
// base is always the same
$color = Color\Coordinates\RGB::__constructFromArray([$r, $g, $b]);
$color = new Color\Coordinates\RGB([$r, $g, $b]);
$base = 'rgb';
foreach (['hsb', 'hsl', 'hwb'] as $coord) {
// print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n";
@@ -145,6 +151,8 @@ final class CoreLibsConvertColorTest extends TestCase
// HSB: saturation or brightness 0
// HWB: blackness >= 80 and whitness >= 20 or B>=20 & W>=20 or B>=50 & W>=50
// MARK: HSL base
/**
* Undocumented function
*
@@ -165,7 +173,7 @@ final class CoreLibsConvertColorTest extends TestCase
if (($L == 0 or $L == 100)) {
continue;
}
$color = Color\Coordinates\HSL::__constructFromArray([$H, $S, $L]);
$color = new Color\Coordinates\HSL([$H, $S, $L]);
$base = 'hsl';
foreach (['hsb', 'hwb', 'rgb'] as $coord) {
// for rgb hue on S = 0 is irrelevant (B/W)
@@ -190,6 +198,8 @@ final class CoreLibsConvertColorTest extends TestCase
}
}
// MARK: HSB
/**
* Undocumented function
*
@@ -210,7 +220,7 @@ final class CoreLibsConvertColorTest extends TestCase
if ($S == 0 or $B == 0) {
continue;
}
$color = Color\Coordinates\HSB::__constructFromArray([$H, $S, $B]);
$color = new Color\Coordinates\HSB([$H, $S, $B]);
$base = 'hsb';
foreach (['hwb', 'hsl', 'rgb'] as $coord) {
$target = $base . 'To' . ucfirst($coord);
@@ -231,6 +241,7 @@ final class CoreLibsConvertColorTest extends TestCase
}
}
// MARK: HWB
/**
* Undocumented function
@@ -258,7 +269,7 @@ final class CoreLibsConvertColorTest extends TestCase
continue;
}
$base = 'hwb';
$color = Color\Coordinates\HWB::__constructFromArray([$H, $W, $B]);
$color = new Color\Coordinates\HWB([$H, $W, $B]);
foreach (['hsl', 'hsb', 'rgb'] as $coord) {
// for rgb hue on S = 0 is irrelevant (B/W)
if ($H > 0 && $coord == 'rgb') {
@@ -282,12 +293,13 @@ final class CoreLibsConvertColorTest extends TestCase
}
}
// MARK: RGB to hex
/**
* Undocumented function
*
* covers ::returnAsHex()
* covers ::__constructFromHexString()
* @testdox Convert from RGB to hex and back
* @covers ::returnAsHex()
* @testdox Convert from and to RGB via hex
*
* @return void
*/
@@ -308,10 +320,10 @@ final class CoreLibsConvertColorTest extends TestCase
}
// with or without prefix
foreach ([true, false] as $hex_prefix) {
$hex_color = Color\Coordinates\RGB::__constructFromArray([$r, $g, $b])
$hex_color = (new Color\Coordinates\RGB([$r, $g, $b]))
->returnAsHex($hex_prefix);
// parse into hex to rgb and see if we get the same r/g/b
$color = Color\Coordinates\RGB::__constructFromHexString($hex_color)->returnAsArray();
$color = (new Color\Coordinates\RGB($hex_color))->returnAsArray();
//
$this->assertEquals(
[$r, $g, $b],
@@ -325,20 +337,849 @@ final class CoreLibsConvertColorTest extends TestCase
}
}
// oklab
// MARK: RGB Linear
// cie lab
// create exceptions for all color spaces
public function testExceptionHSB(): void
/**
* linear RGB conversion tests
*
* @covers ::fromLinear
* @covers ::toLinear
* @testdox Convert from and to RGB linear conversion check
*
* @return void
*/
public function testRgbFromToLinear()
{
$rgb = (new Color\Coordinates\RGB([10, 20, 30]))->toLinear();
$this->assertEquals(
true,
$rgb->linear,
'On create flagged linear missing'
);
$rgb_color = $rgb->returnAsArray();
$rgb->toLinear();
$this->assertEquals(
$rgb_color,
$rgb->returnAsArray(),
'Double linear call does double linear encoding'
);
$rgb->fromLinear();
$this->assertEquals(
false,
$rgb->linear,
'On reverse linear, flag is missing'
);
$rgb_color = $rgb->returnAsArray();
$this->assertEquals(
$rgb_color,
$rgb->returnAsArray(),
'Double linear inverse call does double linear decoding'
);
$rgb = new Color\Coordinates\RGB([20, 30, 40]);
$rgb_color = $rgb->returnAsArray();
$this->assertEquals(
false,
$rgb->linear,
'On create without linear flag is linear'
);
$rgb->toLinear();
$this->assertEquals(
true,
$rgb->linear,
'On linear call flag is not linear'
);
$rgb->fromLinear();
$this->assertEquals(
$rgb_color,
$rgb->returnAsArray(),
'conversion to and from linear not matching'
);
}
// MARK: okLab
/**
* From/To RGB <-> OkLab / OkLch
*
* @covers ::rgbToOkLab
* @covers ::rgbToOkLch
* @covers ::okLabToRgb
* @covers ::okLchToRgb
* @testdox Convert from and to RGB to OkLab / OkLch
*
* @return void
*/
public function testRgbColorCoordinateConvertToAndBackBackOkLab()
{
for ($r = 0; $r <= 300; $r += 60) {
for ($g = 0; $g <= 300; $g += 60) {
for ($b = 0; $b <= 300; $b += 60) {
// for this test we stay in the correct lane
if ($r > 255) {
$r = 255;
}
if ($g > 255) {
$g = 255;
}
if ($b > 255) {
$b = 255;
}
// base is always the same
$color = new Color\Coordinates\RGB([$r, $g, $b]);
$base = 'rgb';
foreach (['okLab', 'okLch'] as $coord) {
// print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n";
// rgb to X and back must be same
$target = $base . 'To' . ucfirst($coord);
$source = $coord . 'To' . ucfirst($base);
$converted_color = Color\Color::$target($color);
$color_b = Color\Color::$source($converted_color);
// $converted_color = Color\Color::rgbToHsb($color);
// $rgb_b = Color\Color::hsbToRgb($converted_color);
$this->assertEqualsWithDelta(
$color->returnAsArray(),
$color_b->returnAsArray(),
self::DELTA_OKLAB,
'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/'
. print_r($color_b->returnAsArray(), true)
);
}
}
}
}
}
/**
* internal oklab/oklch conversion
*
* @covers ::okLchToOkLab
* @covers ::okLabToOkLch
* @testdox Convert from and to OkLab / OkLch
*
* @return void
*/
public function testOkLabOkLchColorCoordinateConvertToFrom()
{
for ($L = 0.0; $L <= 1.0; $L += 0.2) {
for ($C = 0.0; $C <= 0.5; $C += 0.1) {
for ($H = 0.0; $H <= 360.0; $H += 60.0) {
// chroma 0.0 is B/W skip it
if ($C == 0.0) {
continue;
}
$color = new Color\Coordinates\LCH([$L, $C, $H], 'OkLab');
$base = 'okLch';
foreach (['okLab'] as $coord) {
// rgb to X and back must be same
$target = $base . 'To' . ucfirst($coord);
$source = $coord . 'To' . ucfirst($base);
$converted_color = Color\Color::$target($color);
$color_b = Color\Color::$source($converted_color);
// $converted_color = Color\Color::rgbToHsb($color);
// $rgb_b = Color\Color::hsbToRgb($converted_color);
$this->assertEqualsWithDelta(
$color->returnAsArray(),
$color_b->returnAsArray(),
self::DELTA,
'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/'
. print_r($color_b->returnAsArray(), true)
);
}
}
}
}
}
// MARK: CIELab
/**
* From/To RGB <-> Cie lab / Cie lch
*
* @covers ::rgbToLab
* @covers ::rgbToLch
* @covers ::labToRgb
* @covers ::lchToRgb
* @testdox Convert from and to RGB to Cie Lab / Cie Lch
*
* @return void
*/
public function testRgbColorCoordinateConvertToAndBackBackCieLab()
{
for ($r = 0; $r <= 300; $r += 60) {
for ($g = 0; $g <= 300; $g += 60) {
for ($b = 0; $b <= 300; $b += 60) {
// for this test we stay in the correct lane
if ($r > 255) {
$r = 255;
}
if ($g > 255) {
$g = 255;
}
if ($b > 255) {
$b = 255;
}
// base is always the same
$color = new Color\Coordinates\RGB([$r, $g, $b]);
$base = 'rgb';
foreach (['lab', 'lch'] as $coord) {
// print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n";
// rgb to X and back must be same
$target = $base . 'To' . ucfirst($coord);
$source = $coord . 'To' . ucfirst($base);
$converted_color = Color\Color::$target($color);
$color_b = Color\Color::$source($converted_color);
// $converted_color = Color\Color::rgbToHsb($color);
// $rgb_b = Color\Color::hsbToRgb($converted_color);
$this->assertEqualsWithDelta(
$color->returnAsArray(),
$color_b->returnAsArray(),
self::DELTA_OKLAB,
'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/'
. print_r($color_b->returnAsArray(), true)
);
}
}
}
}
}
/**
* internal cie lab/cie lch conversion
*
* @covers ::lchToLab
* @covers ::labToLch
* @testdox Convert from and to Cie Lab / Cie Lch
*
* @return void
*/
public function testLabLchColorCoordinateConvertToFrom()
{
for ($L = 0.0; $L <= 1.0; $L += 0.2) {
for ($C = 0.0; $C <= 0.5; $C += 0.1) {
for ($H = 0.0; $H <= 360.0; $H += 60.0) {
// chroma 0.0 is B/W skip it
if ($C == 0.0) {
continue;
}
$color = new Color\Coordinates\LCH([$L, $C, $H], 'OkLab');
$base = 'lch';
foreach (['lab'] as $coord) {
// rgb to X and back must be same
$target = $base . 'To' . ucfirst($coord);
$source = $coord . 'To' . ucfirst($base);
$converted_color = Color\Color::$target($color);
$color_b = Color\Color::$source($converted_color);
// $converted_color = Color\Color::rgbToHsb($color);
// $rgb_b = Color\Color::hsbToRgb($converted_color);
$this->assertEqualsWithDelta(
$color->returnAsArray(),
$color_b->returnAsArray(),
self::DELTA,
'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/'
. print_r($color_b->returnAsArray(), true)
);
}
}
}
}
}
// MARK: Exceptions
/**
* Undocumented function
*
* @return array
*/
public function providerHueBased(): array
{
// all HSB/V HSL HWB have the same value range, create test data for all of them
return [
'H' => [
'color' => [900, 10, 10],
'error_code' => 1,
'error_string' => '/ for hue is not in the range of 0 to 360$/'
],
'H' => [
'color' => [-1, 10, 10], 'error_code' => 1,
'error_string' => '/ for hue is not in the range of 0 to 360$/',
],
'H close' => [
'color' => [360.1, 10, 10],
'error_code' => 1,
'error_string' => '/ for hue is not in the range of 0 to 360$/'
],
'H close' => [
'color' => [-0.1, 10, 10], 'error_code' => 1,
'error_string' => '/ for hue is not in the range of 0 to 360$/',
],
'S/W' => [
'color' => [90, 900, 10], 'error_code' => 2,
'error_string' => 'is not in the range of 0 to 100',
],
'S/W' => [
'color' => [90, -1, 10], 'error_code' => 2,
'error_string' => 'is not in the range of 0 to 100',
],
'S/W close' => [
'color' => [90, 100.1, 10], 'error_code' => 2,
'error_string' => 'is not in the range of 0 to 100',
],
'S/W close' => [
'color' => [90, -0.1, 10], 'error_code' => 2,
'error_string' => 'is not in the range of 0 to 100',
],
'L/B' => [
'color' => [90, 10, 900], 'error_code' => 3,
'error_string' => 'is not in the range of 0 to 100',
],
'L/B' => [
'color' => [90, 10, -1], 'error_code' => 3,
'error_string' => 'is not in the range of 0 to 100',
],
'L/B close' => [
'color' => [90, 10, 100.1], 'error_code' => 3,
'error_string' => 'is not in the range of 0 to 100',
],
'L/B close' => [
'color' => [90, 10, -0.1], 'error_code' => 3,
'error_string' => 'is not in the range of 0 to 100',
],
];
}
// MARK: HSB Exceptions
/**
* Undocumented function
*
* @dataProvider providerHueBased
* @testdox Exception handling for HSB for error $error_code [$_dataName]
*
* @param array $color
* @param int $error_code
* @param string $error_string
* @return void
*/
public function testExceptionHSB(array $color, int $error_code, string $error_string): void
{
// error string based on code
switch ($error_code) {
case 2:
$error_string = "/ for saturation $error_string$/";
break;
case 3:
$error_string = "/ for brightness $error_string$/";
break;
}
// for H/S/B exception the same
$this->expectException(\LengthException::class);
Color\Coordinates\HSB::__constructFromArray([900, 10, 10]);
$this->expectExceptionCode($error_code);
$this->expectExceptionMessageMatches($error_string);
new Color\Coordinates\HSB($color);
}
/**
* Undocumented function
*
* @testdox Exception handling for HSB general calls
*
* @return void
*/
public function testExceptionHSBGeneral()
{
// allow
$b = new Color\Coordinates\HSB([0, 0, 0], 'sRGB');
// invalid access to class
// $this->expectException(\ErrorException::class);
$b = new Color\Coordinates\HSB([0, 0, 0]);
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->g;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Only array colors allowed");
new Color\Coordinates\HSB('string');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\HSB([0, 0, 0], 'FOO_BAR');
}
// MARK: HSL Exceptions
/**
* Undocumented function
*
* @dataProvider providerHueBased
* @testdox Exception handling for HSL for error $error_code [$_dataName]
*
* @param array $color
* @param int $error_code
* @param string $error_string
* @return void
*/
public function testExceptionHSL(array $color, int $error_code, string $error_string): void
{
// error string based on code
switch ($error_code) {
case 2:
$error_string = "/ for saturation $error_string$/";
break;
case 3:
$error_string = "/ for lightness $error_string$/";
break;
}
// for H/S/B exception the same
$this->expectException(\LengthException::class);
$this->expectExceptionCode($error_code);
$this->expectExceptionMessageMatches($error_string);
new Color\Coordinates\HSL($color);
}
/**
* Undocumented function
*
* @testdox Exception handling for HSL general calls
*
* @return void
*/
public function testExceptionHSLGeneral()
{
// allow
$b = new Color\Coordinates\HSL([0, 0, 0], 'sRGB');
// invalid access to class
$b = new Color\Coordinates\HSL([0, 0, 0]);
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->g;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Only array colors allowed");
new Color\Coordinates\HSL('string');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\HSL([0, 0, 0], 'FOO_BAR');
}
// MARK: HWB Exceptions
/**
* Undocumented function
*
* @dataProvider providerHueBased
* @testdox Exception handling for HWB for error $error_code [$_dataName]
*
* @param array $color
* @param int $error_code
* @param string $error_string
* @return void
*/
public function testExceptionHWB(array $color, int $error_code, string $error_string): void
{
// error string based on code
switch ($error_code) {
case 2:
$error_string = "/ for whiteness $error_string$/";
break;
case 3:
$error_string = "/ for blackness $error_string$/";
break;
}
// for H/S/B exception the same
$this->expectException(\LengthException::class);
$this->expectExceptionCode($error_code);
$this->expectExceptionMessageMatches($error_string);
new Color\Coordinates\HWB($color);
}
/**
* Undocumented function
*
* @testdox Exception handling for HWB general calls
*
* @return void
*/
public function testExceptionHWBGeneral()
{
// allow
$b = new Color\Coordinates\HWB([0, 0, 0], 'sRGB');
// invalid access to class
$b = new Color\Coordinates\HWB([0, 0, 0]);
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->g;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Only array colors allowed");
new Color\Coordinates\HWB('string');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\HWB([0, 0, 0], 'FOO_BAR');
}
// MARK: RGB Exceptions
/**
* Undocumented function
*
* @return array
*/
public function providerRgbBased(): array
{
// all HSB/V HSL HWB have the same value range, create test data for all of them
return [
'R' => [
'color' => [900, 10, 10],
'error_code' => 1,
'error_string' => '/ is not in the range of 0 to 255$/',
'linear' => false,
],
'R' => [
'color' => [-1, 10, 10], 'error_code' => 1,
'error_string' => '/ is not in the range of 0 to 255$/',
'linear' => false,
],
'G' => [
'color' => [90, 900, 10], 'error_code' => 1,
'error_string' => '/ is not in the range of 0 to 255$/',
'linear' => false,
],
'G' => [
'color' => [90, -1, 10], 'error_code' => 1,
'error_string' => '/ is not in the range of 0 to 255$/',
'linear' => false,
],
'B' => [
'color' => [90, 10, 900], 'error_code' => 1,
'error_string' => '/ is not in the range of 0 to 255$/',
'linear' => false,
],
'B' => [
'color' => [90, 10, -1], 'error_code' => 1,
'error_string' => '/ is not in the range of 0 to 255$/',
'linear' => false,
],
'R linear' => [
'color' => [2, 0.5, 0.5],
'error_code' => 2,
'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/',
'linear' => true,
],
'R linear' => [
'color' => [-1, 0.5, 0.5],
'error_code' => 2,
'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/',
'linear' => true,
],
'G linear' => [
'color' => [0.5, 2, 0.5],
'error_code' => 2,
'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/',
'linear' => true,
],
'G linear' => [
'color' => [0.5, -1, 0.5],
'error_code' => 2,
'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/',
'linear' => true,
],
'B linear' => [
'color' => [0.5, 0.5, 2],
'error_code' => 2,
'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/',
'linear' => true,
],
'B linear' => [
'color' => [0.5, 0.5, -1],
'error_code' => 2,
'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/',
'linear' => true,
],
];
}
/**
* Undocumented function
*
* @dataProvider providerRgbBased
* @testdox Exception handling for RGB for error $error_code [$_dataName]
*
* @param string|array $color
* @param int $error_code
* @param string $error_string
* @param bool $linear
* @return void
*/
public function testExceptionRGB(string|array $color, int $error_code, string $error_string, bool $linear): void
{
// for RGB exception the same
$this->expectException(\LengthException::class);
$this->expectExceptionCode($error_code);
$this->expectExceptionMessageMatches($error_string);
new Color\Coordinates\RGB($color, options: ["linear" => $linear]);
}
/**
* Undocumented function
*
* @covers ::__get
* @testdox Exception handling for RGB general calls
*
* @return void
*/
public function testExceptionRGBGeneral()
{
// allow
$b = new Color\Coordinates\RGB([0, 0, 0], 'sRGB');
// invalid access to class
$b = new Color\Coordinates\RGB([0, 0, 0]);
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->h;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\RGB([0, 0, 0], 'FOO_BAR');
}
/**
* Undocumented function
*
* @covers ::setFromHex
* @testdox Exception handling for RGB setFromHex failues
*
* @return void
*/
public function testExceptionRGBFromHex()
{
$color = '';
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(3);
$this->expectExceptionMessage('hex_string argument cannot be empty');
new Color\Coordinates\RGB($color);
$color = 'zshj';
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(3);
$this->expectExceptionMessage('hex_string argument cannot be empty');
new Color\Coordinates\RGB($color);
$color = 'aabff';
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionCode(4);
$this->expectExceptionMessageMatches('/^Invalid hex_string: /');
new Color\Coordinates\RGB($color);
}
// MARK: Lab Exceptions
/**
* Undocumented function
*
* @return array
*/
public function providerLabBased(): array
{
// all HSB/V HSL HWB have the same value range, create test data for all of them
return [
'L CieLab' => [
'color' => [900, 10, 10], 'error_code' => 1,
'error_string' => '/ for lightness is not in the range of 0 to 100 for CIE Lab$/',
'colorspace' => 'CIELab',
],
'L CieLab' => [
'color' => [-1, 10, 10], 'error_code' => 1,
'error_string' => '/ for lightness is not in the range of 0 to 100 for CIE Lab$/',
'colorspace' => 'CIELab',
],
'L OkLab' => [
'color' => [900, 0.2, 0.2], 'error_code' => 1,
'error_string' => '/ for lightness is not in the range of 0.0 to 1.0 for OkLab$/',
'colorspace' => 'OkLab',
],
'L OkLab' => [
'color' => [-1, 0.2, 0.2], 'error_code' => 1,
'error_string' => '/ for lightness is not in the range of 0.0 to 1.0 for OkLab$/',
'colorspace' => 'OkLab',
],
'a CieLab' => [
'color' => [90, 900, 10], 'error_code' => 2,
'error_string' => '/ for a is not in the range of -125 to 125 for CIE Lab$/',
'colorspace' => 'CIELab',
],
'a CieLab' => [
'color' => [90, -900, 10], 'error_code' => 2,
'error_string' => '/ for a is not in the range of -125 to 125 for CIE Lab$/',
'colorspace' => 'CIELab',
],
'a OkLab' => [
'color' => [0.5, 900, 0.2], 'error_code' => 2,
'error_string' => '/ for a is not in the range of -0.5 to 0.5 for OkLab$/',
'colorspace' => 'OkLab',
],
'a OkLab' => [
'color' => [0.6, -900, 0.2], 'error_code' => 2,
'error_string' => '/ for a is not in the range of -0.5 to 0.5 for OkLab$/',
'colorspace' => 'OkLab',
],
'b CieLab' => [
'color' => [90, 10, 900], 'error_code' => 3,
'error_string' => '/ for b is not in the range of -125 to 125 for CIE Lab$/',
'colorspace' => 'CIELab',
],
'b CieLab' => [
'color' => [90, 10, -999], 'error_code' => 3,
'error_string' => '/ for b is not in the range of -125 to 125 for CIE Lab$/',
'colorspace' => 'CIELab',
],
'b OkLab' => [
'color' => [0.6, 0.2, 900], 'error_code' => 3,
'error_string' => '/ for b is not in the range of -0.5 to 0.5 for OkLab$/',
'colorspace' => 'OkLab',
],
'b OkLab' => [
'color' => [0.6, 0.2, -999], 'error_code' => 3,
'error_string' => '/ for b is not in the range of -0.5 to 0.5 for OkLab$/',
'colorspace' => 'OkLab',
],
];
}
/**
* Undocumented function
*
* @dataProvider providerLabBased
* @testdox Exception handling for Lab for error $error_code [$_dataName]
*
* @param string|array $color
* @param int $error_code
* @param string $error_string
* @param string $colorspace
* @return void
*/
public function testExceptionLab(
string|array $color,
int $error_code,
string $error_string,
string $colorspace
): void {
// for RGB exception the same
$this->expectException(\LengthException::class);
$this->expectExceptionCode($error_code);
$this->expectExceptionMessageMatches($error_string);
new Color\Coordinates\Lab($color, colorspace: $colorspace);
}
/**
* Undocumented function
*
* @covers ::__get
* @testdox Exception handling for Lab general calls
*
* @return void
*/
public function testExceptionLabGeneral()
{
// allow
$b = new Color\Coordinates\Lab([0, 0, 0], 'OkLab');
// invalid access to class
$b = new Color\Coordinates\Lab([0, 0, 0], 'CIELab');
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->x;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Only array colors allowed");
new Color\Coordinates\Lab('string', 'CIELab');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\Lab([0, 0, 0], 'FOO_BAR');
}
// MARK: LCH Exceptions
// public function testExceptionLch(string|array $color, int $error_code, string $error_string): void
/**
* Undocumented function
*
* @covers ::__get
* @testdox Exception handling for LCH general calls
*
* @return void
*/
public function testExceptionLchGeneral()
{
// allow
$b = new Color\Coordinates\LCH([0, 0, 0], 'OkLab');
// invalid access to class
$b = new Color\Coordinates\LCH([0, 0, 0], 'CIELab');
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->x;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Only array colors allowed");
new Color\Coordinates\LCH('string', 'CIELab');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\LCH([0, 0, 0], 'FOO_BAR');
}
// MARK: XYZ Exceptions
// Note, we do not check for value exceptions here
// public function testExceptionXyz(string|array $color, int $error_code, string $error_string): void
/**
* Undocumented function
*
* @covers ::__get
* @testdox Exception handling for XYZ general calls
*
* @return void
*/
public function testExceptionXyzGeneral()
{
// allow
$b = new Color\Coordinates\XYZ([0, 0, 0], 'CIEXYZ');
// invalid access to class
$b = new Color\Coordinates\XYZ([0, 0, 0]);
$this->expectException(\ErrorException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Creation of dynamic property is not allowed");
$b->x;
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Only array colors allowed");
new Color\Coordinates\XYZ('string');
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(0);
$this->expectExceptionMessage("Not allowed colorspace");
new Color\Coordinates\XYZ([0, 0, 0], 'FOO_BAR');
}
}

View File

@@ -142,7 +142,6 @@ final class CoreLibsConvertMathTest extends TestCase
*/
public function testCbrt(float|int $number, float|string $expected, int $round_to): void
{
print "OUT: " . \CoreLibs\Convert\Math::cbrt($number) . "\n";
$this->assertEquals(
$expected,
round(\CoreLibs\Convert\Math::cbrt($number), $round_to)
@@ -264,6 +263,130 @@ final class CoreLibsConvertMathTest extends TestCase
\CoreLibs\Convert\Math::multiplyMatrices($input_a, $input_b)
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerEqualWithEpsilon(): array
{
return [
'equal' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000000222,
'epsilon' => PHP_FLOAT_EPSILON,
'equal' => true,
],
'almost equal' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000000232,
'epsilon' => PHP_FLOAT_EPSILON,
'equal' => true,
],
'not equal' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000004222,
'epsilon' => PHP_FLOAT_EPSILON,
'equal' => false,
],
'equal, different epsilon' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000004222,
'epsilon' => 0.0001,
'equal' => true,
],
'not equal, different epsilon' => [
'a' => 0.0001,
'b' => 0.0002,
'epsilon' => 0.0001,
'equal' => false,
]
];
}
/**
* Undocumented function
*
* @covers ::equalWithEpsilon
* @dataProvider providerEqualWithEpsilon
* @testdox equalWithEpsilon with $a and $b and Epsilon: $epsilon must be equal: $equal [$_dataName]
*
* @return void
*/
public function testEqualWithEpsilon(float $a, float $b, float $epsilon, bool $equal): void
{
$this->assertEquals(
$equal,
\CoreLibs\Convert\Math::equalWithEpsilon($a, $b, $epsilon)
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerCompareWithEpsilon(): array
{
return [
'smaller, true' => [
'value' => 0.0001,
'compare' => '<',
'limit' => 0.0002,
'epsilon' => 0.00001,
'match' => true,
],
'smaller, false' => [
'value' => 0.0001,
'compare' => '<',
'limit' => 0.0001,
'epsilon' => 0.00001,
'match' => false,
],
'bigger, true' => [
'value' => 0.0002,
'compare' => '>',
'limit' => 0.0001,
'epsilon' => 0.00001,
'match' => true,
],
'bigger, false' => [
'value' => 0.0001,
'compare' => '>',
'limit' => 0.0001,
'epsilon' => 0.00001,
'match' => false,
],
];
}
/**
* Undocumented function
*
* @covers ::compareWithEpsilon
* @dataProvider providerCompareWithEpsilon
* @testdox compareWithEpsilon $value $compare $limit with $epsilon must match: $match [$_dataName]
*
* @param float $value
* @param string $compare
* @param float $limit
* @param float $epslion
* @param bool $match
* @return void
*/
public function testCompareWithEpsilon(
float $value,
string $compare,
float $limit,
float $epsilon,
bool $match
): void {
$this->assertEquals(
$match,
\CoreLibs\Convert\Math::compareWithEpsilon($value, $compare, $limit, $epsilon)
);
}
}
// __END__

View File

@@ -72,7 +72,7 @@ print '<div><h1>' . $PAGE_NAME . '</h1></div>';
// define a list of from to color sets for conversion test
$hwb = Color::hsbToHwb(Coordinates\HSB::__constructFromArray([
$hwb = Color::hsbToHwb(new Coordinates\HSB([
160,
0,
50,
@@ -81,7 +81,7 @@ print "HWB: " . DgS::printAr($hwb) . "<br>";
$hsb = Color::hwbToHsb($hwb);
print "HSB: " . DgS::printAr($hsb) . "<br>";
$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([
$oklch = Color::rgbToOkLch(Coordinates\RGB::create([
250,
0,
0
@@ -90,7 +90,7 @@ print "OkLch: " . DgS::printAr($oklch) . "<br>";
$rgb = Color::okLchToRgb($oklch);
print "OkLch -> RGB: " . DgS::printAr($rgb) . "<br>";
$oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([
$oklab = Color::rgbToOkLab(Coordinates\RGB::create([
250,
0,
0
@@ -101,24 +101,24 @@ $rgb = Color::okLabToRgb($oklab);
print "OkLab -> RGB: " . DgS::printAr($rgb) . "<br>";
print display($rgb->toCssString(), $rgb->toCssString(), 'OkLab to RGB');
$rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear();
$rgb = Coordinates\RGB::create([250, 100, 10])->toLinear();
print "RGBlinear: " . DgS::printAr($rgb) . "<br>";
$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear();
$rgb = Coordinates\RGB::create([0, 0, 0])->toLinear();
print "RGBlinear: " . DgS::printAr($rgb) . "<br>";
$cie_lab = Color::okLabToLab($oklab);
print "CieLab: " . DgS::printAr($cie_lab) . "<br>";
print display($cie_lab->toCssString(), $cie_lab->toCssString(), 'OkLab to Cie Lab');
$rgb = Coordinates\RGB::__constructFromArray([0, 0, 60]);
$rgb = Coordinates\RGB::create([0, 0, 60]);
$hsb = Color::rgbToHsb($rgb);
$rgb_b = Color::hsbToRgb($hsb);
print "RGB: " . DgS::printAr($rgb) . "<br>";
print "RGB->HSB: " . DgS::printAr($hsb) . "<br>";
print "HSB->RGB: " . DgS::printAr($rgb_b) . "<br>";
$hsl = Coordinates\HSL::__constructFromArray([0, 20, 0]);
$hsb = Coordinates\HSB::__constructFromArray([0, 20, 0]);
$hsl = Coordinates\HSL::create([0, 20, 0]);
$hsb = Coordinates\HSB::create([0, 20, 0]);
$hsl_from_hsb = Color::hsbToHsl($hsb);
print "HSL from HSB: " . DgS::printAr($hsl_from_hsb) . "<br>";

View File

@@ -250,7 +250,7 @@ class CieXyz
{
// if not linear, convert to linear
if (!$rgb->linear) {
$rgb->toLinear();
$rgb = (new RGB($rgb->returnAsArray()))->toLinear();
}
return new XYZ(Math::multiplyMatrices(
[

View File

@@ -766,11 +766,6 @@ class Color
public static function rgbToLab(RGB $rgb): Lab
{
return CieXyz::rgbViaXyzD65ViaXyzD50ToLab($rgb);
/* return CieXyz::xyzD50ToLab(
CieXyz::xyzD65ToXyzD50(
CieXyz::linRgbToXyzD65($rgb)
)
); */
}
/**
@@ -783,11 +778,6 @@ class Color
public static function labToRgb(Lab $lab): RGB
{
return CieXyz::labViaXyzD50ViaXyzD65ToRgb($lab);
/* return CieXyz::xyzD65ToLinRgb(
CieXyz::xyzD50ToXyxD65(
CieXyz::labToXyzD50($lab)
)
)->fromLinear(); */
}
// MARK: RGB <-> Lch (Cie)

View File

@@ -11,6 +11,8 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class HSB implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
@@ -83,10 +85,11 @@ class HSB implements Interface\CoordinatesInterface
}
switch ($name) {
case 'H':
if ((int)$value == 360) {
if ($value == 360.0) {
$value = 0;
}
if ((int)$value < 0 || (int)$value > 360) {
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0 to 360',
1
@@ -94,7 +97,8 @@ class HSB implements Interface\CoordinatesInterface
}
break;
case 'S':
if ((int)$value < 0 || (int)$value > 100) {
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for saturation is not in the range of 0 to 100',
2
@@ -102,7 +106,8 @@ class HSB implements Interface\CoordinatesInterface
}
break;
case 'B':
if ((int)$value < 0 || (int)$value > 100) {
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for brightness is not in the range of 0 to 100',
3

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Stringify;
use CoreLibs\Convert\Color\Utils;
class HSL implements Interface\CoordinatesInterface
{
@@ -84,10 +84,11 @@ class HSL implements Interface\CoordinatesInterface
}
switch ($name) {
case 'H':
if ((int)$value == 360) {
if ($value == 360.0) {
$value = 0;
}
if ((int)$value < 0 || (int)$value > 360) {
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0 to 360',
1
@@ -95,7 +96,8 @@ class HSL implements Interface\CoordinatesInterface
}
break;
case 'S':
if ((int)$value < 0 || (int)$value > 100) {
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for saturation is not in the range of 0 to 100',
2
@@ -103,7 +105,8 @@ class HSL implements Interface\CoordinatesInterface
}
break;
case 'L':
if ((int)$value < 0 || (int)$value > 100) {
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0 to 100',
3
@@ -183,7 +186,7 @@ class HSL implements Interface\CoordinatesInterface
. $this->S
. ' '
. $this->L
. Stringify::setOpacity($opacity)
. Utils::setOpacity($opacity)
. ')';
return $string;
}

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Stringify;
use CoreLibs\Convert\Color\Utils;
class HWB implements Interface\CoordinatesInterface
{
@@ -84,10 +84,11 @@ class HWB implements Interface\CoordinatesInterface
}
switch ($name) {
case 'H':
if ((int)$value == 360) {
if ($value == 360.0) {
$value = 0;
}
if ((int)$value < 0 || (int)$value > 360) {
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0 to 360',
1
@@ -95,7 +96,8 @@ class HWB implements Interface\CoordinatesInterface
}
break;
case 'W':
if ((int)$value < 0 || (int)$value > 100) {
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for whiteness is not in the range of 0 to 100',
2
@@ -103,7 +105,8 @@ class HWB implements Interface\CoordinatesInterface
}
break;
case 'B':
if ((int)$value < 0 || (int)$value > 100) {
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for blackness is not in the range of 0 to 100',
3
@@ -183,7 +186,7 @@ class HWB implements Interface\CoordinatesInterface
. $this->W
. ' '
. $this->B
. Stringify::setOpacity($opacity)
. Utils::setOpacity($opacity)
. ')';
return $string;
}

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Stringify;
use CoreLibs\Convert\Color\Utils;
class LCH implements Interface\CoordinatesInterface
{
@@ -94,43 +94,43 @@ class LCH implements Interface\CoordinatesInterface
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
switch ($name) {
// case 'L':
// if ($this->colorspace == 'cie' && ($value < 0 || $value > 100)) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for lightness is not in the range of '
// . '0 to 100',
// 3
// );
// } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 1)) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for lightness is not in the range of '
// . '0 to 1',
// 3
// );
// }
// break;
// case 'c':
// if ($this->colorspace == 'cie' && ($value < 0 || $value > 230)) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for chroma is not in the range of '
// . '0 to 230 with normal upper limit of 150',
// 3
// );
// } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 0.5)) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for chroma is not in the range of '
// . '0 to 0.5 with normal upper limit of 0.5',
// 3
// );
// }
// break;
case 'h':
if ($value == 360) {
$value = 0;
}
if ($value < 0 || $value > 360) {
case 'L':
// if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 100)) {
if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 100.0, Utils::ESPILON_BIG)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0 to 360',
'Argument value ' . $value . ' for lightness is not in the range of 0 to 100 for CIE Lab',
1
);
// } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 1)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 1.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0.0 to 1.0 for OkLab',
1
);
}
break;
case 'C':
// if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 230)) {
if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 230.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for chroma is not in the range of '
. '0 to 150 and a maximum of 230 for CIE Lab',
1
);
// } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 0.55)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 0.55, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of '
. '0.0 to 0.4 and a maximum of 0.5 for OkLab',
1
);
}
break;
case 'H':
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0.0 to 360.0',
1
);
}
@@ -217,7 +217,7 @@ class LCH implements Interface\CoordinatesInterface
. $this->c
. ' '
. $this->h
. Stringify::setOpacity($opacity)
. Utils::setOpacity($opacity)
. ');';
return $string;

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Stringify;
use CoreLibs\Convert\Color\Utils;
class Lab implements Interface\CoordinatesInterface
{
@@ -95,35 +95,53 @@ class Lab implements Interface\CoordinatesInterface
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
// switch ($name) {
// case 'L':
// if ($value == 360) {
// $value = 0;
// }
// if ($value < 0 || $value > 360) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360',
// 1
// );
// }
// break;
// case 'a':
// if ($value < 0 || $value > 100) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for a is not in the range of 0 to 100',
// 2
// );
// }
// break;
// case 'b':
// if ($value < 0 || $value > 100) {
// throw new \LengthException(
// 'Argument value ' . $value . ' for b is not in the range of 0 to 100',
// 3
// );
// }
// break;
// }
switch ($name) {
case 'L':
// if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 100)) {
if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 100.0, Utils::ESPILON_BIG)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0 to 100 for CIE Lab',
1
);
// } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 1)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 1.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0.0 to 1.0 for OkLab',
1
);
}
break;
case 'a':
// if ($this->colorspace == 'CIELab' && ($value < -125 || $value > 125)) {
if ($this->colorspace == 'CIELab' && Utils::compare(-125.0, $value, 125.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for a is not in the range of -125 to 125 for CIE Lab',
2
);
// } elseif ($this->colorspace == 'OkLab' && ($value < -0.55 || $value > 0.55)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(-0.55, $value, 0.55, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for a is not in the range of -0.5 to 0.5 for OkLab',
2
);
}
break;
case 'b':
// if ($this->colorspace == 'CIELab' && ($value < -125 || $value > 125)) {
if ($this->colorspace == 'CIELab' && Utils::compare(-125.0, $value, 125.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for b is not in the range of -125 to 125 for CIE Lab',
3
);
// } elseif ($this->colorspace == 'OkLab' && ($value < -0.55 || $value > 0.55)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(-0.55, $value, 0.55, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for b is not in the range of -0.5 to 0.5 for OkLab',
3
);
}
break;
}
$this->$name = $value;
}
@@ -205,7 +223,7 @@ class Lab implements Interface\CoordinatesInterface
. $this->a
. ' '
. $this->b
. Stringify::setOpacity($opacity)
. Utils::setOpacity($opacity)
. ');';
return $string;

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Stringify;
use CoreLibs\Convert\Color\Utils;
class RGB implements Interface\CoordinatesInterface
{
@@ -94,8 +94,11 @@ class RGB implements Interface\CoordinatesInterface
// if not linear
if (!$this->linear && ((int)$value < 0 || (int)$value > 255)) {
throw new \LengthException('Argument value ' . $value . ' for color ' . $name
. ' is not in the range of 0 to 255', 1);
} elseif ($this->linear && ((int)$value < 0 || (int)$value > 1)) {
. ' is not in the range of 0 to 255', 1);
} elseif (
// $this->linear && ($value < 0.0 || $value > 1.0)
$this->linear && Utils::compare(0.0, $value, 1.0, 0.000001)
) {
throw new \LengthException('Argument value ' . $value . ' for color ' . $name
. ' is not in the range of 0 to 1 for linear rgb', 2);
}
@@ -244,6 +247,10 @@ class RGB implements Interface\CoordinatesInterface
*/
public function toLinear(): self
{
// if linear, as is
if ($this->linear) {
return $this;
}
$this->flagLinear(true)->setFromArray(array_map(
callback: function (int|float $v) {
$v = (float)($v / 255);
@@ -268,6 +275,10 @@ class RGB implements Interface\CoordinatesInterface
*/
public function fromLinear(): self
{
// if not linear, as is
if (!$this->linear) {
return $this;
}
$this->flagLinear(false)->setFromArray(array_map(
callback: function (int|float $v) {
$abs = abs($v);
@@ -282,7 +293,6 @@ class RGB implements Interface\CoordinatesInterface
},
array: $this->returnAsArray(),
));
// $this->linear = false;
return $this;
}
@@ -307,7 +317,7 @@ class RGB implements Interface\CoordinatesInterface
. (int)round($this->G, 0)
. ' '
. (int)round($this->B, 0)
. Stringify::setOpacity($opacity)
. Utils::setOpacity($opacity)
. ')';
if ($was_linear) {
$this->toLinear();

View File

@@ -15,6 +15,8 @@ declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
// use CoreLibs\Convert\Color\Utils;
class XYZ implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
@@ -101,9 +103,11 @@ class XYZ implements Interface\CoordinatesInterface
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
// if ($value < 0 || $value > 255) {
// TODO: setup XYZ value limits
// X: 0 to 95.047, Y: 0 to 100, Z: 0 to 108.88
// if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL))) {
// throw new \LengthException('Argument value ' . $value . ' for color ' . $name
// . ' is not in the range of 0 to 255', 1);
// . ' is not in the range of 0 to 100.0', 1);
// }
$this->$name = $value;
}

View File

@@ -19,25 +19,6 @@ use CoreLibs\Convert\Color\Coordinates\LCH;
class Stringify
{
/**
* Build the opactiy sub string part and return it
*
* @param null|float|string|null $opacity
* @return string
*/
public static function setOpacity(null|float|string $opacity = null): string
{
// set opacity, either a string or float
if (is_string($opacity)) {
$opacity = ' / ' . $opacity;
} elseif ($opacity !== null) {
$opacity = ' / ' . $opacity;
} else {
$opacity = '';
}
return $opacity;
}
/**
* return the CSS string including optional opacity
*

View File

@@ -0,0 +1,56 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/14
* DESCRIPTION:
* Utils for color
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color;
use CoreLibs\Convert\Math;
class Utils
{
/** @var int deviation allowed for valid data checks, small */
public const EPSILON_SMALL = 0.000000000001;
/** @var int deviation allowed for valid data checks, medium */
public const EPSILON_MEDIUM = 0.0000001;
/** @var int deviation allowed for valid data checks, big */
public const ESPILON_BIG = 0.0001;
public static function compare(float $lower, float $value, float $upper, float $epslion): bool
{
if (
Math::compareWithEpsilon($value, '<', $lower, $epslion) ||
Math::compareWithEpsilon($value, '>', $upper, $epslion)
) {
return true;
}
return false;
}
/**
* Build the opactiy sub string part and return it
*
* @param null|float|string|null $opacity
* @return string
*/
public static function setOpacity(null|float|string $opacity = null): string
{
// set opacity, either a string or float
if (is_string($opacity)) {
$opacity = ' / ' . $opacity;
} elseif ($opacity !== null) {
$opacity = ' / ' . $opacity;
} else {
$opacity = '';
}
return $opacity;
}
}
// __END__

View File

@@ -68,6 +68,66 @@ class Math
return pow((float)$number, 1.0 / 3);
}
/**
* use PHP_FLOAT_EPSILON to compare if two float numbers are matching
*
* @param float $x
* @param float $y
* @param float $epsilon [default=PHP_FLOAT_EPSILON]
* @return bool True equal
*/
public static function equalWithEpsilon(float $x, float $y, float $epsilon = PHP_FLOAT_EPSILON): bool
{
if (abs($x - $y) < $epsilon) {
return true;
}
return false;
}
/**
* Compare two value base on direction given
* The default delta is PHP_FLOAT_EPSILON
*
* @param float $value
* @param string $compare
* @param float $limit
* @param float $epsilon [default=PHP_FLOAT_EPSILON]
* @return bool True on smaller/large or equal
*/
public static function compareWithEpsilon(
float $value,
string $compare,
float $limit,
float $epsilon = PHP_FLOAT_EPSILON
): bool {
switch ($compare) {
case '<':
if ($value < ($limit - $epsilon)) {
return true;
}
break;
case '<=':
if ($value <= ($limit - $epsilon)) {
return true;
}
break;
case '==':
return self::equalWithEpsilon($value, $limit, $epsilon);
break;
case '>':
if ($value > ($limit + $epsilon)) {
return true;
}
break;
case '>=':
if ($value >= ($limit + $epsilon)) {
return true;
}
break;
}
return false;
}
/**
* This function is directly inspired by the multiplyMatrices() function in color.js
* form Lea Verou and Chris Lilley.