Skip to content

Commit b0105c7

Browse files
authored
fix: correctly rotate eyes when using inherited colors (#174)
1 parent 1b26475 commit b0105c7

File tree

12 files changed

+206
-4
lines changed

12 files changed

+206
-4
lines changed

src/Renderer/Eye/PointyEye.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace BaconQrCode\Renderer\Eye;
5+
6+
use BaconQrCode\Renderer\Path\Path;
7+
8+
/**
9+
* Renders the outer eye as solid with a curved corner and inner eye as a circle.
10+
*/
11+
final class PointyEye implements EyeInterface
12+
{
13+
/**
14+
* @var self|null
15+
*/
16+
private static $instance;
17+
18+
private function __construct()
19+
{
20+
}
21+
22+
public static function instance() : self
23+
{
24+
return self::$instance ?: self::$instance = new self();
25+
}
26+
27+
public function getExternalPath() : Path
28+
{
29+
return (new Path())
30+
->move(-3.5, 3.5)
31+
->line(-3.5, 0)
32+
->ellipticArc(3.5, 3.5, 0, false, true, 0, -3.5)
33+
->line(3.5, -3.5)
34+
->line(3.5, 3.5)
35+
->close()
36+
->move(2.5, 0)
37+
->ellipticArc(2.5, 2.5, 0, false, true, 0, 2.5)
38+
->ellipticArc(2.5, 2.5, 0, false, true, -2.5, 0)
39+
->ellipticArc(2.5, 2.5, 0, false, true, 0, -2.5)
40+
->ellipticArc(2.5, 2.5, 0, false, true, 2.5, 0)
41+
->close()
42+
;
43+
}
44+
45+
public function getInternalPath() : Path
46+
{
47+
return (new Path())
48+
->move(1.5, 0)
49+
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
50+
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
51+
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
52+
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
53+
->close()
54+
;
55+
}
56+
}

src/Renderer/ImageRenderer.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,12 @@ private function drawEye(
112112
) : Path {
113113
if ($fill->inheritsBothColors()) {
114114
return $modulePath
115-
->append($externalPath->translate($xTranslation, $yTranslation))
116-
->append($internalPath->translate($xTranslation, $yTranslation));
115+
->append(
116+
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
117+
)
118+
->append(
119+
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
120+
);
117121
}
118122

119123
$this->imageBackEnd->push();
@@ -124,13 +128,17 @@ private function drawEye(
124128
}
125129

126130
if ($fill->inheritsExternalColor()) {
127-
$modulePath = $modulePath->append($externalPath->translate($xTranslation, $yTranslation));
131+
$modulePath = $modulePath->append(
132+
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
133+
);
128134
} else {
129135
$this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
130136
}
131137

132138
if ($fill->inheritsInternalColor()) {
133-
$modulePath = $modulePath->append($internalPath->translate($xTranslation, $yTranslation));
139+
$modulePath = $modulePath->append(
140+
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
141+
);
134142
} else {
135143
$this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
136144
}

src/Renderer/Path/Close.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,12 @@ public function translate(float $x, float $y) : OperationInterface
2323
{
2424
return $this;
2525
}
26+
27+
/**
28+
* @return self
29+
*/
30+
public function rotate(int $degrees) : OperationInterface
31+
{
32+
return $this;
33+
}
2634
}

src/Renderer/Path/Curve.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,28 @@ public function translate(float $x, float $y) : OperationInterface
5959
$this->y3 + $y
6060
);
6161
}
62+
63+
/**
64+
* @return self
65+
*/
66+
public function rotate(int $degrees) : OperationInterface
67+
{
68+
$radians = deg2rad($degrees);
69+
$sin = sin($radians);
70+
$cos = cos($radians);
71+
$x1r = $this->x1 * $cos - $this->y1 * $sin;
72+
$y1r = $this->x1 * $sin + $this->y1 * $cos;
73+
$x2r = $this->x2 * $cos - $this->y2 * $sin;
74+
$y2r = $this->x2 * $sin + $this->y2 * $cos;
75+
$x3r = $this->x3 * $cos - $this->y3 * $sin;
76+
$y3r = $this->x3 * $sin + $this->y3 * $cos;
77+
return new self(
78+
$x1r,
79+
$y1r,
80+
$x2r,
81+
$y2r,
82+
$x3r,
83+
$y3r
84+
);
85+
}
6286
}

src/Renderer/Path/EllipticArc.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ public function translate(float $x, float $y) : OperationInterface
7676
);
7777
}
7878

79+
/**
80+
* @return self
81+
*/
82+
public function rotate(int $degrees) : OperationInterface
83+
{
84+
$radians = deg2rad($degrees);
85+
$sin = sin($radians);
86+
$cos = cos($radians);
87+
$xr = $this->x * $cos - $this->y * $sin;
88+
$yr = $this->x * $sin + $this->y * $cos;
89+
return new self(
90+
$this->xRadius,
91+
$this->yRadius,
92+
$this->xAxisAngle,
93+
$this->largeArc,
94+
$this->sweep,
95+
$xr,
96+
$yr
97+
);
98+
}
99+
79100
/**
80101
* Converts the elliptic arc to multiple curves.
81102
*

src/Renderer/Path/Line.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,17 @@ public function translate(float $x, float $y) : OperationInterface
2626
{
2727
return new self($this->x + $x, $this->y + $y);
2828
}
29+
30+
/**
31+
* @return self
32+
*/
33+
public function rotate(int $degrees) : OperationInterface
34+
{
35+
$radians = deg2rad($degrees);
36+
$sin = sin($radians);
37+
$cos = cos($radians);
38+
$xr = $this->x * $cos - $this->y * $sin;
39+
$yr = $this->x * $sin + $this->y * $cos;
40+
return new self($xr, $yr);
41+
}
2942
}

src/Renderer/Path/Move.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,17 @@ public function translate(float $x, float $y) : OperationInterface
2626
{
2727
return new self($this->x + $x, $this->y + $y);
2828
}
29+
30+
/**
31+
* @return self
32+
*/
33+
public function rotate(int $degrees) : OperationInterface
34+
{
35+
$radians = deg2rad($degrees);
36+
$sin = sin($radians);
37+
$cos = cos($radians);
38+
$xr = $this->x * $cos - $this->y * $sin;
39+
$yr = $this->x * $sin + $this->y * $cos;
40+
return new self($xr, $yr);
41+
}
2942
}

src/Renderer/Path/OperationInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ interface OperationInterface
99
* Translates the operation's coordinates.
1010
*/
1111
public function translate(float $x, float $y) : self;
12+
13+
/**
14+
* Rotates the operation's coordinates.
15+
*/
16+
public function rotate(int $degrees) : self;
1217
}

src/Renderer/Path/Path.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ public function translate(float $x, float $y) : self
9494
return $path;
9595
}
9696

97+
public function rotate(int $degrees) : self
98+
{
99+
$path = new self();
100+
101+
foreach ($this->operations as $operation) {
102+
$path->operations[] = $operation->rotate($degrees);
103+
}
104+
105+
return $path;
106+
}
107+
97108
/**
98109
* @return Traversable<int, OperationInterface>
99110
*/

test/Integration/ImagickRenderingTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use BaconQrCode\Renderer\Color\Rgb;
77
use BaconQrCode\Renderer\Eye\SquareEye;
8+
use BaconQrCode\Renderer\Eye\PointyEye;
89
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
910
use BaconQrCode\Renderer\ImageRenderer;
1011
use BaconQrCode\Renderer\Module\SquareModule;
@@ -65,4 +66,46 @@ public function testIssue79() : void
6566
$this->assertMatchesFileSnapshot($tempName);
6667
unlink($tempName);
6768
}
69+
70+
public function testIssue105() : void
71+
{
72+
$squareModule = SquareModule::instance();
73+
$pointyEye = PointyEye::instance();
74+
75+
$renderer1 = new ImageRenderer(
76+
new RendererStyle(
77+
400,
78+
2,
79+
$squareModule,
80+
$pointyEye,
81+
Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(0, 0, 255))
82+
),
83+
new ImagickImageBackEnd()
84+
);
85+
$writer1 = new Writer($renderer1);
86+
$tempName1 = tempnam(sys_get_temp_dir(), 'test') . '.png';
87+
$writer1->writeFile('rotation without eye color', $tempName1);
88+
89+
$this->assertMatchesFileSnapshot($tempName1);
90+
unlink($tempName1);
91+
92+
$eyeFill = new EyeFill(new Rgb(255, 0, 0), new Rgb(0, 255, 0));
93+
94+
$renderer2 = new ImageRenderer(
95+
new RendererStyle(
96+
400,
97+
2,
98+
$squareModule,
99+
$pointyEye,
100+
Fill::withForegroundColor(new Rgb(255, 255, 255), new Rgb(0, 0, 255), $eyeFill, $eyeFill, $eyeFill)
101+
),
102+
new ImagickImageBackEnd()
103+
);
104+
$writer2 = new Writer($renderer2);
105+
$tempName2 = tempnam(sys_get_temp_dir(), 'test') . '.png';
106+
$writer2->writeFile('rotation with eye color', $tempName2);
107+
108+
$this->assertMatchesFileSnapshot($tempName2);
109+
unlink($tempName2);
110+
}
68111
}

0 commit comments

Comments
 (0)