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
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.
meyfa committed Mar 6, 2022
commit 19f6dfc87e42fb44f329f433ea1dce5d13089322
281 changes: 87 additions & 194 deletions src/Rasterization/Renderers/RectRenderer.php
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

namespace SVG\Rasterization\Renderers;

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

/**
@@ -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;
}
}