-
-
Notifications
You must be signed in to change notification settings - Fork 302
/
svgRoundQuietzone.php
364 lines (302 loc) · 10.4 KB
/
svgRoundQuietzone.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
<?php
/**
* round quiet zone example
*
* @see https://github.com/chillerlan/php-qrcode/discussions/137
*
* @created 09.07.2022
* @author smiley <[email protected]>
* @copyright 2022 smiley
* @license MIT
*
* @noinspection PhpIllegalPsrClassPathInspection
*/
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\Output\{QROutputInterface, QRMarkupSVG};
use chillerlan\QRCode\{QRCode, QRCodeException, QROptions};
require_once __DIR__.'/../vendor/autoload.php';
/*
* Class definition
*/
/**
* Create SVG QR Codes with embedded logos (that are also SVG),
* randomly colored dots and a round quiet zone with added circle
*/
class RoundQuietzoneSVGoutput extends QRMarkupSVG{
/**
* @inheritDoc
*/
protected function createMarkup(bool $saveToFile):string{
// some Pythagorean magick
$diameter = sqrt(2 * pow(($this->moduleCount + $this->options->additionalModules), 2));
// calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it
$quietzoneSize = ((int)ceil(($diameter - $this->moduleCount) / 2) + 1);
// add the quiet zone to fill the circle
$this->matrix->setQuietZone($quietzoneSize);
// update the matrix dimensions to avoid errors in subsequent calculations
// the moduleCount is now QR Code matrix + 2x quiet zone
$this->setMatrixDimensions();
// color the quiet zone
$this->colorQuietzone($quietzoneSize, ($diameter / 2));
// calculate the logo space
$logoSpaceSize = (int)(ceil($this->moduleCount * $this->options->svgLogoScale) + 1);
// we're calling QRMatrix::setLogoSpace() manually, so QROptions::$addLogoSpace has no effect here
$this->matrix->setLogoSpace($logoSpaceSize);
// start SVG output
$svg = $this->header();
if(!empty($this->options->svgDefs)){
$svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
}
$svg .= $this->paths();
$svg .= $this->getLogo();
$svg .= $this->addCircle($diameter / 2);
// close svg
$svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
// transform to data URI only when not saving to file
if(!$saveToFile && $this->options->outputBase64){
$svg = $this->toBase64DataURI($svg, 'image/svg+xml');
}
return $svg;
}
/**
* @inheritDoc
*/
protected function path(string $path, int $M_TYPE):string{
// omit the "fill" and "opacity" attributes on the path element
return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
}
/**
* Sets random modules of the quiet zone to dark
*/
protected function colorQuietzone(int $quietzoneSize, float $radius):void{
$l1 = ($quietzoneSize - 1);
$l2 = ($this->moduleCount - $quietzoneSize);
// substract 1/2 stroke width and module radius from the circle radius to not cut off modules
$r = ($radius - $this->options->circleRadius * 2);
for($y = 0; $y < $this->moduleCount; $y++){
for($x = 0; $x < $this->moduleCount; $x++){
// skip anything that's not quiet zone
if(!$this->matrix->checkType($x, $y, QRMatrix::M_QUIETZONE)){
continue;
}
// leave one row of quiet zone around the matrix
if(
($x === $l1 && $y >= $l1 && $y <= $l2)
|| ($x === $l2 && $y >= $l1 && $y <= $l2)
|| ($y === $l1 && $x >= $l1 && $x <= $l2)
|| ($y === $l2 && $x >= $l1 && $x <= $l2)
){
continue;
}
// we need to add 0.5 units to the check values since we're calculating the element centers
// ($x/$y is the element's assumed top left corner)
if($this->checkIfInsideCircle(($x + 0.5), ($y + 0.5), $r)){
$this->matrix->set($x, $y, (bool)rand(0, 1), QRMatrix::M_QUIETZONE);
}
}
}
}
/**
* @see https://stackoverflow.com/a/7227057
*/
protected function checkIfInsideCircle(float $x, float $y, float $radius):bool{
$dx = abs($x - $this->moduleCount / 2);
$dy = abs($y - $this->moduleCount / 2);
if(($dx + $dy) <= $radius){
return true;
}
if($dx > $radius || $dy > $radius){
return false;
}
if((pow($dx, 2) + pow($dy, 2)) <= pow($radius, 2)){
return true;
}
return false;
}
/**
* add a solid circle around the matrix
*/
protected function addCircle(float $radius):string{
return sprintf(
'%4$s<circle id="circle" cx="%1$s" cy="%1$s" r="%2$s" stroke-width="%3$s"/>',
($this->moduleCount / 2),
round($radius, 5),
($this->options->circleRadius * 2),
$this->options->eol
);
}
/**
* returns a <g> element that contains the SVG logo and positions it properly within the QR Code
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
*/
protected function getLogo():string{
// @todo: customize the <g> element to your liking (css class, style...)
return sprintf(
'%5$s<g transform="translate(%1$s %1$s) scale(%2$s)" class="%3$s">%5$s %4$s%5$s</g>',
(($this->moduleCount - ($this->moduleCount * $this->options->svgLogoScale)) / 2),
$this->options->svgLogoScale,
$this->options->svgLogoCssClass,
file_get_contents($this->options->svgLogo),
$this->options->eol
);
}
/**
* @inheritDoc
*/
protected function collectModules(Closure $transform):array{
$paths = [];
$dotColors = $this->options->dotColors; // avoid magic getter in long loops
// collect the modules for each type
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$M_TYPE_LAYER = $M_TYPE;
if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){
// to connect paths we'll redeclare the $M_TYPE_LAYER to data only
$M_TYPE_LAYER = QRMatrix::M_DATA;
if($this->matrix->isDark($M_TYPE)){
$M_TYPE_LAYER = QRMatrix::M_DATA_DARK;
}
}
// randomly assign another $M_TYPE_LAYER for the given types
// note that the layer id has to be an integer value,
// ideally outside the several bitmask values
if($M_TYPE_LAYER === QRMatrix::M_DATA_DARK){
$M_TYPE_LAYER = array_rand($dotColors);
}
// collect the modules per $M_TYPE
$module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER);
if(!empty($module)){
$paths[$M_TYPE_LAYER][] = $module;
}
}
}
// beautify output
ksort($paths);
return $paths;
}
}
/**
* the augmented options class
*/
class RoundQuietzoneOptions extends QROptions{
/**
* The amount of additional modules to be used in the circle diameter calculation
*
* Note that the middle of the circle stroke goes through the (assumed) outer corners
* or centers of the QR Code (excluding quiet zone)
*
* Example:
*
* - a value of -1 would go through the center of the outer corner modules of the finder patterns
* - a value of 0 would go through the corner of the outer modules of the finder patterns
* - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45-degree angle
*/
protected int $additionalModules = 0;
/**
* a map of $M_TYPE_LAYER => color
*
* @see \array_rand()
*/
protected array $dotColors = [];
// path to svg logo
protected string $svgLogo;
// logo scale in % of QR Code size, clamped to 10%-30%
protected float $svgLogoScale = 0.20;
// css class for the logo (defined in $svgDefs)
protected string $svgLogoCssClass = '';
// check logo
protected function set_svgLogo(string $svgLogo):void{
if(!file_exists($svgLogo) || !is_readable($svgLogo)){
throw new QRCodeException('invalid svg logo');
}
// @todo: validate svg
$this->svgLogo = $svgLogo;
}
// clamp logo scale
protected function set_svgLogoScale(float $svgLogoScale):void{
$this->svgLogoScale = max(0.05, min(0.3, $svgLogoScale));
}
}
/*
* Runtime
*/
$options = new RoundQuietzoneOptions;
// custom dot options (see extended class)
$options->additionalModules = 5;
$options->dotColors = [
111 => '#e2453c',
222 => '#e07e39',
333 => '#e5d667',
444 => '#51b95b',
555 => '#1e72b7',
666 => '#6f5ba7',
];
// generate the CSS for the several colored layers
$layerColors = '';
foreach($options->dotColors as $layer => $color){
$layerColors .= sprintf("\n\t\t.qr-%s{ fill: %s; }", $layer, $color);
}
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient
// please forgive me for I have committed colorful crimes
$options->svgDefs = '
<linearGradient id="blurple" x1="100%" y2="100%">
<stop stop-color="#D70071" offset="0"/>
<stop stop-color="#9C4E97" offset="0.5"/>
<stop stop-color="#0035A9" offset="1"/>
</linearGradient>
<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[
.light{ fill: #dedede; }
.dark{ fill: url(#rainbow); }
.logo{ fill: url(#blurple); }
#circle{ fill: none; stroke: url(#blurple); }
'.$layerColors.'
]]></style>';
// custom SVG logo options
$options->svgLogo = __DIR__.'/github.svg'; // logo from: https://github.com/simple-icons/simple-icons
$options->svgLogoScale = 0.2;
$options->svgLogoCssClass = 'logo';
// common QRCode options
$options->version = 7;
$options->eccLevel = EccLevel::H;
$options->addQuietzone = false; // we're not adding a quiet zone, this is done internally in our own module
$options->outputBase64 = false; // avoid base64 URI output for the example
$options->outputType = QROutputInterface::CUSTOM;
$options->outputInterface = RoundQuietzoneSVGoutput::class; // load our own output class
$options->drawLightModules = false; // set to true to add the light modules
// common SVG options
#$options->connectPaths = true; // this has been set to "always on" internally
$options->excludeFromConnect = [
QRMatrix::M_FINDER_DARK,
QRMatrix::M_FINDER_DOT,
QRMatrix::M_ALIGNMENT_DARK,
QRMatrix::M_QUIETZONE_DARK,
];
$options->drawCircularModules = true;
$options->circleRadius = 0.4;
$options->keepAsSquare = [
QRMatrix::M_FINDER_DARK,
QRMatrix::M_FINDER_DOT,
QRMatrix::M_ALIGNMENT_DARK,
];
$out = (new QRCode($options))->render('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
if(php_sapi_name() !== 'cli'){
header('Content-type: image/svg+xml');
if(extension_loaded('zlib')){
header('Vary: Accept-Encoding');
header('Content-Encoding: gzip');
$out = gzencode($out, 9);
}
}
echo $out;
exit;