init commit

This commit is contained in:
2026-03-17 09:56:00 +08:00
commit e2c8ae752d
6827 changed files with 1211784 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Close implements OperationInterface
{
/**
* @var self|null
*/
private static $instance;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return $this;
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Curve implements OperationInterface
{
/**
* @var float
*/
private $x1;
/**
* @var float
*/
private $y1;
/**
* @var float
*/
private $x2;
/**
* @var float
*/
private $y2;
/**
* @var float
*/
private $x3;
/**
* @var float
*/
private $y3;
public function __construct(float $x1, float $y1, float $x2, float $y2, float $x3, float $y3)
{
$this->x1 = $x1;
$this->y1 = $y1;
$this->x2 = $x2;
$this->y2 = $y2;
$this->x3 = $x3;
$this->y3 = $y3;
}
public function getX1() : float
{
return $this->x1;
}
public function getY1() : float
{
return $this->y1;
}
public function getX2() : float
{
return $this->x2;
}
public function getY2() : float
{
return $this->y2;
}
public function getX3() : float
{
return $this->x3;
}
public function getY3() : float
{
return $this->y3;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->x1 + $x,
$this->y1 + $y,
$this->x2 + $x,
$this->y2 + $y,
$this->x3 + $x,
$this->y3 + $y
);
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class EllipticArc implements OperationInterface
{
private const ZERO_TOLERANCE = 1e-05;
/**
* @var float
*/
private $xRadius;
/**
* @var float
*/
private $yRadius;
/**
* @var float
*/
private $xAxisAngle;
/**
* @var bool
*/
private $largeArc;
/**
* @var bool
*/
private $sweep;
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
public function __construct(
float $xRadius,
float $yRadius,
float $xAxisAngle,
bool $largeArc,
bool $sweep,
float $x,
float $y
) {
$this->xRadius = abs($xRadius);
$this->yRadius = abs($yRadius);
$this->xAxisAngle = $xAxisAngle % 360;
$this->largeArc = $largeArc;
$this->sweep = $sweep;
$this->x = $x;
$this->y = $y;
}
public function getXRadius() : float
{
return $this->xRadius;
}
public function getYRadius() : float
{
return $this->yRadius;
}
public function getXAxisAngle() : float
{
return $this->xAxisAngle;
}
public function isLargeArc() : bool
{
return $this->largeArc;
}
public function isSweep() : bool
{
return $this->sweep;
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->xRadius,
$this->yRadius,
$this->xAxisAngle,
$this->largeArc,
$this->sweep,
$this->x + $x,
$this->y + $y
);
}
/**
* Converts the elliptic arc to multiple curves.
*
* Since not all image back ends support elliptic arcs, this method allows to convert the arc into multiple curves
* resembling the same result.
*
* @see https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
* @return array<Curve|Line>
*/
public function toCurves(float $fromX, float $fromY) : array
{
if (sqrt(($fromX - $this->x) ** 2 + ($fromY - $this->y) ** 2) < self::ZERO_TOLERANCE) {
return [];
}
if ($this->xRadius < self::ZERO_TOLERANCE || $this->yRadius < self::ZERO_TOLERANCE) {
return [new Line($this->x, $this->y)];
}
return $this->createCurves($fromX, $fromY);
}
/**
* @return Curve[]
*/
private function createCurves(float $fromX, $fromY) : array
{
$xAngle = deg2rad($this->xAxisAngle);
list($centerX, $centerY, $radiusX, $radiusY, $startAngle, $deltaAngle) =
$this->calculateCenterPointParameters($fromX, $fromY, $xAngle);
$s = $startAngle;
$e = $s + $deltaAngle;
$sign = ($e < $s) ? -1 : 1;
$remain = abs($e - $s);
$p1 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s);
$curves = [];
while ($remain > self::ZERO_TOLERANCE) {
$step = min($remain, pi() / 2);
$signStep = $step * $sign;
$p2 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s + $signStep);
$alphaT = tan($signStep / 2);
$alpha = sin($signStep) * (sqrt(4 + 3 * $alphaT ** 2) - 1) / 3;
$d1 = self::derivative($radiusX, $radiusY, $xAngle, $s);
$d2 = self::derivative($radiusX, $radiusY, $xAngle, $s + $signStep);
$curves[] = new Curve(
$p1[0] + $alpha * $d1[0],
$p1[1] + $alpha * $d1[1],
$p2[0] - $alpha * $d2[0],
$p2[1] - $alpha * $d2[1],
$p2[0],
$p2[1]
);
$s += $signStep;
$remain -= $step;
$p1 = $p2;
}
return $curves;
}
/**
* @return float[]
*/
private function calculateCenterPointParameters(float $fromX, float $fromY, float $xAngle)
{
$rX = $this->xRadius;
$rY = $this->yRadius;
// F.6.5.1
$dx2 = ($fromX - $this->x) / 2;
$dy2 = ($fromY - $this->y) / 2;
$x1p = cos($xAngle) * $dx2 + sin($xAngle) * $dy2;
$y1p = -sin($xAngle) * $dx2 + cos($xAngle) * $dy2;
// F.6.5.2
$rxs = $rX ** 2;
$rys = $rY ** 2;
$x1ps = $x1p ** 2;
$y1ps = $y1p ** 2;
$cr = $x1ps / $rxs + $y1ps / $rys;
if ($cr > 1) {
$s = sqrt($cr);
$rX *= $s;
$rY *= $s;
$rxs = $rX ** 2;
$rys = $rY ** 2;
}
$dq = ($rxs * $y1ps + $rys * $x1ps);
$pq = ($rxs * $rys - $dq) / $dq;
$q = sqrt(max(0, $pq));
if ($this->largeArc === $this->sweep) {
$q = -$q;
}
$cxp = $q * $rX * $y1p / $rY;
$cyp = -$q * $rY * $x1p / $rX;
// F.6.5.3
$cx = cos($xAngle) * $cxp - sin($xAngle) * $cyp + ($fromX + $this->x) / 2;
$cy = sin($xAngle) * $cxp + cos($xAngle) * $cyp + ($fromY + $this->y) / 2;
// F.6.5.5
$theta = self::angle(1, 0, ($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY);
// F.6.5.6
$delta = self::angle(($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY, (-$x1p - $cxp) / $rX, (-$y1p - $cyp) / $rY);
$delta = fmod($delta, pi() * 2);
if (! $this->sweep) {
$delta -= 2 * pi();
}
return [$cx, $cy, $rX, $rY, $theta, $delta];
}
private static function angle(float $ux, float $uy, float $vx, float $vy) : float
{
// F.6.5.4
$dot = $ux * $vx + $uy * $vy;
$length = sqrt($ux ** 2 + $uy ** 2) * sqrt($vx ** 2 + $vy ** 2);
$angle = acos(min(1, max(-1, $dot / $length)));
if (($ux * $vy - $uy * $vx) < 0) {
return -$angle;
}
return $angle;
}
/**
* @return float[]
*/
private static function point(
float $centerX,
float $centerY,
float $radiusX,
float $radiusY,
float $xAngle,
float $angle
) : array {
return [
$centerX + $radiusX * cos($xAngle) * cos($angle) - $radiusY * sin($xAngle) * sin($angle),
$centerY + $radiusX * sin($xAngle) * cos($angle) + $radiusY * cos($xAngle) * sin($angle),
];
}
/**
* @return float[]
*/
private static function derivative(float $radiusX, float $radiusY, float $xAngle, float $angle) : array
{
return [
-$radiusX * cos($xAngle) * sin($angle) - $radiusY * sin($xAngle) * cos($angle),
-$radiusX * sin($xAngle) * sin($angle) + $radiusY * cos($xAngle) * cos($angle),
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Line implements OperationInterface
{
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self($this->x + $x, $this->y + $y);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Move implements OperationInterface
{
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self($this->x + $x, $this->x + $y);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
interface OperationInterface
{
/**
* Translates the operation's coordinates.
*/
public function translate(float $x, float $y) : self;
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
use IteratorAggregate;
use Traversable;
/**
* Internal Representation of a vector path.
*/
final class Path implements IteratorAggregate
{
/**
* @var OperationInterface[]
*/
private $operations = [];
/**
* Moves the drawing operation to a certain position.
*/
public function move(float $x, float $y) : self
{
$path = clone $this;
$path->operations[] = new Move($x, $y);
return $path;
}
/**
* Draws a line from the current position to another position.
*/
public function line(float $x, float $y) : self
{
$path = clone $this;
$path->operations[] = new Line($x, $y);
return $path;
}
/**
* Draws an elliptic arc from the current position to another position.
*/
public function ellipticArc(
float $xRadius,
float $yRadius,
float $xAxisRotation,
bool $largeArc,
bool $sweep,
float $x,
float $y
) : self {
$path = clone $this;
$path->operations[] = new EllipticArc($xRadius, $yRadius, $xAxisRotation, $largeArc, $sweep, $x, $y);
return $path;
}
/**
* Draws a curve from the current position to another position.
*/
public function curve(float $x1, float $y1, float $x2, float $y2, float $x3, float $y3) : self
{
$path = clone $this;
$path->operations[] = new Curve($x1, $y1, $x2, $y2, $x3, $y3);
return $path;
}
/**
* Closes a sub-path.
*/
public function close() : self
{
$path = clone $this;
$path->operations[] = Close::instance();
return $path;
}
/**
* Appends another path to this one.
*/
public function append(self $other) : self
{
$path = clone $this;
$path->operations = array_merge($this->operations, $other->operations);
return $path;
}
public function translate(float $x, float $y) : self
{
$path = new self();
foreach ($this->operations as $operation) {
$path->operations[] = $operation->translate($x, $y);
}
return $path;
}
/**
* @return OperationInterface[]|Traversable
*/
public function getIterator() : Traversable
{
foreach ($this->operations as $operation) {
yield $operation;
}
}
}