| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 | <?phpdeclare(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()        );    }}
 |