Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codemasher/6309f5159f1008cb988f54dde439a64b to your computer and use it in GitHub Desktop.
Save codemasher/6309f5159f1008cb988f54dde439a64b 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 v5 dev-main)
*/
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\Output\QRMarkupSVG;
use chillerlan\QRCode\{QRCode, QROptions};
require_once __DIR__.'/../vendor/autoload.php';
class RoundedCornerSVGQRCodeOutput extends QRMarkupSVG{
// this constant may be added to QRMatrix
protected const neighbours = [
0b00000001 => [-1, -1],
0b00000010 => [ 0, -1],
0b00000100 => [ 1, -1],
0b00001000 => [ 1, 0],
0b00010000 => [ 1, 1],
0b00100000 => [ 0, 1],
0b01000000 => [-1, 1],
0b10000000 => [-1, 0]
];
/**
* Checks the status neighbouring modules of the given module at ($x, $y) and returns a bitmask with the results.
*
* The 8 flags of the bitmask represent the status of each of the neighbouring fields,
* starting with the lowest bit for top left, going clockwise:
*
* 1 2 3
* 8 # 4
* 7 6 5
*
* @todo: when $M_TYPE_VALUE is given, it should check for the same $M_TYPE while igrnoring the IS_DARK flag
*
* (this method may be added to QRMatrix as direct array access is faster than method calls)
*/
protected function checkNeighbours(int $x, int $y, int $M_TYPE_VALUE):int{
$bits = 0;
foreach($this::neighbours as $bit => $coord){
if($this->matrix->check($x + $coord[0], $y + $coord[1])){
$bits |= $bit;
}
}
return $bits;
}
protected function paths():string{
// main square with rounded corners
$svg = $this->symbolWithPath('s', ['M0,0 m0,0.3 v0.4 q0,0.3 0.3,0.3 h0.4 q0.3,0 0.3,-0.3 v-0.4 q0,-0.3 -0.3,-0.3 h-0.4 q-0.3,0 -0.3,0.3Z']);
// "plus" to invert the corner radius
$svg .= $this->symbolWithPath('p', [
// a curved plus, radius slightly righter than the main square
'M0.3 0.6 Q0.58,0.58 0.6,0.3 Q0.62,0.58 0.9,0.6 Q0.62,0.62 0.6,0.9 Q0.58,0.61 0.3,0.6 Z',
// a sharp plus (with points further out logner than the curved one)
'M0.6 0 L0.61 0.59 L1.2 0.6 L0.61 0.61 L0.6 1.2 L0.59 0.61 L0 0.6 L0.59 0.59 Z'
]);
// top/left/bottom/right triangles fill in edges
$svg .= $this->symbolWithPath('t', ['M0 0.3 L0.6 0.3 L0.3 0.9 Z']);
$svg .= $this->symbolWithPath('l', ['M0.6 0 L0 0.3 L0.6 0.6 Z']);
$svg .= $this->symbolWithPath('b', ['M0 0.6 L0.6 0.6 L0.3 0 Z']);
$svg .= $this->symbolWithPath('r', ['M0.3 0 L1.2 0.3 L0.3 0.6 Z']);
// a daimond to fill in block areas
$svg .= $this->symbolWithPath('d', ['M0.6 0 L1.2 0.6 L0.6 1.2 L0 0.6 Z']);
foreach($this->matrix->matrix() as $y => $row){
foreach($row as $x => $val){
if(($val & QRMatrix::IS_DARK) !== QRMatrix::IS_DARK){
continue;
}
$bits = $this->checkNeighbours($x, $y, $val);
$check = fn(int $mask):bool => ($bits & $mask) === $mask;
// main square block
$svg .= $this->use('s', $x, $y);
// top left corner
if($check(0b10000011)){
$svg .= $this->use('d', $x - 0.6, $y - 0.6);
}
elseif($check(0b10000001)){
$svg .= $this->use('p', $x - 0.6, $y - 0.6);
}
elseif($check(0b00000011)){
$svg .= $this->use('p', $x - 0.6, $y - 0.6);
}
elseif($check(0b00000010)){
$svg .= $this->use('r', $x - 0.3, $y - 0.3);
}
elseif($check(0b10000000)){
$svg .= $this->use('t', $x - 0.3, $y - 0.3);
}
// bottom right corner
# if($check(0b00111000)){
// already done
# } else
if($check(0b00011000)){
$svg .= $this->use('p', $x + 1 - 0.6, $y + 1 - 0.6);
}
elseif($check(0b00110000)){
$svg .= $this->use('p', $x + 1 - 0.6, $y + 1 - 0.6);
}
elseif($check(0b00100000)){
$svg .= $this->use('l', $x + 1 - 0.6, $y + 1 - 0.3);
}
elseif($check(0b00001000)){
$svg .= $this->use('b', $x + 1 - 0.3, $y + 1 - 0.6);
}
// top right corner
# if($check(0b00001110)){
// already done
# } else
if($check(0b00001100)){
$svg .= $this->use('p', $x + 1 - 0.6, $y - 0.6);
}
elseif($check(0b00000110)){
$svg .= $this->use('p', $x + 1 - 0.6, $y - 0.6);
}
}
}
return $svg;
}
protected function use($node, $x, $y){
return "<use href=\"#$node\" x=\"$x\" y=\"$y\" />\n";
}
protected function symbolWithPath($id, $paths, $fill = null){
$fillOutput = $fill === null ? '' : " fill=\"$fill\"";
$output = "<symbol id=\"$id\"$fillOutput>";
foreach($paths as $path){
$output .= "<path d=\"$path\" class=\"dark\" shape-rendering=\"geometricPrecision\" />"; //
}
$output .= "</symbol>\n";
return $output;
}
}
$url = 'https://github.com/chillerlan/php-qrcode/issues/127';
$options = new QROptions([
'eccLevel' => EccLevel::L,
'imageBase64' => false,
'addQuietzone' => true,
'outputType' => QRCode::OUTPUT_CUSTOM,
'outputInterface' => RoundedCornerSVGQRCodeOutput::class,
]);
header('content-type: image/svg+xml');
echo (new QRCode($options))->render($url);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment