Created
June 4, 2022 08:13
-
-
Save abhibeckert/2bfb6a884c7fdd3194af86bd28ef9c10 to your computer and use it in GitHub Desktop.
Rounded Corners SVG QR Code in PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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