🛀 SVG output rework

This commit is contained in:
codemasher
2021-12-06 21:27:40 +01:00
parent eac6f7ddac
commit 6006df8c9a
7 changed files with 223 additions and 120 deletions
+33 -49
View File
@@ -14,66 +14,50 @@ use chillerlan\QRCode\Common\EccLevel;
require_once __DIR__.'/../vendor/autoload.php';
$data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
$data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs';
$gzip = true;
$options = new QROptions([
'version' => 7,
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
'imageBase64' => false,
'eccLevel' => EccLevel::L,
'svgViewBoxSize' => 530,
'addQuietzone' => true,
'cssClass' => 'my-css-class',
'svgOpacity' => 1.0,
'svgDefs' => '
<linearGradient id="g2">
<stop offset="0%" stop-color="#39F" />
<stop offset="100%" stop-color="#F3F" />
</linearGradient>
<linearGradient id="g1">
<stop offset="0%" stop-color="#F3F" />
<stop offset="100%" stop-color="#39F" />
</linearGradient>
<style>rect{shape-rendering:crispEdges}</style>',
'moduleValues' => [
// finder
QRMatrix::M_FINDER | QRMatrix::IS_DARK => 'url(#g1)', // dark (true)
QRMatrix::M_FINDER => '#fff', // light (false)
QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => 'url(#g2)', // finder dot, dark (true)
// alignment
QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => 'url(#g1)',
QRMatrix::M_ALIGNMENT => '#fff',
// timing
QRMatrix::M_TIMING | QRMatrix::IS_DARK => 'url(#g1)',
QRMatrix::M_TIMING => '#fff',
// format
QRMatrix::M_FORMAT | QRMatrix::IS_DARK => 'url(#g1)',
QRMatrix::M_FORMAT => '#fff',
// version
QRMatrix::M_VERSION | QRMatrix::IS_DARK => 'url(#g1)',
QRMatrix::M_VERSION => '#fff',
// data
QRMatrix::M_DATA | QRMatrix::IS_DARK => 'url(#g2)',
QRMatrix::M_DATA => '#fff',
// darkmodule
QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => 'url(#g1)',
// separator
QRMatrix::M_SEPARATOR => '#fff',
// quietzone
QRMatrix::M_QUIETZONE => '#fff',
],
'version' => 7,
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
'imageBase64' => false,
'eccLevel' => EccLevel::L,
'addQuietzone' => true,
// if set to true, the light modules won't be rendered
'imageTransparent' => false,
// empty the default value to remove the fill* attributes from the <path> elements
'markupDark' => '',
'markupLight' => '',
// draw the modules as circles isntead of squares
'svgDrawCircularModules' => true,
'svgCircleRadius' => 0.3,
// keep modules of thhese types as square
'svgKeepAsSquare' => [QRMatrix::M_FINDER|QRMatrix::IS_DARK, QRMatrix::M_FINDER_DOT, QRMatrix::M_ALIGNMENT|QRMatrix::IS_DARK],
// connect
'svgConnectPaths' => true,
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient
'svgDefs' => '
<linearGradient id="rainbow" x1="100%" y2="100%">
<stop stop-color="#e2453c" offset="2.5%"/>
<stop stop-color="#e07e39" offset="21.5%"/>
<stop stop-color="#e5d667" offset="40.5%"/>
<stop stop-color="#51b95b" offset="59.5%"/>
<stop stop-color="#1e72b7" offset="78.5%"/>
<stop stop-color="#6f5ba7" offset="97.5%"/>
</linearGradient>
<style><![CDATA[
.dark{fill: url(#rainbow);}
.light{fill: #eee;}
]]></style>',
]);
$qrcode = (new QRCode($options))->render($data);
header('Content-type: image/svg+xml');
if($gzip === true){
if($gzip){
header('Vary: Accept-Encoding');
header('Content-Encoding: gzip');
$qrcode = gzencode($qrcode, 9);
}
echo $qrcode;
+14
View File
@@ -203,6 +203,20 @@ final class QRMatrix{
return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE;
}
/**
* checks whether a module matches one of the given $M_TYPES
*/
public function checkTypes(int $x, int $y, array $M_TYPES):bool{
foreach($M_TYPES as $type){
if($this->checkType($x, $y, $type)){
return true;
}
}
return false;
}
/**
* Checks whether a module is true (dark) or false (light)
*
+115 -66
View File
@@ -10,9 +10,10 @@
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\QRCode;
use function is_string, sprintf, strip_tags, trim;
use function implode, is_string, ksort, sprintf, strip_tags, trim;
/**
* Converts the matrix into markup types: HTML, SVG, ...
@@ -21,12 +22,6 @@ class QRMarkup extends QROutputAbstract{
protected string $defaultMode = QRCode::OUTPUT_MARKUP_SVG;
/**
* @see \sprintf()
*/
protected string $svgHeader = '<svg xmlns="http://www.w3.org/2000/svg"'.
' class="qr-svg %1$s" style="width: 100%%; height: auto;" viewBox="0 0 %2$d %2$d">';
/**
* @inheritDoc
*/
@@ -41,7 +36,7 @@ class QRMarkup extends QROutputAbstract{
: $this->options->markupLight;
}
else{
$this->moduleValues[$M_TYPE] = trim(strip_tags($v), '\'"');
$this->moduleValues[$M_TYPE] = trim(strip_tags($v), " '\"\r\n\t");
}
}
@@ -84,75 +79,129 @@ class QRMarkup extends QROutputAbstract{
* SVG output
*
* @see https://github.com/codemasher/php-qrcode/pull/5
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
* @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/
*/
protected function svg(string $file = null):string{
$matrix = $this->matrix->matrix();
$svg = sprintf($this->svgHeader, $this->options->cssClass, $this->options->svgViewBoxSize ?? $this->moduleCount)
.$this->options->eol
.'<defs>'.$this->options->svgDefs.'</defs>'
.$this->options->eol;
foreach($this->moduleValues as $M_TYPE => $value){
$path = '';
foreach($matrix as $y => $row){
//we'll combine active blocks within a single row as a lightweight compression technique
$start = null;
$count = 0;
foreach($row as $x => $module){
if($module === $M_TYPE){
$count++;
if($start === null){
$start = $x;
}
if(isset($row[$x + 1])){
continue;
}
}
if($count > 0){
$len = $count;
$start ??= 0; // avoid type coercion in sprintf() - phan happy
$path .= sprintf('M%s %s h%s v1 h-%sZ ', $start, $y, $len, $len);
// reset count
$count = 0;
$start = null;
}
}
}
if(!empty($path)){
$svg .= sprintf(
'<path class="qr-%s %s" stroke="transparent" fill="%s" fill-opacity="%s" d="%s" />',
$M_TYPE, $this->options->cssClass, $value, $this->options->svgOpacity, $path
);
}
$svg = $this->svgHeader();
if(!empty($this->options->svgDefs)){
$svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
}
$svg .= $this->svgPaths();
// close svg
$svg .= '</svg>'.$this->options->eol;
$svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
// if saving to file, append the correct headers
if($file !== null){
return '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'.
$this->options->eol.$svg;
}
if($this->options->imageBase64){
// transform to data URI only when not saving to file
if($file === null && $this->options->imageBase64){
$svg = $this->base64encode($svg, 'image/svg+xml');
}
return $svg;
}
/**
* returns the <svg> header with the given options parsed
*/
protected function svgHeader():string{
$width = $this->options->svgWidth !== null ? sprintf(' width="%s"', $this->options->svgWidth) : '';
$height = $this->options->svgHeight !== null ? sprintf(' height="%s"', $this->options->svgHeight) : '';
/** @noinspection HtmlUnknownAttribute */
return sprintf(
'<?xml version="1.0" encoding="UTF-8"?>%6$s'.
'<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" viewBox="0 0 %2$s %2$s" preserveAspectRatio="%3$s"%4$s%5$s>%6$s',
$this->options->cssClass,
$this->options->svgViewBoxSize ?? $this->moduleCount,
$this->options->svgPreserveAspectRatio,
$width,
$height,
$this->options->eol
);
}
/**
* returns one or more SVG <path> elements
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
*/
protected function svgPaths():string{
$paths = [];
// collect the modules for each type
foreach($this->matrix->matrix() as $y => $row){
foreach($row as $x => $M_TYPE){
if($this->options->svgConnectPaths && !$this->matrix->checkTypes($x, $y, $this->options->svgExcludeFromConnect)){
// to connect paths we'll redeclare the $M_TYPE to data only
$M_TYPE = QRMatrix::M_DATA;
if($this->matrix->check($x, $y)){
$M_TYPE |= QRMatrix::IS_DARK;
}
}
// collect the modules per $M_TYPE
$paths[$M_TYPE][] = $this->svgModule($x, $y);
}
}
// beautify output
ksort($paths);
$svg = [];
// create the path elements
foreach($paths as $M_TYPE => $path){
$path = trim(implode(' ', $path));
if(empty($path)){
continue;
}
$cssClass = implode(' ', [
'qr-'.$M_TYPE,
($M_TYPE & QRMatrix::IS_DARK) === QRMatrix::IS_DARK ? 'dark' : 'light',
$this->options->cssClass,
]);
$format = empty($this->moduleValues[$M_TYPE])
? '<path class="%1$s" d="%2$s"/>'
: '<path class="%1$s" fill="%3$s" fill-opacity="%4$s" d="%2$s"/>';
$svg[] = sprintf($format, $cssClass, $path, $this->moduleValues[$M_TYPE], $this->options->svgOpacity);
}
return implode($this->options->eol, $svg);
}
/**
* returns a path segment for a single module
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
*/
protected function svgModule(int $x, int $y):string{
if($this->options->imageTransparent && !$this->matrix->check($x, $y)){
return '';
}
if($this->options->svgDrawCircularModules && !$this->matrix->checkTypes($x, $y, $this->options->svgKeepAsSquare)){
$r = $this->options->svgCircleRadius;
return sprintf(
'M%1$s %2$s a%3$s %3$s 0 1 0 %4$s 0 a%3$s,%3$s 0 1 0 -%4$s 0Z',
($x + 0.5 - $r),
($y + 0.5),
$r,
($r * 2)
);
}
return sprintf('M%1$s %2$s h%3$s v1 h-%4$sZ', $x, $y, 1, 1);
}
}
+8
View File
@@ -31,6 +31,14 @@ use chillerlan\Settings\SettingsContainerAbstract;
* @property float $svgOpacity
* @property string $svgDefs
* @property int $svgViewBoxSize
* @property string $svgPreserveAspectRatio
* @property string $svgWidth
* @property string $svgHeight
* @property bool $svgConnectPaths
* @property array $svgExcludeFromConnect
* @property bool $svgDrawCircularModules
* @property float $svgCircleRadius
* @property array $svgKeepAsSquare
* @property string $textDark
* @property string $textLight
* @property string $markupDark
+49 -2
View File
@@ -115,19 +115,66 @@ trait QROptionsTrait{
/**
* anything between <defs>
*
* @see https://developer.mozilla.org/docs/Web/SVG/Element/defs
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
*/
protected string $svgDefs = '<style>rect{shape-rendering:crispEdges}</style>';
protected string $svgDefs = '';
/**
* SVG viewBox size. a single integer number which defines width/height of the viewBox attribute.
*
* viewBox="0 0 x x"
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
* @see https://css-tricks.com/scale-svg/#article-header-id-3
*/
protected ?int $svgViewBoxSize = null;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
*/
protected string $svgPreserveAspectRatio = 'xMidYMid';
/**
* optional "width" attribute with the specified value (note that the value is not checked!)
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/width
*/
protected ?string $svgWidth = null;
/**
* optional "height" attribute with the specified value (note that the value is not checked!)
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/height
*/
protected ?string $svgHeight = null;
/**
* whether to connect the paths for the several module types to avoid weird glitches when using gradients etc.
*
* @see https://github.com/chillerlan/php-qrcode/issues/57
*/
protected bool $svgConnectPaths = false;
/**
* specify which paths/patterns to exclude from connecting if $svgConnectPaths is set to true
*/
protected array $svgExcludeFromConnect = [];
/**
* specify whether to draw the modules as filled circles
*/
protected bool $svgDrawCircularModules = false;
/**
* specifies the radius of the modules when $svgDrawCircularModules is set to true
*/
protected float $svgCircleRadius = 0.45;
/**
* specifies which module types to exclude when $svgDrawCircularModules is set to true
*/
protected array $svgKeepAsSquare = [];
/**
* string substitute for dark
*/
+3 -2
View File
@@ -41,8 +41,9 @@ class QRMarkupTest extends QROutputTestAbstract{
* @inheritDoc
*/
public function testSetModuleValues():void{
$this->options->imageBase64 = false;
$this->options->moduleValues = [
$this->options->imageBase64 = false;
$this->options->imageTransparent = false;
$this->options->moduleValues = [
// data
QRMatrix::M_DATA | QRMatrix::IS_DARK => '#4A6000',
QRMatrix::M_DATA => '#ECF9BE',
File diff suppressed because one or more lines are too long