diff --git a/examples/QRSvgWithLogo.php b/examples/QRSvgWithLogo.php index fcfa8580c..a6e366d0c 100644 --- a/examples/QRSvgWithLogo.php +++ b/examples/QRSvgWithLogo.php @@ -10,44 +10,26 @@ namespace chillerlan\QRCodeExamples; -use chillerlan\QRCode\Output\QRMarkup; -use function file_get_contents, sprintf; +use chillerlan\QRCode\Output\QRMarkupSVG; +use function ceil, file_get_contents, sprintf; /** * Create SVG QR Codes with embedded logos (that are also SVG) */ -class QRSvgWithLogo extends QRMarkup{ +class QRSvgWithLogo extends QRMarkupSVG{ /** - * SVG output - * - * @see https://github.com/codemasher/php-qrcode/pull/5 - * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg - * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/ + * @inheritDoc */ - protected function svg(bool $saveToFile):string{ + protected function paths():string{ $size = (int)ceil($this->moduleCount * $this->options->svgLogoScale); // we're calling QRMatrix::setLogoSpace() manually, so QROptions::$addLogoSpace has no effect here $this->matrix->setLogoSpace($size, $size); - $svg = $this->svgHeader(); - - if(!empty($this->options->svgDefs)){ - $svg .= sprintf('%1$s%2$s%2$s', $this->options->svgDefs, $this->options->eol); - } - - $svg .= $this->svgPaths(); + $svg = parent::paths(); $svg .= $this->getLogo(); - // close svg - $svg .= sprintf('%1$s%1$s', $this->options->eol); - - // transform to data URI only when not saving to file - if(!$saveToFile && $this->options->imageBase64){ - $svg = $this->base64encode($svg, 'image/svg+xml'); - } - return $svg; } diff --git a/src/Output/QRMarkup.php b/src/Output/QRMarkup.php index 027281617..062423510 100644 --- a/src/Output/QRMarkup.php +++ b/src/Output/QRMarkup.php @@ -10,15 +10,12 @@ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\Data\QRMatrix; -use chillerlan\QRCode\QRCode; - -use function implode, is_string, sprintf, strip_tags, trim; +use function is_string, strip_tags, trim; /** - * Converts the matrix into markup types: HTML, SVG, ... + * Abstract for markup types: HTML, SVG, ... XML anyone? */ -class QRMarkup extends QROutputAbstract{ +abstract class QRMarkup extends QROutputAbstract{ /** * @inheritDoc @@ -44,18 +41,11 @@ class QRMarkup extends QROutputAbstract{ /** * @inheritDoc */ - public function dump(string $file = null){ + public function dump(string $file = null):string{ $file ??= $this->options->cachefile; $saveToFile = $file !== null; - switch($this->options->outputType){ - case QRCode::OUTPUT_MARKUP_HTML: - $data = $this->html($saveToFile); - break; - case QRCode::OUTPUT_MARKUP_SVG: - default: - $data = $this->svg($saveToFile); - } + $data = $this->createMarkup($saveToFile); if($saveToFile){ $this->saveToFile($data, $file); @@ -65,143 +55,7 @@ class QRMarkup extends QROutputAbstract{ } /** - * HTML output - */ - protected function html(bool $saveToFile):string{ - - $html = empty($this->options->cssClass) - ? '
' - : sprintf('
', $this->options->cssClass); - - $html .= $this->options->eol; - - foreach($this->matrix->matrix() as $row){ - $html .= '
'; - - foreach($row as $M_TYPE){ - $html .= sprintf('', $this->moduleValues[$M_TYPE]); - } - - $html .= '
'.$this->options->eol; - } - - $html .= '
'.$this->options->eol; - - if($saveToFile){ - return sprintf( - 'QR Code%s', - $this->options->eol.$html - ); - } - - return $html; - } - - /** - * SVG output * - * @see https://github.com/codemasher/php-qrcode/pull/5 - * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg - * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/ */ - protected function svg(bool $saveToFile):string{ - $svg = $this->svgHeader(); - - if(!empty($this->options->svgDefs)){ - $svg .= sprintf('%1$s%2$s%2$s', $this->options->svgDefs, $this->options->eol); - } - - $svg .= $this->svgPaths(); - - // close svg - $svg .= sprintf('%1$s%1$s', $this->options->eol); - - // transform to data URI only when not saving to file - if(!$saveToFile && $this->options->imageBase64){ - $svg = $this->base64encode($svg, 'image/svg+xml'); - } - - return $svg; - } - - /** - * returns the header with the given options parsed - */ - protected function svgHeader():string{ - $width = $this->options->svgWidth !== null ? sprintf(' width="%s"', $this->options->svgWidth) : ''; - $height = $this->options->svgHeight !== null ? sprintf(' height="%s"', $this->options->svgHeight) : ''; - - /** @noinspection HtmlUnknownAttribute */ - return sprintf( - '%6$s'. - '%6$s', - $this->options->cssClass, - $this->options->svgViewBoxSize ?? $this->moduleCount, - $this->options->svgPreserveAspectRatio, - $width, - $height, - $this->options->eol - ); - } - - /** - * returns one or more SVG elements - * - * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path - */ - protected function svgPaths():string{ - $paths = $this->collectModules(fn(int $x, int $y):string => $this->svgModule($x, $y)); - $svg = []; - - // create the path elements - foreach($paths as $M_TYPE => $path){ - $path = trim(implode(' ', $path)); - - if(empty($path)){ - continue; - } - - $cssClass = implode(' ', [ - 'qr-'.$M_TYPE, - ($M_TYPE & QRMatrix::IS_DARK) === QRMatrix::IS_DARK ? 'dark' : 'light', - $this->options->cssClass, - ]); - - $format = empty($this->moduleValues[$M_TYPE]) - ? '' - : ''; - - $svg[] = sprintf($format, $cssClass, $path, $this->moduleValues[$M_TYPE], $this->options->svgOpacity); - } - - return implode($this->options->eol, $svg); - } - - /** - * returns a path segment for a single module - * - * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d - */ - protected function svgModule(int $x, int $y):string{ - - if($this->options->imageTransparent && !$this->matrix->check($x, $y)){ - return ''; - } - - if($this->options->drawCircularModules && $this->matrix->checkTypeNotIn($x, $y, $this->options->keepAsSquare)){ - $r = $this->options->circleRadius; - - return sprintf( - 'M%1$s %2$s a%3$s %3$s 0 1 0 %4$s 0 a%3$s %3$s 0 1 0 -%4$s 0Z', - ($x + 0.5 - $r), - ($y + 0.5), - $r, - ($r * 2) - ); - - } - - return sprintf('M%1$s %2$s h%3$s v1 h-%4$sZ', $x, $y, 1, 1); - } - + abstract protected function createMarkup(bool $saveToFile):string; } diff --git a/src/Output/QRMarkupHTML.php b/src/Output/QRMarkupHTML.php new file mode 100644 index 000000000..2d978c81a --- /dev/null +++ b/src/Output/QRMarkupHTML.php @@ -0,0 +1,53 @@ + + * @copyright 2022 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use function sprintf; + +/** + * HTML output + */ +class QRMarkupHTML extends QRMarkup{ + + /** + * @inheritDoc + */ + protected function createMarkup(bool $saveToFile):string{ + $html = empty($this->options->cssClass) + ? '
' + : sprintf('
', $this->options->cssClass); + + $html .= $this->options->eol; + + foreach($this->matrix->matrix() as $row){ + $html .= '
'; + + foreach($row as $M_TYPE){ + $html .= sprintf('', $this->moduleValues[$M_TYPE]); + } + + $html .= '
'.$this->options->eol; + } + + $html .= '
'.$this->options->eol; + + // wrap the snippet into a body when saving to file + if($saveToFile){ + $html = sprintf( + 'QR Code%s', + $this->options->eol.$html + ); + } + + return $html; + } + +} diff --git a/src/Output/QRMarkupSVG.php b/src/Output/QRMarkupSVG.php new file mode 100644 index 000000000..cc7679799 --- /dev/null +++ b/src/Output/QRMarkupSVG.php @@ -0,0 +1,137 @@ + + * @copyright 2022 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use chillerlan\QRCode\Data\QRMatrix; +use function array_chunk, implode, sprintf; + +/** + * SVG output + * + * @see https://github.com/codemasher/php-qrcode/pull/5 + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg + * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/ + * @see http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html + */ +class QRMarkupSVG extends QRMarkup{ + + /** + * @inheritDoc + */ + protected function createMarkup(bool $saveToFile):string{ + $svg = $this->header(); + + if(!empty($this->options->svgDefs)){ + $svg .= sprintf('%1$s%2$s%2$s', $this->options->svgDefs, $this->options->eol); + } + + $svg .= $this->paths(); + + // close svg + $svg .= sprintf('%1$s%1$s', $this->options->eol); + + // transform to data URI only when not saving to file + if(!$saveToFile && $this->options->imageBase64){ + $svg = $this->base64encode($svg, 'image/svg+xml'); + } + + return $svg; + } + + /** + * returns the header with the given options parsed + */ + protected function header():string{ + $width = $this->options->svgWidth !== null ? sprintf(' width="%s"', $this->options->svgWidth) : ''; + $height = $this->options->svgHeight !== null ? sprintf(' height="%s"', $this->options->svgHeight) : ''; + + /** @noinspection HtmlUnknownAttribute */ + return sprintf( + '%6$s'. + '%6$s', + $this->options->cssClass, + $this->options->svgViewBoxSize ?? $this->moduleCount, + $this->options->svgPreserveAspectRatio, + $width, + $height, + $this->options->eol + ); + } + + /** + * returns one or more SVG elements + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path + */ + protected function paths():string{ + $paths = $this->collectModules(fn(int $x, int $y):string => $this->module($x, $y)); + $svg = []; + + // create the path elements + foreach($paths as $M_TYPE => $modules){ + // limit the total line length + $chunks = array_chunk($modules, 100); + $chonks = []; + + foreach($chunks as $chunk){ + $chonks[] = implode(' ', $chunk); + } + + $path = implode($this->options->eol, $chonks); + + if(empty($path)){ + continue; + } + + $cssClass = implode(' ', [ + 'qr-'.$M_TYPE, + ($M_TYPE & QRMatrix::IS_DARK) === QRMatrix::IS_DARK ? 'dark' : 'light', + $this->options->cssClass, + ]); + + $format = empty($this->moduleValues[$M_TYPE]) + ? '' + : ''; + + $svg[] = sprintf($format, $cssClass, $path, $this->moduleValues[$M_TYPE], $this->options->svgOpacity); + } + + return implode($this->options->eol, $svg); + } + + /** + * returns a path segment for a single module + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + */ + protected function module(int $x, int $y):string{ + + if($this->options->imageTransparent && !$this->matrix->check($x, $y)){ + return ''; + } + + if($this->options->drawCircularModules && $this->matrix->checkTypeNotIn($x, $y, $this->options->keepAsSquare)){ + $r = $this->options->circleRadius; + + return sprintf( + 'M%1$s %2$s a%3$s %3$s 0 1 0 %4$s 0 a%3$s %3$s 0 1 0 -%4$s 0Z', + ($x + 0.5 - $r), + ($y + 0.5), + $r, + ($r * 2) + ); + + } + + return sprintf('M%1$s %2$s h1 v1 h-1Z', $x, $y); + } + +} diff --git a/src/QRCode.php b/src/QRCode.php index dd3091617..5ac86eaeb 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -13,7 +13,9 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Common\{EccLevel, ECICharset, MaskPattern, Mode}; use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRCodeDataException, QRData, QRDataModeInterface, QRMatrix}; use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, LuminanceSourceInterface}; -use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRGdImage, QRImagick, QRMarkup, QREps, QROutputInterface, QRString}; +use chillerlan\QRCode\Output\{ + QRCodeOutputException, QRFpdf, QRGdImage, QRImagick, QRMarkupHTML, QRMarkupSVG, QREps, QROutputInterface, QRString +}; use chillerlan\Settings\SettingsContainerInterface; use function class_exists, class_implements, in_array, mb_convert_encoding, mb_detect_encoding; @@ -90,8 +92,8 @@ class QRCode{ * @var string[] */ public const OUTPUT_MODES = [ - self::OUTPUT_MARKUP_SVG => QRMarkup::class, - self::OUTPUT_MARKUP_HTML => QRMarkup::class, + self::OUTPUT_MARKUP_SVG => QRMarkupSVG::class, + self::OUTPUT_MARKUP_HTML => QRMarkupHTML::class, self::OUTPUT_IMAGE_PNG => QRGdImage::class, self::OUTPUT_IMAGE_GIF => QRGdImage::class, self::OUTPUT_IMAGE_JPG => QRGdImage::class, diff --git a/tests/Output/QRMarkupHTMLTest.php b/tests/Output/QRMarkupHTMLTest.php index 497620fed..cd390c614 100644 --- a/tests/Output/QRMarkupHTMLTest.php +++ b/tests/Output/QRMarkupHTMLTest.php @@ -10,6 +10,7 @@ namespace chillerlan\QRCodeTest\Output; +use chillerlan\QRCode\Output\QRMarkupHTML; use chillerlan\QRCode\QRCode; /** @@ -17,6 +18,7 @@ use chillerlan\QRCode\QRCode; */ final class QRMarkupHTMLTest extends QRMarkupTestAbstract{ + protected string $FQN = QRMarkupHTML::class; protected string $type = QRCode::OUTPUT_MARKUP_HTML; } diff --git a/tests/Output/QRMarkupSVGTest.php b/tests/Output/QRMarkupSVGTest.php index f55b141d8..435cc9b00 100644 --- a/tests/Output/QRMarkupSVGTest.php +++ b/tests/Output/QRMarkupSVGTest.php @@ -10,6 +10,7 @@ namespace chillerlan\QRCodeTest\Output; +use chillerlan\QRCode\Output\QRMarkupSVG; use chillerlan\QRCode\QRCode; /** @@ -17,6 +18,7 @@ use chillerlan\QRCode\QRCode; */ final class QRMarkupSVGTest extends QRMarkupTestAbstract{ + protected string $FQN = QRMarkupSVG::class; protected string $type = QRCode::OUTPUT_MARKUP_SVG; } diff --git a/tests/Output/QRMarkupTestAbstract.php b/tests/Output/QRMarkupTestAbstract.php index 329f2e219..a4b05890b 100644 --- a/tests/Output/QRMarkupTestAbstract.php +++ b/tests/Output/QRMarkupTestAbstract.php @@ -11,15 +11,12 @@ namespace chillerlan\QRCodeTest\Output; use chillerlan\QRCode\Data\QRMatrix; -use chillerlan\QRCode\Output\QRMarkup; /** * Tests the QRMarkup output module */ abstract class QRMarkupTestAbstract extends QROutputTestAbstract{ - protected string $FQN = QRMarkup::class; - /** * @inheritDoc */