Issues (39)

src/Output/QRMarkupSVG.php (3 issues)

1
<?php
2
/**
3
 * Class QRMarkupSVG
4
 *
5
 * @created      06.06.2022
6
 * @author       smiley <[email protected]>
7
 * @copyright    2022 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\QRCode\Output;
12
13
use chillerlan\QRCode\Data\QRMatrix;
14
use function array_chunk, implode, is_string, preg_match, sprintf, trim;
15
16
/**
17
 * SVG output
18
 *
19
 * @see https://github.com/codemasher/php-qrcode/pull/5
20
 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
21
 * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/
22
 * @see http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html
23
 */
24
class QRMarkupSVG extends QRMarkup{
25
26
	/**
27
	 * @todo: XSS proof
28
	 *
29
	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
30
	 * @inheritDoc
31
	 */
32
	public static function moduleValueIsValid($value):bool{
33
34
		if(!is_string($value)){
35
			return false;
36
		}
37
38
		$value = trim($value);
39
40
		// url(...)
41
		if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){
42
			return true;
43
		}
44
45
		// otherwise check for standard css notation
46
		return parent::moduleValueIsValid($value);
47
	}
48
49
	/**
50
	 * @inheritDoc
51
	 */
52
	protected function createMarkup(bool $saveToFile):string{
53
		$svg = $this->header();
54
55
		if(!empty($this->options->svgDefs)){
56
			$svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
57
		}
58
59
		$svg .= $this->paths();
60
61
		// close svg
62
		$svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
63
64
		// transform to data URI only when not saving to file
65
		if(!$saveToFile && $this->options->imageBase64){
66
			$svg = $this->toBase64DataURI($svg, 'image/svg+xml');
67
		}
68
69
		return $svg;
70
	}
71
72
	/**
73
	 * returns the <svg> header with the given options parsed
74
	 */
75
	protected function header():string{
76
		$width  = ($this->options->svgWidth !== null) ? sprintf(' width="%s"', $this->options->svgWidth) : '';
77
		$height = ($this->options->svgHeight !== null) ? sprintf(' height="%s"', $this->options->svgHeight) : '';
78
79
		/** @noinspection HtmlUnknownAttribute */
80
		return sprintf(
81
			'<?xml version="1.0" encoding="UTF-8"?>%6$s'.
82
			'<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',
83
			$this->options->cssClass,
84
			($this->options->svgViewBoxSize ?? $this->moduleCount),
85
			$this->options->svgPreserveAspectRatio,
86
			$width,
87
			$height,
88
			$this->options->eol
89
		);
90
	}
91
92
	/**
93
	 * returns one or more SVG <path> elements
94
	 */
95
	protected function paths():string{
96
		$paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE));
97
		$svg   = [];
98
99
		// create the path elements
100
		foreach($paths as $M_TYPE => $modules){
101
			// limit the total line length
102
			$chunks = array_chunk($modules, 100);
103
			$chonks = [];
104
105
			foreach($chunks as $chunk){
106
				$chonks[] = implode(' ', $chunk);
107
			}
108
109
			$path = implode($this->options->eol, $chonks);
0 ignored issues
show
It seems like $this->options->eol can also be of type null; however, parameter $glue of implode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
			$path = implode(/** @scrutinizer ignore-type */ $this->options->eol, $chonks);
Loading history...
110
111
			if(empty($path)){
112
				continue;
113
			}
114
115
			$svg[] = $this->path($path, $M_TYPE);
116
		}
117
118
		return implode($this->options->eol, $svg);
119
	}
120
121
	/**
122
	 * renders and returns a single <path> element
123
	 *
124
	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
125
	 */
126
	protected function path(string $path, int $M_TYPE):string{
127
		$val = $this->getModuleValue($M_TYPE);
128
		// ignore non-existent module values
129
		$format = empty($val)
130
			? '<path class="%1$s" d="%2$s"/>'
131
			: '<path class="%1$s" fill="%3$s" fill-opacity="%4$s" d="%2$s"/>';
132
133
		return sprintf(
134
			$format,
135
			$this->getCssClass($M_TYPE),
136
			$path,
137
			($val ?? ''), // value may or may not exist
138
			$this->options->svgOpacity
139
		);
140
	}
141
142
	/**
143
	 * @inheritDoc
144
	 */
145
	protected function getCssClass(int $M_TYPE = 0):string{
146
		return implode(' ', [
147
			'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE),
148
			(($M_TYPE & QRMatrix::IS_DARK) === QRMatrix::IS_DARK) ? 'dark' : 'light',
149
			$this->options->cssClass,
150
		]);
151
	}
152
153
	/**
154
	 * returns a path segment for a single module
155
	 *
156
	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
157
	 */
158
	protected function module(int $x, int $y, int $M_TYPE):string{
0 ignored issues
show
The parameter $M_TYPE is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

158
	protected function module(int $x, int $y, /** @scrutinizer ignore-unused */ int $M_TYPE):string{

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
159
160
		if(!$this->options->drawLightModules && !$this->matrix->check($x, $y)){
161
			return '';
162
		}
163
164
		if($this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->options->keepAsSquare)){
0 ignored issues
show
It seems like $this->options->keepAsSquare can also be of type null; however, parameter $M_TYPES of chillerlan\QRCode\Data\QRMatrix::checkTypeIn() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
		if($this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, /** @scrutinizer ignore-type */ $this->options->keepAsSquare)){
Loading history...
165
			$r = $this->options->circleRadius;
166
167
			return sprintf(
168
				'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',
169
				($x + 0.5 - $r),
170
				($y + 0.5),
171
				$r,
172
				($r * 2)
173
			);
174
175
		}
176
177
		return sprintf('M%1$s %2$s h1 v1 h-1Z', $x, $y);
178
	}
179
180
}
181