From 19f6dfc87e42fb44f329f433ea1dce5d13089322 Mon Sep 17 00:00:00 2001 From: Fabian Meyer <3982806+meyfa@users.noreply.github.com> Date: Sun, 6 Mar 2022 18:44:29 +0100 Subject: [PATCH] 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. --- src/Rasterization/Renderers/RectRenderer.php | 281 ++++++------------- 1 file changed, 87 insertions(+), 194 deletions(-) diff --git a/src/Rasterization/Renderers/RectRenderer.php b/src/Rasterization/Renderers/RectRenderer.php index f10b592..d91b3b3 100644 --- a/src/Rasterization/Renderers/RectRenderer.php +++ b/src/Rasterization/Renderers/RectRenderer.php @@ -2,6 +2,7 @@ namespace SVG\Rasterization\Renderers; +use SVG\Rasterization\Path\ArcApproximator; use SVG\Rasterization\Transform\Transform; /** @@ -15,8 +16,10 @@ * - float rx: the x radius of the corners. * - float ry: the y radius of the corners. */ -class RectRenderer extends MultiPassRenderer +class RectRenderer extends PolygonRenderer { + private static $arc; + /** * @inheritdoc */ @@ -24,27 +27,25 @@ protected function prepareRenderParams(array $options, Transform $transform) { $w = $options['width']; $h = $options['height']; - $transform->resize($w, $h); if ($w <= 0 || $h <= 0) { - return array('empty' => true); + return array( + 'open' => false, + 'points' => array(), + 'fill-rule' => 'nonzero', + ); } - $x1 = $options['x']; - $y1 = $options['y']; - $transform->map($x1, $y1); - // Corner radii may at most be (width-1)/2 pixels long. // Anything larger than that and the circles start expanding beyond the rectangle. $rx = empty($options['rx']) ? 0 : $options['rx']; - $ry = empty($options['ry']) ? 0 : $options['ry']; - $transform->resize($rx, $ry); if ($rx > ($w - 1) / 2) { $rx = floor(($w - 1) / 2); } if ($rx < 0) { $rx = 0; } + $ry = empty($options['ry']) ? 0 : $options['ry']; if ($ry > ($h - 1) / 2) { $ry = floor(($h - 1) / 2); } @@ -52,210 +53,102 @@ protected function prepareRenderParams(array $options, Transform $transform) $ry = 0; } - return array( - 'empty' => false, - 'x1' => $x1, - 'y1' => $y1, - 'x2' => $x1 + $w - 1, - 'y2' => $y1 + $h - 1, - 'rx' => $rx, - 'ry' => $ry, - ); - } - - /** - * @inheritdoc - */ - protected function renderFill($image, array $params, $color) - { - if ($params['empty']) { - return; - } + $x1 = $options['x']; + $y1 = $options['y']; - if ($params['rx'] != 0 || $params['ry'] != 0) { - $this->renderFillRounded($image, $params, $color); - return; - } + $points = $rx > 0 && $ry > 0 + ? self::getPointsForRoundedRect($x1, $y1, $w, $h, $rx, $ry, $transform) + : self::getPointsForRect($x1, $y1, $w, $h, $transform); - imagefilledrectangle( - $image, - $params['x1'], - $params['y1'], - $params['x2'], - $params['y2'], - $color + return array( + 'open' => false, + 'points' => $points, + 'fill-rule' => 'nonzero', ); } - private function renderFillRounded($image, array $params, $color) + private static function getPointsForRect($x1, $y1, $width, $height, Transform $transform) { - $x1 = $params['x1']; - $y1 = $params['y1']; - $x2 = $params['x2']; - $y2 = $params['y2']; - $rx = $params['rx']; - $ry = $params['ry']; - - // draws 3 non-overlapping rectangles so that transparency is preserved - - // full vertical area - imagefilledrectangle($image, $x1 + $rx, $y1, $x2 - $rx, $y2, $color); - // left side - imagefilledrectangle($image, $x1, $y1 + $ry, $x1 + $rx - 1, $y2 - $ry, $color); - // right side - imagefilledrectangle($image, $x2 - $rx + 1, $y1 + $ry, $x2, $y2 - $ry, $color); - - // prepares a separate image containing the corners ellipse, which is - // then copied onto $image at the corner positions - - $corners = imagecreatetruecolor($rx * 2 + 1, $ry * 2 + 1); - imagealphablending($corners, true); - imagesavealpha($corners, true); - imagefill($corners, 0, 0, 0x7F000000); - - imagefilledellipse($corners, $rx, $ry, $rx * 2, $ry * 2, $color); + $points = array(); - // left-top - imagecopy($image, $corners, $x1, $y1, 0, 0, $rx, $ry); - // right-top - imagecopy($image, $corners, $x2 - $rx + 1, $y1, $rx + 1, 0, $rx, $ry); - // left-bottom - imagecopy($image, $corners, $x1, $y2 - $ry + 1, 0, $ry + 1, $rx, $ry); - // right-bottom - imagecopy($image, $corners, $x2 - $rx + 1, $y2 - $ry + 1, $rx + 1, $ry + 1, $rx, $ry); + $transform->mapInto($x1, $y1, $points); + $transform->mapInto($x1 + $width, $y1, $points); + $transform->mapInto($x1 + $width, $y1 + $height, $points); + $transform->mapInto($x1, $y1 + $height, $points); - imagedestroy($corners); + return $points; } - /** - * @inheritdoc - */ - protected function renderStroke($image, array $params, $color, $strokeWidth) + private static function getPointsForRoundedRect($x1, $y1, $width, $height, $rx, $ry, Transform $transform) { - if ($params['empty']) { - return; + if (!isset(self::$arc)) { + self::$arc = new ArcApproximator(); } - imagesetthickness($image, round($strokeWidth)); - - if ($params['rx'] != 0 || $params['ry'] != 0) { - $this->renderStrokeRounded($image, $params, $color, $strokeWidth); - return; + // guess a scale factor + $scaledRx = $rx; + $scaledRy = $ry; + $transform->resize($scaledRx, $scaledRy); + $scale = $rx == 0 || $ry == 0 ? 1.0 : hypot($scaledRx / $rx, $scaledRy / $ry); + + $points = array(); + + $topLeft = self::$arc->approximate( + array($x1, $y1 + $ry), + array($x1 + $rx, $y1), + false, + true, + $rx, + $ry, + 0, + $scale + ); + foreach ($topLeft as $point) { + $transform->mapInto($point[0], $point[1], $points); } - $x1 = $params['x1']; - $y1 = $params['y1']; - $x2 = $params['x2']; - $y2 = $params['y2']; - - // imagerectangle draws left and right side 1px thicker than it should, - // and drawing 4 lines instead doesn't work either because of - // unpredictable positioning as well as overlaps, - // so we draw four filled rectangles instead - - $halfStrokeFloor = floor($strokeWidth / 2); - $halfStrokeCeil = ceil($strokeWidth / 2); - - // top - imagefilledrectangle( - $image, - $x1 - $halfStrokeFloor, - $y1 - $halfStrokeFloor, - $x2 + $halfStrokeFloor, - $y1 + $halfStrokeCeil - 1, - $color - ); - // bottom - imagefilledrectangle( - $image, - $x1 - $halfStrokeFloor, - $y2 - $halfStrokeCeil + 1, - $x2 + $halfStrokeFloor, - $y2 + $halfStrokeFloor, - $color + $topRight = self::$arc->approximate( + array($x1 + $width - $rx, $y1), + array($x1 + $width, $y1 + $ry), + false, + true, + $rx, + $ry, + 0, + $scale ); - // left - imagefilledrectangle( - $image, - $x1 - $halfStrokeFloor, - $y1 + $halfStrokeCeil, - $x1 + $halfStrokeCeil - 1, - $y2 - $halfStrokeCeil, - $color - ); - // right - imagefilledrectangle( - $image, - $x2 - $halfStrokeCeil + 1, - $y1 + $halfStrokeCeil, - $x2 + $halfStrokeFloor, - $y2 - $halfStrokeCeil, - $color - ); - } - - private function renderStrokeRounded($image, array $params, $color, $strokeWidth) - { - $x1 = $params['x1']; - $y1 = $params['y1']; - $x2 = $params['x2']; - $y2 = $params['y2']; - $rx = $params['rx']; - $ry = $params['ry']; - - $halfStrokeFloor = floor($strokeWidth / 2); - $halfStrokeCeil = ceil($strokeWidth / 2); + foreach ($topRight as $point) { + $transform->mapInto($point[0], $point[1], $points); + } - // top - imagefilledrectangle( - $image, - $x1 + $rx + 1, - $y1 - $halfStrokeFloor, - $x2 - $rx - 1, - $y1 + $halfStrokeCeil - 1, - $color - ); - // bottom - imagefilledrectangle( - $image, - $x1 + $rx + 1, - $y2 - $halfStrokeCeil + 1, - $x2 - $rx - 1, - $y2 + $halfStrokeFloor, - $color - ); - // left - imagefilledrectangle( - $image, - $x1 - $halfStrokeFloor, - $y1 + $ry + 1, - $x1 + $halfStrokeCeil - 1, - $y2 - $ry - 1, - $color + $bottomRight = self::$arc->approximate( + array($x1 + $width, $y1 + $height - $ry), + array($x1 + $width - $rx, $y1 + $height), + false, + true, + $rx, + $ry, + 0, + $scale ); - // right - imagefilledrectangle( - $image, - $x2 - $halfStrokeCeil + 1, - $y1 + $ry + 1, - $x2 + $halfStrokeFloor, - $y2 - $ry - 1, - $color - ); - - imagesetthickness($image, 1); + foreach ($bottomRight as $point) { + $transform->mapInto($point[0], $point[1], $points); + } - for ($sw = -$halfStrokeFloor; $sw < $halfStrokeCeil; ++$sw) { - $arcW = $rx * 2 + 1 + $sw * 2; - $arcH = $ry * 2 + 1 + $sw * 2; - // left-top - imagearc($image, $x1 + $rx, $y1 + $ry, $arcW, $arcH, 180, 270, $color); - // right-top - imagearc($image, $x2 - $rx, $y1 + $ry, $arcW, $arcH, 270, 360, $color); - // left-bottom - imagearc($image, $x1 + $rx, $y2 - $ry, $arcW, $arcH, 90, 180, $color); - // right-bottom - imagearc($image, $x2 - $rx, $y2 - $ry, $arcW, $arcH, 0, 90, $color); + $bottomLeft = self::$arc->approximate( + array($x1 + $rx, $y1 + $height), + array($x1, $y1 + $height - $ry), + false, + true, + $rx, + $ry, + 0, + $scale + ); + foreach ($bottomLeft as $point) { + $transform->mapInto($point[0], $point[1], $points); } + + return $points; } }