diff --git a/src/Decoder/Binarizer.php b/src/Decoder/Binarizer.php index c83322dfe..a148703e3 100644 --- a/src/Decoder/Binarizer.php +++ b/src/Decoder/Binarizer.php @@ -45,12 +45,12 @@ final class Binarizer{ private const LUMINANCE_SHIFT = 3; private const LUMINANCE_BUCKETS = 32; - private LuminanceSource $source; + private LuminanceSourceInterface $source; /** * */ - public function __construct(LuminanceSource $source){ + public function __construct(LuminanceSourceInterface $source){ $this->source = $source; } diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index 1a31e724b..0f7ba50d8 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -31,12 +31,12 @@ final class Decoder{ *
Decodes a QR Code represented as a {@link \chillerlan\QRCode\Decoder\BitMatrix}. * A 1 or "true" is taken to mean a black module.
* - * @param \chillerlan\QRCode\Decoder\LuminanceSource $source + * @param \chillerlan\QRCode\Decoder\LuminanceSourceInterface $source * * @return \chillerlan\QRCode\Decoder\DecoderResult text and bytes encoded within the QR Code * @throws \Exception if the QR Code cannot be decoded */ - public function decode(LuminanceSource $source):DecoderResult{ + public function decode(LuminanceSourceInterface $source):DecoderResult{ $matrix = (new Binarizer($source))->getBlackMatrix(); $bitMatrix = (new Detector($matrix))->detect(); diff --git a/src/Decoder/GDLuminanceSource.php b/src/Decoder/GDLuminanceSource.php index ec53da3a2..61939f19a 100644 --- a/src/Decoder/GDLuminanceSource.php +++ b/src/Decoder/GDLuminanceSource.php @@ -14,14 +14,15 @@ namespace chillerlan\QRCode\Decoder; use InvalidArgumentException; -use function get_resource_type, imagecolorat, imagecolorsforindex, imagesx, imagesy, is_resource; +use function file_get_contents, get_resource_type, imagecolorat, imagecolorsforindex, + imagecreatefromstring, imagesx, imagesy, is_resource; use const PHP_MAJOR_VERSION; /** * This class is used to help decode images from files which arrive as GD Resource * It does not support rotation. */ -final class GDLuminanceSource extends LuminanceSource{ +final class GDLuminanceSource extends LuminanceSourceAbstract{ /** * @var resource|\GdImage @@ -40,7 +41,7 @@ final class GDLuminanceSource extends LuminanceSource{ /** @noinspection PhpFullyQualifiedNameUsageInspection */ if( (PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) - || (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd')) + || (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd') ){ throw new InvalidArgumentException('Invalid GD image source.'); } @@ -66,4 +67,16 @@ final class GDLuminanceSource extends LuminanceSource{ } } + /** @inheritDoc */ + public static function fromFile(string $path):self{ + $path = self::checkFile($path); + + return new self(imagecreatefromstring(file_get_contents($path))); + } + + /** @inheritDoc */ + public static function fromBlob(string $blob):self{ + return new self(imagecreatefromstring($blob)); + } + } diff --git a/src/Decoder/IMagickLuminanceSource.php b/src/Decoder/IMagickLuminanceSource.php index d0106eeb9..c0356ed47 100644 --- a/src/Decoder/IMagickLuminanceSource.php +++ b/src/Decoder/IMagickLuminanceSource.php @@ -20,7 +20,7 @@ use function count; * This class is used to help decode images from files which arrive as Imagick Resource * It does not support rotation. */ -final class IMagickLuminanceSource extends LuminanceSource{ +final class IMagickLuminanceSource extends LuminanceSourceAbstract{ private Imagick $imagick; @@ -53,4 +53,19 @@ final class IMagickLuminanceSource extends LuminanceSource{ } } + /** @inheritDoc */ + public static function fromFile(string $path):self{ + $path = self::checkFile($path); + + return new self(new Imagick($path)); + } + + /** @inheritDoc */ + public static function fromBlob(string $blob):self{ + $im = new Imagick; + $im->readImageBlob($blob); + + return new self($im); + } + } diff --git a/src/Decoder/LuminanceSource.php b/src/Decoder/LuminanceSourceAbstract.php similarity index 61% rename from src/Decoder/LuminanceSource.php rename to src/Decoder/LuminanceSourceAbstract.php index 8dc8fb513..6ce4f71be 100644 --- a/src/Decoder/LuminanceSource.php +++ b/src/Decoder/LuminanceSourceAbstract.php @@ -1,6 +1,6 @@ luminances = []; - // @todo: grayscale? - //$this->luminances = $this->grayScaleToBitmap($this->grayscale()); } - /** - * Fetches luminance data for the underlying bitmap. Values should be fetched using: - * {@code int luminance = array[y * width + x] & 0xff} - * - * @return array A row-major 2D array of luminance values. Do not use result.length as it may be - * larger than width * height bytes on some platforms. Do not modify the contents - * of the result. - */ + /** @inheritDoc */ public function getMatrix():array{ return $this->luminances; } - /** - * @return int The width of the bitmap. - */ + /** @inheritDoc */ public function getWidth():int{ return $this->width; } - /** - * @return int The height of the bitmap. - */ + /** @inheritDoc */ public function getHeight():int{ return $this->height; } - /** - * Fetches one row of luminance data from the underlying platform's bitmap. Values range from - * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have - * to bitwise and with 0xff for each value. It is preferable for implementations of this method - * to only fetch this row rather than the whole image, since no 2D Readers may be installed and - * getMatrix() may never be called. - * - * @param int $y The row to fetch, which must be in [0,getHeight()) - * - * @return array An array containing the luminance data. - */ + /** @inheritDoc */ public function getRow(int $y):array{ if($y < 0 || $y >= $this->getHeight()){ @@ -93,11 +70,7 @@ abstract class LuminanceSource{ } /** - * @param int $r - * @param int $g - * @param int $b * - * @return void */ protected function setLuminancePixel(int $r, int $g, int $b):void{ $this->luminances[] = $r === $g && $g === $b @@ -107,4 +80,23 @@ abstract class LuminanceSource{ : ($r + 2 * $g + $b) / 4; // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; } + /** + * + */ + protected static function checkFile(string $path):string{ + $path = trim($path); + + if(!file_exists($path) || !is_file($path) || !is_readable($path)){ + throw new InvalidArgumentException('invalid file: '.$path); + } + + $realpath = realpath($path); + + if($realpath === false){ + throw new InvalidArgumentException('unable to resolve path: '.$path); + } + + return $realpath; + } + } diff --git a/src/Decoder/LuminanceSourceInterface.php b/src/Decoder/LuminanceSourceInterface.php new file mode 100644 index 000000000..50aa557f7 --- /dev/null +++ b/src/Decoder/LuminanceSourceInterface.php @@ -0,0 +1,54 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Decoder; + +/** + */ +interface LuminanceSourceInterface{ + + /** + * Fetches luminance data for the underlying bitmap. Values should be fetched using: + * {@code int luminance = array[y * width + x] & 0xff} + * + * @return array A row-major 2D array of luminance values. Do not use result.length as it may be + * larger than width * height bytes on some platforms. Do not modify the contents + * of the result. + */ + public function getMatrix():array; + + /** + * @return int The width of the bitmap. + */ + public function getWidth():int; + + /** + * @return int The height of the bitmap. + */ + public function getHeight():int; + + /** + * Fetches one row of luminance data from the underlying platform's bitmap. Values range from + * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have + * to bitwise and with 0xff for each value. It is preferable for implementations of this method + * to only fetch this row rather than the whole image, since no 2D Readers may be installed and + * getMatrix() may never be called. + * + * @param int $y The row to fetch, which must be in [0,getHeight()) + * + * @return array An array containing the luminance data. + */ + public function getRow(int $y):array; + + + public static function fromFile(string $path):self; + public static function fromBlob(string $blob):self; + +} diff --git a/src/QRCode.php b/src/QRCode.php index f5e023d08..5b71f063d 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Common\{ECICharset, MaskPattern, MaskPatternTester, Mode}; use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix}; +use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; use function class_exists, class_implements, in_array, mb_convert_encoding, mb_detect_encoding; @@ -80,15 +81,6 @@ class QRCode{ ], ]; - /** - * A collection of one or more data segments of [classname, data] to write - * - * @see \chillerlan\QRCode\Data\QRDataModeInterface - * - * @var \chillerlan\QRCode\Data\QRDataModeInterface[] - */ - protected array $dataSegments = []; - /** * The settings container * @@ -101,13 +93,32 @@ class QRCode{ */ protected QRData $dataInterface; + /** + * A collection of one or more data segments of [classname, data] to write + * + * @see \chillerlan\QRCode\Data\QRDataModeInterface + * + * @var \chillerlan\QRCode\Data\QRDataModeInterface[] + */ + protected array $dataSegments = []; + + /** + * The FQCN of the luminance sporce class to use in the reader (GD or Imagick) + * + * @see \chillerlan\QRCode\Decoder\LuminanceSourceInterface + */ + private string $luminanceSourceClass; + /** * QRCode constructor. * * Sets the options instance */ public function __construct(SettingsContainerInterface $options = null){ - $this->options = $options ?? new QROptions; + $this->options = $options ?? new QROptions; + $this->luminanceSourceClass = $this->options->useImagickIfAvailable + ? IMagickLuminanceSource::class + : GDLuminanceSource::class; } /** @@ -305,4 +316,29 @@ class QRCode{ throw new QRCodeException('unable to add ECI segment'); } + /** + * Clears the data segments array + */ + public function clearSegments():self{ + $this->dataSegments = []; + + return $this; + } + + /** + * Reads a QR Code from a given file + */ + public function readFromFile(string $path):DecoderResult{ + /** @noinspection PhpUndefinedMethodInspection */ + return (new Decoder)->decode($this->luminanceSourceClass::fromFile($path)); + } + + /** + * Reads a QR Code from the given data blob + */ + public function readFromBlob(string $blob):DecoderResult{ + /** @noinspection PhpUndefinedMethodInspection */ + return (new Decoder)->decode($this->luminanceSourceClass::fromBlob($blob)); + } + } diff --git a/src/QRCodeReader.php b/src/QRCodeReader.php deleted file mode 100644 index 41e5f323f..000000000 --- a/src/QRCodeReader.php +++ /dev/null @@ -1,99 +0,0 @@ - - * @copyright 2021 Smiley - * @license Apache-2.0 - * - * @noinspection PhpComposerExtensionStubsInspection - */ - -namespace chillerlan\QRCode; - -use chillerlan\Settings\SettingsContainerInterface; -use Imagick, InvalidArgumentException; -use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource}; -use function extension_loaded, file_exists, file_get_contents, imagecreatefromstring, is_file, is_readable; - -/** - * - */ -final class QRCodeReader{ - - /** - * The settings container - * - * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface - */ - private SettingsContainerInterface $options; - - /** - * - */ - public function __construct(SettingsContainerInterface $options = null){ - $this->options = $options ?? new QROptions; - } - - /** - * @param \Imagick|\GdImage|resource $im - * - * @return \chillerlan\QRCode\Decoder\DecoderResult - */ - private function decode($im):DecoderResult{ - - $source = $this->options->useImagickIfAvailable - ? new IMagickLuminanceSource($im) - : new GDLuminanceSource($im); - - return (new Decoder)->decode($source); - } - - /** - * @param string $imgFilePath - * - * @return \chillerlan\QRCode\Decoder\DecoderResult - */ - public function readFile(string $imgFilePath):DecoderResult{ - - if(!file_exists($imgFilePath) || !is_file($imgFilePath) || !is_readable($imgFilePath)){ - throw new InvalidArgumentException('invalid file: '.$imgFilePath); - } - - $im = $this->options->useImagickIfAvailable - ? new Imagick($imgFilePath) - : imagecreatefromstring(file_get_contents($imgFilePath)); - - return $this->decode($im); - } - - /** - * @param string $imgBlob - * - * @return \chillerlan\QRCode\Decoder\DecoderResult - */ - public function readBlob(string $imgBlob):DecoderResult{ - - if($this->options->useImagickIfAvailable){ - $im = new Imagick; - $im->readImageBlob($imgBlob); - } - else{ - $im = imagecreatefromstring($imgBlob); - } - - return $this->decode($im); - } - - /** - * @param \Imagick|\GdImage|resource $imgSource - * - * @return \chillerlan\QRCode\Decoder\DecoderResult - */ - public function readResource($imgSource):DecoderResult{ - return $this->decode($imgSource); - } - -} diff --git a/tests/QRCodeReaderTest.php b/tests/QRCodeReaderTest.php index d5230b2e4..d08e6d19c 100644 --- a/tests/QRCodeReaderTest.php +++ b/tests/QRCodeReaderTest.php @@ -15,7 +15,7 @@ namespace chillerlan\QRCodeTest; use chillerlan\Settings\SettingsContainerInterface; use Exception; use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; -use chillerlan\QRCode\{QRCode, QROptions, QRCodeReader}; +use chillerlan\QRCode\{QRCode, QROptions}; use PHPUnit\Framework\TestCase; use function extension_loaded, range, str_repeat, substr; @@ -64,9 +64,9 @@ class QRCodeReaderTest extends TestCase{ public function testReaderGD(string $img, string $expected):void{ $this->options->useImagickIfAvailable = false; - $reader = new QRCodeReader($this->options); + $reader = new QRCode($this->options); - $this::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); + $this::assertSame($expected, (string)$reader->readFromFile(__DIR__.'/qrcodes/'.$img)); } /** @@ -80,9 +80,9 @@ class QRCodeReaderTest extends TestCase{ $this->options->useImagickIfAvailable = true; - $reader = new QRCodeReader($this->options); + $reader = new QRCode($this->options); - $this::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); + $this::assertSame($expected, (string)$reader->readFromFile(__DIR__.'/qrcodes/'.$img)); } public function dataTestProvider():array{ @@ -120,8 +120,9 @@ class QRCodeReaderTest extends TestCase{ $this->options->useImagickIfAvailable = true; try{ - $imagedata = (new QRCode($this->options))->render($expected); - $result = (new QRCodeReader($this->options))->readBlob($imagedata); + $qrcode = new QRCode($this->options); + $imagedata = $qrcode->render($expected); + $result = $qrcode->readFromBlob($imagedata); } catch(Exception $e){ $this::markTestSkipped($version.$ecc.': '.$e->getMessage());