| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- <?php
- /**
- * round quiet zone example
- *
- * @see https://github.com/chillerlan/php-qrcode/discussions/137
- *
- * @created 09.07.2022
- * @author smiley <smiley@chillerlan.net>
- * @copyright 2022 smiley
- * @license MIT
- *
- * @noinspection PhpIllegalPsrClassPathInspection
- */
- declare(strict_types=1);
- use chillerlan\QRCode\{QRCode, QRCodeException, QROptions};
- use chillerlan\QRCode\Common\EccLevel;
- use chillerlan\QRCode\Data\QRMatrix;
- use chillerlan\QRCode\Output\QRMarkupSVG;
- require_once __DIR__.'/../vendor/autoload.php';
- /*
- * Class definition
- */
- /**
- * Create SVG QR Codes with embedded logos (that are also SVG),
- * randomly colored dots and a round quiet zone with added circle
- */
- class RoundQuietzoneSVGoutput extends QRMarkupSVG{
- protected float $center;
- protected function createMarkup(bool $saveToFile):string{
- // some Pythagorean magick
- $diameter = sqrt(2 * pow(($this->moduleCount + $this->options->additionalModules), 2));
- // calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it
- $quietzoneSize = ((int)ceil(($diameter - $this->moduleCount) / 2) + 1);
- // add the quiet zone to fill the circle
- $this->matrix->setQuietZone($quietzoneSize);
- // update the matrix dimensions to avoid errors in subsequent calculations
- // the moduleCount is now QR Code matrix + 2x quiet zone
- $this->setMatrixDimensions();
- $this->center = ($this->moduleCount / 2);
- // calculate the logo space
- $logoSpaceSize = (int)(ceil($this->moduleCount * $this->options->svgLogoScale) + 1);
- // we're calling QRMatrix::setLogoSpace() manually, so QROptions::$addLogoSpace has no effect here
- $this->matrix->setLogoSpace($logoSpaceSize);
- // color the quiet zone
- $this->colorQuietzone($quietzoneSize, ($diameter / 2));
- // start SVG output
- $svg = $this->header();
- if(!empty($this->options->svgDefs)){
- $svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
- }
- $svg .= $this->paths();
- $svg .= $this->getLogo();
- $svg .= $this->addCircle($diameter / 2);
- // close svg
- $svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
- return $svg;
- }
- protected function path(string $path, int $M_TYPE):string{
- // omit the "fill" and "opacity" attributes on the path element
- return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
- }
- /**
- * Sets random modules of the quiet zone to dark
- */
- protected function colorQuietzone(int $quietzoneSize, float $radius):void{
- $l1 = ($quietzoneSize - 1);
- $l2 = ($this->moduleCount - $quietzoneSize);
- // substract 1/2 stroke width and module radius from the circle radius to not cut off modules
- $r = ($radius - $this->options->circleRadius * 2);
- for($y = 0; $y < $this->moduleCount; $y++){
- for($x = 0; $x < $this->moduleCount; $x++){
- // skip anything that's not quiet zone
- if(!$this->matrix->checkType($x, $y, QRMatrix::M_QUIETZONE)){
- continue;
- }
- // leave one row of quiet zone around the matrix
- if(
- ($x === $l1 && $y >= $l1 && $y <= $l2)
- || ($x === $l2 && $y >= $l1 && $y <= $l2)
- || ($y === $l1 && $x >= $l1 && $x <= $l2)
- || ($y === $l2 && $x >= $l1 && $x <= $l2)
- ){
- continue;
- }
- // we need to add 0.5 units to the check values since we're calculating the element centers
- // ($x/$y is the element's assumed top left corner)
- if($this->checkIfInsideCircle(($x + 0.5), ($y + 0.5), $this->center, $this->center, $r)){
- $this->matrix->set($x, $y, (bool)rand(0, 1), QRMatrix::M_QUIETZONE);
- }
- }
- }
- }
- /**
- * @see https://stackoverflow.com/a/7227057
- */
- protected function checkIfInsideCircle(float $x, float $y, float $centerX, float $centerY, float $radius):bool{
- $dx = abs($x - $centerX);
- $dy = abs($y - $centerY);
- if(($dx + $dy) <= $radius){
- return true;
- }
- if($dx > $radius || $dy > $radius){
- return false;
- }
- return (pow($dx, 2) + pow($dy, 2)) <= pow($radius, 2);
- }
- /**
- * add a solid circle around the matrix
- */
- protected function addCircle(float $radius):string{
- return sprintf(
- '%4$s<circle id="circle" cx="%1$s" cy="%1$s" r="%2$s" stroke-width="%3$s"/>',
- $this->center,
- round($radius, 5),
- ($this->options->circleRadius * 2),
- $this->options->eol,
- );
- }
- /**
- * returns a <g> element that contains the SVG logo and positions it properly within the QR Code
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
- * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
- */
- protected function getLogo():string{
- // @todo: customize the <g> element to your liking (css class, style...)
- return sprintf(
- '%5$s<g transform="translate(%1$s %1$s) scale(%2$s)" class="%3$s">%5$s %4$s%5$s</g>',
- (($this->moduleCount - ($this->moduleCount * $this->options->svgLogoScale)) / 2),
- $this->options->svgLogoScale,
- $this->options->svgLogoCssClass,
- file_get_contents($this->options->svgLogo),
- $this->options->eol,
- );
- }
- protected function collectModules():array{
- $paths = [];
- $dotColors = $this->options->dotColors; // avoid magic getter in long loops
- // collect the modules for each type
- foreach($this->matrix->getMatrix() as $y => $row){
- foreach($row as $x => $M_TYPE){
- $M_TYPE_LAYER = $M_TYPE;
- if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){
- // to connect paths we'll redeclare the $M_TYPE_LAYER to data only
- $M_TYPE_LAYER = QRMatrix::M_DATA;
- if($this->matrix->isDark($M_TYPE)){
- $M_TYPE_LAYER = QRMatrix::M_DATA_DARK;
- }
- }
- // randomly assign another $M_TYPE_LAYER for the given types
- // note that the layer id has to be an integer value,
- // ideally outside the several bitmask values
- if($M_TYPE_LAYER === QRMatrix::M_QUIETZONE_DARK){
- $M_TYPE_LAYER = array_rand($dotColors);
- }
- // collect the modules per $M_TYPE
- $module = $this->moduleTransform($x, $y, $M_TYPE, $M_TYPE_LAYER);
- if(!empty($module)){
- $paths[$M_TYPE_LAYER][] = $module;
- }
- }
- }
- // beautify output
- ksort($paths);
- return $paths;
- }
- }
- /**
- * the augmented options class
- *
- * @property int $additionalModules
- * @property array<int, string> $dotColors
- * @property string $svgLogo
- * @property float $svgLogoScale
- * @property string $svgLogoCssClass
- */
- class RoundQuietzoneOptions extends QROptions{
- /**
- * The amount of additional modules to be used in the circle diameter calculation
- *
- * Note that the middle of the circle stroke goes through the (assumed) outer corners
- * or centers of the QR Code (excluding quiet zone)
- *
- * Example:
- *
- * - a value of -1 would go through the center of the outer corner modules of the finder patterns
- * - a value of 0 would go through the corner of the outer modules of the finder patterns
- * - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45-degree angle
- */
- protected int $additionalModules = 0;
- /**
- * a map of $M_TYPE_LAYER => color
- *
- * @see \array_rand()
- * @var array<int, string>
- */
- protected array $dotColors = [];
- // path to svg logo
- protected string $svgLogo;
- // logo scale in % of QR Code size, clamped to 10%-30%
- protected float $svgLogoScale = 0.20;
- // css class for the logo (defined in $svgDefs)
- protected string $svgLogoCssClass = '';
- // check logo
- protected function set_svgLogo(string $svgLogo):void{
- if(!file_exists($svgLogo) || !is_readable($svgLogo)){
- throw new QRCodeException('invalid svg logo');
- }
- // @todo: validate svg
- $this->svgLogo = $svgLogo;
- }
- // clamp logo scale
- protected function set_svgLogoScale(float $svgLogoScale):void{
- $this->svgLogoScale = max(0.05, min(0.3, $svgLogoScale));
- }
- }
- /*
- * Runtime
- */
- $options = new RoundQuietzoneOptions;
- // custom dot options (see extended class)
- $options->additionalModules = 5;
- $options->dotColors = [
- 111 => '#e2453c',
- 222 => '#e07e39',
- 333 => '#e5d667',
- 444 => '#51b95b',
- 555 => '#1e72b7',
- 666 => '#6f5ba7',
- ];
- // generate the CSS for the several colored layers
- $layerColors = '';
- foreach($options->dotColors as $layer => $color){
- $layerColors .= sprintf("\n\t\t.qr-%s{ fill: %s; }", $layer, $color);
- }
- // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient
- // please forgive me for I have committed colorful crimes
- $options->svgDefs = '
- <linearGradient id="blurple" x1="100%" y2="100%">
- <stop stop-color="#D70071" offset="0"/>
- <stop stop-color="#9C4E97" offset="0.5"/>
- <stop stop-color="#0035A9" offset="1"/>
- </linearGradient>
- <linearGradient id="rainbow" x1="100%" y2="100%">
- <stop stop-color="#e2453c" offset="2.5%"/>
- <stop stop-color="#e07e39" offset="21.5%"/>
- <stop stop-color="#e5d667" offset="40.5%"/>
- <stop stop-color="#51b95b" offset="59.5%"/>
- <stop stop-color="#1e72b7" offset="78.5%"/>
- <stop stop-color="#6f5ba7" offset="97.5%"/>
- </linearGradient>
- <style><![CDATA[
- .light{ fill: #dedede; }
- .dark{ fill: url(#rainbow); }
- .logo{ fill: url(#blurple); }
- #circle{ fill: none; stroke: url(#blurple); }
- '.$layerColors.'
- ]]></style>';
- // custom SVG logo options
- $options->svgLogo = __DIR__.'/github.svg'; // logo from: https://github.com/simple-icons/simple-icons
- $options->svgLogoScale = 0.2;
- $options->svgLogoCssClass = 'logo';
- // common QRCode options
- $options->version = 7;
- $options->eccLevel = EccLevel::H;
- $options->addQuietzone = false; // we're not adding a quiet zone, this is done internally in our own module
- $options->outputBase64 = false; // avoid base64 URI output for the example
- $options->outputInterface = RoundQuietzoneSVGoutput::class; // load our own output class
- $options->drawLightModules = false; // set to true to add the light modules
- // common SVG options
- $options->connectPaths = true;
- $options->excludeFromConnect = [
- QRMatrix::M_QUIETZONE_DARK,
- ];
- $options->drawCircularModules = true;
- $options->circleRadius = 0.4;
- $options->keepAsSquare = [
- QRMatrix::M_FINDER_DARK,
- QRMatrix::M_FINDER_DOT,
- QRMatrix::M_ALIGNMENT_DARK,
- ];
- $out = (new QRCode($options))->render('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
- if(PHP_SAPI !== 'cli'){
- header('Content-type: image/svg+xml');
- if(extension_loaded('zlib')){
- header('Vary: Accept-Encoding');
- header('Content-Encoding: gzip');
- $out = gzencode($out, 9);
- }
- }
- echo $out;
- exit;
|