Jelajahi Sumber

:bath: SVG output rework

codemasher 4 tahun lalu
induk
melakukan
6006df8c9a

+ 33 - 49
examples/svg.php

@@ -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 - 0
src/Data/QRMatrix.php

@@ -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)
 	 *

+ 100 - 51
src/Output/QRMarkup.php

@@ -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 = $this->svgHeader();
 
-		$svg = sprintf($this->svgHeader, $this->options->cssClass, $this->options->svgViewBoxSize ?? $this->moduleCount)
-		       .$this->options->eol
-		       .'<defs>'.$this->options->svgDefs.'</defs>'
-		       .$this->options->eol;
+		if(!empty($this->options->svgDefs)){
+			$svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
+		}
 
-		foreach($this->moduleValues as $M_TYPE => $value){
-			$path = '';
+		$svg .= $this->svgPaths();
 
-			foreach($matrix as $y => $row){
-				//we'll combine active blocks within a single row as a lightweight compression technique
-				$start = null;
-				$count = 0;
+		// close svg
+		$svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
 
-				foreach($row as $x => $module){
+		// transform to data URI only when not saving to file
+		if($file === null && $this->options->imageBase64){
+			$svg = $this->base64encode($svg, 'image/svg+xml');
+		}
 
-					if($module === $M_TYPE){
-						$count++;
+		return $svg;
+	}
 
-						if($start === null){
-							$start = $x;
-						}
+	/**
+	 * 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
+		);
+	}
 
-						if(isset($row[$x + 1])){
-							continue;
-						}
-					}
+	/**
+	 * returns one or more SVG <path> elements
+	 *
+	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
+	 */
+	protected function svgPaths():string{
+		$paths = [];
 
-					if($count > 0){
-						$len   = $count;
-						$start ??= 0; // avoid type coercion in sprintf() - phan happy
+		// collect the modules for each type
+		foreach($this->matrix->matrix() as $y => $row){
+			foreach($row as $x => $M_TYPE){
 
-						$path .= sprintf('M%s %s h%s v1 h-%sZ ', $start, $y, $len, $len);
+				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;
 
-						// reset count
-						$count = 0;
-						$start = null;
+					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 = [];
 
-			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
-				);
+		// 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);
 		}
 
-		// close svg
-		$svg .= '</svg>'.$this->options->eol;
+		return implode($this->options->eol, $svg);
+	}
 
-		// 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;
+	/**
+	 * 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->imageBase64){
-			$svg = $this->base64encode($svg, 'image/svg+xml');
+		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 $svg;
+		return sprintf('M%1$s %2$s h%3$s v1 h-%4$sZ', $x, $y, 1, 1);
 	}
 
 }

+ 8 - 0
src/QROptions.php

@@ -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
src/QROptionsTrait.php

@@ -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
tests/Output/QRMarkupTest.php

@@ -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 ditekan karena terlalu besar
+ 0 - 0
tests/Output/samples/svg


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini