From c8fcb2d0435279421fa03f6a3f217ccae9bddd59 Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 4 Jun 2020 18:47:37 +0200 Subject: [PATCH] :sparkles: added PDF support (#49) --- README.md | 8 ++- composer.json | 8 +-- examples/fpdf.php | 46 ++++++++++++++++ src/Output/QRFpdf.php | 102 ++++++++++++++++++++++++++++++++++++ src/QRCode.php | 6 ++- src/QROptions.php | 2 + src/QROptionsTrait.php | 24 ++++++++- tests/Output/QRFpdfTest.php | 74 ++++++++++++++++++++++++++ 8 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 examples/fpdf.php create mode 100644 src/Output/QRFpdf.php create mode 100644 tests/Output/QRFpdfTest.php diff --git a/README.md b/README.md index 20967477f..d8952136b 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,11 @@ namespaced, cleaned up, improved and other stuff. ### Requirements - PHP 7.2+ - - `ext-gd`, `ext-json`, `ext-mbstring` - - optional `ext-imagick` + - `ext-mbstring` + - optional: + - `ext-json`, `ext-gd` + - `ext-imagick` with [ImageMagick](https://imagemagick.org) installed + - [`setasign/fpdf`](https://github.com/setasign/fpdf) for the PDF output module ### Installation **requires [composer](https://getcomposer.org)** @@ -289,6 +292,7 @@ name | description `OUTPUT_IMAGE_PNG`, `OUTPUT_IMAGE_JPG`, `OUTPUT_IMAGE_GIF` | `QROptions::$outputType` image `OUTPUT_STRING_JSON`, `OUTPUT_STRING_TEXT` | `QROptions::$outputType` string `OUTPUT_IMAGICK` | `QROptions::$outputType` ImageMagick +`OUTPUT_FPDF` | `QROptions::$outputType` PDF, using [FPDF](https://github.com/setasign/fpdf) `OUTPUT_CUSTOM` | `QROptions::$outputType`, requires `QROptions::$outputInterface` `ECC_L`, `ECC_M`, `ECC_Q`, `ECC_H`, | ECC-Level: 7%, 15%, 25%, 30% in `QROptions::$eccLevel` `DATA_NUMBER`, `DATA_ALPHANUM`, `DATA_BYTE`, `DATA_KANJI` | `QRDataInterface::$datamode` diff --git a/composer.json b/composer.json index 414d05c39..c2299a350 100644 --- a/composer.json +++ b/composer.json @@ -25,16 +25,16 @@ ], "require": { "php": "^7.2", - "ext-gd": "*", - "ext-json": "*", "ext-mbstring": "*", "chillerlan/php-settings-container": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^8.5", + "setasign/fpdf": "^1.8.2" }, "suggest": { - "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps." + "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "setasign/fpdf": "Required to use the QR FPDF output." }, "autoload": { "psr-4": { diff --git a/examples/fpdf.php b/examples/fpdf.php new file mode 100644 index 000000000..02207b9df --- /dev/null +++ b/examples/fpdf.php @@ -0,0 +1,46 @@ + 7, + 'outputType' => QRCode::OUTPUT_FPDF, + 'eccLevel' => QRCode::ECC_L, + 'scale' => 5, + 'moduleValues' => [ + // finder + 1536 => [0, 63, 255], // dark (true) + 6 => [255, 255, 255], // light (false), white is the transparency color and is enabled by default + // alignment + 2560 => [255, 0, 255], + 10 => [255, 255, 255], + // timing + 3072 => [255, 0, 0], + 12 => [255, 255, 255], + // format + 3584 => [67, 191, 84], + 14 => [255, 255, 255], + // version + 4096 => [62, 174, 190], + 16 => [255, 255, 255], + // data + 1024 => [0, 0, 0], + 4 => [255, 255, 255], + // darkmodule + 512 => [0, 0, 0], + // separator + 8 => [255, 255, 255], + // quietzone + 18 => [255, 255, 255], + ], +]); + +\header('Content-type: application/pdf'); + +echo (new QRCode($options))->render($data); diff --git a/src/Output/QRFpdf.php b/src/Output/QRFpdf.php new file mode 100644 index 000000000..4cd972148 --- /dev/null +++ b/src/Output/QRFpdf.php @@ -0,0 +1,102 @@ + $defaultValue){ + $v = $this->options->moduleValues[$M_TYPE] ?? null; + + if(!is_array($v) || count($v) < 3){ + $this->moduleValues[$M_TYPE] = $defaultValue + ? [0, 0, 0] + : [255, 255, 255]; + } + else{ + $this->moduleValues[$M_TYPE] = array_values($v); + } + + } + + } + + /** + * @inheritDoc + */ + public function dump(string $file = null):string{ + $file = $file ?? $this->options->cachefile; + + $fpdf = new FPDF('P', $this->options->fpdfMeasureUnit, [$this->length, $this->length]); + $fpdf->AddPage(); + + $prevColor = null; + + foreach($this->matrix->matrix() as $y => $row){ + + foreach($row as $x => $M_TYPE){ + /** @var int $M_TYPE */ + $color = $this->moduleValues[$M_TYPE]; + + if($prevColor === null || $prevColor !== $color){ + $fpdf->SetFillColor(...$color); + $prevColor = $color; + } + + $fpdf->Rect($x * $this->scale, $y * $this->scale, 1 * $this->scale, 1 * $this->scale, 'F'); + } + + } + + $pdfData = $fpdf->Output('S'); + + if($file !== null){ + $this->saveToFile($pdfData, $file); + } + + return $pdfData; + } + +} diff --git a/src/QRCode.php b/src/QRCode.php index d9b471df0..a71740420 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -16,7 +16,7 @@ use chillerlan\QRCode\Data\{ MaskPatternTester, QRCodeDataException, QRDataInterface, QRMatrix }; use chillerlan\QRCode\Output\{ - QRCodeOutputException, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString + QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString }; use chillerlan\Settings\SettingsContainerInterface; @@ -42,6 +42,7 @@ class QRCode{ public const OUTPUT_STRING_JSON = 'json'; public const OUTPUT_STRING_TEXT = 'text'; public const OUTPUT_IMAGICK = 'imagick'; + public const OUTPUT_FPDF = 'fpdf'; public const OUTPUT_CUSTOM = 'custom'; public const VERSION_AUTO = -1; @@ -88,6 +89,9 @@ class QRCode{ QRImagick::class => [ self::OUTPUT_IMAGICK, ], + QRFpdf::class => [ + self::OUTPUT_FPDF + ] ]; /** diff --git a/src/QROptions.php b/src/QROptions.php index 9a8b03d9c..ab4e938f5 100644 --- a/src/QROptions.php +++ b/src/QROptions.php @@ -51,6 +51,8 @@ use chillerlan\Settings\SettingsContainerAbstract; * @property string $imagickFormat * @property string $imagickBG * + * @property string $fpdfMeasureUnit + * * @property array $moduleValues */ class QROptions extends SettingsContainerAbstract{ diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index d653144a1..a4a7acdc6 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode; -use function array_values, count, is_array, is_numeric, max, min, sprintf; +use function array_values, count, in_array, is_array, is_numeric, max, min, sprintf, strtolower; trait QROptionsTrait{ @@ -241,6 +241,13 @@ trait QROptionsTrait{ */ protected $imagickBG = null; + /** + * Measurement unit for FPDF output: pt, mm, cm, in (defaults to "pt") + * + * @see \FPDF::__construct() + */ + protected $fpdfMeasureUnit = 'pt'; + /** * Module values map * @@ -367,4 +374,19 @@ trait QROptionsTrait{ } + /** + * sets the FPDF measurement unit + * + * @codeCoverageIgnore + */ + protected function set_fpdfMeasureUnit(string $unit):void{ + $unit = strtolower($unit); + + if(in_array($unit, ['cm', 'in', 'mm', 'pt'], true)){ + $this->fpdfMeasureUnit = $unit; + } + + // @todo throw or ignore silently? + } + } diff --git a/tests/Output/QRFpdfTest.php b/tests/Output/QRFpdfTest.php new file mode 100644 index 000000000..921bd78e2 --- /dev/null +++ b/tests/Output/QRFpdfTest.php @@ -0,0 +1,74 @@ + + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use FPDF; +use chillerlan\QRCode\Output\{QRFpdf, QROutputInterface}; +use chillerlan\QRCode\{QRCode, QROptions}; + +use function class_exists, substr; + +/** + * Tests the QRFpdf output module + */ +class QRFpdfTest extends QROutputTestAbstract{ + + protected $FQCN = QRFpdf::class; + + /** + * @inheritDoc + * @internal + */ + public function setUp():void{ + + if(!class_exists(FPDF::class)){ + $this->markTestSkipped('FPDF not available'); + return; + } + + parent::setUp(); + } + + /** + * @inheritDoc + */ + public function testSetModuleValues():void{ + + $this->options->moduleValues = [ + // data + 1024 => [0, 0, 0], + 4 => [255, 255, 255], + ]; + + $this->outputInterface->dump(); + + $this::assertTrue(true); // tricking the code coverage + } + + /** + * @inheritDoc + */ + public function testRenderImage():void{ + $type = QRCode::OUTPUT_FPDF; + + $this->options->outputType = $type; + $this->outputInterface->dump($this::cachefile.$type); + + // substr() to avoid CreationDate + $expected = substr(file_get_contents($this::cachefile.$type), 0, 2000); + $actual = substr($this->outputInterface->dump(), 0, 2000); + + $this::assertSame($expected, $actual); + } + +}