From a9f1d878f7e3826bcc195f6757c21c41d9499c35 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 15 Nov 2024 18:13:16 +0900 Subject: [PATCH] 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 --- .../Convert/CoreLibsConvertColorTest.php | 883 +++++++++++++++++- .../tests/Convert/CoreLibsConvertMathTest.php | 125 ++- www/admin/class_test.convert.colors.php | 16 +- www/lib/CoreLibs/Convert/Color/CieXyz.php | 2 +- www/lib/CoreLibs/Convert/Color/Color.php | 10 - .../Convert/Color/Coordinates/HSB.php | 13 +- .../Convert/Color/Coordinates/HSL.php | 15 +- .../Convert/Color/Coordinates/HWB.php | 15 +- .../Convert/Color/Coordinates/LCH.php | 76 +- .../Convert/Color/Coordinates/Lab.php | 80 +- .../Convert/Color/Coordinates/RGB.php | 20 +- .../Convert/Color/Coordinates/XYZ.php | 8 +- www/lib/CoreLibs/Convert/Color/Stringify.php | 19 - www/lib/CoreLibs/Convert/Color/Utils.php | 56 ++ www/lib/CoreLibs/Convert/Math.php | 60 ++ 15 files changed, 1246 insertions(+), 152 deletions(-) create mode 100644 www/lib/CoreLibs/Convert/Color/Utils.php diff --git a/4dev/tests/Convert/CoreLibsConvertColorTest.php b/4dev/tests/Convert/CoreLibsConvertColorTest.php index 3b5d950f..543ff537 100644 --- a/4dev/tests/Convert/CoreLibsConvertColorTest.php +++ b/4dev/tests/Convert/CoreLibsConvertColorTest.php @@ -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'); } } diff --git a/4dev/tests/Convert/CoreLibsConvertMathTest.php b/4dev/tests/Convert/CoreLibsConvertMathTest.php index 476c5c1f..95002850 100644 --- a/4dev/tests/Convert/CoreLibsConvertMathTest.php +++ b/4dev/tests/Convert/CoreLibsConvertMathTest.php @@ -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__ diff --git a/www/admin/class_test.convert.colors.php b/www/admin/class_test.convert.colors.php index 131bc642..0254ce67 100644 --- a/www/admin/class_test.convert.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -72,7 +72,7 @@ print '

' . $PAGE_NAME . '

'; // 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) . "
"; $hsb = Color::hwbToHsb($hwb); print "HSB: " . DgS::printAr($hsb) . "
"; -$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([ +$oklch = Color::rgbToOkLch(Coordinates\RGB::create([ 250, 0, 0 @@ -90,7 +90,7 @@ print "OkLch: " . DgS::printAr($oklch) . "
"; $rgb = Color::okLchToRgb($oklch); print "OkLch -> RGB: " . DgS::printAr($rgb) . "
"; -$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) . "
"; 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) . "
"; -$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); +$rgb = Coordinates\RGB::create([0, 0, 0])->toLinear(); print "RGBlinear: " . DgS::printAr($rgb) . "
"; $cie_lab = Color::okLabToLab($oklab); print "CieLab: " . DgS::printAr($cie_lab) . "
"; 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) . "
"; print "RGB->HSB: " . DgS::printAr($hsb) . "
"; print "HSB->RGB: " . DgS::printAr($rgb_b) . "
"; -$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) . "
"; diff --git a/www/lib/CoreLibs/Convert/Color/CieXyz.php b/www/lib/CoreLibs/Convert/Color/CieXyz.php index 8651a7dd..65157597 100644 --- a/www/lib/CoreLibs/Convert/Color/CieXyz.php +++ b/www/lib/CoreLibs/Convert/Color/CieXyz.php @@ -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( [ diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index 9dd2f063..17f3accc 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -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) diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php index 821fe010..62d83a5b 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -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 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 diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php index 64081e6e..c5198c23 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -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; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php index 0f1ce58f..dbad9861 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -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; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php index 76b3ad69..8f886292 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -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; diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php index 1b475d5c..60635c0c 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -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; diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php index 474c37f1..ce69e855 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -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(); diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php index 01fb926d..2e30b98d 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php @@ -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 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; } diff --git a/www/lib/CoreLibs/Convert/Color/Stringify.php b/www/lib/CoreLibs/Convert/Color/Stringify.php index 72dfa020..b2de79b9 100644 --- a/www/lib/CoreLibs/Convert/Color/Stringify.php +++ b/www/lib/CoreLibs/Convert/Color/Stringify.php @@ -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 * diff --git a/www/lib/CoreLibs/Convert/Color/Utils.php b/www/lib/CoreLibs/Convert/Color/Utils.php new file mode 100644 index 00000000..b273f001 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Utils.php @@ -0,0 +1,56 @@ +', $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__ diff --git a/www/lib/CoreLibs/Convert/Math.php b/www/lib/CoreLibs/Convert/Math.php index 8af796b1..fedc60ed 100644 --- a/www/lib/CoreLibs/Convert/Math.php +++ b/www/lib/CoreLibs/Convert/Math.php @@ -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.