* @copyright 2022 smiley * @license MIT * * @noinspection PhpIllegalPsrClassPathInspection */ use chillerlan\QRCode\Common\EccLevel; use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Output\{QROutputInterface, QRMarkupSVG}; use chillerlan\QRCode\{QRCode, QROptions}; require_once __DIR__.'/../vendor/autoload.php'; /* * Class definition */ /** * the extended SVG output module */ class RoundQuietzoneSVGoutput extends QRMarkupSVG{ /** * @inheritDoc */ 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(); // color the quiet zone $this->colorQuietzone($quietzoneSize, $diameter / 2); // start SVG output $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(); $svg .= $this->addCircle($diameter / 2); // 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->toBase64DataURI($svg, 'image/svg+xml'); } return $svg; } /** * @inheritDoc */ protected function path(string $path, int $M_TYPE):string{ // omit the "fill" and "opacity" attributes on the path element return sprintf('', $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; foreach($this->matrix->matrix() as $y => $row){ foreach($row as $x => $value){ // skip anything that's not quiet zone if($value !== 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, $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 $radius):bool{ $dx = abs($x - $this->moduleCount / 2); $dy = abs($y - $this->moduleCount / 2); if($dx + $dy <= $radius){ return true; } if($dx > $radius || $dy > $radius){ return false; } if(pow($dx, 2) + pow($dy, 2) <= pow($radius, 2)){ return true; } return false; } /** * add a solid circle around the matrix */ protected function addCircle(float $radius):string{ return sprintf( '%4$s', $this->moduleCount / 2, round($radius, 5), $this->options->circleRadius * 2, $this->options->eol ); } } /** * the augmented options class */ 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; } /* * Runtime */ $options = new RoundQuietzoneOptions([ 'additionalModules' => 5, 'version' => 7, 'eccLevel' => EccLevel::H, // maximum error correction capacity, esp. for print 'addQuietzone' => false, // we're not adding a quiet zone, this is done internally in our own module 'imageBase64' => false, // avoid base64 URI output 'outputType' => QROutputInterface::CUSTOM, 'outputInterface' => RoundQuietzoneSVGoutput::class, // load our own output class 'drawLightModules' => false, // set to true to add the light modules 'connectPaths' => true, 'excludeFromConnect' => [ QRMatrix::M_FINDER|QRMatrix::IS_DARK, QRMatrix::M_FINDER_DOT|QRMatrix::IS_DARK, QRMatrix::M_ALIGNMENT|QRMatrix::IS_DARK, QRMatrix::M_QUIETZONE|QRMatrix::IS_DARK ], 'drawCircularModules' => true, 'circleRadius' => 0.4, 'keepAsSquare' => [ QRMatrix::M_FINDER|QRMatrix::IS_DARK, QRMatrix::M_FINDER_DOT|QRMatrix::IS_DARK, QRMatrix::M_ALIGNMENT|QRMatrix::IS_DARK, ], // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient 'svgDefs' => ' ', ]); $qrcode = (new QRCode($options))->render('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); if(php_sapi_name() !== 'cli'){ header('Content-type: image/svg+xml'); if(extension_loaded('zlib')){ header('Vary: Accept-Encoding'); header('Content-Encoding: gzip'); $qrcode = gzencode($qrcode, 9); } } echo $qrcode; exit;