|
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