|
2 | 2 |
|
3 | 3 | namespace SVG\Rasterization\Renderers;
|
4 | 4 |
|
| 5 | +use SVG\Rasterization\Path\ArcApproximator; |
5 | 6 | use SVG\Rasterization\Transform\Transform;
|
6 | 7 |
|
7 | 8 | /**
|
|
15 | 16 | * - float rx: the x radius of the corners.
|
16 | 17 | * - float ry: the y radius of the corners.
|
17 | 18 | */
|
18 |
| -class RectRenderer extends MultiPassRenderer |
| 19 | +class RectRenderer extends PolygonRenderer |
19 | 20 | {
|
| 21 | + private static $arc; |
| 22 | + |
20 | 23 | /**
|
21 | 24 | * @inheritdoc
|
22 | 25 | */
|
23 | 26 | protected function prepareRenderParams(array $options, Transform $transform)
|
24 | 27 | {
|
25 | 28 | $w = $options['width'];
|
26 | 29 | $h = $options['height'];
|
27 |
| - $transform->resize($w, $h); |
28 | 30 |
|
29 | 31 | if ($w <= 0 || $h <= 0) {
|
30 |
| - return array('empty' => true); |
| 32 | + return array( |
| 33 | + 'open' => false, |
| 34 | + 'points' => array(), |
| 35 | + 'fill-rule' => 'nonzero', |
| 36 | + ); |
31 | 37 | }
|
32 | 38 |
|
33 |
| - $x1 = $options['x']; |
34 |
| - $y1 = $options['y']; |
35 |
| - $transform->map($x1, $y1); |
36 |
| - |
37 | 39 | // Corner radii may at most be (width-1)/2 pixels long.
|
38 | 40 | // Anything larger than that and the circles start expanding beyond the rectangle.
|
39 | 41 | $rx = empty($options['rx']) ? 0 : $options['rx'];
|
40 |
| - $ry = empty($options['ry']) ? 0 : $options['ry']; |
41 |
| - $transform->resize($rx, $ry); |
42 | 42 | if ($rx > ($w - 1) / 2) {
|
43 | 43 | $rx = floor(($w - 1) / 2);
|
44 | 44 | }
|
45 | 45 | if ($rx < 0) {
|
46 | 46 | $rx = 0;
|
47 | 47 | }
|
| 48 | + $ry = empty($options['ry']) ? 0 : $options['ry']; |
48 | 49 | if ($ry > ($h - 1) / 2) {
|
49 | 50 | $ry = floor(($h - 1) / 2);
|
50 | 51 | }
|
51 | 52 | if ($ry < 0) {
|
52 | 53 | $ry = 0;
|
53 | 54 | }
|
54 | 55 |
|
55 |
| - return array( |
56 |
| - 'empty' => false, |
57 |
| - 'x1' => $x1, |
58 |
| - 'y1' => $y1, |
59 |
| - 'x2' => $x1 + $w - 1, |
60 |
| - 'y2' => $y1 + $h - 1, |
61 |
| - 'rx' => $rx, |
62 |
| - 'ry' => $ry, |
63 |
| - ); |
64 |
| - } |
65 |
| - |
66 |
| - /** |
67 |
| - * @inheritdoc |
68 |
| - */ |
69 |
| - protected function renderFill($image, array $params, $color) |
70 |
| - { |
71 |
| - if ($params['empty']) { |
72 |
| - return; |
73 |
| - } |
| 56 | + $x1 = $options['x']; |
| 57 | + $y1 = $options['y']; |
74 | 58 |
|
75 |
| - if ($params['rx'] != 0 || $params['ry'] != 0) { |
76 |
| - $this->renderFillRounded($image, $params, $color); |
77 |
| - return; |
78 |
| - } |
| 59 | + $points = $rx > 0 && $ry > 0 |
| 60 | + ? self::getPointsForRoundedRect($x1, $y1, $w, $h, $rx, $ry, $transform) |
| 61 | + : self::getPointsForRect($x1, $y1, $w, $h, $transform); |
79 | 62 |
|
80 |
| - imagefilledrectangle( |
81 |
| - $image, |
82 |
| - $params['x1'], |
83 |
| - $params['y1'], |
84 |
| - $params['x2'], |
85 |
| - $params['y2'], |
86 |
| - $color |
| 63 | + return array( |
| 64 | + 'open' => false, |
| 65 | + 'points' => $points, |
| 66 | + 'fill-rule' => 'nonzero', |
87 | 67 | );
|
88 | 68 | }
|
89 | 69 |
|
90 |
| - private function renderFillRounded($image, array $params, $color) |
| 70 | + private static function getPointsForRect($x1, $y1, $width, $height, Transform $transform) |
91 | 71 | {
|
92 |
| - $x1 = $params['x1']; |
93 |
| - $y1 = $params['y1']; |
94 |
| - $x2 = $params['x2']; |
95 |
| - $y2 = $params['y2']; |
96 |
| - $rx = $params['rx']; |
97 |
| - $ry = $params['ry']; |
98 |
| - |
99 |
| - // draws 3 non-overlapping rectangles so that transparency is preserved |
100 |
| - |
101 |
| - // full vertical area |
102 |
| - imagefilledrectangle($image, $x1 + $rx, $y1, $x2 - $rx, $y2, $color); |
103 |
| - // left side |
104 |
| - imagefilledrectangle($image, $x1, $y1 + $ry, $x1 + $rx - 1, $y2 - $ry, $color); |
105 |
| - // right side |
106 |
| - imagefilledrectangle($image, $x2 - $rx + 1, $y1 + $ry, $x2, $y2 - $ry, $color); |
107 |
| - |
108 |
| - // prepares a separate image containing the corners ellipse, which is |
109 |
| - // then copied onto $image at the corner positions |
110 |
| - |
111 |
| - $corners = imagecreatetruecolor($rx * 2 + 1, $ry * 2 + 1); |
112 |
| - imagealphablending($corners, true); |
113 |
| - imagesavealpha($corners, true); |
114 |
| - imagefill($corners, 0, 0, 0x7F000000); |
115 |
| - |
116 |
| - imagefilledellipse($corners, $rx, $ry, $rx * 2, $ry * 2, $color); |
| 72 | + $points = array(); |
117 | 73 |
|
118 |
| - // left-top |
119 |
| - imagecopy($image, $corners, $x1, $y1, 0, 0, $rx, $ry); |
120 |
| - // right-top |
121 |
| - imagecopy($image, $corners, $x2 - $rx + 1, $y1, $rx + 1, 0, $rx, $ry); |
122 |
| - // left-bottom |
123 |
| - imagecopy($image, $corners, $x1, $y2 - $ry + 1, 0, $ry + 1, $rx, $ry); |
124 |
| - // right-bottom |
125 |
| - imagecopy($image, $corners, $x2 - $rx + 1, $y2 - $ry + 1, $rx + 1, $ry + 1, $rx, $ry); |
| 74 | + $transform->mapInto($x1, $y1, $points); |
| 75 | + $transform->mapInto($x1 + $width, $y1, $points); |
| 76 | + $transform->mapInto($x1 + $width, $y1 + $height, $points); |
| 77 | + $transform->mapInto($x1, $y1 + $height, $points); |
126 | 78 |
|
127 |
| - imagedestroy($corners); |
| 79 | + return $points; |
128 | 80 | }
|
129 | 81 |
|
130 |
| - /** |
131 |
| - * @inheritdoc |
132 |
| - */ |
133 |
| - protected function renderStroke($image, array $params, $color, $strokeWidth) |
| 82 | + private static function getPointsForRoundedRect($x1, $y1, $width, $height, $rx, $ry, Transform $transform) |
134 | 83 | {
|
135 |
| - if ($params['empty']) { |
136 |
| - return; |
| 84 | + if (!isset(self::$arc)) { |
| 85 | + self::$arc = new ArcApproximator(); |
137 | 86 | }
|
138 | 87 |
|
139 |
| - imagesetthickness($image, round($strokeWidth)); |
140 |
| - |
141 |
| - if ($params['rx'] != 0 || $params['ry'] != 0) { |
142 |
| - $this->renderStrokeRounded($image, $params, $color, $strokeWidth); |
143 |
| - return; |
| 88 | + // guess a scale factor |
| 89 | + $scaledRx = $rx; |
| 90 | + $scaledRy = $ry; |
| 91 | + $transform->resize($scaledRx, $scaledRy); |
| 92 | + $scale = $rx == 0 || $ry == 0 ? 1.0 : hypot($scaledRx / $rx, $scaledRy / $ry); |
| 93 | + |
| 94 | + $points = array(); |
| 95 | + |
| 96 | + $topLeft = self::$arc->approximate( |
| 97 | + array($x1, $y1 + $ry), |
| 98 | + array($x1 + $rx, $y1), |
| 99 | + false, |
| 100 | + true, |
| 101 | + $rx, |
| 102 | + $ry, |
| 103 | + 0, |
| 104 | + $scale |
| 105 | + ); |
| 106 | + foreach ($topLeft as $point) { |
| 107 | + $transform->mapInto($point[0], $point[1], $points); |
144 | 108 | }
|
145 | 109 |
|
146 |
| - $x1 = $params['x1']; |
147 |
| - $y1 = $params['y1']; |
148 |
| - $x2 = $params['x2']; |
149 |
| - $y2 = $params['y2']; |
150 |
| - |
151 |
| - // imagerectangle draws left and right side 1px thicker than it should, |
152 |
| - // and drawing 4 lines instead doesn't work either because of |
153 |
| - // unpredictable positioning as well as overlaps, |
154 |
| - // so we draw four filled rectangles instead |
155 |
| - |
156 |
| - $halfStrokeFloor = floor($strokeWidth / 2); |
157 |
| - $halfStrokeCeil = ceil($strokeWidth / 2); |
158 |
| - |
159 |
| - // top |
160 |
| - imagefilledrectangle( |
161 |
| - $image, |
162 |
| - $x1 - $halfStrokeFloor, |
163 |
| - $y1 - $halfStrokeFloor, |
164 |
| - $x2 + $halfStrokeFloor, |
165 |
| - $y1 + $halfStrokeCeil - 1, |
166 |
| - $color |
167 |
| - ); |
168 |
| - // bottom |
169 |
| - imagefilledrectangle( |
170 |
| - $image, |
171 |
| - $x1 - $halfStrokeFloor, |
172 |
| - $y2 - $halfStrokeCeil + 1, |
173 |
| - $x2 + $halfStrokeFloor, |
174 |
| - $y2 + $halfStrokeFloor, |
175 |
| - $color |
| 110 | + $topRight = self::$arc->approximate( |
| 111 | + array($x1 + $width - $rx, $y1), |
| 112 | + array($x1 + $width, $y1 + $ry), |
| 113 | + false, |
| 114 | + true, |
| 115 | + $rx, |
| 116 | + $ry, |
| 117 | + 0, |
| 118 | + $scale |
176 | 119 | );
|
177 |
| - // left |
178 |
| - imagefilledrectangle( |
179 |
| - $image, |
180 |
| - $x1 - $halfStrokeFloor, |
181 |
| - $y1 + $halfStrokeCeil, |
182 |
| - $x1 + $halfStrokeCeil - 1, |
183 |
| - $y2 - $halfStrokeCeil, |
184 |
| - $color |
185 |
| - ); |
186 |
| - // right |
187 |
| - imagefilledrectangle( |
188 |
| - $image, |
189 |
| - $x2 - $halfStrokeCeil + 1, |
190 |
| - $y1 + $halfStrokeCeil, |
191 |
| - $x2 + $halfStrokeFloor, |
192 |
| - $y2 - $halfStrokeCeil, |
193 |
| - $color |
194 |
| - ); |
195 |
| - } |
196 |
| - |
197 |
| - private function renderStrokeRounded($image, array $params, $color, $strokeWidth) |
198 |
| - { |
199 |
| - $x1 = $params['x1']; |
200 |
| - $y1 = $params['y1']; |
201 |
| - $x2 = $params['x2']; |
202 |
| - $y2 = $params['y2']; |
203 |
| - $rx = $params['rx']; |
204 |
| - $ry = $params['ry']; |
205 |
| - |
206 |
| - $halfStrokeFloor = floor($strokeWidth / 2); |
207 |
| - $halfStrokeCeil = ceil($strokeWidth / 2); |
| 120 | + foreach ($topRight as $point) { |
| 121 | + $transform->mapInto($point[0], $point[1], $points); |
| 122 | + } |
208 | 123 |
|
209 |
| - // top |
210 |
| - imagefilledrectangle( |
211 |
| - $image, |
212 |
| - $x1 + $rx + 1, |
213 |
| - $y1 - $halfStrokeFloor, |
214 |
| - $x2 - $rx - 1, |
215 |
| - $y1 + $halfStrokeCeil - 1, |
216 |
| - $color |
217 |
| - ); |
218 |
| - // bottom |
219 |
| - imagefilledrectangle( |
220 |
| - $image, |
221 |
| - $x1 + $rx + 1, |
222 |
| - $y2 - $halfStrokeCeil + 1, |
223 |
| - $x2 - $rx - 1, |
224 |
| - $y2 + $halfStrokeFloor, |
225 |
| - $color |
226 |
| - ); |
227 |
| - // left |
228 |
| - imagefilledrectangle( |
229 |
| - $image, |
230 |
| - $x1 - $halfStrokeFloor, |
231 |
| - $y1 + $ry + 1, |
232 |
| - $x1 + $halfStrokeCeil - 1, |
233 |
| - $y2 - $ry - 1, |
234 |
| - $color |
| 124 | + $bottomRight = self::$arc->approximate( |
| 125 | + array($x1 + $width, $y1 + $height - $ry), |
| 126 | + array($x1 + $width - $rx, $y1 + $height), |
| 127 | + false, |
| 128 | + true, |
| 129 | + $rx, |
| 130 | + $ry, |
| 131 | + 0, |
| 132 | + $scale |
235 | 133 | );
|
236 |
| - // right |
237 |
| - imagefilledrectangle( |
238 |
| - $image, |
239 |
| - $x2 - $halfStrokeCeil + 1, |
240 |
| - $y1 + $ry + 1, |
241 |
| - $x2 + $halfStrokeFloor, |
242 |
| - $y2 - $ry - 1, |
243 |
| - $color |
244 |
| - ); |
245 |
| - |
246 |
| - imagesetthickness($image, 1); |
| 134 | + foreach ($bottomRight as $point) { |
| 135 | + $transform->mapInto($point[0], $point[1], $points); |
| 136 | + } |
247 | 137 |
|
248 |
| - for ($sw = -$halfStrokeFloor; $sw < $halfStrokeCeil; ++$sw) { |
249 |
| - $arcW = $rx * 2 + 1 + $sw * 2; |
250 |
| - $arcH = $ry * 2 + 1 + $sw * 2; |
251 |
| - // left-top |
252 |
| - imagearc($image, $x1 + $rx, $y1 + $ry, $arcW, $arcH, 180, 270, $color); |
253 |
| - // right-top |
254 |
| - imagearc($image, $x2 - $rx, $y1 + $ry, $arcW, $arcH, 270, 360, $color); |
255 |
| - // left-bottom |
256 |
| - imagearc($image, $x1 + $rx, $y2 - $ry, $arcW, $arcH, 90, 180, $color); |
257 |
| - // right-bottom |
258 |
| - imagearc($image, $x2 - $rx, $y2 - $ry, $arcW, $arcH, 0, 90, $color); |
| 138 | + $bottomLeft = self::$arc->approximate( |
| 139 | + array($x1 + $rx, $y1 + $height), |
| 140 | + array($x1, $y1 + $height - $ry), |
| 141 | + false, |
| 142 | + true, |
| 143 | + $rx, |
| 144 | + $ry, |
| 145 | + 0, |
| 146 | + $scale |
| 147 | + ); |
| 148 | + foreach ($bottomLeft as $point) { |
| 149 | + $transform->mapInto($point[0], $point[1], $points); |
259 | 150 | }
|
| 151 | + |
| 152 | + return $points; |
260 | 153 | }
|
261 | 154 | }
|
0 commit comments