Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save abhibeckert/2bfb6a884c7fdd3194af86bd28ef9c10 to your computer and use it in GitHub Desktop.
Save abhibeckert/2bfb6a884c7fdd3194af86bd28ef9c10 to your computer and use it in GitHub Desktop.
Rounded Corners SVG QR Code in PHP
<?php
/**
* This code generates an SVG QR code with rounded corners. It uses a round rect for each square and then additional
* paths to fill in the gap where squares are next to each other. Adjacent squares overlap - to almost completely
* eliminate hairline antialias "cracks" that tend to appear when two SVG paths are exactly adjacent to each other.
*
* composer require chillerlan/php-qrcode (tested with version 4.2)
*/
use chillerlan\QRCode\{QRCode, QROptions};
use chillerlan\QRCode\Output\QROutputAbstract;
require 'vendor/autoload.php';
class RoundedCornerSVGQRCodeOutput extends QROutputAbstract {
public function dump(string $file = null)
{
$output = $this->openSvg();
// main square with rounded corners
$output .= $this->symbolWithPath('s', 100, 100, 'M0 30 Q0,0 30,0 L70 0 Q100,0 100,30 L100 70 Q100,100 70,100 L30 100 Q0,100 0,70 Z');
// "plus" to invert the corner radius
$output .= $this->symbolWithPaths('p', 120, 120, [
'M30 60 Q58,58 60,30 Q62,58 90,60 Q62,62 60,90 Q58,61 30,60 Z', // a curved plus, radius slightly righter than the main square
'M60 0 L61 59 L120 60 L61 61 L60 120 L59 61 L0 60 L59 59 Z' // a sharp plus (with points further out logner than the curved one)
]);
// top/left/bottom/right triangles fill in edges
$output .= $this->symbolWithPath('t', 60, 90, 'M0 30 L60 30 L30 90 Z');
$output .= $this->symbolWithPath('l', 60, 60, 'M60 0 L0 30 L60 60 Z');
$output .= $this->symbolWithPath('b', 60, 60, 'M0 60 L60 60 L30 0 Z');
$output .= $this->symbolWithPath('r', 120, 60, 'M30 0 L120 30 L30 60 Z');
// a daimond to fill in block areas
$output .= $this->symbolWithPath('d', 120, 120, 'M60 0 L120 60 L60 120 L0 60 Z');
$chk = function($col, $r) {
return $this->matrix->check($col, $r);
};
$use = function($node, $x, $y) {
return "<use href=\"#$node\" x=\"$x\" y=\"$y\" />\n";
};
for($r = 0; $r < $this->moduleCount; $r++){
for($c = 0; $c < $this->moduleCount; $c++){
if (!$chk($c, $r)) {
continue;
}
// prep
$cMin = ($c == 0);
$rMin = ($r == 0);
$cMax = $c == ($this->moduleCount - 1);
$rMax = $r == ($this->moduleCount - 1);
// main square block
$output .= $use(
's',
($c * 100),
($r * 100)
);
// top left corner
if (!$cMin && !$rMin && $chk($c-1, $r) && $chk($c-1, $r-1) && $chk($c, $r-1)) {
$output .= $use(
'd',
(($c * 100) - 60),
(($r * 100) - 60)
);
} else if (!$cMin && !$rMin && $chk($c-1, $r) && $chk($c-1, $r-1)) {
$output .= $use(
'p',
(($c * 100) - 60),
(($r * 100) - 60)
);
} else if (!$cMin && !$rMin && $chk($c, $r-1) && $chk($c-1, $r-1)) {
$output .= $use(
'p',
(($c * 100) - 60),
(($r * 100) - 60)
);
} else if (!$rMin && $chk($c, $r-1)) {
$output .= $use(
'r',
(($c * 100) - 30),
(($r * 100) - 30)
);
} else if (!$cMin && $chk($c-1, $r)) {
$output .= $use(
't',
(($c * 100) - 30),
(($r * 100) - 30)
);
}
// bottom right corner
if (!$cMax && !$rMax && $chk($c+1, $r) && $chk($c+1, $r+1) && $chk($c, $r+1)) {
// already done
} else if (!$cMax && !$rMax && $chk($c+1, $r) && $chk($c+1, $r+1)) {
$output .= $use(
'p',
((($c+1) * 100) - 60),
((($r+1) * 100) - 60)
);
} else if (!$cMax && !$rMax && $chk($c, $r+1) && $chk($c+1, $r+1)) {
$output .= $use(
'p',
((($c+1) * 100) - 60),
((($r+1) * 100) - 60)
);
} else if (!$rMax && $chk($c, $r+1)) {
$output .= $use(
'l',
((($c+1) * 100) - 60),
((($r+1) * 100) - 30)
);
} else if (!$cMax && $chk($c+1, $r)) {
$output .= $use(
'b',
((($c+1) * 100) - 30),
((($r+1) * 100) - 60)
);
}
// top right corner
if (!$cMax && !$rMin && $chk($c+1, $r) && $chk($c+1, $r-1) && $chk($c, $r-1)) {
// already done
} else if (!$cMax && !$rMin && $chk($c+1, $r) && $chk($c+1, $r-1)) {
$output .= $use(
'p',
((($c+1) * 100) - 60),
(($r * 100) - 60)
);
} else if (!$cMax && !$rMin && $chk($c, $r-1) && $chk($c+1, $r-1)) {
$output .= $use(
'p',
((($c+1) * 100) - 60),
(($r * 100) - 60)
);
}
}
}
$output .= '
</svg>
';
return $output;
}
function openSvg()
{
return '<svg version="1.1" viewBox="0 0 ' . ($this->moduleCount * 100) . ' ' . ($this->moduleCount * 100) . '" xmlns="http://www.w3.org/2000/svg" width="' . ($this->moduleCount * 10) . '" height="' . ($this->moduleCount * 10) . '">';
}
function symbolWithPath($id, $width, $height, $path, $fill=null)
{
return $this->symbolWithPaths($id, $width, $height, [$path], $fill);
}
function symbolWithPaths($id, $width, $height, $paths, $fill=null)
{
$fillOutput = $fill === null ? '' : " fill=\"$fill\"";
$output = "<symbol id=\"$id\" width=\"$width\" height=\"$height\"$fillOutput>";
foreach ($paths as $path) {
$output .= "<path d=\"$path\" />";
}
$output .= "</symbol>\n";
return $output;
}
protected function setModuleValues():void
{
// do nothing - abstract class requires this
}
}
$url = 'https://github.com/chillerlan/php-qrcode/issues/127';
$options = new QROptions([
'eccLevel' => QRCode::ECC_L,
'addQuietzone' => true,
]);
$qrOutputInterface = new RoundedCornerSVGQRCodeOutput($options, (new QRCode($options))->getMatrix($url));
header('content-type: image/svg+xml');
print($qrOutputInterface->dump());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment