init commit
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\Cmyk;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Color\Gray;
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Path\Close;
|
||||
use BaconQrCode\Renderer\Path\Curve;
|
||||
use BaconQrCode\Renderer\Path\EllipticArc;
|
||||
use BaconQrCode\Renderer\Path\Line;
|
||||
use BaconQrCode\Renderer\Path\Move;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
use BaconQrCode\Renderer\RendererStyle\GradientType;
|
||||
|
||||
final class EpsImageBackEnd implements ImageBackEndInterface
|
||||
{
|
||||
private const PRECISION = 3;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $eps;
|
||||
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void
|
||||
{
|
||||
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
|
||||
. "%%Creator: BaconQrCode\n"
|
||||
. sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
|
||||
. "%%BeginProlog\n"
|
||||
. "save\n"
|
||||
. "50 dict begin\n"
|
||||
. "/q { gsave } bind def\n"
|
||||
. "/Q { grestore } bind def\n"
|
||||
. "/s { scale } bind def\n"
|
||||
. "/t { translate } bind def\n"
|
||||
. "/r { rotate } bind def\n"
|
||||
. "/n { newpath } bind def\n"
|
||||
. "/m { moveto } bind def\n"
|
||||
. "/l { lineto } bind def\n"
|
||||
. "/c { curveto } bind def\n"
|
||||
. "/z { closepath } bind def\n"
|
||||
. "/f { eofill } bind def\n"
|
||||
. "/rgb { setrgbcolor } bind def\n"
|
||||
. "/cmyk { setcmykcolor } bind def\n"
|
||||
. "/gray { setgray } bind def\n"
|
||||
. "%%EndProlog\n"
|
||||
. "1 -1 s\n"
|
||||
. sprintf("0 -%d t\n", $size);
|
||||
|
||||
if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->eps .= wordwrap(
|
||||
'0 0 m'
|
||||
. sprintf(' %s 0 l', (string) $size)
|
||||
. sprintf(' %s %s l', (string) $size, (string) $size)
|
||||
. sprintf(' 0 %s l', (string) $size)
|
||||
. ' z'
|
||||
. ' ' .$this->getColorSetString($backgroundColor) . " f\n",
|
||||
75,
|
||||
"\n "
|
||||
);
|
||||
}
|
||||
|
||||
public function scale(float $size) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
|
||||
}
|
||||
|
||||
public function translate(float $x, float $y) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
|
||||
}
|
||||
|
||||
public function rotate(int $degrees) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= sprintf("%d r\n", $degrees);
|
||||
}
|
||||
|
||||
public function push() : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= "q\n";
|
||||
}
|
||||
|
||||
public function pop() : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= "Q\n";
|
||||
}
|
||||
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$fromX = 0;
|
||||
$fromY = 0;
|
||||
$this->eps .= wordwrap(
|
||||
'n '
|
||||
. $this->drawPathOperations($path, $fromX, $fromY)
|
||||
. ' ' . $this->getColorSetString($color) . " f\n",
|
||||
75,
|
||||
"\n "
|
||||
);
|
||||
}
|
||||
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void {
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$fromX = 0;
|
||||
$fromY = 0;
|
||||
$this->eps .= wordwrap(
|
||||
'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
|
||||
75,
|
||||
"\n "
|
||||
);
|
||||
|
||||
$this->createGradientFill($gradient, $x, $y, $width, $height);
|
||||
}
|
||||
|
||||
public function done() : string
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= "%%TRAILER\nend restore\n%%EOF";
|
||||
$blob = $this->eps;
|
||||
$this->eps = null;
|
||||
|
||||
return $blob;
|
||||
}
|
||||
|
||||
private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
|
||||
{
|
||||
$pathData = [];
|
||||
|
||||
foreach ($ops as $op) {
|
||||
switch (true) {
|
||||
case $op instanceof Move:
|
||||
$fromX = $toX = round($op->getX(), self::PRECISION);
|
||||
$fromY = $toY = round($op->getY(), self::PRECISION);
|
||||
$pathData[] = sprintf('%s %s m', $toX, $toY);
|
||||
break;
|
||||
|
||||
case $op instanceof Line:
|
||||
$fromX = $toX = round($op->getX(), self::PRECISION);
|
||||
$fromY = $toY = round($op->getY(), self::PRECISION);
|
||||
$pathData[] = sprintf('%s %s l', $toX, $toY);
|
||||
break;
|
||||
|
||||
case $op instanceof EllipticArc:
|
||||
$pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
|
||||
break;
|
||||
|
||||
case $op instanceof Curve:
|
||||
$x1 = round($op->getX1(), self::PRECISION);
|
||||
$y1 = round($op->getY1(), self::PRECISION);
|
||||
$x2 = round($op->getX2(), self::PRECISION);
|
||||
$y2 = round($op->getY2(), self::PRECISION);
|
||||
$fromX = $x3 = round($op->getX3(), self::PRECISION);
|
||||
$fromY = $y3 = round($op->getY3(), self::PRECISION);
|
||||
$pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
|
||||
break;
|
||||
|
||||
case $op instanceof Close:
|
||||
$pathData[] = 'z';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $pathData);
|
||||
}
|
||||
|
||||
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
|
||||
{
|
||||
$startColor = $gradient->getStartColor();
|
||||
$endColor = $gradient->getEndColor();
|
||||
|
||||
if ($startColor instanceof Alpha) {
|
||||
$startColor = $startColor->getBaseColor();
|
||||
}
|
||||
|
||||
$startColorType = get_class($startColor);
|
||||
|
||||
if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
|
||||
$startColorType = Cmyk::class;
|
||||
$startColor = $startColor->toCmyk();
|
||||
}
|
||||
|
||||
if (get_class($endColor) !== $startColorType) {
|
||||
switch ($startColorType) {
|
||||
case Cmyk::class:
|
||||
$endColor = $endColor->toCmyk();
|
||||
break;
|
||||
|
||||
case Rgb::class:
|
||||
$endColor = $endColor->toRgb();
|
||||
break;
|
||||
|
||||
case Gray::class:
|
||||
$endColor = $endColor->toGray();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->eps .= "eoclip\n<<\n";
|
||||
|
||||
if ($gradient->getType() === GradientType::RADIAL()) {
|
||||
$this->eps .= " /ShadingType 3\n";
|
||||
} else {
|
||||
$this->eps .= " /ShadingType 2\n";
|
||||
}
|
||||
|
||||
$this->eps .= " /Extend [ true true ]\n"
|
||||
. " /AntiAlias true\n";
|
||||
|
||||
switch ($startColorType) {
|
||||
case Cmyk::class:
|
||||
$this->eps .= " /ColorSpace /DeviceCMYK\n";
|
||||
break;
|
||||
|
||||
case Rgb::class:
|
||||
$this->eps .= " /ColorSpace /DeviceRGB\n";
|
||||
break;
|
||||
|
||||
case Gray::class:
|
||||
$this->eps .= " /ColorSpace /DeviceGray\n";
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($gradient->getType()) {
|
||||
case GradientType::HORIZONTAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y, self::PRECISION),
|
||||
round($x + $width, self::PRECISION),
|
||||
round($y, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::VERTICAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y, self::PRECISION),
|
||||
round($x, self::PRECISION),
|
||||
round($y + $height, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::DIAGONAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y, self::PRECISION),
|
||||
round($x + $width, self::PRECISION),
|
||||
round($y + $height, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::INVERSE_DIAGONAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y + $height, self::PRECISION),
|
||||
round($x + $width, self::PRECISION),
|
||||
round($y, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::RADIAL():
|
||||
$centerX = ($x + $width) / 2;
|
||||
$centerY = ($y + $height) / 2;
|
||||
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s 0 %s %s %s ]\n",
|
||||
round($centerX, self::PRECISION),
|
||||
round($centerY, self::PRECISION),
|
||||
round($centerX, self::PRECISION),
|
||||
round($centerY, self::PRECISION),
|
||||
round(max($width, $height) / 2, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
$this->eps .= " /Function\n"
|
||||
. " <<\n"
|
||||
. " /FunctionType 2\n"
|
||||
. " /Domain [ 0 1 ]\n"
|
||||
. sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor))
|
||||
. sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor))
|
||||
. " /N 1\n"
|
||||
. " >>\n>>\nshfill\nQ\n";
|
||||
}
|
||||
|
||||
private function getColorSetString(ColorInterface $color) : string
|
||||
{
|
||||
if ($color instanceof Rgb) {
|
||||
return $this->getColorString($color) . ' rgb';
|
||||
}
|
||||
|
||||
if ($color instanceof Cmyk) {
|
||||
return $this->getColorString($color) . ' cmyk';
|
||||
}
|
||||
|
||||
if ($color instanceof Gray) {
|
||||
return $this->getColorString($color) . ' gray';
|
||||
}
|
||||
|
||||
return $this->getColorSetString($color->toCmyk());
|
||||
}
|
||||
|
||||
private function getColorString(ColorInterface $color) : string
|
||||
{
|
||||
if ($color instanceof Rgb) {
|
||||
return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
|
||||
}
|
||||
|
||||
if ($color instanceof Cmyk) {
|
||||
return sprintf(
|
||||
'%s %s %s %s',
|
||||
$color->getCyan() / 100,
|
||||
$color->getMagenta() / 100,
|
||||
$color->getYellow() / 100,
|
||||
$color->getBlack() / 100
|
||||
);
|
||||
}
|
||||
|
||||
if ($color instanceof Gray) {
|
||||
return sprintf('%s', $color->getGray() / 100);
|
||||
}
|
||||
|
||||
return $this->getColorString($color->toCmyk());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
|
||||
/**
|
||||
* Interface for back ends able to to produce path based images.
|
||||
*/
|
||||
interface ImageBackEndInterface
|
||||
{
|
||||
/**
|
||||
* Starts a new image.
|
||||
*
|
||||
* If a previous image was already started, previous data get erased.
|
||||
*/
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void;
|
||||
|
||||
/**
|
||||
* Transforms all following drawing operation coordinates by scaling them by a given factor.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function scale(float $size) : void;
|
||||
|
||||
/**
|
||||
* Transforms all following drawing operation coordinates by translating them by a given amount.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function translate(float $x, float $y) : void;
|
||||
|
||||
/**
|
||||
* Transforms all following drawing operation coordinates by rotating them by a given amount.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function rotate(int $degrees) : void;
|
||||
|
||||
/**
|
||||
* Pushes the current coordinate transformation onto a stack.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function push() : void;
|
||||
|
||||
/**
|
||||
* Pops the last coordinate transformation from a stack.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function pop() : void;
|
||||
|
||||
/**
|
||||
* Draws a path with a given color.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void;
|
||||
|
||||
/**
|
||||
* Draws a path with a given gradient which spans the box described by the position and size.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void;
|
||||
|
||||
/**
|
||||
* Ends the image drawing operation and returns the resulting blob.
|
||||
*
|
||||
* This should reset the state of the back end and thus this method should only be callable once per image.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function done() : string;
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\Cmyk;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Color\Gray;
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Path\Close;
|
||||
use BaconQrCode\Renderer\Path\Curve;
|
||||
use BaconQrCode\Renderer\Path\EllipticArc;
|
||||
use BaconQrCode\Renderer\Path\Line;
|
||||
use BaconQrCode\Renderer\Path\Move;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
use BaconQrCode\Renderer\RendererStyle\GradientType;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
|
||||
final class ImagickImageBackEnd implements ImageBackEndInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $imageFormat;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $compressionQuality;
|
||||
|
||||
/**
|
||||
* @var Imagick|null
|
||||
*/
|
||||
private $image;
|
||||
|
||||
/**
|
||||
* @var ImagickDraw|null
|
||||
*/
|
||||
private $draw;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $gradientCount;
|
||||
|
||||
/**
|
||||
* @var TransformationMatrix[]|null
|
||||
*/
|
||||
private $matrices;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $matrixIndex;
|
||||
|
||||
public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
|
||||
{
|
||||
if (! class_exists(Imagick::class)) {
|
||||
throw new RuntimeException('You need to install the imagick extension to use this back end');
|
||||
}
|
||||
|
||||
$this->imageFormat = $imageFormat;
|
||||
$this->compressionQuality = $compressionQuality;
|
||||
}
|
||||
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void
|
||||
{
|
||||
$this->image = new Imagick();
|
||||
$this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
|
||||
$this->image->setImageFormat($this->imageFormat);
|
||||
$this->image->setCompressionQuality($this->compressionQuality);
|
||||
$this->draw = new ImagickDraw();
|
||||
$this->gradientCount = 0;
|
||||
$this->matrices = [new TransformationMatrix()];
|
||||
$this->matrixIndex = 0;
|
||||
}
|
||||
|
||||
public function scale(float $size) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->scale($size, $size);
|
||||
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
|
||||
->multiply(TransformationMatrix::scale($size));
|
||||
}
|
||||
|
||||
public function translate(float $x, float $y) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->translate($x, $y);
|
||||
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
|
||||
->multiply(TransformationMatrix::translate($x, $y));
|
||||
}
|
||||
|
||||
public function rotate(int $degrees) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->rotate($degrees);
|
||||
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
|
||||
->multiply(TransformationMatrix::rotate($degrees));
|
||||
}
|
||||
|
||||
public function push() : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->push();
|
||||
$this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
|
||||
}
|
||||
|
||||
public function pop() : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->pop();
|
||||
unset($this->matrices[$this->matrixIndex--]);
|
||||
}
|
||||
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->setFillColor($this->getColorPixel($color));
|
||||
$this->drawPath($path);
|
||||
}
|
||||
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void {
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
|
||||
$this->drawPath($path);
|
||||
}
|
||||
|
||||
public function done() : string
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->image->drawImage($this->draw);
|
||||
$blob = $this->image->getImageBlob();
|
||||
$this->draw->clear();
|
||||
$this->image->clear();
|
||||
$this->draw = null;
|
||||
$this->image = null;
|
||||
$this->gradientCount = null;
|
||||
|
||||
return $blob;
|
||||
}
|
||||
|
||||
private function drawPath(Path $path) : void
|
||||
{
|
||||
$this->draw->pathStart();
|
||||
|
||||
foreach ($path as $op) {
|
||||
switch (true) {
|
||||
case $op instanceof Move:
|
||||
$this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
|
||||
break;
|
||||
|
||||
case $op instanceof Line:
|
||||
$this->draw->pathLineToAbsolute($op->getX(), $op->getY());
|
||||
break;
|
||||
|
||||
case $op instanceof EllipticArc:
|
||||
$this->draw->pathEllipticArcAbsolute(
|
||||
$op->getXRadius(),
|
||||
$op->getYRadius(),
|
||||
$op->getXAxisAngle(),
|
||||
$op->isLargeArc(),
|
||||
$op->isSweep(),
|
||||
$op->getX(),
|
||||
$op->getY()
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Curve:
|
||||
$this->draw->pathCurveToAbsolute(
|
||||
$op->getX1(),
|
||||
$op->getY1(),
|
||||
$op->getX2(),
|
||||
$op->getY2(),
|
||||
$op->getX3(),
|
||||
$op->getY3()
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Close:
|
||||
$this->draw->pathClose();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
|
||||
}
|
||||
}
|
||||
|
||||
$this->draw->pathFinish();
|
||||
}
|
||||
|
||||
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
|
||||
{
|
||||
list($width, $height) = $this->matrices[$this->matrixIndex]->apply($x + $width, $y + $height);
|
||||
list($x, $y) = $this->matrices[$this->matrixIndex]->apply($x, $y);
|
||||
$width -= $x;
|
||||
$height -= $y;
|
||||
|
||||
$startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
|
||||
$endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
|
||||
$gradientImage = new Imagick();
|
||||
|
||||
switch ($gradient->getType()) {
|
||||
case GradientType::HORIZONTAL():
|
||||
$gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
|
||||
'gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
$gradientImage->rotateImage('transparent', -90);
|
||||
break;
|
||||
|
||||
case GradientType::VERTICAL():
|
||||
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
|
||||
'gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
break;
|
||||
|
||||
case GradientType::DIAGONAL():
|
||||
case GradientType::INVERSE_DIAGONAL():
|
||||
$gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
|
||||
'gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
|
||||
if (GradientType::DIAGONAL() === $gradient->getType()) {
|
||||
$gradientImage->rotateImage('transparent', -45);
|
||||
} else {
|
||||
$gradientImage->rotateImage('transparent', -135);
|
||||
}
|
||||
|
||||
$rotatedWidth = $gradientImage->getImageWidth();
|
||||
$rotatedHeight = $gradientImage->getImageHeight();
|
||||
|
||||
$gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
|
||||
$gradientImage->cropImage(
|
||||
intdiv($rotatedWidth, 2) - 2,
|
||||
intdiv($rotatedHeight, 2) - 2,
|
||||
intdiv($rotatedWidth, 4) + 1,
|
||||
intdiv($rotatedWidth, 4) + 1
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::RADIAL():
|
||||
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
|
||||
'radial-gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
$id = sprintf('g%d', ++$this->gradientCount);
|
||||
$this->draw->pushPattern($id, 0, 0, $x + $width, $y + $height);
|
||||
$this->draw->composite(Imagick::COMPOSITE_COPY, $x, $y, $width, $height, $gradientImage);
|
||||
$this->draw->popPattern();
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function getColorPixel(ColorInterface $color) : ImagickPixel
|
||||
{
|
||||
$alpha = 100;
|
||||
|
||||
if ($color instanceof Alpha) {
|
||||
$alpha = $color->getAlpha();
|
||||
$color = $color->getBaseColor();
|
||||
}
|
||||
|
||||
if ($color instanceof Rgb) {
|
||||
return new ImagickPixel(sprintf(
|
||||
'rgba(%d, %d, %d, %F)',
|
||||
$color->getRed(),
|
||||
$color->getGreen(),
|
||||
$color->getBlue(),
|
||||
$alpha / 100
|
||||
));
|
||||
}
|
||||
|
||||
if ($color instanceof Cmyk) {
|
||||
return new ImagickPixel(sprintf(
|
||||
'cmyka(%d, %d, %d, %d, %F)',
|
||||
$color->getCyan(),
|
||||
$color->getMagenta(),
|
||||
$color->getYellow(),
|
||||
$color->getBlack(),
|
||||
$alpha / 100
|
||||
));
|
||||
}
|
||||
|
||||
if ($color instanceof Gray) {
|
||||
return new ImagickPixel(sprintf(
|
||||
'graya(%d%%, %F)',
|
||||
$color->getGray(),
|
||||
$alpha / 100
|
||||
));
|
||||
}
|
||||
|
||||
return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Path\Close;
|
||||
use BaconQrCode\Renderer\Path\Curve;
|
||||
use BaconQrCode\Renderer\Path\EllipticArc;
|
||||
use BaconQrCode\Renderer\Path\Line;
|
||||
use BaconQrCode\Renderer\Path\Move;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
use BaconQrCode\Renderer\RendererStyle\GradientType;
|
||||
use XMLWriter;
|
||||
|
||||
final class SvgImageBackEnd implements ImageBackEndInterface
|
||||
{
|
||||
private const PRECISION = 3;
|
||||
|
||||
/**
|
||||
* @var XMLWriter|null
|
||||
*/
|
||||
private $xmlWriter;
|
||||
|
||||
/**
|
||||
* @var int[]|null
|
||||
*/
|
||||
private $stack;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $currentStack;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $gradientCount;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (! class_exists(XMLWriter::class)) {
|
||||
throw new RuntimeException('You need to install the libxml extension to use this back end');
|
||||
}
|
||||
}
|
||||
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void
|
||||
{
|
||||
$this->xmlWriter = new XMLWriter();
|
||||
$this->xmlWriter->openMemory();
|
||||
|
||||
$this->xmlWriter->startDocument('1.0', 'UTF-8');
|
||||
$this->xmlWriter->startElement('svg');
|
||||
$this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
$this->xmlWriter->writeAttribute('version', '1.1');
|
||||
$this->xmlWriter->writeAttribute('width', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('height', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
|
||||
|
||||
$this->gradientCount = 0;
|
||||
$this->currentStack = 0;
|
||||
$this->stack[0] = 0;
|
||||
|
||||
$alpha = 1;
|
||||
|
||||
if ($backgroundColor instanceof Alpha) {
|
||||
$alpha = $backgroundColor->getAlpha() / 100;
|
||||
}
|
||||
|
||||
if (0 === $alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('rect');
|
||||
$this->xmlWriter->writeAttribute('x', '0');
|
||||
$this->xmlWriter->writeAttribute('y', '0');
|
||||
$this->xmlWriter->writeAttribute('width', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('height', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
|
||||
|
||||
if ($alpha < 1) {
|
||||
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
public function scale(float $size) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->xmlWriter->writeAttribute(
|
||||
'transform',
|
||||
sprintf('scale(%s)', round($size, self::PRECISION))
|
||||
);
|
||||
++$this->stack[$this->currentStack];
|
||||
}
|
||||
|
||||
public function translate(float $x, float $y) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->xmlWriter->writeAttribute(
|
||||
'transform',
|
||||
sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION))
|
||||
);
|
||||
++$this->stack[$this->currentStack];
|
||||
}
|
||||
|
||||
public function rotate(int $degrees) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
|
||||
++$this->stack[$this->currentStack];
|
||||
}
|
||||
|
||||
public function push() : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->stack[] = 1;
|
||||
++$this->currentStack;
|
||||
}
|
||||
|
||||
public function pop() : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
array_pop($this->stack);
|
||||
--$this->currentStack;
|
||||
}
|
||||
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$alpha = 1;
|
||||
|
||||
if ($color instanceof Alpha) {
|
||||
$alpha = $color->getAlpha() / 100;
|
||||
}
|
||||
|
||||
$this->startPathElement($path);
|
||||
$this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
|
||||
|
||||
if ($alpha < 1) {
|
||||
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void {
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
|
||||
$this->startPathElement($path);
|
||||
$this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
public function done() : string
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
foreach ($this->stack as $openElements) {
|
||||
for ($i = $openElements; $i > 0; --$i) {
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
$this->xmlWriter->endDocument();
|
||||
$blob = $this->xmlWriter->outputMemory(true);
|
||||
$this->xmlWriter = null;
|
||||
$this->stack = null;
|
||||
$this->currentStack = null;
|
||||
$this->gradientCount = null;
|
||||
|
||||
return $blob;
|
||||
}
|
||||
|
||||
private function startPathElement(Path $path) : void
|
||||
{
|
||||
$pathData = [];
|
||||
|
||||
foreach ($path as $op) {
|
||||
switch (true) {
|
||||
case $op instanceof Move:
|
||||
$pathData[] = sprintf(
|
||||
'M%s %s',
|
||||
round($op->getX(), self::PRECISION),
|
||||
round($op->getY(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Line:
|
||||
$pathData[] = sprintf(
|
||||
'L%s %s',
|
||||
round($op->getX(), self::PRECISION),
|
||||
round($op->getY(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof EllipticArc:
|
||||
$pathData[] = sprintf(
|
||||
'A%s %s %s %u %u %s %s',
|
||||
round($op->getXRadius(), self::PRECISION),
|
||||
round($op->getYRadius(), self::PRECISION),
|
||||
round($op->getXAxisAngle(), self::PRECISION),
|
||||
$op->isLargeArc(),
|
||||
$op->isSweep(),
|
||||
round($op->getX(), self::PRECISION),
|
||||
round($op->getY(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Curve:
|
||||
$pathData[] = sprintf(
|
||||
'C%s %s %s %s %s %s',
|
||||
round($op->getX1(), self::PRECISION),
|
||||
round($op->getY1(), self::PRECISION),
|
||||
round($op->getX2(), self::PRECISION),
|
||||
round($op->getY2(), self::PRECISION),
|
||||
round($op->getX3(), self::PRECISION),
|
||||
round($op->getY3(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Close:
|
||||
$pathData[] = 'Z';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
|
||||
}
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('path');
|
||||
$this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
|
||||
$this->xmlWriter->writeAttribute('d', implode('', $pathData));
|
||||
}
|
||||
|
||||
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
|
||||
{
|
||||
$this->xmlWriter->startElement('defs');
|
||||
|
||||
$startColor = $gradient->getStartColor();
|
||||
$endColor = $gradient->getEndColor();
|
||||
|
||||
if ($gradient->getType() === GradientType::RADIAL()) {
|
||||
$this->xmlWriter->startElement('radialGradient');
|
||||
} else {
|
||||
$this->xmlWriter->startElement('linearGradient');
|
||||
}
|
||||
|
||||
$this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
|
||||
|
||||
switch ($gradient->getType()) {
|
||||
case GradientType::HORIZONTAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::VERTICAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::DIAGONAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::INVERSE_DIAGONAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::RADIAL():
|
||||
$this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
|
||||
break;
|
||||
}
|
||||
|
||||
$id = sprintf('g%d', ++$this->gradientCount);
|
||||
$this->xmlWriter->writeAttribute('id', $id);
|
||||
|
||||
$this->xmlWriter->startElement('stop');
|
||||
$this->xmlWriter->writeAttribute('offset', '0%');
|
||||
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
|
||||
|
||||
if ($startColor instanceof Alpha) {
|
||||
$this->xmlWriter->writeAttribute('stop-opacity', $startColor->getAlpha());
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
|
||||
$this->xmlWriter->startElement('stop');
|
||||
$this->xmlWriter->writeAttribute('offset', '100%');
|
||||
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
|
||||
|
||||
if ($endColor instanceof Alpha) {
|
||||
$this->xmlWriter->writeAttribute('stop-opacity', $endColor->getAlpha());
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
$this->xmlWriter->endElement();
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function getColorString(ColorInterface $color) : string
|
||||
{
|
||||
$color = $color->toRgb();
|
||||
|
||||
return sprintf(
|
||||
'#%02x%02x%02x',
|
||||
$color->getRed(),
|
||||
$color->getGreen(),
|
||||
$color->getBlue()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
final class TransformationMatrix
|
||||
{
|
||||
/**
|
||||
* @var float[]
|
||||
*/
|
||||
private $values;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->values = [1, 0, 0, 1, 0, 0];
|
||||
}
|
||||
|
||||
public function multiply(self $other) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
|
||||
$matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
|
||||
$matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
|
||||
$matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
|
||||
$matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
|
||||
+ $this->values[4];
|
||||
$matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
|
||||
+ $this->values[5];
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
public static function scale(float $size) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$matrix->values = [$size, 0, 0, $size, 0, 0];
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
public static function translate(float $x, float $y) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$matrix->values = [1, 0, 0, 1, $x, $y];
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
public static function rotate(int $degrees) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$rad = deg2rad($degrees);
|
||||
$matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies this matrix onto a point and returns the resulting viewport point.
|
||||
*
|
||||
* @return float[]
|
||||
*/
|
||||
public function apply(float $x, float $y) : array
|
||||
{
|
||||
return [
|
||||
$x * $this->values[0] + $y * $this->values[2] + $this->values[4],
|
||||
$x * $this->values[1] + $y * $this->values[3] + $this->values[5],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user