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; } }