diff --git a/src/Renderer/Color/Cmyk.php b/src/Renderer/Color/Cmyk.php index d028210..eaf34f4 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) 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 76603e4..760b861 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) round($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 9e388da..051c5be 100644 --- a/src/Renderer/Color/Rgb.php +++ b/src/Renderer/Color/Rgb.php @@ -49,25 +49,27 @@ 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)), - (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 { - return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55)); + // use integer-based calculation to avoid floating-point precision loss + return new Gray((int) round(($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 @@ - + 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..5c3684a --- /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 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 (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)'); + } + + /** + * 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)'); + } +}