diff --git a/.editorconfig b/.editorconfig index 5fe17df..b7463f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root = true [*] charset = utf-8 end_of_line = lf -indent_style = tab +indent_style = space insert_final_newline = true trim_trailing_whitespace = true diff --git a/README.md b/README.md index cacedab..f1ba116 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,17 @@ Only PayBySquare document type is currently supported. ## Requirements -This library uses `xz` system executable (`/usr/bin/xz`) for lzma compression/decompression. -Any suggestions how to remove this dependency are welcome. - +This library uses `xz` system executable (`/usr/bin/xz`) for lzma compression/decompression. Path should be set in BySquare constructor. ## Instalation -`composer require peterbodnar.com/bsqr` +`composer require depesr/php-bsqr` ## Define a PayBySquare document ```php -use com\peterbodnar\bsqr; +use bsqr; $document = (new bsqr\model\Payment()) ->setDueDate("0000-00-00") // YYYY-MM-DD @@ -34,7 +32,7 @@ According to the specification, document can contain invoice ID, multiple paymen payments can contain multiple bank accounts, extensions, etc. ```php -use com\peterbodnar\bsqr; +use bsqr; $document = (new bsqr\model\Pay()) ->setInvoiceId("1234567890") @@ -59,7 +57,7 @@ $document = (new bsqr\model\Pay()) ## Render document to svg including BySqure logo and border ```php -use com\peterbodnar\bsqr; +use bsqr; $bysquare = new bsqr\BySquare(); @@ -70,7 +68,7 @@ $svg = (string) $bysquare->render($document); ## Get bsqr data only ```php -use com\peterbodnar\bsqr; +use bsqr; $bsqrCoder = new bsqr\utils\BsqrCoder(); @@ -82,7 +80,7 @@ Use any qr-code library to encode/render data to qr matrix/image. ## Parse bsqr data ```php -use com\peterbodnar\bsqr; +use bsqr; $bsqrCoder = new bsqr\utils\BsqrCoder(); @@ -92,6 +90,7 @@ $document = $bsqrCoder->parse($bsqrData); ## Links +- https://github.com/prog/php-bsqr - https://www.sbaonline.sk/projekt/projekty-z-oblasti-platobnych-sluzieb/ - https://bsqr.co/schema/ - http://www.bysquare.com/ diff --git a/composer.json b/composer.json index 9074f27..1633af0 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,13 @@ { - "name": "peterbodnar.com/bsqr", + "name": "depesr/php-bsqr", "description": "By Square document encoding, parsing and rendering utilities", "keywords": ["bysquare", "paybysquare"], "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], - "autoload": {"psr-4": {"com\\peterbodnar\\bsqr\\": "src/"}}, + "autoload": {"psr-4": {"bsqr\\": "src/"}}, "require": { - "php": "^5.4 || ^7.0", - "peterbodnar.com/base32": "^1.0", - "peterbodnar.com/cmd": "^1.0", - "peterbodnar.com/mx2svg": "^1.0", - "peterbodnar.com/qrcoder": "^1.0" + "php": ">=8.0", + "ext-imagick": "*", + "bacon/bacon-qr-code": "^v3" }, - "require-dev": { - "nette/tester": "^1.7" - } -} \ No newline at end of file + "version": "1.0" +} diff --git a/example/test.php b/example/test.php new file mode 100644 index 0000000..5772e93 --- /dev/null +++ b/example/test.php @@ -0,0 +1,24 @@ +setInvoiceId("1234567890") + ->addPayment( + (new Payment()) + ->setDueDate("2024-09-20") + ->setAmount(123.45, "EUR") + ->setSymbols("1234567890", null) + ->addBankAccount("SK3112000000198742637543", "XXXXXXXXXXX") + ->setNote("Add note") + ); +$lzmaPath = BySquare::LZMA_PATH_HOMEBREW; +$bysquare = new BySquare($lzmaPath); + +$svg = (string)$bysquare->render($document); + +echo $svg; diff --git a/src/Base32.php b/src/Base32.php new file mode 100644 index 0000000..5773b87 --- /dev/null +++ b/src/Base32.php @@ -0,0 +1,94 @@ +alphabet = $alphabet; + } + + + /** + * @param string $alphabet ~ Alphabet. + */ + public function setAlphabet($alphabet) { + $this->alphabet = $alphabet; + } + + + /** + * Encode data. + * + * @param string $data ~ Data to encode. + * @return string + */ + public function encode($data) { + $hexData = bin2hex($data); + $hexLen = strlen($hexData); + $binData = ""; + for ($i=0; $i<$hexLen; $i++) { + $binData .= str_pad(base_convert($hexData[$i], 16, 2), 4, "0", STR_PAD_LEFT); + } + $binLen = strlen($binData); + $rem = $binLen % 5; + if ($rem > 0) { + $pad = 5 - $rem; + $binData .= str_repeat("0", $pad); + $binLen += $pad; + } + $reslen = $binLen / 5; + $result = str_repeat("_", $reslen); + for ($i=0; $i<$reslen; $i += 1) { + $result[$i] = $this->alphabet[bindec(substr($binData, $i * 5, 5))]; + } + return $result; + } + + + /** + * @param string $data + * @return string + * @throws Base32Exception + */ + public function decode($data) { + $dataLen = strlen($data); + $binData = ""; + for ($i=0; $i<$dataLen; $i++) { + $ord = strpos($this->alphabet, $data[$i]); + if (FALSE === $ord) { + $charCode = "0x" . bin2hex($data[$i]); + throw new Base32Exception("Invalid input char ({$charCode}) at index {$i}"); + } + $binData .= str_pad(decbin($ord), 5, "0", STR_PAD_LEFT); + } + $binLen = strlen($binData); + $hexLen = floor($binLen / 8) * 2; + $hexData = str_repeat("_", $hexLen); + for ($i=0; $i<$hexLen; $i++) { + $hexData[$i] = base_convert(substr($binData, $i * 4, 4), 2, 16); + } + return hex2bin($hexData); + } + +} + + + +class Base32Exception extends \Exception { } diff --git a/src/BySquare.php b/src/BySquare.php index 3c0826a..c05351f 100644 --- a/src/BySquare.php +++ b/src/BySquare.php @@ -1,23 +1,22 @@ bsqrCoder = new BsqrCoder(); + public function __construct(string $lzmaPath = self::LZMA_PATH) { + $this->bsqrCoder = new BsqrCoder($lzmaPath); $this->qrCoder = new QrCoder(); $this->mx2svg = new MxToSvg(); $this->bsqrRenderer = new BsqrRenderer(); @@ -70,6 +69,7 @@ public function render(model\Document $document) { } catch (QrCoderException $ex) { throw new BySquareException("Error while encoding data to qr-code matrix: " . $ex->getMessage(), 0, $ex); } + $this->bsqrRenderer->setQrMatrixSize($qrMatrix->getRows(), $qrMatrix->getColumns()); $this->bsqrRenderer->setQrCodeSvg($qrSvg); $this->bsqrRenderer->setQuiteAreaRatio(4 / $qrMatrix->getRows()); if ($document instanceof model\Pay) { diff --git a/src/Command.php b/src/Command.php new file mode 100644 index 0000000..d3fe737 --- /dev/null +++ b/src/Command.php @@ -0,0 +1,92 @@ +command = $command; + } + + + /** + * Execute command. + * + * @param string[] $arguments ~ Command line arguments. + * @param string|null $inputData ~ Input data. + * @return CommandResult + * @throws CommandException + */ + public function execute(array $arguments = [], $inputData = null) { + $cmd = escapeshellcmd($this->command); + foreach ($arguments as $name => $arg) { + if (is_string($name)) { + $arg = $name . "=" . $arg; + } + $cmd .= " " . escapeshellarg($arg); + } + + $process = proc_open($cmd, [ + 0 => ["pipe", "r"], + 1 => ["pipe", "w"], + 2 => ["pipe", "w"], + ], $pipes); + if (!is_resource($process)) { + throw new CommandException("Can not open process \"" . $cmd . "\""); + } + + if (null !== $inputData) { + fwrite($pipes[0], $inputData); + } + fclose($pipes[0]); + $stdOut = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $stdErr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + $exitCode = proc_close($process); + + $result = new CommandResult(); + $result->stdOut = $stdOut; + $result->stdErr = $stdErr; + $result->exitCode = $exitCode; + return $result; + } + +} + + + +/** + * Command Execution Result + */ +class CommandResult { + + + /** @var string */ + public $stdOut; + /** @var string */ + public $stdErr; + /** @var int */ + public $exitCode; + +} + + + +/** + * Command Exception + */ +class CommandException extends \Exception { } diff --git a/src/Exception.php b/src/Exception.php index 15c800e..095798f 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -1,6 +1,6 @@ amount = $amount; if (NULL !== $currencyCode) { $this->setCurrencyCode($currencyCode); @@ -68,9 +70,11 @@ public function setAmount($amount, $currencyCode = NULL) { * Payment currency code, 3 letter ISO4217 code. * * @param string $currencyCode + * * @return static */ - public function setCurrencyCode($currencyCode) { + public function setCurrencyCode(string $currencyCode): Payment + { $this->currencyCode = $currencyCode; return $this; } @@ -80,9 +84,11 @@ public function setCurrencyCode($currencyCode) { * Set payment due date. Used also as first payment date for standing order. * * @param string|null $dueDate + * * @return static */ - public function setDueDate($dueDate) { + public function setDueDate(?string $dueDate): Payment + { $this->dueDate = $dueDate; return $this; } @@ -92,9 +98,11 @@ public function setDueDate($dueDate) { * Set variable symbol. * * @param string|null $variableSymbol + * * @return static */ - public function setVariableSymbol($variableSymbol) { + public function setVariableSymbol(?string $variableSymbol): Payment + { $this->variableSymbol = $variableSymbol; return $this; } @@ -104,9 +112,11 @@ public function setVariableSymbol($variableSymbol) { * Set constant symbol. * * @param string|null $constantSymbol + * * @return static */ - public function setConstantSymbol($constantSymbol) { + public function setConstantSymbol(?string $constantSymbol): Payment + { $this->constantSymbol = $constantSymbol; return $this; } @@ -116,9 +126,11 @@ public function setConstantSymbol($constantSymbol) { * Set specific symbol. * * @param string|null $specificSymbol + * * @return static */ - public function setSpecificSymbol($specificSymbol) { + public function setSpecificSymbol(?string $specificSymbol): Payment + { $this->specificSymbol = $specificSymbol; return $this; } @@ -130,9 +142,11 @@ public function setSpecificSymbol($specificSymbol) { * @param string|null $variableSymbol * @param string|null $constantSymbol * @param string|null $specificSymbol + * * @return static */ - public function setSymbols($variableSymbol, $constantSymbol, $specificSymbol = NULL) { + public function setSymbols(?string $variableSymbol = null, string $constantSymbol = null, string $specificSymbol = null): Payment + { $this->variableSymbol = $variableSymbol; $this->constantSymbol = $constantSymbol; if (func_num_args() > 2) { diff --git a/src/model/StandingOrderExt.php b/src/model/StandingOrderExt.php index 00c7255..1e40e37 100644 --- a/src/model/StandingOrderExt.php +++ b/src/model/StandingOrderExt.php @@ -1,6 +1,6 @@ '#000']) { + $this->defaultPaths = $defaultPaths; + } + + + /** + * Render matrix to svg. + * + * @param IMatrix $matrix ~ Matrix. + * @param string[] $paths ~ Path colors. + * @return Svg + */ + public function render(IMatrix $matrix, $paths = NULL) { + if (NULL === $paths) { + $paths = $this->defaultPaths; + } + return Utils::renderSvg($matrix, $paths); + } + +} diff --git a/src/mx2svg/Utils.php b/src/mx2svg/Utils.php new file mode 100644 index 0000000..fb0372f --- /dev/null +++ b/src/mx2svg/Utils.php @@ -0,0 +1,242 @@ +getColumns(); + $h = $matrix->getRows(); + return + (($x > 0) && ($y > 0) && ($val === $matrix->getValue($y - 1, $x - 1)) ? static::BLOCK_TL : 0) + | (($x < $w) && ($y > 0) && ($val === $matrix->getValue($y - 1, $x)) ? static::BLOCK_TR : 0) + | (($x > 0) && ($y < $h) && ($val === $matrix->getValue($y, $x - 1)) ? static::BLOCK_BL : 0) + | (($x < $w) && ($y < $h) && ($val === $matrix->getValue($y, $x)) ? static::BLOCK_BR : 0); + } + + + /** + * Return new direction by current direction and blocks around current position. + * + * @internal + * + * @param int $dir ~ Current direction. + * @param int $blocks ~ Blocks around current position. + * @return int + */ + static protected function getDir($dir, $blocks) { + $turns = [static::BLOCK_TL, static::BLOCK_TR, static::BLOCK_BR, static::BLOCK_BL]; + if (!in_array($blocks, $turns, true) && !in_array(($blocks = static::BLOCK_ALL & ~$blocks), $turns)) { + return $dir; + } + $dirVert = !($dir & static::DIR_VERT); + $dirBack = $blocks & ($dirVert ? static::BLOCK_T : static::BLOCK_L); + return ($dirVert ? static::DIR_VERT : 0) | ($dirBack ? static::DIR_BACK : 0); + } + + + /** + * Render one segment of matrix starting from specified position. + * + * @internal + * + * @param IMatrix $matrix ~ Matrix. + * @param mixed $val ~ Value. + * @param int $x ~ Starting position + * @param int $y ~ Start + * @param SplFixedArray $visited ~ Array to save visited positions. + * @return string + */ + static public function renderSegment(IMatrix $matrix, $val, $x, $y, SplFixedArray $visited) { + $result = ""; + + $w = $matrix->getColumns(); + $dir = static::DIR_R; + $startX = $x0 = $x; + $startY = $y0 = $y; + + for (;;) { + $step = ($dir & static::DIR_BACK) ? -1 : 1; + if ($dir & static::DIR_VERT) { + $y += $step; + } else { + $x += $step; + } + if ($x === $startX && $y === $startY) { + return $result . "z"; + } + $visited[$y * ($w + 1) + $x] = true; + $b = static::getBlocks($matrix, $val, $x, $y); + $newDir = static::getDir($dir, $b); + if ($newDir !== $dir) { + $result .= static::lineTo($x, $y, $x0, $y0); + $x0 = $x; + $y0 = $y; + $dir = $newDir; + } + } + } + + + /** + * Render svg path directives for specifed value in matrix. + * + * @internal + * + * @param IMatrix $matrix + * @param mixed $val + * @return string + */ + static public function renderPath(IMatrix $matrix, $val) { + $result = ""; + $curX = NULL; + $curY = NULL; + $w = $matrix->getColumns(); + $h = $matrix->getRows(); + $visited = new SplFixedArray(($w + 1) * ($h + 1)); + + for ($y = 0; $y < $h; $y++) { + for ($x = 0; $x < $w; $x++) { + if (isset($visited[$y * ($w + 1) + $x])) { + continue; + } + $b = static::getBlocks($matrix, $val, $x, $y); + if ((static::BLOCK_BR === $b) || ((static::BLOCK_ALL & ~static::BLOCK_BR) === $b)) { + $result .= static::moveTo($x, $y, $curX, $curY); + $result .= static::renderSegment($matrix, $val, $x, $y, $visited); + $curX = $x; + $curY = $y; + } + } + } + + return $result; + } + + + /** + * Render path directives for specifed values in matrix. + * + * @internal + * + * @param IMatrix $matrix ~ Matrix. + * @param string[] $paths ~ Path colors. + * @return string + */ + public static function renderSvg(IMatrix $matrix, array $paths) { + $w = $matrix->getColumns(); + $h = $matrix->getRows(); + + $args = ["viewBox" => "0 0 {$w} {$h}"]; + $result = ""; + foreach ($paths as $pathValue => $pathColor) { + $pathDirectives = static::renderPath($matrix, $pathValue); + $result .= ""; + } + return new Svg($result, $args); + } + +} diff --git a/src/qrcoder/Exception.php b/src/qrcoder/Exception.php new file mode 100644 index 0000000..40b2c8c --- /dev/null +++ b/src/qrcoder/Exception.php @@ -0,0 +1,10 @@ +byteMatrix = $byteMatrix; + } + + + /** + * @inheritdoc + * @return int + */ + public function getRows() { + return $this->byteMatrix->getHeight(); + } + + + /** + * @inheritdoc + * @return int + */ + public function getColumns() { + return $this->byteMatrix->getWidth(); + } + + + /** + * @inheritdoc + * @return mixed + */ + public function getValue($rowIndex, $columnIndex) { + return $this->byteMatrix->get($columnIndex, $rowIndex); + } + +} diff --git a/src/qrcoder/QrCoder.php b/src/qrcoder/QrCoder.php new file mode 100644 index 0000000..1529c20 --- /dev/null +++ b/src/qrcoder/QrCoder.php @@ -0,0 +1,75 @@ +EC_LEVEL_L = BaconQrECLevel::L(); + $this->EC_LEVEL_M = BaconQrECLevel::M(); + $this->EC_LEVEL_Q = BaconQrECLevel::Q(); + $this->EC_LEVEL_H = BaconQrECLevel::H(); + + if (is_null($defaultEcLevel)) { + $defaultEcLevel = $this->EC_LEVEL_L; + } + $this->defaultEcLevel = $defaultEcLevel; + } + + + /** + * Encode data to qr-code matrix. + * + * @param string $data ~ Data to encode. + * @param int|null $ecLevel ~ Error correction level. + * @return IMatrix + * @throws QrCoderException + */ + public function encode($data, $ecLevel = NULL) { + if (is_null($ecLevel)) { + $ecLevel = $this->defaultEcLevel; + } + try { +// $qrCode = BaconQrEncoder::encode($data, BaconQrECLevel::forBits($ecLevel)); + $qrCode = BaconQrEncoder::encode($data, $ecLevel); + return new MatrixAdapter($qrCode->getMatrix()); + } catch (\Exception $ex) { + throw new QrCoderException("Error encoding data: " . $ex->getMessage(), 0, $ex); + } + } + +} + + + +/** + * Class QrEncoderException + */ +class QrCoderException extends Exception { } diff --git a/src/svg/Svg.php b/src/svg/Svg.php new file mode 100644 index 0000000..e997cf9 --- /dev/null +++ b/src/svg/Svg.php @@ -0,0 +1,86 @@ + and tags.*/ + protected $content; + /** @var string[] ~ Attributes of svg root element */ + protected $attributes; + + + /** + * @param string $content ~ Content between and tags. + * @param string[] $attributes ~ Attributes of svg root element. + */ + public function __construct($content, $attributes = []) { + $this->content = $content; + $this->attributes = $attributes; + } + + + /** + * Return an instance with current and specified attributes merged. + * + * @param string[] $attributes + * @return Svg + */ + public function withAttrs(array $attributes) { + return new Svg($this->content, array_merge($this->attributes, $attributes)); + } + + + /** + * Return an instance with specified size. + * + * @param string|int|float $width + * @param string|int|float $height + * @return Svg + */ + public function withSize($width, $height) { + return $this->withAttrs(["width" => $width, "height" => $height]); + } + + + /** @return string */ + public function __toString() { + $result = "attributes as $arg => $value) { + if (NULL !== $value) { + $result .= " " . $arg . "=\"" . htmlspecialchars($value, ENT_QUOTES) . "\""; + } + } + $result .= ">" . $this->content . ""; + return $result; + } + + + /** + * + * + * @param int $width ~ Width. + * @param int $height ~ Height. + * + * @return Imagick + * @throws \ImagickException + */ + public function toImagick($width, $height): Imagick + { + $svgData = (string) $this->withSize($width, $height); + + $img = new Imagick(); + $img->setBackgroundColor(new ImagickPixel("transparent")); + $img->readImageBlob($svgData); + return $img; + } + +} diff --git a/src/utils/BsqrCoder.php b/src/utils/BsqrCoder.php index e777811..ca7a8ad 100644 --- a/src/utils/BsqrCoder.php +++ b/src/utils/BsqrCoder.php @@ -1,11 +1,11 @@ base32 = new Base32("0123456789ABCDEFGHIJKLMNOPQRSTUV"); $this->cldEncoder = new ClientDataEncoder(); $this->cldParser = new ClientDataParser(); - $this->lzma = new Lzma(); + $this->lzma = new Lzma($lzmaPath); } diff --git a/src/utils/BsqrRenderer.php b/src/utils/BsqrRenderer.php index c92de6c..9b4acf2 100644 --- a/src/utils/BsqrRenderer.php +++ b/src/utils/BsqrRenderer.php @@ -1,10 +1,9 @@ ]*?>(.*)$~", "\${1}", $svgString); + $res = str_replace(["{primary}", "{secondary}"], [$this->colorPrimary, $this->colorSecondary], $res); + return $res; + } + /** * Set QR code svg image. @@ -70,6 +80,12 @@ public function setQrCodeSvg(Svg $qrCodeSvg, $qaRatio = null) { } } + public function setQrMatrixSize(int $rows, int $columns): void + { + $this->qrMatrixRows = $rows; + $this->qrMatrixColumns = $columns; + } + /** * Set ratio of quite area size to qrcode square size. @@ -140,14 +156,15 @@ public function setColors($primaryColor, $secondaryColor) { * @param Svg $svg - Image to render. * @return string */ - protected function renderQrCode(array $pos, $size, $rotate, Svg $svg) { + protected function renderQrCode(array $pos, $size, $rotate, Svg $svg, $scaleFactorY, $scaleFactorX) { $transform = "translate(" . ($pos[0]) . "," . ($pos[1]) . ")"; if (0 !== $rotate) { $transform .= " rotate(" . $rotate . "," . ($size / 2) . "," . ($size / 2) . ")"; } + $transform .= " scale($scaleFactorX $scaleFactorY)"; return "" . - ((string) $svg->withSize($size, $size)) . + $this->includeSvgElement((string) $svg->withSize($size, $size)) . ""; } @@ -296,7 +313,7 @@ public function render() { $qOffset = $baseSize * $this->qaRatio; $qrSize = $baseSize - 2 * $qOffset; $qrPos = [$basePos[0] + $qOffset, $basePos[1] + $qOffset]; - $content .= $this->renderQrCode($qrPos, $qrSize, $qrRotate, $this->qrSvg); + $content .= $this->renderQrCode($qrPos, $qrSize, $qrRotate, $this->qrSvg, $qrSize/$this->qrMatrixRows, $qrSize/$this->qrMatrixColumns); } if ($this->showBorder) { $content .= $this->renderBorder($basePos, $baseSize, $bw, $noLogo); diff --git a/src/utils/ClientDataEncoder.php b/src/utils/ClientDataEncoder.php index e149bae..b4ae39a 100644 --- a/src/utils/ClientDataEncoder.php +++ b/src/utils/ClientDataEncoder.php @@ -1,8 +1,8 @@ separator, " ", $value); + protected function encodeValue(string $value = null) { + if (!is_null($value)) { + return str_replace($this->separator, " ", $value); + } + return null; } diff --git a/src/utils/ClientDataParser.php b/src/utils/ClientDataParser.php index 8c61c2d..1297cc7 100644 --- a/src/utils/ClientDataParser.php +++ b/src/utils/ClientDataParser.php @@ -1,9 +1,9 @@