Просмотр исходного кода

:octocat: integrate reader into QRCode, extract LuminanceSourceInterface

codemasher 4 лет назад
Родитель
Сommit
5e9cf0047e

+ 2 - 2
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;
 	}
 

+ 2 - 2
src/Decoder/Decoder.php

@@ -31,12 +31,12 @@ final class Decoder{
 	 * <p>Decodes a QR Code represented as a {@link \chillerlan\QRCode\Decoder\BitMatrix}.
 	 * A 1 or "true" is taken to mean a black module.</p>
 	 *
-	 * @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();
 

+ 16 - 3
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));
+	}
+
 }

+ 16 - 1
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);
+	}
+
 }

+ 26 - 34
src/Decoder/LuminanceSource.php → src/Decoder/LuminanceSourceAbstract.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * Class LuminanceSource
+ * Class LuminanceSourceAbstract
  *
  * @created      24.01.2021
  * @author       ZXing Authors
@@ -12,7 +12,7 @@
 namespace chillerlan\QRCode\Decoder;
 
 use InvalidArgumentException;
-use function array_slice, array_splice;
+use function array_slice, array_splice, file_exists, is_file, is_readable, realpath;
 
 /**
  * The purpose of this class hierarchy is to abstract different bitmap implementations across
@@ -23,7 +23,7 @@ use function array_slice, array_splice;
  *
  * @author dswitkin@google.com (Daniel Switkin)
  */
-abstract class LuminanceSource{
+abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
 
 	protected array $luminances;
 	protected int   $width;
@@ -38,47 +38,24 @@ abstract class LuminanceSource{
 		// In order to measure pure decoding speed, we convert the entire image to a greyscale array
 		// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app.
 		$this->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;
+	}
+
 }

+ 54 - 0
src/Decoder/LuminanceSourceInterface.php

@@ -0,0 +1,54 @@
+<?php
+/**
+ * Interface LuminanceSourceInterface
+ *
+ * @created      18.11.2021
+ * @author       smiley <smiley@chillerlan.net>
+ * @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;
+
+}

+ 45 - 9
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,6 +81,18 @@ class QRCode{
 		],
 	];
 
+	/**
+	 * The settings container
+	 *
+	 * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
+	 */
+	protected SettingsContainerInterface $options;
+
+	/**
+	 * The selected data interface (Number, AlphaNum, Kanji, Byte)
+	 */
+	protected QRData $dataInterface;
+
 	/**
 	 * A collection of one or more data segments of [classname, data] to write
 	 *
@@ -90,16 +103,11 @@ class QRCode{
 	protected array $dataSegments = [];
 
 	/**
-	 * The settings container
+	 * The FQCN of the luminance sporce class to use in the reader (GD or Imagick)
 	 *
-	 * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
-	 */
-	protected SettingsContainerInterface $options;
-
-	/**
-	 * The selected data interface (Number, AlphaNum, Kanji, Byte)
+	 * @see \chillerlan\QRCode\Decoder\LuminanceSourceInterface
 	 */
-	protected QRData $dataInterface;
+	private string $luminanceSourceClass;
 
 	/**
 	 * QRCode constructor.
@@ -107,7 +115,10 @@ class QRCode{
 	 * 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));
+	}
+
 }

+ 0 - 99
src/QRCodeReader.php

@@ -1,99 +0,0 @@
-<?php
-/**
- * Class QRCodeReader
- *
- * @created      17.01.2021
- * @author       ZXing Authors
- * @author       Smiley <smiley@chillerlan.net>
- * @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);
-	}
-
-}

+ 8 - 7
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());