Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement rect renderer on top of path painting algorithms #165

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 87 additions & 194 deletions src/Rasterization/Renderers/RectRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace SVG\Rasterization\Renderers;

use SVG\Rasterization\Path\ArcApproximator;
use SVG\Rasterization\Transform\Transform;

/**
Expand All @@ -15,247 +16,139 @@
* - 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
*/
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);
}
if ($ry < 0) {
$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;
}
}