diff --git a/src/Output/QRImage.php b/src/Output/QRImage.php index c4de43939..fa831f683 100644 --- a/src/Output/QRImage.php +++ b/src/Output/QRImage.php @@ -12,14 +12,18 @@ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\Data\QRMatrix; -use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\{QRCode, Data\QRMatrix}; /** * Converts the matrix into images, raw or base64 output */ class QRImage extends QROutputAbstract{ + const transparencyTypes = [ + QRCode::OUTPUT_IMAGE_PNG, + QRCode::OUTPUT_IMAGE_GIF, + ]; + protected $moduleValues = [ // light QRMatrix::M_DATA => [255, 255, 255], @@ -42,40 +46,51 @@ class QRImage extends QROutputAbstract{ QRMatrix::M_TEST << 8 => [0, 0, 0], ]; + /** + * @see imagecreatetruecolor() + * @var resource + */ + protected $image; + + /** + * @var int + */ + protected $scale; + + /** + * @var int + */ + protected $length; + + /** + * @see imagecolorallocate() + * @var int + */ + protected $background; + /** * @return string + * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ public function dump():string{ - $scale = $this->options->scale; - $length = $this->moduleCount * $scale; - $image = imagecreatetruecolor($length, $length); - $background = imagecolorallocate($image, ...$this->options->imageTransparencyBG); - $moduleValues = is_array($this->options->moduleValues[QRMatrix::M_DATA]) + + if($this->options->cachefile !== null && !is_writable(dirname($this->options->cachefile))){ + throw new QRCodeOutputException('Could not write data to cache file: '.$this->options->cachefile); + } + + $this->setImage(); + + $moduleValues = is_array($this->options->moduleValues[$this->matrix::M_DATA]) ? $this->options->moduleValues // @codeCoverageIgnore : $this->moduleValues; - if((bool)$this->options->imageTransparent && in_array($this->options->outputType, [QRCode::OUTPUT_IMAGE_PNG, QRCode::OUTPUT_IMAGE_GIF,], true)){ - imagecolortransparent($image, $background); - } - - imagefilledrectangle($image, 0, 0, $length, $length, $background); - foreach($this->matrix->matrix() as $y => $row){ foreach($row as $x => $pixel){ - $color = imagecolorallocate($image, ...$moduleValues[$pixel]); - - imagefilledrectangle($image, $x * $scale, $y * $scale, ($x + 1) * $scale - 1, ($y + 1) * $scale - 1, $color); + $this->setPixel($x, $y, imagecolorallocate($this->image, ...$moduleValues[$pixel])); } } - ob_start(); - - call_user_func_array([$this, $this->options->outputType ?? QRCode::OUTPUT_IMAGE_PNG], [&$image]); - - $imageData = ob_get_contents(); - imagedestroy($image); - - ob_end_clean(); + $imageData = $this->dumpImage(); if((bool)$this->options->imageBase64){ $imageData = 'data:image/'.$this->options->outputType.';base64,'.base64_encode($imageData); @@ -85,34 +100,90 @@ class QRImage extends QROutputAbstract{ } /** - * @param $image + * @return void */ - protected function png(&$image){ + protected function setImage(){ + $this->scale = $this->options->scale; + $this->length = $this->moduleCount * $this->scale; + $this->image = imagecreatetruecolor($this->length, $this->length); + $this->background = imagecolorallocate($this->image, ...$this->options->imageTransparencyBG); + + if((bool)$this->options->imageTransparent && in_array($this->options->outputType, $this::transparencyTypes, true)){ + imagecolortransparent($this->image, $this->background); + } + + imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background); + } + + /** + * @param $x + * @param $y + * @param $color + * @return void + */ + protected function setPixel($x, $y, $color){ + imagefilledrectangle( + $this->image, + $x * $this->scale, + $y * $this->scale, + ($x + 1) * $this->scale - 1, + ($y + 1) * $this->scale - 1, + $color + ); + } + + /** + * @return string + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + protected function dumpImage():string { + ob_start(); + + try{ + call_user_func([$this, $this->options->outputType ?? QRCode::OUTPUT_IMAGE_PNG]); + } + // not going to cover edge cases + // @codeCoverageIgnoreStart + catch(\Exception $e){ + throw new QRCodeOutputException($e->getMessage()); + } + // @codeCoverageIgnoreEnd + + $imageData = ob_get_contents(); + imagedestroy($this->image); + + ob_end_clean(); + + return $imageData; + } + + /** + * @return void + */ + protected function png(){ imagepng( - $image, + $this->image, $this->options->cachefile, in_array($this->options->pngCompression, range(-1, 9), true) ? $this->options->pngCompression : -1 ); - } /** * Jiff - like... JitHub! - * - * @param $image + * @return void */ - protected function gif(&$image){ - imagegif($image, $this->options->cachefile); + protected function gif(){ + imagegif($this->image, $this->options->cachefile); } /** - * @param $image + * @return void */ - protected function jpg(&$image){ + protected function jpg(){ imagejpeg( - $image, + $this->image, $this->options->cachefile, in_array($this->options->jpegQuality, range(0, 100), true) ? $this->options->jpegQuality diff --git a/src/Output/QRMarkup.php b/src/Output/QRMarkup.php index d32944b33..7b191310a 100644 --- a/src/Output/QRMarkup.php +++ b/src/Output/QRMarkup.php @@ -23,13 +23,20 @@ class QRMarkup extends QROutputAbstract{ * @return string */ public function dump(){ - switch($this->options->outputType){ - case QRCode::OUTPUT_MARKUP_HTML: - return $this->toHTML(); - case QRCode::OUTPUT_MARKUP_SVG : - default: - return $this->toSVG(); + + if($this->options->cachefile !== null && !is_writable(dirname($this->options->cachefile))){ + throw new QRCodeOutputException('Could not write data to cache file: '.$this->options->cachefile); } + + $data = $this->options->outputType === QRCode::OUTPUT_MARKUP_HTML + ? $this->toHTML() + : $this->toSVG(); + + if($this->options->cachefile !== null){ + $this->saveToFile($data); + } + + return $data; } /** @@ -45,14 +52,11 @@ class QRMarkup extends QROutputAbstract{ $html .= ''; } - $html .= ''; - $html .= $this->options->eol; + $html .= ''.$this->options->eol; } if($this->options->cachefile){ - $html = '
'.$this->options->eol.$html.''; - - return $this->saveToFile($html); + return ''.$this->options->eol.$html.''; } return $html; @@ -117,9 +121,7 @@ class QRMarkup extends QROutputAbstract{ // if saving to file, append the correct headers if($this->options->cachefile){ - $svg = ''.$this->options->eol.$svg; - - return $this->saveToFile($svg); + return ''.$this->options->eol.$svg; } return $svg; diff --git a/src/Output/QROutputAbstract.php b/src/Output/QROutputAbstract.php index fd7bfd35a..498b4bac6 100644 --- a/src/Output/QROutputAbstract.php +++ b/src/Output/QROutputAbstract.php @@ -12,9 +12,7 @@ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\{ - Data\QRMatrix, QROptions -}; +use chillerlan\QRCode\{Data\QRMatrix, QROptions}; /** * @@ -41,36 +39,22 @@ abstract class QROutputAbstract implements QROutputInterface{ * * @param \chillerlan\QRCode\QROptions $options * @param \chillerlan\QRCode\Data\QRMatrix $matrix - * - * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ public function __construct(QROptions $options, QRMatrix $matrix){ - $this->options = $options; - - $this->moduleCount = $matrix->size(); - - if($this->moduleCount < 21){ // minimum QR modules @todo: quet zone - throw new QRCodeOutputException('Invalid matrix!'); - } - - $this->matrix = $matrix; + $this->options = $options; + $this->matrix = $matrix; + $this->moduleCount = $this->matrix->size(); } /** - * @param string $data + * @see file_put_contents() * - * @return bool - * @throws \chillerlan\QRCode\Output\QRCodeOutputException + * @param string $data + + * @return bool|int */ - protected function saveToFile(string $data):bool { - - try{ - return (bool)file_put_contents($this->options->cachefile, $data); - } - catch(\Exception $e){ - throw new QRCodeOutputException('Could not write data to cache file: '.$e->getMessage()); - } - + protected function saveToFile(string $data) { + return file_put_contents($this->options->cachefile, $data); } } diff --git a/src/Output/QRString.php b/src/Output/QRString.php index 65a6e3474..8cb367a24 100644 --- a/src/Output/QRString.php +++ b/src/Output/QRString.php @@ -24,39 +24,48 @@ class QRString extends QROutputAbstract{ */ public function dump():string{ - switch($this->options->outputType){ - case QRCode::OUTPUT_STRING_TEXT: - return $this->toString(); - case QRCode::OUTPUT_STRING_JSON: - default: - return json_encode($this->matrix->matrix()); + $data = $this->options->outputType === QRCode::OUTPUT_STRING_JSON + ? json_encode($this->matrix->matrix()) + : $this->toString(); + + if($this->options->cachefile !== null){ + + if(!is_writable(dirname($this->options->cachefile))){ + throw new QRCodeOutputException('Could not write data to cache file: '.$this->options->cachefile); + } + + $this->saveToFile($data); } + return $data; } /** * @return string */ protected function toString():string{ - $str = ''; + $str = []; foreach($this->matrix->matrix() as $row){ + $r = []; + foreach($row as $col){ $col = $this->options->moduleValues[$col]; - + // fallback if(is_bool($col) || !is_string($col)){ - $col = $col ? $this->options->textDark : $this->options->textLight; + $col = $col + ? $this->options->textDark + : $this->options->textLight; } - - $str .= $col; + $r[] = $col; } - $str .= $this->options->eol; + $str[] = implode('', $r); } - return $str; + return implode($this->options->eol, $str); } } diff --git a/src/QRCode.php b/src/QRCode.php index bdd631c6f..5e0b22aff 100644 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -321,8 +321,6 @@ class QRCode{ /** * a dummy * - * @codeCoverageIgnore - * * @param $data * * @return bool diff --git a/tests/Output/QRImageTest.php b/tests/Output/QRImageTest.php new file mode 100644 index 000000000..1ef0a9f48 --- /dev/null +++ b/tests/Output/QRImageTest.php @@ -0,0 +1,52 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use chillerlan\QRCode\Output\QRImage; +use chillerlan\QRCode\QRCode; + +class QRImageTest extends QROutputTestAbstract{ + + protected $FQCN = QRImage::class; + + public function types(){ + return [ + [QRCode::OUTPUT_IMAGE_PNG], + [QRCode::OUTPUT_IMAGE_GIF], + [QRCode::OUTPUT_IMAGE_JPG], + ]; + } + + /** + * @dataProvider types + * @param $type + */ + public function testImageOutput($type){ + $this->options->outputType = $type; + $this->options->cachefile = $this::cachefile.$type; + $this->setOutputInterface(); + $this->outputInterface->dump(); + + $this->options->cachefile = null; + $this->options->imageBase64 = false; + $this->setOutputInterface(); + $img = $this->outputInterface->dump(); + + if($type === QRCode::OUTPUT_IMAGE_JPG){ // jpeg encoding may cause different results + $this->markAsRisky(); + } + + $this->assertSame($img, file_get_contents($this::cachefile.$type)); + } + +} diff --git a/tests/Output/QRMarkupTest.php b/tests/Output/QRMarkupTest.php new file mode 100644 index 000000000..caa646679 --- /dev/null +++ b/tests/Output/QRMarkupTest.php @@ -0,0 +1,64 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use chillerlan\QRCode\Output\QRMarkup; +use chillerlan\QRCode\QRCode; + +class QRMarkupTest extends QROutputTestAbstract{ + + protected $FQCN = QRMarkup::class; + + public function types(){ + return [ + [QRCode::OUTPUT_MARKUP_HTML], + [QRCode::OUTPUT_MARKUP_SVG], + ]; + } + + /** + * @dataProvider types + * @param $type + */ + public function testMarkupOutputFile($type){ + $this->options->outputType = $type; + $this->options->cachefile = $this::cachefile.$type; + $this->setOutputInterface(); + $data = $this->outputInterface->dump(); + + $this->assertSame($data, file_get_contents($this->options->cachefile)); + } + + /** + * @dataProvider types + * @param $type + */ + public function testMarkupOutput($type){ + $this->options->outputType = $type; + $this->setOutputInterface(); + + $expected = explode($this->options->eol, file_get_contents($this::cachefile.$type)); + // cut off the doctype & head + array_shift($expected); + + if($type === QRCode::OUTPUT_MARKUP_HTML){ + // cut off the