Skip to content

Commit 19f6dfc

Browse files
committedMar 6, 2022
feat: Implement rect renderer on top of path painting algorithms
This implements one part of issue #149. By basing the renderer for rectangles off of the path painting algorithms, we can easily obtain rotated and skewed rectangles. This would be very difficult to do manually especially for rectangles with rounded corners. Now, we can simply use the arc approximator to construct a polygon. I was torn between basing the RectRenderer off of either the PathRenderer, which would be the obvious choice but would require needlessly constructing an intermediate command array, or the PolygonRenderer, which means we have to work with the ArcApproximator on a lower level but possibly get better performance. I chose the latter.
1 parent c8f3e14 commit 19f6dfc

File tree

1 file changed

+87
-194
lines changed

1 file changed

+87
-194
lines changed
 

‎src/Rasterization/Renderers/RectRenderer.php

+87-194
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace SVG\Rasterization\Renderers;
44

5+
use SVG\Rasterization\Path\ArcApproximator;
56
use SVG\Rasterization\Transform\Transform;
67

78
/**
@@ -15,247 +16,139 @@
1516
* - float rx: the x radius of the corners.
1617
* - float ry: the y radius of the corners.
1718
*/
18-
class RectRenderer extends MultiPassRenderer
19+
class RectRenderer extends PolygonRenderer
1920
{
21+
private static $arc;
22+
2023
/**
2124
* @inheritdoc
2225
*/
2326
protected function prepareRenderParams(array $options, Transform $transform)
2427
{
2528
$w = $options['width'];
2629
$h = $options['height'];
27-
$transform->resize($w, $h);
2830

2931
if ($w <= 0 || $h <= 0) {
30-
return array('empty' => true);
32+
return array(
33+
'open' => false,
34+
'points' => array(),
35+
'fill-rule' => 'nonzero',
36+
);
3137
}
3238

33-
$x1 = $options['x'];
34-
$y1 = $options['y'];
35-
$transform->map($x1, $y1);
36-
3739
// Corner radii may at most be (width-1)/2 pixels long.
3840
// Anything larger than that and the circles start expanding beyond the rectangle.
3941
$rx = empty($options['rx']) ? 0 : $options['rx'];
40-
$ry = empty($options['ry']) ? 0 : $options['ry'];
41-
$transform->resize($rx, $ry);
4242
if ($rx > ($w - 1) / 2) {
4343
$rx = floor(($w - 1) / 2);
4444
}
4545
if ($rx < 0) {
4646
$rx = 0;
4747
}
48+
$ry = empty($options['ry']) ? 0 : $options['ry'];
4849
if ($ry > ($h - 1) / 2) {
4950
$ry = floor(($h - 1) / 2);
5051
}
5152
if ($ry < 0) {
5253
$ry = 0;
5354
}
5455

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'];
7458

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);
7962

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',
8767
);
8868
}
8969

90-
private function renderFillRounded($image, array $params, $color)
70+
private static function getPointsForRect($x1, $y1, $width, $height, Transform $transform)
9171
{
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();
11773

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);
12678

127-
imagedestroy($corners);
79+
return $points;
12880
}
12981

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)
13483
{
135-
if ($params['empty']) {
136-
return;
84+
if (!isset(self::$arc)) {
85+
self::$arc = new ArcApproximator();
13786
}
13887

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);
144108
}
145109

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
176119
);
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+
}
208123

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
235133
);
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+
}
247137

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);
259150
}
151+
152+
return $points;
260153
}
261154
}

0 commit comments

Comments
 (0)
Please sign in to comment.