Forked from abhibeckert/gist:2bfb6a884c7fdd3194af86bd28ef9c10
Last active
June 7, 2022 13:14
-
-
Save codemasher/6309f5159f1008cb988f54dde439a64b 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 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