mirror of
https://github.com/chillerlan/php-qrcode.git
synced 2026-08-31 04:28:03 +00:00
🛀 SVG output rework
This commit is contained in:
+33
-49
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user