Procházet zdrojové kódy

:octocat: allow contrast adjustment and grayscaling in the reader

codemasher před 4 roky
rodič
revize
6c34213aca

+ 20 - 10
src/Decoder/GDLuminanceSource.php

@@ -13,9 +13,10 @@
 
 namespace chillerlan\QRCode\Decoder;
 
+use chillerlan\Settings\SettingsContainerInterface;
 use function file_get_contents, get_resource_type, imagecolorat, imagecolorsforindex,
-	imagecreatefromstring, imagesx, imagesy, is_resource;
-use const PHP_MAJOR_VERSION;
+	imagecreatefromstring, imagefilter, imagesx, imagesy, is_resource;
+use const IMG_FILTER_BRIGHTNESS, IMG_FILTER_CONTRAST, IMG_FILTER_GRAYSCALE, PHP_MAJOR_VERSION;
 
 /**
  * This class is used to help decode images from files which arrive as GD Resource
@@ -35,7 +36,7 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
 	 *
 	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
 	 */
-	public function __construct($gdImage){
+	public function __construct($gdImage, SettingsContainerInterface $options = null){
 
 		/** @noinspection PhpFullyQualifiedNameUsageInspection */
 		if(
@@ -45,7 +46,7 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
 			throw new QRCodeDecoderException('Invalid GD image source.');
 		}
 
-		parent::__construct(imagesx($gdImage), imagesy($gdImage));
+		parent::__construct(imagesx($gdImage), imagesy($gdImage), $options);
 
 		$this->gdImage = $gdImage;
 
@@ -56,6 +57,16 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
 	 *
 	 */
 	private function setLuminancePixels():void{
+
+		if($this->options->readerGrayscale){
+			imagefilter($this->gdImage,  IMG_FILTER_GRAYSCALE);
+		}
+
+		if($this->options->readerIncreaseContrast){
+			imagefilter($this->gdImage, IMG_FILTER_BRIGHTNESS, -100);
+			imagefilter($this->gdImage, IMG_FILTER_CONTRAST, -100);
+		}
+
 		for($j = 0; $j < $this->height; $j++){
 			for($i = 0; $i < $this->width; $i++){
 				$argb  = imagecolorat($this->gdImage, $i, $j);
@@ -64,18 +75,17 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
 				$this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
 			}
 		}
+
 	}
 
 	/** @inheritDoc */
-	public static function fromFile(string $path):self{
-		$path = self::checkFile($path);
-
-		return new self(imagecreatefromstring(file_get_contents($path)));
+	public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
+		return new self(imagecreatefromstring(file_get_contents(self::checkFile($path))), $options);
 	}
 
 	/** @inheritDoc */
-	public static function fromBlob(string $blob):self{
-		return new self(imagecreatefromstring($blob));
+	public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
+		return new self(imagecreatefromstring($blob), $options);
 	}
 
 }

+ 20 - 12
src/Decoder/IMagickLuminanceSource.php

@@ -13,6 +13,7 @@
 
 namespace chillerlan\QRCode\Decoder;
 
+use chillerlan\Settings\SettingsContainerInterface;
 use Imagick;
 use function count;
 
@@ -31,8 +32,8 @@ final class IMagickLuminanceSource extends LuminanceSourceAbstract{
 	 *
 	 * @throws \InvalidArgumentException
 	 */
-	public function __construct(Imagick $imagick){
-		parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight());
+	public function __construct(Imagick $imagick, SettingsContainerInterface $options = null){
+		parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight(), $options);
 
 		$this->imagick = $imagick;
 
@@ -43,29 +44,36 @@ final class IMagickLuminanceSource extends LuminanceSourceAbstract{
 	 *
 	 */
 	private function setLuminancePixels():void{
-		$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
-		$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
 
-		$countPixels = count($pixels);
+		if($this->options->readerGrayscale){
+			$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
+		}
+
+		if($this->options->readerIncreaseContrast){
+			for($i = 0; $i < 10; $i++){
+				$this->imagick->contrastImage(0);
+			}
+		}
 
-		for($i = 0; $i < $countPixels; $i += 3){
+		$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
+		$count  = count($pixels);
+
+		for($i = 0; $i < $count; $i += 3){
 			$this->setLuminancePixel($pixels[$i] & 0xff, $pixels[$i + 1] & 0xff, $pixels[$i + 2] & 0xff);
 		}
 	}
 
 	/** @inheritDoc */
-	public static function fromFile(string $path):self{
-		$path = self::checkFile($path);
-
-		return new self(new Imagick($path));
+	public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
+		return new self(new Imagick(self::checkFile($path)), $options);
 	}
 
 	/** @inheritDoc */
-	public static function fromBlob(string $blob):self{
+	public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
 		$im = new Imagick;
 		$im->readImageBlob($blob);
 
-		return new self($im);
+		return new self($im, $options);
 	}
 
 }

+ 9 - 5
src/Decoder/LuminanceSourceAbstract.php

@@ -11,6 +11,8 @@
 
 namespace chillerlan\QRCode\Decoder;
 
+use chillerlan\QRCode\QROptions;
+use chillerlan\Settings\SettingsContainerInterface;
 use function array_slice, array_splice, file_exists, is_file, is_readable, realpath;
 
 /**
@@ -24,6 +26,8 @@ use function array_slice, array_splice, file_exists, is_file, is_readable, realp
  */
 abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
 
+	/** @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface */
+	protected SettingsContainerInterface $options;
 	protected array $luminances;
 	protected int   $width;
 	protected int   $height;
@@ -31,11 +35,11 @@ abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
 	/**
 	 *
 	 */
-	public function __construct(int $width, int $height){
-		$this->width  = $width;
-		$this->height = $height;
-		// 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.
+	public function __construct(int $width, int $height, SettingsContainerInterface $options = null){
+		$this->width   = $width;
+		$this->height  = $height;
+		$this->options = $options ?? new QROptions;
+
 		$this->luminances = [];
 	}
 

+ 7 - 1
src/Decoder/LuminanceSourceInterface.php

@@ -48,8 +48,14 @@ interface LuminanceSourceInterface{
 	 */
 	public function getRow(int $y):array;
 
-
+	/**
+	 * Creates a LuminanceSource instance from the given file
+	 */
 	public static function fromFile(string $path):self;
+
+	/**
+	 * Creates a LuminanceSource instance from the given data blob
+	 */
 	public static function fromBlob(string $blob):self;
 
 }

+ 2 - 2
src/QRCode.php

@@ -340,14 +340,14 @@ class QRCode{
 	 * Reads a QR Code from a given file
 	 */
 	public function readFromFile(string $path):DecoderResult{
-		return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromFile($path));
+		return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromFile($path, $this->options));
 	}
 
 	/**
 	 * Reads a QR Code from the given data blob
 	 */
 	public function readFromBlob(string $blob):DecoderResult{
-		return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromBlob($blob));
+		return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromBlob($blob, $this->options));
 	}
 
 	/**

+ 3 - 1
src/QROptions.php

@@ -53,7 +53,9 @@ use chillerlan\Settings\SettingsContainerAbstract;
  * @property string|null $imagickBG
  * @property string      $fpdfMeasureUnit
  * @property array|null  $moduleValues
- * @property bool        $useImagickIfAvailable
+ * @property bool        $readerUseImagickIfAvailable
+ * @property bool        $readerGrayscale
+ * @property bool        $readerIncreaseContrast
  */
 class QROptions extends SettingsContainerAbstract{
 	use QROptionsTrait;

+ 15 - 3
src/QROptionsTrait.php

@@ -270,7 +270,19 @@ trait QROptionsTrait{
 	/**
 	 * use Imaagick (if available) when reading QR Codes
 	 */
-	protected bool $useImagickIfAvailable = false;
+	protected bool $readerUseImagickIfAvailable = false;
+
+	/**
+	 * grayscale the image before reading
+	 */
+	protected bool $readerGrayscale = false;
+
+	/**
+	 * increase the contrast before reading
+	 *
+	 * note that applying contrast works different in GD and Imagick, so mileage may vary
+	 */
+	protected bool $readerIncreaseContrast = false;
 
 	/**
 	 * clamp min/max version number
@@ -385,7 +397,7 @@ trait QROptionsTrait{
 	 * enables Imagick for the QR Code reader if the extension is available
 	 */
 	protected function set_useImagickIfAvailable(bool $useImagickIfAvailable):void{
-		$this->useImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick');
+		$this->readerUseImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick');
 	}
 
 	/**
@@ -395,7 +407,7 @@ trait QROptionsTrait{
 	 */
 	public function getLuminanceSourceFQCN():string{
 		// i still hate this
-		return $this->useImagickIfAvailable
+		return $this->readerUseImagickIfAvailable
 			? IMagickLuminanceSource::class
 			: GDLuminanceSource::class;
 	}

+ 32 - 20
tests/QRCodeReaderTest.php

@@ -12,6 +12,7 @@
 
 namespace chillerlan\QRCodeTest;
 
+use chillerlan\Settings\SettingsContainerInterface;
 use Exception, Generator;
 use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
 use chillerlan\QRCode\{QRCode, QROptions};
@@ -34,6 +35,12 @@ class QRCodeReaderTest extends TestCase{
 		.'They say everything looks better with odd numbers of things. But sometimes I put even numbers—just '
 		.'to upset the critics. We\'ll lay all these little funky little things in there. ';
 
+	protected SettingsContainerInterface $options;
+
+	protected function setUp():void{
+		$this->options = new QROptions;
+	}
+
 	public function qrCodeProvider():array{
 		return [
 			'helloworld' => ['hello_world.png', 'Hello world!'],
@@ -51,6 +58,7 @@ class QRCodeReaderTest extends TestCase{
 			'tilted'     => ['tilted.png', 'Hello world!'], // tilted 22° CCW
 			'rotated'    => ['rotated.png', 'Hello world!'], // rotated 90° CW
 			'gradient'   => ['example_svg.png', 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'], // color gradient (from svg example)
+			'dots'       => ['example_svg_dots.png', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'], // color gradient (from svg example)
 		];
 	}
 
@@ -58,7 +66,11 @@ class QRCodeReaderTest extends TestCase{
 	 * @dataProvider qrCodeProvider
 	 */
 	public function testReaderGD(string $img, string $expected):void{
-		$this::assertSame($expected, (string)(new QRCode)->readFromSource(GDLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img)));
+		$this->options->readerGrayscale        = true;
+		$this->options->readerIncreaseContrast = true;
+
+		$this::assertSame($expected, (string)(new QRCode)
+			->readFromSource(GDLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img, $this->options)));
 	}
 
 	/**
@@ -70,26 +82,28 @@ class QRCodeReaderTest extends TestCase{
 			$this::markTestSkipped('imagick not installed');
 		}
 
-		// Y THO?? https://github.com/chillerlan/php-qrcode/runs/4270411373
-		// "could not find enough finder patterns"
-		if($img === 'example_svg.png' && PHP_OS_FAMILY === 'Windows' && PHP_VERSION_ID < 80100){
-			$this::markTestSkipped('random gradient example issue??');
+		$this->options->readerGrayscale        = true;
+		$this->options->readerIncreaseContrast = true;
+
+		if($img === 'damaged.png'){
+			// for some reason that don't work for the damaged example, GD does a better job here
+			$this->options->readerIncreaseContrast = false;
 		}
 
-		$this::assertSame($expected, (string)(new QRCode)->readFromSource(IMagickLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img)));
+		$this::assertSame($expected, (string)(new QRCode)
+			->readFromSource(IMagickLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img, $this->options)));
 	}
 
 	public function testReaderMultiSegment():void{
-		$options = new QROptions;
-		$options->outputType  = QRCode::OUTPUT_IMAGE_PNG;
-		$options->imageBase64 = false;
+		$this->options->outputType  = QRCode::OUTPUT_IMAGE_PNG;
+		$this->options->imageBase64 = false;
 
 		$numeric  = '123456789012345678901234567890';
 		$alphanum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:';
 		$kanji    = '茗荷茗荷茗荷茗荷';
 		$byte     = 'https://smiley.codes/qrcode/';
 
-		$qrcode = (new QRCode($options))
+		$qrcode = (new QRCode($this->options))
 			->addNumericSegment($numeric)
 			->addAlphaNumSegment($alphanum)
 			->addKanjiSegment($kanji)
@@ -119,22 +133,20 @@ class QRCodeReaderTest extends TestCase{
 	 * @dataProvider dataTestProvider
 	 */
 	public function testReadData(Version $version, EccLevel $ecc, string $expected):void{
-		$options = new QROptions;
-
-		$options->outputType            = QRCode::OUTPUT_IMAGE_PNG;
-#		$options->imageTransparent      = false;
-		$options->eccLevel              = $ecc->getLevel();
-		$options->version               = $version->getVersionNumber();
-		$options->imageBase64           = false;
-		$options->useImagickIfAvailable = true;
+		$this->options->outputType                  = QRCode::OUTPUT_IMAGE_PNG;
+#		$this->options->imageTransparent            = false;
+		$this->options->eccLevel                    = $ecc->getLevel();
+		$this->options->version                     = $version->getVersionNumber();
+		$this->options->imageBase64                 = false;
+		$this->options->readerUseImagickIfAvailable = true;
 		// what's interesting is that a smaller scale seems to produce fewer reader errors???
 		// usually from version 20 up, independend of the luminance source
 		// scale 1-2 produces none, scale 3: 1 error, scale 4: 6 errors, scale 5: 5 errors, scale 10: 10 errors
 		// @see \chillerlan\QRCode\Detector\GridSampler::checkAndNudgePoints()
-		$options->scale                 = 2;
+		$this->options->scale                       = 2;
 
 		try{
-			$qrcode    = new QRCode($options);
+			$qrcode    = new QRCode($this->options);
 			$imagedata = $qrcode->render($expected);
 			$result    = $qrcode->readFromBlob($imagedata);
 		}

binární
tests/qrcodes/example_svg_dots.png