From ad8772a9ee5291ecf81c44ebee682268fe75bdcb Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:14:41 +0100 Subject: [PATCH 1/7] fix(color): avoid division by zero when converting rgb(0,0,0) to cmyk Previous code was instead handling rgb(255,255,255) as a specific case, which was useless. --- src/Renderer/Color/Rgb.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Renderer/Color/Rgb.php b/src/Renderer/Color/Rgb.php index 9e388da..3b05f9f 100644 --- a/src/Renderer/Color/Rgb.php +++ b/src/Renderer/Color/Rgb.php @@ -49,15 +49,16 @@ public function toRgb() : Rgb public function toCmyk() : Cmyk { + // avoid division by zero with input rgb(0,0,0), by handling it as a specific case + if (0 === $this->red && 0 === $this->green && 0 === $this->blue) { + return new Cmyk(0, 0, 0, 100); + } + $c = 1 - ($this->red / 255); $m = 1 - ($this->green / 255); $y = 1 - ($this->blue / 255); $k = min($c, $m, $y); - if ($k === 0) { - return new Cmyk(0, 0, 0, 0); - } - return new Cmyk( (int) (100 * ($c - $k) / (1 - $k)), (int) (100 * ($m - $k) / (1 - $k)), From b87b303380004e549ff24bc990ec23bfe0cd44fd Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:20:57 +0100 Subject: [PATCH 2/7] refactor(color): simplify CMYK to RGB conversion using standard formula --- src/Renderer/Color/Cmyk.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Renderer/Color/Cmyk.php b/src/Renderer/Color/Cmyk.php index d028210..cb4648e 100644 --- a/src/Renderer/Color/Cmyk.php +++ b/src/Renderer/Color/Cmyk.php @@ -58,15 +58,15 @@ public function getBlack() : int public function toRgb() : Rgb { + $c = $this->cyan / 100; + $m = $this->magenta / 100; + $y = $this->yellow / 100; $k = $this->black / 100; - $c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100; - $m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100; - $y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100; return new Rgb( - (int) (-$c * 255 + 255), - (int) (-$m * 255 + 255), - (int) (-$y * 255 + 255) + (int) (255 * (1 - $c) * (1 - $k)), + (int) (255 * (1 - $m) * (1 - $k)), + (int) (255 * (1 - $y) * (1 - $k)) ); } From 761f3c2304bbafab1edd86e6a270feaff7c13662 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:25:07 +0100 Subject: [PATCH 3/7] fix(color): use the correct luminance coefficients to rgb to gray conversion These coefficients were cluelessly truncated. --- src/Renderer/Color/Rgb.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Renderer/Color/Rgb.php b/src/Renderer/Color/Rgb.php index 3b05f9f..6b7e35e 100644 --- a/src/Renderer/Color/Rgb.php +++ b/src/Renderer/Color/Rgb.php @@ -69,6 +69,6 @@ public function toCmyk() : Cmyk public function toGray() : Gray { - return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55)); + return new Gray((int) (($this->red * 0.2126 + $this->green * 0.7152 + $this->blue * 0.0722) / 2.55)); } } From 81a1485d46244caff98e8e0b4c7f57e995f6e545 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:30:10 +0100 Subject: [PATCH 4/7] fix(color): use integer-based calculation to avoid floating-point precision loss Notably, this fixes gray(100) to rgb(255,255,255) instead of rgb(254,254,254). --- src/Renderer/Color/Gray.php | 5 ++++- src/Renderer/Color/Rgb.php | 3 ++- .../__snapshots__/SVGRenderingTest__testGenericQrCode__1.xml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Renderer/Color/Gray.php b/src/Renderer/Color/Gray.php index 76603e4..172a741 100644 --- a/src/Renderer/Color/Gray.php +++ b/src/Renderer/Color/Gray.php @@ -24,7 +24,10 @@ public function getGray() : int public function toRgb() : Rgb { - return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55)); + // use 255/100 instead of 2.55 to avoid floating-point precision loss (100 * 2.55 = 254.999...) + $value = (int) ($this->gray * 255 / 100); + + return new Rgb($value, $value, $value); } public function toCmyk() : Cmyk diff --git a/src/Renderer/Color/Rgb.php b/src/Renderer/Color/Rgb.php index 6b7e35e..7483602 100644 --- a/src/Renderer/Color/Rgb.php +++ b/src/Renderer/Color/Rgb.php @@ -69,6 +69,7 @@ public function toCmyk() : Cmyk public function toGray() : Gray { - return new Gray((int) (($this->red * 0.2126 + $this->green * 0.7152 + $this->blue * 0.0722) / 2.55)); + // use integer-based calculation to avoid floating-point precision loss + return new Gray((int) (($this->red * 2126 + $this->green * 7152 + $this->blue * 722) / 25500)); } } diff --git a/test/Integration/__snapshots__/SVGRenderingTest__testGenericQrCode__1.xml b/test/Integration/__snapshots__/SVGRenderingTest__testGenericQrCode__1.xml index b602bbb..f3be725 100644 --- a/test/Integration/__snapshots__/SVGRenderingTest__testGenericQrCode__1.xml +++ b/test/Integration/__snapshots__/SVGRenderingTest__testGenericQrCode__1.xml @@ -1,6 +1,6 @@ - + From cfe7bbcfb1e646fd2a0ba74133825b8ce1a05fc5 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:36:23 +0100 Subject: [PATCH 5/7] fix(color): use rounding instead of implicit truncation --- src/Renderer/Color/Cmyk.php | 6 +++--- src/Renderer/Color/Gray.php | 2 +- src/Renderer/Color/Rgb.php | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Renderer/Color/Cmyk.php b/src/Renderer/Color/Cmyk.php index cb4648e..eaf34f4 100644 --- a/src/Renderer/Color/Cmyk.php +++ b/src/Renderer/Color/Cmyk.php @@ -64,9 +64,9 @@ public function toRgb() : Rgb $k = $this->black / 100; return new Rgb( - (int) (255 * (1 - $c) * (1 - $k)), - (int) (255 * (1 - $m) * (1 - $k)), - (int) (255 * (1 - $y) * (1 - $k)) + (int) round(255 * (1 - $c) * (1 - $k)), + (int) round(255 * (1 - $m) * (1 - $k)), + (int) round(255 * (1 - $y) * (1 - $k)) ); } diff --git a/src/Renderer/Color/Gray.php b/src/Renderer/Color/Gray.php index 172a741..760b861 100644 --- a/src/Renderer/Color/Gray.php +++ b/src/Renderer/Color/Gray.php @@ -25,7 +25,7 @@ public function getGray() : int public function toRgb() : Rgb { // use 255/100 instead of 2.55 to avoid floating-point precision loss (100 * 2.55 = 254.999...) - $value = (int) ($this->gray * 255 / 100); + $value = (int) round($this->gray * 255 / 100); return new Rgb($value, $value, $value); } diff --git a/src/Renderer/Color/Rgb.php b/src/Renderer/Color/Rgb.php index 7483602..051c5be 100644 --- a/src/Renderer/Color/Rgb.php +++ b/src/Renderer/Color/Rgb.php @@ -60,16 +60,16 @@ public function toCmyk() : Cmyk $k = min($c, $m, $y); return new Cmyk( - (int) (100 * ($c - $k) / (1 - $k)), - (int) (100 * ($m - $k) / (1 - $k)), - (int) (100 * ($y - $k) / (1 - $k)), - (int) (100 * $k) + (int) round(100 * ($c - $k) / (1 - $k)), + (int) round(100 * ($m - $k) / (1 - $k)), + (int) round(100 * ($y - $k) / (1 - $k)), + (int) round(100 * $k) ); } public function toGray() : Gray { // use integer-based calculation to avoid floating-point precision loss - return new Gray((int) (($this->red * 2126 + $this->green * 7152 + $this->blue * 722) / 25500)); + return new Gray((int) round(($this->red * 2126 + $this->green * 7152 + $this->blue * 722) / 25500)); } } From 666a16246636f268a819bb5fe4a7c624eb7dbc0d Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:43:22 +0100 Subject: [PATCH 6/7] test(color): add unit tests for color mode conversions --- test/Renderer/Color/CmykTest.php | 62 +++++++++++++++++++++++++ test/Renderer/Color/GrayTest.php | 63 ++++++++++++++++++++++++++ test/Renderer/Color/RgbTest.php | 78 ++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 test/Renderer/Color/CmykTest.php create mode 100644 test/Renderer/Color/GrayTest.php create mode 100644 test/Renderer/Color/RgbTest.php diff --git a/test/Renderer/Color/CmykTest.php b/test/Renderer/Color/CmykTest.php new file mode 100644 index 0000000..51df420 --- /dev/null +++ b/test/Renderer/Color/CmykTest.php @@ -0,0 +1,62 @@ + RGB(0, 0, 0) + $cmykBlack = new Cmyk(0, 0, 0, 100); + $this->assertEquals(new Rgb(0, 0, 0), $cmykBlack->toRgb(), 'CMYK Black to RGB'); + + // Pure White (C:0, M:0, Y:0, K:0) -> RGB(255, 255, 255) + $cmykWhite = new Cmyk(0, 0, 0, 0); + $this->assertEquals(new Rgb(255, 255, 255), $cmykWhite->toRgb(), 'CMYK White to RGB'); + + // Mid Gray (C:0, M:0, Y:0, K:50) -> RGB(128, 128, 128) + // Check for rounding: 255 * (1 - 0) * (1 - 0.5) = 127.5 -> round(127.5) = 128 + $cmykGray = new Cmyk(0, 0, 0, 50); + $this->assertEquals(new Rgb(128, 128, 128), $cmykGray->toRgb(), 'CMYK Gray to RGB (rounding check)'); + + // Complex Color (Dark Red): C:10, M:80, Y:70, K:30 + // R: round(255 * 0.9 * 0.7) = round(160.65) = 161 + // G: round(255 * 0.2 * 0.7) = round(35.7) = 36 + // B: round(255 * 0.3 * 0.7) = round(53.55) = 54 + $cmykColor = new Cmyk(10, 80, 70, 30); + $this->assertEquals(new Rgb(161, 36, 54), $cmykColor->toRgb(), 'CMYK Complex Color to RGB'); + } + + public function testToCmyk() : void + { + $cmyk = new Cmyk(10, 20, 30, 40); + $this->assertSame($cmyk, $cmyk->toCmyk(), 'toCmyk should return $this'); + } + + /** + * Tests CMYK to Gray conversion via RGB. + */ + public function testToGray() : void + { + // White (K:0) -> Gray(100) + $cmykWhite = new Cmyk(0, 0, 0, 0); + $this->assertEquals(new Gray(100), $cmykWhite->toGray(), 'CMYK White to Gray'); + + // Black (K:100) -> Gray(0) + $cmykBlack = new Cmyk(0, 0, 0, 100); + $this->assertEquals(new Gray(0), $cmykBlack->toGray(), 'CMYK Black to Gray'); + + // Pure Gray (K:50) -> Should result in Gray(50) + $cmykGray = new Cmyk(0, 0, 0, 50); + $this->assertEquals(new Gray(50), $cmykGray->toGray(), 'CMYK Gray to Gray'); + } +} diff --git a/test/Renderer/Color/GrayTest.php b/test/Renderer/Color/GrayTest.php new file mode 100644 index 0000000..624e1f2 --- /dev/null +++ b/test/Renderer/Color/GrayTest.php @@ -0,0 +1,63 @@ + RGB(0, 0, 0) + $grayBlack = new Gray(0); + $this->assertEquals(new Rgb(0, 0, 0), $grayBlack->toRgb(), 'Gray Black to RGB'); + + // White (100) -> RGB(255, 255, 255) + // 100 * 255 / 100 = 255 + $grayWhite = new Gray(100); + $this->assertEquals(new Rgb(255, 255, 255), $grayWhite->toRgb(), 'Gray White to RGB'); + + // Midpoint (50) -> RGB(128, 128, 128) + // Check for rounding: 50 * 255 / 100 = 127.5 -> round(127.5) = 128 + $grayMiddle = new Gray(50); + $this->assertEquals(new Rgb(128, 128, 128), $grayMiddle->toRgb(), 'Gray 50 to RGB (rounding check)'); + + // Custom value checking rounding + // 33 * 255 / 100 = 84.15 -> round(84.15) = 84 + $grayCustom = new Gray(33); + $this->assertEquals(new Rgb(84, 84, 84), $grayCustom->toRgb(), 'Gray Custom to RGB'); + } + + /** + * Tests Gray to CMYK conversion (K=100-Gray). + */ + public function testToCmyk() : void + { + // Black (0) -> K:100 + $grayBlack = new Gray(0); + $this->assertEquals(new Cmyk(0, 0, 0, 100), $grayBlack->toCmyk(), 'Gray Black to CMYK'); + + // White (100) -> K:0 + $grayWhite = new Gray(100); + $this->assertEquals(new Cmyk(0, 0, 0, 0), $grayWhite->toCmyk(), 'Gray White to CMYK'); + + // Middle (50) -> K:50 + $grayMiddle = new Gray(50); + $this->assertEquals(new Cmyk(0, 0, 0, 50), $grayMiddle->toCmyk(), 'Gray Middle to CMYK'); + } + + public function testToGray() : void + { + $gray = new Gray(75); + $this->assertSame($gray, $gray->toGray(), 'toGray should return $this'); + } +} diff --git a/test/Renderer/Color/RgbTest.php b/test/Renderer/Color/RgbTest.php new file mode 100644 index 0000000..0e33cd8 --- /dev/null +++ b/test/Renderer/Color/RgbTest.php @@ -0,0 +1,78 @@ +assertSame($rgb, $rgb->toRgb(), 'toRgb should return $this'); + } + + /** + * Tests RGB to CMYK conversion, focusing on: + * 1. Handling the special case RGB(0, 0, 0) -> CMYK(0, 0, 0, 100). + * 2. Correct application of rounding. + */ + public function testToCmyk() : void + { + // Special Case: Black (0, 0, 0) -> C:0, M:0, Y:0, K:100 + $rgbBlack = new Rgb(0, 0, 0); + $this->assertEquals(new Cmyk(0, 0, 0, 100), $rgbBlack->toCmyk(), 'RGB Black to CMYK (special case)'); + + // White (255, 255, 255) -> C:0, M:0, Y:0, K:0 + $rgbWhite = new Rgb(255, 255, 255); + $this->assertEquals(new Cmyk(0, 0, 0, 0), $rgbWhite->toCmyk(), 'RGB White to CMYK'); + + // Pure Red (255, 0, 0) -> C:0, M:100, Y:100, K:0 + $rgbRed = new Rgb(255, 0, 0); + $this->assertEquals(new Cmyk(0, 100, 100, 0), $rgbRed->toCmyk(), 'RGB Red to CMYK'); + + // Complex Color checking rounding: RGB(100, 150, 200) + // K=22 (round(100 * 0.2156)) + // C=50 (round(100 * 0.3922 / 0.7844)) + // M=25 (round(100 * 0.1961 / 0.7844)) + // Y=0 + $rgbCustom = new Rgb(100, 150, 200); + $this->assertEquals(new Cmyk(50, 25, 0, 22), $rgbCustom->toCmyk(), 'RGB Custom to CMYK (rounding check)'); + } + + /** + * Tests RGB to Gray conversion, focusing on: + * 1. Correct luminance coefficients (0.2126, 0.7152, 0.0722). + * 2. Integer-based calculation to avoid floating-point precision loss. + * 3. Correct application of rounding. + */ + public function testToGray() : void + { + // Black (0, 0, 0) -> Gray(0) + $rgbBlack = new Rgb(0, 0, 0); + $this->assertEquals(new Gray(0), $rgbBlack->toGray(), 'RGB Black to Gray'); + + // White (255, 255, 255) -> Gray(100) + $rgbWhite = new Rgb(255, 255, 255); + $this->assertEquals(new Gray(100), $rgbWhite->toGray(), 'RGB White to Gray'); + + // Pure Red (255, 0, 0) + // round((255 * 2126) / 25500) = round(21.26) = 21 + $rgbRed = new Rgb(255, 0, 0); + $this->assertEquals(new Gray(21), $rgbRed->toGray(), 'RGB Red to Gray'); + + // Pure Green (0, 255, 0) + // round((255 * 7152) / 25500) = round(71.52) = 72 + $rgbGreen = new Rgb(0, 255, 0); + $this->assertEquals(new Gray(72), $rgbGreen->toGray(), 'RGB Green to Gray'); + + // Complex Color checking rounding: RGB(100, 150, 200) + // round((100*2126 + 150*7152 + 200*722) / 25500) = round(56.07...) = 56 + $rgbCustom = new Rgb(100, 150, 200); + $this->assertEquals(new Gray(56), $rgbCustom->toGray(), 'RGB Custom to Gray (rounding check)'); + } +} From 0f46d4738cffac49ec9842e5694b3810ae2f8d1c Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:41:40 +0100 Subject: [PATCH 7/7] test(color): for gray-to-rgb, replace input 33 with 90 Similar to input 50, input 90 is the only other case that depends on using "255/100" in order to be rounded up instead of down. For these cases, rounding down would yield a different but equally linear progression, but inconsistent with expected calculation results. --- test/Renderer/Color/GrayTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Renderer/Color/GrayTest.php b/test/Renderer/Color/GrayTest.php index 624e1f2..5c3684a 100644 --- a/test/Renderer/Color/GrayTest.php +++ b/test/Renderer/Color/GrayTest.php @@ -27,14 +27,14 @@ public function testToRgb() : void $this->assertEquals(new Rgb(255, 255, 255), $grayWhite->toRgb(), 'Gray White to RGB'); // Midpoint (50) -> RGB(128, 128, 128) - // Check for rounding: 50 * 255 / 100 = 127.5 -> round(127.5) = 128 + // Check for precision and rounding: 50 * 255 / 100 = 127.5 -> round(127.5) = 128 $grayMiddle = new Gray(50); $this->assertEquals(new Rgb(128, 128, 128), $grayMiddle->toRgb(), 'Gray 50 to RGB (rounding check)'); - // Custom value checking rounding - // 33 * 255 / 100 = 84.15 -> round(84.15) = 84 - $grayCustom = new Gray(33); - $this->assertEquals(new Rgb(84, 84, 84), $grayCustom->toRgb(), 'Gray Custom to RGB'); + // Custom value (90) -> RGB(230, 230, 230) + // Check for precision and rounding: 90 * 255 / 100 = 229.5 -> round(229.5) = 230 + $grayCustom = new Gray(90); + $this->assertEquals(new Rgb(230, 230, 230), $grayCustom->toRgb(), 'Gray Custom to RGB (rounding check)'); } /**