Last active
September 2, 2024 13:22
-
-
Save ruby232/18c80d61b281d35ac8903e4c768226eb to your computer and use it in GitHub Desktop.
Custom configuration for a SVG QR based on points and the position pattern with colors.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$result = Builder::create() | |
->writer(new CustomSvgWriter()) | |
->writerOptions(['compact' => false, 'style' => 'dot']) | |
->data('text To QR') | |
->encoding(new Encoding('UTF-8')) | |
->errorCorrectionLevel(ErrorCorrectionLevel::High) | |
->size(300) | |
->margin(10) | |
->roundBlockSizeMode(RoundBlockSizeMode::Margin) | |
->validateResult(false) | |
->build(); | |
return $result->getString(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Services; | |
use Endroid\QrCode\Bacon\MatrixFactory; | |
use Endroid\QrCode\ImageData\LogoImageData; | |
use Endroid\QrCode\Label\LabelInterface; | |
use Endroid\QrCode\Logo\LogoInterface; | |
use Endroid\QrCode\Matrix\MatrixInterface; | |
use Endroid\QrCode\QrCodeInterface; | |
use Endroid\QrCode\Writer\Result\ResultInterface; | |
use Endroid\QrCode\Writer\Result\SvgResult; | |
use Endroid\QrCode\Writer\WriterInterface; | |
// Fue necesario customizar esta clase completa para personalizar el QR | |
final class CustomSvgWriter implements WriterInterface | |
{ | |
public const DECIMAL_PRECISION = 2; | |
public const WRITER_OPTION_COMPACT = 'compact'; | |
public const WRITER_OPTION_BLOCK_ID = 'block_id'; | |
public const WRITER_OPTION_EXCLUDE_XML_DECLARATION = 'exclude_xml_declaration'; | |
public const WRITER_OPTION_EXCLUDE_SVG_WIDTH_AND_HEIGHT = 'exclude_svg_width_and_height'; | |
public const WRITER_OPTION_FORCE_XLINK_HREF = 'force_xlink_href'; | |
// Possible values: 'rect', 'dot' | |
public const WRITER_OPTION_DEFINITION_STYLE = 'style'; | |
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface | |
{ | |
if (!isset($options[self::WRITER_OPTION_COMPACT])) { | |
$options[self::WRITER_OPTION_COMPACT] = true; | |
} | |
if (!isset($options[self::WRITER_OPTION_BLOCK_ID])) { | |
$options[self::WRITER_OPTION_BLOCK_ID] = 'block'; | |
} | |
if (!isset($options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION])) { | |
$options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION] = false; | |
} | |
if (!isset($options[self::WRITER_OPTION_EXCLUDE_SVG_WIDTH_AND_HEIGHT])) { | |
$options[self::WRITER_OPTION_EXCLUDE_SVG_WIDTH_AND_HEIGHT] = false; | |
} | |
if (!isset($options[self::WRITER_OPTION_DEFINITION_STYLE])) { | |
$options[self::WRITER_OPTION_DEFINITION_STYLE] = 'rect'; | |
} | |
$matrixFactory = new MatrixFactory(); | |
$matrix = $matrixFactory->create($qrCode); | |
$xml = new \SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>'); | |
$xml->addAttribute('version', '1.1'); | |
if (!$options[self::WRITER_OPTION_EXCLUDE_SVG_WIDTH_AND_HEIGHT]) { | |
$xml->addAttribute('width', $matrix->getOuterSize() . 'px'); | |
$xml->addAttribute('height', $matrix->getOuterSize() . 'px'); | |
} | |
$xml->addAttribute('viewBox', '0 0 ' . $matrix->getOuterSize() . ' ' . $matrix->getOuterSize()); | |
$background = $xml->addChild('rect'); | |
$background->addAttribute('x', '0'); | |
$background->addAttribute('y', '0'); | |
$background->addAttribute('width', strval($matrix->getOuterSize())); | |
$background->addAttribute('height', strval($matrix->getOuterSize())); | |
$background->addAttribute('fill', '#' . sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()->getRed(), $qrCode->getBackgroundColor()->getGreen(), $qrCode->getBackgroundColor()->getBlue())); | |
$background->addAttribute('fill-opacity', strval($qrCode->getBackgroundColor()->getOpacity())); | |
if ($options[self::WRITER_OPTION_COMPACT]) { | |
$this->writePath($xml, $qrCode, $matrix); | |
} else { | |
$this->writeBlockDefinitions($xml, $qrCode, $matrix, $options); | |
} | |
$result = new SvgResult($matrix, $xml, boolval($options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION])); | |
if ($logo instanceof LogoInterface) { | |
$this->addLogo($logo, $result, $options); | |
} | |
return $result; | |
} | |
private function writePath(\SimpleXMLElement $xml, QrCodeInterface $qrCode, MatrixInterface $matrix): void | |
{ | |
$path = ''; | |
for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { | |
$left = $matrix->getMarginLeft(); | |
for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { | |
if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) { | |
// When we are at the first column or when the previous column was 0 set new left | |
if (0 === $columnIndex || 0 === $matrix->getBlockValue($rowIndex, $columnIndex - 1)) { | |
$left = $matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex; | |
} | |
// When we are at the | |
if ($columnIndex === $matrix->getBlockCount() - 1 || 0 === $matrix->getBlockValue($rowIndex, $columnIndex + 1)) { | |
$top = $matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex; | |
$bottom = $matrix->getMarginLeft() + $matrix->getBlockSize() * ($rowIndex + 1); | |
$right = $matrix->getMarginLeft() + $matrix->getBlockSize() * ($columnIndex + 1); | |
$path .= 'M' . $this->formatNumber($left) . ',' . $this->formatNumber($top); | |
$path .= 'L' . $this->formatNumber($right) . ',' . $this->formatNumber($top); | |
$path .= 'L' . $this->formatNumber($right) . ',' . $this->formatNumber($bottom); | |
$path .= 'L' . $this->formatNumber($left) . ',' . $this->formatNumber($bottom) . 'Z'; | |
} | |
} | |
} | |
} | |
$pathDefinition = $xml->addChild('path'); | |
$pathDefinition->addAttribute('fill', '#' . sprintf('%02x%02x%02x', $qrCode->getForegroundColor()->getRed(), $qrCode->getForegroundColor()->getGreen(), $qrCode->getForegroundColor()->getBlue())); | |
$pathDefinition->addAttribute('fill-opacity', strval($qrCode->getForegroundColor()->getOpacity())); | |
$pathDefinition->addAttribute('d', $path); | |
} | |
private function getBlockDefinition(\SimpleXMLElement $xml, MatrixInterface $matrix, array $options): ?\SimpleXMLElement | |
{ | |
if (strval($options[self::WRITER_OPTION_DEFINITION_STYLE]) === 'dot') { | |
$blockDefinition = $xml->defs->addChild('circle'); | |
$blockDefinition->addAttribute('id', strval($options[self::WRITER_OPTION_BLOCK_ID])); | |
$blockDefinition->addAttribute('cx', $this->formatNumber($matrix->getBlockSize() / 2)); | |
$blockDefinition->addAttribute('cy', $this->formatNumber($matrix->getBlockSize() / 2)); | |
$blockDefinition->addAttribute('r', $this->formatNumber($matrix->getBlockSize() / 2)); | |
return $blockDefinition; | |
} | |
$blockDefinition = $xml->defs->addChild('rect'); | |
$blockDefinition->addAttribute('id', strval($options[self::WRITER_OPTION_BLOCK_ID])); | |
$blockDefinition->addAttribute('width', $this->formatNumber($matrix->getBlockSize())); | |
$blockDefinition->addAttribute('height', $this->formatNumber($matrix->getBlockSize())); | |
return $blockDefinition; | |
} | |
/** @param array<string, mixed> $options */ | |
private function writeBlockDefinitions(\SimpleXMLElement $xml, QrCodeInterface $qrCode, MatrixInterface $matrix, array $options): void | |
{ | |
$xml->addChild('defs'); | |
$blockDefinition = $this->getBlockDefinition($xml, $matrix, $options); | |
$blockDefinition->addAttribute('fill', '#' . sprintf('%02x%02x%02x', $qrCode->getForegroundColor()->getRed(), $qrCode->getForegroundColor()->getGreen(), $qrCode->getForegroundColor()->getBlue())); | |
$blockDefinition->addAttribute('fill-opacity', strval($qrCode->getForegroundColor()->getOpacity())); | |
for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { | |
for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { | |
if ($this->isPatternsPositional($matrix, $rowIndex, $columnIndex)) { | |
continue; | |
} | |
if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) { | |
$block = $xml->addChild('use'); | |
$block->addAttribute('x', $this->formatNumber($matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex)); | |
$block->addAttribute('y', $this->formatNumber($matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex)); | |
$block->addAttribute('xlink:href', '#' . $options[self::WRITER_OPTION_BLOCK_ID], 'http://www.w3.org/1999/xlink'); | |
} | |
} | |
} | |
# Add the positional patterns | |
$this->addPositionalPatterns($xml, $matrix); | |
} | |
private function isPatternsPositional(MatrixInterface $matrix, $x, $y): bool | |
{ | |
$patternSize = 7; // Positional patterns are typically 7x7 blocks | |
// Top-left positional pattern | |
if ($x < $patternSize && $y < $patternSize) { | |
return true; | |
} | |
// Top-right positional pattern | |
if ($x >= $matrix->getBlockCount() - $patternSize && $y < $patternSize) { | |
return true; | |
} | |
// Bottom-left positional pattern | |
if ($x < $patternSize && $y >= $matrix->getBlockCount() - $patternSize) { | |
return true; | |
} | |
return false; | |
} | |
private function addPositionalPatterns(\SimpleXMLElement $xml, MatrixInterface $matrix) | |
{ | |
$dotDefinition = $xml->defs->addChild('circle'); | |
$dotDefinition->addAttribute('id', 'positional_patterns_dot'); | |
$dotDefinition->addAttribute('r', '10'); | |
$donutDefinition = $xml->defs->addChild('circle'); | |
$donutDefinition->addAttribute('id', 'positional_patterns_donut'); | |
$donutDefinition->addAttribute('r', '18'); | |
$donutDefinition->addAttribute('fill', 'none'); | |
$donutDefinition->addAttribute('stroke', '#5cbd41'); | |
$donutDefinition->addAttribute('stroke-width', $this->formatNumber(6)); | |
# Top-left positional pattern | |
$xLeft = $this->formatNumber($matrix->getMarginLeft() + $matrix->getBlockSize() * 3.5); | |
$yTop = $xLeft; | |
$this->addPositionalPattern($xml, $xLeft, $yTop); | |
# Top-right positional pattern | |
$xTop = $this->formatNumber($matrix->getMarginLeft() + $matrix->getBlockSize() * ($matrix->getBlockCount() - 3.5)); | |
$this->addPositionalPattern($xml, $xTop, $xLeft); | |
# Bottom-left positional pattern | |
$yBottom = $xTop; | |
$this->addPositionalPattern($xml, $xLeft, $yBottom); | |
} | |
private function addPositionalPattern(\SimpleXMLElement $xml, $x, $y) | |
{ | |
$block = $xml->addChild('use'); | |
$block->addAttribute('x', $x); | |
$block->addAttribute('y', $y); | |
$block->addAttribute('xlink:href', '#positional_patterns_dot', 'http://www.w3.org/1999/xlink'); | |
$block = $xml->addChild('use'); | |
$block->addAttribute('x', $x); | |
$block->addAttribute('y', $y); | |
$block->addAttribute('xlink:href', '#positional_patterns_donut', 'http://www.w3.org/1999/xlink'); | |
} | |
/** @param array<string, mixed> $options */ | |
private function addLogo(LogoInterface $logo, SvgResult $result, array $options): void | |
{ | |
$logoImageData = LogoImageData::createForLogo($logo); | |
if (!isset($options[self::WRITER_OPTION_FORCE_XLINK_HREF])) { | |
$options[self::WRITER_OPTION_FORCE_XLINK_HREF] = false; | |
} | |
$xml = $result->getXml(); | |
/** @var \SimpleXMLElement $xmlAttributes */ | |
$xmlAttributes = $xml->attributes(); | |
$x = intval($xmlAttributes->width) / 2 - $logoImageData->getWidth() / 2; | |
$y = intval($xmlAttributes->height) / 2 - $logoImageData->getHeight() / 2; | |
$imageDefinition = $xml->addChild('image'); | |
$imageDefinition->addAttribute('x', strval($x)); | |
$imageDefinition->addAttribute('y', strval($y)); | |
$imageDefinition->addAttribute('width', strval($logoImageData->getWidth())); | |
$imageDefinition->addAttribute('height', strval($logoImageData->getHeight())); | |
$imageDefinition->addAttribute('preserveAspectRatio', 'none'); | |
if ($options[self::WRITER_OPTION_FORCE_XLINK_HREF]) { | |
$imageDefinition->addAttribute('xlink:href', $logoImageData->createDataUri(), 'http://www.w3.org/1999/xlink'); | |
} else { | |
$imageDefinition->addAttribute('href', $logoImageData->createDataUri()); | |
} | |
} | |
private function formatNumber(float $number): string | |
{ | |
$string = number_format($number, self::DECIMAL_PRECISION, '.', ''); | |
$string = rtrim($string, '0'); | |
return rtrim($string, '.'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello after trying it out, I am unsure whether It is my usage that is wrong or it simply doesn't work.
Basically it does generate Dot-based SVG however they are not readable. I've noticed the 3 corners are dots with 3 green circles and I am unsure whether this is the reason for it not being readable, I also couldn't find an option to change that part, perhaps you can clarify it to me? :)